优化新增耳机流程,优化界面
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 761 KiB |
+68
-175
@@ -1,5 +1,5 @@
|
||||
// @unocss-include
|
||||
import { getColorPalette, getRgb } from '@sa/color';
|
||||
import { getRgb } from '@sa/color';
|
||||
import { DARK_CLASS } from '@/constants/app';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { toggleHtmlClass } from '@/utils/common';
|
||||
@@ -8,47 +8,33 @@ import { $t } from '@/locales';
|
||||
export function setupLoading() {
|
||||
const themeColor = localStg.get('themeColor') || '#646cff';
|
||||
const darkMode = localStg.get('darkMode') || false;
|
||||
const palette = getColorPalette(themeColor);
|
||||
|
||||
const { r, g, b } = getRgb(themeColor);
|
||||
|
||||
const primaryColor = `--primary-color: ${r} ${g} ${b}`;
|
||||
|
||||
const svgCssVars = Array.from(palette.entries())
|
||||
.map(([key, value]) => `--logo-color-${key}: ${value}`)
|
||||
.join(';');
|
||||
|
||||
const cssVars = `${primaryColor}; ${svgCssVars}`;
|
||||
const cssVars = `--primary-color: ${r} ${g} ${b}`;
|
||||
|
||||
if (darkMode) {
|
||||
toggleHtmlClass(DARK_CLASS).add();
|
||||
}
|
||||
|
||||
const loadingClasses = [
|
||||
'left-0 top-0',
|
||||
'left-0 bottom-0 animate-delay-500',
|
||||
'right-0 top-0 animate-delay-1000',
|
||||
'right-0 bottom-0 animate-delay-1500'
|
||||
];
|
||||
|
||||
const dot = loadingClasses
|
||||
.map(item => {
|
||||
return `<div class="absolute w-16px h-16px bg-primary rounded-8px animate-pulse ${item}"></div>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const loading = `
|
||||
<div class="fixed-center flex-col bg-layout" style="${cssVars}">
|
||||
<div class="w-128px h-128px">
|
||||
<div class="loading-logo-wrapper">
|
||||
${getLogoSvg()}
|
||||
</div>
|
||||
<div class="w-56px h-56px my-36px">
|
||||
<div class="relative h-full animate-spin">
|
||||
${dot}
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-28px font-500 text-primary">${$t('system.title')}</h2>
|
||||
</div>`;
|
||||
</div>
|
||||
<style>
|
||||
.loading-logo-wrapper {
|
||||
width: 220px;
|
||||
color: rgb(var(--primary-color));
|
||||
animation: loading-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes loading-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
</style>`;
|
||||
|
||||
const app = document.getElementById('app');
|
||||
|
||||
@@ -59,152 +45,59 @@ export function setupLoading() {
|
||||
|
||||
function getLogoSvg() {
|
||||
const logoSvg = `<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
version="1.1"
|
||||
viewBox="0 0 1000 1000"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
|
||||
fill="url(#LinearGradient)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
|
||||
fill="url(#LinearGradient_2)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
|
||||
fill="url(#LinearGradient_3)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
|
||||
fill="url(#LinearGradient_4)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
|
||||
fill="url(#LinearGradient_5)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
|
||||
fill="url(#LinearGradient_6)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
|
||||
fill="url(#LinearGradient_7)"
|
||||
fill-rule="nonzero"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="LinearGradient"
|
||||
gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-700)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-600)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_2"
|
||||
gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-300)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-500)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_3"
|
||||
gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-600)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-700)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_4"
|
||||
gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-400)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-600)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_5"
|
||||
gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-500)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-300)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_6"
|
||||
gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-300)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-500)" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="LinearGradient_7"
|
||||
gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop offset="0" stop-color="var(--logo-color-700)" />
|
||||
<stop offset="1" stop-color="var(--logo-color-600)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 130 45"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="lp-clip-1"><path d="M5.31,4.34V20.78a8.28,8.28,0,0,0,1.12,4.29A8.66,8.66,0,0,0,9.52,28.2a8.35,8.35,0,0,0,4.27,1.16H24v-3.9H15.74a4.38,4.38,0,0,1-3.27-1.37,4.52,4.52,0,0,1-1.36-3.31V4.34Z"/></clipPath>
|
||||
<clipPath id="lp-clip-2"><polygon points="62.05 10.06 57.87 15.63 53.66 10.06 47.5 10.06 54.78 19.71 47.5 29.39 53.66 29.39 57.87 23.82 62.05 29.39 68.21 29.39 60.94 19.71 68.21 10.06 62.05 10.06"/></clipPath>
|
||||
<clipPath id="lp-clip-3"><path d="M75.25,10.06a5.39,5.39,0,0,0-2.88.77A5.9,5.9,0,0,0,70.28,13a5.64,5.64,0,0,0-.77,2.92,5.39,5.39,0,0,0,.77,2.88,5.67,5.67,0,0,0,5,2.89h6.19a1.83,1.83,0,0,1,1.36.57,1.86,1.86,0,0,1,.56,1.37,1.8,1.8,0,0,1-.56,1.34,1.83,1.83,0,0,1-1.36.57H70.25v3.9H83.36a5.4,5.4,0,0,0,2.88-.78,5.9,5.9,0,0,0,2.1-2.11,5.8,5.8,0,0,0,.76-2.89,5.61,5.61,0,0,0-.76-2.92,5.9,5.9,0,0,0-2.1-2.11,5.52,5.52,0,0,0-2.88-.78H77.17a1.75,1.75,0,0,1-1.33-.56,2,2,0,0,1,0-2.72A1.75,1.75,0,0,1,77.17,14h11v-3.9Z"/></clipPath>
|
||||
<clipPath id="lp-clip-4"><rect x="91.8" y="11.4" width="5.75" height="17.87"/></clipPath>
|
||||
<clipPath id="lp-clip-5"><path d="M93.82,4.55a2.61,2.61,0,0,0-.9.61,2.61,2.61,0,0,0-.61.9,2.83,2.83,0,0,0-.21,1.07,2.8,2.8,0,0,0,.21,1.07,2.56,2.56,0,0,0,.61.91,2.76,2.76,0,0,0,.9.61,2.83,2.83,0,0,0,1.07.21,2.77,2.77,0,0,0,2-.82,2.67,2.67,0,0,0,.6-.91,2.63,2.63,0,0,0,.22-1.07,2.66,2.66,0,0,0-.22-1.07,2.73,2.73,0,0,0-.6-.9A2.65,2.65,0,0,0,96,4.55a2.8,2.8,0,0,0-1.07-.21,2.83,2.83,0,0,0-1.07.21"/></clipPath>
|
||||
<clipPath id="lp-clip-6"><path d="M110.33,9.64a9.41,9.41,0,0,0-9.41,9.4V29.36H106V19.79a4.78,4.78,0,0,1,9.56,0v9.57h5.08V19a9.41,9.41,0,0,0-9.41-9.4Z"/></clipPath>
|
||||
<clipPath id="lp-clip-7"><path d="M40.83,9.64V19.2a4.78,4.78,0,0,1-9.56,0V9.64H26.2V20a9.4,9.4,0,0,0,9.4,9.41h.9A9.41,9.41,0,0,0,45.91,20V9.64Z"/></clipPath>
|
||||
<clipPath id="lp-clip-8"><path d="M28.43,40a2.44,2.44,0,0,1-.95-1,3.22,3.22,0,0,1-.35-1.55,3.13,3.13,0,0,1,.35-1.54,2.33,2.33,0,0,1,.95-1,2.71,2.71,0,0,1,1.37-.35,2.56,2.56,0,0,1,1.34.36,2.43,2.43,0,0,1,1,1,3.41,3.41,0,0,1,0,3,2.51,2.51,0,0,1-1,1,2.65,2.65,0,0,1-1.34.35A2.71,2.71,0,0,1,28.43,40m-.57-5.79a3.1,3.1,0,0,0-1.22,1.29,4.52,4.52,0,0,0,0,3.89,3.19,3.19,0,0,0,1.22,1.3,3.35,3.35,0,0,0,1.76.46,3.15,3.15,0,0,0,1.76-.48,2.84,2.84,0,0,0,1.09-1.22V41h.92V33.82h-.92v1.57a2.78,2.78,0,0,0-1.08-1.21,3.23,3.23,0,0,0-1.76-.47,3.48,3.48,0,0,0-1.77.45"/></clipPath>
|
||||
<clipPath id="lp-clip-9"><path d="M49.56,33.82v3.9a2.68,2.68,0,0,1-.62,1.93,2.15,2.15,0,0,1-1.65.66,2.1,2.1,0,0,1-1.6-.61,2.53,2.53,0,0,1-.58-1.8V33.82H44.2V38A3.09,3.09,0,0,0,45,40.32a2.91,2.91,0,0,0,2.11.8,3,3,0,0,0,1.47-.36,2.2,2.2,0,0,0,1-1V41h.92V33.82Z"/></clipPath>
|
||||
<clipPath id="lp-clip-10"><path d="M63.17,40a2.44,2.44,0,0,1-1-1,3.34,3.34,0,0,1-.35-1.55,3.24,3.24,0,0,1,.35-1.54,2.33,2.33,0,0,1,1-1,2.74,2.74,0,0,1,1.37-.35,2.62,2.62,0,0,1,1.35.36,2.4,2.4,0,0,1,1,1,3.41,3.41,0,0,1,0,3,2.49,2.49,0,0,1-1,1,2.82,2.82,0,0,1-2.72,0m4-8.69v4.11a2.8,2.8,0,0,0-1.12-1.2,3.17,3.17,0,0,0-1.71-.46,3.47,3.47,0,0,0-1.76.45,3.06,3.06,0,0,0-1.23,1.29,4.05,4.05,0,0,0-.45,1.94,4.18,4.18,0,0,0,.44,2,3.19,3.19,0,0,0,1.22,1.3,3.39,3.39,0,0,0,1.77.46,3.18,3.18,0,0,0,1.76-.48,2.77,2.77,0,0,0,1.08-1.23V41h.92V31.26Z"/></clipPath>
|
||||
<clipPath id="lp-clip-11"><path d="M79,41h.92V33.82H79Zm0-9.7a.62.62,0,0,0-.19.48.65.65,0,0,0,.19.48.62.62,0,0,0,.48.2.61.61,0,0,0,.47-.2.66.66,0,0,0,.2-.48.63.63,0,0,0-.2-.48.64.64,0,0,0-.47-.19.65.65,0,0,0-.48.19"/></clipPath>
|
||||
<clipPath id="lp-clip-12"><path d="M92.62,40a2.42,2.42,0,0,1-1-1,3.29,3.29,0,0,1-.36-1.6,3.24,3.24,0,0,1,.36-1.6,2.41,2.41,0,0,1,1-1,2.86,2.86,0,0,1,2.63,0,2.41,2.41,0,0,1,1,1,3.13,3.13,0,0,1,.37,1.6,3.18,3.18,0,0,1-.37,1.6,2.53,2.53,0,0,1-1,1,2.83,2.83,0,0,1-1.32.32A2.79,2.79,0,0,1,92.62,40m-.49-5.82a3.33,3.33,0,0,0-1.29,1.29,4.34,4.34,0,0,0,0,3.91,3.24,3.24,0,0,0,1.28,1.29,3.69,3.69,0,0,0,1.82.45,3.77,3.77,0,0,0,1.84-.45,3.19,3.19,0,0,0,1.3-1.3,3.85,3.85,0,0,0,.48-1.94,4,4,0,0,0-.47-2,3.2,3.2,0,0,0-1.29-1.29A3.76,3.76,0,0,0,94,33.71a3.7,3.7,0,0,0-1.83.45"/></clipPath>
|
||||
<clipPath id="lp-clip-13"><path d="M121,7.13A2.88,2.88,0,1,1,123.89,10,2.88,2.88,0,0,1,121,7.13m-.37,0a3.25,3.25,0,1,0,3.25-3.25,3.25,3.25,0,0,0-3.25,3.25"/></clipPath>
|
||||
<clipPath id="lp-clip-14"><path d="M123.2,5.7h.45c.44,0,.66.19.66.56a.58.58,0,0,1-.19.45.66.66,0,0,1-.49.18h-.43ZM122.32,5V9.15h.88V7.56h.27a.48.48,0,0,1,.27.09,1.86,1.86,0,0,1,.32.43l.65,1.07h1l-.77-1.22a1.54,1.54,0,0,0-.63-.63h0a1.32,1.32,0,0,0,.68-.42,1.12,1.12,0,0,0,.24-.72c0-.74-.49-1.11-1.46-1.11Z"/></clipPath>
|
||||
</defs>
|
||||
<g opacity="0.9">
|
||||
<path d="M5.31,4.34V20.78a8.28,8.28,0,0,0,1.12,4.29A8.66,8.66,0,0,0,9.52,28.2a8.35,8.35,0,0,0,4.27,1.16H24v-3.9H15.74a4.38,4.38,0,0,1-3.27-1.37,4.52,4.52,0,0,1-1.36-3.31V4.34Z" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-1)"><rect x="-0.84" y="1.44" width="31.01" height="30.82" fill="currentColor" transform="translate(-7.65 15.78) rotate(-46.22)"/></g>
|
||||
<polygon points="62.05 10.06 57.87 15.63 53.66 10.06 47.5 10.06 54.78 19.71 47.5 29.39 53.66 29.39 57.87 23.82 62.05 29.39 68.21 29.39 60.94 19.71 68.21 10.06 62.05 10.06" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-2)"><rect x="43.71" y="5.56" width="28.29" height="28.33" fill="currentColor" transform="translate(3.59 47.85) rotate(-46.22)"/></g>
|
||||
<path d="M75.25,10.06a5.39,5.39,0,0,0-2.88.77A5.9,5.9,0,0,0,70.28,13a5.64,5.64,0,0,0-.77,2.92,5.39,5.39,0,0,0,.77,2.88,5.67,5.67,0,0,0,5,2.89h6.19a1.83,1.83,0,0,1,1.36.57,1.86,1.86,0,0,1,.56,1.37,1.8,1.8,0,0,1-.56,1.34,1.83,1.83,0,0,1-1.36.57H70.25v3.9H83.36a5.4,5.4,0,0,0,2.88-.78,5.9,5.9,0,0,0,2.1-2.11,5.8,5.8,0,0,0,.76-2.89,5.61,5.61,0,0,0-.76-2.92,5.9,5.9,0,0,0-2.1-2.11,5.52,5.52,0,0,0-2.88-.78H77.17a1.75,1.75,0,0,1-1.33-.56,2,2,0,0,1,0-2.72A1.75,1.75,0,0,1,77.17,14h11v-3.9Z" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-3)"><rect x="65.55" y="5.96" width="27.51" height="27.52" fill="currentColor" transform="translate(10.2 63.34) rotate(-46.22)"/></g>
|
||||
<rect x="91.8" y="11.4" width="5.75" height="17.87" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-4)"><rect x="86.24" y="12.08" width="16.88" height="16.51" fill="currentColor" transform="translate(14.49 74.63) rotate(-46.22)"/></g>
|
||||
<path d="M93.82,4.55a2.61,2.61,0,0,0-.9.61,2.61,2.61,0,0,0-.61.9,2.83,2.83,0,0,0-.21,1.07,2.8,2.8,0,0,0,.21,1.07,2.56,2.56,0,0,0,.61.91,2.76,2.76,0,0,0,.9.61,2.83,2.83,0,0,0,1.07.21,2.77,2.77,0,0,0,2-.82,2.67,2.67,0,0,0,.6-.91,2.63,2.63,0,0,0,.22-1.07,2.66,2.66,0,0,0-.22-1.07,2.73,2.73,0,0,0-.6-.9A2.65,2.65,0,0,0,96,4.55a2.8,2.8,0,0,0-1.07-.21,2.83,2.83,0,0,0-1.07.21" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-5)"><rect x="90.94" y="3.18" width="7.91" height="7.91" fill="currentColor" transform="translate(24.09 70.72) rotate(-46.22)"/></g>
|
||||
<path d="M110.33,9.64a9.41,9.41,0,0,0-9.41,9.4V29.36H106V19.79a4.78,4.78,0,0,1,9.56,0v9.57h5.08V19a9.41,9.41,0,0,0-9.41-9.4Z" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-6)"><rect x="96.84" y="5.56" width="27.88" height="27.88" fill="currentColor" transform="translate(20.06 86) rotate(-46.22)"/></g>
|
||||
<path d="M40.83,9.64V19.2a4.78,4.78,0,0,1-9.56,0V9.64H26.2V20a9.4,9.4,0,0,0,9.4,9.41h.9A9.41,9.41,0,0,0,45.91,20V9.64Z" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-7)"><rect x="22.11" y="5.56" width="27.88" height="27.88" fill="currentColor" transform="translate(-2.97 32.04) rotate(-46.22)"/></g>
|
||||
<path d="M28.43,40a2.44,2.44,0,0,1-.95-1,3.22,3.22,0,0,1-.35-1.55,3.13,3.13,0,0,1,.35-1.54,2.33,2.33,0,0,1,.95-1,2.71,2.71,0,0,1,1.37-.35,2.56,2.56,0,0,1,1.34.36,2.43,2.43,0,0,1,1,1,3.41,3.41,0,0,1,0,3,2.51,2.51,0,0,1-1,1,2.65,2.65,0,0,1-1.34.35A2.71,2.71,0,0,1,28.43,40m-.57-5.79a3.1,3.1,0,0,0-1.22,1.29,4.52,4.52,0,0,0,0,3.89,3.19,3.19,0,0,0,1.22,1.3,3.35,3.35,0,0,0,1.76.46,3.15,3.15,0,0,0,1.76-.48,2.84,2.84,0,0,0,1.09-1.22V41h.92V33.82h-.92v1.57a2.78,2.78,0,0,0-1.08-1.21,3.23,3.23,0,0,0-1.76-.47,3.48,3.48,0,0,0-1.77.45" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-8)"><rect x="24.63" y="32.25" width="10.31" height="10.31" fill="currentColor" transform="translate(-17.83 33.04) rotate(-46.22)"/></g>
|
||||
<path d="M49.56,33.82v3.9a2.68,2.68,0,0,1-.62,1.93,2.15,2.15,0,0,1-1.65.66,2.1,2.1,0,0,1-1.6-.61,2.53,2.53,0,0,1-.58-1.8V33.82H44.2V38A3.09,3.09,0,0,0,45,40.32a2.91,2.91,0,0,0,2.11.8,3,3,0,0,0,1.47-.36,2.2,2.2,0,0,0,1-1V41h.92V33.82Z" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-9)"><rect x="42.53" y="32.67" width="9.62" height="9.59" fill="currentColor" transform="translate(-12.46 45.73) rotate(-46.22)"/></g>
|
||||
<path d="M63.17,40a2.44,2.44,0,0,1-1-1,3.34,3.34,0,0,1-.35-1.55,3.24,3.24,0,0,1,.35-1.54,2.33,2.33,0,0,1,1-1,2.74,2.74,0,0,1,1.37-.35,2.62,2.62,0,0,1,1.35.36,2.4,2.4,0,0,1,1,1,3.41,3.41,0,0,1,0,3,2.49,2.49,0,0,1-1,1,2.82,2.82,0,0,1-2.72,0m4-8.69v4.11a2.8,2.8,0,0,0-1.12-1.2,3.17,3.17,0,0,0-1.71-.46,3.47,3.47,0,0,0-1.76.45,3.06,3.06,0,0,0-1.23,1.29,4.05,4.05,0,0,0-.45,1.94,4.18,4.18,0,0,0,.44,2,3.19,3.19,0,0,0,1.22,1.3,3.39,3.39,0,0,0,1.77.46,3.18,3.18,0,0,0,1.76-.48,2.77,2.77,0,0,0,1.08-1.23V41h.92V31.26Z" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-10)"><rect x="58.49" y="30.18" width="12.08" height="12" fill="currentColor" transform="translate(-6.24 57.75) rotate(-46.22)"/></g>
|
||||
<path d="M79,41h.92V33.82H79Zm0-9.7a.62.62,0,0,0-.19.48.65.65,0,0,0,.19.48.62.62,0,0,0,.48.2.61.61,0,0,0,.47-.2.66.66,0,0,0,.2-.48.63.63,0,0,0-.2-.48.64.64,0,0,0-.47-.19.65.65,0,0,0-.48.19" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-11)"><rect x="75.43" y="32.14" width="8.07" height="7.81" fill="currentColor" transform="translate(-1.54 68.49) rotate(-46.22)"/></g>
|
||||
<path d="M92.62,40a2.42,2.42,0,0,1-1-1,3.29,3.29,0,0,1-.36-1.6,3.24,3.24,0,0,1,.36-1.6,2.41,2.41,0,0,1,1-1,2.86,2.86,0,0,1,2.63,0,2.41,2.41,0,0,1,1,1,3.13,3.13,0,0,1,.37,1.6,3.18,3.18,0,0,1-.37,1.6,2.53,2.53,0,0,1-1,1,2.83,2.83,0,0,1-1.32.32A2.79,2.79,0,0,1,92.62,40m-.49-5.82a3.33,3.33,0,0,0-1.29,1.29,4.34,4.34,0,0,0,0,3.91,3.24,3.24,0,0,0,1.28,1.29,3.69,3.69,0,0,0,1.82.45,3.77,3.77,0,0,0,1.84-.45,3.19,3.19,0,0,0,1.3-1.3,3.85,3.85,0,0,0,.48-1.94,4,4,0,0,0-.47-2,3.2,3.2,0,0,0-1.29-1.29A3.76,3.76,0,0,0,94,33.71a3.7,3.7,0,0,0-1.83.45" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-12)"><rect x="88.81" y="32.26" width="10.31" height="10.3" fill="currentColor" transform="translate(1.95 79.37) rotate(-46.22)"/></g>
|
||||
<path d="M121,7.13A2.88,2.88,0,1,1,123.89,10,2.88,2.88,0,0,1,121,7.13m-.37,0a3.25,3.25,0,1,0,3.25-3.25,3.25,3.25,0,0,0-3.25,3.25" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-13)"><rect x="119.29" y="2.54" width="9.19" height="9.19" fill="currentColor" transform="translate(33.03 91.65) rotate(-46.22)"/></g>
|
||||
<path d="M123.2,5.7h.45c.44,0,.66.19.66.56a.58.58,0,0,1-.19.45.66.66,0,0,1-.49.18h-.43ZM122.32,5V9.15h.88V7.56h.27a.48.48,0,0,1,.27.09,1.86,1.86,0,0,1,.32.43l.65,1.07h1l-.77-1.22a1.54,1.54,0,0,0-.63-.63h0a1.32,1.32,0,0,0,.68-.42,1.12,1.12,0,0,0,.24-.72c0-.74-.49-1.11-1.46-1.11Z" fill="currentColor"/>
|
||||
<g style="clip-path:url(#lp-clip-14)"><rect x="121.36" y="4.45" width="5.31" height="5.29" fill="currentColor" transform="translate(33.1 91.73) rotate(-46.22)"/></g>
|
||||
</g>
|
||||
</svg>`;
|
||||
|
||||
return logoSvg;
|
||||
}
|
||||
|
||||
@@ -13,3 +13,10 @@ html {
|
||||
overflow-x: hidden;
|
||||
color: rgb(var(--base-text-color));
|
||||
}
|
||||
|
||||
/* vue-json-pretty 深色模式适配:默认悬浮/高亮背景 #e6f7ff 为浅色实色,
|
||||
在深色主题下过于刺眼且导致浅色文字不可读,改为柔和的半透明蓝色 */
|
||||
html.dark .vjs-tree-node.is-highlight,
|
||||
html.dark .vjs-tree-node:hover {
|
||||
background-color: rgba(56, 189, 248, 0.14);
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ defineExpose({ openForEdit, openForAdd });
|
||||
<NInput
|
||||
v-model:value="squigLinkUrl"
|
||||
clearable
|
||||
placeholder="粘贴 squig.link 分享链接,例如 https://theaudiostore.squig.link/?share=..."
|
||||
placeholder="粘贴 squig.link 分享链接或 TXT 频响文件直链"
|
||||
@keyup.enter="handleSquigLinkFetch()"
|
||||
/>
|
||||
<NButton
|
||||
@@ -318,14 +318,16 @@ defineExpose({ openForEdit, openForAdd });
|
||||
</div>
|
||||
|
||||
<NFormItem label="品牌名称" path="brand_name">
|
||||
<NAutoComplete
|
||||
v-model:value="formData.brand_name"
|
||||
:options="brandSelectOptions"
|
||||
clearable
|
||||
placeholder="请输入或选择品牌"
|
||||
/>
|
||||
<div v-if="showBrandNotInDbTip" class="brand-not-in-db-tip">
|
||||
品牌「{{ (formData.brand_name || '').trim() }}」尚未在品牌库中,可继续填写并保存;保存新型号时将自动创建该品牌。
|
||||
<div class="brand-field-wrapper">
|
||||
<NAutoComplete
|
||||
v-model:value="formData.brand_name"
|
||||
:options="brandSelectOptions"
|
||||
clearable
|
||||
placeholder="请输入或选择品牌"
|
||||
/>
|
||||
<div v-if="showBrandNotInDbTip" class="brand-not-in-db-tip">
|
||||
品牌「{{ (formData.brand_name || '').trim() }}」尚未在品牌库中,可继续填写并保存;保存新型号时将自动创建该品牌。
|
||||
</div>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NFormItem label="型号名称" path="name">
|
||||
@@ -407,6 +409,12 @@ defineExpose({ openForEdit, openForAdd });
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.brand-field-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.brand-not-in-db-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -42,31 +42,31 @@ const quickLinks: QuickLink[] = [
|
||||
key: 'headphone_brand',
|
||||
title: '耳机品牌管理',
|
||||
desc: '维护耳机品牌信息',
|
||||
icon: 'mdi:layers-triple'
|
||||
icon: 'mdi:tag-multiple-outline'
|
||||
},
|
||||
{
|
||||
key: 'headphone_model',
|
||||
title: '耳机型号管理',
|
||||
desc: '维护耳机型号与配置',
|
||||
icon: 'mdi:format-list-bulleted'
|
||||
icon: 'mdi:earbuds'
|
||||
},
|
||||
{
|
||||
key: 'upgrade_ota',
|
||||
title: 'OTA 管理',
|
||||
desc: '固件升级包管理',
|
||||
icon: 'mdi:file-document-outline'
|
||||
icon: 'mdi:cellphone-arrow-down'
|
||||
},
|
||||
{
|
||||
key: 'share-code_log',
|
||||
title: '分享日志',
|
||||
desc: '查看分享码使用记录',
|
||||
icon: 'mdi:share-variant'
|
||||
icon: 'mdi:file-document-outline'
|
||||
},
|
||||
{
|
||||
key: 'toolbox_luxsin-controller',
|
||||
title: 'Luxsin 控制器',
|
||||
desc: '局域网设备数据同步',
|
||||
icon: 'mdi:tools'
|
||||
icon: 'mdi:tune-vertical'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed, onBeforeUnmount } from 'vue';
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||
|
||||
defineOptions({ name: 'ToolboxLuxsinController' });
|
||||
|
||||
@@ -10,10 +11,37 @@ const deviceIp = ref('');
|
||||
const syncDataLoading = ref(false);
|
||||
const syncPeqLoading = ref(false);
|
||||
const rawResponse = ref<string | null>(null);
|
||||
const rawExpanded = ref(false);
|
||||
const decodedData = ref<string | null>(null);
|
||||
const decodeError = ref('');
|
||||
|
||||
// ── IP 历史记录 ────────────────────────────────────
|
||||
const IP_HISTORY_KEY = 'luxsin-controller:ip-history';
|
||||
const MAX_IP_HISTORY = 10;
|
||||
const ipHistory = ref<string[]>(loadIpHistory());
|
||||
|
||||
function loadIpHistory(): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(IP_HISTORY_KEY);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveIpHistory(ip: string) {
|
||||
const list = ipHistory.value.filter(i => i !== ip);
|
||||
list.unshift(ip);
|
||||
if (list.length > MAX_IP_HISTORY) list.length = MAX_IP_HISTORY;
|
||||
ipHistory.value = list;
|
||||
localStorage.setItem(IP_HISTORY_KEY, JSON.stringify(list));
|
||||
}
|
||||
|
||||
const ipOptions = computed(() =>
|
||||
ipHistory.value
|
||||
.filter(ip => !deviceIp.value || ip.includes(deviceIp.value))
|
||||
.map(ip => ({ label: ip, value: ip }))
|
||||
);
|
||||
|
||||
function decodeCustomBase64(input: string) {
|
||||
const map: Record<string, string> = {};
|
||||
for (let i = 0; i < 64; i++) {
|
||||
@@ -53,6 +81,7 @@ async function doFetch(action: 'syncData' | 'syncPeq', loadingRef: { value: bool
|
||||
if (!res.ok) {
|
||||
throw new Error(`设备返回 HTTP ${res.status}`);
|
||||
}
|
||||
saveIpHistory(ip);
|
||||
const raw = await res.text();
|
||||
rawResponse.value = raw;
|
||||
|
||||
@@ -82,6 +111,224 @@ function handleSyncData() {
|
||||
function handleSyncPeq() {
|
||||
doFetch('syncPeq', syncPeqLoading);
|
||||
}
|
||||
|
||||
function handleCopyJson() {
|
||||
if (!decodedData.value) return;
|
||||
navigator.clipboard.writeText(decodedData.value).then(() => {
|
||||
window.$message?.success('JSON 已复制到剪贴板');
|
||||
});
|
||||
}
|
||||
|
||||
// ── 定时侦测 ────────────────────────────────────────
|
||||
type MonitorAction = 'syncData' | 'syncPeq';
|
||||
|
||||
const monitorAction = ref<MonitorAction>('syncData');
|
||||
const monitorInterval = ref(5);
|
||||
const monitoring = ref(false);
|
||||
const countdown = ref(5);
|
||||
const calling = ref(false);
|
||||
const countdownTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
const changeLog = ref<{ time: string; path: string; oldVal: string; newVal: string }[]>([]);
|
||||
const monitorFilter = ref('');
|
||||
const monitorInclude = ref('');
|
||||
|
||||
const excludeSelected = computed({
|
||||
get: () => monitorFilter.value ? monitorFilter.value.split(',').map(s => s.trim()).filter(Boolean) : [],
|
||||
set: (val: string[]) => { monitorFilter.value = val.join(','); },
|
||||
});
|
||||
|
||||
const includeSelected = computed({
|
||||
get: () => monitorInclude.value ? monitorInclude.value.split(',').map(s => s.trim()).filter(Boolean) : [],
|
||||
set: (val: string[]) => { monitorInclude.value = val.join(','); },
|
||||
});
|
||||
let previousSnapshot: Record<string, unknown> | null = null;
|
||||
let noChangeCount = 0;
|
||||
|
||||
const MAX_LOG_ENTRIES = 100;
|
||||
const MAX_IDLE_TICKS = 20;
|
||||
|
||||
const MONITOR_OPTIONS = [
|
||||
{ label: '同步数据 (syncData)', value: 'syncData' as MonitorAction },
|
||||
{ label: '同步 PEQ (syncPeq)', value: 'syncPeq' as MonitorAction },
|
||||
];
|
||||
|
||||
const INTERVAL_OPTIONS = [
|
||||
{ label: '5s', value: 5 },
|
||||
{ label: '10s', value: 10 },
|
||||
{ label: '30s', value: 30 },
|
||||
{ label: '1min', value: 60 },
|
||||
];
|
||||
|
||||
const COMMON_FIELDS = [
|
||||
'language', 'volume', 'input', 'output', 'version', 'screenLight', 'vu', 'vu_count',
|
||||
'sleep', 'screenOff', 'buttonLight', 'buttonShort', 'device', 'audioFormat', 'autoHome',
|
||||
'knob_breathlight', 'vuSensor', 'hdmimutepolar', 'hdmiType', 'pcm', 'dreMode',
|
||||
'dacGain', 'dacArc', 'balance', 'xlr', 'soundStep', 'bootSound', 'dsp_enable',
|
||||
'peqSelect', 'peqEnable', 'effect_enable', 'effect_value', 'width_enable', 'width_value',
|
||||
'scene_enable', 'scene_value', 'crossfeed_enable', 'crossfeed_value', 'subwoofer_enable',
|
||||
'subwoofer_value', 'subwoofer_rate', 'subwoofer_mix_type', 'subwoofer_gain',
|
||||
'subwoofer_lpf_enable', 'subwoofer_hpf_enable', 'audio_enable', 'analogGain',
|
||||
'subwoofer_delay_main', 'subwoofer_delay', 'subwoofer_delay_main_r', 'subwoofer_delay_r',
|
||||
'color_enable', 'color_bass_gain', 'color_mid_gain', 'color_treble_gain',
|
||||
'dacVolumeDirect', 'dacImpedance', 'loudness_bass_gain', 'loudness_treble_gain',
|
||||
'loudness_threshold_gain', 'loudness_enable', 'bt_status', 'mac', 'msgCount',
|
||||
].map(f => ({ label: f, value: f }));
|
||||
|
||||
function flattenObject(obj: unknown, prefix = ''): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
||||
result[prefix || '(root)'] = obj;
|
||||
return result;
|
||||
}
|
||||
for (const [key, val] of Object.entries(obj as Record<string, unknown>)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
|
||||
Object.assign(result, flattenObject(val, path));
|
||||
} else if (Array.isArray(val)) {
|
||||
result[path] = JSON.stringify(val);
|
||||
} else {
|
||||
result[path] = val;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parsePatterns(input: string): string[] {
|
||||
return input
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function matchesPattern(path: string, pattern: string): boolean {
|
||||
if (pattern.endsWith('.*')) {
|
||||
return path.startsWith(pattern.slice(0, -1)) || path === pattern.slice(0, -2);
|
||||
}
|
||||
return path === pattern || path.startsWith(pattern + '.');
|
||||
}
|
||||
|
||||
function shouldTrack(path: string, includePatterns: string[], excludePatterns: string[]): boolean {
|
||||
// include has higher priority: if specified, field must match at least one include pattern
|
||||
if (includePatterns.length > 0) {
|
||||
if (!includePatterns.some(p => matchesPattern(path, p))) return false;
|
||||
}
|
||||
// then check exclude
|
||||
if (excludePatterns.some(p => matchesPattern(path, p))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function deepDiff(oldObj: Record<string, unknown>, newObj: Record<string, unknown>) {
|
||||
const oldFlat = flattenObject(oldObj);
|
||||
const newFlat = flattenObject(newObj);
|
||||
const allKeys = new Set([...Object.keys(oldFlat), ...Object.keys(newFlat)]);
|
||||
const diffs: { path: string; oldVal: string; newVal: string }[] = [];
|
||||
const includePatterns = parsePatterns(monitorInclude.value);
|
||||
const excludePatterns = parsePatterns(monitorFilter.value);
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (!shouldTrack(key, includePatterns, excludePatterns)) continue;
|
||||
const oldVal = oldFlat[key];
|
||||
const newVal = newFlat[key];
|
||||
if (String(oldVal) !== String(newVal)) {
|
||||
diffs.push({
|
||||
path: key,
|
||||
oldVal: oldVal === undefined ? '(新增)' : String(oldVal),
|
||||
newVal: newVal === undefined ? '(删除)' : String(newVal),
|
||||
});
|
||||
}
|
||||
}
|
||||
return diffs;
|
||||
}
|
||||
|
||||
async function doMonitorTick() {
|
||||
const ip = deviceIp.value.trim();
|
||||
if (!ip) {
|
||||
stopMonitor();
|
||||
window.$message?.warning('设备 IP 为空,已停止侦测');
|
||||
return;
|
||||
}
|
||||
|
||||
calling.value = true;
|
||||
try {
|
||||
const res = await fetch(`http://${ip}/dev/info.cgi?action=${monitorAction.value}`);
|
||||
if (!res.ok) return;
|
||||
const raw = await res.text();
|
||||
if (!raw) return;
|
||||
saveIpHistory(ip);
|
||||
|
||||
const jsonStr = decodeCustomBase64(raw.trim());
|
||||
const parsed = JSON.parse(jsonStr) as Record<string, unknown>;
|
||||
|
||||
if (previousSnapshot) {
|
||||
const diffs = deepDiff(previousSnapshot, parsed);
|
||||
|
||||
if (diffs.length > 0) {
|
||||
noChangeCount = 0;
|
||||
const now = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||
for (const d of diffs) {
|
||||
changeLog.value.unshift({ time: now, ...d });
|
||||
}
|
||||
// keep max entries
|
||||
if (changeLog.value.length > MAX_LOG_ENTRIES) {
|
||||
changeLog.value.length = MAX_LOG_ENTRIES;
|
||||
}
|
||||
} else {
|
||||
noChangeCount++;
|
||||
if (noChangeCount >= MAX_IDLE_TICKS) {
|
||||
stopMonitor();
|
||||
window.$message?.info(`连续 ${MAX_IDLE_TICKS} 次无变化,已自动停止侦测`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousSnapshot = parsed;
|
||||
} catch {
|
||||
// silent – don't disturb monitoring loop
|
||||
} finally {
|
||||
calling.value = false;
|
||||
countdown.value = monitorInterval.value;
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
countdownTimer.value = setInterval(() => {
|
||||
if (calling.value) return; // skip countdown while fetching
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
doMonitorTick();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function startMonitor() {
|
||||
changeLog.value = [];
|
||||
previousSnapshot = null;
|
||||
noChangeCount = 0;
|
||||
monitoring.value = true;
|
||||
countdown.value = monitorInterval.value;
|
||||
doMonitorTick(); // first tick immediately
|
||||
startCountdown();
|
||||
}
|
||||
|
||||
function stopMonitor() {
|
||||
monitoring.value = false;
|
||||
calling.value = false;
|
||||
countdown.value = monitorInterval.value;
|
||||
noChangeCount = 0;
|
||||
if (countdownTimer.value) {
|
||||
clearInterval(countdownTimer.value);
|
||||
countdownTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearChangeLog() {
|
||||
changeLog.value = [];
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (countdownTimer.value) clearInterval(countdownTimer.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -89,10 +336,11 @@ function handleSyncPeq() {
|
||||
<NCard :bordered="false" class="card-wrapper" size="small">
|
||||
<NForm inline label-placement="left" @submit.prevent="handleSyncData">
|
||||
<NFormItem label="设备 IP">
|
||||
<NInput
|
||||
<NAutoComplete
|
||||
v-model:value="deviceIp"
|
||||
:options="ipOptions"
|
||||
clearable
|
||||
placeholder="例如 192.168.9.125"
|
||||
placeholder="输入 IP 或选择历史记录"
|
||||
class="w-260px"
|
||||
@keyup.enter="handleSyncData"
|
||||
/>
|
||||
@@ -115,28 +363,102 @@ function handleSyncPeq() {
|
||||
</NForm>
|
||||
</NCard>
|
||||
|
||||
<NCard v-if="rawResponse !== null" :bordered="false" class="card-wrapper result-card" size="small">
|
||||
<NCard :bordered="false" class="card-wrapper" size="small">
|
||||
<template #header>
|
||||
<div class="card-header card-header--collapsible" @click="rawExpanded = !rawExpanded">
|
||||
<span class="title text-15px">
|
||||
<SvgIcon
|
||||
icon="mdi:chevron-right"
|
||||
class="collapse-arrow"
|
||||
:class="{ 'is-open': rawExpanded }"
|
||||
/>
|
||||
原始响应(Base64)
|
||||
</span>
|
||||
<NTag size="small">{{ rawResponse.length }} chars</NTag>
|
||||
<div class="card-header">
|
||||
<span class="title text-15px">定时侦测</span>
|
||||
<NTag v-if="monitoring" type="success" size="small">运行中</NTag>
|
||||
</div>
|
||||
</template>
|
||||
<pre v-show="rawExpanded" class="raw-text">{{ rawResponse }}</pre>
|
||||
<div class="monitor-row">
|
||||
<NSelect
|
||||
v-model:value="monitorAction"
|
||||
:options="MONITOR_OPTIONS"
|
||||
:disabled="monitoring"
|
||||
style="width: 200px; flex-shrink: 0"
|
||||
size="small"
|
||||
/>
|
||||
<NSelect
|
||||
v-model:value="monitorInterval"
|
||||
:options="INTERVAL_OPTIONS"
|
||||
:disabled="monitoring"
|
||||
style="width: 80px; flex-shrink: 0"
|
||||
size="small"
|
||||
/>
|
||||
<NButton
|
||||
v-if="!monitoring"
|
||||
type="warning"
|
||||
:disabled="!deviceIp.trim()"
|
||||
style="flex-shrink: 0"
|
||||
@click="startMonitor"
|
||||
>
|
||||
开始侦测
|
||||
</NButton>
|
||||
<NButton v-else type="error" style="flex-shrink: 0" @click="stopMonitor">
|
||||
{{ calling ? '正在调用…' : `下次调用 ${countdown}s` }}
|
||||
<template v-if="!calling" #icon>
|
||||
<span class="countdown-dot" />
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton v-if="changeLog.length" text size="small" style="flex-shrink: 0" @click="clearChangeLog">
|
||||
清空日志
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="monitor-row monitor-filters">
|
||||
<NSelect
|
||||
v-model:value="includeSelected"
|
||||
:options="COMMON_FIELDS"
|
||||
multiple
|
||||
filterable
|
||||
tag
|
||||
clearable
|
||||
placeholder="仅监听字段(可多选或手动输入)"
|
||||
size="small"
|
||||
class="monitor-input"
|
||||
max-tag-count="responsive"
|
||||
/>
|
||||
<NSelect
|
||||
v-model:value="excludeSelected"
|
||||
:options="COMMON_FIELDS"
|
||||
multiple
|
||||
filterable
|
||||
tag
|
||||
clearable
|
||||
placeholder="忽略字段(可多选或手动输入)"
|
||||
size="small"
|
||||
class="monitor-input"
|
||||
max-tag-count="responsive"
|
||||
/>
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<NCard v-if="changeLog.length" :bordered="false" class="card-wrapper result-card" size="small">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title text-15px">变更日志</span>
|
||||
<NTag size="small">{{ changeLog.length }} 条</NTag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="change-log">
|
||||
<div v-for="(entry, idx) in changeLog" :key="idx" class="log-entry">
|
||||
<span class="log-time">{{ entry.time }}</span>
|
||||
<span class="log-path">{{ entry.path }}</span>
|
||||
<span class="log-old">{{ entry.oldVal }}</span>
|
||||
<span class="log-arrow">→</span>
|
||||
<span class="log-new">{{ entry.newVal }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<NCard v-if="decodedData !== null" :bordered="false" class="card-wrapper result-card" size="small">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title text-15px">解码结果(JSON)</span>
|
||||
<NTag type="success" size="small">decoded</NTag>
|
||||
<NButton text size="small" class="copy-btn" @click="handleCopyJson">
|
||||
<template #icon>
|
||||
<SvgIcon icon="mdi:content-copy" />
|
||||
</template>
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="json-output">{{ decodedData }}</pre>
|
||||
@@ -155,6 +477,21 @@ function handleSyncPeq() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.monitor-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.monitor-filters {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.monitor-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.result-card :deep(.n-card__content) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -166,27 +503,15 @@ function handleSyncPeq() {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header--collapsible {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
.copy-btn {
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.card-header--collapsible:hover .title {
|
||||
color: var(--n-color-target);
|
||||
.copy-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.collapse-arrow {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.collapse-arrow.is-open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.raw-text,
|
||||
.json-output,
|
||||
.error-text {
|
||||
margin: 0;
|
||||
@@ -200,18 +525,90 @@ function handleSyncPeq() {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.raw-text {
|
||||
color: #94a3b8;
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
.json-output {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.json-output {
|
||||
.error-text {
|
||||
color: #e11d48;
|
||||
background: rgba(251, 113, 133, 0.06);
|
||||
}
|
||||
|
||||
:global(html.dark) .json-output {
|
||||
color: #cbd5e1;
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
:global(html.dark) .error-text {
|
||||
color: #fb7185;
|
||||
background: rgba(251, 113, 133, 0.06);
|
||||
}
|
||||
|
||||
.change-log {
|
||||
padding: 12px 20px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-path {
|
||||
color: #6366f1;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-old {
|
||||
color: #ef4444;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.log-arrow {
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-new {
|
||||
color: #22c55e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(html.dark) .log-entry {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
:global(html.dark) .log-path {
|
||||
color: #818cf8;
|
||||
}
|
||||
|
||||
.countdown-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user