官网开发 0810
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Router, type Request, type Response } from 'express';
|
import { Router, type Request, type Response } from 'express';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and, max } from 'drizzle-orm';
|
||||||
import { db } from '../../config/database.js';
|
import { db } from '../../config/database.js';
|
||||||
import { wwwSectionBlocks, wwwSpecGroups, wwwSpecItems } from '../../schemas/index.js';
|
import { wwwSectionBlocks, wwwSpecGroups, wwwSpecItems } from '../../schemas/index.js';
|
||||||
import logger from '../../config/logger.js';
|
import logger from '../../config/logger.js';
|
||||||
@@ -77,6 +77,15 @@ router.post('/api/www/sections', async (req: Request, res: Response) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 未指定排序时,自动追加到同 page_type(及同产品)已有区块的最后,避免默认 0 导致乱序
|
||||||
|
let sortOrder = Number(b.sort_order) || 0;
|
||||||
|
if (b.sort_order === undefined || b.sort_order === null) {
|
||||||
|
const scope = [eq(wwwSectionBlocks.pageType, b.page_type)];
|
||||||
|
if (b.product_id) scope.push(eq(wwwSectionBlocks.productId, b.product_id));
|
||||||
|
const [agg] = await db.select({ max: max(wwwSectionBlocks.sortOrder) }).from(wwwSectionBlocks).where(and(...scope));
|
||||||
|
sortOrder = (agg?.max ?? -1) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await db.insert(wwwSectionBlocks).values({
|
const result = await db.insert(wwwSectionBlocks).values({
|
||||||
pageType: b.page_type,
|
pageType: b.page_type,
|
||||||
productId: b.product_id || null,
|
productId: b.product_id || null,
|
||||||
@@ -108,7 +117,7 @@ router.post('/api/www/sections', async (req: Request, res: Response) => {
|
|||||||
videoUrl: b.video_url || '',
|
videoUrl: b.video_url || '',
|
||||||
config: b.config || null,
|
config: b.config || null,
|
||||||
isVisible: b.is_visible !== undefined ? (b.is_visible ? 1 : 0) : 1,
|
isVisible: b.is_visible !== undefined ? (b.is_visible ? 1 : 0) : 1,
|
||||||
sortOrder: b.sort_order || 0,
|
sortOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newId = result[0].insertId;
|
const newId = result[0].insertId;
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'www',
|
title: 'www',
|
||||||
i18nKey: 'route.www',
|
i18nKey: 'route.www',
|
||||||
icon: 'mdi:globe-model',
|
icon: 'iconoir:www',
|
||||||
order: 7
|
order: 7
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function fetchDuplicateSection(id: number) {
|
|||||||
|
|
||||||
// ===== 参数分组 =====
|
// ===== 参数分组 =====
|
||||||
export function fetchGetSpecGroups(sectionId: number) {
|
export function fetchGetSpecGroups(sectionId: number) {
|
||||||
return request<Api.Www.SpecGroup[]>({ url: `/www/sections/${sectionId}/spec-groups`, method: 'get' });
|
return request<{ items: Api.Www.SpecGroup[]; total: number }>({ url: `/www/sections/${sectionId}/spec-groups`, method: 'get' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchCreateSpecGroup(sectionId: number, data: { title_zh: string; title_en: string; default_visible?: number }) {
|
export function fetchCreateSpecGroup(sectionId: number, data: { title_zh: string; title_en: string; default_visible?: number }) {
|
||||||
@@ -56,7 +56,7 @@ export function fetchSortSpecGroups(sectionId: number, data: Array<{ id: number;
|
|||||||
|
|
||||||
// ===== 参数项 =====
|
// ===== 参数项 =====
|
||||||
export function fetchGetSpecItems(groupId: number) {
|
export function fetchGetSpecItems(groupId: number) {
|
||||||
return request<Api.Www.SpecItem[]>({ url: `/www/spec-groups/${groupId}/items`, method: 'get' });
|
return request<{ items: Api.Www.SpecItem[]; total: number }>({ url: `/www/spec-groups/${groupId}/items`, method: 'get' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchCreateSpecItem(groupId: number, data: { name_zh: string; name_en: string; value_zh: string; value_en: string }) {
|
export function fetchCreateSpecItem(groupId: number, data: { name_zh: string; name_en: string; value_zh: string; value_en: string }) {
|
||||||
|
|||||||
+2
@@ -147,6 +147,8 @@ declare namespace Api {
|
|||||||
title_en: string;
|
title_en: string;
|
||||||
default_visible: number;
|
default_visible: number;
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
|
/** GET spec-groups 列表接口内联返回的参数项 */
|
||||||
|
items?: SpecItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpecItem {
|
interface SpecItem {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { NButton, NSpace, NSwitch, NTag, type DataTableColumns, type FormInst, t
|
|||||||
import ColorInput from '../page/home/components/ColorInput.vue';
|
import ColorInput from '../page/home/components/ColorInput.vue';
|
||||||
import MediaSelectInput from './MediaSelectInput.vue';
|
import MediaSelectInput from './MediaSelectInput.vue';
|
||||||
import RichTextEditor from './RichTextEditor.vue';
|
import RichTextEditor from './RichTextEditor.vue';
|
||||||
|
import SpecManager from './SpecManager.vue';
|
||||||
import {
|
import {
|
||||||
fetchCreateSection,
|
fetchCreateSection,
|
||||||
fetchDeleteSection,
|
fetchDeleteSection,
|
||||||
@@ -110,7 +111,7 @@ const formData = reactive({
|
|||||||
media_url_en: '',
|
media_url_en: '',
|
||||||
video_url: '',
|
video_url: '',
|
||||||
is_visible: true,
|
is_visible: true,
|
||||||
config: { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null } as Record<string, unknown>
|
config: { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, margin_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', split_ratio: 50, mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null } as Record<string, unknown>
|
||||||
});
|
});
|
||||||
|
|
||||||
// 背景图/视频显示模式选项(bg_type 为图片/视频时生效)
|
// 背景图/视频显示模式选项(bg_type 为图片/视频时生效)
|
||||||
@@ -128,6 +129,20 @@ const containerWidthOptions = [
|
|||||||
{ label: '全宽', value: 'full' }
|
{ label: '全宽', value: 'full' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 图文分栏比例选项(值为素材栏宽占比);标签顺序随「媒体位置」联动:
|
||||||
|
// 素材在左 → 「素材 : 文案」,素材在右 → 「文案 : 素材」(与视觉左右顺序一致,底层存值不变)
|
||||||
|
const splitRatioOptions = computed(() => {
|
||||||
|
const mediaRight = formData.config.media_position === 'right';
|
||||||
|
const label = (media: number) => (mediaRight ? `${100 - media} : ${media}` : `${media} : ${100 - media}`);
|
||||||
|
return [
|
||||||
|
{ label: `${label(30)}`, value: 30 },
|
||||||
|
{ label: `${label(40)}`, value: 40 },
|
||||||
|
{ label: `${label(50)}(默认)`, value: 50 },
|
||||||
|
{ label: `${label(60)}`, value: 60 },
|
||||||
|
{ label: `${label(70)}`, value: 70 }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
// ===== 马赛克拼贴(layout=mosaic):固定版式 + 槽位素材,存 config.mosaic_pattern / config.mosaic_items =====
|
// ===== 马赛克拼贴(layout=mosaic):固定版式 + 槽位素材,存 config.mosaic_pattern / config.mosaic_items =====
|
||||||
interface MosaicItemForm {
|
interface MosaicItemForm {
|
||||||
type: 'image' | 'video';
|
type: 'image' | 'video';
|
||||||
@@ -247,11 +262,15 @@ const columns = computed((): DataTableColumns<Api.Www.SectionBlock> => [
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 240,
|
width: 290,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render(row) {
|
render(row) {
|
||||||
return h('div', { class: 'flex-y-center gap-8px' }, [
|
return h('div', { class: 'flex-y-center gap-8px' }, [
|
||||||
h(NButton, { size: 'small', type: 'primary', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
|
h(NButton, { size: 'small', type: 'primary', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
|
||||||
|
// spec_table 布局:管理参数分组与参数项
|
||||||
|
row.layout === 'spec_table'
|
||||||
|
? h(NButton, { size: 'small', type: 'info', onClick: () => handleSpecManage(row) }, { default: () => '参数' })
|
||||||
|
: null,
|
||||||
h(NButton, { size: 'small', onClick: () => handleCopy(row) }, { default: () => '复制' }),
|
h(NButton, { size: 'small', onClick: () => handleCopy(row) }, { default: () => '复制' }),
|
||||||
h(NButton, { size: 'small', type: 'error', onClick: () => handleDelete(row) }, { default: () => '删除' })
|
h(NButton, { size: 'small', type: 'error', onClick: () => handleDelete(row) }, { default: () => '删除' })
|
||||||
]);
|
]);
|
||||||
@@ -315,7 +334,7 @@ function handleAdd() {
|
|||||||
formData.media_url_en = '';
|
formData.media_url_en = '';
|
||||||
formData.video_url = '';
|
formData.video_url = '';
|
||||||
formData.is_visible = true;
|
formData.is_visible = true;
|
||||||
formData.config = { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null };
|
formData.config = { text_position: 'middle_left', width_percent: 100, container_width: '1200', border_radius: 0, padding_y: null, margin_y: null, title_font_size: null, bg_fit: 'cover', media_position: 'left', split_ratio: 50, mosaic_pattern: 'big_3', mosaic_items: emptyMosaicItems(4), min_height: null };
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,9 +375,11 @@ function fillFormFromRow(row: Api.Www.SectionBlock) {
|
|||||||
container_width: ['1400', '1600', 'full'].includes(String(cfg.container_width)) ? String(cfg.container_width) : '1200',
|
container_width: ['1400', '1600', 'full'].includes(String(cfg.container_width)) ? String(cfg.container_width) : '1200',
|
||||||
border_radius: typeof cfg.border_radius === 'number' && cfg.border_radius > 0 ? cfg.border_radius : 0,
|
border_radius: typeof cfg.border_radius === 'number' && cfg.border_radius > 0 ? cfg.border_radius : 0,
|
||||||
padding_y: typeof cfg.padding_y === 'number' && cfg.padding_y >= 0 ? cfg.padding_y : null,
|
padding_y: typeof cfg.padding_y === 'number' && cfg.padding_y >= 0 ? cfg.padding_y : null,
|
||||||
|
margin_y: typeof cfg.margin_y === 'number' && cfg.margin_y >= 0 ? cfg.margin_y : null,
|
||||||
title_font_size: typeof cfg.title_font_size === 'number' && cfg.title_font_size > 0 ? cfg.title_font_size : null,
|
title_font_size: typeof cfg.title_font_size === 'number' && cfg.title_font_size > 0 ? cfg.title_font_size : null,
|
||||||
bg_fit: cfg.bg_fit === 'contain' || cfg.bg_fit === 'auto' ? cfg.bg_fit : 'cover',
|
bg_fit: cfg.bg_fit === 'contain' || cfg.bg_fit === 'auto' ? cfg.bg_fit : 'cover',
|
||||||
media_position: cfg.media_position === 'right' ? 'right' : 'left',
|
media_position: cfg.media_position === 'right' ? 'right' : 'left',
|
||||||
|
split_ratio: [30, 40, 50, 60, 70].includes(Number(cfg.split_ratio)) ? Number(cfg.split_ratio) : 50,
|
||||||
min_height: typeof cfg.min_height === 'number' && cfg.min_height >= 0 ? cfg.min_height : null,
|
min_height: typeof cfg.min_height === 'number' && cfg.min_height >= 0 ? cfg.min_height : null,
|
||||||
mosaic_pattern: ['big_3', 'half_2', 'big_4', 'big_2'].includes(String(cfg.mosaic_pattern)) ? String(cfg.mosaic_pattern) : 'big_3'
|
mosaic_pattern: ['big_3', 'half_2', 'big_4', 'big_2'].includes(String(cfg.mosaic_pattern)) ? String(cfg.mosaic_pattern) : 'big_3'
|
||||||
};
|
};
|
||||||
@@ -404,7 +425,14 @@ function handleDelete(row: Api.Www.SectionBlock) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
// 参数管理(spec_table 布局):唤起 SpecManager
|
||||||
|
const specManagerRef = ref<InstanceType<typeof SpecManager> | null>(null);
|
||||||
|
|
||||||
|
function handleSpecManage(row: Api.Www.SectionBlock) {
|
||||||
|
specManagerRef.value?.open(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(closeAfter = true) {
|
||||||
submitLoading.value = true;
|
submitLoading.value = true;
|
||||||
const { id, ...payload } = formData;
|
const { id, ...payload } = formData;
|
||||||
const reqData = {
|
const reqData = {
|
||||||
@@ -412,13 +440,15 @@ async function handleSubmit() {
|
|||||||
page_type: props.pageType,
|
page_type: props.pageType,
|
||||||
product_id: props.productId || null
|
product_id: props.productId || null
|
||||||
};
|
};
|
||||||
const { error } = id
|
const { data, error } = id
|
||||||
? await fetchUpdateSection(id, reqData)
|
? await fetchUpdateSection(id, reqData)
|
||||||
: await fetchCreateSection(reqData);
|
: await fetchCreateSection(reqData);
|
||||||
submitLoading.value = false;
|
submitLoading.value = false;
|
||||||
if (!error) {
|
if (!error) {
|
||||||
|
// 「应用」新建后回填新 id,后续保存走更新接口,避免重复创建
|
||||||
|
if (!id && data?.id) formData.id = data.id;
|
||||||
window.$message?.success(id ? '更新成功' : '创建成功');
|
window.$message?.success(id ? '更新成功' : '创建成功');
|
||||||
dialogVisible.value = false;
|
if (closeAfter) dialogVisible.value = false;
|
||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,6 +477,8 @@ onMounted(loadData);
|
|||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
class="w-1000px"
|
class="w-1000px"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
|
:segmented="{ content: true, footer: 'soft' }"
|
||||||
|
content-style="max-height: calc(85vh - 130px); overflow-y: auto;"
|
||||||
>
|
>
|
||||||
<NForm ref="formRef" :model="formData" label-placement="left" label-width="120">
|
<NForm ref="formRef" :model="formData" label-placement="left" label-width="120">
|
||||||
<NDivider>基础配置</NDivider>
|
<NDivider>基础配置</NDivider>
|
||||||
@@ -515,6 +547,23 @@ onMounted(loadData);
|
|||||||
<span class="text-12px opacity-60">内容区与区块上下边缘的间距。留空 = 布局默认(多数 96px / 全幅横幅 120px / 主视觉分屏 64px);0 = 完全贴边</span>
|
<span class="text-12px opacity-60">内容区与区块上下边缘的间距。留空 = 布局默认(多数 96px / 全幅横幅 120px / 主视觉分屏 64px);0 = 完全贴边</span>
|
||||||
</div>
|
</div>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
<NFormItem label="上下外边距">
|
||||||
|
<div class="w-full flex flex-col gap-8px">
|
||||||
|
<NInputNumber
|
||||||
|
:value="(formData.config.margin_y as number | null) ?? null"
|
||||||
|
:min="0"
|
||||||
|
:max="240"
|
||||||
|
:step="8"
|
||||||
|
class="w-full"
|
||||||
|
clearable
|
||||||
|
placeholder="留空 = 紧贴相邻区块"
|
||||||
|
@update:value="v => (formData.config.margin_y = v)"
|
||||||
|
>
|
||||||
|
<template #suffix>px</template>
|
||||||
|
</NInputNumber>
|
||||||
|
<span class="text-12px opacity-60">区块与上下相邻区块之间的间隙,露出页面背景色;适合「区块宽度 < 100 + 圆角」的卡片式区块。留空/0 = 紧贴(默认)</span>
|
||||||
|
</div>
|
||||||
|
</NFormItem>
|
||||||
<NFormItem label="标题字号">
|
<NFormItem label="标题字号">
|
||||||
<div class="w-full flex flex-col gap-8px">
|
<div class="w-full flex flex-col gap-8px">
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
@@ -596,10 +645,20 @@ onMounted(loadData);
|
|||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem v-if="formData.layout === 'split_content'" label="媒体位置">
|
<NFormItem v-if="formData.layout === 'split_content'" label="媒体位置">
|
||||||
<NRadioGroup :value="(formData.config.media_position as string) ?? 'left'" @update:value="v => (formData.config.media_position = v)">
|
<NRadioGroup :value="(formData.config.media_position as string) ?? 'left'" @update:value="v => (formData.config.media_position = v)">
|
||||||
<NRadio value="left">图片/视频在左</NRadio>
|
<NRadio value="left">素材|文案</NRadio>
|
||||||
<NRadio value="right">图片/视频在右</NRadio>
|
<NRadio value="right">文案|素材</NRadio>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
<NFormItem v-if="formData.layout === 'split_content'" label="分栏比例">
|
||||||
|
<div class="w-full flex flex-col gap-8px">
|
||||||
|
<NSelect
|
||||||
|
:value="(formData.config.split_ratio as number) ?? 50"
|
||||||
|
:options="splitRatioOptions"
|
||||||
|
@update:value="v => (formData.config.split_ratio = v)"
|
||||||
|
/>
|
||||||
|
<span class="text-12px opacity-60">按页面视觉左右顺序标注({{ formData.config.media_position === 'right' ? '文案 : 素材' : '素材 : 文案' }}),默认五五等分;移动端(小屏)始终单列堆叠不受影响</span>
|
||||||
|
</div>
|
||||||
|
</NFormItem>
|
||||||
<NFormItem v-if="formData.layout === 'mosaic'" label="拼贴版式">
|
<NFormItem v-if="formData.layout === 'mosaic'" label="拼贴版式">
|
||||||
<div class="w-full flex flex-col gap-8px">
|
<div class="w-full flex flex-col gap-8px">
|
||||||
<NRadioGroup :value="(formData.config.mosaic_pattern as string) ?? 'big_3'" @update:value="handleMosaicPatternChange">
|
<NRadioGroup :value="(formData.config.mosaic_pattern as string) ?? 'big_3'" @update:value="handleMosaicPatternChange">
|
||||||
@@ -700,10 +759,14 @@ onMounted(loadData);
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<NSpace justify="end">
|
<NSpace justify="end">
|
||||||
<NButton @click="dialogVisible = false">取消</NButton>
|
<NButton @click="dialogVisible = false">取消</NButton>
|
||||||
<NButton type="primary" :loading="submitLoading" @click="handleSubmit">确定</NButton>
|
<NButton type="success" secondary :loading="submitLoading" @click="handleSubmit(false)">应用</NButton>
|
||||||
|
<NButton type="primary" :loading="submitLoading" @click="handleSubmit()">确定</NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</template>
|
</template>
|
||||||
</NModal>
|
</NModal>
|
||||||
|
|
||||||
|
<!-- 参数规格管理(spec_table 布局专用) -->
|
||||||
|
<SpecManager ref="specManagerRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { NButton, NCollapse, NCollapseItem, NEmpty, NForm, NFormItem, NInput, NInputNumber, NModal, NSpace } from 'naive-ui';
|
||||||
|
import {
|
||||||
|
fetchCreateSpecGroup,
|
||||||
|
fetchCreateSpecItem,
|
||||||
|
fetchDeleteSpecGroup,
|
||||||
|
fetchDeleteSpecItem,
|
||||||
|
fetchGetSpecGroups,
|
||||||
|
fetchSortSpecGroups,
|
||||||
|
fetchSortSpecItems,
|
||||||
|
fetchUpdateSpecGroup,
|
||||||
|
fetchUpdateSpecItem
|
||||||
|
} from '@/service/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数规格管理(spec_table 布局专用)
|
||||||
|
*
|
||||||
|
* 管理区块下的参数分组(title_zh/en、默认显示行数)与参数项(中英名称/值),
|
||||||
|
* 由 SectionManager 操作列的「参数」按钮唤起。
|
||||||
|
*/
|
||||||
|
defineOptions({ name: 'SpecManager' });
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const sectionId = ref<number | null>(null);
|
||||||
|
const sectionTitle = ref('');
|
||||||
|
const groups = ref<Api.Www.SpecGroup[]>([]);
|
||||||
|
|
||||||
|
const expandedNames = ref<number[]>([]);
|
||||||
|
|
||||||
|
async function open(row: Api.Www.SectionBlock) {
|
||||||
|
sectionId.value = row.id;
|
||||||
|
sectionTitle.value = row.title_zh || '参数规格';
|
||||||
|
visible.value = true;
|
||||||
|
await loadData();
|
||||||
|
// 默认展开首个分组
|
||||||
|
expandedNames.value = groups.value.length ? [groups.value[0].id] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
if (!sectionId.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
const { data, error } = await fetchGetSpecGroups(sectionId.value);
|
||||||
|
loading.value = false;
|
||||||
|
if (!error && data) {
|
||||||
|
groups.value = data.items || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
|
||||||
|
// ===== 分组排序 =====
|
||||||
|
async function moveGroup(index: number, direction: -1 | 1) {
|
||||||
|
const swapIndex = index + direction;
|
||||||
|
if (swapIndex < 0 || swapIndex >= groups.value.length || !sectionId.value) return;
|
||||||
|
const newData = [...groups.value];
|
||||||
|
[newData[index], newData[swapIndex]] = [newData[swapIndex], newData[index]];
|
||||||
|
const { error } = await fetchSortSpecGroups(
|
||||||
|
sectionId.value,
|
||||||
|
newData.map((g, i) => ({ id: g.id, sort_order: i }))
|
||||||
|
);
|
||||||
|
if (!error) {
|
||||||
|
groups.value = newData;
|
||||||
|
window.$message?.success('排序已更新');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 分组编辑 =====
|
||||||
|
const groupDialogVisible = ref(false);
|
||||||
|
const groupSubmitting = ref(false);
|
||||||
|
const groupForm = ref<{ id: number | null; title_zh: string; title_en: string; default_visible: number }>({
|
||||||
|
id: null,
|
||||||
|
title_zh: '',
|
||||||
|
title_en: '',
|
||||||
|
default_visible: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleAddGroup() {
|
||||||
|
groupForm.value = { id: null, title_zh: '', title_en: '', default_visible: 5 };
|
||||||
|
groupDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditGroup(group: Api.Www.SpecGroup) {
|
||||||
|
groupForm.value = {
|
||||||
|
id: group.id,
|
||||||
|
title_zh: group.title_zh,
|
||||||
|
title_en: group.title_en,
|
||||||
|
default_visible: group.default_visible || 5
|
||||||
|
};
|
||||||
|
groupDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGroupSubmit() {
|
||||||
|
if (!sectionId.value) return;
|
||||||
|
if (!groupForm.value.title_zh || !groupForm.value.title_en) {
|
||||||
|
window.$message?.warning('请填写中英文标题');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
groupSubmitting.value = true;
|
||||||
|
const payload = {
|
||||||
|
title_zh: groupForm.value.title_zh,
|
||||||
|
title_en: groupForm.value.title_en,
|
||||||
|
default_visible: groupForm.value.default_visible
|
||||||
|
};
|
||||||
|
const { error } = groupForm.value.id
|
||||||
|
? await fetchUpdateSpecGroup(sectionId.value, groupForm.value.id, payload)
|
||||||
|
: await fetchCreateSpecGroup(sectionId.value, payload);
|
||||||
|
groupSubmitting.value = false;
|
||||||
|
if (!error) {
|
||||||
|
window.$message?.success(groupForm.value.id ? '更新成功' : '创建成功');
|
||||||
|
groupDialogVisible.value = false;
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteGroup(group: Api.Www.SpecGroup) {
|
||||||
|
window.$dialog?.warning({
|
||||||
|
title: '警告',
|
||||||
|
content: `确定要删除参数分组「${group.title_zh}」吗?其下所有参数项将一并删除。`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
if (!sectionId.value) return;
|
||||||
|
const { error } = await fetchDeleteSpecGroup(sectionId.value, group.id);
|
||||||
|
if (!error) {
|
||||||
|
window.$message?.success('删除成功');
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 参数项编辑 =====
|
||||||
|
const itemDialogVisible = ref(false);
|
||||||
|
const itemSubmitting = ref(false);
|
||||||
|
const itemForm = ref<{ groupId: number; id: number | null; name_zh: string; name_en: string; value_zh: string; value_en: string }>({
|
||||||
|
groupId: 0,
|
||||||
|
id: null,
|
||||||
|
name_zh: '',
|
||||||
|
name_en: '',
|
||||||
|
value_zh: '',
|
||||||
|
value_en: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleAddItem(group: Api.Www.SpecGroup) {
|
||||||
|
itemForm.value = { groupId: group.id, id: null, name_zh: '', name_en: '', value_zh: '', value_en: '' };
|
||||||
|
itemDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditItem(group: Api.Www.SpecGroup, item: Api.Www.SpecItem) {
|
||||||
|
itemForm.value = {
|
||||||
|
groupId: group.id,
|
||||||
|
id: item.id,
|
||||||
|
name_zh: item.name_zh,
|
||||||
|
name_en: item.name_en,
|
||||||
|
value_zh: item.value_zh,
|
||||||
|
value_en: item.value_en
|
||||||
|
};
|
||||||
|
itemDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleItemSubmit() {
|
||||||
|
const f = itemForm.value;
|
||||||
|
if (!f.name_zh || !f.name_en || !f.value_zh || !f.value_en) {
|
||||||
|
window.$message?.warning('请完整填写中英文参数名与参数值');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
itemSubmitting.value = true;
|
||||||
|
const payload = { name_zh: f.name_zh, name_en: f.name_en, value_zh: f.value_zh, value_en: f.value_en };
|
||||||
|
const { error } = f.id ? await fetchUpdateSpecItem(f.id, payload) : await fetchCreateSpecItem(f.groupId, payload);
|
||||||
|
itemSubmitting.value = false;
|
||||||
|
if (!error) {
|
||||||
|
window.$message?.success(f.id ? '更新成功' : '创建成功');
|
||||||
|
itemDialogVisible.value = false;
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteItem(item: Api.Www.SpecItem) {
|
||||||
|
window.$dialog?.warning({
|
||||||
|
title: '警告',
|
||||||
|
content: `确定要删除参数「${item.name_zh}」吗?`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
const { error } = await fetchDeleteSpecItem(item.id);
|
||||||
|
if (!error) {
|
||||||
|
window.$message?.success('删除成功');
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 参数项排序 =====
|
||||||
|
async function moveItem(group: Api.Www.SpecGroup, index: number, direction: -1 | 1) {
|
||||||
|
const items = [...(group.items || [])];
|
||||||
|
const swapIndex = index + direction;
|
||||||
|
if (swapIndex < 0 || swapIndex >= items.length) return;
|
||||||
|
[items[index], items[swapIndex]] = [items[swapIndex], items[index]];
|
||||||
|
const { error } = await fetchSortSpecItems(
|
||||||
|
group.id,
|
||||||
|
items.map((it, i) => ({ id: it.id, sort_order: i }))
|
||||||
|
);
|
||||||
|
if (!error) {
|
||||||
|
await loadData();
|
||||||
|
window.$message?.success('排序已更新');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NModal v-model:show="visible" preset="card" :title="`参数管理:${sectionTitle}`" class="w-900px" :mask-closable="false">
|
||||||
|
<NSpace justify="space-between" align="center" class="mb-12px">
|
||||||
|
<span class="text-12px opacity-60">前台按分组顺序渲染参数表;「默认显示行数」之外的参数折叠,用户可自行展开</span>
|
||||||
|
<NButton size="small" type="primary" @click="handleAddGroup">新增分组</NButton>
|
||||||
|
</NSpace>
|
||||||
|
|
||||||
|
<NEmpty v-if="!loading && groups.length === 0" description="暂无参数分组,请先新增分组" class="py-40px" />
|
||||||
|
|
||||||
|
<NCollapse v-else :default-expanded-names="expandedNames" arrow-placement="left">
|
||||||
|
<NCollapseItem v-for="(group, gIdx) in groups" :key="group.id" :name="group.id">
|
||||||
|
<template #header>
|
||||||
|
<span class="font-medium">{{ group.title_zh }}</span>
|
||||||
|
<span class="ml-8px text-12px opacity-60">{{ group.title_en }} · 默认显示 {{ group.default_visible || 5 }} 行 · {{ group.items?.length || 0 }} 项</span>
|
||||||
|
</template>
|
||||||
|
<template #header-extra>
|
||||||
|
<div class="flex-y-center gap-4px" @click.stop>
|
||||||
|
<NButton text size="tiny" type="primary" :disabled="gIdx === 0" @click="moveGroup(gIdx, -1)">↑</NButton>
|
||||||
|
<NButton text size="tiny" type="primary" :disabled="gIdx === groups.length - 1" @click="moveGroup(gIdx, 1)">↓</NButton>
|
||||||
|
<NButton text size="tiny" type="primary" @click="handleEditGroup(group)">编辑</NButton>
|
||||||
|
<NButton text size="tiny" type="error" @click="handleDeleteGroup(group)">删除</NButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-8px">
|
||||||
|
<div
|
||||||
|
v-for="(item, iIdx) in group.items || []"
|
||||||
|
:key="item.id"
|
||||||
|
class="flex items-center gap-12px rounded-8px bg-gray-50 px-12px py-8px dark:bg-black/10"
|
||||||
|
>
|
||||||
|
<span class="w-25% shrink-0 text-13px font-medium">{{ item.name_zh }}</span>
|
||||||
|
<span class="flex-1 truncate text-13px opacity-80" :title="item.value_zh">{{ item.value_zh }}</span>
|
||||||
|
<span class="hidden w-30% truncate text-12px opacity-50 md:block" :title="`${item.name_en} / ${item.value_en}`">
|
||||||
|
{{ item.name_en }} / {{ item.value_en }}
|
||||||
|
</span>
|
||||||
|
<NSpace :size="4" class="shrink-0">
|
||||||
|
<NButton text size="tiny" type="primary" :disabled="iIdx === 0" @click="moveItem(group, iIdx, -1)">↑</NButton>
|
||||||
|
<NButton text size="tiny" type="primary" :disabled="iIdx === (group.items?.length || 0) - 1" @click="moveItem(group, iIdx, 1)">↓</NButton>
|
||||||
|
<NButton text size="tiny" type="primary" @click="handleEditItem(group, item)">编辑</NButton>
|
||||||
|
<NButton text size="tiny" type="error" @click="handleDeleteItem(item)">删除</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</div>
|
||||||
|
<NButton dashed size="small" @click="handleAddItem(group)">+ 新增参数项</NButton>
|
||||||
|
</div>
|
||||||
|
</NCollapseItem>
|
||||||
|
</NCollapse>
|
||||||
|
|
||||||
|
<!-- 分组编辑弹窗 -->
|
||||||
|
<NModal v-model:show="groupDialogVisible" preset="card" :title="groupForm.id ? '编辑参数分组' : '新增参数分组'" class="w-560px" :mask-closable="false">
|
||||||
|
<NForm :model="groupForm" label-placement="left" label-width="100">
|
||||||
|
<NFormItem label="标题(中)" required>
|
||||||
|
<NInput v-model:value="groupForm.title_zh" maxlength="100" placeholder="如:基本参数" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="标题(英)" required>
|
||||||
|
<NInput v-model:value="groupForm.title_en" maxlength="100" placeholder="e.g. Basic Parameters" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="默认显示行数">
|
||||||
|
<NInputNumber v-model:value="groupForm.default_visible" :min="1" :max="50" class="w-full" />
|
||||||
|
<span class="ml-8px text-12px opacity-60">前台默认显示的行数,超出的折叠由用户展开</span>
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
<template #footer>
|
||||||
|
<NSpace justify="end">
|
||||||
|
<NButton @click="groupDialogVisible = false">取消</NButton>
|
||||||
|
<NButton type="primary" :loading="groupSubmitting" @click="handleGroupSubmit">确定</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
|
||||||
|
<!-- 参数项编辑弹窗 -->
|
||||||
|
<NModal v-model:show="itemDialogVisible" preset="card" :title="itemForm.id ? '编辑参数项' : '新增参数项'" class="w-640px" :mask-closable="false">
|
||||||
|
<NForm :model="itemForm" label-placement="left" label-width="100">
|
||||||
|
<NFormItem label="参数名(中)" required>
|
||||||
|
<NInput v-model:value="itemForm.name_zh" maxlength="100" placeholder="如:型号" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="参数名(英)" required>
|
||||||
|
<NInput v-model:value="itemForm.name_en" maxlength="100" placeholder="e.g. Model" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="参数值(中)" required>
|
||||||
|
<NInput v-model:value="itemForm.value_zh" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" placeholder="如:Luxsin X8" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="参数值(英)" required>
|
||||||
|
<NInput v-model:value="itemForm.value_en" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" placeholder="e.g. Luxsin X8" />
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
<template #footer>
|
||||||
|
<NSpace justify="end">
|
||||||
|
<NButton @click="itemDialogVisible = false">取消</NButton>
|
||||||
|
<NButton type="primary" :loading="itemSubmitting" @click="handleItemSubmit">确定</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
</NModal>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- 产品详情(product_id=1)参数区块「平衡耳机输出性能参数(4.4mm/XLR4输出)」参数项
|
||||||
|
-- 区块:www_section_blocks.id=28(spec_table),分组已预建:gid=5
|
||||||
|
|
||||||
|
INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
|
||||||
|
(5, '高增益', 'High Gain', 'L+R≥4840mW+4840mW(16Ω, THD+N<1%)', 'L+R≥4840mW+4840mW (16Ω, THD+N < 1%)', 0),
|
||||||
|
(5, '输出阻抗', 'Output Impedance', '<1Ω', '< 1Ω', 1),
|
||||||
|
(5, '总谐波失真', 'THD+N', '<−117dB (1kHz/0dB@32Ω)', '< -117dB (1kHz / 0dB @ 32Ω)', 2),
|
||||||
|
(5, '信噪比', 'SNR', '≥123dB', '≥123dB', 3),
|
||||||
|
(5, '声道分离度', 'Crosstalk', '≥119dB (1kHz@32Ω)', '≥119dB (1kHz @ 32Ω)', 4);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- 产品详情(product_id=1)参数区块「基本规格」补充参数项
|
||||||
|
-- 区块:www_section_blocks.id=28(spec_table),分组:www_spec_groups.id=1(基本规格)
|
||||||
|
-- 已存在:型号 / 尺寸 / 重量(手工新增,sort_order 均为 0,此处一并规范化)
|
||||||
|
|
||||||
|
-- 1) 规范化已有 3 项排序
|
||||||
|
UPDATE www_spec_items SET sort_order = 0 WHERE id = 1;
|
||||||
|
UPDATE www_spec_items SET sort_order = 1 WHERE id = 2;
|
||||||
|
UPDATE www_spec_items SET sort_order = 2 WHERE id = 3;
|
||||||
|
|
||||||
|
-- 2) 按图片补充剩余 9 项
|
||||||
|
INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
|
||||||
|
(1, '电源输入', 'Input Power', 'AC 100–120V/220–240V~50/60Hz', 'AC 100-120V/220-240V~50/60Hz', 3),
|
||||||
|
(1, '屏幕', 'Display', '4英寸 LCD(480×960)触摸屏', '4" LCD (480*960) Touchscreen', 4),
|
||||||
|
(1, '解码', 'DAC', 'Cirrus Logic CS43198 × 8', 'Cirrus Logic CS43198 × 8', 5),
|
||||||
|
(1, '内部电源', 'Power Supply', '超低底噪可调电压线性电源', 'Ultra-low-noise, voltage-adjustable Linear Power Supply', 6),
|
||||||
|
(1, 'WiFi', 'WiFi', '支持2.4G/5G双频WiFi', 'Wi-Fi 2.4GHz / 5GHz', 7),
|
||||||
|
(1, '蓝牙接收', 'Bluetooth', 'Qualcomm SXW5125 (BT 5.1)', 'Qualcomm SXW5125 (BT 5.1)', 8),
|
||||||
|
(1, '控制方式', 'Control Methods', '触控屏, 移动设备 APP (Android/iOS), 电脑浏览器, 红外遥控器(选购配件)', 'Touchscreen, Mobile APP (Android/iOS), Web Browser, IR Remote(Optional)', 9),
|
||||||
|
(1, '额定功率', 'Rated Power', '25W', '25W', 10),
|
||||||
|
(1, '包装清单', 'Packing List', '电源线×1,USB-B线×1, 3.5mm转6.35mm适配器×1', 'Power Cable ×1, USB-B cable ×1, 3.5mm to 6.35mm adapter ×1', 11);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- 产品详情(product_id=1)参数区块「输入 / 输出」参数项
|
||||||
|
-- 区块:www_section_blocks.id=28(spec_table),分组:www_spec_groups.id=2(输入 / 输出)
|
||||||
|
|
||||||
|
INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
|
||||||
|
(2, 'USB-A 输入', 'USB-A Input', '用于本地固件更新', 'For local firmware update', 0),
|
||||||
|
(2, 'USB-B 输入', 'USB-B Input', '最高支持 PCM 768kHz/32bit; DSD512(Native)', 'Up to PCM 768kHz/32bit; DSD512(Native)', 1),
|
||||||
|
(2, 'USB-C 输入', 'USB-C Input', '最高支持 PCM 768kHz/32bit; DSD512(Native)', 'Up to PCM 768kHz/32bit; DSD512(Native)', 2),
|
||||||
|
(2, 'IIS 输入', 'IIS Input', '最高支持 PCM 768kHz/32bit; DSD512(Native)', 'Up to PCM 768kHz/32bit; DSD512(Native)', 3),
|
||||||
|
(2, '同轴输入', 'Coaxial Input', '最高支持 PCM 192kHz/24bit', 'Up to PCM 192kHz/24bit', 4),
|
||||||
|
(2, '光纤输入', 'Optical Input', '最高支持 PCM 192kHz/24bit', 'Up to PCM 192kHz/24bit', 5),
|
||||||
|
(2, 'TRIGGER IN', 'TRIGGER IN', '标准3.5mm耳机插座', 'Standard 3.5mm headphone jack', 6),
|
||||||
|
(2, 'TRIGGER OUT', 'TRIGGER OUT', '标准3.5mm耳机插座', 'Standard 3.5mm headphone jack', 7),
|
||||||
|
(2, '模拟输出接口', 'Analog Output', 'RCA', 'RCA', 8),
|
||||||
|
(2, '数字输出接口', 'Balanced Output', 'XLR', 'XLR', 9),
|
||||||
|
(2, '耳机输出接口', 'Headphone Output', '6.35mm(非平衡),4.4mm(平衡),4 pin XLR(平衡)', '6.35mm (Unbalanced), 4.4mm (Balanced), 4 pin XLR (Balanced)', 10);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- 产品详情(product_id=1)参数区块「RCA 输出音频特性」参数项
|
||||||
|
-- 区块:www_section_blocks.id=28(spec_table),分组已预建:gid=4
|
||||||
|
|
||||||
|
INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
|
||||||
|
(4, '信噪比', 'SNR', '≥124.5dB', '≥124.5dB', 0),
|
||||||
|
(4, '总谐波失真', 'THD+N', '<−120dB @不加权', '< -120dB @ unweighted', 1),
|
||||||
|
(4, '声道分离度', 'Crosstalk', '≥125dB (1kHz@100kΩ)', '≥125dB (1kHz@100kΩ)', 2),
|
||||||
|
(4, '底噪', 'Noise Floor', '<1.3uVrms', '< 1.3uVrms', 3),
|
||||||
|
(4, '线路输出电平', 'Output Level', '2.1Vrms (1kHz@100kΩ)', '2.1Vrms (1kHz@100kΩ)', 4);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- 产品详情(product_id=1)参数区块「单端耳机输出性能参数(6.35mm输出)」参数项
|
||||||
|
-- 区块:www_section_blocks.id=28(spec_table),分组已预建:gid=6
|
||||||
|
|
||||||
|
INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
|
||||||
|
(6, '高增益', 'High Gain', 'L+R≥2900mW+2900mW(16Ω,THD+N<1%)', 'L+R≥2900mW+2900mW (16Ω, THD+N < 1%)', 0),
|
||||||
|
(6, '输出阻抗', 'Output Impedance', '<1Ω', '< 1Ω', 1),
|
||||||
|
(6, '总谐波失真', 'THD+N', '<−115dB (1kHz/0dB@32Ω)', '< -115dB (1kHz/0dB@32Ω)', 2),
|
||||||
|
(6, '信噪比', 'SNR', '≥121dB', '≥121dB', 3);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Luxsin X8(product_id=1)区块排序修复
|
||||||
|
-- 背景:所有区块 sort_order 均为 0,前台按 sort_order 排序导致顺序混乱。
|
||||||
|
-- 已确认 www_section_blocks.id 为自增主键,按 id 升序(即创建顺序)重新分配 sort_order(0 起)。
|
||||||
|
-- 同时后端 POST /api/www/sections 已改为:未指定 sort_order 时取同 page_type(+product_id)
|
||||||
|
-- 下现有区块的 max(sort_order)+1 自动追加,不再默认 0。
|
||||||
|
|
||||||
|
SET @row := -1;
|
||||||
|
UPDATE www_section_blocks
|
||||||
|
SET sort_order = (@row := @row + 1)
|
||||||
|
WHERE page_type = 'product_detail' AND product_id = 1
|
||||||
|
ORDER BY id;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- 产品详情(product_id=1)参数区块「XLR 输出音频特性」参数项
|
||||||
|
-- 区块:www_section_blocks.id=28(spec_table)
|
||||||
|
-- 说明:分组「XLR 输出音频特性」已预先存在(gid=3),本脚本仅补参数项;
|
||||||
|
-- 执行中发现重复建组(gid=7)已清理:参数项改挂 gid=3,重复组已删除。
|
||||||
|
-- 顺带规范了各分组 sort_order:基本规格=0、输入/输出=1、XLR=2、RCA=3、平衡耳机=4、单端耳机=5
|
||||||
|
|
||||||
|
INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
|
||||||
|
(3, '信噪比', 'SNR', '≥129.5dB', '≥129.5dB', 0),
|
||||||
|
(3, '总谐波失真', 'THD+N', '<−121.8dB @不加权', '< -121.8dB @ unweighted', 1),
|
||||||
|
(3, '声道分离度', 'Crosstalk', '≥133dB (1kHz@200kΩ)', '≥133dB (1kHz@200kΩ)', 2),
|
||||||
|
(3, '底噪', 'Noise Floor', '<1.1uVrms', '< 1.1uVrms', 3),
|
||||||
|
(3, '线路输出电平', 'Output Level', '4.2Vrms (1kHz@200kΩ)', '4.2Vrms (1kHz@200kΩ)', 4);
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -4,6 +4,17 @@ html {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* UnoCSS wind3 preflight 未含 Tailwind 标准的 border 重置:浏览器默认 border-style 为 none,
|
||||||
|
导致 border-b / border 等宽度工具类不可见,此处补上(宽度默认 0,不影响未显式设置边框的元素) */
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family:
|
font-family:
|
||||||
|
|||||||
@@ -72,6 +72,13 @@ const paddingY = computed(() => {
|
|||||||
return typeof v === 'number' && v >= 0 ? `${v}px` : '';
|
return typeof v === 'number' && v >= 0 ? `${v}px` : '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 区块上下外边距(config.margin_y):单位 px,区块与相邻区块的间隙(露出页面背景);
|
||||||
|
// 未设置或 0 = 紧贴相邻区块(默认,全宽无缝拼接设计)
|
||||||
|
const marginY = computed(() => {
|
||||||
|
const v = props.section.config?.margin_y;
|
||||||
|
return typeof v === 'number' && v > 0 ? `${v}px` : '';
|
||||||
|
});
|
||||||
|
|
||||||
// 标题字号(config.title_font_size):桌面端 px,未设置时不下发变量(main.css 的 section h2 规则兜底 36px);
|
// 标题字号(config.title_font_size):桌面端 px,未设置时不下发变量(main.css 的 section h2 规则兜底 36px);
|
||||||
// 移动端按桌面值 62.5% 等比缩小(--sec-title-fs-m)
|
// 移动端按桌面值 62.5% 等比缩小(--sec-title-fs-m)
|
||||||
const titleFontSize = computed(() => {
|
const titleFontSize = computed(() => {
|
||||||
@@ -89,6 +96,7 @@ const sectionStyleVars = computed<CSSProperties>(() => {
|
|||||||
'--sec-cw': containerWidth,
|
'--sec-cw': containerWidth,
|
||||||
borderRadius: borderRadius.value,
|
borderRadius: borderRadius.value,
|
||||||
...(paddingY.value ? { '--sec-py': paddingY.value } : {}),
|
...(paddingY.value ? { '--sec-py': paddingY.value } : {}),
|
||||||
|
...(marginY.value ? { marginTop: marginY.value, marginBottom: marginY.value } : {}),
|
||||||
...(titleFontSize.value ? { '--sec-title-fs': titleFontSize.value.desktop, '--sec-title-fs-m': titleFontSize.value.mobile } : {})
|
...(titleFontSize.value ? { '--sec-title-fs': titleFontSize.value.desktop, '--sec-title-fs-m': titleFontSize.value.mobile } : {})
|
||||||
} as CSSProperties;
|
} as CSSProperties;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import type { SectionBlock, SpecGroup } from '~/types/site';
|
import type { SectionBlock, SpecGroup } from '~/types/site';
|
||||||
|
|
||||||
const props = defineProps<{ section: SectionBlock }>();
|
const props = defineProps<{ section: SectionBlock }>();
|
||||||
|
|
||||||
const { overline, title, subtitle, isEn, isDark, overlineStyle, titleStyle, bodyStyle } = useSectionContent(props.section);
|
const { overline, title, subtitle, content, isEn, isDark, overlineStyle, titleStyle, bodyStyle } = useSectionContent(props.section);
|
||||||
|
|
||||||
const specGroups = computed<SpecGroup[]>(() => props.section.spec_groups ?? []);
|
const specGroups = computed<SpecGroup[]>(() => props.section.spec_groups ?? []);
|
||||||
|
|
||||||
// 每个分组的展开状态(默认收起,仅显示前 default_visible 项)
|
// 每个分组的展开状态(默认收起,仅显示前 default_visible 行)
|
||||||
const expandedMap = ref<Record<number, boolean>>({});
|
const expandedMap = ref<Record<number, boolean>>({});
|
||||||
|
|
||||||
function isExpanded(group: SpecGroup): boolean {
|
function isExpanded(group: SpecGroup): boolean {
|
||||||
@@ -18,10 +19,10 @@ function toggle(group: SpecGroup) {
|
|||||||
expandedMap.value[group.id] = !expandedMap.value[group.id];
|
expandedMap.value[group.id] = !expandedMap.value[group.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分组内可见的参数项:展开时全部,收起时前 default_visible 项
|
// 固定显示的参数项:始终为前 default_visible 项(默认 5 行),
|
||||||
function visibleItems(group: SpecGroup) {
|
// 其余项由下方 Transition 区域渲染,避免展开时重复
|
||||||
|
function fixedItems(group: SpecGroup) {
|
||||||
const items = group.items ?? [];
|
const items = group.items ?? [];
|
||||||
if (isExpanded(group)) return items;
|
|
||||||
return items.slice(0, group.default_visible || 5);
|
return items.slice(0, group.default_visible || 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,11 +30,44 @@ function visibleItems(group: SpecGroup) {
|
|||||||
function needToggle(group: SpecGroup): boolean {
|
function needToggle(group: SpecGroup): boolean {
|
||||||
return (group.items?.length ?? 0) > (group.default_visible || 5);
|
return (group.items?.length ?? 0) > (group.default_visible || 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 展开/收起高度过渡:从 0 动画到内容自然高度
|
||||||
|
// 注意:钩子必须声明并调用 done 回调(Vue 按形参个数判断是否显式接管完成时机),
|
||||||
|
// 否则 leave 结束后元素不会被移除,导致再次展开失效
|
||||||
|
const SPEC_TOGGLE_MS = 300;
|
||||||
|
|
||||||
|
function onEnter(el: Element, done: () => void) {
|
||||||
|
const html = el as HTMLElement;
|
||||||
|
html.style.overflow = 'hidden';
|
||||||
|
html.style.height = '0';
|
||||||
|
// 强制 reflow 后再切到目标高度,确保过渡生效
|
||||||
|
void html.offsetHeight;
|
||||||
|
html.style.transition = `height ${SPEC_TOGGLE_MS}ms ease`;
|
||||||
|
html.style.height = `${html.scrollHeight}px`;
|
||||||
|
setTimeout(done, SPEC_TOGGLE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAfterEnter(el: Element) {
|
||||||
|
const html = el as HTMLElement;
|
||||||
|
html.style.overflow = '';
|
||||||
|
html.style.height = '';
|
||||||
|
html.style.transition = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeave(el: Element, done: () => void) {
|
||||||
|
const html = el as HTMLElement;
|
||||||
|
html.style.overflow = 'hidden';
|
||||||
|
html.style.height = `${html.scrollHeight}px`;
|
||||||
|
void html.offsetHeight;
|
||||||
|
html.style.transition = `height ${SPEC_TOGGLE_MS}ms ease`;
|
||||||
|
html.style.height = '0';
|
||||||
|
setTimeout(done, SPEC_TOGGLE_MS);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="section-container py-[var(--sec-py,96px)]">
|
<div class="section-container py-[var(--sec-py,96px)]">
|
||||||
<!-- 标题区 -->
|
<!-- 区块标题:整个区块顶部居中 -->
|
||||||
<div class="text-center max-w-640px mx-auto mb-56px">
|
<div class="text-center max-w-640px mx-auto mb-56px">
|
||||||
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
||||||
{{ overline }}
|
{{ overline }}
|
||||||
@@ -44,49 +78,84 @@ function needToggle(group: SpecGroup): boolean {
|
|||||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle">
|
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle">
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</p>
|
</p>
|
||||||
|
<div v-if="content" class="rich-text mt-20px text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 参数分组卡片 -->
|
<!-- 参数分组:左分组标题 + 右参数卡片 -->
|
||||||
<div class="flex flex-col gap-24px max-w-960px mx-auto">
|
<div class="flex flex-col gap-64px">
|
||||||
<div
|
<div
|
||||||
v-for="group in specGroups"
|
v-for="group in specGroups"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
class="rounded-16px overflow-hidden"
|
class="md:grid md:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] gap-64px items-start"
|
||||||
:class="isDark ? 'bg-white/5 border border-white/10' : 'bg-white border border-gray-100 shadow-sm dark:bg-dark-800 dark:border-dark-700'"
|
|
||||||
>
|
>
|
||||||
<!-- 分组标题 -->
|
<!-- 左栏:分组标题(桌面端吸顶) -->
|
||||||
<div class="px-32px py-20px border-b" :class="isDark ? 'border-white/10' : 'border-gray-100 dark:border-dark-700'">
|
<div class="mb-24px md:sticky md:top-120px md:mb-0">
|
||||||
<h3 class="text-lg font-semibold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
<h3 class="text-xl md:text-24px font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||||
{{ isEn ? group.title_en : group.title_zh }}
|
{{ isEn ? group.title_en : group.title_zh }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 参数项列表 -->
|
<!-- 右栏:参数卡片(2 列:参数名 / 参数值) -->
|
||||||
<div class="px-32px">
|
<div
|
||||||
<div
|
class="rounded-16px overflow-hidden"
|
||||||
v-for="item in visibleItems(group)"
|
:class="isDark ? 'bg-white/5 border border-white/10' : 'bg-gray-100 border border-gray-200 dark:bg-dark-800 dark:border-dark-700'"
|
||||||
:key="item.id"
|
>
|
||||||
class="flex items-start justify-between gap-24px py-14px border-b last:border-b-0"
|
<div class="px-24px md:px-32px py-16px">
|
||||||
:class="isDark ? 'border-white/5' : 'border-gray-50 dark:border-dark-700/50'"
|
<div
|
||||||
>
|
v-for="(item, idx) in fixedItems(group)"
|
||||||
<span class="text-sm shrink-0" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
:key="item.id"
|
||||||
{{ isEn ? item.name_en : item.name_zh }}
|
class="flex items-baseline justify-between gap-24px py-16px border-b"
|
||||||
</span>
|
:class="[isDark ? 'border-white/10' : 'border-gray-300 dark:border-dark-600', idx === fixedItems(group).length - 1 && (!needToggle(group) || !isExpanded(group)) ? 'border-b-0' : '']"
|
||||||
<span class="text-sm text-right font-medium" :class="isDark ? 'text-gray-200' : 'text-gray-800 dark:text-gray-200'">
|
>
|
||||||
{{ isEn ? item.value_en : item.value_zh }}
|
<span class="text-sm font-semibold w-20% shrink-0" :class="isDark ? 'text-gray-200' : 'text-gray-800 dark:text-gray-200'">
|
||||||
</span>
|
{{ isEn ? item.name_en : item.name_zh }}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
<span class="flex-1 text-sm text-left" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||||
|
{{ isEn ? item.value_en : item.value_zh }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 展开/收起按钮 -->
|
<!-- 收起时隐藏的参数项:高度过渡动画 -->
|
||||||
<div v-if="needToggle(group)" class="px-32px py-16px">
|
<Transition @enter="onEnter" @after-enter="onAfterEnter" @leave="onLeave">
|
||||||
<button
|
<div v-if="isExpanded(group)">
|
||||||
class="text-sm font-medium transition-colors"
|
<div
|
||||||
:class="isDark ? 'text-primary-300 hover:text-primary-200' : 'text-primary hover:opacity-80'"
|
v-for="item in group.items?.slice(group.default_visible || 5)"
|
||||||
@click="toggle(group)"
|
:key="item.id"
|
||||||
>
|
class="flex items-baseline justify-between gap-24px py-16px border-b"
|
||||||
{{ isExpanded(group) ? '收起参数 ∧' : `查看全部参数(${group.items?.length ?? 0} 项)∨` }}
|
:class="[isDark ? 'border-white/10' : 'border-gray-300 dark:border-dark-600', item.id === group.items?.at(-1)?.id ? 'border-b-0' : '']"
|
||||||
</button>
|
>
|
||||||
|
<span class="text-sm font-semibold w-20% shrink-0" :class="isDark ? 'text-gray-200' : 'text-gray-800 dark:text-gray-200'">
|
||||||
|
{{ isEn ? item.name_en : item.name_zh }}
|
||||||
|
</span>
|
||||||
|
<span class="flex-1 text-sm text-left" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||||
|
{{ isEn ? item.value_en : item.value_zh }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 展开/收起按钮 -->
|
||||||
|
<div v-if="needToggle(group)" class="px-24px md:px-32px py-16px flex justify-center">
|
||||||
|
<button
|
||||||
|
class="text-sm font-medium inline-flex items-center gap-8px bg-transparent cursor-pointer transition-colors"
|
||||||
|
:class="isDark ? 'text-white' : 'text-gray-600 dark:text-white'"
|
||||||
|
@click="toggle(group)"
|
||||||
|
>
|
||||||
|
<template v-if="isExpanded(group)">
|
||||||
|
{{ isEn ? 'View less' : '收起参数' }}
|
||||||
|
<span class="w-24px h-24px rounded-full flex items-center justify-center text-14px" :class="isDark ? 'bg-white/10 text-white' : 'bg-gray-200 text-gray-700'" aria-hidden="true">
|
||||||
|
<Icon icon="mingcute:up-line" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ isEn ? `View all (${group.items?.length ?? 0})` : `查看全部参数(${group.items?.length ?? 0} 项)` }}
|
||||||
|
<span class="w-24px h-24px rounded-full flex items-center justify-center text-14px" :class="isDark ? 'bg-white/10 text-white' : 'bg-gray-200 text-gray-700'" aria-hidden="true">
|
||||||
|
<Icon icon="mingcute:down-line" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ const videoUrl = computed(() => resolveAssetUrl(props.section.video_url));
|
|||||||
// 媒体位置(config.media_position):left 图/视频在左(默认)/ right 在右;移动端始终文案在上
|
// 媒体位置(config.media_position):left 图/视频在左(默认)/ right 在右;移动端始终文案在上
|
||||||
const mediaPosition = computed(() => (props.section.config as Record<string, string> | null)?.media_position === 'right' ? 'right' : 'left');
|
const mediaPosition = computed(() => (props.section.config as Record<string, string> | null)?.media_position === 'right' ? 'right' : 'left');
|
||||||
|
|
||||||
|
// 分栏比例(config.split_ratio):媒体栏宽占比,档位 30/40/50/60/70,默认 50(五五等分);移动端单列不受影响
|
||||||
|
const splitRatio = computed(() => {
|
||||||
|
const v = Number((props.section.config as Record<string, unknown> | null)?.split_ratio);
|
||||||
|
return [30, 40, 50, 60, 70].includes(v) ? v : 50;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 栅格轨道随媒体位置:媒体在左 = 首轨为媒体栏,媒体在右 = 次轨为媒体栏(完整字面量供 UnoCSS 提取)
|
||||||
|
const gridClass = computed(() =>
|
||||||
|
mediaPosition.value === 'right'
|
||||||
|
? 'md:grid-cols-[minmax(0,1fr)_var(--split-media,50%)]'
|
||||||
|
: 'md:grid-cols-[var(--split-media,50%)_minmax(0,1fr)]'
|
||||||
|
);
|
||||||
|
|
||||||
// 媒体填满所在栏(去掉 520px 上限与栏内居中),高度按原素材比例自适应
|
// 媒体填满所在栏(去掉 520px 上限与栏内居中),高度按原素材比例自适应
|
||||||
const mediaWrapClass = computed(() =>
|
const mediaWrapClass = computed(() =>
|
||||||
mediaPosition.value === 'right'
|
mediaPosition.value === 'right'
|
||||||
@@ -27,7 +40,11 @@ const textWrapClass = computed(() =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="section-container py-[var(--sec-py,96px)] grid md:grid-cols-2 gap-48px items-center">
|
<div
|
||||||
|
class="section-container py-[var(--sec-py,96px)] grid gap-48px items-center"
|
||||||
|
:class="gridClass"
|
||||||
|
:style="{ '--split-media': `${splitRatio}%` }"
|
||||||
|
>
|
||||||
<!-- 媒体:视频优先,其次图片 -->
|
<!-- 媒体:视频优先,其次图片 -->
|
||||||
<div v-if="videoUrl || mediaUrl" :class="mediaWrapClass">
|
<div v-if="videoUrl || mediaUrl" :class="mediaWrapClass">
|
||||||
<video
|
<video
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"typecheck": "nuxt typecheck"
|
"typecheck": "nuxt typecheck"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@iconify/vue": "^5.0.1",
|
||||||
"@nuxtjs/i18n": "^10.5.0",
|
"@nuxtjs/i18n": "^10.5.0",
|
||||||
"@pinia/nuxt": "^1.0.1",
|
"@pinia/nuxt": "^1.0.1",
|
||||||
"@vueuse/nuxt": "^14.4.0",
|
"@vueuse/nuxt": "^14.4.0",
|
||||||
|
|||||||
Generated
+13
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@iconify/vue':
|
||||||
|
specifier: ^5.0.1
|
||||||
|
version: 5.0.1(vue@3.5.40(typescript@5.8.2))
|
||||||
'@nuxtjs/i18n':
|
'@nuxtjs/i18n':
|
||||||
specifier: ^10.5.0
|
specifier: ^10.5.0
|
||||||
version: 10.5.0(@vue/compiler-dom@3.5.40)(db0@0.3.4)(esbuild@0.28.1)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.0.0))(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.3)(pinia@4.0.2(@vue/devtools-api@8.2.1)(typescript@5.8.2)(vue@3.5.40(typescript@5.8.2)))(rolldown@1.2.0)(rollup@4.62.3)(supports-color@10.0.0)(typescript@5.8.2)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.109.1(cssnano@8.0.2(postcss@8.5.23))(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.23))
|
version: 10.5.0(@vue/compiler-dom@3.5.40)(db0@0.3.4)(esbuild@0.28.1)(eslint@10.8.0(jiti@2.7.0)(supports-color@10.0.0))(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.3)(pinia@4.0.2(@vue/devtools-api@8.2.1)(typescript@5.8.2)(vue@3.5.40(typescript@5.8.2)))(rolldown@1.2.0)(rollup@4.62.3)(supports-color@10.0.0)(typescript@5.8.2)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.109.1(cssnano@8.0.2(postcss@8.5.23))(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(postcss@8.5.23))
|
||||||
@@ -622,6 +625,11 @@ packages:
|
|||||||
'@iconify/utils@3.1.4':
|
'@iconify/utils@3.1.4':
|
||||||
resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==}
|
resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==}
|
||||||
|
|
||||||
|
'@iconify/vue@5.0.1':
|
||||||
|
resolution: {integrity: sha512-aumwwooJlFJ5H5qYWB6ZTAyM0C8hpfcSVLB9/a3qnH1GGvIJ+FEbpEs4s/HfErYe/M5qZeLjwmESR5fFm3lXEw==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3.0.0'
|
||||||
|
|
||||||
'@intlify/bundle-utils@11.2.4':
|
'@intlify/bundle-utils@11.2.4':
|
||||||
resolution: {integrity: sha512-eE18yR9eM9k5n8snCkHIYp2MuVTxa19aF8z9OMyxXWv0frz2HlBZDGIPFjA38pP3OJ1IlRBXC/dW5GILeLMSCQ==}
|
resolution: {integrity: sha512-eE18yR9eM9k5n8snCkHIYp2MuVTxa19aF8z9OMyxXWv0frz2HlBZDGIPFjA38pP3OJ1IlRBXC/dW5GILeLMSCQ==}
|
||||||
engines: {node: '>= 22.13'}
|
engines: {node: '>= 22.13'}
|
||||||
@@ -5221,6 +5229,11 @@ snapshots:
|
|||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
import-meta-resolve: 4.2.0
|
import-meta-resolve: 4.2.0
|
||||||
|
|
||||||
|
'@iconify/vue@5.0.1(vue@3.5.40(typescript@5.8.2))':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
vue: 3.5.40(typescript@5.8.2)
|
||||||
|
|
||||||
'@intlify/bundle-utils@11.2.4(vue-i18n@11.4.8(vue@3.5.40(typescript@5.8.2)))':
|
'@intlify/bundle-utils@11.2.4(vue-i18n@11.4.8(vue@3.5.40(typescript@5.8.2)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/message-compiler': 11.4.8
|
'@intlify/message-compiler': 11.4.8
|
||||||
|
|||||||
Reference in New Issue
Block a user