官网开发 0804
This commit is contained in:
+13
-1
@@ -22,10 +22,22 @@ const head = useLocaleHead({
|
||||
seo: true
|
||||
});
|
||||
|
||||
// Favicon(来自 dashboard 站点信息,站点数据异步加载完成后动态注入;/uploads/... 相对路径已由数据层解析)
|
||||
const faviconHref = computed(() => siteStore.site?.favicon_url || '');
|
||||
const faviconType = computed(() => {
|
||||
if (faviconHref.value.endsWith('.svg')) return 'image/svg+xml';
|
||||
if (faviconHref.value.endsWith('.png')) return 'image/png';
|
||||
if (faviconHref.value.endsWith('.ico')) return 'image/x-icon';
|
||||
return '';
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: () => (locale.value === 'zh' ? zhCN.app.name : enUS.app.name),
|
||||
htmlAttrs: head.value.htmlAttrs,
|
||||
link: [...(head.value.link || [])],
|
||||
link: () => [
|
||||
...(head.value.link || []),
|
||||
...(faviconHref.value ? [{ rel: 'icon' as const, type: faviconType.value || undefined, href: faviconHref.value }] : [])
|
||||
],
|
||||
meta: [...(head.value.meta || [])]
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavItem } from '~/types/site';
|
||||
import type { NavItem, Product } from '~/types/site';
|
||||
|
||||
const { t, locale, locales } = useI18n();
|
||||
const localePath = useLocalePath();
|
||||
@@ -60,9 +60,16 @@ function openMega() {
|
||||
function closeMega() {
|
||||
megaTimer = setTimeout(() => {
|
||||
megaOpen.value = false;
|
||||
hoveredProduct.value = null;
|
||||
}, 120);
|
||||
}
|
||||
|
||||
// Mega Menu 右侧大图预览:hover 某产品时展示其封面,默认展示第一个产品
|
||||
const hoveredProduct = ref<Product | null>(null);
|
||||
const previewProduct = computed<Product | null>(
|
||||
() => hoveredProduct.value ?? productSeries.value.find(s => s.products?.length)?.products[0] ?? null
|
||||
);
|
||||
|
||||
// 移动端:关闭抽屉
|
||||
function closeMobile() {
|
||||
appStore.toggleMobileMenu(false);
|
||||
@@ -112,31 +119,42 @@ function closeMobile() {
|
||||
v-if="megaOpen"
|
||||
class="absolute top-64px left-50% -translate-x-50% min-w-640px bg-white/95 backdrop-blur-xl rounded-12px shadow-xl border border-gray-100 p-24px dark:bg-dark-900/95 dark:border-dark-700"
|
||||
>
|
||||
<div class="flex gap-40px">
|
||||
<div v-for="series in productSeries" :key="series.id" class="min-w-200px">
|
||||
<p class="text-12px font-semibold text-gray-400 mb-12px uppercase tracking-wide">
|
||||
{{ locale === 'en' ? series.name_en : series.name_zh }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-8px">
|
||||
<NuxtLink
|
||||
v-for="product in series.products"
|
||||
:key="product.id"
|
||||
:to="localePath(`/products/${product.slug}`)"
|
||||
class="flex items-center gap-12px p-8px rounded-8px hover:bg-gray-50 transition-colors dark:hover:bg-dark-800"
|
||||
@click="megaOpen = false"
|
||||
>
|
||||
<img
|
||||
v-if="product.cover_url"
|
||||
:src="product.cover_url"
|
||||
:alt="product.name_zh"
|
||||
class="w-48px h-36px object-cover rounded-6px bg-gray-100"
|
||||
/>
|
||||
<span class="text-sm text-gray-800 dark:text-gray-100">
|
||||
{{ locale === 'en' ? product.name_en : product.name_zh }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
<div class="flex gap-32px">
|
||||
<!-- 左侧:系列/产品列表 -->
|
||||
<div class="flex gap-40px">
|
||||
<div v-for="series in productSeries" :key="series.id" class="min-w-200px">
|
||||
<p class="text-12px font-semibold text-gray-400 mb-12px uppercase tracking-wide">
|
||||
{{ locale === 'en' ? series.name_en : series.name_zh }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-8px">
|
||||
<NuxtLink
|
||||
v-for="product in series.products"
|
||||
:key="product.id"
|
||||
:to="localePath(`/products/${product.slug}`)"
|
||||
class="p-8px rounded-8px hover:bg-gray-50 transition-colors dark:hover:bg-dark-800"
|
||||
@click="megaOpen = false"
|
||||
@mouseenter="hoveredProduct = product"
|
||||
>
|
||||
<span class="text-sm text-gray-800 dark:text-gray-100">
|
||||
{{ locale === 'en' ? product.name_en : product.name_zh }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:hover 产品封面大图预览 -->
|
||||
<div v-if="previewProduct" class="w-360px shrink-0 border-l border-gray-100 pl-32px dark:border-dark-700">
|
||||
<img
|
||||
v-if="previewProduct.cover_url"
|
||||
:src="previewProduct.cover_url"
|
||||
:alt="locale === 'en' ? previewProduct.name_en : previewProduct.name_zh"
|
||||
class="w-full h-240px object-contain rounded-8px bg-gray-50 dark:bg-dark-800"
|
||||
/>
|
||||
<p class="mt-12px text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ locale === 'en' ? previewProduct.name_en : previewProduct.name_zh }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
import type { SectionBlock, SectionLayout } from '~/types/site';
|
||||
import { resolveAssetUrl } from '~/utils/resolveAssetUrl';
|
||||
import LayoutHeroSplit from './layouts/LayoutHeroSplit.vue';
|
||||
import LayoutFullBanner from './layouts/LayoutFullBanner.vue';
|
||||
import LayoutFeatureGrid from './layouts/LayoutFeatureGrid.vue';
|
||||
@@ -46,26 +48,57 @@ const bgStyle = computed(() => {
|
||||
|
||||
const hasBgImage = computed(() => props.section.bg_type === 'image' && !!props.section.bg_value);
|
||||
const hasBgVideo = computed(() => props.section.bg_type === 'video' && !!props.section.bg_value);
|
||||
// 背景图/视频资源地址(支持 /uploads/... 相对路径)
|
||||
const bgMediaUrl = computed(() => resolveAssetUrl(props.section.bg_value));
|
||||
|
||||
// 区块宽度百分比(config.width_percent):占视口宽度比例并居中,
|
||||
// 未配置或 100 时全宽;md 以下始终全宽(见模板 class)
|
||||
const sectionWidthStyle = computed<CSSProperties>(() => {
|
||||
const w = Number(props.section.config?.width_percent);
|
||||
return { '--sec-w': w > 0 && w < 100 ? `${w}%` : '100%' } as CSSProperties;
|
||||
});
|
||||
|
||||
// 背景图/视频显示模式(config.bg_fit):cover 铺满(默认)/ contain 完整显示 / auto 原尺寸
|
||||
const bgFit = computed<'cover' | 'contain' | 'auto'>(() => {
|
||||
const f = props.section.config?.bg_fit;
|
||||
return f === 'contain' || f === 'auto' ? f : 'cover';
|
||||
});
|
||||
|
||||
const bgImageClass = computed(() =>
|
||||
({
|
||||
cover: 'absolute inset-0 bg-cover bg-center',
|
||||
contain: 'absolute inset-0 bg-contain bg-center bg-no-repeat',
|
||||
auto: 'absolute inset-0 bg-auto bg-center bg-no-repeat'
|
||||
})[bgFit.value]
|
||||
);
|
||||
|
||||
const bgVideoClass = computed(() =>
|
||||
({
|
||||
cover: 'absolute inset-0 w-full h-full object-cover',
|
||||
contain: 'absolute inset-0 w-full h-full object-contain',
|
||||
auto: 'absolute inset-0 w-full h-full object-none'
|
||||
})[bgFit.value]
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="relative overflow-hidden"
|
||||
class="relative overflow-hidden md:w-[var(--sec-w)] md:mx-auto"
|
||||
:class="isDark ? 'bg-gray-950 text-white' : 'bg-white text-gray-900 dark:bg-dark-900 dark:text-white'"
|
||||
:style="bgStyle"
|
||||
:style="[bgStyle, sectionWidthStyle]"
|
||||
>
|
||||
<!-- 背景图 -->
|
||||
<!-- 背景图:显示模式由 config.bg_fit 控制(默认铺满) -->
|
||||
<div
|
||||
v-if="hasBgImage"
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ backgroundImage: `url(${section.bg_value})` }"
|
||||
:class="bgImageClass"
|
||||
:style="{ backgroundImage: `url(${bgMediaUrl})` }"
|
||||
/>
|
||||
|
||||
<!-- 背景视频 -->
|
||||
<!-- 背景视频:显示模式由 config.bg_fit 控制(默认铺满) -->
|
||||
<video
|
||||
v-if="hasBgVideo"
|
||||
:src="section.bg_value"
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
:src="bgMediaUrl"
|
||||
:class="bgVideoClass"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
|
||||
@@ -12,7 +12,7 @@ const features = computed(() => config.value.features ?? []);
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-56px">
|
||||
<p v-if="overline" class="text-13px 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 }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
|
||||
@@ -8,7 +8,7 @@ const { overline, title, subtitle, content, overlineStyle, titleStyle, bodyStyle
|
||||
|
||||
<template>
|
||||
<div class="section-container py-120px flex-col-center text-center max-w-800px">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="section.theme === 'dark' ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
||||
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="section.theme === 'dark' ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-16px text-3xl md:text-5xl font-bold leading-tight" :class="section.theme === 'dark' ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
|
||||
@@ -12,7 +12,7 @@ const images = computed(() => config.value.images ?? []);
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-56px">
|
||||
<p v-if="overline" class="text-13px 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 }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
|
||||
@@ -38,7 +38,7 @@ const hPad = computed(() => ({
|
||||
:class="[vAlign, hAlign, hPad]"
|
||||
>
|
||||
<div class="flex flex-col gap-20px max-w-560px">
|
||||
<p v-if="overline" class="text-13px 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 }}
|
||||
</p>
|
||||
<h2 class="text-3xl md:text-5xl font-bold leading-tight" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
|
||||
@@ -12,7 +12,7 @@ const steps = computed(() => config.value.steps ?? []);
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-64px">
|
||||
<p v-if="overline" class="text-13px 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 }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
|
||||
@@ -35,7 +35,7 @@ function needToggle(group: SpecGroup): boolean {
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-56px">
|
||||
<p v-if="overline" class="text-13px 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 }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
|
||||
@@ -1,21 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
import { resolveAssetUrl } from '~/utils/resolveAssetUrl';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, content, mediaUrl, isDark, overlineStyle, titleStyle, bodyStyle } = useSectionContent(props.section);
|
||||
|
||||
// 分栏媒体视频(video_url 有值时优先渲染视频,支持 /uploads/... 相对路径)
|
||||
const videoUrl = computed(() => resolveAssetUrl(props.section.video_url));
|
||||
|
||||
// 媒体位置(config.media_position):left 图/视频在左(默认)/ right 在右;移动端始终文案在上
|
||||
const mediaPosition = computed(() => (props.section.config as Record<string, string> | null)?.media_position === 'right' ? 'right' : 'left');
|
||||
|
||||
const mediaWrapClass = computed(() =>
|
||||
mediaPosition.value === 'right'
|
||||
? 'flex-center order-2'
|
||||
: 'flex-center order-2 md:order-1'
|
||||
);
|
||||
|
||||
const textWrapClass = computed(() =>
|
||||
mediaPosition.value === 'right'
|
||||
? 'flex flex-col gap-20px order-1'
|
||||
: 'flex flex-col gap-20px order-1 md:order-2'
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px grid md:grid-cols-2 gap-48px items-center">
|
||||
<!-- 图片 -->
|
||||
<div v-if="mediaUrl" class="flex-center order-2 md:order-1">
|
||||
<img :src="mediaUrl" :alt="title" class="w-full max-w-520px rounded-16px object-cover shadow-lg" />
|
||||
<!-- 媒体:视频优先,其次图片 -->
|
||||
<div v-if="videoUrl || mediaUrl" :class="mediaWrapClass">
|
||||
<video
|
||||
v-if="videoUrl"
|
||||
:src="videoUrl"
|
||||
class="w-full max-w-520px rounded-16px object-cover shadow-lg"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
/>
|
||||
<img v-else :src="mediaUrl" :alt="title" class="w-full max-w-520px rounded-16px object-cover shadow-lg" />
|
||||
</div>
|
||||
|
||||
<!-- 文案 -->
|
||||
<div class="flex flex-col gap-20px order-1 md:order-2">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
||||
<div :class="textWrapClass">
|
||||
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="text-3xl md:text-4xl font-bold leading-tight" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
|
||||
@@ -12,7 +12,7 @@ const stats = computed(() => config.value.stats ?? []);
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-64px">
|
||||
<p v-if="overline" class="text-13px 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 }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
|
||||
@@ -13,7 +13,7 @@ const isEmbed = computed(() => /youtube\.com|youtu\.be|bilibili\.com|player\.bil
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-48px">
|
||||
<p v-if="overline" class="text-13px 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 }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* - code = 2:无数据
|
||||
*/
|
||||
|
||||
import { resolveAssetUrls } from '~/utils/resolveAssetUrl';
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number;
|
||||
msg: string;
|
||||
@@ -36,7 +38,8 @@ export function useRequest() {
|
||||
}
|
||||
|
||||
// code = 2(无数据)时 data 为 null,正常返回
|
||||
return res.data;
|
||||
// 统一解析 *_url / *_path 字段中的相对路径(如 /uploads/...)为完整资源地址
|
||||
return resolveAssetUrls(res.data);
|
||||
}
|
||||
|
||||
function get<T = unknown>(url: string, params?: Record<string, unknown>) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SectionBlock, SectionConfig } from '~/types/site';
|
||||
import { resolveAssetUrl } from '~/utils/resolveAssetUrl';
|
||||
|
||||
/**
|
||||
* 区块内容多语言解析
|
||||
@@ -16,7 +17,10 @@ export function useSectionContent(section: SectionBlock) {
|
||||
const content = computed(() => (isEn.value ? section.content_en : section.content_zh) || '');
|
||||
const ctaPrimaryText = computed(() => (isEn.value ? section.cta_primary_en : section.cta_primary_zh) || '');
|
||||
const ctaSecondaryText = computed(() => (isEn.value ? section.cta_secondary_en : section.cta_secondary_zh) || '');
|
||||
const mediaUrl = computed(() => (isEn.value ? section.media_url_en : section.media_url_zh) || section.media_url_zh || '');
|
||||
// media_url_zh/_en 以语言后缀结尾,不命中数据层 resolveAssetUrls 的 *_url 匹配,需在此单独解析相对路径
|
||||
const mediaUrl = computed(() =>
|
||||
resolveAssetUrl((isEn.value ? section.media_url_en : section.media_url_zh) || section.media_url_zh || '')
|
||||
);
|
||||
|
||||
const config = computed<SectionConfig>(() => section.config ?? {});
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 资源 URL 解析
|
||||
*
|
||||
* dashboard 后台填写素材地址时支持相对路径(如 /uploads/2026/07/xxx.mp4),
|
||||
* www 端渲染时自动拼接资源服务地址(NUXT_PUBLIC_ASSET_BASE_URL,
|
||||
* 未配置时默认取 apiBaseUrl 去掉 /api 后缀的 origin)。
|
||||
* 完整 URL(http(s)://、//、data: 等)保持原样。
|
||||
* 仅解析 /uploads/ 开头的素材路径;其他相对路径(如 /products/luxsin-x8)
|
||||
* 是站内路由链接(CTA 按钮等),保持原样由 NuxtLink 消费。
|
||||
*/
|
||||
|
||||
// 匹配 *_url / *_path / url 字段(如 cover_url、file_path、gallery 的 url)
|
||||
const ASSET_KEY_RE = /^url$|_(url|path)$/;
|
||||
|
||||
export function resolveAssetUrl(url?: string | null): string {
|
||||
if (!url) return '';
|
||||
// 绝对地址(协议开头 / 协议相对 // / data: / blob:)保持原样
|
||||
if (/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(url)) return url;
|
||||
// 仅解析 /uploads/ 下的素材相对路径;其他 / 开头路径是站内路由链接,不能拼后端基地址
|
||||
if (!url.startsWith('/uploads/')) return url;
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const assetBase =
|
||||
(config.public.assetBaseUrl as string) ||
|
||||
(config.public.apiBaseUrl as string).replace(/\/api\/?$/, '');
|
||||
return assetBase.replace(/\/+$/, '') + url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度遍历响应数据,将所有 *_url / *_path 字段的相对路径解析为完整地址。
|
||||
* 在 useRequest 统一调用,页面/组件无需逐个处理。
|
||||
*/
|
||||
export function resolveAssetUrls<T>(data: T): T {
|
||||
if (!data || typeof data !== 'object') return data;
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(item => resolveAssetUrls(item)) as unknown as T;
|
||||
}
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
|
||||
if (typeof value === 'string' && ASSET_KEY_RE.test(key)) {
|
||||
result[key] = resolveAssetUrl(value);
|
||||
} else if (value && typeof value === 'object') {
|
||||
result[key] = resolveAssetUrls(value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
Reference in New Issue
Block a user