diff --git a/package.json b/package.json index 4aa8d0e..b546bbd 100755 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "commit:comment": "引导设置规范化的提交信息", "commit": "git-cz", "husky": "npx husky install", - "husky:add": "npx husky add .husky/pre-commit 'npx --no-install lint-staged' & npx husky add .husky/commit-msg 'npx --no-install commitlint --edit \"$1\"'" + "husky:add": "npx husky add .husky/pre-commit 'npx --no-install lint-staged' & npx husky add .husky/commit-msg 'npx --no-install commitlint --edit \"$1\"'", + "export:decoration-schema": "node scripts/export-decoration-schema.mjs ../ecshopx-web/app/decoration-engine/schema/decoration-schema.json", + "lint:decoration-contract": "node scripts/check-decoration-contract.mjs && npm run export:decoration-schema" }, "dependencies": { "@antv/g2plot": "^2.4.31", diff --git a/scripts/check-decoration-contract.mjs b/scripts/check-decoration-contract.mjs new file mode 100644 index 0000000..ddb62b8 --- /dev/null +++ b/scripts/check-decoration-contract.mjs @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict' +import { copyFileSync, mkdirSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { pathToFileURL } from 'node:url' + +const ROOT = process.cwd() +const tempDir = path.join(tmpdir(), 'ecx-decoration-contract') +const tempSchemaPath = path.join(tempDir, `schema-${process.pid}.mjs`) +mkdirSync(tempDir, { recursive: true }) +copyFileSync( + path.resolve(ROOT, 'src/components/sp-web-decoration/definitions/schema.js'), + tempSchemaPath +) +const schemaUrl = pathToFileURL(tempSchemaPath).href + +const { + blockFields, + blockTypeAliases, + sectionFields, + sectionTypeAliases, + toDefaultSettings +} = await import(schemaUrl) +rmSync(tempSchemaPath, { force: true }) + +const EXPECTED_SECTIONS = [ + 'header', + 'footer', + 'announcement-bar', + 'main-carousel', + 'image-hotspot', + 'product-shelf', + 'product-tab-shelf', + 'native-product-list' +] + +const EXPECTED_BLOCKS = [ + 'announcement', + 'header_product_list', + 'header_collection_product_list', + 'mega_menu', + 'image', + 'video', + 'hotspot', + 'product-tab', + 'footer-link', + 'footer-menu', + 'footer-image', + 'footer-text' +] + +function hasOwn(object, key) { + return Object.prototype.hasOwnProperty.call(object || {}, key) +} + +function assertFields(groupName, schemas, expectedTypes) { + assert.deepEqual( + Object.keys(schemas).sort(), + expectedTypes.slice().sort(), + `${groupName} schema type list drifted` + ) + + expectedTypes.forEach((type) => { + const fields = schemas[type] + assert.ok(fields && typeof fields === 'object', `${groupName} ${type} missing fields`) + Object.entries(fields).forEach(([name, spec]) => { + assert.ok(spec && typeof spec === 'object', `${groupName} ${type}.${name} missing spec`) + assert.ok(typeof spec.type === 'string', `${groupName} ${type}.${name} missing type`) + assert.ok(hasOwn(spec, 'default'), `${groupName} ${type}.${name} missing default`) + if (spec.type === 'enum') { + assert.ok(Array.isArray(spec.values), `${groupName} ${type}.${name} enum missing values`) + assert.ok( + spec.values.includes(spec.default), + `${groupName} ${type}.${name} default is not in enum values` + ) + } + ;(spec.aliases || []).forEach((alias) => { + assert.notEqual(alias, name, `${groupName} ${type}.${name} has self alias`) + assert.ok( + !hasOwn(fields, alias), + `${groupName} ${type}.${name} alias ${alias} is also a canonical field` + ) + }) + }) + }) +} + +function assertNoAliasDefaults(groupName, schemas) { + Object.entries(schemas).forEach(([type, fields]) => { + const defaults = toDefaultSettings(fields) + Object.entries(fields).forEach(([name, spec]) => { + assert.ok(hasOwn(defaults, name), `${groupName} ${type}.${name} missing default output`) + ;(spec.aliases || []).forEach((alias) => { + assert.ok( + !hasOwn(defaults, alias), + `${groupName} ${type} defaultSettings contains alias ${alias}` + ) + }) + }) + }) +} + +assertFields('section', sectionFields, EXPECTED_SECTIONS) +assertFields('block', blockFields, EXPECTED_BLOCKS) +assertNoAliasDefaults('section', sectionFields) +assertNoAliasDefaults('block', blockFields) + +assert.deepEqual(sectionTypeAliases, { carousel: 'main-carousel' }) +assert.deepEqual(blockTypeAliases, {}) +assert.deepEqual(sectionFields['product-shelf'].displayMode.aliases, ['sourceMode']) +assert.deepEqual(blockFields.image.pc_image.aliases, ['imageUrl']) +assert.deepEqual(blockFields.image.mobile_image.aliases, ['imageUrl']) +assert.ok(blockFields['footer-menu'].menu) +assert.ok(blockFields['footer-menu'].menu_items) +assert.equal(blockFields['footer-menu'].menu.aliases, undefined) +assert.ok(!blockFields['footer-link'].url.aliases?.includes('link')) + +console.log('Decoration contract checks passed') diff --git a/scripts/export-decoration-schema.mjs b/scripts/export-decoration-schema.mjs new file mode 100644 index 0000000..103e5e9 --- /dev/null +++ b/scripts/export-decoration-schema.mjs @@ -0,0 +1,26 @@ +import { copyFileSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { pathToFileURL } from 'node:url' + +const root = process.cwd() +const tempDir = path.join(tmpdir(), 'ecx-decoration-contract') +const tempSchemaPath = path.join(tempDir, `schema-${process.pid}.mjs`) +mkdirSync(tempDir, { recursive: true }) +copyFileSync( + path.resolve(root, 'src/components/sp-web-decoration/definitions/schema.js'), + tempSchemaPath +) +const schemaUrl = pathToFileURL(tempSchemaPath).href +const { createDecorationSchema } = await import(schemaUrl) +rmSync(tempSchemaPath, { force: true }) + +const outputPath = process.argv[2] +if (!outputPath) { + throw new Error('Usage: node scripts/export-decoration-schema.mjs ') +} + +const resolvedOutput = path.resolve(root, outputPath) +mkdirSync(path.dirname(resolvedOutput), { recursive: true }) +writeFileSync(resolvedOutput, `${JSON.stringify(createDecorationSchema(), null, 2)}\n`, 'utf8') +console.log(`Exported decoration schema to ${resolvedOutput}`) diff --git a/src/components/sp-web-decoration/definitions/blocks/announcement.js b/src/components/sp-web-decoration/definitions/blocks/announcement.js index 2098d86..1ce6438 100644 --- a/src/components/sp-web-decoration/definitions/blocks/announcement.js +++ b/src/components/sp-web-decoration/definitions/blocks/announcement.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'announcement', name: i18n.t('d65dbcac.fa86f1'), - defaultSettings: { + fields: blockFields.announcement, + defaultSettings: toDefaultSettings(blockFields.announcement), + legacyDefaultSettings: { text: '' } } diff --git a/src/components/sp-web-decoration/definitions/blocks/footer-image.js b/src/components/sp-web-decoration/definitions/blocks/footer-image.js index 4657e3f..26c8cb1 100644 --- a/src/components/sp-web-decoration/definitions/blocks/footer-image.js +++ b/src/components/sp-web-decoration/definitions/blocks/footer-image.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'footer-image', name: '图片', - defaultSettings: { + fields: blockFields['footer-image'], + defaultSettings: toDefaultSettings(blockFields['footer-image']), + legacyDefaultSettings: { image: '', alignment: 'left', width: '100', diff --git a/src/components/sp-web-decoration/definitions/blocks/footer-link.js b/src/components/sp-web-decoration/definitions/blocks/footer-link.js index b00635b..35742f9 100644 --- a/src/components/sp-web-decoration/definitions/blocks/footer-link.js +++ b/src/components/sp-web-decoration/definitions/blocks/footer-link.js @@ -1,8 +1,11 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'footer-link', name: i18n.t('d65dbcac.406baf'), - defaultSettings: { + fields: blockFields['footer-link'], + defaultSettings: toDefaultSettings(blockFields['footer-link']), + legacyDefaultSettings: { label: '', url: '/help' } diff --git a/src/components/sp-web-decoration/definitions/blocks/footer-menu.js b/src/components/sp-web-decoration/definitions/blocks/footer-menu.js index 26f8666..cd33caa 100644 --- a/src/components/sp-web-decoration/definitions/blocks/footer-menu.js +++ b/src/components/sp-web-decoration/definitions/blocks/footer-menu.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'footer-menu', name: '菜单', - defaultSettings: { + fields: blockFields['footer-menu'], + defaultSettings: toDefaultSettings(blockFields['footer-menu']), + legacyDefaultSettings: { title: '', menu: null, size: 'small', diff --git a/src/components/sp-web-decoration/definitions/blocks/footer-text.js b/src/components/sp-web-decoration/definitions/blocks/footer-text.js index bf393f4..11b6117 100644 --- a/src/components/sp-web-decoration/definitions/blocks/footer-text.js +++ b/src/components/sp-web-decoration/definitions/blocks/footer-text.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'footer-text', name: '文本', - defaultSettings: { + fields: blockFields['footer-text'], + defaultSettings: toDefaultSettings(blockFields['footer-text']), + legacyDefaultSettings: { title: '', content: '', title_size: 'medium', diff --git a/src/components/sp-web-decoration/definitions/blocks/header-collection-product-list.js b/src/components/sp-web-decoration/definitions/blocks/header-collection-product-list.js index 8b04be0..48e8de7 100644 --- a/src/components/sp-web-decoration/definitions/blocks/header-collection-product-list.js +++ b/src/components/sp-web-decoration/definitions/blocks/header-collection-product-list.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'header_collection_product_list', name: '产品系列菜单', - defaultSettings: { + fields: blockFields.header_collection_product_list, + defaultSettings: toDefaultSettings(blockFields.header_collection_product_list), + legacyDefaultSettings: { title: '', link: '', columns_desktop: 4, diff --git a/src/components/sp-web-decoration/definitions/blocks/header-product-list.js b/src/components/sp-web-decoration/definitions/blocks/header-product-list.js index f2528bd..13abfa1 100644 --- a/src/components/sp-web-decoration/definitions/blocks/header-product-list.js +++ b/src/components/sp-web-decoration/definitions/blocks/header-product-list.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'header_product_list', name: '产品菜单', - defaultSettings: { + fields: blockFields.header_product_list, + defaultSettings: toDefaultSettings(blockFields.header_product_list), + legacyDefaultSettings: { title: '', link: '', columns_desktop: 4, diff --git a/src/components/sp-web-decoration/definitions/blocks/hotspot.js b/src/components/sp-web-decoration/definitions/blocks/hotspot.js index 456431f..a46e89b 100644 --- a/src/components/sp-web-decoration/definitions/blocks/hotspot.js +++ b/src/components/sp-web-decoration/definitions/blocks/hotspot.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'hotspot', name: i18n.t('d65dbcac.50da72'), - defaultSettings: { + fields: blockFields.hotspot, + defaultSettings: toDefaultSettings(blockFields.hotspot), + legacyDefaultSettings: { label: '', x: 0, y: 0, diff --git a/src/components/sp-web-decoration/definitions/blocks/image.js b/src/components/sp-web-decoration/definitions/blocks/image.js index a55d9ea..57f708f 100644 --- a/src/components/sp-web-decoration/definitions/blocks/image.js +++ b/src/components/sp-web-decoration/definitions/blocks/image.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'image', name: '图片', - defaultSettings: { + fields: blockFields.image, + defaultSettings: toDefaultSettings(blockFields.image), + legacyDefaultSettings: { pc_image: '', mobile_image: '', introduction: '提供背景或介绍,吸引用户注意', diff --git a/src/components/sp-web-decoration/definitions/blocks/mega-menu.js b/src/components/sp-web-decoration/definitions/blocks/mega-menu.js index 09bab5c..bc9aa03 100644 --- a/src/components/sp-web-decoration/definitions/blocks/mega-menu.js +++ b/src/components/sp-web-decoration/definitions/blocks/mega-menu.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'mega_menu', name: '超级菜单', - defaultSettings: { + fields: blockFields.mega_menu, + defaultSettings: toDefaultSettings(blockFields.mega_menu), + legacyDefaultSettings: { mega_menu_item: '', alignment: 'page', image_position: 'right', diff --git a/src/components/sp-web-decoration/definitions/blocks/product-tab.js b/src/components/sp-web-decoration/definitions/blocks/product-tab.js index 9d4293c..3e35710 100644 --- a/src/components/sp-web-decoration/definitions/blocks/product-tab.js +++ b/src/components/sp-web-decoration/definitions/blocks/product-tab.js @@ -1,7 +1,11 @@ +import { blockFields, toDefaultSettings } from '../schema.js' + export default { type: 'product-tab', name: '产品系列', - defaultSettings: { + fields: blockFields['product-tab'], + defaultSettings: toDefaultSettings(blockFields['product-tab']), + legacyDefaultSettings: { tab_label: '产品系列', product_ids: [], limit: 8 diff --git a/src/components/sp-web-decoration/definitions/blocks/video.js b/src/components/sp-web-decoration/definitions/blocks/video.js index c560ed7..192d035 100644 --- a/src/components/sp-web-decoration/definitions/blocks/video.js +++ b/src/components/sp-web-decoration/definitions/blocks/video.js @@ -1,9 +1,12 @@ import { i18n } from '@/i18n' +import { blockFields, toDefaultSettings } from '../schema.js' export default { type: 'video', name: i18n.t('bdd52c70.7fcf42'), - defaultSettings: { + fields: blockFields.video, + defaultSettings: toDefaultSettings(blockFields.video), + legacyDefaultSettings: { videoUrl: '', posterUrl: '', mobileVideoUrl: '', diff --git a/src/components/sp-web-decoration/definitions/registry.js b/src/components/sp-web-decoration/definitions/registry.js index f28850a..4f73aa8 100644 --- a/src/components/sp-web-decoration/definitions/registry.js +++ b/src/components/sp-web-decoration/definitions/registry.js @@ -19,6 +19,14 @@ import footerLinkBlock from './blocks/footer-link.js' import footerMenuBlock from './blocks/footer-menu.js' import footerImageBlock from './blocks/footer-image.js' import footerTextBlock from './blocks/footer-text.js' +import { + blockTypeAliases, + getBlockFields, + getSectionFields, + resolveBlockType, + resolveSectionType, + sectionTypeAliases +} from './schema.js' const sectionDefinitions = { [headerSection.type]: headerSection, @@ -53,11 +61,11 @@ function getSectionTypesByScope(scope) { } export function getTypedSectionDefinition(type) { - return sectionDefinitions[type] || null + return sectionDefinitions[resolveSectionType(type)] || null } export function getTypedBlockDefinition(type) { - return blockDefinitions[type] || null + return blockDefinitions[resolveBlockType(type)] || null } export function getSectionDefaultSettings(type) { @@ -68,6 +76,14 @@ export function getBlockDefaultSettings(type) { return getTypedBlockDefinition(type)?.defaultSettings || null } +export function getSectionSchemaFields(type) { + return getSectionFields(type) +} + +export function getBlockSchemaFields(type) { + return getBlockFields(type) +} + export function getAllowedBlockTypes(sectionType) { return getTypedSectionDefinition(sectionType)?.blocks?.allowed?.slice() || [] } @@ -80,7 +96,15 @@ export function getSectionBlockTypes(sectionType) { return getAllowedBlockTypes(sectionType) } -export { sectionDefinitions, blockDefinitions, getSectionTypesByScope } +export { + sectionDefinitions, + blockDefinitions, + sectionTypeAliases, + blockTypeAliases, + getSectionTypesByScope, + resolveSectionType, + resolveBlockType +} export default { sectionDefinitions, @@ -89,8 +113,12 @@ export default { getTypedBlockDefinition, getSectionDefaultSettings, getBlockDefaultSettings, + getSectionSchemaFields, + getBlockSchemaFields, getAllowedBlockTypes, getSectionTypes, getSectionBlockTypes, - getSectionTypesByScope + getSectionTypesByScope, + resolveSectionType, + resolveBlockType } diff --git a/src/components/sp-web-decoration/definitions/schema.js b/src/components/sp-web-decoration/definitions/schema.js new file mode 100644 index 0000000..81b1a23 --- /dev/null +++ b/src/components/sp-web-decoration/definitions/schema.js @@ -0,0 +1,296 @@ +function field(type, defaultValue, extra = {}) { + return { + type, + default: defaultValue, + ...extra + } +} + +export const sectionTypeAliases = { + carousel: 'main-carousel' +} + +export const blockTypeAliases = {} + +export const sectionFields = { + header: { + color_mode: field('enum', 'dark', { values: ['light', 'dark'] }), + full_width: field('boolean', false), + sticky_header_type: field('string', 'none'), + padding_top: field('string', 'xxs'), + menu_color_style: field('string', 'pure'), + menu_type_desktop: field('string', 'dropdown'), + show_line_separator: field('boolean', true), + menu: field('object', null), + color_scheme: field('string', 'scheme-1'), + layout: field('string', 'middle'), + margin_bottom: field('number', 0), + enable_country_selector: field('boolean', true), + mobile_logo_position: field('string', 'center'), + menu_color_mode: field('enum', 'dark', { values: ['light', 'dark'] }), + logo_position: field('string', 'center'), + enable_customer_avatar: field('boolean', true), + padding_bottom: field('string', 'xxs'), + menu_color_scheme: field('string', 'scheme-1'), + enable_language_selector: field('boolean', true) + }, + footer: { + content_alignment: field('string', 'center'), + title: field('string', ''), + copyright: field('string', ''), + newsletter_heading: field('string', ''), + color_mode: field('enum', 'dark', { values: ['light', 'dark'] }), + margin_top: field('number', 48), + full_width: field('boolean', false), + padding_top: field('string', 'm'), + color_scheme: field('string', 'scheme-3'), + show_social: field('boolean', true), + payment_enable: field('boolean', true), + newsletter_enable: field('boolean', false), + enable_country_selector: field('boolean', true), + show_policy: field('boolean', true), + padding_bottom: field('string', 'xs'), + enable_brand_information: field('boolean', true), + enable_language_selector: field('boolean', true) + }, + 'announcement-bar': { + full_width: field('boolean', false), + auto_rotate: field('boolean', false), + enable_country_selector: field('boolean', false), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + show_line_separator: field('boolean', true), + change_slides_speed: field('number', 5), + color_scheme: field('string', 'scheme-1'), + padding_top: field('string', 'none'), + padding_bottom: field('string', 'none'), + show_social: field('boolean', false), + enable_language_selector: field('boolean', false) + }, + 'main-carousel': { + image_height: field('enum', 'medium', { values: ['small', 'medium', 'large', 'adapt'] }), + height: field('string', ''), + paginate_type: field('enum', 'point', { values: ['point', 'counter', 'number'] }), + paginate_size: field('enum', 'medium', { values: ['small', 'medium', 'large'] }), + enable_auto_play: field('boolean', false), + enable_arrow: field('boolean', true), + showDots: field('boolean', true), + full_width: field('boolean', false), + interval: field('number', 5), + show_text_below: field('boolean', false), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + color_scheme: field('string', 'scheme-1'), + padding_top: field('string', 'm'), + padding_bottom: field('string', 'm') + }, + 'image-hotspot': { + pc_image: field('string', '', { aliases: ['imageUrl', 'image_url', 'image'] }), + mobile_image: field('string', '', { aliases: ['mobileImage', 'mobile_image_url'] }), + pc_hotspots: field('array', []), + mobile_hotspots: field('array', []), + full_width: field('boolean', false), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + color_scheme: field('string', 'scheme-1'), + padding_top: field('string', 'm'), + padding_bottom: field('string', 'm') + }, + 'product-shelf': { + title: field('string', ''), + displayMode: field('enum', 'manual', { + values: ['category', 'manual'], + aliases: ['sourceMode'] + }), + itemIds: field('string[]', []), + categoryId: field('string', ''), + columns: field('number', 4), + limit: field('number', 8), + showPrice: field('boolean', true), + showAddCart: field('boolean', true), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + color_scheme: field('string', 'scheme-1'), + padding_top: field('string', 'm'), + padding_bottom: field('string', 'm'), + full_width: field('boolean', false) + }, + 'product-tab-shelf': { + title: field('string', ''), + intro: field('string', '新品'), + size: field('enum', 'medium', { + values: ['xsmall', 'small', 'medium', 'large', 'xlarge'] + }), + alignment: field('enum', 'center', { values: ['left', 'center', 'right'] }), + columns: field('number', 4), + spacing: field('enum', 'medium', { values: ['none', 'small', 'medium', 'large'] }), + full_width: field('boolean', false), + show_price: field('boolean', true), + show_add_cart: field('boolean', true), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + color_scheme: field('string', 'scheme-1'), + padding_top: field('string', 'm'), + padding_bottom: field('string', 'm') + }, + 'native-product-list': { + locked: field('boolean', true) + } +} + +export const blockFields = { + announcement: { + text: field('string', '') + }, + header_product_list: { + title: field('string', ''), + link: field('string', ''), + columns_desktop: field('number', 4), + full_width: field('boolean', false), + show_discount: field('boolean', false), + image_ratio: field('string', 'square'), + show_secondary_image: field('boolean', false), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + color_style: field('string', 'pure') + }, + header_collection_product_list: { + title: field('string', ''), + link: field('string', ''), + columns_desktop: field('number', 4), + full_width: field('boolean', false), + show_discount: field('boolean', false), + image_ratio: field('string', 'adapt'), + show_secondary_image: field('boolean', false), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + color_style: field('string', 'pure') + }, + mega_menu: { + mega_menu_item: field('string', ''), + alignment: field('string', 'page'), + image_position: field('string', 'right'), + image_length: field('string', 'one'), + enable_image_1_link: field('boolean', false), + enable_image_2_link: field('boolean', false), + enable_image_3_link: field('boolean', false), + image_layout: field('string', 'outside'), + image_content_alignment: field('string', 'center'), + image_ratio: field('number', 1), + image_shape: field('string', 'none'), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + color_style: field('string', 'pure') + }, + image: { + pc_image: field('string', '', { aliases: ['imageUrl'] }), + mobile_image: field('string', '', { aliases: ['imageUrl'] }), + introduction: field('string', '提供背景或介绍,吸引用户注意'), + introduction_size: field('string', 's'), + heading: field('string', '关键主题或焦点,简明直接'), + heading_size: field('string', 'm'), + description: field('string', '简要概述幻灯片内容,突出重点'), + description_size: field('string', 's'), + button_text: field('string', '按钮文字'), + button_size: field('string', 'large'), + button_style: field('string', 'primary'), + button_scheme: field('string', 'brand'), + content_alignment: field('string', 'left'), + content_layout: field('string', 'top-left'), + image_opacity: field('number', 20), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + enable_card: field('boolean', false), + color_style: field('string', 'pure') + }, + video: { + videoUrl: field('string', ''), + posterUrl: field('string', ''), + mobileVideoUrl: field('string', ''), + introduction: field('string', '
1231231
'), + introduction_size: field('string', 'l'), + heading: field('string', '关键主题或焦点,简明直接'), + heading_size: field('string', 'm'), + description: field('string', '简要概述幻灯片内容,突出重点'), + description_size: field('string', 'm'), + button_text: field('string', '按钮文字'), + button_size: field('string', 'small'), + button_style: field('string', 'primary'), + button_scheme: field('string', 'brand'), + content_alignment: field('string', 'left'), + content_layout: field('string', 'middle-left'), + image_opacity: field('number', 15), + color_mode: field('enum', 'light', { values: ['light', 'dark'] }), + enable_card: field('boolean', true), + color_style: field('string', 'pure') + }, + hotspot: { + label: field('string', ''), + x: field('number', 0), + y: field('number', 0), + link: field('string', ''), + shape: field('enum', 'circle', { values: ['circle', 'rect'] }) + }, + 'product-tab': { + tab_label: field('string', '产品系列'), + product_ids: field('string[]', []), + product_snapshots: field('array', []), + limit: field('number', 8) + }, + 'footer-link': { + label: field('string', ''), + url: field('string', '/help') + }, + 'footer-menu': { + title: field('string', ''), + menu: field('object', null), + menu_items: field('array', []), + size: field('string', 'small'), + alignment: field('string', 'left'), + column_span: field('number', 3) + }, + 'footer-image': { + image: field('string', ''), + alignment: field('string', 'left'), + width: field('string', '100'), + radius: field('string', 'medium'), + column_span: field('number', 4) + }, + 'footer-text': { + title: field('string', ''), + content: field('string', ''), + title_size: field('string', 'medium'), + content_size: field('string', 'small'), + alignment: field('string', 'left'), + column_span: field('number', 6) + } +} + +export function cloneValue(value) { + return value === undefined ? undefined : JSON.parse(JSON.stringify(value)) +} + +export function toDefaultSettings(fields = {}) { + return Object.entries(fields).reduce((settings, [name, spec]) => { + settings[name] = cloneValue(spec.default) + return settings + }, {}) +} + +export function resolveSectionType(type) { + return sectionTypeAliases[type] || type +} + +export function resolveBlockType(type) { + return blockTypeAliases[type] || type +} + +export function getSectionFields(type) { + return sectionFields[resolveSectionType(type)] || null +} + +export function getBlockFields(type) { + return blockFields[resolveBlockType(type)] || null +} + +export function createDecorationSchema() { + return { + sections: cloneValue(sectionFields), + blocks: cloneValue(blockFields), + aliases: { + sections: cloneValue(sectionTypeAliases), + blocks: cloneValue(blockTypeAliases) + } + } +} diff --git a/src/components/sp-web-decoration/definitions/sections/announcement-bar.js b/src/components/sp-web-decoration/definitions/sections/announcement-bar.js index defef2b..24594fe 100644 --- a/src/components/sp-web-decoration/definitions/sections/announcement-bar.js +++ b/src/components/sp-web-decoration/definitions/sections/announcement-bar.js @@ -1,10 +1,13 @@ import { i18n } from '@/i18n' +import { sectionFields, toDefaultSettings } from '../schema.js' export default { type: 'announcement-bar', scope: 'header', name: i18n.t('d65dbcac.3921b6'), - defaultSettings: { + fields: sectionFields['announcement-bar'], + defaultSettings: toDefaultSettings(sectionFields['announcement-bar']), + legacyDefaultSettings: { full_width: false, auto_rotate: false, enable_country_selector: false, diff --git a/src/components/sp-web-decoration/definitions/sections/carousel.js b/src/components/sp-web-decoration/definitions/sections/carousel.js index a907560..3080e26 100644 --- a/src/components/sp-web-decoration/definitions/sections/carousel.js +++ b/src/components/sp-web-decoration/definitions/sections/carousel.js @@ -1,10 +1,13 @@ import { i18n } from '@/i18n' +import { sectionFields, toDefaultSettings } from '../schema.js' export default { type: 'main-carousel', scope: 'template', name: i18n.t('d65dbcac.0c0180'), - defaultSettings: { + fields: sectionFields['main-carousel'], + defaultSettings: toDefaultSettings(sectionFields['main-carousel']), + legacyDefaultSettings: { image_height: 'medium', paginate_type: 'point', paginate_size: 'medium', diff --git a/src/components/sp-web-decoration/definitions/sections/footer.js b/src/components/sp-web-decoration/definitions/sections/footer.js index 531566a..877c8e8 100644 --- a/src/components/sp-web-decoration/definitions/sections/footer.js +++ b/src/components/sp-web-decoration/definitions/sections/footer.js @@ -1,11 +1,14 @@ import { i18n } from '@/i18n' +import { sectionFields, toDefaultSettings } from '../schema.js' export default { type: 'footer', scope: 'footer', fixedId: 'footer', name: i18n.t('d65dbcac.4eb88f'), - defaultSettings: { + fields: sectionFields.footer, + defaultSettings: toDefaultSettings(sectionFields.footer), + legacyDefaultSettings: { content_alignment: 'center', title: '', copyright: '', diff --git a/src/components/sp-web-decoration/definitions/sections/header.js b/src/components/sp-web-decoration/definitions/sections/header.js index ede3669..a9137b5 100644 --- a/src/components/sp-web-decoration/definitions/sections/header.js +++ b/src/components/sp-web-decoration/definitions/sections/header.js @@ -1,11 +1,14 @@ import { i18n } from '@/i18n' +import { sectionFields, toDefaultSettings } from '../schema.js' export default { type: 'header', scope: 'header', fixedId: 'header', name: i18n.t('d65dbcac.917f14'), - defaultSettings: { + fields: sectionFields.header, + defaultSettings: toDefaultSettings(sectionFields.header), + legacyDefaultSettings: { color_mode: 'dark', full_width: false, sticky_header_type: 'none', diff --git a/src/components/sp-web-decoration/definitions/sections/image-hotspot.js b/src/components/sp-web-decoration/definitions/sections/image-hotspot.js index 4a798c4..7cf7a21 100644 --- a/src/components/sp-web-decoration/definitions/sections/image-hotspot.js +++ b/src/components/sp-web-decoration/definitions/sections/image-hotspot.js @@ -1,10 +1,13 @@ import { i18n } from '@/i18n' +import { sectionFields, toDefaultSettings } from '../schema.js' export default { type: 'image-hotspot', scope: 'template', name: i18n.t('d65dbcac.081a81'), - defaultSettings: { + fields: sectionFields['image-hotspot'], + defaultSettings: toDefaultSettings(sectionFields['image-hotspot']), + legacyDefaultSettings: { pc_image: '', mobile_image: '', pc_hotspots: [], diff --git a/src/components/sp-web-decoration/definitions/sections/native-product-list.js b/src/components/sp-web-decoration/definitions/sections/native-product-list.js index 61d9866..238f2d5 100644 --- a/src/components/sp-web-decoration/definitions/sections/native-product-list.js +++ b/src/components/sp-web-decoration/definitions/sections/native-product-list.js @@ -1,3 +1,5 @@ +import { sectionFields, toDefaultSettings } from '../schema.js' + export default { type: 'native-product-list', scope: 'template', @@ -6,7 +8,9 @@ export default { native: true, locked: true, addable: false, - defaultSettings: { + fields: sectionFields['native-product-list'], + defaultSettings: toDefaultSettings(sectionFields['native-product-list']), + legacyDefaultSettings: { locked: true }, defaultBlocks: [], diff --git a/src/components/sp-web-decoration/definitions/sections/product-shelf.js b/src/components/sp-web-decoration/definitions/sections/product-shelf.js index efd0b52..d9a2aad 100644 --- a/src/components/sp-web-decoration/definitions/sections/product-shelf.js +++ b/src/components/sp-web-decoration/definitions/sections/product-shelf.js @@ -1,10 +1,13 @@ import { i18n } from '@/i18n' +import { sectionFields, toDefaultSettings } from '../schema.js' export default { type: 'product-shelf', scope: 'template', name: i18n.t('d65dbcac.4a02ad'), - defaultSettings: { + fields: sectionFields['product-shelf'], + defaultSettings: toDefaultSettings(sectionFields['product-shelf']), + legacyDefaultSettings: { title: '', sourceMode: 'manual', itemIds: [], diff --git a/src/components/sp-web-decoration/definitions/sections/product-tab-shelf.js b/src/components/sp-web-decoration/definitions/sections/product-tab-shelf.js index 3a91ac3..de4955e 100644 --- a/src/components/sp-web-decoration/definitions/sections/product-tab-shelf.js +++ b/src/components/sp-web-decoration/definitions/sections/product-tab-shelf.js @@ -1,8 +1,12 @@ +import { sectionFields, toDefaultSettings } from '../schema.js' + export default { type: 'product-tab-shelf', scope: 'template', name: 'Tab产品系列', - defaultSettings: { + fields: sectionFields['product-tab-shelf'], + defaultSettings: toDefaultSettings(sectionFields['product-tab-shelf']), + legacyDefaultSettings: { title: '', intro: '新品', size: 'medium', diff --git a/src/components/sp-web-decoration/panels/FooterImageBlockPanel.vue b/src/components/sp-web-decoration/panels/FooterImageBlockPanel.vue index 8e31a21..a6922d0 100644 --- a/src/components/sp-web-decoration/panels/FooterImageBlockPanel.vue +++ b/src/components/sp-web-decoration/panels/FooterImageBlockPanel.vue @@ -3,14 +3,14 @@
图片
- +
对齐方式
@@ -23,7 +23,7 @@
图片宽度
@@ -37,7 +37,7 @@
圆角
@@ -53,7 +53,7 @@
占列宽度
import SpImagePicker from '@/components/sp-image-picker/index.vue' +import { normalizeTypedBlockSettings } from '../utils/panelState.js' export default { name: 'FooterImageBlockPanel', @@ -87,6 +88,11 @@ export default { default: () => ({}) } }, + computed: { + settings() { + return normalizeTypedBlockSettings('footer-image', this.value) + } + }, methods: { updateField(field, fieldValue) { this.$emit('change', { diff --git a/src/components/sp-web-decoration/panels/FooterLinkBlockPanel.vue b/src/components/sp-web-decoration/panels/FooterLinkBlockPanel.vue index efd37bb..d35a76b 100644 --- a/src/components/sp-web-decoration/panels/FooterLinkBlockPanel.vue +++ b/src/components/sp-web-decoration/panels/FooterLinkBlockPanel.vue @@ -1,16 +1,18 @@