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; } }