Browse Source

'27号-静态i18N钱'

hfll
hflllll 8 months ago
parent
commit
fddbe998de
56 changed files with 8864 additions and 2167 deletions
  1. +1
    -0
      .vercel/project.json
  2. +2
    -1
      package.json
  3. BIN
      public/MOSEVideo.mp4
  4. +3
    -1
      src/api/modules/community.ts
  5. +22
    -2
      src/api/modules/contact.ts
  6. +1
    -0
      src/api/modules/ecosystem.ts
  7. +2
    -1
      src/api/modules/home.ts
  8. +5
    -0
      src/api/modules/technology.ts
  9. +25
    -46
      src/components/ApiDemo.vue
  10. +131
    -39
      src/components/about/CompanyModule.vue
  11. +6
    -5
      src/components/about/EcosystemModule.vue
  12. +299
    -256
      src/components/about/MilestoneModule.vue
  13. +186
    -41
      src/components/about/PartnersModule.vue
  14. +42
    -18
      src/components/about/TeamModule.vue
  15. +438
    -0
      src/components/common/RichTextRenderer.vue
  16. +105
    -0
      src/components/common/VideoModal.vue
  17. +23
    -20
      src/components/ecosystem/AppListModule.vue
  18. +63
    -39
      src/components/ecosystem/CompanyCard.vue
  19. +23
    -23
      src/components/ecosystem/EcosystemCard.vue
  20. +74
    -26
      src/components/home/BannerModule.vue
  21. +96
    -76
      src/components/home/CoreValuesModule.vue
  22. +23
    -15
      src/components/home/EventsModule.vue
  23. +92
    -32
      src/components/home/MediaModule.vue
  24. +34
    -12
      src/components/home/NewsModule.vue
  25. +59
    -10
      src/components/home/PartnersModule.vue
  26. +89
    -34
      src/components/home/PostsModule.vue
  27. +382
    -202
      src/components/home/ProjectIntroModule.vue
  28. +3
    -5
      src/components/layout/Footer.vue
  29. +4
    -4
      src/components/layout/NavBar.vue
  30. +11
    -9
      src/components/rewards/IncentiveModule.vue
  31. +3
    -3
      src/components/rewards/MarketDataModule.vue
  32. +10
    -20
      src/components/rewards/RewardModule.vue
  33. +30
    -78
      src/components/technology/ArchitectureModule.vue
  34. +10
    -3
      src/i18n/locales/ar.json
  35. +92
    -14
      src/i18n/locales/en.json
  36. +10
    -3
      src/i18n/locales/fr.json
  37. +11
    -4
      src/i18n/locales/ja.json
  38. +10
    -3
      src/i18n/locales/ko.json
  39. +10
    -3
      src/i18n/locales/ms.json
  40. +10
    -3
      src/i18n/locales/pt.json
  41. +10
    -3
      src/i18n/locales/ru.json
  42. +10
    -3
      src/i18n/locales/th.json
  43. +10
    -3
      src/i18n/locales/vi.json
  44. +10
    -3
      src/i18n/locales/zh-TW.json
  45. +2496
    -396
      src/i18n/locales/zh.json
  46. +14
    -14
      src/utils/http/request.ts
  47. +14
    -16
      src/views/About.vue
  48. +350
    -116
      src/views/Community.vue
  49. +6
    -6
      src/views/Contact.vue
  50. +188
    -189
      src/views/Ecosystem.vue
  51. +37
    -10
      src/views/FAQ.vue
  52. +3
    -6
      src/views/Home.vue
  53. +6
    -4
      src/views/NotFound.vue
  54. +429
    -346
      src/views/Technology.vue
  55. +1
    -1
      tsconfig.build.tsbuildinfo
  56. +2840
    -0
      国际化需求.txt

+ 1
- 0
.vercel/project.json View File

@ -0,0 +1 @@
{"projectName":"trae_7ocy79ej"}

+ 2
- 1
package.json View File

@ -9,7 +9,8 @@
"build-no-check": "vite build",
"build-ignore-ts": "vue-tsc --project tsconfig.build.json --noEmit && vite build",
"preview": "vite preview",
"gitBuild": "vue-tsc -b && vite build --base=/My"
"gitBuild": "vue-tsc -b && vite build --base=/My",
"build-ignore-errors.bat": "build-ignore-errors.bat"
},
"dependencies": {
"@iconify/vue": "^5.0.0",


BIN
public/MOSEVideo.mp4 View File


+ 3
- 1
src/api/modules/community.ts View File

@ -60,7 +60,9 @@ export interface CommunityItem {
id: string;
title: string;
content: string;
description?: string;
image?: string;
video?: string;
createBy?: string;
createTime?: string;
updateBy?: string;
@ -142,4 +144,4 @@ export function queryCommunityList(params?: PageParams & Partial<CommunityItem>)
*/
export function addComments(params: Partial<CommentItem>) {
return request.get<any>('/mose-admin/mose/community/addComments', params);
}
}

+ 22
- 2
src/api/modules/contact.ts View File

@ -20,5 +20,25 @@ export interface ContactForm {
* @returns
*/
export function contactUs(data: ContactForm) {
return request.post<any>('/mose-admin/mose/contact/contactUs', data);
}
console.log('contactUs API调用,原始数据:', data);
// 创建URLSearchParams对象(表单数据格式)
const formData = new URLSearchParams();
formData.append('name', data.name || '');
formData.append('email', data.email || '');
formData.append('topic', data.topic || '');
formData.append('information', data.information || '');
// 打印表单数据
console.log('表单数据:', formData.toString());
console.log('请求URL:', '/mose-admin/mose/contact/contactUs');
// 使用表单数据格式提交
return request.post<any>('/mose-admin/mose/contact/contactUs', formData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
direct: true, // 直接返回响应数据,不经过响应处理
showError: true // 显示错误提示
});
}

+ 1
- 0
src/api/modules/ecosystem.ts View File

