refactor: 项目结构重构 #48

Manually merged
remilia merged 10 commits from refactor/components into master 2025-10-29 17:56:08 +08:00
41 changed files with 1293 additions and 1126 deletions

View File

@ -0,0 +1,83 @@
<template>
<el-card shadow="hover" class="p-4">
<template #header>
<div class="header-content">
<el-icon class="header-icon"><ElIconDocument /></el-icon>
<span class="truncate font-medium">{{ file.filename_download }}</span>
</div>
</template>
<dl class="text-gray-600 space-y-1 mb-6">
<div>
<dt class="font-semibold inline">类型</dt>
<dd class="inline">{{ file.type }}</dd>
</div>
<div>
<dt class="font-semibold inline">大小</dt>
<dd class="inline">{{ formatFileSize(file.filesize) }}</dd>
</div>
<div>
<dt class="font-semibold inline">上传时间</dt>
<dd class="inline">
{{ new Date(file.uploaded_on).toLocaleDateString() }}
</dd>
</div>
</dl>
<template #footer>
<div class="button-group">
<el-button type="primary" @click="handleDownload">下载</el-button>
<el-button v-if="file.previewable" @click="handlePreview"
>预览</el-button
>
</div>
</template>
</el-card>
</template>
<script setup lang="ts">
const props = defineProps({
fileId: {
type: String,
required: true,
},
file: {
type: Object as PropType<FileMeta>,
required: true,
},
});
const localePath = useLocalePath();
const router = useRouter();
function handleDownload() {
const link = document.createElement('a');
link.href = `/api/download/${props.fileId}`;
link.download = props.file?.filename_download ?? 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function handlePreview() {
router.push(localePath(`/preview/${props.fileId}`));
}
</script>
<style scoped>
.header-content {
display: flex;
align-items: center;
gap: 3px;
margin-bottom: 4px;
}
.header-icon {
font-size: 1.5rem;
margin-right: 0.5rem;
}
.button-group {
display: flex;
justify-content: flex-end;
align-items: baseline;
gap: 1rem;
}
</style>

View File

@ -0,0 +1,86 @@
<template>
<section v-if="!pending" class="carousel-section">
<el-carousel
class="homepage-carousel"
height="auto"
:interval="5000"
arrow="never"
autoplay
>
<el-carousel-item v-for="(item, index) in carousel" :key="index">
<div class="carousel-item">
<el-image
class="carousel-image"
:src="getImageUrl(item)"
:alt="`Carousel Image ${index + 1}`"
fit="contain"
lazy
/>
</div>
</el-carousel-item>
</el-carousel>
</section>
<section v-else>
<el-skeleton :rows="5" animated />
</section>
</template>
<script setup lang="ts">
const props = defineProps({
homepageData: {
type: Object as PropType<HomepageView>,
default: null,
},
pending: {
type: Boolean,
},
});
const { getImageUrl } = useDirectusImage();
const carousel = computed(() => props.homepageData?.carousel || []);
</script>
<style scoped>
.carousel-section {
padding: 0;
}
.homepage-carousel .el-carousel__item {
width: 100%;
height: 33vw;
/* 16:9 Aspect Ratio */
}
.el-carousel__item h3 {
display: flex;
color: #475669;
opacity: 0.8;
line-height: 300px;
margin: 0;
}
.homepage-carousel .carousel-item {
width: 100%;
height: 100%;
background-color: #f5f7fa;
}
.carousel-image {
position: relative;
width: 100%;
height: 100%;
}
.carousel-image-caption {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 5px 10px;
border-radius: 5px;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<section class="homepage-section">
<h2>推荐产品</h2>
<p>
探索我们的精选产品满足您的各种需求无论是创新技术还是经典设计我们都为您提供优质选择
</p>
<div v-if="!pending">
<el-carousel
class="recommend-carousel"
height="auto"
arrow="never"
indicator-position="outside"
:autoplay="false"
>
<el-carousel-item
v-for="n in Math.floor(products.length / 3) + 1"
:key="n"
class="recommend-list"
>
<div class="recommend-card-group">
<el-card
v-for="(item, index) in products.slice((n - 1) * 3, n * 3)"
:key="index"
class="recommend-card"
@click="handleProductCardClick(item.id.toString() || '')"
>
<template #header>
<el-image
:src="getImageUrl(item.cover)"
:alt="item.name"
fit="cover"
lazy
/>
</template>
<div class="recommend-card-body">
<!-- Title -->
<div class="text-center">
<span class="recommend-card-title">{{ item.name }}</span>
</div>
<!-- Description -->
<div class="recommend-card-description text-left opacity-25">
{{ item.summary }}
</div>
</div>
</el-card>
</div>
</el-carousel-item>
</el-carousel>
</div>
<div v-else>
<el-skeleton :rows="4" animated />
</div>
</section>
</template>
<script setup lang="ts">
const props = defineProps({
homepageData: {
type: Object as PropType<HomepageView>,
default: null,
},
pending: {
type: Boolean,
},
});
const { getImageUrl } = useDirectusImage();
const products = computed(() => props.homepageData?.recommendProducts || []);
const handleProductCardClick = (documentId: string) => {
// 使用路由导航到产品详情页
if (documentId) {
const localePath = useLocalePath();
const router = useRouter();
router.push(localePath(`/products/${documentId}`));
}
};
</script>
<style scoped>
section {
padding: 2rem;
}
section h2 {
color: var(--el-text-color-primary);
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
section p {
color: var(--el-text-color-regular);
line-height: 1.6;
}
.homepage-section {
max-width: 1200px;
margin: 0 auto;
}
.recommend-carousel :deep(.el-carousel__button) {
/* 指示器按钮样式 */
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #475669;
transition: all 0.3s ease;
}
.homepage-section {
max-width: 1200px;
margin: 0 auto;
}
.recommend-carousel :deep(.el-carousel__button) {
/* 指示器按钮样式 */
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #475669;
transition: all 0.3s ease;
}
.recommend-list {
display: flex;
padding: 1rem;
height: 400px;
}
.recommend-card-group {
display: flex;
gap: 1rem;
width: 100%;
margin: 0 auto;
height: 100%;
}
.recommend-card {
width: 33%;
transition: all 0.3s ease;
text-align: center;
}
.recommend-card :deep(.el-card__header) {
padding: 0;
border: none;
}
.recommend-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
cursor: pointer;
}
.recommend-card-body {
margin: 10px auto;
padding: 0px auto;
}
.recommend-card-title {
font-size: 1rem;
font-weight: 600;
}
.recommend-card-description {
font-size: 0.8rem;
margin-top: 5px;
}
.recommend-card .el-image {
width: 100%;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<section class="homepage-section">
<h2>推荐解决方案</h2>
<p>了解我们的定制解决方案帮助您优化业务流程提高效率</p>
<div v-if="!pending">
<el-carousel
class="recommend-carousel"
height="auto"
arrow="never"
indicator-position="outside"
:autoplay="false"
>
<el-carousel-item
v-for="n in Math.floor(solutions.length / 3) + 1"
:key="n"
class="recommend-list"
>
<div class="recommend-card-group">
<el-card
v-for="(item, index) in solutions.slice((n - 1) * 3, n * 3)"
:key="index"
class="recommend-card"
@click="handleSolutionCardClick(item.id.toString() || '')"
>
<template #header>
<el-image
:src="getImageUrl(item.cover)"
:alt="item.title"
fit="cover"
lazy
/>
</template>
<div class="recommend-card-body">
<!-- Title -->
<div class="text-center">
<span class="recommend-card-title">{{ item.title }}</span>
</div>
<!-- Description -->
<div class="recommend-card-description text-left opacity-25">
{{ item.summary }}
</div>
</div>
</el-card>
</div>
</el-carousel-item>
</el-carousel>
</div>
<div v-else>
<el-skeleton :rows="4" animated />
</div>
</section>
</template>
<script setup lang="ts">
const props = defineProps({
homepageData: {
type: Object as PropType<HomepageView>,
default: null,
},
pending: {
type: Boolean,
},
});
const { getImageUrl } = useDirectusImage();
const solutions = computed(
() => props.homepageData?.recommendSolutions || []
);
const handleSolutionCardClick = (documentId: string) => {
// 使用路由导航到产品详情页
if (documentId) {
const localePath = useLocalePath();
const router = useRouter();
router.push(localePath(`/solutions/${documentId}`));
}
};
</script>
<style scoped>
section {
padding: 2rem;
}
section h2 {
color: var(--el-text-color-primary);
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
section p {
color: var(--el-text-color-regular);
line-height: 1.6;
}
.homepage-section {
max-width: 1200px;
margin: 0 auto;
}
.recommend-carousel :deep(.el-carousel__button) {
/* 指示器按钮样式 */
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #475669;
transition: all 0.3s ease;
}
.homepage-section {
max-width: 1200px;
margin: 0 auto;
}
.recommend-carousel :deep(.el-carousel__button) {
/* 指示器按钮样式 */
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #475669;
transition: all 0.3s ease;
}
.recommend-list {
display: flex;
padding: 1rem;
height: 400px;
}
.recommend-card-group {
display: flex;
gap: 1rem;
width: 100%;
margin: 0 auto;
height: 100%;
}
.recommend-card {
width: 33%;
transition: all 0.3s ease;
text-align: center;
}
.recommend-card :deep(.el-card__header) {
padding: 0;
border: none;
}
.recommend-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
cursor: pointer;
}
.recommend-card-body {
margin: 10px auto;
padding: 0px auto;
}
.recommend-card-title {
font-size: 1rem;
font-weight: 600;
}
.recommend-card-description {
font-size: 0.8rem;
margin-top: 5px;
}
.recommend-card .el-image {
width: 100%;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div class="product-detail">
<el-tabs v-model="activeName" class="product-tabs" stretch>
<el-tab-pane label="产品详情" name="details">
<markdown-renderer :content="product.description || ''" />
</el-tab-pane>
<el-tab-pane label="技术规格" name="specs">
<spec-table :data="product.specs" />
</el-tab-pane>
<el-tab-pane label="常见问题" name="faq">
<question-list :questions="product.faqs" />
</el-tab-pane>
<el-tab-pane label="相关文档" name="documents">
<document-list :documents="product.documents" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
defineProps({
product: {
type: Object as PropType<ProductView>,
required: true,
},
});
const activeName = ref('details'); // 默认选中概览标签
</script>
<style scoped>
.product-tabs ::v-deep(.el-tabs__nav) {
min-width: 30%;
float: right;
}
.product-tabs ::v-deep(.el-tabs__content) {
padding: 10px;
}
.product-detail h2 {
color: var(--el-text-color-primary);
margin: 0;
}
</style>

View File

@ -0,0 +1,102 @@
<template>
<div class="product-header">
<div class="product-image">
<el-image
v-if="product.images.length <= 1"
:src="getImageUrl(product.images[0].image)"
:alt="product.name"
fit="contain"
/>
<el-carousel
v-else
class="product-carousel"
height="500px"
:autoplay="false"
:loop="false"
arrow="always"
>
<el-carousel-item
v-for="(item, index) in product.images || []"
:key="index"
>
<div class="product-carousel-item">
<el-image
:src="getImageUrl(item.image || '')"
:alt="product.name"
fit="contain"
lazy
/>
<p v-if="item.caption" class="product-image-caption">
{{ item.caption }}
</p>
</div>
</el-carousel-item>
</el-carousel>
</div>
<div class="product-info">
<h1>{{ product.name }}</h1>
<p class="summary">{{ product.summary }}</p>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
product: {
type: Object as PropType<ProductView>,
required: true,
},
});
const { getImageUrl } = useDirectusImage();
</script>
<style scoped>
.product-header {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 3rem;
}
.product-image .el-image {
position: relative;
width: 100%;
height: 500px;
border-radius: 8px;
}
.product-image-caption {
position: absolute;
bottom: 10px;
/* left: 10%; */
background-color: rgba(0, 0, 0, 0.2);
border-radius: 5px;
padding: 5px 10px;
text-align: center;
color: white;
font-size: 0.8rem;
}
.product-carousel :deep(.el-carousel__button) {
/* 指示器按钮样式 */
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #475669;
transition: all 0.3s ease;
}
.product-info h1 {
margin-top: 2rem;
margin-bottom: 1rem;
font-size: 2rem;
}
.summary {
color: var(--el-color-info);
font-size: 1rem;
line-height: 1.6;
margin-bottom: 2rem;
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<div class="search-header">
<h1 class="page-title">{{ $t('search.title') }}</h1>
<div class="search-bar">
<el-input
v-model="keyword"
class="search-input"
:placeholder="$t('search-placeholder')"
:prefix-icon="Search"
clearable
@keyup.enter="navigateToQuery(keyword)"
@clear="handleClear"
/>
<el-button
type="primary"
class="search-button"
@click="navigateToQuery(keyword)"
>
{{ $t('search.search-button') }}
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue';
const keyword = defineModel<string>({ default: '' });
const localePath = useLocalePath();
const router = useRouter();
const navigateToQuery = (value: string) => {
const trimmed = value.trim();
if (!trimmed) return;
navigateTo({
path: localePath('/search'),
query: { query: trimmed },
});
};
const handleClear = () => {
keyword.value = '';
router.replace(localePath({ path: '/search' }));
};
</script>
<style scoped>
.search-header {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.page-title {
font-size: 2.25rem;
font-weight: 600;
color: var(--el-text-color-primary);
}
.search-bar {
display: flex;
gap: 1rem;
align-items: center;
font-size: 16px;
}
.search-input {
flex: 1;
height: 50px;
}
.search-button {
height: 50px;
width: 100px;
font-size: 16px;
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<el-tabs v-model="activeTab">
<el-tab-pane
v-for="tab in tabs"
:key="tab.name"
:label="`${tab.label}(${resultCount[tab.name] || 0})`"
:name="tab.name"
>
<SearchResults
v-model:current-page="currentPage"
:search-items="searchItems"
:category="tab.name === 'all' ? undefined : tab.name"
/>
</el-tab-pane>
</el-tabs>
</template>
<script setup lang="ts">
const props = defineProps<{
// resultCount: Record<string, number>;
searchItems: SearchItemView[];
}>();
const tabs = [
{ name: 'all', label: '全部' },
{ name: 'product', label: '产品' },
{ name: 'solution', label: '解决方案' },
{ name: 'question', label: '相关问题' },
{ name: 'document', label: '文档资料' },
];
const resultCount = computed(() => {
const map: Record<string, number> = { all: props.searchItems.length };
for (const item of props.searchItems) {
map[item.type] = (map[item.type] ?? 0) + 1;
}
return map;
});
// 分类控制
const activeTab = ref('all');
// 分页控制
const currentPage = ref(1);
watch(activeTab, () => {
currentPage.value = 1; // 重置页码
});
</script>

View File

@ -0,0 +1,54 @@
<template>
<article class="solution-defail">
<header class="solution-header">
<h1 class="solution-title">{{ solution.title }}</h1>
<dl class="solution-meta">
<dt class="visually-hidden">CreatedAt:</dt>
<dd class="solution-date">
{{ new Date(solution.createAt).toLocaleDateString() }}
</dd>
</dl>
<p class="solution-summary">{{ solution.summary }}</p>
</header>
<section class="solution-content">
<markdown-renderer :content="solution?.content || ''" />
</section>
</article>
</template>
<script setup lang="ts">
defineProps({
solution: {
type: Object as PropType<SolutionView>,
default: null,
},
});
</script>
<style scoped>
.solution-title {
font-size: 2rem;
font-weight: bold;
color: var(--el-color-primary);
text-align: center;
}
.solution-meta {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 0.5rem;
font-size: 0.8rem;
color: var(--el-text-color-secondary);
}
.solution-summary {
font-size: 0.8rem;
color: var(--el-text-color-secondary);
text-align: center;
}
.solution-content {
margin-top: 1rem;
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<el-card class="support-card">
<el-row>
<el-col :span="6">
<el-icon class="card-icon" size="80">
<component :is="iconComponent" />
</el-icon>
</el-col>
<el-col :span="18">
<div class="card-title">
<span>{{ title }}</span>
</div>
</el-col>
</el-row>
<el-row>
<div class="card-content">
<p>{{ description }}</p>
</div>
</el-row>
<el-row>
<NuxtLink class="card-link" :to="to">
<el-button class="card-button" round>
<span>了解更多 > </span>
</el-button>
</NuxtLink>
</el-row>
</el-card>
</template>
<script setup lang="ts">
defineProps({
title: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
to: {
type: String,
default: '',
},
iconComponent: {
type: Object,
default: null,
},
});
</script>
<style scoped>
.support-card {
width: 100%;
padding: 20px;
box-shadow: none;
border-radius: none;
}
.card-icon {
color: var(--el-color-primary);
}
.card-title {
display: flex;
height: 100%;
align-items: center;
margin-left: 2rem;
font-size: 1.5rem;
font-weight: bold;
color: var(--el-color-primary);
}
.card-link {
margin-left: auto;
}
.card-button {
cursor: pointer;
text-align: center;
font-size: 1rem;
color: var(--el-color-primary);
transition: all 0.3s ease;
}
.el-row {
margin-bottom: 20px;
}
.el-row:last-child {
margin-bottom: 0;
}
.el-col {
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,21 @@
<template>
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in items"
:key="index"
class="text-md opacity-50"
>
<NuxtLink v-if="item.to" :to="item.to">{{ item.label }}</NuxtLink>
<span v-else>{{ item.label }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
defineProps({
items: {
type: Array as () => Array<{ label: string; to?: string }>,
required: true,
},
});
</script>

View File

@ -0,0 +1,33 @@
<template>
<el-result icon="warning" :title="title" :sub-title="subTitle">
<template #extra>
<el-button type="primary" @click="onBack">
{{ backText || $t('back') }}
</el-button>
</template>
</el-result>
</template>
<script setup lang="ts">
defineProps({
title: {
type: String,
required: true,
},
subTitle: {
type: String,
required: false,
default: '',
},
backText: {
type: String,
required: false,
default: '',
},
onBack: {
type: Function as () => unknown,
required: false,
default: undefined,
},
});
</script>

View File

@ -0,0 +1,85 @@
<template>
<div class="question-category">
<el-row :gutter="12">
<el-col :span="8">
<span class="select-label">产品分类</span>
<el-select
v-model="model.selectedType"
placeholder="选择产品类型"
clearable
>
<el-option
v-for="type in productTypeOptions"
:key="type.id"
:label="type.name"
:value="type.id"
/>
</el-select>
</el-col>
<el-col :span="8">
<span class="select-label">产品系列</span>
<el-select
v-model="model.selectedProduct"
placeholder="选择系列产品"
clearable
>
<el-option
v-for="product in productOptions"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-col>
<el-col :span="8">
<span class="select-label">关键词</span>
<el-input
v-model="model.keyword"
placeholder="输入关键词..."
clearable
:prefix-icon="Search"
/>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue';
defineProps({
productTypeOptions: {
type: Array as () => Array<{ id: number; name: string }>,
default: () => [],
},
productOptions: {
type: Array as () => Array<{ id: number; name: string }>,
default: () => [],
},
});
const model = defineModel<{
keyword: string;
selectedType: number | null;
selectedProduct: number | null;
}>();
</script>
<style scoped>
.question-category {
margin-bottom: 1rem;
}
.select-label {
color: var(--el-text-color-secondary);
font-size: 0.9rem;
}
:deep(.el-select__wrapper),
:deep(.el-input__wrapper) {
height: 40px;
font-size: 0.9rem;
}
</style>

View File

@ -1,17 +1,10 @@
<template>
<main p="x4 y10" text="center teal-700 dark:gray-200">
<div text4xl>
<div i-ep-warning inline-block />
</div>
<div>{{ $t('not-found') }}</div>
<div>
<button text-sm btn m="3 t8" @click="router.back()">
{{ $t('back') }}
</button>
</div>
<not-found-result
:title="$t('page-not-found')"
:sub-title="$t('page-not-found-desc')"
:back-text="$t('back-to-home')"
:on-back="() => $router.push($localePath('/'))"
/>
</main>
</template>
<script setup lang="ts">
const router = useRouter();
</script>

View File

@ -1,18 +1,7 @@
<template>
<div class="page-container">
<div v-if="!pending">
<el-breadcrumb class="breadcrumb" separator="/">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">
{{ $t('navigation.home') }}
</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/about')">
{{ $t('navigation.about-us') }}
</NuxtLink>
</el-breadcrumb-item>
</el-breadcrumb>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
<div class="content">
<markdown-renderer :content="content.content || ''" />
@ -38,6 +27,11 @@
</template>
<script setup lang="ts">
const localePath = useLocalePath();
const breadcrumbItems = [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.about-us') },
];
const { data, pending, error } = await useCompanyProfile();
const content = computed(() => toCompanyProfileView(data.value));

View File

@ -2,54 +2,10 @@
<div class="page-container">
<div class="page-header">
<h1 class="page-title">文件下载</h1>
<el-breadcrumb class="breadcrumb">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">{{
$t('navigation.home')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/products')">{{
$t('navigation.downloads')
}}</NuxtLink>
</el-breadcrumb-item>
</el-breadcrumb>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
</div>
<div v-if="!pending" class="page-content">
<el-card shadow="hover" class="p-4">
<template #header>
<div class="header-content">
<el-icon class="header-icon"><ElIconDocument /></el-icon>
<span class="truncate font-medium">{{
file.filename_download
}}</span>
</div>
</template>
<dl class="text-gray-600 space-y-1 mb-6">
<div>
<dt class="font-semibold inline">类型</dt>
<dd class="inline">{{ file.type }}</dd>
</div>
<div>
<dt class="font-semibold inline">大小</dt>
<dd class="inline">{{ formatFileSize(file.filesize) }}</dd>
</div>
<div>
<dt class="font-semibold inline">上传时间</dt>
<dd class="inline">
{{ new Date(file.uploaded_on).toLocaleDateString() }}
</dd>
</div>
</dl>
<template #footer>
<div class="button-group">
<el-button type="primary" @click="handleDownload">下载</el-button>
<el-button v-if="file.previewable" @click="handlePreview"
>预览</el-button
>
</div>
</template>
</el-card>
<file-card :file-id="id" :file="file" />
</div>
<div v-else>
<el-skeleton :rows="6" animated />
@ -59,16 +15,20 @@
<script setup lang="ts">
const route = useRoute();
const router = useRouter();
const localePath = useLocalePath();
// 获取路由参数
const id = computed(() => route.params.id as string);
const breadcrumbItems = [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.downloads'), to: localePath('/downloads') },
];
const id = route.params.id as string;
const {
data: file,
pending,
error,
} = await useFetch<FileMeta>(`/api/file/${id.value}`);
} = await useFetch<FileMeta>(`/api/file/${id}`);
if (error.value || !file.value) {
throw createError({
@ -76,19 +36,6 @@
statusMessage: '文件未找到',
});
}
function handleDownload() {
const link = document.createElement('a');
link.href = `/api/download/${id.value}`;
link.download = file.value?.filename_download ?? 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function handlePreview() {
router.push(`/preview/${id.value}`);
}
</script>
<style scoped>
@ -112,22 +59,4 @@
.breadcrumb {
margin-left: auto;
}
.header-content {
display: flex;
align-items: center;
gap: 3px;
margin-bottom: 4px;
}
.header-icon {
font-size: 1.5rem;
margin-right: 0.5rem;
}
.button-group {
display: flex;
justify-content: flex-end;
align-items: baseline;
gap: 1rem;
}
</style>

View File

@ -1,309 +1,27 @@
<template>
<div class="homepage">
<section v-if="!pending" class="carousel-section">
<el-carousel
class="homepage-carousel"
height="auto"
:interval="5000"
arrow="never"
autoplay
>
<el-carousel-item v-for="(item, index) in carousel" :key="index">
<div class="carousel-item">
<el-image
class="carousel-image"
:src="getImageUrl(item)"
:alt="`Carousel Image ${index + 1}`"
fit="contain"
lazy
/>
</div>
</el-carousel-item>
</el-carousel>
</section>
<section v-else>
<el-skeleton :rows="5" animated />
</section>
<section class="homepage-section">
<h2>推荐产品</h2>
<p>
探索我们的精选产品满足您的各种需求无论是创新技术还是经典设计我们都为您提供优质选择
</p>
<div v-if="!pending">
<el-carousel
class="recommend-carousel"
height="auto"
arrow="never"
indicator-position="outside"
:autoplay="false"
>
<el-carousel-item
v-for="n in Math.floor(recommend_products.length / 3) + 1"
:key="n"
class="recommend-list"
>
<div class="recommend-card-group">
<el-card
v-for="(item, index) in recommend_products.slice(
(n - 1) * 3,
n * 3
)"
:key="index"
class="recommend-card"
@click="handleProductCardClick(item.id.toString() || '')"
>
<template #header>
<el-image
:src="getImageUrl(item.cover)"
:alt="item.name"
fit="cover"
lazy
/>
</template>
<div class="recommend-card-body">
<!-- Title -->
<div class="text-center">
<span class="recommend-card-title">{{ item.name }}</span>
</div>
<!-- Description -->
<div class="recommend-card-description text-left opacity-25">
{{ item.summary }}
</div>
</div>
</el-card>
</div>
</el-carousel-item>
</el-carousel>
</div>
<div v-else>
<el-skeleton :rows="4" animated />
</div>
</section>
<section class="homepage-section">
<h2>推荐解决方案</h2>
<p>了解我们的定制解决方案,帮助您优化业务流程,提高效率。</p>
<div v-if="!pending">
<el-carousel
class="recommend-carousel"
height="auto"
arrow="never"
indicator-position="outside"
:autoplay="false"
>
<el-carousel-item
v-for="n in Math.floor(recommend_solutions.length / 3) + 1"
:key="n"
class="recommend-list"
>
<div class="recommend-card-group">
<el-card
v-for="(item, index) in recommend_solutions.slice(
(n - 1) * 3,
n * 3
)"
:key="index"
class="recommend-card"
@click="handleSolutionCardClick(item.id.toString() || '')"
>
<template #header>
<el-image
:src="getImageUrl(item.cover)"
:alt="item.title"
fit="cover"
lazy
/>
</template>
<div class="recommend-card-body">
<!-- Title -->
<div class="text-center">
<span class="recommend-card-title">{{ item.title }}</span>
</div>
<!-- Description -->
<div class="recommend-card-description text-left opacity-25">
{{ item.summary }}
</div>
</div>
</el-card>
</div>
</el-carousel-item>
</el-carousel>
</div>
<div v-else>
<el-skeleton :rows="4" animated />
</div>
</section>
<homepage-carousel :homepage-data="homepageData" :pending="pending" />
<homepage-product-section
:homepage-data="homepageData"
:pending="pending"
/>
<homepage-solution-section
:homepage-data="homepageData"
:pending="pending"
/>
</div>
</template>
<script setup lang="ts">
const { getImageUrl } = useDirectusImage();
const { data, pending, error } = await useHomepage();
const homepageData = computed(() => {
return toHomepageView(data.value);
});
const carousel = computed(() => homepageData.value?.carousel);
const recommend_products = computed(
() => homepageData.value?.recommendProducts
);
const recommend_solutions = computed(
() => homepageData.value?.recommendSolutions
);
watch(pending, () => {
console.log(data.value);
});
watch(error, (value) => {
if (value) {
console.error('数据获取失败: ', value);
}
});
const handleProductCardClick = (documentId: string) => {
// 使用路由导航到产品详情页
if (documentId) {
const localePath = useLocalePath();
const router = useRouter();
router.push(localePath(`/products/${documentId}`));
}
};
const handleSolutionCardClick = (documentId: string) => {
// 使用路由导航到解决方案详情页
if (documentId) {
const localePath = useLocalePath();
const router = useRouter();
router.push(localePath(`/solutions/${documentId}`));
}
};
</script>
<style scoped lang="scss">
section {
padding: 2rem;
}
section h2 {
color: var(--el-text-color-primary);
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
section p {
color: var(--el-text-color-regular);
line-height: 1.6;
}
.carousel-section {
padding: 0;
}
.homepage-carousel .el-carousel__item {
width: 100%;
height: 33vw;
/* 16:9 Aspect Ratio */
}
.el-carousel__item h3 {
display: flex;
color: #475669;
opacity: 0.8;
line-height: 300px;
margin: 0;
}
.homepage-carousel .carousel-item {
width: 100%;
height: 100%;
background-color: #f5f7fa;
}
.carousel-image {
position: relative;
width: 100%;
height: 100%;
}
.carousel-image-caption {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 5px 10px;
border-radius: 5px;
font-size: 14px;
}
.homepage-section {
max-width: 1200px;
margin: 0 auto;
}
.recommend-carousel :deep(.el-carousel__button) {
/* 指示器按钮样式 */
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #475669;
transition: all 0.3s ease;
}
.recommend-list {
display: flex;
padding: 1rem;
height: 400px;
}
.recommend-card-group {
display: flex;
gap: 1rem;
width: 100%;
margin: 0 auto;
height: 100%;
}
.recommend-card {
width: 33%;
transition: all 0.3s ease;
text-align: center;
}
.recommend-card :deep(.el-card__header) {
padding: 0;
border: none;
}
.recommend-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
cursor: pointer;
}
.recommend-card-body {
margin: 10px auto;
padding: 0px auto;
}
.recommend-card-title {
font-size: 1rem;
font-weight: 600;
}
.recommend-card-description {
font-size: 0.8rem;
margin-top: 5px;
}
.recommend-card .el-image {
width: 100%;
border-radius: 4px;
}
</style>

View File

@ -3,95 +3,20 @@
<div v-if="!pending">
<div v-if="product">
<!-- 面包屑导航 -->
<el-breadcrumb class="breadcrumb" separator="/">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">{{
$t('navigation.home')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/products')">{{
$t('navigation.products')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opactiy-50">{{
product.name
}}</el-breadcrumb-item>
</el-breadcrumb>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
<!-- 产品详情内容 -->
<div class="product-header">
<div class="product-image">
<el-image
v-if="product.images.length <= 1"
:src="getImageUrl(product.images[0].image)"
:alt="product.name"
fit="contain"
/>
<el-carousel
v-else
class="product-carousel"
height="500px"
:autoplay="false"
:loop="false"
arrow="always"
>
<el-carousel-item
v-for="(item, index) in product.images || []"
:key="index"
>
<div class="product-carousel-item">
<el-image
:src="getImageUrl(item.image || '')"
:alt="product.name"
fit="contain"
lazy
/>
<p v-if="item.caption" class="product-image-caption">
{{ item.caption }}
</p>
</div>
</el-carousel-item>
</el-carousel>
</div>
<div class="product-info">
<h1>{{ product.name }}</h1>
<p class="summary">{{ product.summary }}</p>
</div>
</div>
<product-header :product="product" />
<!-- 产品详细描述 -->
<div class="product-content">
<el-tabs v-model="activeName" class="product-tabs" stretch>
<el-tab-pane label="产品详情" name="details">
<markdown-renderer :content="product.description || ''" />
</el-tab-pane>
<el-tab-pane label="技术规格" name="specs">
<spec-table :data="product.specs" />
</el-tab-pane>
<el-tab-pane label="常见问题" name="faq">
<question-list :questions="product.faqs" />
</el-tab-pane>
<el-tab-pane label="相关文档" name="documents">
<document-list :documents="product.documents" />
</el-tab-pane>
</el-tabs>
</div>
<product-detail :product="product" />
</div>
<!-- 未找到产品 -->
<div v-else class="not-found">
<el-result
icon="warning"
<not-found-result
:title="$t('product-not-found')"
:sub-title="$t('product-not-found-desc')"
>
<template #extra>
<el-button type="primary" @click="$router.push('/products')">
{{ $t('back-to-products') }}
</el-button>
</template>
</el-result>
:back-text="$t('back-to-products')"
:on-back="() => $router.push($localePath('/products'))"
/>
</div>
</div>
<div v-else class="loading">
@ -107,24 +32,26 @@
<script setup lang="ts">
const route = useRoute();
const { getImageUrl } = useDirectusImage();
const localePath = useLocalePath();
// 获取路由参数
const id = computed(() => route.params.slug as string);
const { data, pending, error } = await useProduct(id.value);
console.log('Raw Data: ', data.value);
const rawProduct = computed(() => data.value ?? null);
const product = computed(() => {
if (rawProduct.value === null) {
return null;
}
return toProductView(rawProduct.value);
});
console.log('View Data: ', product.value);
const activeName = ref('details'); // 默认选中概览标签
const breadcrumbItems = computed(() => [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.products'), to: localePath('/products') },
{ label: product.value?.name || '' },
]);
watch(error, (value) => {
if (value) {
@ -155,67 +82,6 @@
padding: 2rem;
}
.product-header {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 3rem;
}
.product-image .el-image {
position: relative;
width: 100%;
height: 500px;
border-radius: 8px;
}
.product-image-caption {
position: absolute;
bottom: 10px;
/* left: 10%; */
background-color: rgba(0, 0, 0, 0.2);
border-radius: 5px;
padding: 5px 10px;
text-align: center;
color: white;
font-size: 0.8rem;
}
.product-carousel :deep(.el-carousel__button) {
/* 指示器按钮样式 */
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #475669;
transition: all 0.3s ease;
}
.product-info h1 {
margin-top: 2rem;
margin-bottom: 1rem;
font-size: 2rem;
}
.summary {
color: var(--el-color-info);
font-size: 1rem;
line-height: 1.6;
margin-bottom: 2rem;
}
.product-tabs ::v-deep(.el-tabs__nav) {
min-width: 30%;
float: right;
}
.product-tabs ::v-deep(.el-tabs__content) {
padding: 10px;
}
.product-content h2 {
color: var(--el-text-color-primary);
margin: 0;
}
.loading {
display: flex;
justify-content: center;
@ -229,16 +95,4 @@
align-items: center;
min-height: 400px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.product-header {
grid-template-columns: 1fr;
gap: 2rem;
}
.product-info h1 {
font-size: 2rem;
}
}
</style>

View File

@ -2,18 +2,7 @@
<div class="page-container">
<div class="page-header">
<h1 class="page-title">{{ $t('our-products') }}</h1>
<el-breadcrumb class="breadcrumb">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">{{
$t('navigation.home')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/products')">{{
$t('navigation.products')
}}</NuxtLink>
</el-breadcrumb-item>
</el-breadcrumb>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
</div>
<div v-if="!pending" class="page-content">
<div class="products-container">
@ -45,12 +34,18 @@
</template>
<script setup lang="ts">
const localePath = useLocalePath();
const { getImageUrl } = useDirectusImage();
const { data, pending, error } = useProductList();
const activeNames = ref<string[]>([]);
const breadcrumbItems = [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.products') },
];
const productsRaw = computed(() => data.value ?? []);
const products = computed(() =>
productsRaw.value.map((item) => toProductListView(item))

View File

@ -1,80 +1,10 @@
<template>
<div class="search-page">
<div class="search-header">
<h1 class="page-title">{{ $t('search.title') }}</h1>
<div class="search-bar">
<el-input
v-model="keyword"
class="search-input"
:placeholder="$t('search-placeholder')"
:prefix-icon="Search"
clearable
@keyup.enter="navigateToQuery(keyword)"
@clear="handleClear"
/>
<el-button
type="primary"
class="search-button"
@click="navigateToQuery(keyword)"
>
{{ $t('search.search-button') }}
</el-button>
</div>
</div>
<search-header v-model="keyword" />
<div v-if="loading" class="search-state">
<el-skeleton :rows="4" animated />
</div>
<div v-else-if="hasResults" class="search-results">
<el-tabs v-model="activeTab">
<el-tab-pane :label="`全部(${resultCount['all']})`" name="all">
<search-results
v-model:current-page="currentPage"
:search-items="searchItems"
/>
</el-tab-pane>
<el-tab-pane
:label="`产品(${resultCount['product'] || 0})`"
name="product"
>
<search-results
v-model:current-page="currentPage"
:search-items="searchItems"
category="product"
/>
</el-tab-pane>
<el-tab-pane
:label="`解决方案(${resultCount['solution'] || 0})`"
name="solution"
>
<search-results
v-model:current-page="currentPage"
:search-items="searchItems"
category="solution"
/>
</el-tab-pane>
<el-tab-pane
:label="`相关问题(${resultCount['question'] || 0})`"
name="question"
>
<search-results
v-model:current-page="currentPage"
:search-items="searchItems"
category="question"
/>
</el-tab-pane>
<el-tab-pane
:label="`文档资料(${resultCount['document'] || 0})`"
name="document"
>
<search-results
v-model:current-page="currentPage"
:search-items="searchItems"
category="document"
/>
</el-tab-pane>
</el-tabs>
</div>
<search-tabs v-else-if="hasResults" :search-items="searchItems" />
<div v-else class="search-state">
<el-empty
:description="
@ -88,8 +18,6 @@
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue';
// i18n相关
const { t } = useI18n();
const { getDirectusLocale } = useLocalizations();
@ -97,8 +25,6 @@
// 路由相关
const route = useRoute();
const router = useRouter();
const localePath = useLocalePath();
// 搜索相关
const { search } = useMeilisearch();
@ -144,44 +70,10 @@
})
);
console.log(searchItems.value);
// 分类控制
const activeTab = ref('all');
const resultCount = computed(() => {
const map: Record<string, number> = { all: searchItems.value.length };
for (const item of searchItems.value) {
map[item.type] = (map[item.type] ?? 0) + 1;
}
return map;
});
// 分页控制
const currentPage = ref(1);
const hasResults = computed(() =>
filteredSections.value.some((section) => section.hits.length > 0)
);
const navigateToQuery = (value: string) => {
const trimmed = value.trim();
if (!trimmed) return;
navigateTo({
path: localePath('/search'),
query: { query: trimmed },
});
};
const handleClear = () => {
keyword.value = '';
sections.value = [];
router.replace(localePath({ path: '/search' }));
};
watch(activeTab, () => {
currentPage.value = 1; // 重置页码
});
watch(
() => route.query.query,
async (newQuery) => {
@ -216,42 +108,6 @@
min-height: 70vh;
}
.search-header {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.page-title {
font-size: 2.25rem;
font-weight: 600;
color: var(--el-text-color-primary);
}
.search-bar {
display: flex;
gap: 1rem;
align-items: center;
font-size: 16px;
}
.search-input {
flex: 1;
height: 50px;
}
.search-button {
height: 50px;
width: 100px;
font-size: 16px;
}
.search-meta {
font-size: 0.9rem;
color: var(--el-text-color-secondary);
}
.search-state {
display: flex;
justify-content: center;

View File

@ -3,50 +3,17 @@
<div v-if="!pending">
<div v-if="solution">
<div class="page-header">
<el-breadcrumb class="breadcrumb" separator="/">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">{{
$t('navigation.home')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/solutions')">{{
$t('navigation.solutions')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">{{
solution.title
}}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="page-content">
<div class="solution-info">
<h1>{{ solution.title }}</h1>
<div class="solution-meta">
<span class="solution-date">
CreatedAt:
{{ new Date(solution.createAt).toLocaleDateString() }}
</span>
</div>
</div>
<p class="summary">{{ solution.summary }}</p>
<div class="solution-content">
<markdown-renderer :content="solution.content || ''" />
</div>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
</div>
<solution-detail :solution="solution" />
</div>
<div v-else class="not-found">
<el-result
icon="warning"
<not-found-result
:title="$t('solution-not-found')"
:sub-title="$t('solution-not-found-desc')"
>
<template #extra>
<el-button type="primary" @click="$router.push('/solutions')">
{{ $t('back-to-solutions') }}
</el-button>
</template>
</el-result>
:back-text="$t('back-to-solutions')"
:on-back="() => $router.push($localePath('/solutions'))"
/>
</div>
</div>
<div v-else class="loading">
@ -57,17 +24,25 @@
<script setup lang="ts">
const route = useRoute();
const localePath = useLocalePath();
// 获取路由参数(documentId)
const id = computed(() => route.params.slug as string);
const { data, pending, error } = await useSolution(id.value);
console.log('RawData: ', data.value);
const process = toSolutionView(data.value);
console.log('Processed Solution: ', process);
const solution = computed(() => {
if (!data.value) {
return null;
}
return toSolutionView(data.value);
});
const solution = computed(() => toSolutionView(data.value));
const breadcrumbItems = computed(() => [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.solutions'), to: localePath('/solutions') },
{ label: solution.value ? solution.value.title : '' },
]);
watch(error, (value) => {
if (value) {
@ -89,44 +64,6 @@
margin-bottom: 1rem;
}
.solution-header {
display: flex;
align-items: center;
gap: 20px;
}
.solution-header el-image {
width: 200px;
height: 200px;
border-radius: 4px;
}
.page-content h1 {
font-size: 2rem;
font-weight: bold;
color: var(--el-color-primary);
text-align: center;
}
.solution-meta {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 0.5rem;
font-size: 0.8rem;
color: var(--el-text-color-secondary);
}
.summary {
font-size: 0.8rem;
color: var(--el-text-color-secondary);
text-align: center;
}
.solution-content {
margin-top: 1rem;
}
.loading {
display: flex;
justify-content: center;

View File

@ -2,18 +2,7 @@
<div class="page-container">
<div class="page-header">
<h1 class="page-title">{{ $t('learn-our-solutions') }}</h1>
<el-breadcrumb class="breadcrumb">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">{{
$t('navigation.home')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/solutions')">{{
$t('navigation.solutions')
}}</NuxtLink>
</el-breadcrumb-item>
</el-breadcrumb>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
</div>
<div v-if="!pending" class="solutions-container">
<el-tabs v-model="activeName" class="solutions-tabs">
@ -55,8 +44,14 @@
</template>
<script setup lang="ts">
const localePath = useLocalePath();
const { getImageUrl } = useDirectusImage();
const breadcrumbItems = [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.solutions') },
];
const { data, pending, error } = await useSolutionList();
const solutionsRaw = computed(() => data.value ?? []);
@ -66,8 +61,6 @@
const activeName = ref<string>('all');
console.log('Processed Data', solutions.value);
// 按类型分组
const groupedSolutions = computed(() => {
const gourps: Record<string, SolutionListView[]> = {};

View File

@ -3,23 +3,7 @@
<support-tabs model-value="contact-us" />
<div class="page-header">
<h1 class="page-title">{{ $t('navigation.contact-info') }}</h1>
<el-breadcrumb class="breadcrumb" separator="/">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">
{{ $t('navigation.home') }}
</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/support')">
{{ $t('navigation.support') }}
</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/support/contact-us')">
{{ $t('navigation.contact-info') }}
</NuxtLink>
</el-breadcrumb-item>
</el-breadcrumb>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
</div>
<div v-if="!pending" class="page-content">
@ -32,6 +16,12 @@
</template>
<script setup lang="ts">
const localePath = useLocalePath();
const breadcrumbItems = [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.support'), to: localePath('/support') },
{ label: $t('navigation.contact-info') },
];
const { data, pending, error } = await useContactInfo();
const content = computed(() => toContactInfoView(data.value));

View File

@ -7,68 +7,14 @@
<support-tabs model-value="documents" />
<div class="page-header">
<h1 class="page-title">{{ $t('navigation.documents') }}</h1>
<el-breadcrumb class="breadcrumb" separator="/">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">{{
$t('navigation.home')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/support')">{{
$t('navigation.support')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/support/documents')">{{
$t('navigation.documents')
}}</NuxtLink>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="document-category">
<el-row :gutter="12">
<el-col :span="8">
<span class="select-label">产品分类</span>
<el-select
v-model="selectedType"
placeholder="选择产品类型"
clearable
>
<el-option
v-for="type in productTypeOptions"
:key="type.id"
:label="type.name"
:value="type.id"
/>
</el-select>
</el-col>
<el-col :span="8">
<span class="select-label">产品系列</span>
<el-select
v-model="selectedProduct"
placeholder="选择系列产品"
clearable
>
<el-option
v-for="product in productOptions"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-col>
<el-col :span="8">
<span class="select-label">关键词</span>
<el-input
v-model="keyword"
placeholder="输入关键词..."
clearable
:prefix-icon="Search"
/>
</el-col>
</el-row>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
</div>
<div class="page-content">
<product-filter
v-model="filters"
:product-type-options="productTypeOptions"
:product-options="productOptions"
/>
<document-list :documents="filteredDocuments" />
</div>
</div>
@ -76,7 +22,18 @@
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue';
const localePath = useLocalePath();
const breadcrumbItems = [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.support'), to: localePath('/support') },
{ label: $t('navigation.documents') },
];
const filters = reactive({
selectedType: null as number | null,
selectedProduct: null as number | null,
keyword: '',
});
const { data, pending, error } = await useDocumentList();
@ -84,11 +41,6 @@
() => data?.value.map((item) => toDocumentListView(item)) ?? []
);
const keyword = ref('');
const selectedType = ref<number | null>(null);
const selectedProduct = ref<number | null>(null);
const productTypeOptions = computed(() => {
const types: DocumentListProductType[] = [];
documents.value.forEach((doc: DocumentListView) => {
@ -104,13 +56,13 @@
});
const productOptions = computed(() => {
if (!selectedType.value) return [];
if (!filters.selectedType) return [];
const products: DocumentListProduct[] = [];
documents.value.forEach((doc: DocumentListView) => {
doc.products?.forEach((product: DocumentListProduct) => {
if (
product.type.id === selectedType.value &&
product.type.id === filters.selectedType &&
!products.some((item) => item.id === product.id)
) {
products.push(product);
@ -123,29 +75,32 @@
const filteredDocuments = computed(() =>
documents.value.filter((doc: DocumentListView) => {
const matchProduct = selectedProduct.value
const matchProduct = filters.selectedProduct
? doc.products?.some(
(product: DocumentListProduct) =>
product.id === selectedProduct.value
product.id === filters.selectedProduct
)
: selectedType.value
: filters.selectedType
? doc.products?.some(
(product: DocumentListProduct) =>
product.type?.id === selectedType.value
product.type?.id === filters.selectedType
)
: true;
const matchKeyword = keyword.value
? doc.title && doc.title.includes(keyword.value)
const matchKeyword = filters.keyword
? doc.title && doc.title.includes(filters.keyword)
: true;
return matchProduct && matchKeyword;
})
);
watch(selectedType, () => {
selectedProduct.value = null;
});
watch(
() => filters.selectedType,
() => {
filters.selectedProduct = null;
}
);
watch(documents, (value) => {
console.log(value);

View File

@ -7,69 +7,16 @@
<support-tabs model-value="faq" />
<div class="page-header">
<h1 class="page-title">{{ $t('navigation.faq') }}</h1>
<el-breadcrumb class="breadcrumb" separator="/">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">{{
$t('navigation.home')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/support')">{{
$t('navigation.support')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/support/faq')">{{
$t('navigation.faq')
}}</NuxtLink>
</el-breadcrumb-item>
</el-breadcrumb>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
</div>
<div class="question-category">
<el-row :gutter="12">
<el-col :span="8">
<span class="select-label">产品分类</span>
<el-select
v-model="selectedType"
placeholder="选择产品类型"
clearable
>
<el-option
v-for="type in productTypeOptions"
:key="type.id"
:label="type.name"
:value="type.id"
/>
</el-select>
</el-col>
<el-col :span="8">
<span class="select-label">产品系列</span>
<el-select
v-model="selectedProduct"
placeholder="选择系列产品"
clearable
>
<el-option
v-for="product in productOptions"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-col>
<el-col :span="8">
<span class="select-label">关键词</span>
<el-input
v-model="keyword"
placeholder="输入关键词..."
clearable
:prefix-icon="Search"
/>
</el-col>
</el-row>
</div>
<div class="page-content">
<product-filter
v-model="filters"
:product-type-options="productTypeOptions"
:product-options="productOptions"
/>
<question-list :questions="filteredQuestions" />
</div>
</div>
@ -77,7 +24,19 @@
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue';
const localePath = useLocalePath();
const filters = reactive({
selectedType: null as number | null,
selectedProduct: null as number | null,
keyword: '',
});
const breadcrumbItems = [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.support'), to: localePath('/support') },
{ label: $t('navigation.faq') },
];
const { data, pending, error } = await useQuestionList();
@ -85,11 +44,6 @@
() => data.value.map((item) => toQuestionListView(item)) ?? null
);
const keyword = ref('');
const selectedType = ref<number | null>(null);
const selectedProduct = ref<number | null>(null);
const productTypeOptions = computed(() => {
const types: QuestionListProductType[] = [];
questions.value.forEach((q: QuestionListView) => {
@ -104,12 +58,12 @@
});
const productOptions = computed(() => {
if (!selectedType.value) return [];
if (!filters.selectedType) return [];
const products: QuestionListProduct[] = [];
questions.value.forEach((q: QuestionListView) => {
q.products.forEach((product: QuestionListProduct) => {
if (
product.type.id === selectedType.value &&
product.type.id === filters.selectedType &&
!products.some((p) => p.id === product.id)
) {
products.push(product);
@ -121,30 +75,33 @@
const filteredQuestions = computed(() => {
return questions.value.filter((question: QuestionListView) => {
const matchProduct = selectedProduct.value
const matchProduct = filters.selectedProduct
? question.products?.some(
(product: QuestionListProduct) =>
product.id === selectedProduct.value
product.id === filters.selectedProduct
)
: selectedType.value
: filters.selectedType
? question.products?.some(
(product: QuestionListProduct) =>
product.type.id === selectedType.value
product.type.id === filters.selectedType
)
: true;
const matchKeyword = keyword.value
? (question.title && question.title.includes(keyword.value)) ||
(question.content && question.content.includes(keyword.value))
const matchKeyword = filters.keyword
? (question.title && question.title.includes(filters.keyword)) ||
(question.content && question.content.includes(filters.keyword))
: true;
return matchProduct && matchKeyword;
});
});
watch(selectedType, () => {
selectedProduct.value = null;
});
watch(
() => filters.selectedType,
() => {
filters.selectedProduct = null;
}
);
watch(data, (newVal) => {
console.log('useAsyncData updated:', newVal);
@ -179,24 +136,7 @@
margin-left: auto;
}
.question-category {
padding: 0rem 2rem;
gap: 4px;
margin-bottom: 0.5rem;
}
.page-content {
padding: 1rem 2rem 2rem;
}
.select-label {
color: var(--el-text-color-secondary);
font-size: 0.9rem;
}
:deep(.el-select__wrapper),
:deep(.el-input__wrapper) {
height: 40px;
font-size: 0.9rem;
}
</style>

View File

@ -1,115 +1,58 @@
<template>
<div class="page-container">
<support-tabs />
<div class="page-header">
<h1 class="page-title">{{ $t('navigation.support') }}</h1>
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
</div>
<div class="page-content">
<div class="page-header">
<h1 class="page-title">{{ $t('navigation.support') }}</h1>
<el-breadcrumb class="breadcrumb" separator="/">
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/')">{{
$t('navigation.home')
}}</NuxtLink>
</el-breadcrumb-item>
<el-breadcrumb-item class="text-md opacity-50">
<NuxtLink :to="$localePath('/support')">{{
$t('navigation.support')
}}</NuxtLink>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<section style="margin-bottom: 2rem">
<p>
金申机械制造有限公司致力于为客户提供优质的产品与服务针对纸管机分纸机纸吸管等产品我们提供全方位的售后服务确保客户能够安心地使用我们的产品
</p>
</section>
<div class="card-group">
<el-card class="card">
<el-row>
<el-col :span="6">
<el-icon class="card-icon" size="80">
<ElIconQuestionFilled />
</el-icon>
</el-col>
<el-col :span="18">
<div class="card-title">
<span>{{ $t('navigation.faq') }}</span>
</div>
</el-col>
</el-row>
<el-row>
<div class="card-content">
<p>我们为用户整理了常见问题的答案帮助您快速解决疑惑</p>
</div>
</el-row>
<el-row>
<NuxtLink class="card-link" :to="$localePath('/support/faq')">
<el-button class="card-button" round>
<span>了解更多 > </span>
</el-button>
</NuxtLink>
</el-row>
</el-card>
<el-card class="card">
<el-row>
<el-col :span="6">
<el-icon class="card-icon" size="80">
<ElIconDocumentChecked />
</el-icon>
</el-col>
<el-col :span="18">
<div class="card-title">
<span>{{ $t('navigation.documents') }}</span>
</div>
</el-col>
</el-row>
<el-row>
<div class="card-content">
<p>我们为用户整理了常见问题的答案为您快速解决疑惑</p>
</div>
</el-row>
<el-row>
<NuxtLink class="card-link" :to="$localePath('/support/documents')">
<el-button class="card-button" round>
<span>了解更多 > </span>
</el-button>
</NuxtLink>
</el-row>
</el-card>
<el-card class="card">
<el-row>
<el-col :span="6">
<el-icon class="card-icon" size="80">
<ElIconService />
</el-icon>
</el-col>
<el-col :span="18">
<div class="card-title">
<span>{{ $t('navigation.contact-info') }}</span>
</div>
</el-col>
</el-row>
<el-row>
<div class="card-content">
<p>通过电话邮箱联系我们我们将现场为您服务</p>
</div>
</el-row>
<el-row>
<NuxtLink
class="card-link"
:to="$localePath('/support/contact-us')"
>
<el-button class="card-button" round>
<span>了解更多 > </span>
</el-button>
</NuxtLink>
</el-row>
</el-card>
<support-card
v-for="(item, index) in supportItems"
:key="index"
:title="item.title"
:description="item.description"
:to="item.to"
:icon-component="item.iconComponent"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
const localePath = useLocalePath();
const breadcrumbItems = [
{ label: $t('navigation.home'), to: localePath('/') },
{ label: $t('navigation.support') },
];
const supportItems = [
{
title: $t('navigation.faq'),
description: '我们为用户整理了常见问题的答案,帮助您快速解决疑惑。',
to: localePath('/support/faq'),
iconComponent: ElIconQuestionFilled,
},
{
title: $t('navigation.documents'),
description: '我们为用户整理了常见问题的答案,帮助您快速解决疑惑。',
to: localePath('/support/documents'),
iconComponent: ElIconDocumentChecked,
},
{
title: $t('navigation.contact-info'),
description: '通过电话、邮箱联系我们,我们将现场为您服务。',
to: localePath('/support/contact-us'),
iconComponent: ElIconService,
},
];
</script>
<style scoped>
.page-container {
@ -119,6 +62,7 @@
.page-header {
display: flex;
padding: 2rem 2rem 0rem;
}
.page-title {
@ -173,34 +117,6 @@
margin-left: auto;
}
.card-button {
cursor: pointer;
text-align: center;
font-size: 1rem;
color: var(--el-color-primary);
transition: all 0.3s ease;
}
.button-group {
display: flex;
justify-content: left;
margin-top: 2rem;
margin-left: 2rem;
gap: 20px;
}
.el-row {
margin-bottom: 20px;
}
.el-row:last-child {
margin-bottom: 0;
}
.el-col {
border-radius: 4px;
}
.grid-content {
border-radius: 4px;
min-height: 36px;

View File

@ -56,6 +56,9 @@
"solution-not-found": "Solution Not Found",
"solution-not-found-desc": "Sorry, the solution you are lokking for does not exist or has been removed.",
"back-to-solutions": "Back to Solutions",
"page-not-found": "Page Not Found",
"page-not-found-desc": "Sorry, the page you are looking for does not exist or has been removed.",
"back-to-home": "Back to Home",
"no-content-available": "No detailed information available",
"loading": "Loading...",
"our-products": "Our Products",

View File

@ -56,6 +56,9 @@
"solution-not-found": "解决方案未找到",
"solution-not-found-desc": "抱歉,您访问的解决方案不存在或已被删除",
"back-to-solutions": "返回解决方案列表",
"page-not-found": "页面未找到",
"page-not-found-desc": "抱歉,您访问的页面不存在或已被删除。",
"back-to-home": "返回首页",
"no-content-available": "暂无详细信息",
"loading": "加载中...",
"our-products": "我们的产品",

View File

@ -18,6 +18,13 @@ export default defineNuxtConfig({
},
},
components: [
{
path: '~/components',
pathPrefix: false,
},
],
runtimeConfig: {
public: {
meili: {