選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

175 行
7.7 KiB

  1. <template>
  2. <div>
  3. <!-- Hero -->
  4. <section class="border-b border-[var(--c-e8eef7)] bg-gradient-to-br from-[var(--c-ffffff)] via-[var(--c-eef6ff)] to-[var(--c-f5faff)]">
  5. <div class="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
  6. <!-- 面包屑(SEO) -->
  7. <nav aria-label="面包屑" class="flex items-center gap-1.5 text-xs text-[var(--c-8a97a8)]">
  8. <NuxtLink to="/" class="hover:text-[var(--c-0b8cff)]">首页</NuxtLink>
  9. <span>/</span>
  10. <NuxtLink v-if="colId || tagName" to="/info" class="hover:text-[var(--c-0b8cff)]">AI 资讯</NuxtLink>
  11. <span v-if="colId || tagName">/</span>
  12. <span class="text-[var(--c-4b5b70)]">{{ heading }}</span>
  13. </nav>
  14. <h1 class="mt-4 text-3xl font-extrabold text-[var(--c-172033)] sm:text-4xl">{{ heading }}</h1>
  15. <p class="mt-3 max-w-2xl text-sm leading-relaxed text-[var(--c-66758a)] sm:text-base">{{ subtitle }}</p>
  16. <!-- 栏目切换 -->
  17. <div class="mt-6 flex flex-wrap gap-2">
  18. <NuxtLink
  19. to="/info"
  20. class="rounded-full px-4 py-2 text-sm font-bold transition-colors"
  21. :class="!colId && !tagName ? 'bg-[var(--c-0b8cff)] text-white shadow-[0_8px_18px_var(--sh-11-140-255-220)]' : 'bg-[var(--c-ffffff)] text-[var(--c-5b6b80)] hover:text-[var(--c-0b8cff)]'"
  22. >
  23. 全部
  24. </NuxtLink>
  25. <NuxtLink
  26. v-for="col in COLUMNS"
  27. :key="col.colId"
  28. :to="col.path"
  29. class="inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-sm font-bold transition-colors"
  30. :class="colId === col.colId ? 'bg-[var(--c-0b8cff)] text-white shadow-[0_8px_18px_var(--sh-11-140-255-220)]' : 'bg-[var(--c-ffffff)] text-[var(--c-5b6b80)] hover:text-[var(--c-0b8cff)]'"
  31. >
  32. <UIcon :name="col.icon" class="size-4" />
  33. {{ col.name }}
  34. </NuxtLink>
  35. </div>
  36. </div>
  37. </section>
  38. <!-- 内容 -->
  39. <div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8 lg:py-12">
  40. <div class="lg:grid lg:grid-cols-12 lg:gap-8">
  41. <!-- 列表 -->
  42. <div class="lg:col-span-8">
  43. <div v-if="pending" class="space-y-4">
  44. <div v-for="i in 5" :key="i" class="h-32 animate-pulse rounded-2xl bg-[var(--c-ffffff)]" />
  45. </div>
  46. <div v-else-if="list.length" class="space-y-4">
  47. <article
  48. v-for="item in list"
  49. :key="item.id"
  50. class="group overflow-hidden rounded-2xl bg-[var(--c-ffffff)] shadow-[0_8px_28px_var(--sh-34-68-112-50)] transition-all hover:shadow-[0_14px_34px_var(--sh-34-68-112-120)]"
  51. >
  52. <NuxtLink :to="item.url" class="flex flex-col gap-4 p-4 sm:flex-row sm:p-5">
  53. <!-- 封面 -->
  54. <div v-if="item.cover" class="shrink-0 overflow-hidden rounded-xl bg-[var(--c-eef3f8)] sm:w-52">
  55. <img
  56. :src="item.cover"
  57. :alt="item.title"
  58. class="h-40 w-full object-cover transition-transform duration-500 group-hover:scale-105 sm:h-32"
  59. loading="lazy"
  60. >
  61. </div>
  62. <div class="flex min-w-0 flex-1 flex-col">
  63. <h2 class="line-clamp-2 text-base font-extrabold leading-snug text-[var(--c-172033)] transition-colors group-hover:text-[var(--c-0b8cff)] sm:text-lg">
  64. {{ item.title }}
  65. </h2>
  66. <p v-if="item.info" class="mt-2 line-clamp-2 flex-1 text-sm leading-relaxed text-[var(--c-8a97a8)]">
  67. {{ item.info }}
  68. </p>
  69. <div class="mt-3 flex flex-wrap items-center gap-3 text-xs text-[var(--c-a0aabb)]">
  70. <span class="rounded-full bg-[var(--c-eef7ff)] px-2 py-0.5 font-bold text-[var(--c-0b8cff)]">
  71. {{ colName(item.col_id) }}
  72. </span>
  73. <span class="inline-flex items-center gap-1">
  74. <UIcon name="i-lucide-clock" class="size-3.5" />
  75. {{ item.time }}
  76. </span>
  77. <span class="inline-flex items-center gap-1">
  78. <UIcon name="i-lucide-eye" class="size-3.5" />
  79. {{ item.view_num || 0 }}
  80. </span>
  81. </div>
  82. </div>
  83. </NuxtLink>
  84. </article>
  85. </div>
  86. <!-- 空态 -->
  87. <div v-else class="rounded-2xl bg-[var(--c-ffffff)] py-20 text-center shadow-[0_8px_28px_var(--sh-34-68-112-50)]">
  88. <UIcon name="i-lucide-newspaper" class="mx-auto mb-4 size-12 text-[var(--c-a0aabb)]" />
  89. <div class="text-base font-extrabold text-[var(--c-172033)]">暂无内容</div>
  90. <div class="mt-2 text-sm text-[var(--c-8a95a6)]">内容正在整理中,敬请期待</div>
  91. </div>
  92. <!-- 分页(保留原站 URL 形式,利于 SEO 收录) -->
  93. <nav v-if="totalPages > 1" aria-label="分页" class="mt-8 flex flex-wrap items-center justify-center gap-2">
  94. <NuxtLink
  95. v-if="page > 1"
  96. :to="pageUrl(basePath, page - 1, tagKey)"
  97. class="rounded-lg bg-[var(--c-ffffff)] px-3.5 py-2 text-sm font-bold text-[var(--c-5b6b80)] shadow-sm transition-colors hover:text-[var(--c-0b8cff)]"
  98. rel="prev"
  99. >
  100. 上一页
  101. </NuxtLink>
  102. <NuxtLink
  103. v-for="p in pageNumbers"
  104. :key="p"
  105. :to="pageUrl(basePath, p, tagKey)"
  106. class="min-w-9 rounded-lg px-3 py-2 text-center text-sm font-bold transition-colors"
  107. :class="p === page ? 'bg-[var(--c-0b8cff)] text-white' : 'bg-[var(--c-ffffff)] text-[var(--c-5b6b80)] shadow-sm hover:text-[var(--c-0b8cff)]'"
  108. >
  109. {{ p }}
  110. </NuxtLink>
  111. <NuxtLink
  112. v-if="page < totalPages"
  113. :to="pageUrl(basePath, page + 1, tagKey)"
  114. class="rounded-lg bg-[var(--c-ffffff)] px-3.5 py-2 text-sm font-bold text-[var(--c-5b6b80)] shadow-sm transition-colors hover:text-[var(--c-0b8cff)]"
  115. rel="next"
  116. >
  117. 下一页
  118. </NuxtLink>
  119. </nav>
  120. </div>
  121. <!-- 侧栏 -->
  122. <div class="mt-8 lg:col-span-4 lg:mt-0">
  123. <div class="lg:sticky lg:top-20">
  124. <NewsSidebar :hot-list="hotList" :tag-list="tagList" :active-tag="tagKey" />
  125. </div>
  126. </div>
  127. </div>
  128. </div>
  129. </div>
  130. </template>
  131. <script setup>
  132. import { COLUMNS, colName, pageUrl } from '~/composables/useArticle'
  133. const props = defineProps({
  134. /** 栏目 id,不传表示「全部」 */
  135. colId: { type: Number, default: 0 },
  136. /** 标签 key(标签页使用) */
  137. tagKey: { type: String, default: '' },
  138. /** 列表基础路径,用于生成分页链接 */
  139. basePath: { type: String, default: '/info' },
  140. heading: { type: String, required: true },
  141. subtitle: { type: String, default: '' },
  142. page: { type: Number, default: 1 },
  143. pageSize: { type: Number, default: 10 },
  144. list: { type: Array, default: () => [] },
  145. total: { type: Number, default: 0 },
  146. tagName: { type: String, default: '' },
  147. hotList: { type: Array, default: () => [] },
  148. tagList: { type: Array, default: () => [] },
  149. pending: { type: Boolean, default: false },
  150. })
  151. const totalPages = computed(() => Math.ceil(props.total / props.pageSize) || 0)
  152. // 分页只展示当前页附近的页码,避免页码过多
  153. const pageNumbers = computed(() => {
  154. const total = totalPages.value
  155. const cur = props.page
  156. const arr = []
  157. const start = Math.max(1, cur - 2)
  158. const end = Math.min(total, start + 4)
  159. for (let i = start; i <= end; i++) arr.push(i)
  160. return arr
  161. })
  162. </script>