diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 4ada13c..fbe8964 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -51,7 +51,11 @@ request.interceptors.response.use( async (error: AxiosError) => { const detail = (error.response?.data as any)?.detail 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 } else if (Array.isArray(detail)) { msg = detail.map((e: any) => e.msg || JSON.stringify(e)).join('; ') diff --git a/frontend/src/views/Community.vue b/frontend/src/views/Community.vue index e1b0c2b..002aeea 100644 --- a/frontend/src/views/Community.vue +++ b/frontend/src/views/Community.vue @@ -81,6 +81,11 @@ +
+ +

{{ emptyText }}

@@ -163,6 +168,10 @@ const userStore = useUserStore() const loading = ref(false) const posts = ref([]) const rankedPosts = ref([]) +const comPageSize = 20 +const comCurrentPage = ref(1) +const comHasMore = ref(false) +const comLoadingMore = ref(false) const hotTags = ['互动课件', '教学动画', '课堂游戏', 'AI工具', '公开课', '单元复习', '实验模拟'] const postTypes = [{ value: 'discussion', label: '案例' }, { value: 'share', label: '模板' }, { value: 'help', label: '需求' }] const scopeOptions = [ @@ -267,23 +276,49 @@ function pathFromContentRef(ref: string) { return '/courseware/create' } +async function fetchPostPage(page: number): Promise { + 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() { loading.value = true + comCurrentPage.value = 1 try { - const params = activeScope.value === 'all' ? undefined : { scope: activeScope.value } - const [res, ranking] = await Promise.all([ - getPosts(params), + const [items, ranking] = await Promise.all([ + fetchPostPage(1), getPostRanking({ limit: 5 }), ]) as any[] - posts.value = Array.isArray(res) ? res : [] + posts.value = Array.isArray(items) ? items : [] rankedPosts.value = Array.isArray(ranking) ? ranking : [] + comHasMore.value = posts.value.length >= comPageSize } catch { posts.value = [] rankedPosts.value = [] + comHasMore.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() { const scope = route.query.scope 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-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; } diff --git a/frontend/src/views/Resources.vue b/frontend/src/views/Resources.vue index 49d261f..2f61f94 100644 --- a/frontend/src/views/Resources.vue +++ b/frontend/src/views/Resources.vue @@ -74,6 +74,12 @@ +
+ +
+
没有找到相关资源 @@ -208,6 +214,10 @@ const router = useRouter() const route = useRoute() const loading = ref(false) const resources = ref([]) +const pageSize = 20 +const currentPage = ref(1) +const hasMore = ref(false) +const loadingMore = ref(false) const showUpload = ref(false) const showDetail = ref(false) const showEdit = ref(false) @@ -616,32 +626,59 @@ async function handleCreateResource() { } } +async function fetchPage(page: number): Promise { + 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() { loading.value = true + currentPage.value = 1 try { - if ((filters.resource_type === 'favorite' || filters.resource_type === 'mine') && !ensureResourceLogin('请先登录后再查看个人资源')) { - resources.value = [] - 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, - } - 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 : [] + const items = await fetchPage(1) + resources.value = items + hasMore.value = items.length >= pageSize } catch { resources.value = [] + hasMore.value = false } finally { 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) { if (typeof res.id !== 'number') 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 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; } +.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: 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; } }