国外MOSE官网
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

816 lines
32 KiB

1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
  1. <script setup lang="ts">
  2. import { useI18n } from 'vue-i18n';
  3. import { ref, onMounted, computed } from 'vue';
  4. import { Icon } from '@iconify/vue';
  5. import { useConfig } from '@/utils/config';
  6. import { Swiper, SwiperSlide } from 'swiper/vue';
  7. import { Navigation, Pagination } from 'swiper/modules';
  8. import 'swiper/css';
  9. import 'swiper/css/navigation';
  10. import 'swiper/css/pagination';
  11. import {
  12. queryOfficialMediaList,
  13. queryForumList,
  14. queryCommentsList,
  15. queryMessageList,
  16. queryCommunityList,
  17. addComments,
  18. type OfficialMediaItem,
  19. type ForumItem,
  20. type CommentItem,
  21. type MessageItem,
  22. type CommunityItem
  23. } from '@/api/modules';
  24. const { getConfigImage } = useConfig();
  25. const { t } = useI18n();
  26. // 社交媒体账号数据
  27. const socialMediaAccounts = ref<OfficialMediaItem[]>([]);
  28. const socialMediaLoading = ref(true);
  29. // 论坛信息数据
  30. const forumList = ref<ForumItem[]>([]);
  31. const forumLoading = ref(true);
  32. // 评论信息数据
  33. const commentsMap = ref<Record<string, CommentItem[]>>({});
  34. const commentsLoading = ref(true);
  35. // 信息公示数据
  36. const messageList = ref<MessageItem[]>([]);
  37. const messageLoading = ref(true);
  38. // 社区活动数据
  39. const communityList = ref<CommunityItem[]>([]);
  40. const communityLoading = ref(true);
  41. // 新留言
  42. const newMessage = ref({
  43. forumId: null as string | null,
  44. content: '',
  45. createBy: ''
  46. });
  47. // 是否正在提交
  48. const isSubmitting = ref(false);
  49. // 当前查看的帖子ID
  50. const currentForumId = ref<string | null>(null);
  51. // Swiper实例
  52. const swiperInstance = ref(null);
  53. // Swiper配置
  54. const swiperOptions = {
  55. slidesPerView: 3,
  56. spaceBetween: 40,
  57. navigation: false,
  58. pagination: false,
  59. modules: [Navigation, Pagination],
  60. breakpoints: {
  61. 320: {
  62. slidesPerView: 1,
  63. spaceBetween: 20
  64. },
  65. 768: {
  66. slidesPerView: 2,
  67. spaceBetween: 30
  68. },
  69. 1024: {
  70. slidesPerView: 3,
  71. spaceBetween: 40
  72. }
  73. }
  74. };
  75. // 弹窗相关数据
  76. const showDetailModal = ref(false);
  77. const selectedMessage = ref<MessageItem | null>(null);
  78. // 显示消息详情弹窗
  79. const showMessageDetail = (message: MessageItem) => {
  80. selectedMessage.value = message;
  81. showDetailModal.value = true;
  82. };
  83. // 关闭消息详情弹窗
  84. const closeMessageDetail = () => {
  85. showDetailModal.value = false;
  86. selectedMessage.value = null;
  87. };
  88. // 跳转到公告详情
  89. const goToMessage = (id: string) => {
  90. console.log('查看公告详情:', id);
  91. // 跳转到公告详情页面实现
  92. };
  93. // 格式化日期
  94. const formatDate = (dateString: string) => {
  95. if (!dateString) return '';
  96. const date = new Date(dateString);
  97. return new Intl.DateTimeFormat('zh-CN', {
  98. year: 'numeric',
  99. month: 'long',
  100. day: 'numeric'
  101. }).format(date);
  102. };
  103. // 加载社交媒体数据
  104. const loadSocialMedia = async () => {
  105. try {
  106. socialMediaLoading.value = true;
  107. const data = await queryOfficialMediaList({
  108. pageSize: 10,
  109. pageNo: 1
  110. });
  111. socialMediaAccounts.value = data;
  112. } catch (error) {
  113. console.error('加载社交媒体数据失败:', error);
  114. } finally {
  115. socialMediaLoading.value = false;
  116. }
  117. };
  118. // 加载论坛数据
  119. const loadForum = async () => {
  120. try {
  121. forumLoading.value = true;
  122. const data = await queryForumList({
  123. pageSize: 10,
  124. pageNo: 1
  125. });
  126. forumList.value = data;
  127. // 为每个论坛加载对应的评论
  128. await Promise.all(data.map(forum => loadComments(forum.id)));
  129. } catch (error) {
  130. console.error('加载论坛数据失败:', error);
  131. } finally {
  132. forumLoading.value = false;
  133. }
  134. };
  135. // 加载评论数据
  136. const loadComments = async (forumId: string) => {
  137. try {
  138. commentsLoading.value = true;
  139. const data = await queryCommentsList({
  140. forumId
  141. });
  142. commentsMap.value[forumId] = data;
  143. } catch (error) {
  144. console.error(`加载论坛${forumId}的评论数据失败:`, error);
  145. } finally {
  146. commentsLoading.value = false;
  147. }
  148. };
  149. // 加载信息公示数据
  150. const loadMessages = async () => {
  151. try {
  152. messageLoading.value = true;
  153. const data = await queryMessageList({
  154. pageSize: 10,
  155. pageNo: 1
  156. });
  157. messageList.value = data;
  158. } catch (error) {
  159. console.error('加载信息公示数据失败:', error);
  160. } finally {
  161. messageLoading.value = false;
  162. }
  163. };
  164. // 加载社区数据
  165. const loadCommunity = async () => {
  166. try {
  167. communityLoading.value = true;
  168. const data = await queryCommunityList({
  169. pageSize: 6,
  170. pageNo: 1
  171. });
  172. communityList.value = data;
  173. } catch (error) {
  174. console.error('加载社区数据失败:', error);
  175. } finally {
  176. communityLoading.value = false;
  177. }
  178. };
  179. // 提交留言
  180. const submitComment = async (forumId: string) => {
  181. if (newMessage.value.content) {
  182. isSubmitting.value = true;
  183. try {
  184. await addComments({
  185. forumId: forumId,
  186. content: newMessage.value.content
  187. });
  188. // 重新加载评论
  189. await loadComments(forumId);
  190. // 重置表单
  191. newMessage.value.createBy = '';
  192. newMessage.value.content = '';
  193. newMessage.value.forumId = null;
  194. } catch (error) {
  195. console.error('提交评论失败:', error);
  196. } finally {
  197. isSubmitting.value = false;
  198. }
  199. }
  200. };
  201. // 准备留言
  202. const prepareComment = (forumId: string) => {
  203. // 记录当前滚动位置
  204. const scrollPosition = window.scrollY;
  205. // 设置当前查看的帖子ID
  206. currentForumId.value = forumId;
  207. newMessage.value.forumId = forumId;
  208. // 使用setTimeout确保DOM更新后再恢复滚动位置
  209. setTimeout(() => {
  210. window.scrollTo({
  211. top: scrollPosition,
  212. behavior: 'auto'
  213. });
  214. }, 0);
  215. };
  216. // 关闭评论
  217. const closeComment = () => {
  218. // 记录当前滚动位置
  219. const scrollPosition = window.scrollY;
  220. // 关闭评论区
  221. currentForumId.value = null;
  222. // 使用setTimeout确保DOM更新后再恢复滚动位置
  223. setTimeout(() => {
  224. window.scrollTo({
  225. top: scrollPosition,
  226. behavior: 'auto'
  227. });
  228. }, 0);
  229. };
  230. // 获取特定论坛的评论
  231. const getComments = (forumId: string): CommentItem[] => {
  232. return commentsMap.value[forumId] || [];
  233. };
  234. // 计算帖子发布时间
  235. const getTimeAgo = (timestamp: string) => {
  236. const now = new Date();
  237. const postTime = new Date(timestamp);
  238. const diffTime = Math.abs(now.getTime() - postTime.getTime());
  239. const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
  240. if (diffDays < 1) {
  241. return t('community.time.today');
  242. } else if (diffDays === 1) {
  243. return t('community.time.yesterday');
  244. } else if (diffDays < 7) {
  245. return t('community.time.days_ago', { days: diffDays });
  246. } else {
  247. return new Date(timestamp).toLocaleDateString();
  248. }
  249. };
  250. // 根据平台名称获取图标
  251. const getSocialIcon = (title: string) => {
  252. const lowerTitle = title.toLowerCase();
  253. if (lowerTitle.includes('twitter') || lowerTitle.includes('x')) {
  254. return 'mdi:twitter';
  255. } else if (lowerTitle.includes('telegram')) {
  256. return 'mdi:telegram';
  257. } else if (lowerTitle.includes('discord')) {
  258. return 'mdi:discord';
  259. } else if (lowerTitle.includes('medium')) {
  260. return 'mdi:medium';
  261. } else if (lowerTitle.includes('github')) {
  262. return 'mdi:github';
  263. } else if (lowerTitle.includes('reddit')) {
  264. return 'mdi:reddit';
  265. } else if (lowerTitle.includes('facebook')) {
  266. return 'mdi:facebook';
  267. } else if (lowerTitle.includes('instagram')) {
  268. return 'mdi:instagram';
  269. } else {
  270. return 'mdi:web';
  271. }
  272. };
  273. onMounted(async() => {
  274. await Promise.all([
  275. loadSocialMedia(),
  276. loadForum(),
  277. loadMessages(),
  278. loadCommunity()
  279. ]);
  280. });
  281. </script>
  282. <template>
  283. <div class="bg-background min-h-screen">
  284. <!-- Hero Section -->
  285. <section class="relative py-24 px-6 md:px-12 lg:px-24 bg-background-dark overflow-hidden">
  286. <div class="container mx-auto relative z-10">
  287. <div class="max-w-3xl mx-auto text-center">
  288. <h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-text mb-6 wow animate__animated animate__fadeInDown animate__duration-fast">
  289. {{ t('community.hero.title') }}
  290. </h1>
  291. <p class="text-lg md:text-xl text-text-secondary mb-8 wow animate__animated animate__fadeIn animate__delay-xs animate__duration-fast">
  292. {{ t('community.hero.subtitle') }}
  293. </p>
  294. </div>
  295. </div>
  296. <!-- Background Decoration -->
  297. <div class="absolute top-0 left-0 w-full h-full overflow-hidden opacity-10">
  298. <div class="absolute -top-24 -left-24 w-64 h-64 rounded-full bg-primary-light blur-3xl wow animate__animated animate__pulse animate__infinite"></div>
  299. <div class="absolute top-1/2 right-0 w-80 h-80 rounded-full bg-secondary blur-3xl wow animate__animated animate__pulse animate__infinite animate__delay-sm"></div>
  300. <div class="absolute -bottom-24 left-1/3 w-72 h-72 rounded-full bg-accent blur-3xl wow animate__animated animate__pulse animate__infinite animate__delay-md"></div>
  301. </div>
  302. </section>
  303. <!-- 官方公告 Section -->
  304. <section class="py-16 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('com_mes_bg')})` }">
  305. <div class="container mx-auto">
  306. <h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
  307. 官方公告
  308. </h2>
  309. <div v-if="messageLoading" class="flex justify-center py-10">
  310. <div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary"></div>
  311. </div>
  312. <div v-else class="space-y-6">
  313. <div
  314. v-for="message in messageList"
  315. :key="message.id"
  316. class="flex bg-background rounded-xl overflow-hidden shadow-card hover:shadow-lg transition-all duration-300 wow animate__animated animate__fadeInUp"
  317. >
  318. <!-- 公告图片 -->
  319. <div class="w-1/4">
  320. <img :src="message.image || '/LOGO.png'" :alt="message.title" class="w-full h-full object-cover" />
  321. </div>
  322. <!-- 公告内容 -->
  323. <div class="w-3/4 p-6">
  324. <h3
  325. 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"
  326. @click="showMessageDetail(message)"
  327. >
  328. {{ message.title }}
  329. </h3>
  330. <p class="text-text-secondary text-base mb-4 line-clamp-3">{{ message.description }}</p>
  331. <div class="flex items-center text-text-secondary text-sm">
  332. <Icon icon="carbon:time" class="mr-2" />
  333. <span>{{ formatDate(message.createTime) }}</span>
  334. </div>
  335. </div>
  336. </div>
  337. </div>
  338. <!-- 消息详情弹窗 -->
  339. <div
  340. v-if="showDetailModal"
  341. class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
  342. @click="closeMessageDetail"
  343. >
  344. <div
  345. class="bg-background rounded-xl max-w-2xl w-full max-h-[80vh] overflow-y-auto"
  346. @click.stop
  347. >
  348. <!-- 弹窗头部 -->
  349. <div class="p-6 border-b border-border">
  350. <div class="flex justify-between items-start">
  351. <h3 class="text-2xl font-bold text-text">{{ selectedMessage?.title }}</h3>
  352. <button
  353. @click="closeMessageDetail"
  354. class="text-text-secondary hover:text-text transition-colors duration-300"
  355. >
  356. <Icon icon="carbon:close" class="h-6 w-6" />
  357. </button>
  358. </div>
  359. <div class="flex items-center text-text-secondary text-sm mt-2">
  360. <Icon icon="carbon:time" class="mr-2" />
  361. <span>{{ formatDate(selectedMessage?.createTime) }}</span>
  362. </div>
  363. </div>
  364. <!-- 弹窗内容 -->
  365. <div class="p-6">
  366. <div v-if="selectedMessage?.image" class="mb-6">
  367. <img
  368. :src="selectedMessage.image"
  369. :alt="selectedMessage.title"
  370. class="w-full h-64 object-cover rounded-lg"
  371. />
  372. </div>
  373. <div class="prose prose-lg max-w-none">
  374. <div v-html="selectedMessage?.description"></div>
  375. </div>
  376. </div>
  377. </div>
  378. </div>
  379. </div>
  380. </section>
  381. <!-- 社区风采 Section (原社区亮点) -->
  382. <section class="py-16 px-6 md:px-12 lg:px-24 bg-background-light" :style="{ backgroundImage: `url(${getConfigImage('com_show_bg')})` }">
  383. <div class="container mx-auto">
  384. <h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
  385. 社区风采
  386. </h2>
  387. <div v-if="communityLoading" class="flex justify-center py-10">
  388. <div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary"></div>
  389. </div>
  390. <div v-else class="relative">
  391. <div class="relative">
  392. <Swiper
  393. :slides-per-view="swiperOptions.slidesPerView"
  394. :space-between="swiperOptions.spaceBetween"
  395. :navigation="swiperOptions.navigation"
  396. :pagination="false"
  397. :modules="swiperOptions.modules"
  398. :breakpoints="swiperOptions.breakpoints"
  399. class="community-swiper"
  400. @swiper="swiperInstance = $event"
  401. >
  402. <SwiperSlide v-for="community in communityList" :key="community.id">
  403. <div class="bg-background rounded-2xl overflow-hidden shadow-xl hover:shadow-2xl transition-all duration-500 h-full transform hover:scale-105">
  404. <div class="flex flex-col h-full">
  405. <div class="relative overflow-hidden">
  406. <img
  407. :src="community.image || '/LOGO.png'"
  408. :alt="community.title"
  409. class="w-full h-56 object-cover transition-transform duration-700 hover:scale-110"
  410. />
  411. <div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
  412. </div>
  413. <div class="p-8 flex-1 flex flex-col">
  414. <h3 class="text-2xl font-bold text-text mb-4">{{ community.title }}</h3>
  415. <p class="text-text-secondary mb-6 flex-1 leading-relaxed">{{ community.description }}</p>
  416. </div>
  417. </div>
  418. </div>
  419. </SwiperSlide>
  420. </Swiper>
  421. <!-- 外部自定义导航按钮 -->
  422. <button
  423. @click="swiperInstance?.slidePrev()"
  424. 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"
  425. >
  426. <Icon icon="carbon:chevron-left" class="h-6 w-6 text-primary group-hover:scale-110 transition-transform duration-300" />
  427. </button>
  428. <button
  429. @click="swiperInstance?.slideNext()"
  430. class="absolute right-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"
  431. >
  432. <Icon icon="carbon:chevron-right" class="h-6 w-6 text-primary group-hover:scale-110 transition-transform duration-300" />
  433. </button>
  434. </div>
  435. <!-- 外部自定义分页器 -->
  436. <div class="flex justify-center mt-8 gap-4">
  437. <button
  438. v-for="(community, index) in communityList"
  439. :key="index"
  440. @click="swiperInstance?.slideTo(index)"
  441. class="w-5 h-5 rounded-full transition-all duration-300 border-2 cursor-pointer"
  442. :class="swiperInstance?.activeIndex === index
  443. ? 'bg-primary border-primary scale-125 shadow-lg'
  444. : 'bg-white/50 border-white/70 hover:bg-white/80'"
  445. ></button>
  446. </div>
  447. </div>
  448. </div>
  449. </section>
  450. <!-- 社交媒体账号 Section -->
  451. <section class="py-16 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('com_media_bg')})` }">
  452. <div class="container mx-auto">
  453. <h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
  454. {{ t('community.social_media.title') }}
  455. </h2>
  456. <div v-if="socialMediaLoading" class="flex justify-center py-10">
  457. <div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary"></div>
  458. </div>
  459. <div v-else-if="socialMediaAccounts.length === 0" class="text-center py-10 text-text-secondary">
  460. {{ t('community.social_media.no_accounts') }}
  461. </div>
  462. <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
  463. <div
  464. v-for="account in socialMediaAccounts"
  465. :key="account.id"
  466. class="bg-background rounded-xl p-6 shadow-card hover:shadow-lg transition-all duration-300 wow animate__animated animate__fadeInUp"
  467. >
  468. <div class="flex items-center mb-4">
  469. <div class="w-12 h-12 rounded-full bg-primary bg-opacity-10 flex items-center justify-center mr-4">
  470. <Icon :icon="getSocialIcon(account.title)" class="h-6 w-6 text-primary" />
  471. </div>
  472. <div>
  473. <h3 class="text-lg font-bold text-text">{{ account.title }}</h3>
  474. <p class="text-text-secondary text-sm">{{ account.username }}</p>
  475. </div>
  476. </div>
  477. <p class="text-text-secondary mb-4" v-html="account.description"></p>
  478. <a
  479. :href="account.url"
  480. target="_blank"
  481. rel="noopener noreferrer"
  482. class="inline-flex items-center text-primary-light hover:text-primary-dark transition-colors"
  483. >
  484. <span>{{ t('community.social_media.follow_us') }}</span>
  485. <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  486. <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" />
  487. </svg>
  488. </a>
  489. </div>
  490. </div>
  491. </div>
  492. </section>
  493. <!-- 社区论坛 Section -->
  494. <section class="py-16 px-6 md:px-12 lg:px-24 bg-background-light" :style="{ backgroundImage: `url(${getConfigImage('com_forum_bg')})` }" >
  495. <div class="container mx-auto">
  496. <h2 class="text-2xl md:text-3xl font-bold text-text mb-8 text-center wow animate__animated animate__fadeInUp animate__duration-fast">
  497. {{ t('community.forum.title') }}
  498. </h2>
  499. <div v-if="forumLoading" class="flex justify-center py-10">
  500. <div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary"></div>
  501. </div>
  502. <div v-else-if="forumList.length === 0" class="text-center py-10 text-text-secondary">
  503. {{ t('community.forum.no_topics') }}
  504. </div>
  505. <div v-else class="space-y-6">
  506. <div
  507. v-for="forum in forumList"
  508. :key="forum.id"
  509. class="bg-background rounded-xl shadow-card overflow-hidden wow animate__animated animate__fadeInUp"
  510. >
  511. <!-- 帖子内容 -->
  512. <div class="p-6">
  513. <div class="flex items-center mb-4">
  514. <div class="w-10 h-10 rounded-full bg-primary bg-opacity-10 flex items-center justify-center mr-3">
  515. <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  516. <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" />
  517. </svg>
  518. </div>
  519. <div>
  520. <h3 class="text-lg font-bold text-text">{{ forum.title }}</h3>
  521. <div class="flex items-center text-text-secondary text-sm">
  522. <span>{{ forum.createBy }}</span>
  523. <span class="mx-2"></span>
  524. <span>{{ formatDate(forum.createTime) }}</span>
  525. </div>
  526. </div>
  527. </div>
  528. <div class="mb-4">
  529. <p class="text-text-secondary" v-html="forum.content"></p>
  530. </div>
  531. <div class="flex justify-between items-center">
  532. <div class="flex items-center space-x-4">
  533. <button class="flex items-center text-text-secondary hover:text-primary transition-colors">
  534. <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  535. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5" />
  536. </svg>
  537. <span>{{ forum.likeCount || 0 }}</span>
  538. </button>
  539. <button class="flex items-center text-text-secondary hover:text-primary transition-colors" @click="prepareComment(forum.id)">
  540. <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  541. <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" />
  542. </svg>
  543. <span>{{ getComments(forum.id).length }}</span>
  544. </button>
  545. </div>
  546. <button class="flex items-center text-primary-light hover:text-primary-dark transition-colors" @click="prepareComment(forum.id)">
  547. <span>{{ t('community.forum.add_comment') }}</span>
  548. <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  549. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
  550. </svg>
  551. </button>
  552. </div>
  553. </div>
  554. <!-- 评论区 -->
  555. <div v-if="currentForumId === forum.id">
  556. <div class="border-t border-background-light">
  557. <div class="p-6">
  558. <h4 class="text-lg font-bold text-text mb-4 flex items-center justify-between">
  559. <span>{{ t('community.forum.view_comments') }}</span>
  560. <button class="text-text-secondary hover:text-primary transition-colors" @click="closeComment">
  561. <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  562. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
  563. </svg>
  564. </button>
  565. </h4>
  566. <div v-if="commentsLoading" class="flex justify-center py-4">
  567. <div class="animate-spin rounded-full h-6 w-6 border-t-2 border-b-2 border-primary"></div>
  568. </div>
  569. <div v-else-if="getComments(forum.id).length === 0" class="text-center py-4 text-text-secondary">
  570. {{ t('community.forum.no_comments') }}
  571. </div>
  572. <div v-else class="space-y-4 mb-6">
  573. <div
  574. v-for="comment in getComments(forum.id)"
  575. :key="comment.id"
  576. class="bg-background-dark rounded-lg p-4"
  577. >
  578. <div class="flex items-center mb-2">
  579. <div class="w-8 h-8 rounded-full bg-primary bg-opacity-10 flex items-center justify-center mr-2">
  580. <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  581. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
  582. </svg>
  583. </div>
  584. <div>
  585. <h5 class="text-sm font-medium text-text">{{ comment.createBy || t('community.forum.username') }}</h5>
  586. <p class="text-xs text-text-secondary">{{ formatDate(comment.createTime) }}</p>
  587. </div>
  588. </div>
  589. <p class="text-text-secondary" v-html="comment.content"></p>
  590. </div>
  591. </div>
  592. <!-- 评论表单 -->
  593. <div class="bg-background-dark rounded-lg p-4">
  594. <textarea
  595. v-model="newMessage.content"
  596. :placeholder="t('community.forum.comment')"
  597. class="w-full bg-background border border-background-light rounded-lg p-3 text-text-secondary resize-none focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
  598. rows="3"
  599. ></textarea>
  600. <div class="flex justify-end mt-3">
  601. <button
  602. @click="submitComment(forum.id)"
  603. :disabled="!newMessage.content || isSubmitting"
  604. class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
  605. >
  606. <span v-if="isSubmitting">
  607. <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white inline-block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
  608. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
  609. <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>
  610. </svg>
  611. {{ t('community.forum.submitting') }}
  612. </span>
  613. <span v-else>{{ t('community.forum.submit') }}</span>
  614. </button>
  615. </div>
  616. </div>
  617. </div>
  618. </div>
  619. </div>
  620. </div>
  621. </div>
  622. </div>
  623. </section>
  624. <!-- 3. 信息公示 Section -->
  625. <!-- <section class="py-16 px-6 md:px-12 lg:px-24" :style="{ backgroundImage: `url(${getConfigImage('com_mes_bg')})` }">
  626. <div class="container mx-auto">
  627. <h2 class="text-3xl font-bold text-text mb-4 text-center wow animate__animated animate__fadeInUp">
  628. {{ t('community.announcements.title') }}
  629. </h2>
  630. <div v-if="messageLoading" class="flex justify-center items-center py-12">
  631. <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
  632. </div>
  633. <div v-else-if="messageList.length === 0" class="text-center py-10">
  634. <Icon icon="carbon:no-content" class="mx-auto mb-4" width="48" height="48" />
  635. <p class="text-text-secondary">暂无信息公示数据</p>
  636. </div>
  637. <div v-else class="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto mt-8">
  638. <div
  639. v-for="message in messageList"
  640. :key="message.id"
  641. class="bg-background-light rounded-xl p-6 shadow-card hover:shadow-lg transition-all duration-300"
  642. >
  643. <div class="flex items-start gap-4">
  644. <div v-if="message.image" class="w-16 h-16 rounded-lg overflow-hidden flex-shrink-0">
  645. <img :src="message.image" :alt="message.title" class="w-full h-full object-cover" />
  646. </div>
  647. <div v-else class="w-16 h-16 rounded-lg bg-primary bg-opacity-10 flex items-center justify-center flex-shrink-0">
  648. <Icon icon="carbon:notification" width="32" height="32" class="text-primary" />
  649. </div>
  650. <div class="flex-1">
  651. <h3 class="text-lg font-bold text-text mb-2">{{ message.title }}</h3>
  652. <p class="text-sm text-text-secondary mb-2" v-html="message.content"></p>
  653. <div class="text-xs text-text-secondary">{{ formatDate(message.createTime || '') }}</div>
  654. </div>
  655. </div>
  656. </div>
  657. </div>
  658. </div>
  659. </section> -->
  660. <section class="py-16 px-6 md:px-12 lg:px-24 bg-background-light" :style="{ backgroundImage: `url(${getConfigImage('com_show_bg')})` }">
  661. <div class="container mx-auto">
  662. <h2 class="text-3xl font-bold text-text mb-4 text-center wow animate__animated animate__fadeInUp">
  663. {{ t('community.highlights.title') }}
  664. </h2>
  665. <div v-if="communityLoading" class="flex justify-center items-center py-12">
  666. <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
  667. </div>
  668. <div v-else-if="communityList.length === 0" class="text-center py-10">
  669. <Icon icon="carbon:no-content" class="mx-auto mb-4" width="48" height="48" />
  670. <p class="text-text-secondary">暂无社区活动数据</p>
  671. </div>
  672. <div v-else class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
  673. <div
  674. v-for="community in communityList"
  675. :key="community.id"
  676. class="bg-background rounded-xl overflow-hidden shadow-card hover:shadow-lg transition-all duration-300"
  677. >
  678. <div v-if="community.image" class="h-48 overflow-hidden">
  679. <img :src="community.image" :alt="community.title" class="w-full h-full object-cover hover:scale-105 transition-transform duration-300">
  680. </div>
  681. <div v-else class="h-48 bg-primary bg-opacity-10 flex items-center justify-center">
  682. <Icon icon="carbon:events" width="64" height="64" class="text-primary opacity-50" />
  683. </div>
  684. <div class="p-6">
  685. <h4 class="text-lg font-bold text-text mb-2">{{ community.title }}</h4>
  686. <p class="text-text-secondary text-sm mb-2" v-html="community.content"></p>
  687. <p class="text-text-secondary text-xs">{{ formatDate(community.createTime || '') }}</p>
  688. </div>
  689. </div>
  690. </div>
  691. </div>
  692. </section>
  693. </div>
  694. </template>
  695. <style scoped>
  696. .shadow-card {
  697. box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
  698. transition: transform 0.3s ease, box-shadow 0.3s ease;
  699. }
  700. .shadow-card:hover {
  701. box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  702. }
  703. .line-clamp-2 {
  704. display: -webkit-box;
  705. -webkit-line-clamp: 2;
  706. -webkit-box-orient: vertical;
  707. overflow: hidden;
  708. }
  709. /* Swiper分页器样式 */
  710. :deep(.swiper-pagination-bullet) {
  711. width: 12px;
  712. height: 12px;
  713. background: rgba(255, 255, 255, 0.5);
  714. opacity: 1;
  715. border: 2px solid rgba(255, 255, 255, 0.8);
  716. transition: all 0.3s ease;
  717. }
  718. :deep(.swiper-pagination-bullet-active) {
  719. background: var(--color-primary);
  720. border-color: var(--color-primary);
  721. transform: scale(1.3);
  722. box-shadow: 0 0 10px rgba(var(--color-primary-rgb), 0.5);
  723. }
  724. :deep(.swiper-pagination) {
  725. position: relative;
  726. bottom: 0;
  727. margin-top: 20px;
  728. }
  729. </style>