feat: init wechat
This commit is contained in:
35
frontend/wechat/src/App.vue
Normal file
35
frontend/wechat/src/App.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
// 不需要导入任何组件,路由会自动处理组件的加载
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Reset default styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Mobile-first responsive design */
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
frontend/wechat/src/assets/vue.svg
Normal file
1
frontend/wechat/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
39
frontend/wechat/src/components/HelloWorld.vue
Normal file
39
frontend/wechat/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<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>
|
||||
49
frontend/wechat/src/layouts/MainLayout.vue
Normal file
49
frontend/wechat/src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const activeTab = ref(0)
|
||||
|
||||
const tabs = [
|
||||
{ label: '列表', route: '/' },
|
||||
{ label: '已购买', route: '/purchased' },
|
||||
{ label: '我的', route: '/profile' }
|
||||
]
|
||||
|
||||
const switchTab = (index) => {
|
||||
activeTab.value = index
|
||||
router.push(tabs[index].route)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
<TabMenu :model="tabs" :activeIndex="activeTab" @tab-change="switchTab" class="bottom-tabs" />
|
||||
</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;
|
||||
}
|
||||
</style>
|
||||
22
frontend/wechat/src/main.js
Normal file
22
frontend/wechat/src/main.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Aura from '@primeuix/themes/aura'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { router } from './router'
|
||||
|
||||
// Import only the required PrimeVue styles
|
||||
import 'primeicons/primeicons.css'; // Icons
|
||||
import './style.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Aura
|
||||
}
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
36
frontend/wechat/src/router/index.js
Normal file
36
frontend/wechat/src/router/index.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'home',
|
||||
component: () => import('@/views/ArticleList.vue'),
|
||||
meta: { keepAlive: true }
|
||||
},
|
||||
{
|
||||
path: 'purchased',
|
||||
name: 'purchased',
|
||||
component: () => import('@/views/PurchasedArticles.vue')
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'profile',
|
||||
component: () => import('@/views/UserProfile.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/article/:id',
|
||||
name: 'article-detail',
|
||||
component: () => import('@/views/ArticleDetail.vue')
|
||||
}
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
35
frontend/wechat/src/stores/article.js
Normal file
35
frontend/wechat/src/stores/article.js
Normal file
@@ -0,0 +1,35 @@
|
||||
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/articles?page=${this.page}&q=${this.searchQuery}`)
|
||||
const data = await response.json()
|
||||
this.articles = [...this.articles, ...data.items]
|
||||
this.hasMore = data.hasMore
|
||||
this.page++
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
setSearchQuery(query) {
|
||||
this.searchQuery = query
|
||||
this.articles = []
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.fetchArticles()
|
||||
}
|
||||
}
|
||||
})
|
||||
5
frontend/wechat/src/style.css
Normal file
5
frontend/wechat/src/style.css
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss-primeui";
|
||||
|
||||
@custom-variant dark (&:where(.my-app-dark, .my-app-dark *));
|
||||
27
frontend/wechat/src/views/ArticleDetail.vue
Normal file
27
frontend/wechat/src/views/ArticleDetail.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const article = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
// TODO: Implement API call to fetch article details
|
||||
const { id } = route.params
|
||||
article.value = { id, title: '文章标题', content: '文章内容' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="article-detail p-3">
|
||||
<Button icon="pi pi-arrow-left" @click="router.back()" class="p-button-text mb-3" />
|
||||
|
||||
<div v-if="article">
|
||||
<h1>{{ article.title }}</h1>
|
||||
<div class="content mt-3">
|
||||
{{ article.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
61
frontend/wechat/src/views/ArticleList.vue
Normal file
61
frontend/wechat/src/views/ArticleList.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import { useArticleStore } from '@/stores/article'
|
||||
import { useScroll } from '@vueuse/core'; // Changed to useScroll as a simpler alternative
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
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) {
|
||||
store.fetchArticles()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const showArticle = (id) => {
|
||||
router.push(`/article/${id}`)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
store.setSearchQuery(searchInput.value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (store.articles.length === 0) {
|
||||
store.fetchArticles()
|
||||
}
|
||||
})
|
||||
</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>
|
||||
|
||||
<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 ref="loadingTrigger" v-show="store.hasMore">
|
||||
<ProgressSpinner v-if="store.loading" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
48
frontend/wechat/src/views/PurchasedArticles.vue
Normal file
48
frontend/wechat/src/views/PurchasedArticles.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const purchasedArticles = ref([])
|
||||
|
||||
const showArticle = (id) => {
|
||||
router.push(`/article/${id}`)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// TODO: Implement API call to fetch purchased articles
|
||||
purchasedArticles.value = []
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="purchased-articles">
|
||||
<h2>已购买的文章</h2>
|
||||
<div class="articles-list">
|
||||
<div v-if="purchasedArticles.length === 0" class="empty-state">
|
||||
暂无已购买的文章
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.purchased-articles {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.article-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
29
frontend/wechat/src/views/UserProfile.vue
Normal file
29
frontend/wechat/src/views/UserProfile.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const userInfo = ref({
|
||||
name: '用户名',
|
||||
avatar: '',
|
||||
// Add more user info as needed
|
||||
})
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user