| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- <template>
- <div class="store-page">
- <Breadcrumb />
- <header class="page-header">
- <div>
- <h1 class="store-section-title">文档中心</h1>
- <p class="store-section-desc">使用指南、操作说明与常见问题</p>
- </div>
- </header>
- <a-skeleton animation :loading="categoriesLoading" class="doc-tabs-skeleton">
- <a-skeleton-line :widths="['88px', '96px', '80px', '72px']" :rows="1" />
- <template #content>
- <a-tabs v-if="categories.length" v-model:active-key="activeType" @change="onTabChange">
- <a-tab-pane v-for="cat in categories" :key="cat.slug">
- <template #title>
- <span>{{ cat.icon || '' }} {{ cat.name }}</span>
- </template>
- </a-tab-pane>
- </a-tabs>
- <a-empty v-else description="暂无文档分类" />
- </template>
- </a-skeleton>
- <a-skeleton animation :loading="listLoading" class="doc-list-skeleton">
- <StoreGridSkeleton :count="6" />
- <template #content>
- <div v-if="articles.length" class="doc-grid">
- <article
- v-for="item in articles"
- :key="item.slug"
- class="doc-card"
- @click="goDetail(item.slug)"
- >
- <div class="doc-card__visual">
- <GoodsEmoji :icon="item.cover" size="lg" :fallback="DEFAULT_ARTICLE_EMOJI" :aria-label="item.title" />
- </div>
- <div class="doc-card__body">
- <h3 class="doc-card__title">{{ item.title }}</h3>
- <p v-if="item.describe" class="doc-card__desc">{{ item.describe }}</p>
- <div class="doc-card__meta">
- <!-- <span>{{ item.author || 'RunForge' }}</span> -->
- <span>{{ formatTime(item.time) }}</span>
- <!-- <span>{{ item.views ?? 0 }} 次阅读</span> -->
- </div>
- </div>
- </article>
- </div>
- <a-empty v-else-if="activeType" description="该分类下暂无文档" />
- </template>
- </a-skeleton>
- <div v-if="pagination.total > pagination.pagesize" class="doc-pagination">
- <a-pagination
- :total="pagination.total"
- :current="pagination.current"
- :page-size="pagination.pagesize"
- show-total
- @change="onPageChange"
- />
- </div>
- </div>
- </template>
- <script setup>
- import { ref, reactive, onMounted } from 'vue'
- import { useRouter } from 'vue-router'
- import { Notification } from '@arco-design/web-vue'
- import { getArticleCategories, getArticleList } from '@/api/article'
- import GoodsEmoji from '@/components/store/GoodsEmoji.vue'
- import StoreGridSkeleton from '@/components/skeleton/StoreGridSkeleton.vue'
- import { DEFAULT_ARTICLE_EMOJI } from '@/utils/storeFormat'
- const router = useRouter()
- const categories = ref([])
- const categoriesLoading = ref(false)
- const listLoading = ref(false)
- const articles = ref([])
- const activeType = ref('')
- const pagination = reactive({ total: 0, current: 1, pagesize: 12 })
- const formatTime = (time) => {
- if (!time) return '-'
- return new Date(Number(time)).toLocaleDateString('zh-CN')
- }
- const loadCategories = async () => {
- categoriesLoading.value = true
- try {
- const res = await getArticleCategories()
- if (!res || res.code !== 0) {
- Notification.error({ title: '加载分类失败', content: res?.msg ?? '请稍后再试' })
- return
- }
- categories.value = res.data || []
- if (categories.value.length) {
- activeType.value = categories.value[0].slug
- await loadArticles()
- }
- } finally {
- categoriesLoading.value = false
- }
- }
- const loadArticles = async () => {
- if (!activeType.value) return
- listLoading.value = true
- try {
- const res = await getArticleList({
- type: activeType.value,
- current: pagination.current,
- pagesize: pagination.pagesize
- })
- if (!res || res.code !== 0) {
- Notification.error({ title: '加载文档失败', content: res?.msg ?? '请稍后再试' })
- return
- }
- articles.value = res.data || []
- pagination.total = res.pagination?.total || 0
- } finally {
- listLoading.value = false
- }
- }
- const onTabChange = () => {
- pagination.current = 1
- loadArticles()
- }
- const onPageChange = (page) => {
- pagination.current = page
- loadArticles()
- }
- const goDetail = (slug) => {
- router.push(`/doc/${slug}`)
- }
- onMounted(loadCategories)
- </script>
- <style scoped lang="less">
- @import '@/styles/store-theme.less';
- .doc-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
- gap: 16px;
- }
- .doc-card {
- background: @store-card-bg;
- border: 1px solid @store-card-border;
- border-radius: @store-radius;
- overflow: hidden;
- cursor: pointer;
- transition: transform 0.2s, box-shadow 0.2s;
- &:hover {
- transform: translateY(-2px);
- box-shadow: @store-shadow-hover;
- }
- &__visual {
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 120px;
- background: @store-cover-bg;
- border-bottom: 1px solid @store-card-border;
- }
- &__body {
- padding: 16px;
- }
- &__title {
- margin: 0 0 8px;
- font-size: 1.05rem;
- color: @store-primary;
- font-weight: 600;
- line-height: 1.4;
- }
- &__desc {
- margin: 0 0 12px;
- font-size: 0.875rem;
- color: @store-text-muted;
- line-height: 1.5;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- &__meta {
- display: flex;
- flex-wrap: wrap;
- gap: 8px 12px;
- font-size: 0.75rem;
- color: fade(@store-text-muted, 80%);
- }
- }
- .doc-tabs-skeleton {
- width: 100%;
- }
- .doc-list-skeleton {
- width: 100%;
- margin-top: 16px;
- }
- .doc-pagination {
- margin-top: 24px;
- display: flex;
- justify-content: center;
- }
- </style>
|