修复 bug,优化界面
This commit is contained in:
@@ -26,7 +26,8 @@ services:
|
|||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
- /data/projects/source:/data/projects/source
|
- /data/projects/source:/data/projects/source
|
||||||
# 素材库上传目录:容器内 /app/uploads ↔ 宿主机 /data/project/dashboard/backend/uploads
|
# 素材库上传目录:容器内 /app/uploads ↔ 宿主机 /data/project/dashboard/frontend/dist/uploads
|
||||||
|
# (与 frontend nginx 静态直出同一目录;若该目录被删除重建,需 force-recreate backend 重新挂载)
|
||||||
- /data/project/dashboard/frontend/dist/uploads:/app/uploads
|
- /data/project/dashboard/frontend/dist/uploads:/app/uploads
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# 管理后台前端构建产物(dist 挂载到 /usr/share/nginx/html)
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# 与后端 multer 50MB 上传限制对齐,避免 /api 上传被 nginx 413 拦截
|
||||||
|
client_max_body_size 50m;
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript image/svg+xml;
|
||||||
|
|
||||||
|
# 带内容指纹的构建资源:长缓存(正则含 {},必须加引号避免被当作块定界符)
|
||||||
|
location ~* "\.[0-9a-f]{8}\.(js|css|png|jpg|jpeg|gif|webp|svg|woff2?)$" {
|
||||||
|
expires 365d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 素材库静态文件:backend 上传目录挂载在 dist/uploads,前端直接静态直出
|
||||||
|
location /uploads/ {
|
||||||
|
expires 7d;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA history 回退
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API 反代到 backend 容器(compose 网络内服务名解析)
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:8000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -261,6 +261,7 @@ const local: App.I18n.Schema = {
|
|||||||
www_product: 'Products',
|
www_product: 'Products',
|
||||||
www_product_series: 'Product Series',
|
www_product_series: 'Product Series',
|
||||||
www_product_list: 'Product List',
|
www_product_list: 'Product List',
|
||||||
|
www_product_detail: 'Product Detail',
|
||||||
www_news: 'News',
|
www_news: 'News',
|
||||||
www_news_articles: 'Articles',
|
www_news_articles: 'Articles',
|
||||||
www_media: 'Media Library',
|
www_media: 'Media Library',
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ const local: App.I18n.Schema = {
|
|||||||
www_product: '产品管理',
|
www_product: '产品管理',
|
||||||
www_product_series: '产品系列',
|
www_product_series: '产品系列',
|
||||||
www_product_list: '产品列表',
|
www_product_list: '产品列表',
|
||||||
|
www_product_detail: '产品详情页',
|
||||||
www_news: '新闻管理',
|
www_news: '新闻管理',
|
||||||
www_news_articles: '新闻文章',
|
www_news_articles: '新闻文章',
|
||||||
www_media: '素材库',
|
www_media: '素材库',
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
|||||||
"www_page_news-list": () => import("@/views/www/page/news-list/index.vue"),
|
"www_page_news-list": () => import("@/views/www/page/news-list/index.vue"),
|
||||||
"www_page_product-list": () => import("@/views/www/page/product-list/index.vue"),
|
"www_page_product-list": () => import("@/views/www/page/product-list/index.vue"),
|
||||||
www_page_support: () => import("@/views/www/page/support/index.vue"),
|
www_page_support: () => import("@/views/www/page/support/index.vue"),
|
||||||
|
www_product_detail: () => import("@/views/www/product/detail/index.vue"),
|
||||||
www_product_list: () => import("@/views/www/product/list/index.vue"),
|
www_product_list: () => import("@/views/www/product/list/index.vue"),
|
||||||
www_product_series: () => import("@/views/www/product/series/index.vue"),
|
www_product_series: () => import("@/views/www/product/series/index.vue"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -423,7 +423,7 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'www_news_articles',
|
title: 'www_news_articles',
|
||||||
i18nKey: 'route.www_news_articles',
|
i18nKey: 'route.www_news_articles',
|
||||||
icon: 'mdi:article-outline',
|
icon: 'fluent-mdl2:articles',
|
||||||
order: 1,
|
order: 1,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
@@ -524,6 +524,18 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
order: 4
|
order: 4
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
name: 'www_product_detail',
|
||||||
|
path: '/www/product/detail',
|
||||||
|
component: 'view.www_product_detail',
|
||||||
|
meta: {
|
||||||
|
title: 'www_product_detail',
|
||||||
|
i18nKey: 'route.www_product_detail',
|
||||||
|
icon: 'mdi:file-document-edit-outline',
|
||||||
|
order: 3,
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'www_product_list',
|
name: 'www_product_list',
|
||||||
path: '/www/product/list',
|
path: '/www/product/list',
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ const routeMap: RouteMap = {
|
|||||||
"www_page_support": "/www/page/support",
|
"www_page_support": "/www/page/support",
|
||||||
"www_page-seo": "/www/page-seo",
|
"www_page-seo": "/www/page-seo",
|
||||||
"www_product": "/www/product",
|
"www_product": "/www/product",
|
||||||
|
"www_product_detail": "/www/product/detail",
|
||||||
"www_product_list": "/www/product/list",
|
"www_product_list": "/www/product/list",
|
||||||
"www_product_series": "/www/product/series"
|
"www_product_series": "/www/product/series"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ declare module "@elegant-router/types" {
|
|||||||
"www_page_support": "/www/page/support";
|
"www_page_support": "/www/page/support";
|
||||||
"www_page-seo": "/www/page-seo";
|
"www_page-seo": "/www/page-seo";
|
||||||
"www_product": "/www/product";
|
"www_product": "/www/product";
|
||||||
|
"www_product_detail": "/www/product/detail";
|
||||||
"www_product_list": "/www/product/list";
|
"www_product_list": "/www/product/list";
|
||||||
"www_product_series": "/www/product/series";
|
"www_product_series": "/www/product/series";
|
||||||
};
|
};
|
||||||
@@ -152,6 +153,7 @@ declare module "@elegant-router/types" {
|
|||||||
| "www_page_news-list"
|
| "www_page_news-list"
|
||||||
| "www_page_product-list"
|
| "www_page_product-list"
|
||||||
| "www_page_support"
|
| "www_page_support"
|
||||||
|
| "www_product_detail"
|
||||||
| "www_product_list"
|
| "www_product_list"
|
||||||
| "www_product_series"
|
| "www_product_series"
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ onMounted(loadData);
|
|||||||
<ColorInput v-model:value="formData.body_color" />
|
<ColorInput v-model:value="formData.body_color" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</div>
|
</div>
|
||||||
<NFormItem v-if="formData.bg_type === 'image' || formData.bg_type === 'video'" label="背景显示模式">
|
<NFormItem v-if="formData.layout !== 'split_content' && (formData.bg_type === 'image' || formData.bg_type === 'video')" label="背景显示模式">
|
||||||
<NRadioGroup :value="(formData.config.bg_fit as string) ?? 'cover'" @update:value="v => (formData.config.bg_fit = v)">
|
<NRadioGroup :value="(formData.config.bg_fit as string) ?? 'cover'" @update:value="v => (formData.config.bg_fit = v)">
|
||||||
<NRadio v-for="opt in bgFitOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
|
<NRadio v-for="opt in bgFitOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
@@ -699,22 +699,36 @@ onMounted(loadData);
|
|||||||
<span v-if="formData.layout === 'title_media'" class="text-12px opacity-60">标题+媒体布局仅水平方向生效(左对齐/居中/右对齐),垂直位置忽略</span>
|
<span v-if="formData.layout === 'title_media'" class="text-12px opacity-60">标题+媒体布局仅水平方向生效(左对齐/居中/右对齐),垂直位置忽略</span>
|
||||||
</div>
|
</div>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<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)">
|
<div
|
||||||
<NRadio value="left">素材|文案</NRadio>
|
v-if="formData.layout === 'split_content'"
|
||||||
<NRadio value="right">文案|素材</NRadio>
|
class="grid gap-x-16px"
|
||||||
</NRadioGroup>
|
:class="formData.bg_type === 'image' || formData.bg_type === 'video' ? 'grid-cols-3' : 'grid-cols-2'"
|
||||||
</NFormItem>
|
>
|
||||||
<NFormItem v-if="formData.layout === 'split_content'" label="分栏比例">
|
<NFormItem v-if="formData.bg_type === 'image' || formData.bg_type === 'video'" label="背景显示模式" label-placement="top">
|
||||||
<div class="w-full flex flex-col gap-8px">
|
<NRadioGroup :value="(formData.config.bg_fit as string) ?? 'cover'" @update:value="v => (formData.config.bg_fit = v)">
|
||||||
<NSelect
|
<NRadio v-for="opt in bgFitOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</NRadio>
|
||||||
:value="(formData.config.split_ratio as number) ?? 50"
|
</NRadioGroup>
|
||||||
:options="splitRatioOptions"
|
</NFormItem>
|
||||||
@update:value="v => (formData.config.split_ratio = v)"
|
<NFormItem label="媒体位置" label-placement="top">
|
||||||
/>
|
<NRadioGroup :value="(formData.config.media_position as string) ?? 'left'" @update:value="v => (formData.config.media_position = v)">
|
||||||
<span class="text-12px opacity-60">按页面视觉左右顺序标注({{ formData.config.media_position === 'right' ? '文案 : 素材' : '素材 : 文案' }}),默认五五等分;移动端(小屏)始终单列堆叠不受影响</span>
|
<NRadio value="left">素材|文案</NRadio>
|
||||||
</div>
|
<NRadio value="right">文案|素材</NRadio>
|
||||||
</NFormItem>
|
</NRadioGroup>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="分栏比例" label-placement="top">
|
||||||
|
<NTooltip trigger="focus" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<NSelect
|
||||||
|
:value="(formData.config.split_ratio as number) ?? 50"
|
||||||
|
:options="splitRatioOptions"
|
||||||
|
@update:value="v => (formData.config.split_ratio = v)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
按页面视觉左右顺序标注({{ formData.config.media_position === 'right' ? '文案 : 素材' : '素材 : 文案' }}),默认五五等分;移动端(小屏)始终单列堆叠不受影响
|
||||||
|
</NTooltip>
|
||||||
|
</NFormItem>
|
||||||
|
</div>
|
||||||
<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">
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onActivated, onMounted, ref } from 'vue';
|
||||||
|
import SectionManager from '../../components/SectionManager.vue';
|
||||||
|
import { fetchGetProducts, fetchGetProductSeries } from '@/service/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品详情页区块管理
|
||||||
|
*
|
||||||
|
* 统一入口:选择产品后直接管理该产品详情页(page_type='product_detail')的所有区块,
|
||||||
|
* 复用 SectionManager(与产品列表操作列的「区块」按钮同一组件)。
|
||||||
|
*/
|
||||||
|
defineOptions({ name: 'WwwProductDetail' });
|
||||||
|
|
||||||
|
const products = ref<Api.Www.Product[]>([]);
|
||||||
|
const seriesList = ref<Api.Www.ProductSeries[]>([]);
|
||||||
|
const selectedId = ref<number | null>(null);
|
||||||
|
|
||||||
|
// 产品下拉按系列分组,标签为「中文名(英文名)」,与视觉列表一致
|
||||||
|
// 分组必须用 type: 'group'(不可选、用 key 标识),否则系列父项会被当作普通选项,
|
||||||
|
// 其 value 与产品 id 碰撞导致误选中,且子项标签无法回显
|
||||||
|
const productOptions = ref<
|
||||||
|
Array<{ type: 'group'; label: string; key: string; children: Array<{ label: string; value: number }> }>
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
async function loadAll() {
|
||||||
|
const [seriesRes, productRes] = await Promise.all([
|
||||||
|
fetchGetProductSeries(),
|
||||||
|
fetchGetProducts({ skip: 0, limit: 500 })
|
||||||
|
]);
|
||||||
|
if (seriesRes.error || productRes.error || !productRes.data) return;
|
||||||
|
|
||||||
|
seriesList.value = seriesRes.data?.items || [];
|
||||||
|
products.value = productRes.data.items || [];
|
||||||
|
|
||||||
|
const groups: typeof productOptions.value = [];
|
||||||
|
for (const s of seriesList.value) {
|
||||||
|
const children = products.value.filter(p => p.series_id === s.id);
|
||||||
|
if (children.length === 0) continue;
|
||||||
|
groups.push({
|
||||||
|
type: 'group',
|
||||||
|
key: `series-${s.id}`,
|
||||||
|
label: `${s.name_zh}(${s.name_en})`,
|
||||||
|
children: children.map(p => ({ label: `${p.name_zh}(${p.name_en})`, value: p.id }))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 不属于任何已有系列的产品兜底归入「未分组」
|
||||||
|
const grouped = new Set(groups.flatMap(g => g.children.map(c => c.value)));
|
||||||
|
const orphans = products.value.filter(p => !grouped.has(p.id));
|
||||||
|
if (orphans.length > 0) {
|
||||||
|
groups.push({
|
||||||
|
type: 'group',
|
||||||
|
key: 'series-none',
|
||||||
|
label: '未分组',
|
||||||
|
children: orphans.map(p => ({ label: `${p.name_zh}(${p.name_en})`, value: p.id }))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
productOptions.value = groups;
|
||||||
|
|
||||||
|
// 默认选中第一个产品;已有选中且仍存在则保持
|
||||||
|
if (selectedId.value === null || !products.value.some(p => p.id === selectedId.value)) {
|
||||||
|
selectedId.value = products.value[0]?.id ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onActivated(loadAll);
|
||||||
|
onMounted(loadAll);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px">
|
||||||
|
<NCard :bordered="false" class="card-wrapper" size="small">
|
||||||
|
<div class="flex items-center gap-12px">
|
||||||
|
<span class="shrink-0 font-medium">选择产品</span>
|
||||||
|
<NSelect
|
||||||
|
v-model:value="selectedId"
|
||||||
|
:options="productOptions"
|
||||||
|
placeholder="请选择要管理详情页区块的产品"
|
||||||
|
filterable
|
||||||
|
class="w-360px"
|
||||||
|
/>
|
||||||
|
<NButton @click="loadAll">
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon icon="mdi:refresh" />
|
||||||
|
</template>
|
||||||
|
刷新
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
|
<!-- 切换产品时通过 :key 销毁重建 SectionManager(其数据加载在 onMounted,不监听 productId 变化) -->
|
||||||
|
<SectionManager
|
||||||
|
v-if="selectedId"
|
||||||
|
:key="selectedId"
|
||||||
|
page-type="product_detail"
|
||||||
|
:product-id="selectedId"
|
||||||
|
title="详情页区块"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NCard v-else :bordered="false" class="card-wrapper" size="small">
|
||||||
|
<NEmpty description="暂无产品,请先在「产品列表」中创建产品" />
|
||||||
|
</NCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -17,7 +17,7 @@ const { overline, title, subtitle, content, overlineStyle, titleStyle, bodyStyle
|
|||||||
<p v-if="subtitle" class="mt-20px text-lg md:text-xl" :class="section.theme === 'dark' ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
<p v-if="subtitle" class="mt-20px text-lg md:text-xl" :class="section.theme === 'dark' ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="content" class="rich-text mt-16px text-15px leading-relaxed" :class="section.theme === 'dark' ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
<div v-if="content" class="rich-text mt-16px text-16px leading-relaxed" :class="section.theme === 'dark' ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||||
<SectionCta :section="section" class="mt-32px justify-center" />
|
<SectionCta :section="section" class="mt-32px justify-center" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const mediaClass = computed(() =>
|
|||||||
<p v-if="subtitle" class="text-lg md:text-xl" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
<p v-if="subtitle" class="text-lg md:text-xl" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="content" class="rich-text text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
<div v-if="content" class="rich-text text-16px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||||
<SectionCta :section="section" class="mt-8px" />
|
<SectionCta :section="section" class="mt-8px" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function onLeave(el: Element, done: () => void) {
|
|||||||
<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 v-if="content" class="rich-text mt-20px text-16px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 参数分组:左分组标题 + 右参数卡片 -->
|
<!-- 参数分组:左分组标题 + 右参数卡片 -->
|
||||||
@@ -107,10 +107,10 @@ function onLeave(el: Element, done: () => void) {
|
|||||||
class="flex items-baseline justify-between gap-24px py-16px border-b"
|
class="flex items-baseline justify-between gap-24px py-16px border-b"
|
||||||
:class="[isDark ? 'border-white/10' : 'border-gray-300 dark:border-dark-600', idx === fixedItems(group).length - 1 && (!needToggle(group) || !isExpanded(group)) ? 'border-b-0' : '']"
|
: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 font-semibold w-20% shrink-0" :class="isDark ? 'text-gray-200' : 'text-gray-800 dark:text-gray-200'">
|
<span class="text-16px 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 }}
|
{{ isEn ? item.name_en : item.name_zh }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex-1 text-sm text-left whitespace-pre-line" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
<span class="flex-1 text-16px text-left whitespace-pre-line" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||||
{{ isEn ? item.value_en : item.value_zh }}
|
{{ isEn ? item.value_en : item.value_zh }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,10 +124,10 @@ function onLeave(el: Element, done: () => void) {
|
|||||||
class="flex items-baseline justify-between gap-24px py-16px border-b"
|
class="flex items-baseline justify-between gap-24px py-16px border-b"
|
||||||
:class="[isDark ? 'border-white/10' : 'border-gray-300 dark:border-dark-600', item.id === group.items?.at(-1)?.id ? 'border-b-0' : '']"
|
:class="[isDark ? 'border-white/10' : 'border-gray-300 dark:border-dark-600', item.id === group.items?.at(-1)?.id ? 'border-b-0' : '']"
|
||||||
>
|
>
|
||||||
<span class="text-sm font-semibold w-20% shrink-0" :class="isDark ? 'text-gray-200' : 'text-gray-800 dark:text-gray-200'">
|
<span class="text-16px 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 }}
|
{{ isEn ? item.name_en : item.name_zh }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex-1 text-sm text-left whitespace-pre-line" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
<span class="flex-1 text-16px text-left whitespace-pre-line" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||||
{{ isEn ? item.value_en : item.value_zh }}
|
{{ isEn ? item.value_en : item.value_zh }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const textWrapClass = computed(() =>
|
|||||||
<p v-if="subtitle" class="text-lg" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
<p v-if="subtitle" class="text-lg" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="content" class="rich-text text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
<div v-if="content" class="rich-text text-16px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||||
<SectionCta :section="section" class="mt-8px" />
|
<SectionCta :section="section" class="mt-8px" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const hAlign = computed(() => {
|
|||||||
<p v-if="subtitle" class="text-lg md:text-xl" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
<p v-if="subtitle" class="text-lg md:text-xl" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="content" class="rich-text text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
<div v-if="content" class="rich-text text-16px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 媒体:视频优先,其次图片;填满内容容器(宽度跟随「内容区宽度」配置),高度按素材比例自适应 -->
|
<!-- 媒体:视频优先,其次图片;填满内容容器(宽度跟随「内容区宽度」配置),高度按素材比例自适应 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user