feat: production页的CMS变更

This commit is contained in:
2025-10-15 16:49:08 +08:00
parent 98f978484c
commit 1704a7b5c1
5 changed files with 202 additions and 94 deletions

View File

@ -6,18 +6,19 @@
class="document-card" class="document-card"
> >
<div class="document-info"> <div class="document-info">
<h3>{{ doc.caption || doc.name }}</h3> <h3>{{ doc.title }}</h3>
<div class="document-content"> <div class="document-content">
<span v-if="doc.size" class="document-meta" <span v-if="doc.size" class="document-meta"
>大小: {{ formatFileSize(doc.size) }} >大小: {{ formatFileSize(doc.size) }}
</span> </span>
<span v-if="doc.ext" class="document-meta" <span v-if="doc.filename" class="document-meta"
>格式: {{ formatFileExtension(doc.ext) }}</span >格式:
{{ formatFileExtension(getFileExtension(doc.filename)) }}</span
> >
<el-button <el-button
class="download-button" class="download-button"
type="primary" type="primary"
@click="handleDownload(doc.name, doc.url)" @click="handleDownload(doc.title, doc.url)"
> >
下载 下载
</el-button> </el-button>
@ -30,7 +31,7 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps({ defineProps({
documents: { documents: {
type: Array as () => Array<StrapiMedia>, type: Array as () => Array<ProductDocumentView>,
default: () => [], default: () => [],
}, },
}); });

View File

@ -3,9 +3,9 @@
<el-collapse class="question-collapse" accordion> <el-collapse class="question-collapse" accordion>
<el-collapse-item <el-collapse-item
v-for="question in questions" v-for="question in questions"
:key="question.documentId" :key="question.id"
:title="question.title" :title="question.title"
:name="question.documentId" :name="question.id"
> >
<markdown-renderer :content="question.content || ''" /> <markdown-renderer :content="question.content || ''" />
</el-collapse-item> </el-collapse-item>
@ -16,11 +16,7 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps({ defineProps({
questions: { questions: {
type: Array as () => Array<{ type: Array as PropType<QuestionView[]>,
title: string;
content: string;
documentId: string;
}>,
default: () => [], default: () => [],
}, },
}); });

View File

@ -3,15 +3,15 @@
<el-collapse v-model="activeName"> <el-collapse v-model="activeName">
<el-collapse-item <el-collapse-item
v-for="item in data" v-for="item in data"
:key="item.title" :key="item.name"
:title="item.title" :title="item.name"
:name="item.title" :name="item.name"
> >
<el-descriptions :column="1" border> <el-descriptions :column="1" border>
<el-descriptions-item <el-descriptions-item
v-for="subItem in item.items" v-for="subItem in item.specs"
:key="subItem.label" :key="subItem.key"
:label="subItem.label" :label="subItem.value"
> >
{{ subItem.value }} {{ subItem.value }}
</el-descriptions-item> </el-descriptions-item>
@ -24,15 +24,15 @@
<script lang="ts" setup> <script lang="ts" setup>
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object as () => ProductionSpecGroup[], type: Object as () => ProductSpecGroupView[],
required: true, required: true,
}, },
}); });
// 默认全部展开 // 默认全部展开
const activeName = ref<string[]>( const activeName = ref<string[]>(
props.data.map((item: ProductionSpecGroup) => { props.data.map((item: ProductSpecGroupView) => {
return item.title; return item.name;
}) || [] }) || []
); );
</script> </script>

View File