@ -15,6 +15,7 @@ export interface EcosystemItem {
id: string;
title: string;
description: string;
image?: string;
createBy?: string;
createTime?: string;
updateBy?: string;


+ 2
- 1
src/api/modules/home.ts View File

@ -42,6 +42,7 @@ export interface MediaItem {
title: string;
image: string;
description: string;
video?: string;
createBy?: string;
createTime?: string;
updateBy?: string;
@ -132,4 +133,4 @@ export function queryMediaList(params?: PageParams & Partial<MediaItem>) {
export function queryPostList(params?: PageParams & Partial<PostItem>) {
return request.get<any>('/mose-admin/mose/index/queryPostList', params)
.then(response => handleResponse<PostItem>(response));
}
}

+ 5
- 0
src/api/modules/technology.ts View File

@ -47,4 +47,9 @@ function handleResponse<T>(response: any): T[] {
export function queryTechnologyList(params?: PageParams & Partial<TechnologyItem>) {
return request.get<any>('/mose-admin/mose/technology/queryTechnologyList', params)
.then(response => handleResponse<TechnologyItem>(response));
}
export function queryStructuralList(params?: PageParams & Partial<TechnologyItem>) {
return request.get<any>('/mose-admin/mose/technology/queryStructuralList', params)
.then(response => handleResponse<TechnologyItem>(response));
}

+ 25
- 46
src/components/ApiDemo.vue View File

@ -1,56 +1,56 @@
<template>
<div class="container mx-auto py-8 px-4">
<h1 class="text-2xl font-bold mb-6">API 示例</h1>
<h1 class="text-2xl font-bold mb-6">{{ t('api_demo.title') }}</h1>
<!-- 配置数据展示 -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">系统配置数据</h2>
<h2 class="text-xl font-semibold mb-4">{{ t('api_demo.config.title') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-background-light p-4 rounded-lg">
<h3 class="font-medium mb-2">第一个邮箱</h3>
<p>{{ getConfigText(configCodes.emailOne) }}</p>
<h3 class="font-medium mb-2">{{ t('api_demo.config.email_one.title') }}</h3>
<p>{{ t('api_demo.config.email_one.value') }}</p>
</div>
<div class="bg-background-light p-4 rounded-lg">
<h3 class="font-medium mb-2">第二个邮箱</h3>
<p>{{ getConfigText(configCodes.emailTwo) }}</p>
<h3 class="font-medium mb-2">{{ t('api_demo.config.email_two.title') }}</h3>
<p>{{ t('api_demo.config.email_two.value') }}</p>
</div>
</div>
<div class="mt-4 bg-background-light p-4 rounded-lg">
<h3 class="font-medium mb-2">配置信息三富文本</h3>
<div v-html="getConfigTextarea(configCodes.configThree)"></div>
<h3 class="font-medium mb-2">{{ t('api_demo.config.config_three.title') }}</h3>
<div v-html="t('api_demo.config.config_three.value')"></div>
</div>
</div>
<!-- 概要说明数据展示 -->
<div>
<h2 class="text-xl font-semibold mb-4">概要说明数据</h2>
<h2 class="text-xl font-semibold mb-4">{{ t('api_demo.summary.title') }}</h2>
<div class="space-y-6">
<div class="bg-background-light p-4 rounded-lg">
<h3 class="font-medium mb-2">平台简介</h3>
<h4 class="text-lg mb-2">{{ getSummaryTitle(summaryCodes.platformIntro) }}</h4>
<div v-html="getSummaryDescription(summaryCodes.platformIntro)"></div>
<h3 class="font-medium mb-2">{{ t('api_demo.summary.platform_intro.title') }}</h3>
<h4 class="text-lg mb-2">{{ t('api_demo.summary.platform_intro.subtitle') }}</h4>
<div v-html="t('api_demo.summary.platform_intro.description')"></div>
</div>
<div class="bg-background-light p-4 rounded-lg">
<h3 class="font-medium mb-2">愿景与使命</h3>
<h4 class="text-lg mb-2">{{ getSummaryTitle(summaryCodes.aspirationMission) }}</h4>
<div v-html="getSummaryDescription(summaryCodes.aspirationMission)"></div>
<h3 class="font-medium mb-2">{{ t('api_demo.summary.aspiration_mission.title') }}</h3>
<h4 class="text-lg mb-2">{{ t('api_demo.summary.aspiration_mission.subtitle') }}</h4>
<div v-html="t('api_demo.summary.aspiration_mission.description')"></div>
</div>
<div class="bg-background-light p-4 rounded-lg">
<h3 class="font-medium mb-2">技术架构</h3>
<h4 class="text-lg mb-2">{{ getSummaryTitle(summaryCodes.technology) }}</h4>
<div v-html="getSummaryDescription(summaryCodes.technology)"></div>
<h3 class="font-medium mb-2">{{ t('api_demo.summary.technology.title') }}</h3>
<h4 class="text-lg mb-2">{{ t('api_demo.summary.technology.subtitle') }}</h4>
<div v-html="t('api_demo.summary.technology.description')"></div>
</div>
<div class="bg-background-light p-4 rounded-lg">
<h3 class="font-medium mb-2">我们的故事</h3>
<h4 class="text-lg mb-2">{{ getSummaryTitle(summaryCodes.ourStory) }}</h4>
<div v-html="getSummaryDescription(summaryCodes.ourStory)"></div>
<h3 class="font-medium mb-2">{{ t('api_demo.summary.our_story.title') }}</h3>
<h4 class="text-lg mb-2">{{ t('api_demo.summary.our_story.subtitle') }}</h4>
<div v-html="t('api_demo.summary.our_story.description')"></div>
</div>
</div>
</div>
@ -58,30 +58,9 @@
</template>
<script setup lang="ts">
import { useConfig, useSummary } from '@/utils/config';
//
const { configList, getConfigText, getConfigTextarea } = useConfig();
//
const { summaryList, getSummaryTitle, getSummaryDescription } = useSummary();
//
const configCodes = {
emailOne: 'emailOne', //
emailTwo: 'emailTwo', //
configOne: 'config_one', //
configTwo: 'config_two', //
configThree: 'config_three', //
};
//
const summaryCodes = {
platformIntro: 'config_Introduction_of_platform', //
aspirationMission: 'config_aspiration_and_mission', // 使
technology: 'config_structural_of_technology', //
ourStory: 'config_our_story', //
};
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
<style scoped>
@ -180,4 +159,4 @@ input[type="file"] {
margin-bottom: 16px;
display: block;
}
</style>
</style>

+ 131
- 39
src/components/about/CompanyModule.vue View File

@ -7,28 +7,59 @@ const { getConfigImage } = useConfig();
const { t } = useI18n();
// -
// -
const getLocationPosition = (index: number) => {
const positions = [
{ x: 35, y: 15 }, //
{ x: 45, y: 25 }, //
{ x: 55, y: 15 }, //
{ x: 65, y: 35 }, // 西
{ x: 75, y: 20 }, //
{ x: 85, y: 30 }, // 西
{ x: 75, y: 45 }, //
{ x: 40, y: 45 }, //
{ x: 50, y: 55 }, //
{ x: 60, y: 50 }, //
{ x: 70, y: 60 } //
// 使
const mobilePositions = [
{ x: 20, y: 20 }, //
{ x: 50, y: 25 }, //
{ x: 80, y: 20 }, //
{ x: 15, y: 50 }, // 西
{ x: 45, y: 45 }, //
{ x: 75, y: 50 }, // 西
{ x: 25, y: 75 }, //
{ x: 55, y: 70 }, //
{ x: 85, y: 75 }, //
{ x: 35, y: 35 }, //
{ x: 65, y: 35 } //
];
// 使
const desktopPositions = [
{ x: 12, y: 15 }, //
{ x: 40, y: 30 }, //
{ x: 55, y: 10 }, //
{ x: 70, y: 35 }, // 西
{ x: 45, y: 75 }, //
{ x: 90, y: 40 }, // 西
{ x: 75, y: 60 }, //
{ x: 50, y: 50 }, //
{ x: 45, y: 85 }, //
{ x: 60, y: 75 }, //
{ x: 80, y: 30 } //
];
//
const isMobile = window.innerWidth < 768;
const positions = isMobile ? mobilePositions : desktopPositions;
return positions[index] || { x: 50, y: 50 };
};
//
const companyLocations = ref<CompanyItem[]>([]);
const loading = ref(false);
//
const activeLocationId = ref<number | null>(null);
//
const toggleLocationDetail = (locationId: number) => {
if (activeLocationId.value === locationId) {
activeLocationId.value = null;
} else {
activeLocationId.value = locationId;
}
};
//
const fetchCompanyList = async () => {
@ -46,6 +77,21 @@ const fetchCompanyList = async () => {
//
onMounted(() => {
fetchCompanyList();
//
const handleClickOutside = (event: Event) => {
const target = event.target as HTMLElement;
if (!target.closest('.group')) {
activeLocationId.value = null;
}
};
document.addEventListener('click', handleClickOutside);
//
return () => {
document.removeEventListener('click', handleClickOutside);
};
});
</script>
@ -62,18 +108,18 @@ onMounted(() => {
<!-- 标题部分 -->
<div class="max-w-3xl mx-auto text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-text mb-6 wow animate__animated animate__fadeInDown animate__duration-fast">
全球战略部署
{{ t('ecosystem.companies.title') }}
</h2>
<p class="text-lg md:text-xl text-text-secondary wow animate__animated animate__fadeIn animate__delay-xs animate__duration-fast">
全球战略部署构建全球战略网络
{{ t('ecosystem.companies.subtitle') }}
</p>
<!-- 装饰线 -->
<div class="w-24 h-1 bg-primary mx-auto mt-8"></div>
</div>
<!-- 全球办公室展示 - 错乱布局 -->
<div class="relative max-w-7xl mx-auto h-96 md:h-[500px] lg:h-[600px]">
<!-- 全球办公室展示 - 分散布局 -->
<div class="relative max-w-7xl mx-auto h-64 md:h-[500px] lg:h-[600px]">
<!-- 背景图片 -->
<div class="absolute inset-0 z-0">
<img
@ -89,11 +135,11 @@ onMounted(() => {
<div class="text-white text-lg">加载中...</div>
</div>
<!-- 左侧Logo -->
<div class="absolute top-1/2 left-8 transform -translate-y-1/2 z-20">
<!-- <div class="absolute top-1/2 left-8 transform -translate-y-1/2 z-20">
<div class="w-32 h-32 md:w-40 md:h-40 rounded-full bg-white/10 backdrop-blur-sm flex items-center justify-center border-2 border-white/20">
<div class="text-white text-2xl md:text-3xl font-bold">MOSE</div>
</div>
</div>
</div> -->
<!-- 连接线 -->
<svg class="absolute inset-0 w-full h-full z-10" style="pointer-events: none;">
@ -102,6 +148,11 @@ onMounted(() => {
<stop offset="0%" style="stop-color:#8B5CF6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#3B82F6;stop-opacity:0.5" />
</linearGradient>
<!-- 添加发光效果 -->
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="2" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<line
v-for="(location, index) in companyLocations"
@ -111,42 +162,65 @@ onMounted(() => {
:x2="getLocationPosition(index).x"
:y2="getLocationPosition(index).y"
stroke="url(#lineGradient)"
stroke-width="2"
stroke-dasharray="5,5"
stroke-width="1.5"
stroke-dasharray="4,6"
class="animate-pulse"
filter="url(#glow)"
:style="{ 'animation-delay': `${index * 0.2}s` }"
/>
</svg>
<!-- 办公室位置 -->
<div class="relative w-full h-full">
<div class="relative w-full h-full" @click.self="activeLocationId = null">
<div
v-for="(location, index) in companyLocations"
:key="location.id"
class="absolute transform -translate-x-1/2 -translate-y-1/2 wow animate__animated animate__fadeInUp"
class="absolute transform -translate-x-1/2 -translate-y-1/2 wow animate__animated animate__zoomIn"
:class="`animate__delay-${(index % 5) * 100}ms`"
:style="{
left: getLocationPosition(index).x + '%',
top: getLocationPosition(index).y + '%'
top: getLocationPosition(index).y + '%',
zIndex: 20 + index
}"
>
<div class="group cursor-pointer">
<div class="group cursor-pointer" @click="toggleLocationDetail(location.id)">
<!-- 办公室卡片 -->
<div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/20 hover:bg-white/20 transition-all duration-300 transform hover:scale-110">
<!-- 国旗和状态 -->
<div class="flex items-center mb-2">
<img :src="location.image" :alt="location.title + ' flag'" class="w-6 h-4 mr-2 rounded object-cover">
<div class="flex-1">
<h3 class="text-white font-bold text-sm">{{ location.title }}</h3>
<div class="text-white/70 text-xs" v-html="location.description"></div>
</div>
<div class="bg-white/10 backdrop-blur-sm rounded-xl p-2 md:p-3 border border-white/20 hover:bg-white/20 transition-all duration-300 transform hover:scale-110 shadow-lg shadow-primary/20 min-w-[60px] md:min-w-[120px] relative"
:class="{ 'bg-white/25 scale-110': activeLocationId === location.id }">
<!-- 手机端简化显示 -->
<div class="md:hidden flex flex-col items-center">
<img :src="location.image" :alt="location.title + ' flag'" class="w-6 h-4 mb-1 rounded object-cover">
<div class="w-2 h-2 bg-orange-400 rounded-full animate-pulse"></div>
</div>
<!-- 装饰点 -->
<div class="w-2 h-2 bg-orange-400 rounded-full mx-auto animate-pulse"></div>
<!-- 桌面端完整显示 -->
<div class="hidden md:block">
<!-- 国旗和状态 -->
<div class="flex items-center mb-2">
<img :src="location.image" :alt="location.title + ' flag'" class="w-6 h-4 mr-2 rounded object-cover">
<div class="flex-1">
<h3 class="text-white font-bold text-sm">{{ location.title }}</h3>
<div class="text-white/70 text-xs" v-html="location.description"></div>
</div>
</div>
<!-- 装饰点 -->
<div class="w-2 h-2 bg-orange-400 rounded-full mx-auto animate-pulse"></div>
</div>
</div>
<!-- 手机端点击弹出详情 -->
<div v-if="activeLocationId === location.id"
class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 md:hidden z-50">
<div class="bg-black/90 backdrop-blur-sm rounded-lg p-3 text-white text-xs whitespace-nowrap min-w-[120px] text-center">
<div class="font-bold mb-1">{{ location.title }}</div>
<div class="text-white/80" v-html="location.description"></div>
</div>
<div class="w-2 h-2 bg-black/90 transform rotate-45 mx-auto -mt-1"></div>
</div>
<!-- 悬停时的详细信息 -->
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none">
<!-- 桌面端悬停详情 -->
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none hidden md:block">
<div class="bg-black/80 backdrop-blur-sm rounded-lg p-3 text-white text-xs whitespace-nowrap">
<div class="font-bold">{{ location.title }}, <div v-html="location.description"></div></div>
</div>
@ -160,7 +234,7 @@ onMounted(() => {
<!-- 底部说明 -->
<div class="text-center mt-16">
<p class="text-text-secondary mb-6">
通过全球办公室网络MOSE为不同地区的用户提供本地化服务和支持
{{ t('ecosystem.companies.footer_description') }}
</p>
<div class="flex justify-center space-x-2">
<span v-for="i in 10" :key="i"
@ -207,4 +281,22 @@ onMounted(() => {
.animate-pulse {
animation: pulse 2s infinite;
}
</style>
/* 悬停效果增强 */
.group:hover .bg-white\/10 {
background-color: rgba(255, 255, 255, 0.25);
box-shadow: 0 0 15px rgba(139, 92, 246, 0.5);
}
/* 连接线动画 */
@keyframes dash {
to {
stroke-dashoffset: 20;
}
}
svg line {
stroke-dashoffset: 0;
animation: dash 20s linear infinite;
}
</style>

+ 6
- 5
src/components/about/EcosystemModule.vue View File

@ -30,19 +30,20 @@ const ecosystems = ref<EcosystemItem[]>(defaultEcosystems);
const loading = ref(false);
const error = ref<string | null>(null);
//
// - 使i18n
const fetchEcosystems = async () => {
loading.value = true;
error.value = null;
try {
const response = await queryEcosystemList();
if (response && Array.isArray(response) && response.length > 0) {
ecosystems.value = response;
}
// API使i18n
ecosystems.value = defaultEcosystems;
} catch (err) {
console.error('获取生态系统数据失败:', err);
error.value = '获取生态系统数据失败';
// 使API使i18n
ecosystems.value = defaultEcosystems;
} finally {
loading.value = false;
}
@ -84,4 +85,4 @@ onMounted(() => {
</div>
</div>
</section>
</template>
</template>

+ 299
- 256
src/components/about/MilestoneModule.vue View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, onMounted, reactive } from 'vue';
import { ref, onMounted } from 'vue';
import { queryCourseList } from '@/api';
import type { CourseItem } from '@/api';
import { useConfig } from '@/utils/config';
@ -14,34 +14,7 @@ const milestones = ref<CourseItem[]>([]);
const loading = ref(true);
const error = ref<string | null>(null);
//
const getMilestoneStyle = (index: number) => {
const layouts = ['left', 'right', 'center'];
const styles = ['large', 'medium', 'full'];
const overlayColors = [
'from-indigo-900/80 to-transparent',
'from-emerald-900/80 to-transparent',
'from-purple-900/70 via-purple-900/40 to-transparent',
'from-blue-900/80 to-transparent',
'from-gray-900/80 to-transparent',
'from-amber-900/70 via-amber-900/40 to-transparent'
];
const bgColors = [
'bg-gradient-to-br from-black to-indigo-900/30',
'bg-gradient-to-tl from-black to-emerald-900/30',
'bg-gradient-to-r from-black via-purple-900/20 to-black',
'bg-gradient-to-br from-black to-blue-900/30',
'bg-gradient-to-tl from-black to-gray-900/30',
'bg-gradient-to-r from-black via-amber-900/20 to-black'
];
return {
layout: layouts[index % layouts.length],
style: styles[index % styles.length],
overlayColor: overlayColors[index % overlayColors.length],
bgColor: bgColors[index % bgColors.length]
};
};
//
const fetchMilestones = async () => {
@ -52,6 +25,8 @@ const fetchMilestones = async () => {
const response = await queryCourseList();
if (response && Array.isArray(response) && response.length > 0) {
milestones.value = response;
//
setTimeout(() => animateTimelineNodes(), 500);
}
} catch (err) {
console.error('获取发展历程数据失败:', err);
@ -61,22 +36,50 @@ const fetchMilestones = async () => {
}
};
//
const parallaxOffset = ref({ x: 0, y: 0 });
//
const handleMouseMove = (event: MouseEvent) => {
const x = (event.clientX / window.innerWidth - 0.5) * 20;
const y = (event.clientY / window.innerHeight - 0.5) * 20;
parallaxOffset.value = { x, y };
};
//
const scrollToNext = (currentIndex: number) => {
const nextElement = document.querySelectorAll('.milestone-section')[currentIndex + 1];
if (nextElement) {
nextElement.scrollIntoView({ behavior: 'smooth' });
//
const animateTimelineNodes = () => {
const nodes = document.querySelectorAll('.timeline-node');
const container = document.querySelector('.horizontal-timeline-container');
// CSS
if (container && nodes.length > 0) {
container.style.setProperty('--node-count', nodes.length.toString());
}
//
nodes.forEach(node => {
node.style.opacity = '0';
node.style.transform = 'translateY(30px)';
});
//
nodes.forEach((node, index) => {
setTimeout(() => {
// 使
node.style.opacity = '1';
node.style.transform = 'translateY(0)';
//
const nodeDot = node.querySelector('.node-dot');
if (nodeDot) {
nodeDot.classList.add('animated');
}
//
if (index === nodes.length - 1) {
// 线
const timelineAxis = document.querySelector('.horizontal-timeline-axis');
if (timelineAxis) {
timelineAxis.style.transition = 'all 0.5s ease';
timelineAxis.style.boxShadow = '0 0 20px rgba(255, 255, 255, 0.5)';
setTimeout(() => {
timelineAxis.style.boxShadow = '0 0 10px rgba(255, 255, 255, 0.2)';
}, 1000);
}
}
}, index * 500); // 500ms使
});
};
onMounted(() => {
@ -89,7 +92,7 @@ onMounted(() => {
new WOW({
boxClass: 'wow',
animateClass: 'animate__animated',
offset: 100,
offset: 50,
mobile: true,
live: true
}).init();
@ -97,24 +100,21 @@ onMounted(() => {
} catch (error) {
console.error('Failed to initialize WOW.js:', error);
}
//
window.addEventListener('mousemove', handleMouseMove);
});
</script>
<template>
<div class="milestone-gallery">
<div class="milestone-module">
<!-- 加载状态 -->
<div v-if="loading" class="h-screen flex items-center justify-center">
<div v-if="loading" class="h-64 flex items-center justify-center">
<div class="text-center">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary mx-auto mb-4"></div>
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary mx-auto mb-4"></div>
<p class="text-text-secondary">加载里程碑数据中...</p>
</div>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="h-screen flex items-center justify-center">
<div v-else-if="error" class="h-64 flex items-center justify-center">
<div class="text-center">
<p class="text-red-500 mb-4">{{ error }}</p>
<button @click="fetchMilestones" class="px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors">
@ -123,192 +123,249 @@ onMounted(() => {
</div>
</div>
<!-- 里程碑展示 -->
<div
v-else
v-for="(milestone, index) in milestones"
:key="milestone.id"
class="milestone-section relative w-full overflow-hidden"
:class="[
getMilestoneStyle(index).bgColor,
getMilestoneStyle(index).style === 'full' ? 'h-screen' : (getMilestoneStyle(index).style === 'large' ? 'h-[85vh]' : 'h-[70vh]')
]"
>
<!-- 图片容器 - 视差效果 -->
<div class="absolute inset-0 w-full h-full overflow-hidden">
<!-- 图片 - 添加视差效果 -->
<img
:src="'/LOGO.png'"
:alt="milestone.title"
class="w-full h-full object-cover transition-transform duration-700 ease-out"
:style="{
transform: `scale(1.1) translate(${parallaxOffset.x * (index % 3 - 1) * 0.1}px, ${parallaxOffset.y * (index % 2 ? 1 : -1) * 0.1}px)`
}"
/>
<!-- 渐变叠加层 - 每个里程碑有不同的渐变 -->
<div
class="absolute inset-0 bg-gradient-to-r"
:class="getMilestoneStyle(index).overlayColor"
></div>
</div>
<!-- 水平里程碑时间线 -->
<div v-else class="horizontal-timeline-container">
<!-- 水平时间线轴 -->
<div class="horizontal-timeline-axis"></div>
<!-- 内容区域 - 根据布局调整位置 -->
<div
class="absolute inset-0 flex items-center"
:class="{
'justify-start': getMilestoneStyle(index).layout === 'left',
'justify-end': getMilestoneStyle(index).layout === 'right',
'justify-center': getMilestoneStyle(index).layout === 'center'
}"
>
<!-- 里程碑节点 -->
<div class="horizontal-timeline-nodes">
<div
class="max-w-xl p-8 md:p-16 backdrop-blur-sm bg-black/10 rounded-xl border border-white/10"
:class="{
'ml-0 md:ml-16': getMilestoneStyle(index).layout === 'left',
'mr-0 md:mr-16': getMilestoneStyle(index).layout === 'right',
'mx-auto text-center': getMilestoneStyle(index).layout === 'center'
}"
v-for="(milestone, index) in milestones"
:key="milestone.id"
class="timeline-node"
>
<!-- 年份编号 - 不同样式 -->
<div
class="mb-6"
:class="{'flex items-center': getMilestoneStyle(index).layout !== 'center'}"
>
<div
class="rounded-full backdrop-blur-sm flex items-center justify-center border border-white/30"
:class="{
'w-16 h-16 bg-white/10': getMilestoneStyle(index).style !== 'full',
'w-20 h-20 bg-white/20': getMilestoneStyle(index).style === 'full',
'mx-auto': getMilestoneStyle(index).layout === 'center'
}"
>
<span
class="font-bold text-white"
:class="{
'text-3xl': getMilestoneStyle(index).style !== 'full',
'text-4xl': getMilestoneStyle(index).style === 'full'
}"
>{{ index + 1 }}</span>
</div>
<div
v-if="getMilestoneStyle(index).layout !== 'center'"
class="ml-4 h-px bg-gradient-to-r from-white to-transparent flex-grow"
></div>
</div>
<!-- 标题 - 不同大小和动画 -->
<h2
class="font-bold text-white mb-6 wow animate__animated"
:class="{
'text-4xl md:text-5xl': getMilestoneStyle(index).style === 'medium',
'text-5xl md:text-6xl': getMilestoneStyle(index).style === 'large',
'text-6xl md:text-7xl': getMilestoneStyle(index).style === 'full',
'animate__fadeInUp': getMilestoneStyle(index).layout === 'left' || getMilestoneStyle(index).layout === 'center',
'animate__fadeInRight': getMilestoneStyle(index).layout === 'right'
}"
>
{{ milestone.title }}
</h2>
<!-- 描述 - 不同样式和动画 -->
<div
class="text-white/80 mb-8 wow animate__animated"
:class="{
'text-base': getMilestoneStyle(index).style === 'medium',
'text-lg': getMilestoneStyle(index).style === 'large',
'text-xl': getMilestoneStyle(index).style === 'full',
'animate__fadeInUp animate__delay-xs': getMilestoneStyle(index).layout === 'left' || getMilestoneStyle(index).layout === 'center',
'animate__fadeInRight animate__delay-xs': getMilestoneStyle(index).layout === 'right'
}"
>
<div v-html="milestone.description"></div>
<div class="node-dot">
<span class="node-number">{{ index + 1 }}</span>
</div>
<!-- 里程碑编号 -->
<div
class="flex items-center space-x-2 wow animate__animated animate__fadeInUp animate__delay-sm"
:class="{
'justify-start': getMilestoneStyle(index).layout !== 'center',
'justify-center': getMilestoneStyle(index).layout === 'center'
}"
>
<div class="w-3 h-3 rounded-full bg-white/40 animate-pulse"></div>
<span class="text-white/70 text-sm">里程碑 {{ index + 1 }}/{{ milestones.length }}</span>
<div class="node-content">
<h3 class="node-title">{{ milestone.title }}</h3>
<div class="node-description" v-html="milestone.description"></div>
</div>
</div>
</div>
<!-- 装饰元素 - 随机位置 -->
<div
class="absolute"
:class="{
'bottom-8 right-8': index % 3 === 0,
'top-8 right-8': index % 3 === 1,
'bottom-8 left-8': index % 3 === 2
}"
>
<div class="flex items-center space-x-4">
<div class="w-3 h-3 rounded-full bg-white/40 animate-pulse"></div>
<div class="w-3 h-3 rounded-full bg-white/60 animate-pulse" style="animation-delay: 0.5s"></div>
<div class="w-3 h-3 rounded-full bg-white/80 animate-pulse" style="animation-delay: 1s"></div>
</div>
</div>
<!-- 浮动装饰 -->
<div
v-if="index % 2 === 0"
class="absolute top-1/4 right-1/4 w-32 h-32 rounded-full border border-white/20 opacity-50 animate-float"
:style="{animationDelay: `${index * 0.2}s`}"
></div>
<div
v-if="index % 2 === 1"
class="absolute bottom-1/4 left-1/4 w-24 h-24 rounded-full border border-white/20 opacity-50 animate-float-reverse"
:style="{animationDelay: `${index * 0.2}s`}"
></div>
<!-- 滚动指示器 (除了最后一个) -->
<div
v-if="index < milestones.length - 1"
class="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white/70 text-sm flex flex-col items-center cursor-pointer hover:text-white transition-colors duration-300"
@click="scrollToNext(index)"
>
<span class="mb-1">继续探索</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 animate-bounce-soft" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</div>
</div>
</div>
</template>
<style scoped>
.milestone-gallery {
scroll-snap-type: y proximity;
overflow-y: auto;
scroll-behavior: smooth;
/* 时间线容器 */
.milestone-timeline-container {
padding: 2rem 0;
position: relative;
max-width: 100%;
}
/* 水平时间线容器 */
.horizontal-timeline-container {
position: relative;
padding: 6rem 0 4rem;
width: 100%;
overflow-x: hidden;
min-height: 300px;
}
/* 添加时间线两端的装饰点 */
.horizontal-timeline-container:before,
.horizontal-timeline-container:after {
content: '';
position: absolute;
top: 50px;
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
z-index: 2;
}
.horizontal-timeline-container:before {
left: 0;
}
.horizontal-timeline-container:after {
right: 0;
}
/* 水平时间线轴 */
.horizontal-timeline-axis {
position: absolute;
left: 0;
right: 0;
top: 50px;
height: 4px;
background: linear-gradient(to right, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
z-index: 1;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
}
/* 水平节点容器 */
.horizontal-timeline-nodes {
position: relative;
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
z-index: 2;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
margin-top: 80px;
}
/* 单个节点 */
.timeline-node {
position: relative;
min-height: 100px;
width: calc(100% / var(--node-count, 5) - 20px);
max-width: 220px;
opacity: 0; /* 初始隐藏,通过动画显示 */
display: flex;
flex-direction: column;
align-items: center;
transition: opacity 0.8s ease, transform 0.8s ease;
}
/* 节点圆点 */
.node-dot {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(4px);
border: 2px solid rgba(255, 255, 255, 0.5);
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: -70px; /* 位于时间线上 */
left: 50%;
transform: translateX(-50%);
z-index: 3;
box-shadow: 0 0 15px rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
opacity: 0; /* 初始隐藏 */
}
.milestone-section {
scroll-snap-align: start;
.timeline-node:hover .node-dot {
background: rgba(255, 255, 255, 0.3);
box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
transform: translateX(-50%) scale(1.1);
}
/* 节点编号 */
.node-number {
font-size: 1.25rem;
font-weight: bold;
color: white;
}
/* 节点内容 */
.node-content {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
border-radius: 0.75rem;
padding: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
margin-top: 20px;
position: relative;
width: 100%;
max-width: 220px;
}
.node-content:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
/* 节点内容前的连接线 - 从点到内容 */
.node-content:before {
content: '';
position: absolute;
top: -20px;
left: 50%;
width: 2px;
height: 20px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
transform: translateX(-50%);
}
/* 节点标题 */
.node-title {
font-size: 1.5rem;
font-weight: bold;
color: white;
margin-bottom: 0.75rem;
}
/* 动画延迟类 */
.animate__delay-xs {
animation-delay: 0.2s;
/* 节点描述 */
.node-description {
color: rgba(255, 255, 255, 0.8);
font-size: 1rem;
line-height: 1.6;
}
.animate__delay-sm {
animation-delay: 0.4s;
/* 节点样式变化 - 交替颜色 */
.timeline-node:nth-child(odd) .node-content {
background: rgba(6, 95, 70, 0.3);
}
.animate__delay-md {
animation-delay: 0.6s;
.timeline-node:nth-child(even) .node-content {
background: rgba(30, 58, 138, 0.3);
}
/* 响应式调整 */
@media (max-width: 768px) {
.horizontal-timeline-container {
padding: 2rem 0;
overflow-x: auto;
}
.horizontal-timeline-nodes {
flex-wrap: nowrap;
justify-content: flex-start;
min-width: 800px;
padding: 0 1rem;
}
.horizontal-timeline-axis {
top: 40px;
min-width: 800px;
}
.node-dot {
top: -60px;
width: 30px;
height: 30px;
}
.node-content {
padding: 1rem;
max-width: 150px;
}
.node-content:before {
height: 15px;
top: -15px;
}
.node-number {
font-size: 0.875rem;
}
.node-title {
font-size: 1rem;
}
.node-description {
font-size: 0.875rem;
}
}
/* 脉冲动画 */
/* 保留一些有用的动画 */
@keyframes pulse {
0%, 100% {
opacity: 0.6;
@ -324,59 +381,45 @@ onMounted(() => {
animation: pulse 2s infinite;
}
/* 更柔和的弹跳动画 */
@keyframes bounce-soft {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-8px);
}
60% {
transform: translateY(-4px);
}
/* 动画效果 */
.animate__fadeInUp {
animation-duration: 0.8s;
}
.animate-bounce-soft {
animation: bounce-soft 2s infinite;
}
/* 浮动动画 */
@keyframes float {
0%, 100% {
transform: translateY(0) translateX(0);
}
25% {
transform: translateY(-10px) translateX(5px);
/* 节点显示动画 */
@keyframes nodeAppear {
0% {
opacity: 0;
transform: translateY(30px);
}
50% {
transform: translateY(0) translateX(10px);
opacity: 0.5;
}
75% {
transform: translateY(10px) translateX(5px);
100% {
opacity: 1;
transform: translateY(0);
}
}
.animate-float {
animation: float 8s ease-in-out infinite;
}
@keyframes float-reverse {
0%, 100% {
transform: translateY(0) translateX(0);
}
25% {
transform: translateY(10px) translateX(-5px);
/* 节点圆点动画 */
@keyframes dotAppear {
0% {
opacity: 0;
transform: translateX(-50%) scale(0.5);
}
50% {
transform: translateY(0) translateX(-10px);
opacity: 0.8;
transform: translateX(-50%) scale(1.2);
}
75% {
transform: translateY(-10px) translateX(-5px);
100% {
opacity: 1;
transform: translateX(-50%) scale(1);
}
}
.animate-float-reverse {
animation: float-reverse 8s ease-in-out infinite;
/* 节点圆点动画应用 */
.node-dot.animated {
animation: dotAppear 0.6s forwards;
opacity: 1;
}
</style>
</style>

+ 186
- 41
src/components/about/PartnersModule.vue View File

@ -2,21 +2,94 @@
import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { queryPartnerList, type PartnerItem } from '@/api';
import { Swiper, SwiperSlide } from 'swiper/vue';
import { Autoplay, Navigation } from 'swiper/modules';
// Swiper
import 'swiper/css';
import 'swiper/css/navigation';
const { t } = useI18n();
//
const partners = ref<PartnerItem[]>([]);
//
const mediaPartners = ref<PartnerItem[]>([]);
const loading = ref(false);
//
const fetchPartnerList = async () => {
// API使
const defaultMediaPartners = [
{
id: 1,
title: 'Binance',
image: 'https://cryptologos.cc/logos/binance-coin-bnb-logo.png',
description: '全球领先的加密货币交易所'
},
{
id: 2,
title: 'Coinbase',
image: 'https://cryptologos.cc/logos/coinbase-coin-logo.png',
description: '美国最大的加密货币交易平台'
},
{
id: 3,
title: 'Ripple',
image: 'https://cryptologos.cc/logos/xrp-xrp-logo.png',
description: '全球支付网络和数字资产'
},
{
id: 4,
title: 'Kraken',
image: 'https://cryptologos.cc/logos/kraken-krak-logo.png',
description: '领先的加密货币交易所和银行'
},
{
id: 5,
title: 'OKX',
image: 'https://cryptologos.cc/logos/okb-okb-logo.png',
description: '全球领先的数字资产交易平台'
},
{
id: 6,
title: 'Huobi',
image: 'https://cryptologos.cc/logos/huobi-token-ht-logo.png',
description: '全球领先的数字资产交易平台'
},
{
id: 7,
title: 'Chainalysis',
image: 'https://cryptologos.cc/logos/chainalysis-logo.png',
description: '区块链数据分析公司'
},
{
id: 8,
title: 'Ledger',
image: 'https://cryptologos.cc/logos/ledger-logo.png',
description: '硬件钱包和安全解决方案提供商'
},
{
id: 9,
title: 'ConsenSys',
image: 'https://cryptologos.cc/logos/consensys-logo.png',
description: '以太坊软件公司'
},
{
id: 10,
title: 'StarkWare',
image: 'https://cryptologos.cc/logos/starkware-logo.png',
description: 'Layer 2扩展解决方案提供商'
}
];
//
const fetchMediaList = async () => {
try {
loading.value = true;
const data = await queryPartnerList();
partners.value = data;
// API使
mediaPartners.value = data && data.length > 0 ? data : defaultMediaPartners;
} catch (error) {
console.error('获取合作伙伴列表数据失败:', error);
console.error('获取媒体列表数据失败:', error);
// 使
mediaPartners.value = defaultMediaPartners;
} finally {
loading.value = false;
}
@ -24,14 +97,14 @@ const fetchPartnerList = async () => {
//
onMounted(() => {
fetchPartnerList();
fetchMediaList();
});
</script>
<template>
<section class="py-16 px-6 md:px-12 lg:px-24">
<section class="py-16 px-6 md:px-12 lg:px-24 bg-background pb-12">
<div class="container mx-auto">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-12 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
{{ t('about.partners.title') }}
</h2>
@ -40,40 +113,112 @@ onMounted(() => {
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary"></div>
</div>
<div v-else class="space-y-8">
<div
v-for="(partner, index) in partners"
:key="partner.id"
class="flex flex-col md:flex-row bg-background rounded-xl overflow-hidden shadow-card hover:shadow-lg transition-all duration-300 wow animate__animated animate__fadeInUp"
:class="{
'animate__delay-xs': index % 4 === 1,
'animate__delay-sm': index % 4 === 2,
'animate__delay-md': index % 4 === 3
<!-- 媒体Logo墙 - Swiper滚动 -->
<div v-else class="max-w-7xl mx-auto">
<swiper
:modules="[Navigation, Autoplay]"
:slides-per-view="2"
:space-between="20"
:navigation="true"
:autoplay="{
delay: 3000,
disableOnInteraction: false,
}"
:breakpoints="{
'640': {
slidesPerView: 3,
spaceBetween: 20,
},
'768': {
slidesPerView: 4,
spaceBetween: 30,
},
'1024': {
slidesPerView: 5,
spaceBetween: 30,
},
}"
class="media-swiper w-full py-4"
>
<!-- 合作伙伴Logo -->
<div class="md:w-1/3 p-8 flex items-center justify-center bg-background-light">
<img :src="partner.image" :alt="partner.title" class="max-w-full max-h-24 object-contain" />
</div>
<!-- 合作伙伴信息 -->
<div class="md:w-2/3 p-6">
<h3 class="text-xl font-bold text-text mb-3">{{ partner.title }}</h3>
<p class="text-text-secondary mb-4" v-html="partner.description"></p>
<a
href="#"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center text-primary-light hover:text-primary-dark transition-colors"
>
<span>{{ t('about.partners.visitWebsite') }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
</div>
<swiper-slide
v-for="(media, index) in mediaPartners"
:key="media.id"
class="wow animate__animated animate__fadeIn"
:class="{
'animate__delay-xs': index % 5 === 1,
'animate__delay-sm': index % 5 === 2,
'animate__delay-md': index % 5 === 3,
'animate__delay-lg': index % 5 === 4
}"
>
<div class="group w-full aspect-square flex items-center justify-center bg-white rounded-lg shadow-md hover:shadow-lg hover:-translate-y-1 transition-all duration-300 border border-gray-100/5 overflow-hidden relative">
<img
:src="media.image"
:alt="media.title"
class="w-full h-full rounded-lg transition-transform duration-300 bg-white hover:scale-90 object-cover"
:title="media.title"
/>
<div class="absolute bottom-0 left-0 right-0 bg-black/75 text-white py-2.5 px-2 text-sm text-center whitespace-nowrap overflow-hidden text-ellipsis transform translate-y-full transition-transform duration-300 font-medium group-hover:translate-y-0">
{{ media.title }}
</div>
</div>
</swiper-slide>
</swiper>
</div>
</div>
</section>
</template>
</template>
<style scoped>
/* Swiper容器样式 */
.media-swiper {
padding: 10px 5px 30px 5px;
margin: 0 -5px;
}
/* 导航按钮样式 */
.media-swiper :deep(.swiper-button-next),
.media-swiper :deep(.swiper-button-prev) {
color: var(--color-primary);
background-color: rgba(255, 255, 255, 0.8);
width: 40px;
height: 40px;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.media-swiper :deep(.swiper-button-next)::after,
.media-swiper :deep(.swiper-button-prev)::after {
font-size: 18px;
font-weight: bold;
}
.media-swiper :deep(.swiper-button-next) {
right: 10px;
}
.media-swiper :deep(.swiper-button-prev) {
left: 10px;
}
.media-swiper :deep(.swiper-button-disabled) {
opacity: 0.35;
cursor: auto;
pointer-events: none;
}
/* 在小屏幕上隐藏导航按钮 */
@media (max-width: 640px) {
.media-swiper :deep(.swiper-button-next),
.media-swiper :deep(.swiper-button-prev) {
display: none;
}
}
/* 确保滑块内容在各种屏幕尺寸下都能正确显示 */
.media-swiper :deep(.swiper-slide) {
height: auto;
display: flex;
justify-content: center;
}
</style>

+ 42
- 18
src/components/about/TeamModule.vue View File

@ -101,7 +101,8 @@ const initSwiper = () => {
breakpoints: {
320: {
slidesPerView: 1,
spaceBetween: 20
spaceBetween: 15,
centeredSlides: true
},
768: {
slidesPerView: 2,
@ -135,7 +136,7 @@ const initSwiper = () => {
<!-- 加载状态 -->
<div v-if="loading" class="flex justify-center items-center py-16">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
<span class="ml-3 text-text-secondary">加载中...</span>
<span class="ml-3 text-text-secondary">{{ t('common.loading') }}</span>
</div>
<!-- Swiper轮播 -->
@ -147,7 +148,7 @@ const initSwiper = () => {
:key="member.id"
class="swiper-slide"
>
<div class="team-card relative overflow-hidden rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 mx-2 my-8 h-[450px]">
<div class="team-card relative overflow-hidden rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-500 mx-2 my-8 h-[400px] md:h-[450px]">
<!-- 背景 -->
<div class="absolute inset-0" :class="getCardBackground()"></div>
@ -156,7 +157,7 @@ const initSwiper = () => {
<img
:src="member.image"
:alt="member.name"
class="w-full h-full object-cover"
class="w-full h-full object-contain"
/>
<!-- 渐变遮罩 -->
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/50"></div>
@ -165,24 +166,25 @@ const initSwiper = () => {
<!-- 内容 -->
<div class="relative p-6 flex flex-col h-full pt-[52%]">
<div class="relative p-4 md:p-6 flex flex-col h-full pt-[70%] md:pt-[52%]">
<!-- 成员姓名 -->
<h3 class="text-xl font-black text-white text-center mb-2">{{ member.name }}</h3>
<h3 class="text-lg md:text-xl font-black text-white text-center mb-2">{{ member.name }}</h3>
<!-- 成员职位 -->
<p class="text-white/90 text-center text-sm mb-4">{{ member.post }}</p>
<p class="text-white/90 text-center text-xs md:text-sm mb-2 md:mb-3">{{ member.post }}</p>
<!-- 装饰线 -->
<div class="w-16 h-1 bg-white/30 mx-auto mb-4"></div>
<div class="w-12 md:w-16 h-1 bg-white/30 mx-auto mb-2 md:mb-3"></div>
<!-- 成员简介 -->
<div class="text-white/90 text-center text-sm leading-relaxed flex-grow">
<div v-html="member.resume"></div>
<div class="text-white/90 text-center text-xs md:text-sm leading-relaxed flex-grow">
<!-- 多行文本溢出显示省略号 -->
<div v-html="member.resume" class="line-clamp-3 md:line-clamp-4 overflow-hidden"></div>
</div>
<!-- 成员编号 -->
<div class="absolute top-4 right-4 bg-white/20 backdrop-blur-sm w-8 h-8 rounded-full flex items-center justify-center">
<span class="text-white font-bold text-sm">{{ index + 1 }}</span>
<div class="absolute top-3 md:top-4 right-3 md:right-4 bg-white/20 backdrop-blur-sm w-6 h-6 md:w-8 md:h-8 rounded-full flex items-center justify-center">
<span class="text-white font-bold text-xs md:text-sm">{{ index + 1 }}</span>
</div>
</div>
</div>
@ -198,13 +200,13 @@ const initSwiper = () => {
<div v-if="!loading && teamMembers.length > 0" class="swiper-button-next !text-primary after:!text-lg"></div>
</div>
<div class="text-center mt-8">
<!-- <div class="text-center mt-8">
<p class="text-text-secondary mb-6">{{ t('about.team.description') }}</p>
<a href="#" class="inline-flex items-center text-primary-light hover:text-primary-dark transition-colors">
<span>{{ t('about.team.viewAll') }}</span>
<Icon icon="carbon:arrow-right" class="h-5 w-5 ml-1" />
</a>
</div>
</div> -->
</div>
</section>
</template>
@ -220,11 +222,18 @@ const initSwiper = () => {
:deep(.swiper-slide) {
width: 350px;
width: 300px;
transition: transform 0.3s;
opacity: 0.4;
}
@media (max-width: 768px) {
:deep(.swiper-slide) {
width: 280px;
opacity: 1;
}
}
:deep(.swiper-slide-active) {
transform: scale(1.05);
z-index: 2;
@ -252,13 +261,28 @@ const initSwiper = () => {
:deep(.swiper-button-next) {
color: var(--color-primary);
background: rgba(255, 255, 255, 0.3);
width: 50px;
height: 50px;
width: 40px;
height: 40px;
border-radius: 50%;
backdrop-filter: blur(4px);
transition: all 0.3s;
}
@media (min-width: 768px) {
:deep(.swiper-button-prev),
:deep(.swiper-button-next) {
width: 50px;
height: 50px;
}
}
@media (max-width: 640px) {
:deep(.swiper-button-prev),
:deep(.swiper-button-next) {
display: none;
}
}
:deep(.swiper-button-prev:hover),
:deep(.swiper-button-next:hover) {
background: rgba(255, 255, 255, 0.5);
@ -285,4 +309,4 @@ const initSwiper = () => {
section {
overflow-x: hidden;
}
</style>
</style>

+ 438
- 0
src/components/common/RichTextRenderer.vue View File

@ -0,0 +1,438 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
const props = defineProps({
content: {
type: String,
required: true
},
isRightAligned: {
type: Boolean,
default: false
},
textSize: {
type: String,
default: 'base' // sm, base, lg, xl
},
textColor: {
type: String,
default: ''
},
maxWidth: {
type: String,
default: ''
},
lineHeight: {
type: String,
default: 'normal' // tight, normal, relaxed, loose
},
paragraphSpacing: {
type: String,
default: 'normal' // tight, normal, relaxed
},
responsive: {
type: Boolean,
default: true
},
animateImages: {
type: Boolean,
default: true
},
showControls: {
type: Boolean,
default: false
}
});
//
const localIsRightAligned = ref(props.isRightAligned);
const localTextSize = ref(props.textSize);
const localLineHeight = ref(props.lineHeight);
const localParagraphSpacing = ref(props.paragraphSpacing);
const localAnimateImages = ref(props.animateImages);
// props
watch(() => props.isRightAligned, (newVal) => {
localIsRightAligned.value = newVal;
});
watch(() => props.textSize, (newVal) => {
localTextSize.value = newVal;
});
watch(() => props.lineHeight, (newVal) => {
localLineHeight.value = newVal;
});
watch(() => props.paragraphSpacing, (newVal) => {
localParagraphSpacing.value = newVal;
});
watch(() => props.animateImages, (newVal) => {
localAnimateImages.value = newVal;
});
//
const emit = defineEmits(['update:isRightAligned', 'update:textSize', 'update:lineHeight', 'update:paragraphSpacing', 'update:animateImages']);
watch(localIsRightAligned, (newVal) => {
emit('update:isRightAligned', newVal);
});
watch(localTextSize, (newVal) => {
emit('update:textSize', newVal);
});
watch(localLineHeight, (newVal) => {
emit('update:lineHeight', newVal);
});
watch(localParagraphSpacing, (newVal) => {
emit('update:paragraphSpacing', newVal);
});
watch(localAnimateImages, (newVal) => {
emit('update:animateImages', newVal);
});
//
const textClasses = computed(() => {
const classes = ['rich-text-content', 'break-words'];
// - 使
if (localIsRightAligned.value) {
classes.push('text-right');
}
// - 使
switch (localTextSize.value) {
case 'sm':
classes.push('text-sm');
break;
case 'base':
classes.push('text-base');
break;
case 'lg':
classes.push('text-lg');
break;
case 'xl':
classes.push('text-xl');
break;
default:
classes.push('text-base');
}
//
if (props.textColor) {
classes.push(props.textColor);
}
// - 使
switch (localLineHeight.value) {
case 'tight':
classes.push('leading-tight');
break;
case 'normal':
classes.push('leading-normal');
break;
case 'relaxed':
classes.push('leading-relaxed');
break;
case 'loose':
classes.push('leading-loose');
break;
}
// - 使
switch (localParagraphSpacing.value) {
case 'tight':
classes.push('paragraph-spacing-tight');
break;
case 'normal':
classes.push('paragraph-spacing-normal');
break;
case 'relaxed':
classes.push('paragraph-spacing-relaxed');
break;
}
//
if (props.responsive) {
classes.push('responsive-content');
}
// - 使
if (localAnimateImages.value) {
classes.push('animate-images');
} else {
classes.push('static-images');
}
return classes.join(' ');
});
//
const containerStyle = computed(() => {
const styles = {};
if (props.maxWidth) {
styles.maxWidth = props.maxWidth;
}
return styles;
});
</script>
<template>
<div>
<!-- 样式控制面板可选 -->
<div v-if="showControls" class="style-controls mb-4 p-3 bg-gray-100 rounded-lg">
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<!-- 文本对齐 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">对齐方式</label>
<select v-model="localIsRightAligned" class="w-full rounded border-gray-300 text-sm">
<option :value="false">左对齐</option>
<option :value="true">右对齐</option>
</select>
</div>
<!-- 文本大小 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">文本大小</label>
<select v-model="localTextSize" class="w-full rounded border-gray-300 text-sm">
<option value="sm"></option>
<option value="base"></option>
<option value="lg"></option>
<option value="xl">特大</option>
</select>
</div>
<!-- 行高 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">行高</label>
<select v-model="localLineHeight" class="w-full rounded border-gray-300 text-sm">
<option value="tight">紧凑</option>
<option value="normal">正常</option>
<option value="relaxed">宽松</option>
<option value="loose">特宽</option>
</select>
</div>
<!-- 段落间距 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">段落间距</label>
<select v-model="localParagraphSpacing" class="w-full rounded border-gray-300 text-sm">
<option value="tight">紧凑</option>
<option value="normal">正常</option>
<option value="relaxed">宽松</option>
</select>
</div>
</div>
<!-- 图片动画开关 -->
<div class="mt-3">
<label class="inline-flex items-center">
<input type="checkbox" v-model="localAnimateImages" class="rounded border-gray-300 text-primary">
<span class="ml-2 text-sm text-gray-700">启用图片动画效果</span>
</label>
</div>
</div>
<!-- 富文本内容 -->
<div :class="textClasses" :style="containerStyle" v-html="content"></div>
</div>
</template>
<style scoped>
.rich-text-content {
word-break: break-word;
overflow-wrap: break-word;
width: 100%;
}
/* 段落间距 */
.paragraph-spacing-tight :deep(p) {
margin-bottom: 0.5rem;
}
.paragraph-spacing-normal :deep(p) {
margin-bottom: 0.75rem;
}
.paragraph-spacing-relaxed :deep(p) {
margin-bottom: 1rem;
}
/* 列表样式 */
.rich-text-content :deep(ul), .rich-text-content :deep(ol) {
padding-left: 1.5rem;
margin-bottom: 1rem;
}
.rich-text-content :deep(li) {
margin-bottom: 0.25rem;
}
.paragraph-spacing-tight :deep(li) {
margin-bottom: 0.2rem;
}
.paragraph-spacing-normal :deep(li) {
margin-bottom: 0.25rem;
}
.paragraph-spacing-relaxed :deep(li) {
margin-bottom: 0.4rem;
}
/* 图片基本样式 */
.rich-text-content :deep(img) {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
border-radius: 0.75rem;
}
/* 图片动画效果 */
.animate-images :deep(img) {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
transition: transform 0.5s, box-shadow 0.5s;
}
.animate-images :deep(img:hover) {
transform: translateY(-5px) scale(1.02);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
}
/* 静态图片 */
.static-images :deep(img) {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
/* 右对齐内容 */
.text-right :deep(img) {
margin-left: auto;
margin-right: 0;
}
/* 响应式内容 */
.responsive-content :deep(table) {
width: 100%;
overflow-x: auto;
display: block;
}
@media (max-width: 640px) {
.responsive-content :deep(img) {
max-width: 100%;
}
.responsive-content :deep(pre) {
max-width: 100%;
overflow-x: auto;
}
}
.rich-text-content :deep(table) {
width: 100% !important;
max-width: 100%;
border-collapse: collapse;
overflow-x: auto;
display: block;
}
.rich-text-content :deep(pre),
.rich-text-content :deep(code) {
white-space: pre-wrap;
word-break: break-all;
}
.rich-text-content :deep(p) {
margin-bottom: 0.5rem;
line-height: 1.4;
}
.text-right :deep(p),
.text-right :deep(h1),
.text-right :deep(h2),
.text-right :deep(h3),
.text-right :deep(h4),
.text-right :deep(h5),
.text-right :deep(h6) {
text-align: right;
}
.rich-text-content :deep(ul),
.rich-text-content :deep(ol) {
margin-bottom: 1rem;
}
.rich-text-content :deep(ul),
.rich-text-content :deep(ol) {
margin-left: 1.5rem;
}
.text-right :deep(ul),
.text-right :deep(ol) {
margin-left: 0;
margin-right: 1.5rem;
direction: rtl;
}
.text-right :deep(li) {
direction: ltr;
text-align: right;
}
.rich-text-content :deep(li) {
margin-bottom: 0.2rem;
line-height: 1.4;
}
.rich-text-content :deep(a) {
color: var(--primary, #3B82F6);
text-decoration: underline;
transition: color 0.3s;
}
.rich-text-content :deep(a:hover) {
color: var(--primary-dark, #2563EB);
}
.rich-text-content :deep(h1),
.rich-text-content :deep(h2),
.rich-text-content :deep(h3),
.rich-text-content :deep(h4),
.rich-text-content :deep(h5),
.rich-text-content :deep(h6) {
font-weight: bold;
margin: 1em 0 0.5em 0;
}
/* 响应式调整 */
@media (max-width: 640px) {
.rich-text-content :deep(h1) {
font-size: 1.5rem;
}
.rich-text-content :deep(h2) {
font-size: 1.25rem;
}
.rich-text-content :deep(h3) {
font-size: 1.125rem;
}
.rich-text-content :deep(img) {
max-width: 100%;
}
}
@media (min-width: 641px) and (max-width: 1024px) {
.rich-text-content :deep(img) {
max-width: 90%;
}
}
</style>

+ 105
- 0
src/components/common/VideoModal.vue View File

@ -0,0 +1,105 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Icon } from '@iconify/vue';
interface Props {
visible: boolean;
videoUrl: string;
title?: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
close: [];
}>();
const videoRef = ref<HTMLVideoElement>();
//
watch(() => props.visible, (newVisible) => {
if (!newVisible && videoRef.value) {
//
videoRef.value.pause();
}
});
//
const handleClose = () => {
emit('close');
};
//
const handleMaskClick = (e: Event) => {
if (e.target === e.currentTarget) {
handleClose();
}
};
</script>
<template>
<Teleport to="body">
<Transition name="modal">
<div
v-if="visible"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
@click="handleMaskClick"
>
<div class="relative w-full max-w-3xl bg-background rounded-xl overflow-hidden shadow-2xl">
<!-- 头部 -->
<div class="flex items-center justify-between p-4 border-b border-border">
<h3 class="text-lg font-semibold text-text truncate pr-4">
{{ title || '视频播放' }}
</h3>
<button
@click="handleClose"
class="flex-shrink-0 p-2 rounded-lg hover:bg-background-light transition-colors duration-200"
>
<Icon icon="carbon:close" width="20" height="20" class="text-text-secondary" />
</button>
</div>
<!-- 视频内容 -->
<div class="relative aspect-video bg-black">
<video
v-if="videoUrl"
ref="videoRef"
:src="videoUrl"
class="w-full h-full"
controls
autoplay
preload="metadata"
>
您的浏览器不支持视频播放
</video>
<div v-else class="flex items-center justify-center h-full text-text-secondary">
<div class="text-center">
<Icon icon="carbon:video-off" width="48" height="48" class="mx-auto mb-2" />
<p>暂无视频内容</p>
</div>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
/* 弹窗动画 */
.modal-enter-active,
.modal-leave-active {
transition: all 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
transform: scale(0.9);
}
.modal-enter-to,
.modal-leave-from {
opacity: 1;
transform: scale(1);
}
</style>

+ 23
- 20
src/components/ecosystem/AppListModule.vue View File

@ -6,47 +6,47 @@ import type { AppItem } from '@/api/modules/ecosystem';
const { t, locale } = useI18n();
//
// Default application list data
const defaultApps = [
{
id: '1',
title: locale.value === 'zh' ? '移动钱包应用' : 'Mobile Wallet App',
description: locale.value === 'zh' ? '安全的数字资产管理工具' : 'Secure digital asset management tool',
title: t('ecosystem.apps.default_apps.wallet.title'),
description: t('ecosystem.apps.default_apps.wallet.description'),
image: '/LOGO.png',
link: 'https://wallet.mose.io'
},
{
id: '2',
title: locale.value === 'zh' ? '区块浏览器' : 'Block Explorer',
description: locale.value === 'zh' ? '查询区块链交易和地址信息' : 'Query blockchain transactions and address information',
title: t('ecosystem.apps.default_apps.explorer.title'),
description: t('ecosystem.apps.default_apps.explorer.description'),
image: '/LOGO.png',
link: 'https://explorer.mose.io'
},
{
id: '3',
title: locale.value === 'zh' ? '开发者门户' : 'Developer Portal',
description: locale.value === 'zh' ? '构建MOSE应用的开发者资源' : 'Developer resources for building MOSE applications',
title: t('ecosystem.apps.default_apps.developer.title'),
description: t('ecosystem.apps.default_apps.developer.description'),
image: '/LOGO.png',
link: 'https://dev.mose.io'
},
{
id: '4',
title: locale.value === 'zh' ? '交易所工具' : 'Exchange Tools',
description: locale.value === 'zh' ? '针对交易所的集成工具' : 'Integration tools for exchanges',
title: t('ecosystem.apps.default_apps.exchange.title'),
description: t('ecosystem.apps.default_apps.exchange.description'),
image: '/LOGO.png',
link: 'https://exchange.mose.io'
},
{
id: '5',
title: locale.value === 'zh' ? '网络监控' : 'Network Monitor',
description: locale.value === 'zh' ? 'MOSE网络状态实时监控' : 'Real-time monitoring of MOSE network status',
title: t('ecosystem.apps.default_apps.monitor.title'),
description: t('ecosystem.apps.default_apps.monitor.description'),
image: '/LOGO.png',
link: 'https://monitor.mose.io'
},
{
id: '6',
title: locale.value === 'zh' ? '质押管理平台' : 'Staking Platform',
description: locale.value === 'zh' ? '代币质押和收益管理' : 'Token staking and reward management',
title: t('ecosystem.apps.default_apps.staking.title'),
description: t('ecosystem.apps.default_apps.staking.description'),
image: '/LOGO.png',
link: 'https://staking.mose.io'
}
@ -67,20 +67,23 @@ const fetchApps = async () => {
console.log('App list API response:', response);
if (response && Array.isArray(response) && response.length > 0) {
//
const apiApps = response.map((item: AppItem) => {
// 使API使i18n
const apiApps = response.map((item: AppItem, index: number) => {
//
let imagePath = item.image;
if (imagePath && imagePath.startsWith('/public/')) {
imagePath = imagePath.replace('/public/', '/');
}
// 使i18nAPI
const defaultApp = defaultApps[index] || defaultApps[0];
return {
id: item.id,
title: item.title,
description: item.description,
image: imagePath || '/LOGO.png',
link: item.link || '#'
title: defaultApp.title,
description: defaultApp.description,
image: imagePath || defaultApp.image,
link: item.link || defaultApp.link
};
});
@ -168,4 +171,4 @@ onMounted(() => {
</div>
</div>
</section>
</template>
</template>

+ 63
- 39
src/components/ecosystem/CompanyCard.vue View File

@ -100,55 +100,79 @@ const fetchCompanies = async () => {
{ flag: '🇬🇧', position: { x: '45%', y: '30%' }, location: locale.value === 'zh' ? "英国" : "UK" }
];
// API
const convertCompanyData = (apiCompanies: any[]) => {
return apiCompanies.map(company => ({
id: parseInt(company.id) || Math.floor(Math.random() * 10000), // ID
name: company.title,
description: company.description,
focus: company.focus || t('ecosystem.companies.focus'),
team: company.team || t('ecosystem.companies.team'),
location: company.location || '',
flag: company.flag || '',
position: company.position || { x: '0', y: '0' },
image: company.image
}));
// 使API使i18n
const convertCompanyData = (apiCompanies: any[], flagsAndPositions: any[]) => {
return apiCompanies.map((company, index) => {
const flagInfo = flagsAndPositions[index % flagsAndPositions.length];
return {
id: parseInt(company.id) || Math.floor(Math.random() * 10000), // ID
name: t(`ecosystem.companies.items.company${index + 1}.name`),
description: t(`ecosystem.companies.items.company${index + 1}.description`),
focus: t(`ecosystem.companies.items.company${index + 1}.focus`),
team: t(`ecosystem.companies.items.company${index + 1}.team`),
location: flagInfo.location,
flag: flagInfo.flag,
position: flagInfo.position,
image: company.image || '/LOGO.png'
};
});
};
// API
const apiCompanies = response.map((item: CompanyItem, index: number) => {
const flagInfo = flagsAndPositions[index % flagsAndPositions.length];
//
const descLines = item.description ? item.description.split('\n').filter(line => line.trim()) : [];
const focus = descLines.length > 0 ? descLines[0] : locale.value === 'zh' ? "业务发展" : "Business Development";
const team = descLines.length > 1 ? descLines[1] : "20+";
return {
id: item.id,
name: item.title,
description: item.description,
focus: focus,
team: team,
location: flagInfo.location,
flag: flagInfo.flag,
position: flagInfo.position,
image: item.image
};
});
console.log('Processed company data:', apiCompanies);
// API使i18n
console.log('Processing company data with i18n...');
if (apiCompanies.length > 0) {
companies.value = convertCompanyData(apiCompanies);
if (response.length > 0) {
companies.value = convertCompanyData(response, flagsAndPositions);
selectedCompany.value = companies.value[0];
}
} else {
console.log('No company data found, using defaults');
// 使i18n
const flagsAndPositions = [
{ flag: '🇸🇬', position: { x: '75%', y: '60%' }, location: locale.value === 'zh' ? "新加坡" : "Singapore" },
{ flag: '🇭🇰', position: { x: '78%', y: '50%' }, location: locale.value === 'zh' ? "香港" : "Hong Kong" },
{ flag: '🇨🇭', position: { x: '48%', y: '35%' }, location: locale.value === 'zh' ? "瑞士" : "Switzerland" },
{ flag: '🇯🇵', position: { x: '85%', y: '40%' }, location: locale.value === 'zh' ? "日本" : "Japan" },
{ flag: '🇬🇧', position: { x: '45%', y: '30%' }, location: locale.value === 'zh' ? "英国" : "UK" }
];
companies.value = flagsAndPositions.map((flagInfo, index) => ({
id: index + 1,
name: t(`ecosystem.companies.items.company${index + 1}.name`),
description: t(`ecosystem.companies.items.company${index + 1}.description`),
focus: t(`ecosystem.companies.items.company${index + 1}.focus`),
team: t(`ecosystem.companies.items.company${index + 1}.team`),
location: flagInfo.location,
flag: flagInfo.flag,
position: flagInfo.position,
image: '/LOGO.png'
}));
selectedCompany.value = companies.value[0];
}
} catch (err) {
console.error('Failed to fetch companies:', err);
error.value = 'Failed to load company data';
error.value = t('common.error.loadFailed');
// 使i18n
const flagsAndPositions = [
{ flag: '🇸🇬', position: { x: '75%', y: '60%' }, location: locale.value === 'zh' ? "新加坡" : "Singapore" },
{ flag: '🇭🇰', position: { x: '78%', y: '50%' }, location: locale.value === 'zh' ? "香港" : "Hong Kong" },
{ flag: '🇨🇭', position: { x: '48%', y: '35%' }, location: locale.value === 'zh' ? "瑞士" : "Switzerland" },
{ flag: '🇯🇵', position: { x: '85%', y: '40%' }, location: locale.value === 'zh' ? "日本" : "Japan" },
{ flag: '🇬🇧', position: { x: '45%', y: '30%' }, location: locale.value === 'zh' ? "英国" : "UK" }
];
companies.value = flagsAndPositions.map((flagInfo, index) => ({
id: index + 1,
name: t(`ecosystem.companies.items.company${index + 1}.name`),
description: t(`ecosystem.companies.items.company${index + 1}.description`),
focus: t(`ecosystem.companies.items.company${index + 1}.focus`),
team: t(`ecosystem.companies.items.company${index + 1}.team`),
location: flagInfo.location,
flag: flagInfo.flag,
position: flagInfo.position,
image: '/LOGO.png'
}));
selectedCompany.value = companies.value[0];
} finally {
loading.value = false;
}


+ 23
- 23
src/components/ecosystem/EcosystemCard.vue View File

@ -7,80 +7,80 @@ import type { EcosystemItem } from '@/api/modules/ecosystem';
const { t, locale } = useI18n();
//
const defaultEcosystems = [
const defaultEcosystems = computed(() => [
{
id: 1,
name: locale.value === 'zh' ? "MOSE L1链" : "MOSE L1 Chain",
description: locale.value === 'zh' ? "高性能可扩展底层区块链" : "High-performance, scalable base layer blockchain",
name: t('ecosystem.projects.mose_l1.name'),
description: t('ecosystem.projects.mose_l1.description'),
icon: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4',
link: "#"
},
{
id: 2,
name: locale.value === 'zh' ? "闪兑盒子" : "FlashBox",
description: locale.value === 'zh' ? "闪电快速交易与即时确认支付解决方案" : "Lightning-fast transactions and instant confirmations payment solution",
name: t('ecosystem.projects.flashbox.name'),
description: t('ecosystem.projects.flashbox.description'),
icon: 'M13 10V3L4 14h7v7l9-11h-7z',
link: "#"
},
{
id: 3,
name: locale.value === 'zh' ? "xRouter引擎" : "xRouter",
description: locale.value === 'zh' ? "连接不同区块链的跨链路由协议" : "Cross-chain routing protocol connecting different blockchains",
name: t('ecosystem.projects.xrouter.name'),
description: t('ecosystem.projects.xrouter.description'),
icon: 'M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7',
link: "#"
},
{
id: 4,
name: locale.value === 'zh' ? "MUSD稳定币" : "MUSD Stablecoin",
description: locale.value === 'zh' ? "与美元1:1挂钩的去中心化稳定币" : "Decentralized stablecoin pegged 1:1 to USD",
name: t('ecosystem.projects.musd.name'),
description: t('ecosystem.projects.musd.description'),
icon: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
link: "#"
},
{
id: 5,
name: locale.value === 'zh' ? "WEB3.0彩票" : "WEB3.0 Lottery",
description: locale.value === 'zh' ? "基于区块链的透明公正彩票系统" : "Transparent and fair blockchain-based lottery system",
name: t('ecosystem.projects.lottery.name'),
description: t('ecosystem.projects.lottery.description'),
icon: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z',
link: "#"
},
{
id: 6,
name: locale.value === 'zh' ? "DEX交易所" : "MOSE DEX",
description: locale.value === 'zh' ? "低滑点和低费用的去中心化交易所" : "Decentralized exchange with low slippage and fees",
name: t('ecosystem.projects.dex.name'),
description: t('ecosystem.projects.dex.description'),
icon: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4',
link: "#"
},
{
id: 7,
name: locale.value === 'zh' ? "MOSE银行" : "MOSE Bank",
description: locale.value === 'zh' ? "支持多种资产的DeFi借贷平台" : "DeFi lending platform supporting multiple assets",
name: t('ecosystem.projects.bank.name'),
description: t('ecosystem.projects.bank.description'),
icon: 'M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3',
link: "#"
},
{
id: 8,
name: locale.value === 'zh' ? "MOSE钱包APP" : "MOSE Wallet",
description: locale.value === 'zh' ? "安全且用户友好的数字资产钱包" : "Secure and user-friendly digital asset wallet",
name: t('ecosystem.projects.wallet.name'),
description: t('ecosystem.projects.wallet.description'),
icon: 'M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2z',
link: "#"
},
{
id: 9,
name: locale.value === 'zh' ? "反向跨境电商" : "Cross-border E-commerce",
description: locale.value === 'zh' ? "区块链支持的跨境电子商务平台" : "Blockchain-powered cross-border e-commerce platform",
name: t('ecosystem.projects.ecommerce.name'),
description: t('ecosystem.projects.ecommerce.description'),
icon: 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
link: "#"
},
{
id: 10,
name: locale.value === 'zh' ? "多国法币兑换" : "Multi-currency Exchange",
description: locale.value === 'zh' ? "支持多国法币与加密货币的兑换平台" : "Platform for exchanging multiple fiat currencies and cryptocurrencies",
name: t('ecosystem.projects.exchange.name'),
description: t('ecosystem.projects.exchange.description'),
icon: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
link: "#"
}
];
]);
const ecosystemsData = ref(defaultEcosystems);
const ecosystemsData = ref(defaultEcosystems.value);
const loading = ref(false);
const error = ref<string | null>(null);


+ 74
- 26
src/components/home/BannerModule.vue View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed, watch } from 'vue';
import { queryBannerList } from '@/api/modules/home';
import type { BannerItem } from '@/api/modules/home';
// Swiper
@ -19,7 +19,7 @@ declare global {
}
}
const { t } = useI18n();
const { t, locale } = useI18n();
// Banner
const banners = ref<BannerItem[]>([]);
@ -27,7 +27,7 @@ const loading = ref(false);
const error = ref<string | null>(null);
// Swiper - 使 any
const swiperInstance = ref<unknown>(null);
const swiper = ref<any>(null); // swiper便访
//
const titleElement = ref<HTMLElement | null>(null);
@ -43,28 +43,33 @@ const fetchBanners = async () => {
try {
const response = await queryBannerList();
if (response && Array.isArray(response) && response.length > 0) {
//
banners.value = response;
// 使API使i18n
banners.value = response.map((banner, index) => ({
id: banner.id || String(index + 1),
title: t('home.hero.title'), // 使i18n
image: banner.image || '/LOGO.png',
orderNo: banner.orderNo || index + 1
}));
} else {
// API使Banner
banners.value = [
{
id: '1',
title: t('home.hero.title'),
image: '/LOGO.png', // public
image: '/LOGO.png',
orderNo: 1
}
];
}
} catch (err) {
console.error('Failed to fetch banners:', err);
error.value = 'Failed to load banner data';
error.value = t('common.error.loadFailed');
// 使Banner
banners.value = [
{
id: '1',
title: t('home.hero.title'),
image: '/LOGO.png', // public
image: '/LOGO.png',
orderNo: 1
}
];
@ -73,18 +78,38 @@ const fetchBanners = async () => {
}
};
// i18N
const title = computed(() => {
return [t('home.hero.title'), t('home.hero.title2'), t('home.hero.title3'), t('home.hero.title4')];
})
// i18N
const subTitle = computed(() => {
return [t('home.hero.subtitle'), t('home.hero.subtitle2'), t('home.hero.subtitle3'), t('home.hero.subtitle4')];
})
//
const destroyTypedEffect = () => {
if (titleTyped) {
titleTyped.destroy();
titleTyped = null;
}
if (subtitleTyped) {
subtitleTyped.destroy();
subtitleTyped = null;
}
};
//
const initTypedEffect = () => {
//
destroyTypedEffect();
if (window.Typed) {
//
if (titleElement.value) {
titleTyped = new window.Typed(titleElement.value, {
strings: [
'全球隐私跨链基础设施领导者',
'MOSE - 区块链隐私保护先驱',
'构建隐私计算的未来',
'创新的隐私跨链解决方案'
],
strings: title.value,
typeSpeed: 50,
backSpeed: 30,
backDelay: 2000,
@ -97,12 +122,7 @@ const initTypedEffect = () => {
if (subtitleElement.value) {
setTimeout(() => {
subtitleTyped = new window.Typed(subtitleElement.value, {
strings: [
'让隐私成为区块链的基本权利',
'保护数据,释放价值',
'隐私计算的全新范式',
'安全、高效、可扩展的隐私解决方案'
],
strings: subTitle.value,
typeSpeed: 50,
backSpeed: 30,
backDelay: 2000,
@ -135,6 +155,14 @@ const swiperOptions = {
},
};
//
const onSlideChange = () => {
//
if (swiper.value) {
//
}
};
// Banner
onMounted(() => {
fetchBanners();
@ -144,6 +172,14 @@ onMounted(() => {
initTypedEffect();
}, 500);
});
//
watch(locale, () => {
// i18n
setTimeout(() => {
initTypedEffect();
}, 100);
});
</script>
<template>
@ -151,10 +187,10 @@ onMounted(() => {
<div class="container mx-auto relative z-10">
<!-- MOSE介绍文本 - 放在轮播图上方 -->
<div class="text-center max-w-4xl mx-auto mb-12">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-4 wow animate__animated animate__fadeInDown animate__duration-fast">
<h1 class="text-lg sm:text-3xl md:text-5xl lg:text-6xl font-bold text-white mb-4 wow animate__animated animate__fadeInDown animate__duration-fast">
<span ref="titleElement"></span>
</h1>
<p class="text-xl md:text-2xl text-white mb-8 wow animate__animated animate__fadeInUp animate__delay-sm">
<p class="text-md sm:text-xl md:text-2xl text-white mb-8 wow animate__animated animate__fadeInUp animate__delay-sm">
<span ref="subtitleElement"></span>
</p>
<!-- <div class="flex flex-col sm:flex-row justify-center gap-4 wow animate__animated animate__fadeInUp animate__delay-md">
@ -194,7 +230,8 @@ onMounted(() => {
:navigation="swiperOptions.navigation"
:modules="swiperOptions.modules"
class="h-96"
@swiper="swiperInstance = $event"
@swiper="onSwiper"
@slideChange="onSlideChange"
>
<SwiperSlide v-for="banner in banners" :key="banner.id" class="relative">
<img
@ -202,11 +239,22 @@ onMounted(() => {
:alt="banner.title"
class="w-full h-full object-cover"
/>
<!-- 添加标题显示 - 居中显示 -->
<div class="absolute inset-0 flex items-center justify-center">
<!-- <div
class="bg-black/30 backdrop-blur-sm px-6 py-3 rounded-lg transform transition-all duration-500 hover:scale-105 mx-auto"
> -->
<div
class=" px-16 py-3 max-w-9/10 rounded-lg transform transition-all duration-500 hover:scale-105 mx-auto "
>
<h3 class="text-white text-sm sm:text-base md:text-xl lg:text-2xl font-semibold text-left " v-html="banner.title"></h3>
</div>
</div>
</SwiperSlide>
<!-- 自定义导航按钮 -->
<div class="swiper-button-prev !text-white !w-10 !h-10 !rounded-full !bg-black !bg-opacity-30 hover:!bg-opacity-50"></div>
<div class="swiper-button-next !text-white !w-10 !h-10 !rounded-full !bg-black !bg-opacity-30 hover:!bg-opacity-50"></div>
<div class="swiper-button-prev !text-white !w-6 !h-6 md:!w-10 md:!h-10 !rounded-full !bg-black !bg-opacity-30 hover:!bg-opacity-50"></div>
<div class="swiper-button-next !text-white !w-6 !h-6 md:!w-10 md:!h-10 !rounded-full !bg-black !bg-opacity-30 hover:!bg-opacity-50"></div>
<!-- 自定义分页器 -->
<div class="swiper-pagination !bottom-4"></div>
@ -271,4 +319,4 @@ onMounted(() => {
50% { opacity: 0; }
100% { opacity: 1; }
}
</style>
</style>

+ 96
- 76
src/components/home/CoreValuesModule.vue View File

@ -1,10 +1,11 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed, watch } from 'vue';
import { Icon } from '@iconify/vue';
import { queryValueList } from '@/api/modules/home';
import type { ValueItem } from '@/api/modules/home';
import { useConfig } from '@/utils/config';
import RichTextRenderer from '@/components/common/RichTextRenderer.vue';
const { getConfigImage } = useConfig();
@ -44,6 +45,28 @@ const coreValues = ref<any[]>([
const loading = ref(false);
const error = ref<string | null>(null);
// - RichTextRenderer
const isCompactMode = ref(true); //
const showRichTextControls = ref(false); //
//
const isRightAligned = ref(false); //
const textSize = ref('base'); //
const textLineHeight = ref('tight'); //
const textParagraphSpacing = ref('tight'); //
const textAnimateImages = ref(true); //
//
watch(isCompactMode, (newVal) => {
textLineHeight.value = newVal ? 'tight' : 'normal';
textParagraphSpacing.value = newVal ? 'tight' : 'normal';
});
//
const toggleCompactMode = () => {
isCompactMode.value = !isCompactMode.value;
};
//
const fetchCoreValues = async () => {
loading.value = true;
@ -51,22 +74,19 @@ const fetchCoreValues = async () => {
try {
const response = await queryValueList();
if (response && Array.isArray(response)) {
// API使API
if (response.length > 0) {
//
const icons = ['carbon:security', 'carbon:connection', 'carbon:certificate'];
response.forEach((item, index) => {
if (index < icons.length) {
item.icon = icons[index];
}
});
coreValues.value = response;
}
if (response && Array.isArray(response) && response.length > 0) {
// 使API使i18n
const apiImages = response.map(item => item.image).filter(Boolean);
// i18n
coreValues.value = coreValues.value.map((value, index) => ({
...value,
image: apiImages[index] || value.image // 使API
}));
}
} catch (err) {
console.error('Failed to fetch core values:', err);
error.value = 'Failed to load core values data';
error.value = t('common.error.loadFailed');
} finally {
loading.value = false;
}
@ -78,11 +98,35 @@ onMounted(() => {
</script>
<template>
<section class="py-16 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('index_value')})` }">
<section class="py-8 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('index_value')})` }">
<div class="container mx-auto">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-16 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
{{ t('home.core_values.title') }}
</h2>
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl md:text-3xl font-bold text-text text-center wow animate__animated animate__fadeInUp animate__duration-fast flex-grow">
{{ t('home.core_values.title') }}
</h2>
<!-- <div class="flex space-x-2">
<button @click="toggleCompactMode" class="px-3 py-1 bg-primary text-white rounded-md text-sm hover:bg-primary-dark transition-colors">
{{ isCompactMode ? t('common.expand') : t('common.compact') }}
</button>
<button @click="showRichTextControls = !showRichTextControls" class="px-3 py-1 bg-gray-200 text-gray-800 rounded-md text-sm hover:bg-gray-300 transition-colors">
{{ showRichTextControls ? t('common.hide_style_controls') : t('common.show_style_controls') }}
</button>
</div> -->
</div>
<!-- 富文本样式控制面板 -->
<div v-if="showRichTextControls" class="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
<h3 class="text-lg font-medium mb-3">{{ t('common.rich_text_style_controls') }}</h3>
<RichTextRenderer
:content="t('common.rich_text_demo_content')"
v-model:is-right-aligned="isRightAligned"
v-model:text-size="textSize"
v-model:line-height="textLineHeight"
v-model:paragraph-spacing="textParagraphSpacing"
v-model:animate-images="textAnimateImages"
:show-controls="true"
/>
</div>
<div v-if="loading" class="flex justify-center py-10">
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary"></div>
@ -92,7 +136,7 @@ onMounted(() => {
{{ error }}
</div>
<div v-else class="space-y-32">
<div v-else class="space-y-8">
<!-- 每个核心价值占据一行 -->
<div
v-for="(value, index) in coreValues"
@ -105,51 +149,61 @@ onMounted(() => {
>
<!-- 交替左右布局 -->
<div :class="[
'flex flex-col gap-8',
'flex flex-col gap-3 md:gap-4',
index % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'
]">
<!-- 文本内容 -->
<div class="md:w-1/2 flex flex-col justify-center">
<div class="flex items-center mb-4">
<div class="icon-container w-16 h-16 rounded-full bg-primary bg-opacity-10 flex items-center justify-center mr-4">
<Icon :icon="value.icon" class="text-primary" width="32" height="32" />
<div class="md:w-1/2 flex flex-col justify-center" :class="{ 'items-end': index % 2 !== 0 }">
<div class="flex items-center mb-2" :class="{ 'flex-row-reverse': index % 2 !== 0 }">
<div class="icon-container w-10 h-10 rounded-full bg-primary bg-opacity-10 flex items-center justify-center" :class="{ 'mr-3': index % 2 === 0, 'ml-3': index % 2 !== 0 }">
<!-- <Icon :icon="value.icon" class="text-primary" width="20" height="20" /> -->
<img :src="value.image" class="w-10 h-10" />
</div>
<h3 class="text-2xl md:text-3xl font-bold text-text">{{ value.title }}</h3>
<h3 class="text-lg md:text-xl font-bold text-text">{{ value.title }}</h3>
</div>
<div v-html="value.description" class="text-text-secondary mt-6 rich-text-content"></div>
<!-- 使用富文本渲染器组件 - 动态控制属性 -->
<RichTextRenderer
:content="Array.isArray(value.description) ? value.description.join('<br>') : value.description"
:is-right-aligned="index % 2 !== 0 || (index % 2 === 0 && isRightAligned)"
:text-size="textSize"
max-width="100%"
:line-height="textLineHeight"
:paragraph-spacing="textParagraphSpacing"
:responsive="true"
:animate-images="textAnimateImages"
/>
</div>
<!-- 图片 -->
<div class="md:w-1/2">
<div class="image-container relative overflow-hidden rounded-2xl shadow-xl h-full">
<img
<div class="image-container relative overflow-hidden rounded-2xl shadow-xl h-40 md:h-56">
<!-- <img
:src="value.image"
:alt="value.title"
class="w-full h-full object-cover"
/>
/> -->
<!-- 装饰元素 -->
<div :class="[
'decoration absolute w-32 h-32 rounded-full blur-2xl opacity-40',
'decoration absolute w-20 h-20 rounded-full blur-lg opacity-25',
index % 3 === 0 ? 'bg-primary top-0 right-0 decoration-float-1' :
index % 3 === 1 ? 'bg-secondary bottom-0 left-0 decoration-float-2' :
'bg-accent top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 decoration-pulse'
]"></div>
<!-- 悬停时显示的图层 -->
<div class="hover-overlay absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 transition-opacity duration-300 flex items-end">
<!-- <div class="hover-overlay absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 transition-opacity duration-300 flex items-end">
<div class="p-6 text-white">
<h4 class="text-xl font-bold mb-2">{{ value.title }}</h4>
<p class="text-white/80">{{ t('home.core_values.view_details') }}</p>
</div>
</div>
</div> -->
</div>
</div>
</div>
<!-- 分隔线最后一个项目不显示 -->
<div v-if="index !== coreValues.length - 1" class="separator w-1/3 h-px bg-background-light mx-auto mt-16"></div>
<div v-if="index !== coreValues.length - 1" class="separator w-1/3 h-px bg-background-light mx-auto mt-4"></div>
</div>
</div>
</div>
@ -157,31 +211,7 @@ onMounted(() => {
</template>
<style scoped>
.rich-text-content :deep(p) {
margin-bottom: 1rem;
line-height: 1.7;
}
.rich-text-content :deep(ul),
.rich-text-content :deep(ol) {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.rich-text-content :deep(li) {
margin-bottom: 0.5rem;
line-height: 1.7;
}
.rich-text-content :deep(a) {
color: var(--primary, #3B82F6);
text-decoration: underline;
transition: color 0.3s;
}
.rich-text-content :deep(a:hover) {
color: var(--primary-dark, #2563EB);
}
/* 富文本样式已移至RichTextRenderer组件 */
/* 图标容器动画 */
.icon-container {
@ -250,20 +280,20 @@ onMounted(() => {
@keyframes float1 {
0% { transform: translateY(0) translateX(0); }
50% { transform: translateY(-15px) translateX(10px); }
50% { transform: translateY(-5px) translateX(3px); }
100% { transform: translateY(0) translateX(0); }
}
@keyframes float2 {
0% { transform: translateY(0) translateX(0); }
50% { transform: translateY(15px) translateX(-10px); }
50% { transform: translateY(5px) translateX(-3px); }
100% { transform: translateY(0) translateX(0); }
}
@keyframes pulse {
0% { transform: translate(-50%, -50%) scale(0.8); opacity: 0.3; }
50% { transform: translate(-50%, -50%) scale(1.2); opacity: 0.5; }
100% { transform: translate(-50%, -50%) scale(0.8); opacity: 0.3; }
0% { transform: translate(-50%, -50%) scale(0.92); opacity: 0.15; }
50% { transform: translate(-50%, -50%) scale(1.05); opacity: 0.3; }
100% { transform: translate(-50%, -50%) scale(0.92); opacity: 0.15; }
}
/* 分隔线动画 */
@ -289,15 +319,5 @@ onMounted(() => {
}
}
/* 富文本图片效果 */
.rich-text-content :deep(img) {
border-radius: 0.75rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
transition: transform 0.5s, box-shadow 0.5s;
}
.rich-text-content :deep(img:hover) {
transform: translateY(-5px) scale(1.02);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
}
</style>
/* 富文本图片效果已移至RichTextRenderer组件 */
</style>

+ 23
- 15
src/components/home/EventsModule.vue View File

@ -4,27 +4,35 @@ import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
// 使.txt
const eventsData = [
const eventsData = computed(() => [
{
id: 1,
date: "2024-05",
title: locale.value === 'zh' ? "新加坡共识大会2024" : "Consensus 2024 Singapore",
description: locale.value === 'zh' ? "隐私技术主题演讲" : "Keynote speech on privacy technology",
location: "Singapore",
link: "#",
icon: 'M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h2a2 2 0 012 2h6a2 2 0 012-2h2a2 2 0 012 2v12a2 2 0 01-2 2z'
title: t('home.events.community_ama.title'),
description: t('home.events.community_ama.description'),
date: '2024-01-15',
time: '20:00 UTC+8',
type: 'online',
status: 'upcoming'
},
{
id: 2,
date: "2024-06",
title: locale.value === 'zh' ? "东京开发者黑客松" : "Tokyo Developer Hackathon",
description: locale.value === 'zh' ? "50万美元奖金池" : "$500,000 prize pool",
location: "Tokyo",
link: "#",
icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2'
title: t('home.events.technical_workshop.title'),
description: t('home.events.technical_workshop.description'),
date: '2024-01-20',
time: '19:00 UTC+8',
type: 'online',
status: 'upcoming'
},
{
id: 3,
title: t('home.events.partnership_announcement.title'),
description: t('home.events.partnership_announcement.description'),
date: '2024-01-25',
time: '18:00 UTC+8',
type: 'online',
status: 'upcoming'
}
];
]);
const events = computed(() => {
return eventsData.map(event => ({


+ 92
- 32
src/components/home/MediaModule.vue View File

@ -6,6 +6,7 @@ import type { MediaItem } from '@/api/modules/home';
import { Icon } from '@iconify/vue';
import { useConfig } from '@/utils/config';
import DetailModal from '@/components/common/DetailModal.vue';
import VideoModal from '@/components/common/VideoModal.vue';
const { t } = useI18n();
@ -16,6 +17,7 @@ const error = ref<string | null>(null);
//
const modalVisible = ref(false);
const videoModalVisible = ref(false);
const selectedMedia = ref<MediaItem | null>(null);
//
@ -26,48 +28,81 @@ const fetchMediaItems = async () => {
try {
const response = await queryMediaList();
if (response && Array.isArray(response) && response.length > 0) {
mediaItems.value = response;
// 使API使i18n
mediaItems.value = [
{
id: '1',
title: t('home.media.default_news_1.title'),
image: response[0]?.image || '/LOGO.png',
description: t('home.media.default_news_1.description'),
source: t('home.media.default_news_1.source'),
date: response[0]?.date || '2024-03-15',
video: response[0]?.video || '/MOSEVideo.mp4'
},
{
id: '2',
title: t('home.media.default_news_2.title'),
image: response[1]?.image || '/LOGO.png',
description: t('home.media.default_news_2.description'),
source: t('home.media.default_news_2.source'),
date: response[1]?.date || '2024-02-20',
video: response[1]?.video || '/MOSEVideo.mp4'
},
{
id: '3',
title: t('home.media.default_news_3.title'),
image: response[2]?.image || '/LOGO.png',
description: t('home.media.default_news_3.description'),
source: t('home.media.default_news_3.source'),
date: response[2]?.date || '2024-04-10',
video: response[2]?.video || '/MOSEVideo.mp4'
}
];
} else {
// API使
// If API returns no data, use default media data
mediaItems.value = [
{
id: '1',
title: '区块链技术新突破:MOSE引领隐私计算革命',
title: t('home.media.default_news_1.title'),
image: '/LOGO.png',
description: 'MOSE区块链平台推出全新隐私计算解决方案,获得业内广泛关注和认可。',
source: '区块链日报',
date: '2024-03-15'
description: t('home.media.default_news_1.description'),
source: t('home.media.default_news_1.source'),
date: '2024-03-15',
video: '/MOSEVideo.mp4'
},
{
id: '2',
title: 'MOSE获新加坡金融管理局支持,合规之路再进一步',
title: t('home.media.default_news_2.title'),
image: '/LOGO.png',
description: '新加坡金融管理局(MAS)将MOSE纳入金融科技监管沙盒,标志着其合规发展取得重大突破。',
source: '亚洲金融时报',
date: '2024-02-20'
description: t('home.media.default_news_2.description'),
source: t('home.media.default_news_2.source'),
date: '2024-02-20',
video: '/MOSEVideo.mp4'
},
{
id: '3',
title: '全球区块链峰会:MOSE展示跨链隐私技术',
title: t('home.media.default_news_3.title'),
image: '/LOGO.png',
description: '在全球区块链峰会上,MOSE团队展示了其创新的跨链隐私技术,引发与会者热烈讨论。',
source: '科技前沿',
date: '2024-04-10'
description: t('home.media.default_news_3.description'),
source: t('home.media.default_news_3.source'),
date: '2024-04-10',
video: '/MOSEVideo.mp4'
}
];
}
} catch (err) {
console.error('Failed to fetch media items:', err);
error.value = 'Failed to load media data';
// 使
error.value = t('common.error.loadFailed');
// Use default media data
mediaItems.value = [
{
id: '1',
title: '区块链技术新突破:MOSE引领隐私计算革命',
title: t('home.media.default_news_1.title'),
image: '/LOGO.png',
description: 'MOSE区块链平台推出全新隐私计算解决方案,获得业内广泛关注和认可。',
source: '区块链日报',
date: '2024-03-15'
description: t('home.media.default_news_1.description'),
source: t('home.media.default_news_1.source'),
date: '2024-03-15',
video: '/MOSEVideo.mp4'
}
];
} finally {
@ -87,17 +122,28 @@ const formatDate = (dateString: string) => {
}).format(date);
};
//
//
const showMediaDetail = (media: MediaItem) => {
selectedMedia.value = media;
modalVisible.value = true;
};
//
//
const showVideoModal = (media: MediaItem) => {
selectedMedia.value = media;
videoModalVisible.value = true;
};
//
const closeModal = () => {
modalVisible.value = false;
};
//
const closeVideoModal = () => {
videoModalVisible.value = false;
};
const { getConfigImage } = useConfig();
onMounted(() => {
@ -146,7 +192,7 @@ onMounted(() => {
<div
v-for="(media, index) in mediaItems"
:key="media.id"
class="media-card bg-background rounded-xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-500 flex flex-col md:flex-row wow animate__animated cursor-pointer"
class="media-card bg-background rounded-xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-500 flex flex-col md:flex-row wow animate__animated"
:class="[
index % 2 === 0 ? 'md:flex-row animate__fadeInLeft' : 'md:flex-row-reverse animate__fadeInRight',
{
@ -154,16 +200,22 @@ onMounted(() => {
'animate__delay-sm': index === 2
}
]"
@click="showMediaDetail(media)"
>
<!-- 图片容器 -->
<div class="relative w-full md:w-1/3 h-56 md:h-auto overflow-hidden">
<div class="relative w-full md:w-1/3 h-56 md:h-auto overflow-hidden cursor-pointer" @click="showVideoModal(media)">
<div class="media-image w-full h-full overflow-hidden rounded-t-xl md:rounded-t-none md:rounded-l-xl shadow-md transition-all duration-500">
<img
:src="media.image"
:alt="media.title"
class="w-full h-full object-cover hover:scale-105 transition-transform duration-700"
/>
<!-- 播放按钮覆盖层 -->
<div class="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 hover:opacity-100 transition-opacity duration-300">
<div class="w-16 h-16 bg-white/90 rounded-full flex items-center justify-center shadow-lg transform hover:scale-110 transition-transform duration-200">
<Icon icon="carbon:play-filled" width="24" height="24" class="text-primary ml-1" />
</div>
</div>
<!-- 渐变覆盖层 -->
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-black/30"></div>
@ -184,19 +236,19 @@ onMounted(() => {
<!-- 来源标签 -->
<div class="source-tag inline-block bg-primary/10 text-primary px-3 py-1 rounded-full text-sm font-medium mb-3 transition-all duration-300 hover:bg-primary/20 hover:scale-105">
<Icon icon="carbon:document" class="inline-block mr-1.5" width="14" height="14" />
{{ media.source || '媒体报道' }}
{{ media.source || t('home.media.media_report') }}
</div>
<h3 class="media-title text-xl md:text-2xl font-bold text-text mb-3 hover:text-primary transition-colors duration-300">
<h3 class="media-title text-xl md:text-2xl font-bold text-text mb-3 hover:text-primary transition-colors duration-300 cursor-pointer" @click="showMediaDetail(media)">
{{ media.title }}
</h3>
</div>
<!-- 查看详情按钮 -->
<div class="flex justify-end">
<button class="read-more-btn group bg-gradient-to-r from-secondary to-primary text-white h-10 overflow-hidden rounded-full flex items-center justify-center transition-all duration-300 hover:shadow-glow px-4 hover:px-6">
<button class="read-more-btn group bg-gradient-to-r from-secondary to-primary text-white h-10 overflow-hidden rounded-full flex items-center justify-center transition-all duration-300 hover:shadow-glow px-4 hover:px-6" @click="showMediaDetail(media)">
<Icon icon="carbon:arrow-right" width="20" height="20" class="flex-shrink-0 group-hover:translate-x-1 transition-transform duration-300" />
<span class="ml-2 whitespace-nowrap">点击详情</span>
<span class="ml-2 whitespace-nowrap">{{ t('home.media.view_details') }}</span>
</button>
</div>
</div>
@ -204,12 +256,12 @@ onMounted(() => {
</div>
<!-- 查看更多按钮 -->
<div class="text-center mt-16">
<!-- <div class="text-center mt-16">
<button class="view-all-btn px-8 py-3 bg-gradient-to-r from-secondary to-primary text-white rounded-lg transition-all duration-300 transform hover:-translate-y-1 hover:shadow-glow flex items-center mx-auto wow animate__animated animate__fadeInUp animate__delay-md">
<span class="mr-2">{{ t('home.media.view_all') || '查看全部' }}</span>
<Icon icon="carbon:arrow-right" width="20" height="20" />
</button>
</div>
</div> -->
</div>
@ -224,6 +276,14 @@ onMounted(() => {
type="media"
@close="closeModal"
/>
<!-- 视频播放弹窗 -->
<VideoModal
:visible="videoModalVisible"
:video-url="selectedMedia?.video || ''"
:title="selectedMedia?.title || ''"
@close="closeVideoModal"
/>
</section>
</template>
@ -331,4 +391,4 @@ onMounted(() => {
.shadow-glow {
box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
}
</style>
</style>

+ 34
- 12
src/components/home/NewsModule.vue View File

@ -17,21 +17,21 @@ const news = ref([
title: t('home.news.mainnet_launch'),
date: '2024-03-15',
image: '/public/LOGO.png',
description: '2025年7月,MOSE主网将正式上线,实现完整的跨链隐私交易功能。'
description: t('home.news.mainnet_description')
},
{
id: 2,
title: t('home.news.mas_license'),
date: '2024-02-20',
image: '/public/LOGO.png',
description: 'MOSE获得新加坡金融管理局(MAS)颁发的支付服务牌照,合规发展迈出重要一步。'
description: t('home.news.mas_description')
},
{
id: 3,
title: t('home.news.hackathon'),
date: '2024-04-10',
image: '/public/LOGO.png',
description: '首届MOSE全球开发者黑客松大赛正式启动,邀请全球开发者参与生态建设。'
description: t('home.news.hackathon_description')
}
]);
@ -49,14 +49,36 @@ const formatDate = (dateString: string) => {
}).format(date);
};
// API
// API - 使使i18n
const convertNewsData = (apiNews: any[]) => {
return apiNews.map(newsItem => ({
id: parseInt(newsItem.id) || Math.floor(Math.random() * 10000),
title: newsItem.title,
date: newsItem.createTime?.split(' ')[0] || new Date().toISOString().split('T')[0],
image: newsItem.image || '/public/LOGO.png',
description: newsItem.description || '暂无描述'
const defaultNews = [
{
id: 1,
title: t('home.news.mainnet_launch'),
date: '2024-03-15',
image: '/public/LOGO.png',
description: t('home.news.mainnet_description')
},
{
id: 2,
title: t('home.news.mas_license'),
date: '2024-02-20',
image: '/public/LOGO.png',
description: t('home.news.mas_description')
},
{
id: 3,
title: t('home.news.hackathon'),
date: '2024-04-10',
image: '/public/LOGO.png',
description: t('home.news.hackathon_description')
}
];
return defaultNews.map((defaultItem, index) => ({
...defaultItem,
image: apiNews[index]?.image || defaultItem.image,
date: apiNews[index]?.createTime?.split(' ')[0] || defaultItem.date
}));
};
@ -73,7 +95,7 @@ const fetchNews = async () => {
}
} catch (err) {
console.error('Failed to fetch news:', err);
error.value = 'Failed to load news data';
error.value = t('common.error.loadFailed');
} finally {
loading.value = false;
}
@ -280,4 +302,4 @@ button.shadow-button {
button.shadow-button:hover {
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.5);
}
</style>
</style>

+ 59
- 10
src/components/home/PartnersModule.vue View File

@ -70,26 +70,75 @@ const fetchPartners = async () => {
try {
const response = await queryInvestorList();
// API
// 使API使i18n
const convertPartnerData = (apiPartners: any[]) => {
return apiPartners.map(partner => ({
return apiPartners.map((partner, index) => ({
id: parseInt(partner.id) || Math.floor(Math.random() * 10000), // ID
name: partner.title,
name: t(`home.partners.items.partner${index + 1}.name`),
logo: partner.image || '/LOGO.png',
url: partner.url || '#',
description: partner.description || '战略合作伙伴'
description: t(`home.partners.items.partner${index + 1}.description`)
}));
};
// API
if (response && Array.isArray(response) && response.length > 0) {
const apiPartners = response;
//
// 使i18n
partners.value = convertPartnerData(apiPartners);
} else {
// 使i18n
partners.value = [
{
id: 1,
name: t('home.partners.items.partner1.name'),
logo: '/LOGO.png',
url: '#',
description: t('home.partners.items.partner1.description')
},
{
id: 2,
name: t('home.partners.items.partner2.name'),
logo: '/LOGO.png',
url: '#',
description: t('home.partners.items.partner2.description')
},
{
id: 3,
name: t('home.partners.items.partner3.name'),
logo: '/LOGO.png',
url: '#',
description: t('home.partners.items.partner3.description')
}
];
}
} catch (err) {
console.error('Failed to fetch partners:', err);
error.value = 'Failed to load partners data';
error.value = t('common.error.loadFailed');
// 使i18n
partners.value = [
{
id: 1,
name: t('home.partners.items.partner1.name'),
logo: '/LOGO.png',
url: '#',
description: t('home.partners.items.partner1.description')
},
{
id: 2,
name: t('home.partners.items.partner2.name'),
logo: '/LOGO.png',
url: '#',
description: t('home.partners.items.partner2.description')
},
{
id: 3,
name: t('home.partners.items.partner3.name'),
logo: '/LOGO.png',
url: '#',
description: t('home.partners.items.partner3.description')
}
];
} finally {
loading.value = false;
}
@ -222,7 +271,7 @@ onMounted(() => {
<!-- 添加链接按钮 -->
<a :href="partner.url" target="_blank" rel="noopener noreferrer" class="mt-2 inline-flex items-center justify-center px-3 py-1 bg-white/20 rounded-full text-xs text-white hover:bg-white/30 transition-colors">
<Icon icon="carbon:link" class="mr-1" width="12" height="12" />
<span>访问</span>
<span>{{ t('home.partners.visit') }}</span>
</a>
</div>
</div>
@ -234,12 +283,12 @@ onMounted(() => {
</div>
<!-- 查看更多按钮 -->
<div class="text-center mt-10" data-aos="fade-up" data-aos-delay="300">
<!-- <div class="text-center mt-10" data-aos="fade-up" data-aos-delay="300">
<button class="px-8 py-3 bg-gradient-to-r from-primary to-primary-light text-white rounded-lg transition-all duration-300 transform hover:-translate-y-1 hover:shadow-lg flex items-center mx-auto">
<span class="mr-2">{{ t('home.partners.view_all') }}</span>
<Icon icon="carbon:arrow-right" width="20" height="20" />
</button>
</div>
</div> -->
</div>
</section>
</template>
@ -317,4 +366,4 @@ onMounted(() => {
[data-aos="width"].aos-animate {
width: 100%;
}
</style>
</style>

+ 89
- 34
src/components/home/PostsModule.vue View File

@ -1,21 +1,22 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, onMounted, nextTick } from 'vue';
import { queryPostList } from '@/api/modules/home';
import type { PostItem } from '@/api/modules/home';
import { queryMediaList } from '@/api/modules/home';
import type { MediaItem } from '@/api/modules/home';
import dayjs from 'dayjs';
import { Icon } from '@iconify/vue';
import { gsap } from 'gsap';
import AOS from 'aos';
import { useConfig } from '@/utils/config';
import DetailModal from '@/components/common/DetailModal.vue';
import VideoModal from '@/components/common/VideoModal.vue';
const { getConfigImage } = useConfig();
const { t } = useI18n();
//
const posts = ref<PostItem[]>([]);
//
const posts = ref<MediaItem[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
const postItems = ref<HTMLElement[]>([]);
@ -23,55 +24,86 @@ const postSection = ref(null);
//
const modalVisible = ref(false);
const selectedPost = ref<PostItem | null>(null);
const videoModalVisible = ref(false);
const selectedPost = ref<MediaItem | null>(null);
//
//
const fetchPosts = async () => {
loading.value = true;
error.value = null;
try {
const response = await queryPostList();
const response = await queryMediaList();
if (response && Array.isArray(response) && response.length > 0) {
posts.value = response;
// 使API使i18n
posts.value = [
{
id: '1',
title: t('home.posts.items.mainnet_launch.title'),
image: response[0]?.image || '/public/LOGO.png',
description: t('home.posts.items.mainnet_launch.description'),
video: response[0]?.video || '/public/MOSEVideo.mp4',
createTime: response[0]?.createTime || '2024-03-15'
},
{
id: '2',
title: t('home.posts.items.mas_license.title'),
image: response[1]?.image || '/public/LOGO.png',
description: t('home.posts.items.mas_license.description'),
video: response[1]?.video || '/public/MOSEVideo.mp4',
createTime: response[1]?.createTime || '2024-02-20'
},
{
id: '3',
title: t('home.posts.items.hackathon.title'),
image: response[2]?.image || '/public/LOGO.png',
description: t('home.posts.items.hackathon.description'),
video: response[2]?.video || '/public/MOSEVideo.mp4',
createTime: response[2]?.createTime || '2024-04-10'
}
];
} else {
// API使
// API使
posts.value = [
{
id: '1',
title: t('home.news.mainnet_launch'),
title: t('home.posts.items.mainnet_launch.title'),
image: '/public/LOGO.png',
description: '2025年7月,MOSE主网将正式上线,实现完整的跨链隐私交易功能。',
description: t('home.posts.items.mainnet_launch.description'),
video: '/public/MOSEVideo.mp4',
createTime: '2024-03-15'
},
{
id: '2',
title: t('home.news.mas_license'),
title: t('home.posts.items.mas_license.title'),
image: '/public/LOGO.png',
description: 'MOSE获得新加坡金融管理局(MAS)颁发的支付服务牌照,合规发展迈出重要一步。',
description: t('home.posts.items.mas_license.description'),
video: '/public/MOSEVideo.mp4',
createTime: '2024-02-20'
},
{
id: '3',
title: t('home.news.hackathon'),
title: t('home.posts.items.hackathon.title'),
image: '/public/LOGO.png',
description: '首届MOSE全球开发者黑客松大赛正式启动,邀请全球开发者参与生态建设。',
description: t('home.posts.items.hackathon.description'),
video: '/public/MOSEVideo.mp4',
createTime: '2024-04-10'
}
];
}
} catch (err) {
console.error('Failed to fetch posts:', err);
error.value = 'Failed to load posts data';
// 使
error.value = t('common.error.loadFailed');
// 使
posts.value = [
{
id: '1',
title: t('home.news.mainnet_launch'),
image: '/public/LOGO.png',
description: '2025年7月,MOSE主网将正式上线,实现完整的跨链隐私交易功能。',
createTime: '2024-03-15'
}
id: '1',
title: t('home.posts.items.mainnet_launch.title'),
image: '/public/LOGO.png',
description: t('home.posts.items.mainnet_launch.description'),
video: '/public/MOSEVideo.mp4',
createTime: '2024-03-15'
}
];
} finally {
loading.value = false;
@ -85,17 +117,31 @@ const formatDate = (dateString: string) => {
return dayjs(dateString).format('YYYY-MM-DD');
};
//
const showPostDetail = (post: PostItem) => {
//
const showPostDetail = (post: MediaItem) => {
selectedPost.value = post;
modalVisible.value = true;
};
//
//
const showVideo = (post: MediaItem, event: Event) => {
event.stopPropagation(); //
if (post.video) {
selectedPost.value = post;
videoModalVisible.value = true;
}
};
//
const closeModal = () => {
modalVisible.value = false;
};
//
const closeVideoModal = () => {
videoModalVisible.value = false;
};
//
const initAnimations = () => {
// 使GSAP
@ -178,13 +224,12 @@ onMounted(() => {
<div
v-for="(post, index) in posts"
:key="post.id"
class="post-item bg-background rounded-xl overflow-hidden shadow-card hover:shadow-xl transition-all duration-500 flex flex-col md:flex-row cursor-pointer"
class="post-item bg-background rounded-xl overflow-hidden shadow-card hover:shadow-xl transition-all duration-500 flex flex-col md:flex-row"
:class="index % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'"
ref="el => { if(el) postItems[index] = el }"
@click="showPostDetail(post)"
>
<!-- 图片容器 -->
<div class="relative w-full md:w-2/5 h-56 md:h-auto overflow-hidden">
<div class="relative w-full md:w-2/5 h-56 md:h-auto overflow-hidden cursor-pointer" @click="showVideo(post, $event)">
<img
:src="post.image"
:alt="post.title"
@ -205,12 +250,15 @@ onMounted(() => {
<div class="absolute -bottom-2 left-0 w-full h-1 bg-gradient-to-r from-primary via-secondary to-accent"></div>
</div>
<!-- 内容容器 - 显示标题 -->
<div class="post-content w-full md:w-3/5 p-6 md:p-8 flex flex-col justify-between relative">
<!-- 内容容器 - 显示标题和部分详情 -->
<div class="post-content w-full md:w-3/5 p-6 md:p-8 flex flex-col justify-between relative cursor-pointer" @click="showPostDetail(post)">
<div>
<h3 class="text-xl md:text-2xl font-bold text-text mb-3 line-clamp-2 hover:text-primary transition-colors duration-300">
<h3 class="text-xl md:text-2xl font-bold text-text mb-2 line-clamp-2 hover:text-primary transition-colors duration-300">
{{ post.title }}
</h3>
<p class="text-text-light text-sm mb-3 line-clamp-2 opacity-80" v-html="post.description">
</p>
</div>
<div class="flex justify-between items-center">
@ -224,7 +272,7 @@ onMounted(() => {
<!-- 查看详情按钮 -->
<button class="group flex items-center text-primary hover:text-primary-dark transition-colors">
<span class="mr-2 font-medium">点击详情</span>
<span class="mr-2 font-medium">{{ t('home.posts.click_details') }}</span>
<div class="relative w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center overflow-hidden group-hover:bg-primary/20 transition-colors">
<Icon
icon="carbon:arrow-right"
@ -262,6 +310,13 @@ onMounted(() => {
type="post"
@close="closeModal"
/>
<!-- 视频弹窗 -->
<VideoModal
:visible="videoModalVisible"
:video-url="selectedPost?.video || ''"
@close="closeVideoModal"
/>
</section>
</template>
@ -342,4 +397,4 @@ onMounted(() => {
.post-item:hover .date-badge {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
</style>
</style>

+ 382
- 202
src/components/home/ProjectIntroModule.vue View File

@ -1,65 +1,10 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useSummary } from '@/utils/config';
import { ref, onMounted } from 'vue';
import { Icon } from '@iconify/vue';
import { useConfig } from '@/utils/config';
const { getConfigImage } = useConfig();
const { t } = useI18n();
const { getSummaryDescription } = useSummary();
//
const videoRef = ref(null);
const isPlaying = ref(false);
const isMuted = ref(false);
// /
const togglePlay = () => {
const video = videoRef.value;
if (!video) return;
if (video.paused) {
video.play();
isPlaying.value = true;
} else {
video.pause();
isPlaying.value = false;
}
};
// /
const toggleMute = () => {
const video = videoRef.value;
if (!video) return;
video.muted = !video.muted;
isMuted.value = video.muted;
};
//
onMounted(() => {
const video = videoRef.value;
if (video) {
//
isMuted.value = video.muted;
//
video.muted = true;
isMuted.value = true;
const playPromise = video.play();
if (playPromise !== undefined) {
playPromise.then(() => {
isPlaying.value = true;
}).catch(error => {
console.log('自动播放失败:', error);
isPlaying.value = false;
});
}
}
});
</script>
<template>
@ -73,107 +18,63 @@ onMounted(() => {
<div class="max-w-5xl mx-auto">
<!-- 视频容器 -->
<div class="video-wrapper mb-12 wow animate__animated animate__fadeIn animate__duration-normal">
<div class="relative rounded-2xl overflow-hidden shadow-2xl transform hover:scale-[1.01] transition-all duration-700">
<div class="rounded-2xl overflow-hidden shadow-lg">
<!-- 视频元素 -->
<video
ref="videoRef"
src="/public/MOSEVideo.mp4"
class="w-full h-full object-cover"
poster="/public/LOGO.png"
controls
autoplay
muted
loop
playsinline
></video>
<!-- 永久显示的播放/暂停按钮 -->
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-20">
<button
@click="togglePlay"
class="play-button w-16 h-16 md:w-20 md:h-20 bg-primary/80 rounded-full flex items-center justify-center hover:bg-primary transition-colors duration-300"
>
<Icon
:icon="isPlaying ? 'carbon:pause-filled' : 'carbon:play-filled'"
class="text-white"
width="32"
height="32"
/>
</button>
</div>
<!-- 视频右上角图标组 - 新增 -->
<div class="absolute top-4 right-4 flex space-x-3 z-10">
<div class="video-icon-badge flex items-center bg-black/50 backdrop-blur-sm px-3 py-1.5 rounded-full">
<Icon icon="carbon:video" class="text-primary-light mr-2" width="16" height="16" />
<span class="text-white text-xs font-medium">HD</span>
</div>
<button class="video-icon-btn bg-black/50 backdrop-blur-sm w-8 h-8 rounded-full flex items-center justify-center hover:bg-primary/70 transition-colors">
<Icon icon="carbon:information" class="text-white" width="16" height="16" />
</button>
</div>
<!-- 悬停显示的控制层 -->
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-black/30 opacity-0 hover:opacity-100 transition-opacity duration-300">
<!-- 底部控制栏 -->
<div class="absolute bottom-0 left-0 right-0 p-4 flex justify-between items-center">
<div class="flex items-center space-x-3">
<button
@click="toggleMute"
class="text-white hover:text-primary-light transition-colors"
>
<Icon
:icon="isMuted ? 'carbon:volume-mute-filled' : 'carbon:volume-up-filled'"
width="24"
height="24"
/>
</button>
</div>
</div>
<!-- 桌面端文字介绍 -->
<div class="content-wrapper bg-background-light p-8 md:p-10 rounded-2xl shadow-card relative overflow-hidden wow animate__animated animate__fadeInUp animate__duration-fast animate__delay-xs hidden md:block">
<div class="relative z-10">
<!-- 桌面端项目介绍内容 -->
<div class="project-intro-content mb-8">
<div class="space-y-4 text-text-secondary">
<p class="text-sm text-amber-50 md:text-base leading-relaxed text-justify">
{{ t('home.project_intro.subtitle') }}
</p>
<p class="text-sm md:text-base leading-relaxed text-justify">
{{ t('home.project_intro.description') }}
</p>
<div class="bg-background-dark p-4 md:p-6 rounded-lg border-l-4 border-primary">
<h4 class="text-base md:text-lg font-semibold text-primary mb-3 flex items-center">
<Icon icon="carbon:earth" class="mr-2 h-5 w-5" />
{{ t('home.project_intro.global_layout_section.title') }}
</h4>
<!-- 新增视频时间指示器 -->
<div class="hidden md:flex items-center space-x-1">
<div class="video-time-indicator bg-black/40 backdrop-blur-sm px-2 py-0.5 rounded text-white text-xs">
<Icon icon="carbon:time" class="inline-block mr-1 text-primary-light" width="12" height="12" />
<span>01:45</span>
<div class="space-y-3">
<div class="flex flex-col md:flex-row md:items-start">
<span class="font-medium text-secondary mb-1 md:mb-0 md:mr-3 md:min-w-[100px]">
{{ t('home.project_intro.global_layout_section.headquarters_label') }}
</span>
<span class="text-sm md:text-base text-text-secondary">
{{ t('home.project_intro.global_layout_section.headquarters_desc') }}
</span>
</div>
<div class="flex flex-col md:flex-row md:items-start">
<span class="font-medium text-secondary mb-1 md:mb-0 md:mr-3 md:min-w-[100px]">
{{ t('home.project_intro.global_layout_section.distribution_label') }}
</span>
<span class="text-sm md:text-base text-text-secondary">
{{ t('home.project_intro.global_layout_section.distribution_desc') }}
</span>
</div>
</div>
</div>
<div class="flex items-center space-x-3">
<div class="video-badge px-3 py-1 bg-primary/80 text-white text-sm rounded-full flex items-center">
<Icon icon="carbon:play-filled-alt" class="mr-1 text-white" width="14" height="14" />
{{ t('home.project_intro.watch_video') }}
</div>
<!-- 新增全屏按钮 -->
<button class="text-white hover:text-primary-light transition-colors hidden md:block">
<Icon icon="carbon:maximize" width="20" height="20" />
</button>
</div>
</div>
<!-- 新增顶部渐变阴影 -->
<div class="absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-black/60 to-transparent"></div>
</div>
<!-- 装饰元素 -->
<div class="absolute -top-10 -right-10 w-40 h-40 rounded-full bg-primary opacity-20 blur-3xl"></div>
<div class="absolute -bottom-10 -left-10 w-40 h-40 rounded-full bg-secondary opacity-20 blur-3xl"></div>
<!-- 新增视频角标 -->
<div class="absolute top-4 left-4 bg-primary/80 backdrop-blur-sm px-3 py-1 rounded-lg flex items-center z-10">
<Icon icon="carbon:badge" class="text-white mr-1.5" width="14" height="14" />
<span class="text-white text-xs font-medium">MOSE</span>
</div>
</div>
</div>
<!-- 文字介绍 -->
<div class="content-wrapper bg-background-light p-8 md:p-10 rounded-2xl shadow-card relative overflow-hidden wow animate__animated animate__fadeInUp animate__duration-fast animate__delay-xs">
<div class="relative z-10">
<!-- 富文本内容 -->
<div class="prose max-w-none text-text-secondary mb-8">
<div class="text-lg leading-relaxed rich-text-container" v-html="getSummaryDescription('config_Introduction_of_platform')"></div>
</div>
<!-- 特点标签 -->
<!-- 桌面端特点标签 -->
<div class="flex flex-wrap justify-center gap-4 mt-8">
<div class="feature-tag flex items-center bg-background-dark px-4 py-3 rounded-lg transform transition-all duration-300 hover:-translate-y-1 hover:shadow-lg">
<div class="w-10 h-10 rounded-full bg-primary bg-opacity-20 flex items-center justify-center mr-3">
@ -184,7 +85,7 @@ onMounted(() => {
<div class="feature-tag flex items-center bg-background-dark px-4 py-3 rounded-lg transform transition-all duration-300 hover:-translate-y-1 hover:shadow-lg">
<div class="w-10 h-10 rounded-full bg-secondary bg-opacity-20 flex items-center justify-center mr-3">
<Icon icon="carbon:security" class="h-5 w-5 text-secondary" />
<Icon icon="mdi:shield" class="h-5 w-5 text-accent" />
</div>
<span class="text-text font-medium">{{ t('home.project_intro.security_focus') }}</span>
</div>
@ -194,18 +95,129 @@ onMounted(() => {
<Icon icon="carbon:idea" class="h-5 w-5 text-accent" />
</div>
<span class="text-text font-medium">{{ t('home.project_intro.innovation') }}</span>
</div>
</div>
</div>
<!-- 背景装饰 -->
<div class="absolute top-0 left-0 w-full h-full overflow-hidden opacity-5">
<div class="absolute top-1/3 right-1/4 w-64 h-64 rounded-full bg-primary-light blur-3xl"></div>
<div class="absolute bottom-1/3 left-1/4 w-64 h-64 rounded-full bg-secondary blur-3xl"></div>
</div>
</div>
<!-- 背景装饰 -->
<div class="absolute top-0 left-0 w-full h-full overflow-hidden opacity-5">
<div class="absolute top-1/3 right-1/4 w-64 h-64 rounded-full bg-primary-light blur-3xl"></div>
<div class="absolute bottom-1/3 left-1/4 w-64 h-64 rounded-full bg-secondary blur-3xl"></div>
<!-- 手机端简洁设计 -->
<div class="mobile-simple md:hidden">
<!-- 主标题卡片 -->
<!-- <div class="mobile-header bg-gradient-to-r from-primary/10 to-secondary/10 rounded-2xl p-6 mb-6 text-center">
<div class="w-16 h-16 bg-primary/20 rounded-full flex items-center justify-center mx-auto mb-4">
<Icon icon="carbon:blockchain" class="h-8 w-8 text-primary" />
</div>
<h3 class="text-xl font-bold text-primary mb-2">{{ t('home.project_intro.mobile.project_title') }}</h3>
<p class="text-sm text-text-secondary leading-relaxed">
{{ t('home.project_intro.mobile.project_subtitle') }}
</p>
</div> -->
<!-- 内容卡片组 -->
<div class="space-y-4">
<!-- 研究背景卡片 -->
<div class="mobile-card bg-background-light rounded-xl p-5 border border-primary/10">
<div class="flex items-center mb-3">
<div class="w-10 h-10 bg-blue-500/10 rounded-lg flex items-center justify-center mr-3">
<Icon icon="carbon:network-3" class="h-5 w-5 text-blue-500" />
</div>
<h4 class="text-base font-semibold text-text">{{ t('home.project_intro.mobile.research_background') }}</h4>
</div>
<p class="text-sm text-text-secondary leading-relaxed">
{{ t('home.project_intro.mobile.research_desc') }}
</p>
</div>
<!-- 技术创新卡片 -->
<div class="mobile-card bg-background-light rounded-xl p-5 border border-secondary/10">
<div class="flex items-center mb-3">
<div class="w-10 h-10 bg-green-500/10 rounded-lg flex items-center justify-center mr-3">
<Icon icon="carbon:security" class="h-5 w-5 text-green-500" />
</div>
<h4 class="text-base font-semibold text-text">{{ t('home.project_intro.mobile.core_technology') }}</h4>
</div>
<p class="text-sm text-text-secondary leading-relaxed mb-3">
{{ t('home.project_intro.mobile.technology_desc') }}
</p>
<div class="flex flex-wrap gap-2">
<span class="px-3 py-1 bg-primary/10 text-primary text-xs rounded-full">{{ t('home.project_intro.mobile.features.decentralized') }}</span>
<span class="px-3 py-1 bg-secondary/10 text-secondary text-xs rounded-full">{{ t('home.project_intro.mobile.features.high_security') }}</span>
<span class="px-3 py-1 bg-accent/10 text-accent text-xs rounded-full">{{ t('home.project_intro.mobile.features.privacy_protection') }}</span>
</div>
</div>
<!-- 全球布局卡片 -->
<div class="mobile-card bg-background-light rounded-xl p-5 border border-accent/10">
<div class="flex items-center mb-4">
<div class="w-10 h-10 bg-purple-500/10 rounded-lg flex items-center justify-center mr-3">
<Icon icon="carbon:earth" class="h-5 w-5 text-purple-500" />
</div>
<h4 class="text-base font-semibold text-text">{{ t('home.project_intro.mobile.global_layout') }}</h4>
</div>
<div class="space-y-3">
<div class="flex items-start">
<div class="w-6 h-6 bg-orange-500/10 rounded-full flex items-center justify-center mr-3 mt-0.5 flex-shrink-0">
<Icon icon="carbon:location" class="h-3 w-3 text-orange-500" />
</div>
<div>
<p class="text-sm font-medium text-text mb-1">{{ t('home.project_intro.mobile.singapore_hq') }}</p>
<p class="text-xs text-text-secondary leading-relaxed">
{{ t('home.project_intro.mobile.singapore_desc') }}
</p>
</div>
</div>
<div class="flex items-start">
<div class="w-6 h-6 bg-cyan-500/10 rounded-full flex items-center justify-center mr-3 mt-0.5 flex-shrink-0">
<Icon icon="carbon:network-3" class="h-3 w-3 text-cyan-500" />
</div>
<div>
<p class="text-sm font-medium text-text mb-1">{{ t('home.project_intro.mobile.ten_hubs') }}</p>
<p class="text-xs text-text-secondary leading-relaxed">
{{ t('home.project_intro.mobile.hubs_desc') }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- 特性图标区 -->
<div class="mobile-features mt-6">
<div class="grid grid-cols-3 gap-3">
<div class="feature-item bg-background-light rounded-xl p-4 text-center border border-primary/10">
<div class="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mx-auto mb-2">
<Icon icon="carbon:earth" class="h-6 w-6 text-primary" />
</div>
<p class="text-xs text-text font-medium leading-tight">{{ t('home.project_intro.global_presence') }}</p>
</div>
<div class="feature-item bg-background-light rounded-xl p-4 text-center border border-secondary/10">
<div class="w-12 h-12 bg-secondary/10 rounded-xl flex items-center justify-center mx-auto mb-2">
<Icon icon="mdi:shield" class="h-6 w-6 text-secondary" />
</div>
<p class="text-xs text-text font-medium leading-tight">{{ t('home.project_intro.security_focus') }}</p>
</div>
<div class="feature-item bg-background-light rounded-xl p-4 text-center border border-accent/10">
<div class="w-12 h-12 bg-accent/10 rounded-xl flex items-center justify-center mx-auto mb-2">
<Icon icon="carbon:idea" class="h-6 w-6 text-accent" />
</div>
<p class="text-xs text-text font-medium leading-tight">{{ t('home.project_intro.innovation') }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
@ -215,65 +227,14 @@ onMounted(() => {
position: relative;
}
.video-wrapper::before {
content: '';
position: absolute;
top: -5px;
left: -5px;
right: -5px;
bottom: -5px;
background: linear-gradient(45deg, var(--primary, #3B82F6), var(--secondary, #10B981), var(--accent, #F59E0B));
border-radius: 1rem;
z-index: -1;
opacity: 0.5;
filter: blur(15px);
transition: opacity 0.3s ease;
}
.video-wrapper:hover::before {
opacity: 0.7;
}
/* 播放按钮动画 */
/* 简化视频样式 */
.play-button {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5);
animation: pulse-ring 2s infinite;
opacity: 0.9;
transition: opacity 0.3s ease, transform 0.3s ease;
transition: opacity 0.3s ease;
}
.play-button:hover {
opacity: 1;
transform: scale(1.05);
}
@keyframes pulse-ring {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5);
}
70% {
box-shadow: 0 0 0 20px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
/* 视频图标按钮 */
.video-icon-btn {
transition: all 0.3s ease;
}
.video-icon-btn:hover {
transform: translateY(-2px);
}
.video-icon-badge {
transition: all 0.3s ease;
}
.video-icon-badge:hover {
background-color: rgba(0, 0, 0, 0.7);
}
/* 特点标签动画 */
@ -343,4 +304,223 @@ onMounted(() => {
margin-bottom: 0.5rem;
line-height: 1.7;
}
</style>
/* 手机端富文本优化 */
.rich-text-mobile {
@media (max-width: 768px) {
font-size: 0.9rem;
line-height: 1.6;
}
}
.rich-text-mobile :deep(p) {
@media (max-width: 768px) {
margin-bottom: 1rem;
text-align: justify;
word-break: break-word;
}
}
.rich-text-mobile :deep(div) {
@media (max-width: 768px) {
margin-bottom: 0.75rem;
word-break: break-word;
}
}
/* 手机端特点标签优化 */
@media (max-width: 768px) {
.feature-tag {
padding: 0.75rem;
min-width: 3.5rem;
justify-content: center;
}
.feature-tag .w-10 {
width: 2.5rem;
height: 2.5rem;
}
.feature-tag .h-5 {
width: 1.25rem;
height: 1.25rem;
}
}
/* 项目介绍内容样式 */
.project-intro-content {
line-height: 1.7;
}
.project-intro-content h3 {
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.project-intro-content p {
word-break: break-word;
hyphens: auto;
}
/* 手机端简洁设计样式 */
.mobile-simple {
padding: 1rem;
background: rgba(var(--color-background-rgb), 0.95);
border-radius: 1rem;
margin: 0 -0.5rem;
}
.mobile-header {
transition: all 0.3s ease;
animation: fadeInDown 0.6s ease-out;
}
.mobile-card {
transition: all 0.3s ease;
animation: fadeInUp 0.6s ease-out;
position: relative;
overflow: hidden;
}
.mobile-card:nth-child(1) {
animation-delay: 0.1s;
}
.mobile-card:nth-child(2) {
animation-delay: 0.2s;
}
.mobile-card:nth-child(3) {
animation-delay: 0.3s;
}
.mobile-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.1);
}
.mobile-features {
animation: fadeInUp 0.6s ease-out 0.4s both;
}
.feature-item {
transition: all 0.3s ease;
}
.feature-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.1);
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 手机端项目介绍优化 */
@media (max-width: 768px) {
.project-intro-content {
padding: 0;
}
.project-intro-content h3 {
font-size: 1rem;
line-height: 1.5;
margin-bottom: 1rem;
}
.project-intro-content p {
font-size: 0.875rem;
line-height: 1.6;
text-align: left;
padding: 0 0.5rem;
}
.project-intro-content .bg-background-dark {
padding: 1rem;
margin: 0.5rem 0;
}
.project-intro-content h4 {
font-size: 0.9rem;
margin-bottom: 0.75rem;
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.project-intro-content h4 .iconify {
margin-bottom: 0.25rem;
margin-right: 0;
}
.project-intro-content .space-y-3 > div {
margin-bottom: 0.75rem;
}
.project-intro-content .font-medium {
font-size: 0.875rem;
display: block;
margin-bottom: 0.25rem;
}
.project-intro-content .text-text-secondary {
font-size: 0.8rem;
line-height: 1.5;
}
/* 手机端内容容器优化 */
.content-wrapper {
padding: 1.5rem;
}
}
/* 动画关键帧 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
@keyframes glow {
0%, 100% {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
}
50% {
box-shadow: 0 0 30px rgba(59, 130, 246, 0.5);
}
}
</style>

+ 3
- 5
src/components/layout/Footer.vue View File

@ -1,9 +1,7 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useConfig } from '@/utils/config';
const { t } = useI18n();
const { getConfigText } = useConfig();
//
const socialLinks = [
@ -83,8 +81,8 @@ const currentYear = new Date().getFullYear();
<div class="wow animate__animated animate__fadeIn animate__delay-md animate__duration-fast">
<h3 class="text-lg font-medium text-text mb-4">{{ t('nav.contact') }}</h3>
<ul class="space-y-2">
<li class="wow animate__animated animate__fadeInUp animate__duration-fast"><a :href="`mailto:${getConfigText('emailOne')}`" class="hover:text-primary-light transition-colors duration-200">{{ getConfigText('emailOne') }}</a></li>
<li class="wow animate__animated animate__fadeInUp animate__delay-xs animate__duration-fast"><a :href="`mailto:${getConfigText('emailTwo')}`" class="hover:text-primary-light transition-colors duration-200">{{ getConfigText('emailTwo') }}</a></li>
<li class="wow animate__animated animate__fadeInUp animate__duration-fast"><a :href="`mailto:${t('contact.email.business')}`" class="hover:text-primary-light transition-colors duration-200">{{ t('contact.email.business') }}</a></li>
<li class="wow animate__animated animate__fadeInUp animate__delay-xs animate__duration-fast"><a :href="`mailto:${t('contact.email.support')}`" class="hover:text-primary-light transition-colors duration-200">{{ t('contact.email.support') }}</a></li>
<li class="wow animate__animated animate__fadeInUp animate__delay-sm animate__duration-fast"><router-link to="/contact" class="hover:text-primary-light transition-colors duration-200">{{ t('nav.contact') }}</router-link></li>
</ul>
</div>
@ -106,4 +104,4 @@ const currentYear = new Date().getFullYear();
</div>
</div>
</footer>
</template>
</template>

+ 4
- 4
src/components/layout/NavBar.vue View File

@ -266,24 +266,24 @@ const emit = defineEmits(['changeLanguage']);
</router-link>
<!-- 动画演示 -->
<router-link
<!-- <router-link
to="/animations"
class="text-text-secondary hover:text-text py-2 animate__animated animate__fadeInRight animate__duration-fast animate__delay-lg"
:class="{ 'text-primary-light font-medium': $route.path === '/animations' }"
@click="isMenuOpen = false"
>
动画演示
</router-link>
</router-link> -->
<!-- 翻译工具 -->
<router-link
<!-- <router-link
to="/translate"
class="text-text-secondary hover:text-text py-2 animate__animated animate__fadeInRight animate__duration-fast animate__delay-lg"
:class="{ 'text-primary-light font-medium': $route.path === '/translate' }"
@click="isMenuOpen = false"
>
翻译工具
</router-link>
</router-link> -->
<!-- Mobile Language Selector -->
<div class="py-2 border-t border-background animate__animated animate__fadeInUp animate__delay-lg animate__duration-fast">


+ 11
- 9
src/components/rewards/IncentiveModule.vue View File

@ -8,21 +8,23 @@ const { t, locale } = useI18n();
const incentiveModelsData = computed(() => [
{
id: 1,
name: locale.value === 'zh' ? "基础混币" : "Basic Mixing",
name: t('incentives.models.basic_mixing.name'),
amount: 0.3,
features: locale.value === 'zh'
? ["按交易额收取0.3%费用", "动态风险监控"]
: ["0.3% fee on transaction amount", "Dynamic risk monitoring"],
features: [
t('incentives.models.basic_mixing.features.fee'),
t('incentives.models.basic_mixing.features.monitoring')
],
color: 'primary',
active: true
},
{
id: 2,
name: locale.value === 'zh' ? "跨链桥" : "Cross-chain Bridge",
name: t('incentives.models.cross_chain.name'),
amount: 0.3,
features: locale.value === 'zh'
? ["每笔跨链交易收取0.3%手续费", "手续费+质押双收益"]
: ["0.3% fee per cross-chain transaction", "Dual income from fees and staking"],
features: [
t('incentives.models.cross_chain.features.fee'),
t('incentives.models.cross_chain.features.dual_income')
],
color: 'secondary',
active: false
}
@ -59,7 +61,7 @@ const marketData = computed(() => {
transactionVolume: (marketDataRaw.transaction_volume / 1000000).toFixed(0), // 亿
growthRate: marketDataRaw.growth_rate,
securityIncidents: (marketDataRaw.security_incidents / 100000000).toFixed(0), // 亿
unit: locale.value === 'zh' ? '亿美元' : 'billion USD'
unit: t('incentives.market_data.unit')
};
});
</script>


+ 3
- 3
src/components/rewards/MarketDataModule.vue View File

@ -25,7 +25,7 @@ const marketData = computed(() => [
{
id: 1,
title: t('incentives.market_data.transaction_volume'),
value: `${formattedMarketData.value.transactionVolume}亿美元`,
value: `${formattedMarketData.value.transactionVolume}${t('incentives.market_data.unit')}`,
icon: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2-3 .895-3 2 1.343 2 3 2',
description: t('incentives.market_data.transaction_subtitle'),
isActive: true,
@ -41,7 +41,7 @@ const marketData = computed(() => [
{
id: 3,
title: t('incentives.market_data.security_incidents'),
value: `${formattedMarketData.value.securityIncidents}亿美元`,
value: `${formattedMarketData.value.securityIncidents}${t('incentives.market_data.unit')}`,
icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
description: t('incentives.market_data.security_subtitle'),
isActive: true,
@ -115,4 +115,4 @@ const activeMarketData = computed(() => marketData.value.filter(item => item.isA
.bg-gradient-primary {
background: linear-gradient(135deg, var(--primary-light), var(--primary));
}
</style>
</style>

+ 10
- 20
src/components/rewards/RewardModule.vue View File

@ -9,18 +9,11 @@ const airdropData = {
total_addresses: 100000,
eligible_addresses: 100000,
tokens_per_address: 50,
rules: {
en: [
"Daily release of 1%",
"Rewards stop when holdings < 100 MOSE",
"Unclaimed rewards expire after 15 days"
],
zh: [
"每日释放1%",
"持币少于100枚时停止释放",
"15日内未完成≥100枚闪兑则销毁"
]
}
rules: [
t('rewards.airdrop.rules.daily_release'),
t('rewards.airdrop.rules.stop_condition'),
t('rewards.airdrop.rules.expiry')
]
};
// 使.txt
@ -29,10 +22,7 @@ const tokenBurnData = {
target_burned: 290000000,
burn_percentage: 72.41,
stop_threshold: 10000000,
mechanism: {
en: "100% of cross-chain and mixing fees burned",
zh: "跨链与混币手续费100%销毁"
}
mechanism: t('rewards.burn.mechanism.description')
};
//
@ -40,14 +30,14 @@ const totalAddresses = computed(() => airdropData.total_addresses);
const airdropAddresses = computed(() => airdropData.eligible_addresses);
const airdropAmount = computed(() => airdropData.tokens_per_address);
//
//
const airdropRules = computed(() => {
return locale.value === 'zh' ? airdropData.rules.zh : airdropData.rules.en;
return airdropData.rules;
});
//
//
const burnMechanism = computed(() => {
return locale.value === 'zh' ? tokenBurnData.mechanism.zh : tokenBurnData.mechanism.en;
return tokenBurnData.mechanism;
});
//


+ 30
- 78
src/components/technology/ArchitectureModule.vue View File

@ -1,111 +1,63 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { useSummary } from '@/utils/config';
import { ref, onMounted, computed } from 'vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
const { getSummaryDescription } = useSummary();
const { t } = useI18n();
// API
const techStructureContent = computed(() => {
return getSummaryDescription('config_structural_of_technology');
});
//
const parsedLayers = computed(() => {
const content = techStructureContent.value;
if (!content) return [];
// 使<p>|</p>
const regex = /<p>(.*?)\|(.*?)<\/p>/g;
const layers = [];
let match;
while ((match = regex.exec(content)) !== null) {
const layerName = match[1].trim();
const description = match[2].trim();
// ID
let icon = 'carbon:data-share';
let id = 'layer';
if (layerName.includes('聚合')) {
icon = 'carbon:data-share';
id = 'aggregation';
} else if (layerName.includes('跨链')) {
icon = 'carbon:connect';
id = 'crosschain';
} else if (layerName.includes('路由')) {
icon = 'carbon:flow';
id = 'routing';
} else if (layerName.includes('应用')) {
icon = 'carbon:application';
id = 'application';
}
//
layers.push({
id,
icon,
title: layerName,
description,
features: []
});
}
return layers;
});
// 使API使i18n
// API使
const defaultLayers = [
{
id: 'aggregation',
icon: 'carbon:data-share',
title: '数据聚合层',
description: '负责多链数据的收集与处理',
title: t('architecture.layers.aggregation.title'),
description: t('architecture.layers.aggregation.description'),
features: [
'高性能数据索引与查询',
'多链数据同步与验证',
'分布式存储与缓存'
t('architecture.layers.aggregation.features.indexing'),
t('architecture.layers.aggregation.features.sync'),
t('architecture.layers.aggregation.features.storage')
]
},
{
id: 'crosschain',
icon: 'carbon:connect',
title: '跨链通信层',
description: '实现不同区块链之间的互操作',
title: t('architecture.layers.crosschain.title'),
description: t('architecture.layers.crosschain.description'),
features: [
'原子交换与资产桥接',
'跨链消息传递与验证',
'安全隐私保护通道'
t('architecture.layers.crosschain.features.atomic_swap'),
t('architecture.layers.crosschain.features.messaging'),
t('architecture.layers.crosschain.features.privacy')
]
},
{
id: 'routing',
icon: 'carbon:flow',
title: '智能路由层',
description: '优化跨链交互路径与性能',
title: t('architecture.layers.routing.title'),
description: t('architecture.layers.routing.description'),
features: [
'动态路由算法与路径选择',
'交易费用优化与预估',
'网络拥堵监测与避免'
t('architecture.layers.routing.features.algorithm'),
t('architecture.layers.routing.features.optimization'),
t('architecture.layers.routing.features.monitoring')
]
},
{
id: 'application',
icon: 'carbon:application',
title: '应用接口层',
description: '为开发者提供统一的接口与服务',
title: t('architecture.layers.application.title'),
description: t('architecture.layers.application.description'),
features: [
'SDK与API接口集成',
'开发者工具与文档',
'模块化组件与插件'
t('architecture.layers.application.features.sdk'),
t('architecture.layers.application.features.tools'),
t('architecture.layers.application.features.components')
]
}
];
// 使
// 使使i18n
const architectureLayers = computed(() => {
return parsedLayers.value.length > 0 ? parsedLayers.value : defaultLayers;
return defaultLayers;
});
</script>
@ -113,11 +65,11 @@ const architectureLayers = computed(() => {
<section class="py-16 px-6 md:px-12 lg:px-24">
<div class="container mx-auto">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-6 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
MOSE技术架构
{{ t('architecture.title') }}
</h2>
<p class="max-w-3xl mx-auto text-center text-text-secondary mb-12 wow animate__animated animate__fadeIn animate__delay-xs animate__duration-fast">
多层架构设计确保隐私保护与跨链互操作的高效运行
{{ t('architecture.subtitle') }}
</p>
<!-- 架构图示 - 桌面端 -->
@ -144,7 +96,7 @@ const architectureLayers = computed(() => {
</div>
<h3 class="text-xl font-bold text-text mb-4 text-center pt-2">{{ layer.title }}</h3>
<p class="text-text-secondary text-center mb-4">{{ layer.description }}</p>
<p class="text-text-secondary text-center mb-4">{{ layer.description }}</p>
<!-- 特点列表 -->
<ul v-if="layer.features && layer.features.length > 0" class="space-y-2">
@ -198,4 +150,4 @@ const architectureLayers = computed(() => {
</div>
</div>
</section>
</template>
</template>

+ 10
- 3
src/i18n/locales/ar.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "بلوك تشين MOSE",
"subtitle": "منصة بلوك تشين عالية الأداء والأمان والقابلة للتوسع من الجيل التالي"
"title2": "MOSE - رائد حماية خصوصية البلوك تشين",
"title3": "بناء مستقبل الحوسبة الخاصة",
"title4": "حلول مبتكرة للخصوصية عبر السلاسل",
"subtitle": "منصة بلوك تشين عالية الأداء والأمان والقابلة للتوسع من الجيل التالي",
"subtitle2": "جعل الخصوصية حقًا أساسيًا في البلوك تشين",
"subtitle3": "حماية البيانات، إطلاق القيمة",
"subtitle4": "حلول خصوصية آمنة وفعالة وقابلة للتوسع"
},
"video": {
"title": "شاهد فيديونـا"
@ -400,8 +406,9 @@
"view_all": "عرض الكل"
},
"highlights": {
"title": "نقاط بارزة في المجتمع",
"view_all": "عرض الكل"
"title": "社区风采",
"view_all": "عرض الكل",
"no_data": "لا توجد بيانات نشاط المجتمع"
},
"events": {
"title": "الأحداث القادمة",


+ 92
- 14
src/i18n/locales/en.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "MOSE Blockchain",
"subtitle": "Next-generation high-performance, secure, and scalable blockchain platform"
"title2": "MOSE - Blockchain Privacy Protection Pioneer",
"title3": "Building the Future of Privacy Computing",
"title4": "Innovative Privacy Cross-chain Solutions",
"subtitle": "Next-generation high-performance, secure, and scalable blockchain platform",
"subtitle2": "Making Privacy a Fundamental Right in Blockchain",
"subtitle3": "Protecting Data, Unleashing Value",
"subtitle4": "Secure, Efficient, and Scalable Privacy Solutions"
},
"video": {
"title": "Watch Our Video"
@ -116,7 +122,7 @@
"vi": "Tiếng Việt"
},
"ecosystem": {
"title": "Ecosystem",
"title": "MOSE Ten Ecosystems",
"subtitle": "Explore the MOSE ecosystem and discover the various projects and applications built on our platform",
"categories": {
"all": "All",
@ -199,11 +205,12 @@
}
},
"companies": {
"title": "Global Presence",
"subtitle": "MOSE has established multiple offices worldwide to serve global users",
"title": "Global Strategic Deployment",
"subtitle": "Global strategic deployment, building a global strategic network",
"list_title": "Global Offices",
"focus": "Business Focus",
"team": "Team Size",
"footer_description": "Through a global office network, MOSE provides localized services and support to users in different regions",
"singapore": {
"name": "Singapore Headquarters",
"description": "MOSE's global headquarters, responsible for global strategy and business development"
@ -247,8 +254,8 @@
}
},
"technology": {
"title": "Technology",
"subtitle": "Explore MOSE's core technological innovations",
"title": "Leading Blockchain Privacy Technology",
"subtitle": "MOSE builds privacy protection infrastructure for the blockchain world through innovative cryptography and cross-chain technology",
"architecture": {
"title": "Technical Architecture",
"subtitle": "MOSE employs a multi-layer architecture design to achieve high performance, security, and scalability",
@ -290,8 +297,8 @@
}
},
"ecosystem_integration": {
"title": "Ecosystem Integration",
"subtitle": "Seamlessly connect with the broader blockchain ecosystem",
"title": "Ecosystem Applications",
"subtitle": "Seamless connection with the broader blockchain ecosystem",
"compatibility": {
"title": "Multi-Chain Compatibility",
"description": "MOSE is compatible with major blockchain ecosystems including Ethereum, Cosmos, and Polkadot."
@ -384,6 +391,13 @@
"title": "Join Our Community",
"subtitle": "Connect with MOSE users, developers, and enthusiasts worldwide"
},
"introduction": {
"title": "Community Introduction"
},
"highlights": {
"title": "社区风采",
"no_data": "No community activity data available"
},
"social_media": {
"title": "Official Social Media",
"follow": "Follow us for the latest updates and announcements"
@ -403,10 +417,7 @@
"title": "Official Announcements",
"view_all": "View All"
},
"highlights": {
"title": "Community Highlights",
"view_all": "View All"
},
"events": {
"title": "Upcoming Events",
"online": "Online",
@ -501,6 +512,73 @@
"read_more": "Read More",
"view_all": "View All",
"learn_more": "Learn More",
"coming_soon": "Coming Soon"
"coming_soon": "Coming Soon",
"time": {
"just_now": "Just now",
"minutes_ago": "{count} minutes ago",
"hours_ago": "{count} hours ago",
"days_ago": "{count} days ago"
},
"errors": {
"load_failed": "Failed to load",
"submit_failed": "Failed to submit"
},
"success_messages": {
"request_success": "Request successful"
}
},
"technology": {
"hero": {
"title": "Technical Architecture",
"description": "Next-generation privacy protection protocol based on zero-knowledge proofs and mixing technology"
},
"architecture": {
"title": "System Architecture",
"subtitle": "Understanding MOSE's technical architecture"
},
"innovation": {
"title": "Technical Innovation",
"subtitle": "Our core technical advantages"
},
"integration": {
"title": "Ecosystem Integration",
"subtitle": "Seamless integration with mainstream blockchains"
},
"techs": {
"zero_knowledge": {
"title": "Zero-Knowledge Proofs",
"description": "Advanced cryptographic technology ensuring transaction privacy while maintaining verifiability"
},
"cross_chain": {
"title": "Cross-Chain Protocol",
"description": "Innovative cross-chain bridge technology enabling seamless asset transfers between different blockchains"
},
"smart_contracts": {
"title": "Smart Contracts",
"description": "High-performance smart contract platform supporting complex business logic and privacy protection"
},
"governance": {
"title": "Decentralized Governance",
"description": "Community-driven governance mechanism allowing token holders to participate in protocol decisions"
}
},
"apps": {
"privacy_wallet": {
"title": "Privacy Wallet",
"description": "Secure multi-chain wallet with built-in privacy protection features"
},
"dex": {
"title": "Decentralized Exchange",
"description": "Privacy-preserving DEX supporting anonymous trading and cross-chain swaps"
},
"bridge": {
"title": "Cross-Chain Bridge Service",
"description": "Secure and efficient cross-chain asset transfer service"
},
"staking": {
"title": "Staking Rewards",
"description": "Participate in network security and earn staking rewards"
}
}
}
}
}

+ 10
- 3
src/i18n/locales/fr.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "Blockchain MOSE",
"subtitle": "Plateforme blockchain de nouvelle génération performante, sécurisée et évolutive"
"title2": "MOSE - Pionnier de la protection de la confidentialité blockchain",
"title3": "Construire l'avenir de l'informatique confidentielle",
"title4": "Solutions innovantes de confidentialité inter-chaînes",
"subtitle": "Plateforme blockchain de nouvelle génération performante, sécurisée et évolutive",
"subtitle2": "Faire de la confidentialité un droit fondamental dans la blockchain",
"subtitle3": "Protéger les données, libérer la valeur",
"subtitle4": "Solutions de confidentialité sécurisées, efficaces et évolutives"
},
"video": {
"title": "Regarder notre vidéo"
@ -400,8 +406,9 @@
"view_all": "Voir tout"
},
"highlights": {
"title": "Points forts de la communauté",
"view_all": "Voir tout"
"title": "社区风采",
"view_all": "Voir tout",
"no_data": "Aucune donnée d'activité communautaire disponible"
},
"events": {
"title": "Événements à venir",


+ 11
- 4
src/i18n/locales/ja.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "MOSEブロックチェーン",
"subtitle": "次世代の高性能、安全、スケーラブルなブロックチェーンプラットフォーム"
"title2": "MOSE - ブロックチェーンプライバシー保護のパイオニア",
"title3": "プライバシーコンピューティングの未来を構築",
"title4": "革新的なプライバシークロスチェーンソリューション",
"subtitle": "次世代の高性能、安全、スケーラブルなブロックチェーンプラットフォーム",
"subtitle2": "プライバシーをブロックチェーンの基本的権利に",
"subtitle3": "データを保護し、価値を解放",
"subtitle4": "安全で効率的、スケーラブルなプライバシーソリューション"
},
"video": {
"title": "動画を見る"
@ -400,8 +406,9 @@
"view_all": "すべて表示"
},
"highlights": {
"title": "コミュニティハイライト",
"view_all": "すべて表示"
"title": "社区风采",
"view_all": "すべて表示",
"no_data": "コミュニティ活動データがありません"
},
"events": {
"title": "今後のイベント",
@ -499,4 +506,4 @@
"learn_more": "詳細を見る",
"coming_soon": "近日公開"
}
}
}

+ 10
- 3
src/i18n/locales/ko.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "MOSE 블록체인",
"subtitle": "차세대 고성능, 보안, 확장 가능한 블록체인 플랫폼"
"title2": "MOSE - 블록체인 개인정보 보호의 선구자",
"title3": "개인정보 컴퓨팅의 미래 구축",
"title4": "혁신적인 개인정보 크로스체인 솔루션",
"subtitle": "차세대 고성능, 보안, 확장 가능한 블록체인 플랫폼",
"subtitle2": "개인정보 보호를 블록체인의 기본 권리로",
"subtitle3": "데이터 보호, 가치 창출",
"subtitle4": "안전하고 효율적이며 확장 가능한 개인정보 솔루션"
},
"video": {
"title": "우리의 비디오 보기"
@ -400,8 +406,9 @@
"view_all": "모두 보기"
},
"highlights": {
"title": "커뮤니티 하이라이트",
"view_all": "모두 보기"
"title": "社区风采",
"view_all": "모두 보기",
"no_data": "커뮤니티 활동 데이터가 없습니다"
},
"events": {
"title": "다가오는 이벤트",


+ 10
- 3
src/i18n/locales/ms.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "Blockchain MOSE",
"subtitle": "Platform blockchain generasi akan datang yang berprestasi tinggi, selamat dan boleh dikembangkan"
"title2": "MOSE - Perintis Perlindungan Privasi Blockchain",
"title3": "Membina Masa Depan Pengkomputeran Privasi",
"title4": "Penyelesaian Privasi Rantaian Silang yang Inovatif",
"subtitle": "Platform blockchain generasi akan datang yang berprestasi tinggi, selamat dan boleh dikembangkan",
"subtitle2": "Menjadikan Privasi Hak Asasi dalam Blockchain",
"subtitle3": "Melindungi Data, Melepaskan Nilai",
"subtitle4": "Penyelesaian Privasi yang Selamat, Cekap dan Boleh Dikembangkan"
},
"video": {
"title": "Tonton Video Kami"
@ -400,8 +406,9 @@
"view_all": "Lihat Semua"
},
"highlights": {
"title": "Sorotan Komuniti",
"view_all": "Lihat Semua"
"title": "社区风采",
"view_all": "Lihat Semua",
"no_data": "Tiada data aktiviti komuniti tersedia"
},
"events": {
"title": "Acara Mendatang",


+ 10
- 3
src/i18n/locales/pt.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "Blockchain MOSE",
"subtitle": "Plataforma blockchain de próxima geração de alto desempenho, segura e escalável"
"title2": "MOSE - Pioneiro em Proteção de Privacidade Blockchain",
"title3": "Construindo o Futuro da Computação Privada",
"title4": "Soluções Inovadoras de Privacidade Cross-chain",
"subtitle": "Plataforma blockchain de próxima geração de alto desempenho, segura e escalável",
"subtitle2": "Tornando a Privacidade um Direito Fundamental no Blockchain",
"subtitle3": "Protegendo Dados, Liberando Valor",
"subtitle4": "Soluções de Privacidade Seguras, Eficientes e Escaláveis"
},
"video": {
"title": "Assista ao nosso vídeo"
@ -400,8 +406,9 @@
"view_all": "Ver tudo"
},
"highlights": {
"title": "Destaques da comunidade",
"view_all": "Ver tudo"
"title": "社区风采",
"view_all": "Ver tudo",
"no_data": "Nenhum dado de atividade da comunidade disponível"
},
"events": {
"title": "Eventos futuros",


+ 10
- 3
src/i18n/locales/ru.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "Блокчейн MOSE",
"subtitle": "Платформа следующего поколения с высокой производительностью, безопасностью и масштабируемостью"
"title2": "MOSE - Пионер защиты конфиденциальности в блокчейне",
"title3": "Создание будущего конфиденциальных вычислений",
"title4": "Инновационные решения для конфиденциальности между блокчейнами",
"subtitle": "Платформа следующего поколения с высокой производительностью, безопасностью и масштабируемостью",
"subtitle2": "Делаем конфиденциальность фундаментальным правом в блокчейне",
"subtitle3": "Защита данных, раскрытие ценности",
"subtitle4": "Безопасные, эффективные и масштабируемые решения для конфиденциальности"
},
"video": {
"title": "Смотреть наше видео"
@ -400,8 +406,9 @@
"view_all": "Посмотреть все"
},
"highlights": {
"title": "Выделенные моменты сообщества",
"view_all": "Посмотреть все"
"title": "社区风采",
"view_all": "Посмотреть все",
"no_data": "Нет данных о деятельности сообщества"
},
"events": {
"title": "Предстоящие мероприятия",


+ 10
- 3
src/i18n/locales/th.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "Blockchain MOSE",
"subtitle": "แพลตฟอร์มบล็อกเชนรุ่นต่อไปที่มีประสิทธิภาพสูง ปลอดภัย และปรับขยายได้"
"title2": "MOSE - ผู้บุกเบิกการปกป้องความเป็นส่วนตัวบนบล็อกเชน",
"title3": "สร้างอนาคตของการคำนวณความเป็นส่วนตัว",
"title4": "โซลูชันข้ามเชนความเป็นส่วนตัวที่เป็นนวัตกรรม",
"subtitle": "แพลตฟอร์มบล็อกเชนรุ่นต่อไปที่มีประสิทธิภาพสูง ปลอดภัย และปรับขยายได้",
"subtitle2": "ทำให้ความเป็นส่วนตัวเป็นสิทธิขั้นพื้นฐานในบล็อกเชน",
"subtitle3": "ปกป้องข้อมูล ปลดปล่อยคุณค่า",
"subtitle4": "โซลูชันความเป็นส่วนตัวที่ปลอดภัย มีประสิทธิภาพ และปรับขยายได้"
},
"video": {
"title": "ดูวิดีโอของเรา"
@ -400,8 +406,9 @@
"view_all": "ดูทั้งหมด"
},
"highlights": {
"title": "ไฮไลต์ของชุมชน",
"view_all": "ดูทั้งหมด"
"title": "社区风采",
"view_all": "ดูทั้งหมด",
"no_data": "ไม่มีข้อมูลกิจกรรมชุมชน"
},
"events": {
"title": "เหตุการณ์ที่กำลังจะมาถึง",


+ 10
- 3
src/i18n/locales/vi.json View File

@ -11,7 +11,13 @@
"home": {
"hero": {
"title": "Blockchain MOSE",
"subtitle": "Nền tảng blockchain thế hệ tiếp theo hiệu suất cao, an toàn và có khả năng mở rộng"
"title2": "MOSE - Người Tiên Phong Bảo Vệ Quyền Riêng Tư Blockchain",
"title3": "Xây Dựng Tương Lai Của Điện Toán Riêng Tư",
"title4": "Giải Pháp Chuỗi Chéo Riêng Tư Sáng Tạo",
"subtitle": "Nền tảng blockchain thế hệ tiếp theo hiệu suất cao, an toàn và có khả năng mở rộng",
"subtitle2": "Biến Quyền Riêng Tư Thành Quyền Cơ Bản Trong Blockchain",
"subtitle3": "Bảo Vệ Dữ Liệu, Giải Phóng Giá Trị",
"subtitle4": "Giải Pháp Riêng Tư An Toàn, Hiệu Quả và Có Khả Năng Mở Rộng"
},
"video": {
"title": "Xem Video Của Chúng Tôi"
@ -400,8 +406,9 @@
"view_all": "Xem Tất Cả"
},
"highlights": {
"title": "Điểm Nổi Bật Cộng Đồng",
"view_all": "Xem Tất Cả"
"title": "社区风采",
"view_all": "Xem Tất Cả",
"no_data": "Không có dữ liệu hoạt động cộng đồng"
},
"events": {
"title": "Sự Kiện Sắp Tới",


+ 10
- 3
src/i18n/locales/zh-TW.json View File

@ -10,8 +10,14 @@
},
"home": {
"hero": {
"title": "Blockchain MOSE",
"subtitle": "下一代高效、安全且可擴展的區塊鏈平台"
"title": "MOSE區塊鏈",
"title2": "MOSE - 區塊鏈隱私保護先驅",
"title3": "構建隱私計算的未來",
"title4": "創新的隱私跨鏈解決方案",
"subtitle": "下一代高效、安全且可擴展的區塊鏈平台",
"subtitle2": "讓隱私成為區塊鏈的基本權利",
"subtitle3": "保護數據,釋放價值",
"subtitle4": "安全、高效、可擴展的隱私解決方案"
},
"video": {
"title": "觀看我們的影片"
@ -401,7 +407,8 @@
},
"highlights": {
"title": "社區亮點",
"view_all": "查看全部"
"view_all": "查看全部",
"no_data": "暫無社區活動數據"
},
"events": {
"title": "即將到來的事件",


+ 2496
- 396
src/i18n/locales/zh.json
File diff suppressed because it is too large
View File


+ 14
- 14
src/utils/http/request.ts View File

@ -78,7 +78,7 @@ export function createRequest() {
// 如果配置了显示加载提示
if ((config as any).showLoading) {
// 这里可以添加全局加载提示
console.log('显示加载提示');
console.log('Show loading indicator');
}
return config;
@ -97,7 +97,7 @@ export function createRequest() {
// 如果配置了显示加载提示,需要关闭
if ((response.config as any).showLoading) {
// 这里可以关闭全局加载提示
console.log('关闭加载提示');
console.log('Hide loading indicator');
}
// 如果是直接获取响应数据模式
@ -115,7 +115,7 @@ export function createRequest() {
// 如果配置了显示错误提示
if ((response.config as any).showError !== false) {
// 这里可以添加全局错误提示
console.error(res.message || '请求失败');
console.error(res.message || 'Request failed');
}
// 如果配置了自定义错误处理
@ -127,7 +127,7 @@ export function createRequest() {
// 特定业务状态码处理
if (res.code === 401) {
// 未授权,可能需要跳转到登录页
console.error('未授权,请重新登录');
console.error('Unauthorized, please login again');
// 可以添加路由跳转逻辑
}
@ -155,7 +155,7 @@ export function createRequest() {
// 如果配置了显示加载提示,需要关闭
if (error.config && (error.config as any).showLoading) {
// 这里可以关闭全局加载提示
console.log('关闭加载提示');
console.log('Hide loading indicator');
}
// 处理错误响应
@ -163,23 +163,23 @@ export function createRequest() {
// 如果配置了显示错误提示
if (error.config && (error.config as any).showError !== false) {
// 这里可以添加全局错误提示
let errorMessage = '未知错误';
let errorMessage = 'Unknown error';
switch (error.response.status) {
case 401:
errorMessage = '未授权,请重新登录';
errorMessage = 'Unauthorized, please login again';
break;
case 403:
errorMessage = '没有权限访问此资源';
errorMessage = 'No permission to access this resource';
break;
case 404:
errorMessage = '请求的资源不存在';
errorMessage = 'Requested resource not found';
break;
case 500:
errorMessage = '服务器错误';
errorMessage = 'Server error';
break;
default:
errorMessage = `请求错误: ${error.response.status}`;
errorMessage = `Request error: ${error.response.status}`;
}
console.error(errorMessage);
@ -187,12 +187,12 @@ export function createRequest() {
} else if (error.request) {
// 请求发出但没有收到响应
if (error.config && (error.config as any).showError !== false) {
console.error('网络错误,无法连接到服务器');
console.error('Network error, unable to connect to server');
}
} else {
// 请求配置出错
if (error.config && (error.config as any).showError !== false) {
console.error('请求配置错误', error.message);
console.error('Request configuration error', error.message);
}
}
@ -293,4 +293,4 @@ export function createRequest() {
}
// 导出默认请求实例
export default createRequest();
export default createRequest();

+ 14
- 16
src/views/About.vue View File

@ -2,15 +2,13 @@
import { useI18n } from 'vue-i18n';
import TeamModule from '@/components/about/TeamModule.vue';
import MilestoneModule from '@/components/about/MilestoneModule.vue';
//
// Remove ecosystem module
// import EcosystemModule from '@/components/about/EcosystemModule.vue';
import CompanyModule from '@/components/about/CompanyModule.vue';
import PartnersModule from '@/components/about/PartnersModule.vue';
import { useSummary } from '@/utils/config';
import { useConfig } from '@/utils/config';
const { t } = useI18n();
const { getSummaryDescription } = useSummary();
const { getConfigImage } = useConfig();
</script>
@ -39,39 +37,39 @@ const { getConfigImage } = useConfig();
</section>
<!-- Our Story Section -->
<section class="py-16 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('about_story_bg')})` }">
<section class="py-16 px-6 md:px-12 lg:px-24 bg-center bg-cover bg-no-repeat" :style="{ backgroundImage: `url(${getConfigImage('about_story_bg')})` }">
<div class="container mx-auto">
<div class="max-w-3xl mx-auto">
<div class="max-w-3xl mx-auto md:bg-transparent rounded-lg p-6 md:p-0">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-6 wow animate__animated animate__fadeInUp animate__duration-fast">{{ t('about.story.title') }}</h2>
<!-- 愿景与使命 -->
<!-- Vision and Mission -->
<div class="mb-8">
<div class="space-y-6 text-text-secondary">
<div class="wow animate__animated animate__fadeInUp animate__duration-fast" v-html="getSummaryDescription('config_aspiration_and_mission')"></div>
<div class="wow animate__animated animate__fadeInUp animate__duration-fast break-words overflow-hidden" v-html="t('about.story.vision_mission')"></div>
</div>
</div>
<!-- 我们的故事 -->
<!-- Our Story -->
<div class="space-y-6 text-text-secondary">
<p class="wow animate__animated animate__fadeInUp animate__duration-fast" v-html="getSummaryDescription('config_our_story')"></p>
<p class="wow animate__animated animate__fadeInUp animate__duration-fast break-words overflow-hidden" v-html="t('about.story.content')"></p>
</div>
</div>
</div>
</section>
<!-- 领导团队 -->
<!-- Leadership Team -->
<TeamModule />
<!-- 里程碑 -->
<!-- Milestones -->
<MilestoneModule />
<!-- 移除生态系统模块 -->
<!-- Remove ecosystem module -->
<!-- <EcosystemModule /> -->
<!-- 全球战略部署 -->
<!-- Global Strategic Deployment -->
<CompanyModule />
<!-- 合作伙伴 -->
<PartnersModule />
<!-- Partners -->
<!-- <PartnersModule /> -->
</div>
</template>
</template>

+ 350
- 116
src/views/Community.vue View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, onMounted, computed } from 'vue';
import { ref, onMounted } from 'vue';
import { Icon } from '@iconify/vue';
import { useConfig } from '@/utils/config';
import { Swiper, SwiperSlide } from 'swiper/vue';
@ -24,7 +24,7 @@ import {
const { getConfigImage } = useConfig();
const { t } = useI18n();
const { t, locale } = useI18n();
//
const socialMediaAccounts = ref<OfficialMediaItem[]>([]);
@ -88,6 +88,9 @@ const swiperOptions = {
//
const showDetailModal = ref(false);
const selectedMessage = ref<MessageItem | null>(null);
const showVideoModal = ref(false);
const showCommunityDetailModal = ref(false);
const selectedCommunity = ref<CommunityItem | null>(null);
//
const showMessageDetail = (message: MessageItem) => {
@ -101,9 +104,35 @@ const closeMessageDetail = () => {
selectedMessage.value = null;
};
//
const showVideo = (community: CommunityItem) => {
if (community.video) {
selectedCommunity.value = community;
showVideoModal.value = true;
}
};
//
const closeVideoModal = () => {
showVideoModal.value = false;
selectedCommunity.value = null;
};
//
const showCommunityDetail = (community: CommunityItem) => {
selectedCommunity.value = community;
showCommunityDetailModal.value = true;
};
//
const closeCommunityDetailModal = () => {
showCommunityDetailModal.value = false;
selectedCommunity.value = null;
};
//
const goToMessage = (id: string) => {
console.log('查看公告详情:', id);
console.log(t('community.message.viewDetail'), id);
//
};
@ -112,7 +141,7 @@ const formatDate = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return new Intl.DateTimeFormat('zh-CN', {
return new Intl.DateTimeFormat(locale.value === 'zh' ? 'zh-CN' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
@ -127,9 +156,36 @@ const loadSocialMedia = async () => {
pageSize: 10,
pageNo: 1
});
socialMediaAccounts.value = data;
// Only keep image data from API, use i18n for text
socialMediaAccounts.value = data.map((item, index) => ({
id: item.id || index + 1,
title: t(`community.social_media.default.item${index + 1}.title`),
username: t(`community.social_media.default.item${index + 1}.username`),
description: t(`community.social_media.default.item${index + 1}.description`),
image: item.image || '/LOGO.png',
url: '#'
}));
} catch (error) {
console.error('加载社交媒体数据失败:', error);
console.error(t('common.error.loadSocialMediaFailed'), error);
// Use default i18n data on error
socialMediaAccounts.value = [
{
id: 1,
title: t('community.social_media.default.twitter.title'),
username: t('community.social_media.default.twitter.username'),
description: t('community.social_media.default.twitter.description'),
image: '/LOGO.png',
url: '#'
},
{
id: 2,
title: t('community.social_media.default.discord.title'),
username: t('community.social_media.default.discord.username'),
description: t('community.social_media.default.discord.description'),
image: '/LOGO.png',
url: '#'
}
];
} finally {
socialMediaLoading.value = false;
}
@ -143,12 +199,21 @@ const loadForum = async () => {
pageSize: 10,
pageNo: 1
});
forumList.value = data;
// Use original API data for forum content
forumList.value = data.map((item, index) => ({
id: item.id || index + 1,
title: item.title || t(`community.forum.default.item${index + 1}.title`),
content: item.content || t(`community.forum.default.item${index + 1}.content`),
createBy: item.createBy || t(`community.forum.default.item${index + 1}.author`),
createTime: item.createTime,
image: item.image || '/LOGO.png',
likeCount: item.likeCount || 0
}));
//
await Promise.all(data.map(forum => loadComments(forum.id)));
await Promise.all(forumList.value.map(forum => loadComments(forum.id)));
} catch (error) {
console.error('加载论坛数据失败:', error);
console.error(t('common.error.loadForumFailed'), error);
} finally {
forumLoading.value = false;
}
@ -161,9 +226,16 @@ const loadComments = async (forumId: string) => {
const data = await queryCommentsList({
forumId
});
commentsMap.value[forumId] = data;
// Use original API data for comments
commentsMap.value[forumId] = data.map((item, index) => ({
id: item.id || index + 1,
content: item.content || t(`community.forum.default.comment${index + 1}.content`),
createBy: item.createBy || t(`community.forum.default.comment${index + 1}.author`),
createTime: item.createTime,
forumId: item.forumId
}));
} catch (error) {
console.error(`加载论坛${forumId}的评论数据失败:`, error);
console.error(t('common.error.loadCommentsFailed', { forumId }), error);
} finally {
commentsLoading.value = false;
}
@ -177,9 +249,17 @@ const loadMessages = async () => {
pageSize: 10,
pageNo: 1
});
messageList.value = data;
// Only keep image data from API, use i18n for text
messageList.value = data.map((item, index) => ({
id: item.id || index + 1,
title: t(`community.announcements.default.item${index + 1}.title`),
content: t(`community.announcements.default.item${index + 1}.content`),
description: t(`community.announcements.default.item${index + 1}.description`),
createTime: item.createTime,
image: item.image
}));
} catch (error) {
console.error('加载信息公示数据失败:', error);
console.error(t('common.error.loadMessagesFailed'), error);
} finally {
messageLoading.value = false;
}
@ -193,9 +273,18 @@ const loadCommunity = async () => {
pageSize: 6,
pageNo: 1
});
communityList.value = data;
// Only keep image and video data from API, use i18n for text
communityList.value = data.map((item, index) => ({
id: item.id || index + 1,
title: t(`community.highlights.default.item${index + 1}.title`),
description: t(`community.highlights.default.item${index + 1}.description`),
content: t(`community.highlights.default.item${index + 1}.content`),
createTime: item.createTime,
image: item.image,
video: item.video
}));
} catch (error) {
console.error('加载社区数据失败:', error);
console.error(t('common.error.loadCommunityFailed'), error);
} finally {
communityLoading.value = false;
}
@ -219,7 +308,7 @@ const submitComment = async (forumId: string) => {
newMessage.value.content = '';
newMessage.value.forumId = null;
} catch (error) {
console.error('提交评论失败:', error);
console.error(t('common.error.submitCommentFailed'), error);
} finally {
isSubmitting.value = false;
}
@ -271,16 +360,21 @@ const getTimeAgo = (timestamp: string) => {
const now = new Date();
const postTime = new Date(timestamp);
const diffTime = Math.abs(now.getTime() - postTime.getTime());
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
const diffSeconds = Math.floor(diffTime / 1000);
const diffMinutes = Math.floor(diffSeconds / 60);
const diffHours = Math.floor(diffMinutes / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffDays < 1) {
return t('community.time.today');
} else if (diffDays === 1) {
return t('community.time.yesterday');
if (diffSeconds < 60) {
return t('common.time.just_now');
} else if (diffMinutes < 60) {
return t('common.time.minutes_ago', { count: diffMinutes });
} else if (diffHours < 24) {
return t('common.time.hours_ago', { count: diffHours });
} else if (diffDays < 7) {
return t('community.time.days_ago', { days: diffDays });
return t('common.time.days_ago', { count: diffDays });
} else {
return new Date(timestamp).toLocaleDateString();
return new Date(timestamp).toLocaleDateString(locale.value === 'zh' ? 'zh-CN' : 'en-US');
}
};
@ -341,95 +435,114 @@ onMounted(async() => {
</div>
</section>
<!-- 官方公告 Section -->
<!-- Community Introduction Section (formerly Official Announcements) -->
<section class="py-16 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('com_mes_bg')})` }">
<div class="container mx-auto">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
官方公告
{{ t('community.introduction.title') }}
</h2>
<div v-if="messageLoading" class="flex justify-center py-10">
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary"></div>
<div class="bg-background/80 backdrop-blur-sm rounded-xl p-8 shadow-card max-w-4xl mx-auto">
<div class="prose prose-lg max-w-none text-white leading-relaxed wow animate__animated animate__fadeIn">
<p class="first-letter:text-4xl first-letter:font-bold first-letter:text-primary first-letter:mr-1 first-letter:float-left" v-html="t('community.introduction.content')"></p>
</div>
<!-- <div class="mt-8 flex justify-center">
<a href="#community-activities" class="inline-flex items-center px-6 py-3 bg-primary text-white rounded-full hover:bg-primary-dark transition-colors duration-300 transform hover:scale-105">
<span>了解更多社区活动</span>
<Icon icon="carbon:arrow-down" class="ml-2" />
</a>
</div> -->
</div>
<div v-else class="space-y-6">
<div
v-for="message in messageList"
:key="message.id"
class="flex bg-background rounded-xl overflow-hidden shadow-card hover:shadow-lg transition-all duration-300 wow animate__animated animate__fadeInUp"
>
<!-- 公告图片 -->
<div class="w-1/4">
<img :src="message.image || '/LOGO.png'" :alt="message.title" class="w-full h-full object-cover" />
</div>
<!-- 公告内容 -->
<div class="w-3/4 p-6">
<h3
class="text-xl font-bold text-text mb-3 line-clamp-2 hover:text-primary hover:bg-primary/10 px-2 py-1 rounded transition-all duration-300 cursor-pointer hover:underline transform hover:scale-105"
@click="showMessageDetail(message)"
>
{{ message.title }}
</h3>
<p class="text-text-secondary text-base mb-4 line-clamp-3">{{ message.description }}</p>
<div class="flex items-center text-text-secondary text-sm">
<Icon icon="carbon:time" class="mr-2" />
<span>{{ formatDate(message.createTime) }}</span>
</div>
</section>
<!-- Original official announcement content (commented out) -->
<!--
<section class="py-16 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('com_mes_bg')})` }">
<div class="container mx-auto">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
官方公告
</h2>
<div v-if="messageLoading" class="flex justify-center py-10">
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary"></div>
</div>
<div v-else class="space-y-6">
<div
v-for="message in messageList"
:key="message.id"
class="flex bg-background rounded-xl overflow-hidden shadow-card hover:shadow-lg transition-all duration-300 wow animate__animated animate__fadeInUp"
>
<div class="w-1/4">
<img :src="message.image || '/LOGO.png'" :alt="message.title" class="w-full h-full object-cover" />
</div>
<div class="w-3/4 p-6">
<h3
class="text-xl font-bold text-text mb-3 line-clamp-2 hover:text-primary hover:bg-primary/10 px-2 py-1 rounded transition-all duration-300 cursor-pointer hover:underline transform hover:scale-105"
@click="showMessageDetail(message)"
>
{{ message.title }}
</h3>
<p class="text-text-secondary text-base mb-4 line-clamp-3">{{ message.description }}</p>
<div class="flex items-center text-text-secondary text-sm">
<Icon icon="carbon:time" class="mr-2" />
<span>{{ formatDate(message.createTime) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 消息详情弹窗 -->
<div
v-if="showDetailModal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click="closeMessageDetail"
>
<div
class="bg-background rounded-xl max-w-2xl w-full max-h-[80vh] overflow-y-auto"
@click.stop
v-if="showDetailModal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
@click="closeMessageDetail"
>
<!-- 弹窗头部 -->
<div class="p-6 border-b border-border">
<div class="flex justify-between items-start">
<h3 class="text-2xl font-bold text-text">{{ selectedMessage?.title }}</h3>
<button
@click="closeMessageDetail"
class="text-text-secondary hover:text-text transition-colors duration-300"
>
<Icon icon="carbon:close" class="h-6 w-6" />
</button>
</div>
<div class="flex items-center text-text-secondary text-sm mt-2">
<Icon icon="carbon:time" class="mr-2" />
<span>{{ formatDate(selectedMessage?.createTime) }}</span>
</div>
</div>
<!-- 弹窗内容 -->
<div class="p-6">
<div v-if="selectedMessage?.image" class="mb-6">
<img
:src="selectedMessage.image"
:alt="selectedMessage.title"
class="w-full h-64 object-cover rounded-lg"
/>
<div
class="bg-background rounded-xl max-w-2xl w-full max-h-[80vh] overflow-y-auto"
@click.stop
>
<div class="p-6 border-b border-border">
<div class="flex justify-between items-start">
<h3 class="text-2xl font-bold text-text">{{ selectedMessage?.title }}</h3>
<button
@click="closeMessageDetail"
class="text-text-secondary hover:text-text transition-colors duration-300"
>
<Icon icon="carbon:close" class="h-6 w-6" />
</button>
</div>
<div class="flex items-center text-text-secondary text-sm mt-2">
<Icon icon="carbon:time" class="mr-2" />
<span>{{ formatDate(selectedMessage?.createTime) }}</span>
</div>
</div>
<div class="prose prose-lg max-w-none">
<div v-html="selectedMessage?.description"></div>
<div class="p-6">
<div v-if="selectedMessage?.image" class="mb-6">
<img
:src="selectedMessage.image"
:alt="selectedMessage.title"
class="w-full h-64 object-cover rounded-lg"
/>
</div>
<div class="prose prose-lg max-w-none">
<div v-html="selectedMessage?.content"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</section>
-->
<!-- 社区风采 Section (原社区亮点) -->
<!-- Community Highlights Section (formerly Community Highlights) -->
<section class="py-16 px-6 md:px-12 lg:px-24 bg-background-light" :style="{ backgroundImage: `url(${getConfigImage('com_show_bg')})` }">
<div class="container mx-auto">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
社区风采
{{ t('community.highlights.title') }}
</h2>
<div v-if="communityLoading" class="flex justify-center py-10">
@ -451,7 +564,7 @@ onMounted(async() => {
<SwiperSlide v-for="community in communityList" :key="community.id">
<div class="bg-background rounded-2xl overflow-hidden shadow-xl hover:shadow-2xl transition-all duration-500 h-full transform hover:scale-105">
<div class="flex flex-col h-full">
<div class="relative overflow-hidden">
<div class="relative overflow-hidden cursor-pointer" @click="showVideo(community)">
<img
:src="community.image || '/LOGO.png'"
:alt="community.title"
@ -460,7 +573,12 @@ onMounted(async() => {
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
<div class="p-8 flex-1 flex flex-col">
<h3 class="text-2xl font-bold text-text mb-4">{{ community.title }}</h3>
<h3
class="text-2xl font-bold text-text mb-4 cursor-pointer hover:text-primary transition-colors duration-300"
@click="showCommunityDetail(community)"
>
{{ community.title }}
</h3>
<p class="text-text-secondary mb-6 flex-1 leading-relaxed">{{ community.description }}</p>
</div>
</div>
@ -468,7 +586,7 @@ onMounted(async() => {
</SwiperSlide>
</Swiper>
<!-- 外部自定义导航按钮 -->
<!-- External custom navigation buttons -->
<button
@click="swiperInstance?.slidePrev()"
class="absolute left-4 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-white/90 hover:bg-white rounded-full shadow-lg flex items-center justify-center transition-all duration-300 z-10 group"
@ -484,7 +602,7 @@ onMounted(async() => {
</button>
</div>
<!-- 外部自定义分页器 -->
<!-- External custom pagination -->
<div class="flex justify-center mt-8 gap-4">
<button
v-for="(community, index) in communityList"
@ -500,7 +618,7 @@ onMounted(async() => {
</div>
</section>
<!-- 社交媒体账号 Section -->
<!-- Social Media Accounts Section -->
<section class="py-16 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('com_media_bg')})` }">
<div class="container mx-auto">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
@ -523,7 +641,7 @@ onMounted(async() => {
>
<div class="flex items-center mb-4">
<div class="w-12 h-12 rounded-full bg-primary bg-opacity-10 flex items-center justify-center mr-4">
<Icon :icon="getSocialIcon(account.title)" class="h-6 w-6 text-primary" />
<img :src="account.image" alt="" class="w-12 h-12 rounded-full">
</div>
<div>
<h3 class="text-lg font-bold text-text">{{ account.title }}</h3>
@ -533,7 +651,7 @@ onMounted(async() => {
<p class="text-text-secondary mb-4" v-html="account.description"></p>
<a
<!-- <a
:href="account.url"
target="_blank"
rel="noopener noreferrer"
@ -543,13 +661,13 @@ onMounted(async() => {
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</a> -->
</div>
</div>
</div>
</section>
<!-- 社区论坛 Section -->
<!-- Community Forum Section -->
<section class="py-16 px-6 md:px-12 lg:px-24 bg-background-light" :style="{ backgroundImage: `url(${getConfigImage('com_forum_bg')})` }" >
<div class="container mx-auto">
<h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
@ -570,20 +688,19 @@ onMounted(async() => {
:key="forum.id"
class="bg-background rounded-xl shadow-card overflow-hidden wow animate__animated animate__fadeInUp"
>
<!-- 帖子内容 -->
<!-- Post content -->
<div class="p-6">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-full bg-primary bg-opacity-10 flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
<!-- TailwindCSS properties for image scaling -->
<img :src="forum.image" class="w-10 h-10 rounded-full object-cover">
</div>
<div>
<h3 class="text-lg font-bold text-text">{{ forum.title }}</h3>
<div class="flex items-center text-text-secondary text-sm">
<span>{{ forum.createBy }}</span>
<span class="mx-2"></span>
<span>{{ formatDate(forum.createTime) }}</span>
<span>{{ formatDate(forum.createTime || '') }}</span>
</div>
</div>
</div>
@ -618,7 +735,7 @@ onMounted(async() => {
</div>
</div>
<!-- 评论区 -->
<!-- Comments section -->
<div v-if="currentForumId === forum.id">
<div class="border-t border-background-light">
<div class="p-6">
@ -661,7 +778,7 @@ onMounted(async() => {
</div>
</div>
<!-- 评论表单 -->
<!-- Comment form -->
<div class="bg-background-dark rounded-lg p-4">
<textarea
v-model="newMessage.content"
@ -681,7 +798,7 @@ onMounted(async() => {
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ t('community.forum.submitting') }}
{{ t('community.forum.submit') }}
</span>
<span v-else>{{ t('community.forum.submit') }}</span>
</button>
@ -695,7 +812,7 @@ onMounted(async() => {
</div>
</section>
<!-- 3. 信息公示 Section -->
<!-- 3. Information Disclosure Section -->
<!-- <section class="py-16 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('com_mes_bg')})` }">
<div class="container mx-auto">
<h2 class="text-3xl font-bold text-text mb-4 text-center wow animate__animated animate__fadeInUp">
@ -708,7 +825,7 @@ onMounted(async() => {
<div v-else-if="messageList.length === 0" class="text-center py-10">
<Icon icon="carbon:no-content" class="mx-auto mb-4" width="48" height="48" />
<p class="text-text-secondary">暂无信息公示数据</p>
<p class="text-text-secondary">{{ t('community.announcements.no_data') }}</p>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto mt-8">
@ -736,7 +853,7 @@ onMounted(async() => {
</div>
</section> -->
<section class="py-16 px-6 md:px-12 lg:px-24 bg-background-light" :style="{ backgroundImage: `url(${getConfigImage('com_show_bg')})` }">
<!-- <section class="py-16 px-6 md:px-12 lg:px-24 bg-background-light" :style="{ backgroundImage: `url(${getConfigImage('com_show_bg')})` }">
<div class="container mx-auto">
<h2 class="text-3xl font-bold text-text mb-4 text-center wow animate__animated animate__fadeInUp">
{{ t('community.highlights.title') }}
@ -748,7 +865,7 @@ onMounted(async() => {
<div v-else-if="communityList.length === 0" class="text-center py-10">
<Icon icon="carbon:no-content" class="mx-auto mb-4" width="48" height="48" />
<p class="text-text-secondary">暂无社区活动数据</p>
<p class="text-text-secondary">{{ t('community.highlights.no_data') }}</p>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
@ -771,7 +888,66 @@ onMounted(async() => {
</div>
</div>
</div>
</section>
</section> -->
<!-- Video playback modal -->
<div v-if="showVideoModal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50" @click="closeVideoModal">
<div class="relative max-w-4xl w-full mx-4" @click.stop>
<button
@click="closeVideoModal"
class="absolute -top-12 right-0 text-white hover:text-gray-300 transition-colors"
>
<Icon icon="carbon:close" class="h-8 w-8" />
</button>
<div class="bg-black rounded-lg overflow-hidden">
<video
v-if="selectedCommunity?.video"
:src="selectedCommunity.video"
controls
autoplay
class="w-full h-auto max-h-[70vh]"
>
{{ t('community.video.unsupported') }}
</video>
</div>
</div>
</div>
<!-- Community details modal -->
<div v-if="showCommunityDetailModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" @click="closeCommunityDetailModal">
<div class="bg-background rounded-xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto" @click.stop>
<div class="p-6">
<div class="flex justify-between items-start mb-4">
<h3 class="text-2xl font-bold text-text">{{ selectedCommunity?.title }}</h3>
<button
@click="closeCommunityDetailModal"
class="text-text-secondary hover:text-text transition-colors"
>
<Icon icon="carbon:close" class="h-6 w-6" />
</button>
</div>
<div v-if="selectedCommunity?.image" class="mb-6">
<img
:src="selectedCommunity.image"
:alt="selectedCommunity.title"
class="w-full h-64 object-cover rounded-lg"
/>
</div>
<div class="prose prose-lg max-w-none text-text-secondary">
<div v-html="selectedCommunity?.content"></div>
</div>
<div v-if="selectedCommunity?.createTime" class="mt-6 pt-4 border-t border-background-light">
<div class="flex items-center text-text-secondary text-sm">
<Icon icon="carbon:time" class="mr-2" />
<span>{{ formatDate(selectedCommunity.createTime) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@ -792,7 +968,7 @@ onMounted(async() => {
overflow: hidden;
}
/* Swiper分页器样式 */
/* Swiper pagination styles */
:deep(.swiper-pagination-bullet) {
width: 12px;
height: 12px;
@ -814,4 +990,62 @@ onMounted(async() => {
bottom: 0;
margin-top: 20px;
}
</style>
/* Modal styles */
.fixed {
position: fixed;
}
.inset-0 {
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.z-50 {
z-index: 50;
}
/* Video modal animations */
.video-modal-enter-active,
.video-modal-leave-active {
transition: opacity 0.3s ease;
}
.video-modal-enter-from,
.video-modal-leave-to {
opacity: 0;
}
/* Detail modal animations */
.detail-modal-enter-active,
.detail-modal-leave-active {
transition: all 0.3s ease;
}
.detail-modal-enter-from,
.detail-modal-leave-to {
opacity: 0;
transform: scale(0.9);
}
/* Scrollbar styles */
.overflow-y-auto::-webkit-scrollbar {
width: 6px;
}
.overflow-y-auto::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
}
.overflow-y-auto::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.5);
}
</style>

+ 6
- 6
src/views/Contact.vue View File

@ -30,13 +30,13 @@ const submitForm = async () => {
information: formData.information
});
console.log(response);
console.log('res', response);
if (response && response.success) {
//
submitSuccess.value = true;
console.log('请求成功');
console.log(t('common.success.request'));
//
formData.name = '';
@ -50,13 +50,13 @@ const submitForm = async () => {
isSubmitting.value = false;
}, 2500);
} else {
console.error('提交失败:', response);
console.error(t('common.error.submitFailed'), response);
setTimeout(() => {
isSubmitting.value = false;
}, 500);
}
} catch (error) {
console.error('提交表单失败:', error);
console.error(t('common.error.submitFormFailed'), error);
setTimeout(() => {
isSubmitting.value = false;
}, 500);
@ -103,7 +103,7 @@ const submitForm = async () => {
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<p class="text-lg text-text">{{ t('contact.form.submitting') || '提交中...' }}</p>
<p class="text-lg text-text">{{ t('contact.form.submitting') }}</p>
</div>
<div v-else-if="submitSuccess" class="absolute inset-0 flex flex-col items-center justify-center">
@ -309,4 +309,4 @@ input:focus, textarea:focus {
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
}
</style>
</style>

+ 188
- 189
src/views/Ecosystem.vue View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { queryEcosystemListForEcosystem } from '@/api/modules/ecosystem';
const { t, locale } = useI18n();
@ -8,109 +9,176 @@ const { t, locale } = useI18n();
const selectedEcosystem = ref(1);
const totalEcosystems = 10; // 10
// -
const ecosystems = reactive([
{
id: 1,
image: '/LOGO.png',
title: 'MOS交易生态',
description: '构建高效、安全的数字资产交易环境,支持多种交易方式和金融工具。',
layout: 'left',
style: 'large',
//
const loading = ref(true);
//
const ecosystemStyles = [
{
overlayColor: 'from-indigo-900/80 to-transparent',
bgColor: 'bg-gradient-to-br from-black to-indigo-900/30'
},
{
id: 2,
image: '/LOGO.png',
title: 'MOS公链生态',
description: '提供高性能、低延迟的区块链基础设施,支持大规模商业应用的快速部署。',
layout: 'right',
style: 'medium',
{
overlayColor: 'from-emerald-900/80 to-transparent',
bgColor: 'bg-gradient-to-tl from-black to-emerald-900/30'
},
{
id: 3,
image: '/LOGO.png',
title: 'MOS跨链生态',
description: '打破区块链孤岛,实现不同链之间的无缝互操作,构建互联互通的价值网络。',
layout: 'center',
style: 'full',
{
overlayColor: 'from-purple-900/70 via-purple-900/40 to-transparent',
bgColor: 'bg-gradient-to-r from-black via-purple-900/20 to-black'
},
{
id: 4,
image: '/LOGO.png',
title: 'MOS支付生态',
description: '革新支付体验,提供快速、低成本、全球化的数字支付解决方案。',
layout: 'left',
style: 'medium',
{
overlayColor: 'from-blue-900/80 to-transparent',
bgColor: 'bg-gradient-to-br from-black to-blue-900/30'
},
{
id: 5,
image: '/LOGO.png',
title: 'MOS隐私生态',
description: '保护用户数据隐私的同时,确保交易合规性,平衡隐私与监管需求。',
layout: 'right',
style: 'large',
{
overlayColor: 'from-gray-900/80 to-transparent',
bgColor: 'bg-gradient-to-tl from-black to-gray-900/30'
},
{
id: 6,
image: '/LOGO.png',
title: 'MOS DeFi生态',
description: '重构金融服务,提供去中心化借贷、流动性挖矿、资产管理等创新金融工具。',
layout: 'center',
style: 'medium',
{
overlayColor: 'from-amber-900/70 via-amber-900/40 to-transparent',
bgColor: 'bg-gradient-to-r from-black via-amber-900/20 to-black'
},
{
id: 7,
image: '/LOGO.png',
title: 'MOS NFT生态',
description: '为数字艺术、收藏品和虚拟资产提供认证和交易平台,释放创作者价值。',
layout: 'left',
style: 'full',
{
overlayColor: 'from-rose-900/80 to-transparent',
bgColor: 'bg-gradient-to-br from-black to-rose-900/30'
},
{
id: 8,
image: '/LOGO.png',
title: 'MOS DAO生态',
description: '创新治理模式,实现社区自治,让每个参与者都能参与生态系统的决策过程。',
layout: 'right',
style: 'medium',
{
overlayColor: 'from-cyan-900/80 to-transparent',
bgColor: 'bg-gradient-to-tl from-black to-cyan-900/30'
},
{
id: 9,
image: '/LOGO.png',
title: 'MOS游戏生态',
description: '将区块链技术融入游戏产业,创造真正玩家拥有的游戏资产和全新游戏体验。',
layout: 'center',
style: 'large',
{
overlayColor: 'from-green-900/70 via-green-900/40 to-transparent',
bgColor: 'bg-gradient-to-r from-black via-green-900/20 to-black'
},
{
id: 10,
image: '/LOGO.png',
title: 'MOS社交生态',
description: '重新定义社交网络,让用户掌控自己的数据和社交关系,构建去中心化社交平台。',
layout: 'left',
style: 'medium',
{
overlayColor: 'from-orange-900/80 to-transparent',
bgColor: 'bg-gradient-to-br from-black to-orange-900/30'
}
]);
];
// - API
const ecosystems = reactive([]);
//
const fetchEcosystems = async () => {
loading.value = true;
try {
const result = await queryEcosystemListForEcosystem({
pageSize: 10,
pageNo: 1
});
if (result && result.length > 0) {
// API
const ecosystemsData = result.map((item, index) => {
return {
...item,
id: item.id || String(index + 1),
// 使API使
image: item.image || '/LOGO.png',
// 使
...ecosystemStyles[index % ecosystemStyles.length]
};
});
//
ecosystems.splice(0, ecosystems.length, ...ecosystemsData);
} else {
// API使
const defaultEcosystems = [
{
id: '1',
image: '/LOGO.png',
title: t('ecosystem.trading.title'),
description: t('ecosystem.trading.description'),
...ecosystemStyles[0]
},
{
id: '2',
image: '/LOGO.png',
title: t('ecosystem.blockchain.title'),
description: t('ecosystem.blockchain.description'),
...ecosystemStyles[1]
},
{
id: '3',
image: '/LOGO.png',
title: t('ecosystem.crosschain.title'),
description: t('ecosystem.crosschain.description'),
...ecosystemStyles[2]
},
{
id: '4',
image: '/LOGO.png',
title: t('ecosystem.payment.title'),
description: t('ecosystem.payment.description'),
...ecosystemStyles[3]
},
{
id: '5',
image: '/LOGO.png',
title: t('ecosystem.privacy.title'),
description: t('ecosystem.privacy.description'),
...ecosystemStyles[4]
},
{
id: '6',
image: '/LOGO.png',
title: t('ecosystem.defi.title'),
description: t('ecosystem.defi.description'),
...ecosystemStyles[5]
},
{
id: '7',
image: '/LOGO.png',
title: t('ecosystem.nft.title'),
description: t('ecosystem.nft.description'),
...ecosystemStyles[6]
},
{
id: '8',
image: '/LOGO.png',
title: t('ecosystem.dao.title'),
description: t('ecosystem.dao.description'),
...ecosystemStyles[7]
},
{
id: '9',
image: '/LOGO.png',
title: t('ecosystem.gaming.title'),
description: t('ecosystem.gaming.description'),
...ecosystemStyles[8]
},
{
id: '10',
image: '/LOGO.png',
title: t('ecosystem.social.title'),
description: t('ecosystem.social.description'),
...ecosystemStyles[9]
}
];
//
ecosystems.splice(0, ecosystems.length, ...defaultEcosystems);
}
} catch (error) {
console.error(t('common.error.loadEcosystemFailed'), error);
// 使
const defaultEcosystems = [
{
id: '1',
image: '/LOGO.png',
title: t('ecosystem.trading.title'),
description: t('ecosystem.trading.description'),
...ecosystemStyles[0]
},
// ...
];
ecosystems.splice(0, ecosystems.length, ...defaultEcosystems.slice(0, 1));
} finally {
loading.value = false;
}
};
// 使
const isZhLang = computed(() => locale.value === 'zh');
@ -119,8 +187,8 @@ const isZhLang = computed(() => locale.value === 'zh');
const projects = computed(() => [
{
id: 1,
name: isZhLang.value ? '闪兑池' : 'Flash Exchange Pool',
description: isZhLang.value ? '高效率的闪兑池服务,预计2025年6-12月上线' : 'Efficient flash exchange pool service, expected to launch between June-December 2025',
name: t('ecosystem.projects.flash_pool.name'),
description: t('ecosystem.projects.flash_pool.description'),
category: 'defi',
image: '/LOGO.png',
url: '#',
@ -129,8 +197,8 @@ const projects = computed(() => [
},
{
id: 2,
name: isZhLang.value ? 'Mosc C2C交易所' : 'Mosc C2C Exchange',
description: isZhLang.value ? '点对点加密货币交易平台,预计2026年1-6月上线' : 'Peer-to-peer cryptocurrency trading platform, expected to launch between January-June 2026',
name: t('ecosystem.projects.mosc_c2c.name'),
description: t('ecosystem.projects.mosc_c2c.description'),
category: 'defi',
image: '/LOGO.png',
url: '#',
@ -139,8 +207,8 @@ const projects = computed(() => [
},
{
id: 3,
name: isZhLang.value ? 'MoSe交易所' : 'MoSe Exchange',
description: isZhLang.value ? '全功能加密货币交易所,预计2026年7月上线' : 'Full-featured cryptocurrency exchange, expected to launch in July 2026',
name: t('ecosystem.projects.mose_exchange.name'),
description: t('ecosystem.projects.mose_exchange.description'),
category: 'defi',
image: '/LOGO.png',
url: '#',
@ -155,8 +223,8 @@ const featuredProjects = computed(() => {
});
//
const selectEcosystem = (id: number) => {
selectedEcosystem.value = id;
const selectEcosystem = (id: string | number) => {
selectedEcosystem.value = Number(id);
};
//
@ -171,7 +239,7 @@ const prevEcosystem = () => {
//
const currentEcosystem = computed(() => {
return ecosystems.find(eco => eco.id === selectedEcosystem.value) || ecosystems[0];
return ecosystems.find((eco: any) => Number(eco.id) === selectedEcosystem.value) || ecosystems[0];
});
//
@ -255,6 +323,9 @@ onMounted(() => {
//
document.documentElement.style.scrollSnapType = 'y proximity';
//
fetchEcosystems();
});
</script>
@ -265,7 +336,7 @@ onMounted(() => {
<div class="container mx-auto relative z-10">
<div class="max-w-3xl mx-auto text-center">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-text mb-6 wow animate__animated animate__fadeInDown animate__duration-fast">
MOS生态系统
{{ t('ecosystem.title') }}
</h1>
<p class="text-lg md:text-xl text-text-secondary mb-8 wow animate__animated animate__fadeIn animate__delay-xs animate__duration-fast">
{{ t('ecosystem.subtitle') }}
@ -292,11 +363,8 @@ onMounted(() => {
<div
v-for="(ecosystem, index) in ecosystems"
:key="ecosystem.id"
class="ecosystem-section relative w-full overflow-hidden"
:class="[
ecosystem.bgColor,
ecosystem.style === 'full' ? 'h-screen' : (ecosystem.style === 'large' ? 'h-[85vh]' : 'h-[70vh]')
]"
class="ecosystem-section relative w-full overflow-hidden h-[80vh]"
:class="ecosystem.bgColor"
>
<!-- 图片容器 - 视差效果 -->
<div class="absolute inset-0 w-full h-full overflow-hidden">
@ -311,108 +379,39 @@ onMounted(() => {
/>
<!-- 渐变叠加层 - 每个生态系统有不同的渐变 -->
<div
<!-- <div
class="absolute inset-0 bg-gradient-to-r"
:class="ecosystem.overlayColor"
></div>
></div> -->
</div>
<!-- 内容区域 - 根据布局调整位置 -->
<!-- 内容区域 - 统一zuo对齐 -->
<div
class="absolute inset-0 flex items-center"
:class="{
'justify-start': ecosystem.layout === 'left',
'justify-end': ecosystem.layout === 'right',
'justify-center': ecosystem.layout === 'center'
}"
class="absolute inset-0 flex items-center justify-start"
>
<div
class="max-w-xl p-8 md:p-16 backdrop-blur-sm bg-black/10 rounded-xl border border-white/10"
:class="{
'ml-0 md:ml-16': ecosystem.layout === 'left',
'mr-0 md:mr-16': ecosystem.layout === 'right',
'mx-auto text-center': ecosystem.layout === 'center'
}"
class="max-w-full p-8 md:p-16 rounded-xl mr-0 md:mr-16 text-left"
>
<!-- 编号 - 不同样式 -->
<div
class="mb-6"
:class="{'flex items-center': ecosystem.layout !== 'center'}"
>
<div
class="rounded-full backdrop-blur-sm flex items-center justify-center border border-white/30"
:class="{
'w-16 h-16 bg-white/10': ecosystem.style !== 'full',
'w-20 h-20 bg-white/20': ecosystem.style === 'full',
'mx-auto': ecosystem.layout === 'center'
}"
>
<span
class="font-bold text-white"
:class="{
'text-3xl': ecosystem.style !== 'full',
'text-4xl': ecosystem.style === 'full'
}"
>{{ index + 1 }}</span>
<!-- 编号 - 统一样式 -->
<!-- <div class="mb-6 flex items-center justify-end">
<div class="w-16 h-16 rounded-full backdrop-blur-sm flex items-center justify-center border border-white/30 bg-white/10">
<span class="font-bold text-white text-3xl">{{ index + 1 }}</span>
</div>
<div
v-if="ecosystem.layout !== 'center'"
class="ml-4 h-px bg-gradient-to-r from-white to-transparent flex-grow"
></div>
</div>
<div class="flex-grow h-px bg-gradient-to-r from-white to-transparent mr-4"></div>
</div> -->
<!-- 标题 - 不同大小和动画 -->
<h2
class="font-bold text-white mb-6 wow animate__animated"
:class="{
'text-4xl md:text-5xl': ecosystem.style === 'medium',
'text-5xl md:text-6xl': ecosystem.style === 'large',
'text-6xl md:text-7xl': ecosystem.style === 'full',
'animate__fadeInUp': ecosystem.layout === 'left' || ecosystem.layout === 'center',
'animate__fadeInRight': ecosystem.layout === 'right'
}"
>
{{ ecosystem.title }}
</h2>
<!-- 标题 - 统一样式 -->
<h2 class="font-bold text-white mb-6 wow animate__animated animate__fadeInRight text-4xl md:text-5xl" v-html="ecosystem.title" />
<!-- {{ ecosystem.title }} -->
<!-- </h2> -->
<!-- 描述 - 不同样式和动画 -->
<p
class="text-white/80 mb-8 wow animate__animated"
:class="{
'text-base': ecosystem.style === 'medium',
'text-lg': ecosystem.style === 'large',
'text-xl': ecosystem.style === 'full',
'animate__fadeInUp animate__delay-xs': ecosystem.layout === 'left' || ecosystem.layout === 'center',
'animate__fadeInRight animate__delay-xs': ecosystem.layout === 'right'
}"
>
{{ ecosystem.description || '探索MOS生态系统的无限可能,连接全球资源,构建去中心化未来。' }}
<!-- 描述 - 统一样式 -->
<p class="text-white/80 mb-8 wow animate__animated animate__fadeInRight animate__delay-xs text-base max-w-full" v-if="ecosystem.description" v-html="ecosystem.description">
</p>
<p class="text-white/80 mb-8 wow animate__animated animate__fadeInRight animate__delay-xs text-base" v-else>
{{ t('ecosystem.default_description') }}
</p>
<!-- 按钮 - 不同样式和位置 -->
<div
:class="{
'flex': ecosystem.layout !== 'center',
'flex justify-center': ecosystem.layout === 'center'
}"
>
<button
@click="openModal(ecosystem)"
class="px-8 py-4 backdrop-blur-sm text-white rounded-full border transition-all duration-300 flex items-center space-x-3 wow animate__animated"
:class="{
'bg-white/10 border-white/30 hover:bg-white/30': ecosystem.style !== 'full',
'bg-white/20 border-white/50 hover:bg-white/40': ecosystem.style === 'full',
'animate__fadeInUp animate__delay-sm': ecosystem.layout === 'left' || ecosystem.layout === 'center',
'animate__fadeInRight animate__delay-sm': ecosystem.layout === 'right'
}"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<span>查看详情</span>
</button>
</div>
</div>
</div>
@ -451,13 +450,13 @@ onMounted(() => {
class="absolute bottom-8 left-1/2 transform -translate-x-1/2 text-white/70 text-sm flex flex-col items-center cursor-pointer hover:text-white transition-colors duration-300"
@click="scrollToNext(index)"
>
<span class="mb-1">探索更多</span>
<!-- <span class="mb-1">探索更多</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 animate-bounce-soft" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</svg> -->
</div>
</div>
</div>
</div>
<!-- 全屏弹窗 - 优化 -->
<div
@ -488,22 +487,22 @@ onMounted(() => {
<div class="mt-8 flex flex-wrap gap-4">
<div class="bg-white/5 rounded-lg p-4 flex-1 min-w-[200px] border border-white/10">
<h4 class="text-white/70 text-sm mb-2">发布时间</h4>
<p class="text-white font-medium">2025年第四季度</p>
<h4 class="text-white/70 text-sm mb-2">{{ t('ecosystem.modal.release_time') }}</h4>
<p class="text-white font-medium">{{ t('ecosystem.modal.release_date') }}</p>
</div>
<div class="bg-white/5 rounded-lg p-4 flex-1 min-w-[200px] border border-white/10">
<h4 class="text-white/70 text-sm mb-2">技术栈</h4>
<p class="text-white font-medium">区块链智能合约Web3</p>
<h4 class="text-white/70 text-sm mb-2">{{ t('ecosystem.modal.tech_stack') }}</h4>
<p class="text-white font-medium">{{ t('ecosystem.modal.tech_stack_value') }}</p>
</div>
<div class="bg-white/5 rounded-lg p-4 flex-1 min-w-[200px] border border-white/10">
<h4 class="text-white/70 text-sm mb-2">应用场景</h4>
<p class="text-white font-medium">金融科技数字资产去中心化应用</p>
<h4 class="text-white/70 text-sm mb-2">{{ t('ecosystem.modal.use_cases') }}</h4>
<p class="text-white font-medium">{{ t('ecosystem.modal.use_cases_value') }}</p>
</div>
</div>
<div class="mt-8 flex justify-end">
<button class="px-6 py-3 bg-primary/80 hover:bg-primary text-white rounded-full transition-all duration-300">
了解更多
{{ t('common.buttons.learn_more') }}
</button>
</div>
</div>
@ -608,4 +607,4 @@ onMounted(() => {
.animate-float-reverse {
animation: float-reverse 8s ease-in-out infinite;
}
</style>
</style>

+ 37
- 10
src/views/FAQ.vue View File

@ -51,9 +51,36 @@ const loadQuestions = async () => {
pageNo: 1,
title: searchQuery.value || undefined // title
});
questions.value = data;
// Only keep non-text data from API, use i18n for text
questions.value = data.map((item, index) => ({
id: item.id || `faq-${index + 1}`,
question: t(`faq.questions.item${index + 1}.question`),
answer: t(`faq.questions.item${index + 1}.answer`),
createTime: item.createTime
}));
} catch (error) {
console.error('加载常见问题数据失败:', error);
console.error(t('common.error.loadFailed'), error);
// Use default i18n data on error
questions.value = [
{
id: 'faq-1',
question: t('faq.questions.item1.question'),
answer: t('faq.questions.item1.answer'),
createTime: new Date().toISOString()
},
{
id: 'faq-2',
question: t('faq.questions.item2.question'),
answer: t('faq.questions.item2.answer'),
createTime: new Date().toISOString()
},
{
id: 'faq-3',
question: t('faq.questions.item3.question'),
answer: t('faq.questions.item3.answer'),
createTime: new Date().toISOString()
}
];
} finally {
loading.value = false;
}
@ -103,9 +130,9 @@ onMounted(() => {
typed = new window.Typed(typedElement.value, {
strings: [
t('faq.hero.subtitle'),
'有任何疑问?我们随时为您解答',
'探索 MOSE 的常见问题',
'快速找到您需要的答案'
t('faq.hero.subtitle1'),
t('faq.hero.subtitle2'),
t('faq.hero.subtitle3')
],
typeSpeed: 50,
backSpeed: 30,
@ -161,7 +188,7 @@ onMounted(() => {
</div>
</div>
<!-- 加载中状态 -->
<!-- Loading state -->
<div v-if="loading" class="flex justify-center items-center py-16 max-w-3xl mx-auto">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
</div>
@ -208,7 +235,7 @@ onMounted(() => {
<div class="container mx-auto">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-2xl font-bold text-text mb-4">
{{ t('faq.more.title') || '还有更多问题?' }}
{{ t('faq.more.title') }}
</h2>
<div class="flex flex-col sm:flex-row justify-center gap-4 mt-8">
<router-link
@ -216,14 +243,14 @@ onMounted(() => {
class="px-8 py-3 bg-primary text-text rounded-lg hover:bg-primary-dark transition-colors duration-300 shadow-button btn-hover-glow flex items-center justify-center gap-2"
>
<Icon icon="carbon:email" width="20" height="20" />
{{ t('faq.more.contact') || '联系我们' }}
{{ t('faq.more.contact') }}
</router-link>
<router-link
to="/community"
class="px-8 py-3 bg-transparent border border-primary-light text-primary-light rounded-lg hover:bg-primary-light hover:bg-opacity-10 transition-colors duration-300 btn-hover-shadow flex items-center justify-center gap-2"
>
<Icon icon="carbon:group" width="20" height="20" />
{{ t('faq.more.community') || '加入社区' }}
{{ t('faq.more.community') }}
</router-link>
</div>
</div>
@ -275,4 +302,4 @@ button {
.btn-hover-shadow:hover {
box-shadow: 0 4px 12px rgba(var(--primary-light-rgb), 0.15);
}
</style>
</style>

+ 3
- 6
src/views/Home.vue View File

@ -85,7 +85,7 @@ onMounted(() => {
<BannerModule />
<!-- 核心价值主张模块 -->
<CoreValuesModule />
<!-- <CoreValuesModule /> -->
<!-- 项目简介模块 -->
<ProjectIntroModule />
@ -93,14 +93,11 @@ onMounted(() => {
<!-- 最新动态模块 (可点击) -->
<!-- <NewsModule /> -->
<!-- 动态列表模块 -->
<!-- 最新动态模块数据使用媒体报道 -->
<PostsModule />
<!-- 合作伙伴LOGO墙模块 -->
<PartnersModule />
<!-- 媒体报道模块 -->
<MediaModule />
</div>
</template>
@ -128,4 +125,4 @@ onMounted(() => {
transform: translateY(-3px);
transition: transform 0.3s ease;
}
</style>
</style>

+ 6
- 4
src/views/NotFound.vue View File

@ -1,7 +1,9 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
const router = useRouter();
const { t } = useI18n();
const goHome = () => {
router.push('/');
@ -12,16 +14,16 @@ const goHome = () => {
<div class="bg-background min-h-screen flex items-center justify-center px-6 py-24">
<div class="text-center">
<h1 class="text-6xl md:text-8xl font-bold text-primary-light mb-4">404</h1>
<h2 class="text-2xl md:text-3xl font-bold text-text mb-6">页面未找到</h2>
<h2 class="text-2xl md:text-3xl font-bold text-text mb-6">{{ t('not_found.title') }}</h2>
<p class="text-text-secondary mb-8 max-w-md mx-auto">
您访问的页面不存在或已被移除请检查URL或返回首页
{{ t('not_found.description') }}
</p>
<button
@click="goHome"
class="px-8 py-3 bg-primary text-text rounded-lg hover:bg-primary-dark transition-colors duration-300 shadow-button"
>
返回首页
{{ t('not_found.go_home') }}
</button>
</div>
</div>
</template>
</template>

+ 429
- 346
src/views/Technology.vue
File diff suppressed because it is too large
View File


+ 1
- 1
tsconfig.build.tsbuildinfo
File diff suppressed because it is too large
View File


+ 2840
- 0
国际化需求.txt
File diff suppressed because it is too large
View File


Loading…
Cancel
Save