feat: update ui

This commit is contained in:
yanghao05
2025-04-16 21:54:27 +08:00
parent 85ece3e899
commit 682a2397d2
17 changed files with 525 additions and 223 deletions

View File

@@ -4,21 +4,18 @@
"": {
"name": "wechat",
"dependencies": {
"@primeuix/themes": "^1.0.3",
"@tailwindcss/vite": "^4.1.4",
"@vueuse/core": "^13.1.0",
"pinia": "^3.0.2",
"primeicons": "^7.0.0",
"primevue": "^4.3.3",
"tailwindcss": "^4.1.4",
"vue": "^3.5.13",
"vue-icons-plus": "^0.1.8",
"vue-router": "^4.5.0",
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"tailwindcss-primeui": "^0.6.1",
"vite": "^6.3.0",
},
},
@@ -84,18 +81,6 @@
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@primeuix/styled": ["@primeuix/styled@0.5.1", "https://registry.npmmirror.com/@primeuix/styled/-/styled-0.5.1.tgz", { "dependencies": { "@primeuix/utils": "^0.5.3" } }, "sha512-5Ftw/KSauDPClQ8F2qCyCUF7cIUEY4yLNikf0rKV7Vsb8zGYNK0dahQe7CChaR6M2Kn+NA2DSBSk76ZXqj6Uog=="],
"@primeuix/styles": ["@primeuix/styles@1.0.3", "https://registry.npmmirror.com/@primeuix/styles/-/styles-1.0.3.tgz", { "dependencies": { "@primeuix/styled": "^0.5.1" } }, "sha512-yHj/Q+fosJ1736Ty5lRbpqhKa9piou+xZPPppNHUDshq0+XhrFwDGggvPGmDAJyUIM+ChM/Nj8lPY/AwTNXAkg=="],
"@primeuix/themes": ["@primeuix/themes@1.0.3", "https://registry.npmmirror.com/@primeuix/themes/-/themes-1.0.3.tgz", { "dependencies": { "@primeuix/styled": "^0.5.1" } }, "sha512-f/1qadrv5TFMHfvtVv4Y9zjrkeDP2BO/cuzbHBO9DYxKL6YBIPT9BjKec2K4Kg8PcfGm6CAvxAvICadJSWejRw=="],
"@primeuix/utils": ["@primeuix/utils@0.5.3", "https://registry.npmmirror.com/@primeuix/utils/-/utils-0.5.3.tgz", {}, "sha512-7SGh7734wcF1/uK6RzO6Z6CBjGQ97GDHfpyl2F1G/c7R0z9hkT/V72ypDo82AWcCS7Ta07oIjDpOCTkSVZuEGQ=="],
"@primevue/core": ["@primevue/core@4.3.3", "https://registry.npmmirror.com/@primevue/core/-/core-4.3.3.tgz", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/utils": "^0.5.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-kSkN5oourG7eueoFPIqiNX3oDT/f0I5IRK3uOY/ytz+VzTZp5yuaCN0Nt42ZQpVXjDxMxDvUhIdaXVrjr58NhQ=="],
"@primevue/icons": ["@primevue/icons@4.3.3", "https://registry.npmmirror.com/@primevue/icons/-/icons-4.3.3.tgz", { "dependencies": { "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.3" } }, "sha512-ouQaxHyeFB6MSfEGGbjaK5Qv9efS1xZGetZoU5jcPm090MSYLFtroP1CuK3lZZAQals06TZ6T6qcoNukSHpK5w=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="],
@@ -286,10 +271,6 @@
"postcss-value-parser": ["postcss-value-parser@4.2.0", "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"primeicons": ["primeicons@7.0.0", "https://registry.npmmirror.com/primeicons/-/primeicons-7.0.0.tgz", {}, "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="],
"primevue": ["primevue@4.3.3", "https://registry.npmmirror.com/primevue/-/primevue-4.3.3.tgz", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/styles": "^1.0.0", "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.3", "@primevue/icons": "4.3.3" } }, "sha512-nooYVoEz5CdP3EhUkD6c3qTdRmpLHZh75fBynkUkl46K8y5rksHTjdSISiDijwTA5STQIOkyqLb+RM+HQ6nC1Q=="],
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rollup": ["rollup@4.40.0", "https://registry.npmmirror.com/rollup/-/rollup-4.40.0.tgz", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="],
@@ -302,8 +283,6 @@
"tailwindcss": ["tailwindcss@4.1.4", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.4.tgz", {}, "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A=="],
"tailwindcss-primeui": ["tailwindcss-primeui@0.6.1", "https://registry.npmmirror.com/tailwindcss-primeui/-/tailwindcss-primeui-0.6.1.tgz", { "peerDependencies": { "tailwindcss": ">=3.1.0" } }, "sha512-T69Rylcrmnt8zy9ik+qZvsLuRIrS9/k6rYJSIgZ1trnbEzGDDQSCIdmfyZknevqiHwpSJHSmQ9XT2C+S/hJY4A=="],
"tapable": ["tapable@2.2.1", "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
"tinyglobby": ["tinyglobby@0.2.12", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.12.tgz", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
@@ -314,6 +293,8 @@
"vue": ["vue@3.5.13", "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", "@vue/runtime-dom": "3.5.13", "@vue/server-renderer": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ=="],
"vue-icons-plus": ["vue-icons-plus@0.1.8", "https://registry.npmmirror.com/vue-icons-plus/-/vue-icons-plus-0.1.8.tgz", { "peerDependencies": { "vue": ">=2.7.0" } }, "sha512-Xc4hDsD/oP9waSUf44nSaFBhUPo+QkpKclx0S7//5BRACpXymctbit02epek0VRW6nb81pR486XmxPP/ofm2yQ=="],
"vue-router": ["vue-router@4.5.0", "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "https://registry.npmmirror.com/@emnapi/core/-/core-1.4.3.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],

View File

@@ -1,29 +1,26 @@
{
"name": "wechat",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@primeuix/themes": "^1.0.3",
"@tailwindcss/vite": "^4.1.4",
"@vueuse/core": "^13.1.0",
"pinia": "^3.0.2",
"primeicons": "^7.0.0",
"primevue": "^4.3.3",
"tailwindcss": "^4.1.4",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"tailwindcss-primeui": "^0.6.1",
"vite": "^6.3.0"
}
}
"name": "wechat",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.4",
"@vueuse/core": "^13.1.0",
"pinia": "^3.0.2",
"tailwindcss": "^4.1.4",
"vue": "^3.5.13",
"vue-icons-plus": "^0.1.8",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"vite": "^6.3.0"
}
}

View File

@@ -0,0 +1,65 @@
<script setup>
import { computed, defineProps } from 'vue'
import { AiOutlineEye, AiOutlineLike } from 'vue-icons-plus/ai'
const props = defineProps({
article: {
type: Object,
required: true
}
})
const formattedDate = computed(() => {
return new Date(props.article.created_at).toLocaleDateString()
})
const discountPrice = computed(() => {
return (props.article.price * props.article.discount / 100).toFixed(2)
})
const mediaTypes = computed(() => {
return [...new Set(props.article.assets.map(asset => asset.type))]
})
</script>
<template>
<div class="flex gap-4 p-4 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer">
<!-- 缩略图占位 -->
<div class="w-24 h-24 flex-shrink-0 bg-gray-100 rounded-lg"></div>
<div class="flex-1 flex flex-col">
<h3 class="text-lg font-medium mb-1">{{ article.title }}</h3>
<p class="text-gray-600 text-sm line-clamp-2 mb-2">{{ article.description }}</p>
<div class="flex flex-wrap gap-2 mb-2">
<span v-for="tag in article.tags" :key="tag"
class="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded-full">
{{ tag }}
</span>
<span v-for="type in mediaTypes" :key="type"
class="px-2 py-0.5 text-xs bg-blue-100 text-blue-600 rounded-full">
{{ type }}
</span>
</div>
<div class="flex items-center justify-between mt-auto text-sm">
<div class="flex items-center gap-3 text-gray-500">
<span>{{ formattedDate }}</span>
<span class="flex items-center gap-1">
<AiOutlineEye class="w-4 h-4" />
{{ article.views }}
</span>
<span class="flex items-center gap-1">
<AiOutlineLike class="w-4 h-4" />
{{ article.likes }}
</span>
</div>
<div class="flex items-center gap-2">
<span class="text-lg font-bold text-red-500">¥{{ discountPrice }}</span>
<span class="text-sm line-through text-gray-400">¥{{ article.price }}</span>
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,39 +0,0 @@
<script setup>
import { ref } from 'vue';
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank">create-vue</a>, the official Vue + Vite
starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support" target="_blank">Vue Docs Scaling up
Guide</a>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -1,25 +1,26 @@
<script setup>
import { ref } from 'vue'
import { AiOutlineHome, AiOutlineShoppingCart, AiOutlineUser } from 'vue-icons-plus/ai'
import { useRouter } from 'vue-router'
const router = useRouter()
const activeTab = ref(0)
const tabs = [
{ label: '列表', route: '/' },
{ label: '已购买', route: '/purchased' },
{ label: '我的', route: '/profile' }
{ label: '列表', route: '/', icon: AiOutlineHome },
{ label: '已购买', route: '/purchased', icon: AiOutlineShoppingCart },
{ label: '我的', route: '/profile', icon: AiOutlineUser }
]
const switchTab = (index) => {
const switchTab = (index, route) => {
activeTab.value = index
router.push(tabs[index].route)
router.push(route)
}
</script>
<template>
<div class="layout">
<div class="content">
<div class="h-screen flex flex-col bg-gray-50">
<div class="flex-1 overflow-hidden">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
@@ -27,23 +28,22 @@ const switchTab = (index) => {
</router-view>
</div>
<TabMenu :model="tabs" :activeIndex="activeTab" @tab-change="switchTab" class="bottom-tabs" />
<nav class="flex-none bg-white border-t border-gray-200">
<div class="flex justify-around items-center h-14">
<button v-for="(tab, index) in tabs" :key="index"
class="flex flex-col items-center justify-center w-full h-full py-1 focus:outline-none" :class="[
activeTab === index
? 'text-blue-600'
: 'text-gray-600 hover:text-blue-600 active:text-blue-700'
]" @click="switchTab(index, tab.route)">
<component :is="tab.icon" class="w-6 h-6" />
<span class="mt-1 text-xs">{{ tab.label }}</span>
</button>
</div>
</nav>
</div>
</template>
<style scoped>
.layout {
display: flex;
flex-direction: column;
height: 100vh;
}
.content {
flex: 1;
overflow-y: auto;
}
.bottom-tabs {
border-top: 1px solid #eee;
}
/* Remove all existing styles */
</style>

View File

@@ -1,14 +1,7 @@
import Aura from '@primeuix/themes/aura';
import { createPinia } from 'pinia';
import PrimeVue from 'primevue/config';
import ConfirmationService from 'primevue/confirmationservice';
import ToastService from 'primevue/toastservice';
import { createApp } from 'vue';
import App from './App.vue';
import { router } from './router.js';
// Import only the required PrimeVue styles
import 'primeicons/primeicons.css'; // Icons
import './style.css';
@@ -17,24 +10,5 @@ const app = createApp(App)
const pinia = createPinia()
app.use(pinia);
app.use(router);
app.use(PrimeVue, {
theme: {
preset: Aura,
},
ripple: true,
options: {
darkModeSelector: '.my-app-dark',
}
})
// Register the ConfirmationService to fix the error
app.use(ConfirmationService);
// Register the ToastService
app.use(ToastService);
// Remove global component registrations
app.mount('#app');

View File

@@ -24,7 +24,7 @@ const routes = [
]
},
{
path: '/article/:id',
path: '/posts/:id',
name: 'article-detail',
component: () => import('@/views/ArticleDetail.vue')
}

View File

@@ -14,10 +14,11 @@ export const useArticleStore = defineStore('article', {
if (this.loading || !this.hasMore) return
this.loading = true
try {
const response = await fetch(`/api/articles?page=${this.page}&q=${this.searchQuery}`)
const response = await fetch(`/api/posts?page=${this.page}&q=${this.searchQuery}`)
const data = await response.json()
this.articles = [...this.articles, ...data.items]
this.hasMore = data.hasMore
const items = data.items || []
this.articles = [...this.articles, ...items]
this.hasMore = items.length > 0 // 只有当返回的列表为空时,才设置 hasMore 为 false
this.page++
} finally {
this.loading = false

View File

@@ -0,0 +1,297 @@
{
"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
}
]
}
]
}

View File

@@ -1,4 +1 @@
@import "tailwindcss";
@import "tailwindcss-primeui";
@custom-variant dark (&:where(.my-app-dark, .my-app-dark *));
@import "tailwindcss";

View File

@@ -14,14 +14,31 @@ onMounted(async () => {
</script>
<template>
<div class="article-detail p-3">
<Button icon="pi pi-arrow-left" @click="router.back()" class="p-button-text mb-3" />
<div class="min-h-screen bg-gray-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()"
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"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<h2 class="text-lg font-medium">{{ article?.title || '文章详情' }}</h2>
</header>
<div v-if="article">
<h1>{{ article.title }}</h1>
<div class="content mt-3">
{{ article.content }}
<main class="pt-14 px-4">
<div v-if="article" class="py-4">
<div class="prose max-w-none">
{{ article.content }}
</div>
</div>
</div>
<div v-else class="flex justify-center items-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-4 border-gray-200 border-t-blue-600"></div>
</div>
</main>
</div>
</template>
<style scoped>
/* 可以移除所有样式,因为都使用了 Tailwind 类 */
</style>

View File

@@ -1,7 +1,8 @@
<script setup>
import ArticleListItem from '@/components/ArticleListItem.vue'
import { useArticleStore } from '@/stores/article'
import { useScroll } from '@vueuse/core'; // Changed to useScroll as a simpler alternative
import { onMounted, ref, watch } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
@@ -9,29 +10,36 @@ const store = useArticleStore()
const searchInput = ref('')
const loadingTrigger = ref(null)
const el = ref(null)
const { y, isScrolling } = useScroll(el)
watch(y, (newY) => {
if (!store.loading && !isScrolling && newY > 0) {
const scrollHeight = el.value.scrollHeight
const scrollTop = newY
const clientHeight = el.value.clientHeight
if (scrollHeight - scrollTop - clientHeight < 50) {
// 优化 Intersection Observer 配置
useIntersectionObserver(
loadingTrigger,
([{ isIntersecting }]) => {
console.log('Intersection state:', { isIntersecting, loading: store.loading, hasMore: store.hasMore })
if (isIntersecting && !store.loading && store.hasMore) {
console.log('Fetching more articles...')
store.fetchArticles()
}
},
{
threshold: 0,
rootMargin: '100px' // 提前 100px 触发加载
}
})
)
const showArticle = (id) => {
router.push(`/article/${id}`)
router.push(`/posts/${id}`)
}
const handleSearch = () => {
store.setSearchQuery(searchInput.value)
}
const handleKeyup = (e) => {
if (e.key === 'Enter') {
handleSearch()
}
}
onMounted(() => {
if (store.articles.length === 0) {
store.fetchArticles()
@@ -40,21 +48,36 @@ onMounted(() => {
</script>
<template>
<div class="article-list" ref="el">
<div class="search-bar p-2">
<InputText v-model="searchInput" placeholder="搜索文章" class="w-full" />
<Button @click="handleSearch">搜索</Button>
<div class="h-full flex flex-col">
<div class="flex-none bg-white border-b border-gray-200 z-50">
<div class="p-4">
<input type="search" v-model="searchInput" @keyup="handleKeyup" placeholder="搜索文章"
class="w-full px-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div>
</div>
<div class="articles p-2">
<Card v-for="article in store.articles" :key="article.id" class="mb-2 article-card"
@click="showArticle(article.id)">
<template #title>{{ article.title }}</template>
<template #content>{{ article.summary }}</template>
</Card>
<div class="flex-1 overflow-y-auto">
<div class="p-4">
<div v-if="store.articles.length === 0 && !store.loading" class="text-center text-gray-500 py-8">
暂无文章
</div>
<div ref="loadingTrigger" v-show="store.hasMore">
<ProgressSpinner v-if="store.loading" />
<ArticleListItem v-for="article in store.articles" :key="article.id" :article="article"
@click="showArticle(article.id)" class="mb-4" />
<!-- 优化加载触发器位置和显示 -->
<div ref="loadingTrigger" class="py-4 text-center" v-show="store.hasMore || store.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">
</div>
<div v-else class="h-8">
<!-- 空白占位保持触发器可见 -->
</div>
</div>
<div v-if="!store.hasMore && store.articles.length > 0" class="text-center text-gray-500 py-4">
没有更多文章了
</div>
</div>
</div>
</div>

View File

@@ -16,33 +16,21 @@ onMounted(async () => {
</script>
<template>
<div class="purchased-articles">
<h2>已购买的文章</h2>
<div class="articles-list">
<div v-if="purchasedArticles.length === 0" class="empty-state">
<div class="p-4">
<h2 class="text-xl font-medium mb-4">已购买的文章</h2>
<div class="space-y-4">
<div v-if="purchasedArticles.length === 0" class="py-12 text-center text-gray-500">
暂无已购买的文章
</div>
<Card v-else v-for="article in purchasedArticles" :key="article.id" class="article-card"
@click="showArticle(article.id)">
<template #title>{{ article.title }}</template>
<template #content>{{ article.summary }}</template>
</Card>
<div v-else v-for="article in purchasedArticles" :key="article.id" @click="showArticle(article.id)"
class="bg-white rounded-lg shadow-sm p-4 hover:shadow-md transition-shadow cursor-pointer">
<h3 class="text-lg font-medium mb-2">{{ article.title }}</h3>
<p class="text-gray-600">{{ article.summary }}</p>
</div>
</div>
</div>
</template>
<style scoped>
.purchased-articles {
padding: 1rem;
}
.empty-state {
text-align: center;
padding: 2rem;
color: #666;
}
.article-card {
margin-bottom: 1rem;
}
/* Remove all styles as they're replaced by Tailwind classes */
</style>

View File

@@ -9,21 +9,21 @@ const userInfo = ref({
</script>
<template>
<div class="user-profile p-3">
<Card>
<template #header>
<div class="flex align-items-center gap-3">
<Avatar :image="userInfo.avatar" size="large" />
<h3>{{ userInfo.name }}</h3>
<div class="p-4">
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="flex items-center gap-4 p-4 border-b border-gray-100">
<div class="w-16 h-16 rounded-full bg-gray-200 overflow-hidden">
<img v-if="userInfo.avatar" :src="userInfo.avatar" alt="头像" class="w-full h-full object-cover">
</div>
</template>
<template #content>
<div class="grid">
<Button label="我的收藏" class="p-button-text" />
<Button label="我的点赞" class="p-button-text" />
<Button label="订单列表" class="p-button-text" />
</div>
</template>
</Card>
<h3 class="text-xl font-medium">{{ userInfo.name }}</h3>
</div>
<div class="grid grid-cols-3 gap-4 p-4">
<button v-for="(item, index) in ['我的收藏', '我的点赞', '订单列表']" :key="index"
class="py-3 text-center text-gray-700 hover:bg-gray-50 active:bg-gray-100 rounded-lg transition-colors">
{{ item }}
</button>
</div>
</div>
</div>
</template>