@ -15,7 +15,7 @@
}}</NuxtLink> }}</NuxtLink>
</el-breadcrumb-item> </el-breadcrumb-item>
<el-breadcrumb-item class="text-md opactiy-50">{{ <el-breadcrumb-item class="text-md opactiy-50">{{
production.title production.name
}}</el-breadcrumb-item> }}</el-breadcrumb-item>
</el-breadcrumb> </el-breadcrumb>
@ -23,9 +23,9 @@
<div class="production-header"> <div class="production-header">
<div class="production-image"> <div class="production-image">
<el-image <el-image
v-if="production.production_images.length <= 1" v-if="production.images.length <= 1"
:src="useStrapiMedia(production?.cover?.url || '')" :src="getImageUrl(production.images[0].image)"
:alt="production.title" :alt="production.name"
fit="contain" fit="contain"
/> />
<el-carousel <el-carousel
@ -37,13 +37,13 @@
arrow="always" arrow="always"
> >
<el-carousel-item <el-carousel-item
v-for="(item, index) in production.production_images || []" v-for="(item, index) in production.images || []"
:key="index" :key="index"
> >
<div class="production-carousel-item"> <div class="production-carousel-item">
<el-image <el-image
:src="useStrapiMedia(item.url || '')" :src="getImageUrl(item.image || '')"
:alt="item.alternativeText || production.title" :alt="production.name"
fit="contain" fit="contain"
lazy lazy
/> />
@ -56,7 +56,7 @@
</div> </div>
<div class="production-info"> <div class="production-info">
<h1>{{ production.title }}</h1> <h1>{{ production.name }}</h1>
<p class="summary">{{ production.summary }}</p> <p class="summary">{{ production.summary }}</p>
</div> </div>
</div> </div>
@ -65,24 +65,16 @@
<div class="production-content"> <div class="production-content">
<el-tabs v-model="activeName" class="production-tabs" stretch> <el-tabs v-model="activeName" class="production-tabs" stretch>
<el-tab-pane label="产品详情" name="details"> <el-tab-pane label="产品详情" name="details">
<markdown-renderer <markdown-renderer :content="production.description || ''" />
:content="production.production_details || ''"
/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="技术规格" name="specs"> <el-tab-pane label="技术规格" name="specs">
<spec-table :data="production.production_specs" /> <spec-table :data="production.specs" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="常见问题" name="faq"> <el-tab-pane label="常见问题" name="faq">
<question-list :questions="production.questions" /> <question-list :questions="production.faqs" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="相关文档" name="documents"> <el-tab-pane label="相关文档" name="documents">
<document-list <document-list :documents="production.documents" />
:documents="
production.production_documents.map(
(item) => item.document
) || []
"
/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
@ -114,40 +106,130 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { readItem } from '@directus/sdk';
const route = useRoute(); const route = useRoute();
const { findOne } = useStrapi();
const { getStrapiLocale } = useLocalizations(); const { getStrapiLocale } = useLocalizations();
const strapiLocale = getStrapiLocale(); const strapiLocale = getStrapiLocale();
const { $directus } = useNuxtApp();
const { getImageUrl } = useDirectusImage();
// 获取路由参数slug 或 id // 获取路由参数slug 或 id
const documentId = computed(() => route.params.slug as string); const documentId = computed(() => route.params.slug as string);
const { data, pending, error } = useAsyncData( const { data, pending, error } = await useAsyncData(
() => `production-${documentId.value}`, () => `production-${documentId.value}`,
() => () =>
findOne<Production>('productions', documentId.value, { $directus.request(
populate: { readItem('products', documentId.value, {
production_specs: { fields: [
populate: '*', 'id',
{ translations: ['id', 'name', 'summary', 'description'] },
{
images: [
'id',
{
product_images_id: [
'id',
'image',
{ translations: ['id', 'caption'] },
],
},
],
},
{
specs: [
'id',
{
translations: ['*'],
},
{
specs: [
'id',
{
translations: ['id', 'key'],
},
'value',
],
},
],
},
{
faqs: [
'id',
{
questions_id: [
'id',
{
translations: ['id', 'title', 'content'],
},
],
},
],
},
{
documents: [
'id',
{
product_documents_id: [
'id',
{
file: ['id', 'filesize', 'filename_download'],
},
{
translations: ['id', 'title'],
},
],
},
],
},
],
deep: {
translations: {
_filter: {
languages_code: { _eq: strapiLocale },
},
},
images: {
product_images_id: {
translations: {
_filter: {
languages_code: { _eq: strapiLocale },
},
},
},
},
faqs: {
questions_id: {
translations: {
_filter: {
languages_code: { _eq: strapiLocale },
},
},
},
},
documents: {
documents_id: {
translations: {
_filter: {
languages_code: { _eq: strapiLocale },
},
},
},
},
}, },
production_images: { })
populate: '*', )
},
cover: {
populate: '*',
},
questions: {
populate: '*',
},
production_documents: {
populate: 'document',
},
},
locale: strapiLocale,
})
); );
const production = computed(() => data.value?.data ?? null); console.log('Raw Data: ', data.value);
const rawProduction = computed(() => data.value ?? null);
const production = computed(() => {
return toProductView(rawProduction.value);
});
console.log('View Data: ', production.value);
const activeName = ref('details'); // 默认选中概览标签 const activeName = ref('details'); // 默认选中概览标签
@ -159,7 +241,7 @@
// SEO // SEO
useHead({ useHead({
title: computed(() => production.value?.title || 'Product Detail'), title: computed(() => production.value?.name || 'Product Detail'),
meta: [ meta: [
{ {
name: 'description', name: 'description',

View File

@ -27,10 +27,10 @@
<div class="group-list"> <div class="group-list">
<production-card <production-card
v-for="production in group" v-for="production in group"
:key="production.documentId || production.id" :key="production.id"
:slug="production.documentId" :slug="production.id.toString()"
:image-url="useStrapiMedia(production?.cover?.url || '')" :image-url="getImageUrl(production.cover.toString())"
:name="production.title" :name="production.name"
:description="production.summary || ''" :description="production.summary || ''"
/> />
</div> </div>
@ -45,29 +45,47 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { find } = useStrapi(); import { readItems } from '@directus/sdk';
const { getStrapiLocale } = useLocalizations(); const { getStrapiLocale } = useLocalizations();
const strapiLocale = getStrapiLocale(); const strapiLocale = getStrapiLocale();
const { $directus } = useNuxtApp();
const { getImageUrl } = useDirectusImage();
const { data, pending, error } = useAsyncData( const { data, pending, error } = useAsyncData(
'productions', 'products',
() => () =>
find<Production>('productions', { $directus.request(
populate: { readItems('products', {
cover: { fields: [
populate: '*', 'id',
{ translations: ['id', 'name', 'summary'] },
'cover',
{
product_type: [
'id',
{
translations: ['id', 'name'],
},
],
},
],
deep: {
translations: {
_filter: {
languages_code: { _eq: strapiLocale },
},
},
product_type: {
translations: {
_filter: {
languages_code: { _eq: strapiLocale },
},
},
},
}, },
production_type: { })
populate: '*', ),
},
},
filters: {
show_in_production_list: {
$eq: true,
},
},
locale: strapiLocale,
}),
{ {
lazy: true, lazy: true,
} }
@ -75,26 +93,31 @@
const activeNames = ref<string[]>([]); const activeNames = ref<string[]>([]);
const productions = computed(() => data.value?.data ?? []); // const productions = computed(() => data.value?.data ?? []);
const productionsRaw = computed(() => data.value ?? []);
const productions = computed(() =>
productionsRaw.value.map((item) => toProductListView(item))
);
// 按类型分组 // 按类型分组
// 兼容 production_type 既可能为对象也可能为字符串 // 兼容 production_type 既可能为对象也可能为字符串
const groupedProductions = computed(() => { const groupedProductions = computed(() => {
const groups: Record<string, Production[]> = {}; const groups: Record<string, ProductListView[]> = {};
for (const prod of productions.value) { for (const prod of productions.value) {
let typeKey = ''; let typeKey = '';
if (typeof prod.production_type === 'string') { if (typeof prod.product_type === 'string') {
typeKey = prod.production_type; typeKey = prod.product_type;
} else if ( } else if (
prod.production_type && prod.product_type &&
typeof prod.production_type === 'object' && typeof prod.product_type === 'object' &&
'type' in prod.production_type 'name' in prod.product_type
) { ) {
typeKey = prod.production_type.type || ''; typeKey = prod.product_type || '';
} }
if (!groups[typeKey]) groups[typeKey] = []; if (!groups[typeKey]) groups[typeKey] = [];
groups[typeKey]?.push(prod); groups[typeKey]?.push(prod);
} }
console.log(groups);
return groups; return groups;
}); });
@ -113,7 +136,13 @@
} }
}); });
onMounted(() => { watch(pending, (value) => {
if (!value) {
console.log('AsyncData: ', data.value);
}
});
onMounted(async () => {
if (groupedProductions.value) { if (groupedProductions.value) {
activeNames.value = [ activeNames.value = [
...Object.keys(groupedProductions.value), ...Object.keys(groupedProductions.value),