index.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <template>
  2. <div class="store-page">
  3. <Breadcrumb />
  4. <header class="page-header">
  5. <div>
  6. <h1 class="store-section-title">文档中心</h1>
  7. <p class="store-section-desc">使用指南、操作说明与常见问题</p>
  8. </div>
  9. </header>
  10. <a-skeleton animation :loading="categoriesLoading" class="doc-tabs-skeleton">
  11. <a-skeleton-line :widths="['88px', '96px', '80px', '72px']" :rows="1" />
  12. <template #content>
  13. <a-tabs v-if="categories.length" v-model:active-key="activeType" @change="onTabChange">
  14. <a-tab-pane v-for="cat in categories" :key="cat.slug">
  15. <template #title>
  16. <span>{{ cat.icon || '' }} {{ cat.name }}</span>
  17. </template>
  18. </a-tab-pane>
  19. </a-tabs>
  20. <a-empty v-else description="暂无文档分类" />
  21. </template>
  22. </a-skeleton>
  23. <a-skeleton animation :loading="listLoading" class="doc-list-skeleton">
  24. <StoreGridSkeleton :count="6" />
  25. <template #content>
  26. <div v-if="articles.length" class="doc-grid">
  27. <article
  28. v-for="item in articles"
  29. :key="item.slug"
  30. class="doc-card"
  31. @click="goDetail(item.slug)"
  32. >
  33. <div class="doc-card__visual">
  34. <GoodsEmoji :icon="item.cover" size="lg" :fallback="DEFAULT_ARTICLE_EMOJI" :aria-label="item.title" />
  35. </div>
  36. <div class="doc-card__body">
  37. <h3 class="doc-card__title">{{ item.title }}</h3>
  38. <p v-if="item.describe" class="doc-card__desc">{{ item.describe }}</p>
  39. <div class="doc-card__meta">
  40. <!-- <span>{{ item.author || 'RunForge' }}</span> -->
  41. <span>{{ formatTime(item.time) }}</span>
  42. <!-- <span>{{ item.views ?? 0 }} 次阅读</span> -->
  43. </div>
  44. </div>
  45. </article>
  46. </div>
  47. <a-empty v-else-if="activeType" description="该分类下暂无文档" />
  48. </template>
  49. </a-skeleton>
  50. <div v-if="pagination.total > pagination.pagesize" class="doc-pagination">
  51. <a-pagination
  52. :total="pagination.total"
  53. :current="pagination.current"
  54. :page-size="pagination.pagesize"
  55. show-total
  56. @change="onPageChange"
  57. />
  58. </div>
  59. </div>
  60. </template>
  61. <script setup>
  62. import { ref, reactive, onMounted } from 'vue'
  63. import { useRouter } from 'vue-router'
  64. import { Notification } from '@arco-design/web-vue'
  65. import { getArticleCategories, getArticleList } from '@/api/article'
  66. import GoodsEmoji from '@/components/store/GoodsEmoji.vue'
  67. import StoreGridSkeleton from '@/components/skeleton/StoreGridSkeleton.vue'
  68. import { DEFAULT_ARTICLE_EMOJI } from '@/utils/storeFormat'
  69. const router = useRouter()
  70. const categories = ref([])
  71. const categoriesLoading = ref(false)
  72. const listLoading = ref(false)
  73. const articles = ref([])
  74. const activeType = ref('')
  75. const pagination = reactive({ total: 0, current: 1, pagesize: 12 })
  76. const formatTime = (time) => {
  77. if (!time) return '-'
  78. return new Date(Number(time)).toLocaleDateString('zh-CN')
  79. }
  80. const loadCategories = async () => {
  81. categoriesLoading.value = true
  82. try {
  83. const res = await getArticleCategories()
  84. if (!res || res.code !== 0) {
  85. Notification.error({ title: '加载分类失败', content: res?.msg ?? '请稍后再试' })
  86. return
  87. }
  88. categories.value = res.data || []
  89. if (categories.value.length) {
  90. activeType.value = categories.value[0].slug
  91. await loadArticles()
  92. }
  93. } finally {
  94. categoriesLoading.value = false
  95. }
  96. }
  97. const loadArticles = async () => {
  98. if (!activeType.value) return
  99. listLoading.value = true
  100. try {
  101. const res = await getArticleList({
  102. type: activeType.value,
  103. current: pagination.current,
  104. pagesize: pagination.pagesize
  105. })
  106. if (!res || res.code !== 0) {
  107. Notification.error({ title: '加载文档失败', content: res?.msg ?? '请稍后再试' })
  108. return
  109. }
  110. articles.value = res.data || []
  111. pagination.total = res.pagination?.total || 0
  112. } finally {
  113. listLoading.value = false
  114. }
  115. }
  116. const onTabChange = () => {
  117. pagination.current = 1
  118. loadArticles()
  119. }
  120. const onPageChange = (page) => {
  121. pagination.current = page
  122. loadArticles()
  123. }
  124. const goDetail = (slug) => {
  125. router.push(`/doc/${slug}`)
  126. }
  127. onMounted(loadCategories)
  128. </script>
  129. <style scoped lang="less">
  130. @import '@/styles/store-theme.less';
  131. .doc-grid {
  132. display: grid;
  133. grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
  134. gap: 16px;
  135. }
  136. .doc-card {
  137. background: @store-card-bg;
  138. border: 1px solid @store-card-border;
  139. border-radius: @store-radius;
  140. overflow: hidden;
  141. cursor: pointer;
  142. transition: transform 0.2s, box-shadow 0.2s;
  143. &:hover {
  144. transform: translateY(-2px);
  145. box-shadow: @store-shadow-hover;
  146. }
  147. &__visual {
  148. display: flex;
  149. align-items: center;
  150. justify-content: center;
  151. min-height: 120px;
  152. background: @store-cover-bg;
  153. border-bottom: 1px solid @store-card-border;
  154. }
  155. &__body {
  156. padding: 16px;
  157. }
  158. &__title {
  159. margin: 0 0 8px;
  160. font-size: 1.05rem;
  161. color: @store-primary;
  162. font-weight: 600;
  163. line-height: 1.4;
  164. }
  165. &__desc {
  166. margin: 0 0 12px;
  167. font-size: 0.875rem;
  168. color: @store-text-muted;
  169. line-height: 1.5;
  170. display: -webkit-box;
  171. -webkit-line-clamp: 2;
  172. -webkit-box-orient: vertical;
  173. overflow: hidden;
  174. }
  175. &__meta {
  176. display: flex;
  177. flex-wrap: wrap;
  178. gap: 8px 12px;
  179. font-size: 0.75rem;
  180. color: fade(@store-text-muted, 80%);
  181. }
  182. }
  183. .doc-tabs-skeleton {
  184. width: 100%;
  185. }
  186. .doc-list-skeleton {
  187. width: 100%;
  188. margin-top: 16px;
  189. }
  190. .doc-pagination {
  191. margin-top: 24px;
  192. display: flex;
  193. justify-content: center;
  194. }
  195. </style>