feat: update
This commit is contained in:
48
frontend/wechat/src/api/client.js
Normal file
48
frontend/wechat/src/api/client.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Create axios instance with default config
|
||||||
|
const client = axios.create({
|
||||||
|
baseURL: '/v1',
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request interceptor
|
||||||
|
client.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
if (authStore.isAuthenticated && authStore.token) {
|
||||||
|
config.headers.Authorization = `Bearer ${authStore.token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
client.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
// Handle HTTP errors here
|
||||||
|
if (error.response) {
|
||||||
|
// Server responded with error status
|
||||||
|
console.error('API Error:', error.response.status, error.response.data);
|
||||||
|
} else if (error.request) {
|
||||||
|
// Request made but no response received
|
||||||
|
console.error('API Error: No response received', error.request);
|
||||||
|
} else {
|
||||||
|
// Something else happened
|
||||||
|
console.error('API Error:', error.message);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default client;
|
||||||
27
frontend/wechat/src/api/postApi.js
Normal file
27
frontend/wechat/src/api/postApi.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import client from './client';
|
||||||
|
|
||||||
|
export const postApi = {
|
||||||
|
list({ page = 1, limit = 10, keyword = '' } = {}) {
|
||||||
|
return client.get('/api/posts', {
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
keyword: keyword.trim()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
show(id) {
|
||||||
|
return client.get(`/api/posts/${id}`);
|
||||||
|
},
|
||||||
|
mine({ page = 1, limit = 10 } = {}) {
|
||||||
|
return client.get('/api/posts/mine', {
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
limit
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buy(id) {
|
||||||
|
return client.post(`/api/posts/buy/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useArticleStore = defineStore('article', {
|
|
||||||
state: () => ({
|
|
||||||
articles: [],
|
|
||||||
page: 1,
|
|
||||||
loading: false,
|
|
||||||
hasMore: true,
|
|
||||||
searchQuery: ''
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
async fetchArticles() {
|
|
||||||
if (this.loading || !this.hasMore) return
|
|
||||||
this.loading = true
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/posts?page=${this.page}&q=${this.searchQuery}`)
|
|
||||||
const data = await response.json()
|
|
||||||
const items = data.items || []
|
|
||||||
this.articles = [...this.articles, ...items]
|
|
||||||
this.hasMore = items.length > 0 // 只有当返回的列表为空时,才设置 hasMore 为 false
|
|
||||||
this.page++
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setSearchQuery(query) {
|
|
||||||
this.searchQuery = query
|
|
||||||
this.articles = []
|
|
||||||
this.page = 1
|
|
||||||
this.hasMore = true
|
|
||||||
this.fetchArticles()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
{
|
|
||||||
"page": 7,
|
|
||||||
"limit": 10,
|
|
||||||
"total": 100,
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": 40,
|
|
||||||
"created_at": "2025-04-11T15:03:25.666419Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.666419Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-39",
|
|
||||||
"description": "test-description-39",
|
|
||||||
"content": "test-content-39",
|
|
||||||
"price": 6747,
|
|
||||||
"discount": 98,
|
|
||||||
"views": 8091,
|
|
||||||
"likes": 9914,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 8143
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 2977
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 39,
|
|
||||||
"created_at": "2025-04-11T15:03:25.661237Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.661237Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-38",
|
|
||||||
"description": "test-description-38",
|
|
||||||
"content": "test-content-38",
|
|
||||||
"price": 2891,
|
|
||||||
"discount": 45,
|
|
||||||
"views": 4462,
|
|
||||||
"likes": 6110,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 5431
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 3866
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 38,
|
|
||||||
"created_at": "2025-04-11T15:03:25.657112Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.657112Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-37",
|
|
||||||
"description": "test-description-37",
|
|
||||||
"content": "test-content-37",
|
|
||||||
"price": 904,
|
|
||||||
"discount": 70,
|
|
||||||
"views": 8988,
|
|
||||||
"likes": 816,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 3307
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 2933
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 37,
|
|
||||||
"created_at": "2025-04-11T15:03:25.65266Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.65266Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-36",
|
|
||||||
"description": "test-description-36",
|
|
||||||
"content": "test-content-36",
|
|
||||||
"price": 4476,
|
|
||||||
"discount": 45,
|
|
||||||
"views": 9910,
|
|
||||||
"likes": 9295,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 3472
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 3283
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 36,
|
|
||||||
"created_at": "2025-04-11T15:03:25.644946Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.644946Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-35",
|
|
||||||
"description": "test-description-35",
|
|
||||||
"content": "test-content-35",
|
|
||||||
"price": 7397,
|
|
||||||
"discount": 56,
|
|
||||||
"views": 2486,
|
|
||||||
"likes": 673,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 1183
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 9735
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 35,
|
|
||||||
"created_at": "2025-04-11T15:03:25.640256Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.640256Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-34",
|
|
||||||
"description": "test-description-34",
|
|
||||||
"content": "test-content-34",
|
|
||||||
"price": 3733,
|
|
||||||
"discount": 15,
|
|
||||||
"views": 3705,
|
|
||||||
"likes": 1863,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 8713
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 9442
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 34,
|
|
||||||
"created_at": "2025-04-11T15:03:25.634088Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.634088Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-33",
|
|
||||||
"description": "test-description-33",
|
|
||||||
"content": "test-content-33",
|
|
||||||
"price": 6591,
|
|
||||||
"discount": 12,
|
|
||||||
"views": 3834,
|
|
||||||
"likes": 7400,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 7429
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 9401
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 33,
|
|
||||||
"created_at": "2025-04-11T15:03:25.628572Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.628572Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-32",
|
|
||||||
"description": "test-description-32",
|
|
||||||
"content": "test-content-32",
|
|
||||||
"price": 3740,
|
|
||||||
"discount": 92,
|
|
||||||
"views": 7324,
|
|
||||||
"likes": 3705,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 8942
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 2618
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 32,
|
|
||||||
"created_at": "2025-04-11T15:03:25.623885Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.623885Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-31",
|
|
||||||
"description": "test-description-31",
|
|
||||||
"content": "test-content-31",
|
|
||||||
"price": 1839,
|
|
||||||
"discount": 37,
|
|
||||||
"views": 4750,
|
|
||||||
"likes": 2995,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 7409
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 8843
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 31,
|
|
||||||
"created_at": "2025-04-11T15:03:25.61745Z",
|
|
||||||
"updated_at": "2025-04-11T15:03:25.61745Z",
|
|
||||||
"deleted_at": null,
|
|
||||||
"status": 1,
|
|
||||||
"title": "test-title-30",
|
|
||||||
"description": "test-description-30",
|
|
||||||
"content": "test-content-30",
|
|
||||||
"price": 2297,
|
|
||||||
"discount": 56,
|
|
||||||
"views": 715,
|
|
||||||
"likes": 1224,
|
|
||||||
"tags": [
|
|
||||||
"tag1",
|
|
||||||
"tag2",
|
|
||||||
"tag3"
|
|
||||||
],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"type": "audio",
|
|
||||||
"media": 9268
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "video",
|
|
||||||
"media": 4416
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,54 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { AiOutlineLeft } from 'vue-icons-plus/ai'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { postApi } from '../api/postApi'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const article = ref(null)
|
const article = ref(null)
|
||||||
|
const buying = ref(false)
|
||||||
|
|
||||||
|
const handleBuy = async () => {
|
||||||
|
if (buying.value) return
|
||||||
|
buying.value = true
|
||||||
|
try {
|
||||||
|
const response = await postApi.buy(route.params.id)
|
||||||
|
const payData = response.data
|
||||||
|
|
||||||
|
// 调用微信支付
|
||||||
|
window.WeixinJSBridge.invoke('getBrandWCPayRequest', {
|
||||||
|
...payData
|
||||||
|
}, function (res) {
|
||||||
|
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||||
|
// 支付成功,刷新文章数据
|
||||||
|
fetchArticle()
|
||||||
|
} else {
|
||||||
|
// 支付失败或取消
|
||||||
|
console.error('Payment failed:', res.err_msg)
|
||||||
|
alert('支付失败:' + (res.err_msg === 'get_brand_wcpay_request:cancel' ? '支付已取消' : '支付异常'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initiate payment:', error)
|
||||||
|
alert('发起支付失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
buying.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchArticle = async () => {
|
||||||
|
try {
|
||||||
|
const { id } = route.params
|
||||||
|
const { data } = await postApi.show(id)
|
||||||
|
article.value = data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch article:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// TODO: Implement API call to fetch article details
|
await fetchArticle()
|
||||||
const { id } = route.params
|
|
||||||
article.value = { id, title: '文章标题', content: '文章内容' }
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -18,10 +57,7 @@ onMounted(async () => {
|
|||||||
<header class="fixed top-0 left-0 right-0 h-14 bg-white border-b border-gray-200 flex items-center px-4 z-50">
|
<header class="fixed top-0 left-0 right-0 h-14 bg-white border-b border-gray-200 flex items-center px-4 z-50">
|
||||||
<button @click="router.back()"
|
<button @click="router.back()"
|
||||||
class="flex items-center justify-center w-10 h-10 mr-2 rounded-full hover:bg-gray-100 active:bg-gray-200 transition-colors">
|
class="flex items-center justify-center w-10 h-10 mr-2 rounded-full hover:bg-gray-100 active:bg-gray-200 transition-colors">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
<AiOutlineLeft class="h-5 w-5" />
|
||||||
stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
<h2 class="text-lg font-medium">{{ article?.title || '文章详情' }}</h2>
|
<h2 class="text-lg font-medium">{{ article?.title || '文章详情' }}</h2>
|
||||||
</header>
|
</header>
|
||||||
@@ -29,7 +65,19 @@ onMounted(async () => {
|
|||||||
<main class="pt-14 px-4">
|
<main class="pt-14 px-4">
|
||||||
<div v-if="article" class="py-4">
|
<div v-if="article" class="py-4">
|
||||||
<div class="prose max-w-none">
|
<div class="prose max-w-none">
|
||||||
{{ article.content }}
|
<template v-if="article.purchased">
|
||||||
|
{{ article.content }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<p class="text-gray-600 mb-4">购买后即可阅读完整内容</p>
|
||||||
|
<button @click="handleBuy" :disabled="buying"
|
||||||
|
class="bg-blue-600 text-white px-6 py-2 rounded-full hover:bg-blue-700 active:bg-blue-800 transition-colors disabled:opacity-50">
|
||||||
|
<span v-if="buying">处理中...</span>
|
||||||
|
<span v-else>购买文章 ¥{{ article.price }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex justify-center items-center py-8">
|
<div v-else class="flex justify-center items-center py-8">
|
||||||
@@ -37,8 +85,4 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 可以移除所有样式,因为都使用了 Tailwind 类 */
|
|
||||||
</style>
|
|
||||||
@@ -1,28 +1,51 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { postApi } from '@/api/postApi'
|
||||||
import ArticleListItem from '@/components/ArticleListItem.vue'
|
import ArticleListItem from '@/components/ArticleListItem.vue'
|
||||||
import { useArticleStore } from '@/stores/article'
|
|
||||||
import { useIntersectionObserver } from '@vueuse/core'
|
import { useIntersectionObserver } from '@vueuse/core'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useArticleStore()
|
|
||||||
const searchInput = ref('')
|
const searchInput = ref('')
|
||||||
const loadingTrigger = ref(null)
|
const loadingTrigger = ref(null)
|
||||||
|
const articles = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const hasMore = ref(true)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const limit = 10
|
||||||
|
|
||||||
|
const fetchArticles = async () => {
|
||||||
|
if (loading.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await postApi.list({
|
||||||
|
page: currentPage.value,
|
||||||
|
limit,
|
||||||
|
keyword: searchInput.value
|
||||||
|
})
|
||||||
|
if (data.items?.length === 0) {
|
||||||
|
hasMore.value = false
|
||||||
|
} else {
|
||||||
|
articles.value.push(...data.items)
|
||||||
|
currentPage.value += 1
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch articles:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 优化 Intersection Observer 配置
|
|
||||||
useIntersectionObserver(
|
useIntersectionObserver(
|
||||||
loadingTrigger,
|
loadingTrigger,
|
||||||
([{ isIntersecting }]) => {
|
([{ isIntersecting }]) => {
|
||||||
console.log('Intersection state:', { isIntersecting, loading: store.loading, hasMore: store.hasMore })
|
if (isIntersecting && !loading.value && hasMore.value) {
|
||||||
if (isIntersecting && !store.loading && store.hasMore) {
|
fetchArticles()
|
||||||
console.log('Fetching more articles...')
|
|
||||||
store.fetchArticles()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
rootMargin: '100px' // 提前 100px 触发加载
|
rootMargin: '100px'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,7 +54,10 @@ const showArticle = (id) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
store.setSearchQuery(searchInput.value)
|
articles.value = []
|
||||||
|
currentPage.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
fetchArticles()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyup = (e) => {
|
const handleKeyup = (e) => {
|
||||||
@@ -41,9 +67,7 @@ const handleKeyup = (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (store.articles.length === 0) {
|
fetchArticles()
|
||||||
store.fetchArticles()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -58,16 +82,15 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div v-if="store.articles.length === 0 && !store.loading" class="text-center text-gray-500 py-8">
|
<div v-if="articles.length === 0 && !loading" class="text-center text-gray-500 py-8">
|
||||||
暂无文章
|
暂无文章
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ArticleListItem v-for="article in store.articles" :key="article.id" :article="article"
|
<ArticleListItem v-for="article in articles" :key="article.id" :article="article"
|
||||||
@click="showArticle(article.id)" class="mb-4" />
|
@click="showArticle(article.id)" class="mb-4" />
|
||||||
|
|
||||||
<!-- 优化加载触发器位置和显示 -->
|
<div ref="loadingTrigger" class="py-4 text-center" v-show="hasMore || loading">
|
||||||
<div ref="loadingTrigger" class="py-4 text-center" v-show="store.hasMore || store.loading">
|
<div v-if="loading"
|
||||||
<div v-if="store.loading"
|
|
||||||
class="animate-spin rounded-full h-8 w-8 border-4 border-gray-200 border-t-blue-600 mx-auto">
|
class="animate-spin rounded-full h-8 w-8 border-4 border-gray-200 border-t-blue-600 mx-auto">
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="h-8">
|
<div v-else class="h-8">
|
||||||
@@ -75,7 +98,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!store.hasMore && store.articles.length > 0" class="text-center text-gray-500 py-4">
|
<div v-if="!hasMore && articles.length > 0" class="text-center text-gray-500 py-4">
|
||||||
没有更多文章了
|
没有更多文章了
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,58 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { postApi } from '@/api/postApi'
|
||||||
import ArticleListItem from '@/components/ArticleListItem.vue'
|
import ArticleListItem from '@/components/ArticleListItem.vue'
|
||||||
|
import { useIntersectionObserver } from '@vueuse/core'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const purchasedArticles = ref([])
|
const purchasedArticles = ref([])
|
||||||
|
const loadingTrigger = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const hasMore = ref(true)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const limit = 10
|
||||||
|
|
||||||
const showArticle = (id) => {
|
const fetchArticles = async () => {
|
||||||
router.push(`/article/${id}`)
|
if (loading.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await postApi.mine({
|
||||||
|
page: currentPage.value,
|
||||||
|
limit
|
||||||
|
})
|
||||||
|
if (response.data.data?.length === 0) {
|
||||||
|
hasMore.value = false
|
||||||
|
} else {
|
||||||
|
purchasedArticles.value.push(...response.data.data)
|
||||||
|
currentPage.value += 1
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch purchased articles:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
useIntersectionObserver(
|
||||||
// TODO: Implement API call to fetch purchased articles
|
loadingTrigger,
|
||||||
purchasedArticles.value = []
|
([{ isIntersecting }]) => {
|
||||||
|
if (isIntersecting && !loading.value && hasMore.value) {
|
||||||
|
fetchArticles()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0,
|
||||||
|
rootMargin: '100px'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const showArticle = (id) => {
|
||||||
|
router.push(`/posts/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchArticles()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -20,23 +60,32 @@ onMounted(async () => {
|
|||||||
<div class="h-full flex flex-col">
|
<div class="h-full flex flex-col">
|
||||||
<div class="flex-none bg-white border-b border-gray-200 z-50 shadow">
|
<div class="flex-none bg-white border-b border-gray-200 z-50 shadow">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h2 class="text-lg font-medium text-gray-800">已购买的文章</h2>
|
<h2 class="text-lg font-medium text-gray-800">已购买</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div v-if="purchasedArticles.length === 0" class="text-center text-gray-500 py-8">
|
<div v-if="purchasedArticles.length === 0 && !loading" class="text-center text-gray-500 py-8">
|
||||||
暂无已购买的文章
|
暂无已购买
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ArticleListItem v-for="article in purchasedArticles" :key="article.id" :article="article"
|
<ArticleListItem v-for="article in purchasedArticles" :key="article.id" :article="article"
|
||||||
@click="showArticle(article.id)" class="mb-4" />
|
@click="showArticle(article.id)" class="mb-4" />
|
||||||
|
|
||||||
|
<div ref="loadingTrigger" class="py-4 text-center" v-show="hasMore || loading">
|
||||||
|
<div v-if="loading"
|
||||||
|
class="animate-spin rounded-full h-8 w-8 border-4 border-gray-200 border-t-blue-600 mx-auto">
|
||||||
|
</div>
|
||||||
|
<div v-else class="h-8">
|
||||||
|
<!-- 空白占位,保持触发器可见 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!hasMore && purchasedArticles.length > 0" class="text-center text-gray-500 py-4">
|
||||||
|
没有更多了
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Remove all styles as they're replaced by Tailwind classes */
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user