feat: 资源库和社区分页加载、网络超时错误提示优化

This commit is contained in:
Zhang Jing Xuan
2026-07-30 16:29:32 +08:00
parent 56a7a274d9
commit 3d3c06999f
3 changed files with 105 additions and 21 deletions
+5 -1
View File
@@ -51,7 +51,11 @@ request.interceptors.response.use(
async (error: AxiosError) => { async (error: AxiosError) => {
const detail = (error.response?.data as any)?.detail const detail = (error.response?.data as any)?.detail
let msg = '请求失败,请稍后重试' let msg = '请求失败,请稍后重试'
if (typeof detail === 'string') { if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
msg = '请求超时,AI 生成可能需要较长时间,请稍后重试'
} else if (!error.response) {
msg = '网络连接失败,请检查网络后重试'
} else if (typeof detail === 'string') {
msg = detail msg = detail
} else if (Array.isArray(detail)) { } else if (Array.isArray(detail)) {
msg = detail.map((e: any) => e.msg || JSON.stringify(e)).join('; ') msg = detail.map((e: any) => e.msg || JSON.stringify(e)).join('; ')
+43 -4
View File
@@ -81,6 +81,11 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="comHasMore && !loading && filteredPosts.length > 0" class="com-load-more">
<button type="button" :disabled="comLoadingMore" @click="loadMorePosts">
{{ comLoadingMore ? '加载中...' : '加载更多' }}
</button>
</div>
<div v-if="!loading && filteredPosts.length === 0" class="empty-state"> <div v-if="!loading && filteredPosts.length === 0" class="empty-state">
<el-icon :size="48" color="#CBD5E1"><ChatDotRound /></el-icon> <el-icon :size="48" color="#CBD5E1"><ChatDotRound /></el-icon>
<p>{{ emptyText }}</p> <p>{{ emptyText }}</p>
@@ -163,6 +168,10 @@ const userStore = useUserStore()
const loading = ref(false) const loading = ref(false)
const posts = ref<any[]>([]) const posts = ref<any[]>([])
const rankedPosts = ref<any[]>([]) const rankedPosts = ref<any[]>([])
const comPageSize = 20
const comCurrentPage = ref(1)
const comHasMore = ref(false)
const comLoadingMore = ref(false)
const hotTags = ['互动课件', '教学动画', '课堂游戏', 'AI工具', '公开课', '单元复习', '实验模拟'] const hotTags = ['互动课件', '教学动画', '课堂游戏', 'AI工具', '公开课', '单元复习', '实验模拟']
const postTypes = [{ value: 'discussion', label: '案例' }, { value: 'share', label: '模板' }, { value: 'help', label: '需求' }] const postTypes = [{ value: 'discussion', label: '案例' }, { value: 'share', label: '模板' }, { value: 'help', label: '需求' }]
const scopeOptions = [ const scopeOptions = [
@@ -267,23 +276,49 @@ function pathFromContentRef(ref: string) {
return '/courseware/create' return '/courseware/create'
} }
async function fetchPostPage(page: number): Promise<any[]> {
const base = activeScope.value === 'all' ? {} : { scope: activeScope.value }
const res: any = await getPosts({ ...base, skip: (page - 1) * comPageSize, limit: comPageSize })
return Array.isArray(res) ? res : []
}
async function loadData() { async function loadData() {
loading.value = true loading.value = true
comCurrentPage.value = 1
try { try {
const params = activeScope.value === 'all' ? undefined : { scope: activeScope.value } const [items, ranking] = await Promise.all([
const [res, ranking] = await Promise.all([ fetchPostPage(1),
getPosts(params),
getPostRanking({ limit: 5 }), getPostRanking({ limit: 5 }),
]) as any[] ]) as any[]
posts.value = Array.isArray(res) ? res : [] posts.value = Array.isArray(items) ? items : []
rankedPosts.value = Array.isArray(ranking) ? ranking : [] rankedPosts.value = Array.isArray(ranking) ? ranking : []
comHasMore.value = posts.value.length >= comPageSize
} catch { } catch {
posts.value = [] posts.value = []
rankedPosts.value = [] rankedPosts.value = []
comHasMore.value = false
} }
finally { loading.value = false } finally { loading.value = false }
} }
async function loadMorePosts() {
if (comLoadingMore.value || !comHasMore.value) return
comLoadingMore.value = true
try {
const nextPage = comCurrentPage.value + 1
const items = await fetchPostPage(nextPage)
if (items.length > 0) {
posts.value = [...posts.value, ...items]
comCurrentPage.value = nextPage
}
comHasMore.value = items.length >= comPageSize
} catch {
ElMessage.warning('加载更多失败,请稍后重试')
} finally {
comLoadingMore.value = false
}
}
function syncScopeFromRoute() { function syncScopeFromRoute() {
const scope = route.query.scope const scope = route.query.scope
const nextScope = scopeOptions.some(item => item.value === scope) ? String(scope) : 'all' const nextScope = scopeOptions.some(item => item.value === scope) ? String(scope) : 'all'
@@ -490,4 +525,8 @@ watch(() => route.query.scope, () => {
.post-card { grid-template-columns: 1fr; } .post-card { grid-template-columns: 1fr; }
.post-actions { flex-direction: column; } .post-actions { flex-direction: column; }
} }
.com-load-more { display: flex; justify-content: center; padding: 20px 0; }
.com-load-more button { border: 1px solid #d1e7dd; border-radius: 22px; background: rgba(255,255,255,.85); color: #00543d; padding: 8px 36px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all .2s; }
.com-load-more button:hover:not(:disabled) { background: #00543d; color: #fff; }
.com-load-more button:disabled { opacity: .6; cursor: not-allowed; }
</style> </style>
+57 -16
View File
@@ -74,6 +74,12 @@
</article> </article>
</section> </section>
<div v-if="hasMore && !loading && displayResources.length > 0" class="load-more-wrap">
<button type="button" class="load-more-btn" :disabled="loadingMore" @click="loadMore">
{{ loadingMore ? '加载中...' : '加载更多' }}
</button>
</div>
<div v-if="!loading && displayResources.length === 0" class="empty-state"> <div v-if="!loading && displayResources.length === 0" class="empty-state">
<el-icon><FolderOpened /></el-icon> <el-icon><FolderOpened /></el-icon>
<strong>没有找到相关资源</strong> <strong>没有找到相关资源</strong>
@@ -208,6 +214,10 @@ const router = useRouter()
const route = useRoute() const route = useRoute()
const loading = ref(false) const loading = ref(false)
const resources = ref<any[]>([]) const resources = ref<any[]>([])
const pageSize = 20
const currentPage = ref(1)
const hasMore = ref(false)
const loadingMore = ref(false)
const showUpload = ref(false) const showUpload = ref(false)
const showDetail = ref(false) const showDetail = ref(false)
const showEdit = ref(false) const showEdit = ref(false)
@@ -616,32 +626,59 @@ async function handleCreateResource() {
} }
} }
async function fetchPage(page: number): Promise<any[]> {
if ((filters.resource_type === 'favorite' || filters.resource_type === 'mine') && !ensureResourceLogin('请先登录后再查看个人资源')) {
return []
}
const params = {
resource_type: filters.resource_type === 'favorite' || filters.resource_type === 'mine' ? '' : filters.resource_type,
subject: filters.subject,
keyword: filters.keyword,
sort: filters.sort,
skip: (page - 1) * pageSize,
limit: pageSize,
}
const res: any = filters.resource_type === 'favorite'
? await getFavoriteResources(params)
: filters.resource_type === 'mine'
? await getMyResources(params)
: await getResources(params)
return Array.isArray(res) ? res : []
}
async function loadData() { async function loadData() {
loading.value = true loading.value = true
currentPage.value = 1
try { try {
if ((filters.resource_type === 'favorite' || filters.resource_type === 'mine') && !ensureResourceLogin('请先登录后再查看个人资源')) { const items = await fetchPage(1)
resources.value = [] resources.value = items
return hasMore.value = items.length >= pageSize
}
const params = {
resource_type: filters.resource_type === 'favorite' || filters.resource_type === 'mine' ? '' : filters.resource_type,
subject: filters.subject,
keyword: filters.keyword,
sort: filters.sort,
}
const res: any = filters.resource_type === 'favorite'
? await getFavoriteResources(params)
: filters.resource_type === 'mine'
? await getMyResources(params)
: await getResources(params)
resources.value = Array.isArray(res) ? res : []
} catch { } catch {
resources.value = [] resources.value = []
hasMore.value = false
} finally { } finally {
loading.value = false loading.value = false
} }
} }
async function loadMore() {
if (loadingMore.value || !hasMore.value) return
loadingMore.value = true
try {
const nextPage = currentPage.value + 1
const items = await fetchPage(nextPage)
if (items.length > 0) {
resources.value = [...resources.value, ...items]
currentPage.value = nextPage
}
hasMore.value = items.length >= pageSize
} catch {
ElMessage.warning('加载更多失败,请稍后重试')
} finally {
loadingMore.value = false
}
}
async function unpublishResource(res: any) { async function unpublishResource(res: any) {
if (typeof res.id !== 'number') return if (typeof res.id !== 'number') return
if (!ensureResourceLogin('请先登录后再下架资源')) return if (!ensureResourceLogin('请先登录后再下架资源')) return
@@ -772,6 +809,10 @@ watch(() => [route.query.keyword, route.query.tab], () => {
.empty-state .el-icon { font-size: 32px; color: #9aaba4; } .empty-state .el-icon { font-size: 32px; color: #9aaba4; }
.empty-state strong { color: #173f35; } .empty-state strong { color: #173f35; }
.empty-state button { margin-top: 6px; border: none; border-radius: 18px; background: #00543d; color: #fff; padding: 8px 18px; font-weight: 800; cursor: pointer; } .empty-state button { margin-top: 6px; border: none; border-radius: 18px; background: #00543d; color: #fff; padding: 8px 18px; font-weight: 800; cursor: pointer; }
.load-more-wrap { display: flex; justify-content: center; padding: 24px 0 8px; }
.load-more-btn { border: 1px solid #cfe0d3; border-radius: 24px; background: rgba(255,255,255,.8); color: #00543d; padding: 10px 40px; font-size: 15px; font-weight: 700; cursor: pointer; transition: all .2s; }
.load-more-btn:hover:not(:disabled) { background: #00543d; color: #fff; border-color: #00543d; }
.load-more-btn:disabled { opacity: .6; cursor: not-allowed; }
@media (max-width: 1380px) { .resource-grid { grid-template-columns: repeat(3, minmax(240px, 1fr)); } } @media (max-width: 1380px) { .resource-grid { grid-template-columns: repeat(3, minmax(240px, 1fr)); } }
@media (max-width: 1080px) { .resource-grid { grid-template-columns: repeat(2, minmax(220px, 1fr)); } .filter-panel { align-items: stretch; flex-direction: column; } .filter-line { justify-content: space-between; } } @media (max-width: 1080px) { .resource-grid { grid-template-columns: repeat(2, minmax(220px, 1fr)); } .filter-panel { align-items: stretch; flex-direction: column; } .filter-line { justify-content: space-between; } }
@media (max-width: 760px) { .resource-plaza { padding: 16px; } .resource-hero { align-items: flex-start; flex-direction: column; } .filter-line { align-items: stretch; flex-direction: column; } .search-box { width: 100%; } .resource-grid, .detail-actions, .upload-grid { grid-template-columns: 1fr; } } @media (max-width: 760px) { .resource-plaza { padding: 16px; } .resource-hero { align-items: flex-start; flex-direction: column; } .filter-line { align-items: stretch; flex-direction: column; } .search-box { width: 100%; } .resource-grid, .detail-actions, .upload-grid { grid-template-columns: 1fr; } }