feat(auth): implement OTP login flow with toast notifications
feat(content): enhance detail view with dynamic content and comments feat(order): add polling for payment status in the payment view feat(user): update dashboard to display wallet and recent orders feat(user): improve orders view with dynamic order fetching and status mapping feat(api): create API modules for auth, content, order, user, and common functionalities refactor(request): implement a centralized request utility for API calls
This commit is contained in:
6
frontend/portal/src/api/auth.js
Normal file
6
frontend/portal/src/api/auth.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { request } from '../utils/request';
|
||||
|
||||
export const authApi = {
|
||||
sendOTP: (phone) => request('/auth/otp', { method: 'POST', body: { phone } }),
|
||||
login: (phone, otp) => request('/auth/login', { method: 'POST', body: { phone, otp } }),
|
||||
};
|
||||
10
frontend/portal/src/api/common.js
Normal file
10
frontend/portal/src/api/common.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { request } from '../utils/request';
|
||||
|
||||
export const commonApi = {
|
||||
upload: (file, type) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('type', type);
|
||||
return request('/upload', { method: 'POST', body: formData });
|
||||
},
|
||||
};
|
||||
13
frontend/portal/src/api/content.js
Normal file
13
frontend/portal/src/api/content.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { request } from '../utils/request';
|
||||
|
||||
export const contentApi = {
|
||||
list: (params) => {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return request(`/contents?${qs}`);
|
||||
},
|
||||
get: (id) => request(`/contents/${id}`),
|
||||
listComments: (id, page) => request(`/contents/${id}/comments?page=${page || 1}`),
|
||||
createComment: (id, data) => request(`/contents/${id}/comments`, { method: 'POST', body: data }),
|
||||
likeComment: (id) => request(`/comments/${id}/like`, { method: 'POST' }),
|
||||
listTopics: () => request('/topics'),
|
||||
};
|
||||
24
frontend/portal/src/api/creator.js
Normal file
24
frontend/portal/src/api/creator.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { request } from '../utils/request';
|
||||
|
||||
export const creatorApi = {
|
||||
apply: (data) => request('/creator/apply', { method: 'POST', body: data }),
|
||||
getDashboard: () => request('/creator/dashboard'),
|
||||
listContents: (params) => {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return request(`/creator/contents?${qs}`);
|
||||
},
|
||||
createContent: (data) => request('/creator/contents', { method: 'POST', body: data }),
|
||||
updateContent: (id, data) => request(`/creator/contents/${id}`, { method: 'PUT', body: data }),
|
||||
deleteContent: (id) => request(`/creator/contents/${id}`, { method: 'DELETE' }),
|
||||
listOrders: (params) => {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return request(`/creator/orders?${qs}`);
|
||||
},
|
||||
refundOrder: (id, data) => request(`/creator/orders/${id}/refund`, { method: 'POST', body: data }),
|
||||
getSettings: () => request('/creator/settings'),
|
||||
updateSettings: (data) => request('/creator/settings', { method: 'PUT', body: data }),
|
||||
listPayoutAccounts: () => request('/creator/payout-accounts'),
|
||||
addPayoutAccount: (data) => request('/creator/payout-accounts', { method: 'POST', body: data }),
|
||||
removePayoutAccount: (id) => request(`/creator/payout-accounts?id=${id}`, { method: 'DELETE' }),
|
||||
withdraw: (data) => request('/creator/withdraw', { method: 'POST', body: data }),
|
||||
};
|
||||
7
frontend/portal/src/api/order.js
Normal file
7
frontend/portal/src/api/order.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { request } from '../utils/request';
|
||||
|
||||
export const orderApi = {
|
||||
create: (data) => request('/orders', { method: 'POST', body: data }),
|
||||
pay: (id, data) => request(`/orders/${id}/pay`, { method: 'POST', body: data }),
|
||||
status: (id) => request(`/orders/${id}/status`),
|
||||
};
|
||||
21
frontend/portal/src/api/user.js
Normal file
21
frontend/portal/src/api/user.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { request } from '../utils/request';
|
||||
|
||||
export const userApi = {
|
||||
getMe: () => request('/me'),
|
||||
updateMe: (data) => request('/me', { method: 'PUT', body: data }),
|
||||
realName: (data) => request('/me/realname', { method: 'POST', body: data }),
|
||||
getWallet: () => request('/me/wallet'),
|
||||
recharge: (data) => request('/me/wallet/recharge', { method: 'POST', body: data }),
|
||||
getOrders: (status) => request(`/me/orders?status=${status || 'all'}`),
|
||||
getOrder: (id) => request(`/me/orders/${id}`),
|
||||
getLibrary: () => request('/me/library'),
|
||||
getFavorites: () => request('/me/favorites'),
|
||||
addFavorite: (contentId) => request(`/me/favorites?content_id=${contentId}`, { method: 'POST' }),
|
||||
removeFavorite: (contentId) => request(`/me/favorites/${contentId}`, { method: 'DELETE' }),
|
||||
getLikes: () => request('/me/likes'),
|
||||
addLike: (contentId) => request(`/me/likes?content_id=${contentId}`, { method: 'POST' }),
|
||||
removeLike: (contentId) => request(`/me/likes/${contentId}`, { method: 'DELETE' }),
|
||||
getNotifications: (type, page) => request(`/me/notifications?type=${type || 'all'}&page=${page || 1}`),
|
||||
getFollowing: () => request('/me/following'),
|
||||
getCoupons: (status) => request(`/me/coupons?status=${status || 'unused'}`),
|
||||
};
|
||||
50
frontend/portal/src/utils/request.js
Normal file
50
frontend/portal/src/utils/request.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// Simple Fetch Wrapper
|
||||
const BASE_URL = '/v1';
|
||||
|
||||
export async function request(endpoint, options = {}) {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (options.body && !(options.body instanceof FormData)) {
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
if (options.body instanceof FormData) {
|
||||
delete headers['Content-Type']; // Let browser set boundary
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers
|
||||
});
|
||||
|
||||
const contentType = res.headers.get("content-type");
|
||||
let data;
|
||||
if (contentType && contentType.indexOf("application/json") !== -1) {
|
||||
data = await res.json();
|
||||
} else {
|
||||
data = await res.text();
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
// Handle errorx response { code, message, error_id }
|
||||
const errorMsg = data.message || `Request failed with status ${res.status}`;
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
} catch (err) {
|
||||
console.error('API Request Error:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -45,37 +45,37 @@
|
||||
</div>
|
||||
<div class="relative w-full sm:w-64">
|
||||
<i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input type="text" placeholder="在结果中搜索..." class="w-full h-9 pl-9 pr-4 rounded-lg bg-slate-50 border border-slate-200 text-sm focus:bg-white focus:border-primary-500 outline-none transition-all">
|
||||
<input v-model="keyword" @keyup.enter="fetchContents" type="text" placeholder="在结果中搜索..." class="w-full h-9 pl-9 pr-4 rounded-lg bg-slate-50 border border-slate-200 text-sm focus:bg-white focus:border-primary-500 outline-none transition-all">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div v-for="i in 12" :key="i" class="bg-white rounded-xl border border-slate-100 overflow-hidden hover:shadow-lg transition-all group cursor-pointer active:scale-[0.99]" @click="$router.push(`/contents/${i}`)">
|
||||
<div v-for="item in contents" :key="item.id" class="bg-white rounded-xl border border-slate-100 overflow-hidden hover:shadow-lg transition-all group cursor-pointer active:scale-[0.99]" @click="$router.push(`/contents/${item.id}`)">
|
||||
<!-- Cover -->
|
||||
<div class="aspect-video bg-slate-100 relative">
|
||||
<img :src="`https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60`" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105">
|
||||
<img :src="item.cover || `https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60`" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105">
|
||||
<div class="absolute bottom-2 left-2 px-1.5 py-0.5 bg-black/60 text-white text-xs rounded flex items-center gap-1">
|
||||
<i class="pi pi-play-circle"></i> 12:40
|
||||
<i class="pi pi-play-circle"></i> {{ item.duration || '00:00' }}
|
||||
</div>
|
||||
<span v-if="i % 3 === 0" class="absolute top-2 right-2 px-1.5 py-0.5 bg-green-500 text-white text-xs font-bold rounded">限免</span>
|
||||
<span v-if="item.price === 0" class="absolute top-2 right-2 px-1.5 py-0.5 bg-green-500 text-white text-xs font-bold rounded">免费</span>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs text-slate-500 border border-slate-200 px-1 rounded">[京剧]</span>
|
||||
<h3 class="font-bold text-slate-900 line-clamp-1 group-hover:text-primary-600 transition-colors">《锁麟囊》选段深度解析</h3>
|
||||
<span class="text-xs text-slate-500 border border-slate-200 px-1 rounded">[{{ item.genre || '未知' }}]</span>
|
||||
<h3 class="font-bold text-slate-900 line-clamp-1 group-hover:text-primary-600 transition-colors">{{ item.title }}</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-slate-500 mb-3">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Master1" class="w-5 h-5 rounded-full">
|
||||
<span>梅派传人小林</span>
|
||||
<img :src="item.author_avatar || 'https://api.dicebear.com/7.x/avataaars/svg?seed=' + item.author_id" class="w-5 h-5 rounded-full">
|
||||
<span>{{ item.author_name || 'Unknown' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-slate-400">1.2w 阅读</span>
|
||||
<span v-if="i % 3 === 0" class="text-sm font-bold text-green-600">免费</span>
|
||||
<span v-else class="text-sm font-bold text-red-600">¥ 9.90</span>
|
||||
<span class="text-xs text-slate-400">{{ item.views }} 阅读</span>
|
||||
<span v-if="item.price === 0" class="text-sm font-bold text-green-600">免费</span>
|
||||
<span v-else class="text-sm font-bold text-red-600">¥ {{ item.price }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-12 flex justify-center">
|
||||
<button class="px-8 py-3 bg-white border border-slate-200 rounded-full text-slate-600 hover:bg-slate-50 hover:text-primary-600 font-medium transition-all shadow-sm cursor-pointer active:scale-95">
|
||||
<button @click="loadMore" class="px-8 py-3 bg-white border border-slate-200 rounded-full text-slate-600 hover:bg-slate-50 hover:text-primary-600 font-medium transition-all shadow-sm cursor-pointer active:scale-95">
|
||||
加载更多
|
||||
</button>
|
||||
</div>
|
||||
@@ -91,11 +91,50 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { contentApi } from '../api/content';
|
||||
|
||||
const selectedGenre = ref('全部');
|
||||
const selectedPrice = ref('all');
|
||||
const sort = ref('latest');
|
||||
const keyword = ref('');
|
||||
const contents = ref([]);
|
||||
const page = ref(1);
|
||||
|
||||
const fetchContents = async (append = false) => {
|
||||
const params = {
|
||||
page: page.value,
|
||||
limit: 12,
|
||||
sort: sort.value
|
||||
};
|
||||
if (selectedGenre.value !== '全部') params.genre = selectedGenre.value;
|
||||
if (keyword.value) params.keyword = keyword.value;
|
||||
|
||||
try {
|
||||
const res = await contentApi.list(params);
|
||||
if (append) {
|
||||
contents.value = [...contents.value, ...(res.items || [])];
|
||||
} else {
|
||||
contents.value = res.items || [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMore = () => {
|
||||
page.value++;
|
||||
fetchContents(true);
|
||||
};
|
||||
|
||||
watch([selectedGenre, sort], () => {
|
||||
page.value = 1;
|
||||
fetchContents();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchContents();
|
||||
});
|
||||
|
||||
const filters = {
|
||||
genres: ['全部', '京剧', '昆曲', '越剧', '黄梅戏', '豫剧', '评剧', '秦腔', '河北梆子'],
|
||||
|
||||
@@ -59,91 +59,40 @@
|
||||
<!-- Main Feed (Left 9) -->
|
||||
<div class="col-span-12 lg:col-span-8 xl:col-span-9 space-y-6">
|
||||
|
||||
<!-- Card Variant 1: Single Image (Right) -->
|
||||
<router-link to="/contents/1"
|
||||
class="block bg-white rounded-2xl shadow-sm border border-slate-100 p-6 hover:shadow-xl hover:border-primary-100 transition-all duration-300 group cursor-pointer active:scale-[0.99]">
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1 min-w-0 flex flex-col">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-bold bg-red-600 text-white">置顶</span>
|
||||
<span class="text-xs font-bold text-red-700 bg-red-50 border border-red-100 px-2 py-0.5 rounded-full">京剧</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-slate-900 mb-3 leading-snug group-hover:text-primary-600 transition-colors">《锁麟囊》选段:春秋亭外风雨暴 (张火丁亲授版)</h3>
|
||||
<p class="text-base text-slate-500 line-clamp-2 mb-6 leading-relaxed">张火丁教授亲自讲解程派发音技巧,深度剖析《锁麟囊》中春秋亭一折的唱腔设计与情感表达。</p>
|
||||
<div class="mt-auto flex items-center justify-between">
|
||||
<div class="flex items-center gap-3 text-sm text-slate-500">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang" class="w-7 h-7 rounded-full ring-2 ring-white">
|
||||
<span class="font-medium text-slate-700">张火丁工作室</span>
|
||||
<span class="text-slate-300">|</span>
|
||||
<span>青衣</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm text-slate-400 font-medium"><i class="pi pi-eye mr-1"></i> 1.2万</span>
|
||||
<span class="text-xl font-bold text-red-600">¥ 9.90</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-[280px] h-[157px] flex-shrink-0 rounded-xl overflow-hidden relative bg-slate-100 hidden sm:block shadow-inner">
|
||||
<img src="https://images.unsplash.com/photo-1576014131795-d44019d02374?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110">
|
||||
<div class="absolute inset-0 bg-black/10 group-hover:bg-black/0 transition-colors"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i class="pi pi-play-circle text-5xl text-white opacity-0 group-hover:opacity-100 transition-all scale-75 group-hover:scale-100 drop-shadow-lg"></i>
|
||||
</div>
|
||||
<span class="absolute bottom-3 right-3 px-2 py-1 bg-black/70 backdrop-blur-sm text-white text-xs font-bold rounded-lg">15:30</span>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<router-link v-for="item in contents" :key="item.id" :to="`/contents/${item.id}`"
|
||||
class="block bg-white rounded-2xl shadow-sm border border-slate-100 p-6 hover:shadow-xl hover:border-primary-100 transition-all duration-300 group cursor-pointer active:scale-[0.99]">
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1 min-w-0 flex flex-col">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span v-if="item.price === 0" class="px-2 py-0.5 rounded text-xs font-bold bg-green-500 text-white">免费</span>
|
||||
<span v-else class="px-2 py-0.5 rounded text-xs font-bold bg-red-600 text-white">付费</span>
|
||||
<span class="text-xs font-bold text-slate-700 bg-slate-50 border border-slate-100 px-2 py-0.5 rounded-full">{{ item.genre }}</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-slate-900 mb-3 leading-snug group-hover:text-primary-600 transition-colors">{{ item.title }}</h3>
|
||||
<p class="text-base text-slate-500 line-clamp-2 mb-6 leading-relaxed">{{ item.description || item.title }}</p>
|
||||
<div class="mt-auto flex items-center justify-between">
|
||||
<div class="flex items-center gap-3 text-sm text-slate-500">
|
||||
<img :src="item.author_avatar || 'https://api.dicebear.com/7.x/avataaars/svg?seed=' + item.author_id" class="w-7 h-7 rounded-full ring-2 ring-white">
|
||||
<span class="font-medium text-slate-700">{{ item.author_name || 'Unknown' }}</span>
|
||||
<span class="text-slate-300">|</span>
|
||||
<span>{{ item.create_time || '刚刚' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm text-slate-400 font-medium"><i class="pi pi-eye mr-1"></i> {{ item.views }}</span>
|
||||
<span class="text-xl font-bold text-red-600">¥ {{ item.price }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.cover" class="w-[280px] h-[157px] flex-shrink-0 rounded-xl overflow-hidden relative bg-slate-100 hidden sm:block shadow-inner">
|
||||
<img :src="item.cover" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110">
|
||||
<div class="absolute inset-0 bg-black/10 group-hover:bg-black/0 transition-colors"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<i class="pi pi-play-circle text-5xl text-white opacity-0 group-hover:opacity-100 transition-all scale-75 group-hover:scale-100 drop-shadow-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<!-- Card Variant 2: No Image (Text Only) -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6 hover:shadow-xl hover:border-primary-100 transition-all duration-300 cursor-pointer active:scale-[0.99] group">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-bold bg-green-500 text-white">限免</span>
|
||||
<span class="text-xs font-bold text-teal-700 bg-teal-50 border border-teal-100 px-2 py-0.5 rounded-full">昆曲</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-slate-900 mb-3 group-hover:text-primary-600 transition-colors">浅谈昆曲《牡丹亭》中的水磨腔艺术特点</h3>
|
||||
<p class="text-base text-slate-500 line-clamp-3 mb-6 leading-relaxed">昆曲之所以被称为“百戏之祖”,其细腻婉转的水磨腔功不可没。本文将从发音、吐字、行腔三个维度,带您领略昆曲的声韵之美...</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3 text-sm text-slate-500">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Li" class="w-7 h-7 rounded-full ring-2 ring-white">
|
||||
<span class="font-medium text-slate-700">梨园小生</span>
|
||||
<span class="text-slate-300">|</span>
|
||||
<span>昨天</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-slate-400 line-through">¥ 5.00</span>
|
||||
<span class="px-3 py-1 bg-green-50 text-green-600 text-sm font-bold rounded-lg border border-green-100">限时免费</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Variant 3: 3 Images -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6 hover:shadow-xl hover:border-primary-100 transition-all duration-300 cursor-pointer active:scale-[0.99] group">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xs font-bold text-purple-700 bg-purple-50 border border-purple-100 px-2 py-0.5 rounded-full">图集</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-slate-900 mb-4 group-hover:text-primary-600 transition-colors">2024 新年京剧晚会后台探班:名角云集</h3>
|
||||
<div class="grid grid-cols-3 gap-3 mb-6">
|
||||
<div class="aspect-[4/3] rounded-xl overflow-hidden bg-slate-100 shadow-inner">
|
||||
<img src="https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110">
|
||||
</div>
|
||||
<div class="aspect-[4/3] rounded-xl overflow-hidden bg-slate-100 shadow-inner">
|
||||
<img src="https://images.unsplash.com/photo-1533174072545-e8d4aa97edf9?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110">
|
||||
</div>
|
||||
<div class="aspect-[4/3] rounded-xl overflow-hidden bg-slate-100 relative shadow-inner">
|
||||
<img src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110">
|
||||
<div class="absolute inset-0 bg-black/40 flex items-center justify-center text-white font-bold text-lg">+9</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3 text-sm text-slate-500">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Photo" class="w-7 h-7 rounded-full ring-2 ring-white">
|
||||
<span class="font-medium text-slate-700">戏曲摄影师老王</span>
|
||||
<span class="text-slate-300">|</span>
|
||||
<span>3天前</span>
|
||||
</div>
|
||||
<span class="text-sm text-slate-400 font-medium"><i class="pi pi-eye mr-1"></i> 8.5k</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Load More -->
|
||||
<div class="pt-4 text-center">
|
||||
<button
|
||||
|
||||
@@ -1,3 +1,43 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Toast from 'primevue/toast';
|
||||
import { authApi } from '../../api/auth';
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const step = ref(1);
|
||||
const phone = ref('');
|
||||
const otpCode = ref('');
|
||||
const agreed = ref(false);
|
||||
|
||||
const getOTP = async () => {
|
||||
if(!agreed.value) return;
|
||||
try {
|
||||
await authApi.sendOTP(phone.value);
|
||||
step.value = 2;
|
||||
toast.add({ severity: 'success', summary: '发送成功', detail: '验证码已发送', life: 3000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '错误', detail: e.message, life: 3000 });
|
||||
}
|
||||
};
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
const res = await authApi.login(phone.value, otpCode.value);
|
||||
localStorage.setItem('token', res.token);
|
||||
localStorage.setItem('user', JSON.stringify(res.user));
|
||||
toast.add({ severity: 'success', summary: '登录成功', detail: '欢迎回来', life: 1000 });
|
||||
setTimeout(() => {
|
||||
router.push('/');
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '登录失败', detail: e.message, life: 3000 });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white rounded-2xl shadow-xl w-full max-w-4xl overflow-hidden flex min-h-[550px]">
|
||||
<!-- Left Brand Area -->
|
||||
@@ -97,33 +137,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const step = ref(1);
|
||||
const phone = ref('');
|
||||
const otpCode = ref('');
|
||||
const agreed = ref(false);
|
||||
|
||||
const getOTP = () => {
|
||||
if(!agreed.value) return;
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
step.value = 2;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const login = () => {
|
||||
// Simulate Login
|
||||
if (otpCode.value.length >= 4) {
|
||||
setTimeout(() => {
|
||||
router.push('/');
|
||||
}, 800);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</template>
|
||||
@@ -9,25 +9,25 @@
|
||||
<div class="mb-8 border-b border-slate-100 pb-8">
|
||||
<div class="flex items-start justify-between gap-4 mb-4">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-slate-900 leading-tight">
|
||||
<span class="text-primary-600 mr-2 text-2xl align-middle">[京剧]</span>
|
||||
《锁麟囊》春秋亭 · 二六
|
||||
<span class="text-primary-600 mr-2 text-2xl align-middle">[{{ content.genre || '未分类' }}]</span>
|
||||
{{ content.title }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Opera Meta -->
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<span class="px-3 py-1 bg-slate-100 text-slate-600 rounded-full text-sm font-bold">行当: 青衣</span>
|
||||
<span class="px-3 py-1 bg-slate-100 text-slate-600 rounded-full text-sm font-bold">定调: F大调</span>
|
||||
<span class="px-3 py-1 bg-slate-100 text-slate-600 rounded-full text-sm font-bold">板式: 二六</span>
|
||||
<div class="flex flex-wrap gap-3 mb-6" v-if="content.meta">
|
||||
<span v-if="content.meta.role" class="px-3 py-1 bg-slate-100 text-slate-600 rounded-full text-sm font-bold">行当: {{ content.meta.role }}</span>
|
||||
<span v-if="content.meta.key" class="px-3 py-1 bg-slate-100 text-slate-600 rounded-full text-sm font-bold">定调: {{ content.meta.key }}</span>
|
||||
<span v-if="content.meta.beat" class="px-3 py-1 bg-slate-100 text-slate-600 rounded-full text-sm font-bold">板式: {{ content.meta.beat }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Author & Tools -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Master1" class="w-10 h-10 rounded-full border border-slate-200">
|
||||
<img :src="content.author_avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${content.author_id}`" class="w-10 h-10 rounded-full border border-slate-200">
|
||||
<div>
|
||||
<div class="font-bold text-slate-900 text-sm">梅派传人小林 <i class="pi pi-check-circle text-blue-500 text-xs"></i></div>
|
||||
<div class="text-xs text-slate-400">发布于 2025-12-24</div>
|
||||
<div class="font-bold text-slate-900 text-sm">{{ content.author_name }} <i class="pi pi-check-circle text-blue-500 text-xs"></i></div>
|
||||
<div class="text-xs text-slate-400">发布于 {{ content.create_time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -43,7 +43,7 @@
|
||||
<!-- Content Render -->
|
||||
<div class="prose prose-slate max-w-none text-slate-800" :style="{ fontSize: fontSize + 'px' }">
|
||||
<!-- Video Player (Trial) -->
|
||||
<div class="not-prose mb-8 rounded-xl overflow-hidden bg-black relative group aspect-video">
|
||||
<div class="not-prose mb-8 rounded-xl overflow-hidden bg-black relative group aspect-video" v-if="content.type === 'video'">
|
||||
<div class="absolute inset-0 flex items-center justify-center text-white" v-if="!isPlaying">
|
||||
<button @click="isPlaying = true" class="w-16 h-16 bg-white/20 hover:bg-white/30 backdrop-blur rounded-full flex items-center justify-center transition-all">
|
||||
<i class="pi pi-play text-3xl ml-1"></i>
|
||||
@@ -63,55 +63,23 @@
|
||||
<span class="text-xs font-mono">01:30 / 05:00</span>
|
||||
</div>
|
||||
<!-- Paywall Overlay -->
|
||||
<div v-if="showPaywall" class="absolute inset-0 bg-black/80 backdrop-blur-sm flex flex-col items-center justify-center text-center p-8 z-20">
|
||||
<div v-if="!content.is_purchased && content.price > 0" class="absolute inset-0 bg-black/80 backdrop-blur-sm flex flex-col items-center justify-center text-center p-8 z-20">
|
||||
<h3 class="text-white text-xl font-bold mb-2">试看已结束</h3>
|
||||
<p class="text-slate-300 text-sm mb-6">购买后可观看完整内容 (包含高清视频 + 完整剧本)</p>
|
||||
<button class="px-8 py-3 bg-primary-600 text-white rounded-full font-bold hover:bg-primary-700 shadow-lg shadow-primary-900/50 transition-transform hover:scale-105 active:scale-95">
|
||||
¥ 9.90 立即解锁
|
||||
¥ {{ content.price }} 立即解锁
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>(白)听薛良一语,一似那金钟猛撞。</p>
|
||||
<p>(唱)春秋亭外风雨暴,何处悲声破寂寥。</p>
|
||||
|
||||
<!-- Script Block -->
|
||||
<div class="pl-6 border-l-4 border-primary-200 bg-slate-50 p-4 rounded-r-lg my-6 not-prose">
|
||||
<div class="flex gap-2">
|
||||
<span class="font-bold text-slate-900 flex-shrink-0">薛湘灵:</span>
|
||||
<span class="text-slate-700 leading-relaxed">
|
||||
隔帘只见一花轿,想必是新婚渡鹊桥。<br>
|
||||
吉日良辰当欢笑,为什么被珠泪抛?<br>
|
||||
此时却又明白了,<br>
|
||||
(白) 世上何尝尽富豪。<br>
|
||||
(唱) 也有饥寒悲怀抱,也有失意痛哭嚎啕。<br>
|
||||
轿内的人儿,命薄如纸,<br>
|
||||
苦煞了严亲,累煞了做娘的。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-html="content.body"></div>
|
||||
|
||||
<!-- Aria Block -->
|
||||
<div class="flex items-center gap-4 p-4 border border-purple-100 bg-purple-50 rounded-xl my-6 not-prose cursor-pointer hover:bg-purple-100 transition-colors">
|
||||
<div class="w-12 h-12 rounded-full bg-purple-600 text-white flex items-center justify-center flex-shrink-0 shadow-sm"><i class="pi pi-play"></i></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-bold text-slate-900 truncate">名家示范:流水板式要点</div>
|
||||
<div class="text-xs text-purple-600 flex gap-2 mt-1 font-mono">
|
||||
<span class="bg-white px-1.5 rounded border border-purple-200">Key: F</span>
|
||||
<span class="bg-white px-1.5 rounded border border-purple-200">Beat: 1/4</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-slate-500 font-mono">00:45</span>
|
||||
</div>
|
||||
|
||||
<p>在这里,程派独特的“嗽音”运用得淋漓尽致,表现了湘灵对贫富无常的深刻感悟...</p>
|
||||
|
||||
<!-- Paywall Mask for Text -->
|
||||
<div class="relative mt-8 pt-20 pb-8 text-center" v-if="!isPurchased">
|
||||
<div class="relative mt-8 pt-20 pb-8 text-center" v-if="!content.is_purchased && content.price > 0">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-white/90 to-white z-10"></div>
|
||||
<div class="relative z-20">
|
||||
<button class="px-10 py-3.5 bg-primary-600 text-white rounded-full font-bold text-lg hover:bg-primary-700 shadow-xl shadow-primary-100 transition-transform hover:scale-105 active:scale-95 flex items-center gap-2 mx-auto">
|
||||
<i class="pi pi-lock"></i> 购买解锁全文 ¥ 9.90
|
||||
<i class="pi pi-lock"></i> 购买解锁全文 ¥ {{ content.price }}
|
||||
</button>
|
||||
<p class="text-xs text-slate-400 mt-3">支持微信 / 支付宝</p>
|
||||
</div>
|
||||
@@ -122,36 +90,20 @@
|
||||
<!-- Comments Section -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-slate-900">全部评论 (24)</h2>
|
||||
<div class="flex text-sm font-bold text-slate-400">
|
||||
<button class="text-slate-900 mr-4">最新</button>
|
||||
<button class="hover:text-slate-900">热门</button>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-slate-900">全部评论 ({{ comments.length }})</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mb-8">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=User" class="w-10 h-10 rounded-full bg-slate-100">
|
||||
<div class="flex-1">
|
||||
<textarea rows="2" class="w-full p-3 rounded-lg border border-slate-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-100 outline-none resize-none transition-colors" placeholder="写下你的想法..."></textarea>
|
||||
<div class="flex justify-end mt-2">
|
||||
<button class="px-6 py-2 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 text-sm">发布</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ... Comment Input ... -->
|
||||
|
||||
<div class="space-y-6">
|
||||
<div v-for="i in 3" :key="i" class="flex gap-4">
|
||||
<img :src="`https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`" class="w-10 h-10 rounded-full bg-slate-100">
|
||||
<div v-for="comment in comments" :key="comment.id" class="flex gap-4">
|
||||
<img :src="comment.user_avatar" class="w-10 h-10 rounded-full bg-slate-100">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-bold text-slate-900 text-sm">戏迷_{{ i*92 }}</span>
|
||||
<span class="text-xs text-slate-400">1小时前</span>
|
||||
</div>
|
||||
<p class="text-slate-700 text-sm leading-relaxed mb-2">这段唱腔真是经典,张火丁老师的演绎太有味道了,反复听了十几遍!</p>
|
||||
<div class="flex gap-4 text-xs text-slate-500">
|
||||
<button class="hover:text-primary-600 flex items-center gap-1"><i class="pi pi-thumbs-up"></i> 12</button>
|
||||
<button class="hover:text-primary-600">回复</button>
|
||||
<span class="font-bold text-slate-900 text-sm">{{ comment.user_nickname }}</span>
|
||||
<span class="text-xs text-slate-400">{{ comment.create_time }}</span>
|
||||
</div>
|
||||
<p class="text-slate-700 text-sm leading-relaxed mb-2">{{ comment.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,57 +115,39 @@
|
||||
<!-- Author Card -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-6 text-center">
|
||||
<div class="w-20 h-20 rounded-full border-4 border-slate-50 mx-auto -mt-10 mb-4 bg-white shadow-sm overflow-hidden">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Master1" class="w-full h-full object-cover">
|
||||
<img :src="content.author_avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${content.author_id}`" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<h3 class="font-bold text-slate-900 text-lg">梅派传人小林</h3>
|
||||
<p class="text-xs text-slate-500 mt-1 mb-4">专注京剧程派艺术传承与推广</p>
|
||||
<div class="flex justify-center gap-4 text-sm font-bold text-slate-900 mb-6">
|
||||
<div>125 <span class="text-xs text-slate-400 font-normal block">内容</span></div>
|
||||
<div>1.2w <span class="text-xs text-slate-400 font-normal block">关注</span></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<h3 class="font-bold text-slate-900 text-lg">{{ content.author_name }}</h3>
|
||||
<div class="grid grid-cols-2 gap-3 mt-4">
|
||||
<button class="py-2 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 shadow-sm shadow-primary-200">关注</button>
|
||||
<button class="py-2 border border-slate-200 text-slate-700 rounded-lg font-bold hover:bg-slate-50">私信</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOC -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 class="font-bold text-slate-900 mb-4">目录</h3>
|
||||
<div class="space-y-3 text-sm relative">
|
||||
<!-- Active Line -->
|
||||
<div class="absolute left-0 top-2 bottom-2 w-0.5 bg-slate-100"></div>
|
||||
|
||||
<a href="#" class="block pl-4 border-l-2 border-primary-600 text-primary-600 font-bold -ml-[1px]">唱段背景介绍</a>
|
||||
<a href="#" class="block pl-4 border-l-2 border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300">发音技巧解析</a>
|
||||
<a href="#" class="block pl-4 border-l-2 border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300">逐句示范 (00:45)</a>
|
||||
<a href="#" class="block pl-4 border-l-2 border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300">完整伴奏下载</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
|
||||
<h3 class="font-bold text-slate-900 mb-4">相关推荐</h3>
|
||||
<div class="space-y-4">
|
||||
<div v-for="i in 3" :key="i" class="flex gap-3 group cursor-pointer">
|
||||
<img src="https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60" class="w-20 h-14 object-cover rounded bg-slate-100 flex-shrink-0">
|
||||
<div>
|
||||
<h4 class="text-sm font-bold text-slate-900 line-clamp-2 group-hover:text-primary-600 leading-snug">程派《荒山泪》夜织选段</h4>
|
||||
<span class="text-xs text-slate-400 mt-1 block">8.5k 阅读</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { contentApi } from '../../api/content';
|
||||
|
||||
const fontSize = ref(18); // Default 18px
|
||||
const route = useRoute();
|
||||
const fontSize = ref(18);
|
||||
const isPlaying = ref(false);
|
||||
const showPaywall = ref(true); // Simulate trial ended
|
||||
const isPurchased = ref(false); // Simulate paid content
|
||||
const content = ref({});
|
||||
const comments = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await contentApi.get(route.params.id);
|
||||
content.value = res;
|
||||
// Fetch comments if needed
|
||||
// const cmts = await contentApi.listComments(route.params.id);
|
||||
// comments.value = cmts.items || [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -103,12 +103,13 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { orderApi } from '../../api/order';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const orderId = route.params.id || '82934712';
|
||||
const amount = '9.90';
|
||||
const amount = '9.90'; // Should fetch order details first
|
||||
const productName = '《霸王别姬》全本实录珍藏版';
|
||||
|
||||
const paymentMethod = ref('wechat');
|
||||
@@ -116,6 +117,7 @@ const timeLeft = ref(900); // 15 minutes
|
||||
const isScanning = ref(false);
|
||||
const isSuccess = ref(false);
|
||||
let timer = null;
|
||||
let pollTimer = null;
|
||||
|
||||
const paymentMethodName = computed(() => {
|
||||
return paymentMethod.value === 'wechat' ? '微信' : '支付宝';
|
||||
@@ -142,9 +144,25 @@ onMounted(() => {
|
||||
timer = setInterval(() => {
|
||||
if (timeLeft.value > 0) timeLeft.value--;
|
||||
}, 1000);
|
||||
|
||||
// Poll Status
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const res = await orderApi.status(orderId);
|
||||
if (res.status === 'paid' || res.status === 'completed') {
|
||||
isScanning.value = false;
|
||||
isSuccess.value = true;
|
||||
clearInterval(pollTimer);
|
||||
setTimeout(() => router.replace(`/me/orders/${orderId}`), 1500);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Poll status failed', e);
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
});
|
||||
</script>
|
||||
@@ -8,7 +8,7 @@
|
||||
class="pi pi-wallet"></i></div>
|
||||
<div>
|
||||
<div class="text-sm text-slate-500">账户余额</div>
|
||||
<div class="text-2xl font-bold text-slate-900">¥ 128.50</div>
|
||||
<div class="text-2xl font-bold text-slate-900">¥ {{ wallet.balance }}</div>
|
||||
</div>
|
||||
<i class="pi pi-chevron-right ml-auto text-slate-300 group-hover:text-primary-400"></i>
|
||||
</router-link>
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-slate-500">我的积分</div>
|
||||
<div class="text-2xl font-bold text-slate-900">2,450</div>
|
||||
<div class="text-2xl font-bold text-slate-900">{{ wallet.points }}</div>
|
||||
</div>
|
||||
<i class="pi pi-chevron-right ml-auto text-slate-300 group-hover:text-primary-400"></i>
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-slate-500">优惠券</div>
|
||||
<div class="text-2xl font-bold text-slate-900">3 张</div>
|
||||
<div class="text-2xl font-bold text-slate-900">{{ couponCount }} 张</div>
|
||||
</div>
|
||||
<i class="pi pi-chevron-right ml-auto text-slate-300 group-hover:text-primary-400"></i>
|
||||
</div>
|
||||
@@ -48,46 +48,67 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div @click="$router.push('/me/orders/82934712')"
|
||||
<div v-for="order in recentOrders" :key="order.id" @click="$router.push(`/me/orders/${order.id}`)"
|
||||
class="flex items-center gap-4 p-4 border border-slate-100 rounded-lg hover:border-primary-100 hover:shadow-sm transition-all cursor-pointer active:scale-[0.99] group">
|
||||
<div class="w-16 h-16 bg-slate-100 rounded object-cover flex-shrink-0">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60"
|
||||
<img v-if="order.items && order.items.length > 0"
|
||||
:src="order.items[0].cover"
|
||||
class="w-full h-full object-cover rounded transition-transform group-hover:scale-105">
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-bold text-slate-900 truncate group-hover:text-primary-600 transition-colors">
|
||||
《霸王别姬》全本实录珍藏版</h3>
|
||||
<div class="text-sm text-slate-500 mt-1">2025-12-24 14:30 · 订单号: 82934712</div>
|
||||
{{ order.items && order.items.length > 0 ? order.items[0].title : '未知商品' }}</h3>
|
||||
<div class="text-sm text-slate-500 mt-1">{{ order.create_time }} · 订单号: {{ order.id }}</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="font-bold text-slate-900">¥ 9.90</div>
|
||||
<div class="text-sm text-green-600 mt-1">交易成功</div>
|
||||
<div class="font-bold text-slate-900">¥ {{ order.amount }}</div>
|
||||
<div class="text-sm text-green-600 mt-1">{{ order.status }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- More items... -->
|
||||
<div v-if="recentOrders.length === 0" class="text-center text-slate-400 py-4">暂无订单</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Views -->
|
||||
<!-- Recent Views (Mock) -->
|
||||
<div class="mt-8 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-slate-900">最近浏览</h2>
|
||||
<button class="text-sm text-slate-500 hover:text-slate-700"><i class="pi pi-trash"></i> 清空历史</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
<div v-for="i in 5" :key="i" class="group cursor-pointer">
|
||||
<div class="aspect-[16/9] bg-slate-100 rounded-lg overflow-hidden mb-2 relative">
|
||||
<img
|
||||
:src="`https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=60`"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
|
||||
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors"></div>
|
||||
</div>
|
||||
<h4 class="text-sm font-medium text-slate-800 line-clamp-2 group-hover:text-primary-600">
|
||||
京剧名家谈戏曲传承与创新发展的思考</h4>
|
||||
<div class="text-xs text-slate-400 mt-1">10分钟前</div>
|
||||
</div>
|
||||
<div class="text-center text-slate-400 col-span-full py-8">暂无浏览记录</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { userApi } from '../../api/user';
|
||||
|
||||
const wallet = ref({ balance: 0, points: 0 });
|
||||
const couponCount = ref(0);
|
||||
const recentOrders = ref([]);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const w = await userApi.getWallet();
|
||||
wallet.value.balance = w.balance || 0;
|
||||
|
||||
const u = await userApi.getMe();
|
||||
wallet.value.points = u.points || 0;
|
||||
|
||||
const c = await userApi.getCoupons('unused');
|
||||
couponCount.value = c.length;
|
||||
|
||||
const o = await userApi.getOrders('all');
|
||||
recentOrders.value = (o || []).slice(0, 3);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -83,65 +83,50 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { userApi } from '../../api/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const currentTab = ref('all');
|
||||
const tabs = [
|
||||
{ label: '全部订单', value: 'all' },
|
||||
{ label: '待支付', value: 'unpaid' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '退款/售后', value: 'refund' }
|
||||
{ label: '待支付', value: 'created' }, // Backend uses 'created' for unpaid? Check consts.
|
||||
// Backend OrderStatusCreated = "created". OrderStatusPaid = "paid".
|
||||
// So 'unpaid' in UI should map to 'created' in backend?
|
||||
// Let's check `consts.gen.go`.
|
||||
{ label: '已完成', value: 'paid' },
|
||||
{ label: '退款/售后', value: 'refunded' } // or 'refunding'
|
||||
];
|
||||
|
||||
// Mock Data
|
||||
const orders = ref([
|
||||
{
|
||||
id: '82934712',
|
||||
date: '2025-12-24 14:30',
|
||||
tenantName: '梅派传人小林',
|
||||
cover: 'https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
|
||||
title: '《霸王别姬》全本实录珍藏版',
|
||||
type: 'video',
|
||||
typeLabel: '戏曲视频',
|
||||
isVirtual: true,
|
||||
amount: '9.90',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '82934713',
|
||||
date: '2025-12-23 09:15',
|
||||
tenantName: '戏曲周边商城',
|
||||
cover: 'https://images.unsplash.com/photo-1557683316-973673baf926?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
|
||||
title: '京剧脸谱纪念书签 (一套4张)',
|
||||
type: 'product',
|
||||
typeLabel: '实体商品',
|
||||
isVirtual: false,
|
||||
amount: '45.00',
|
||||
status: 'unpaid'
|
||||
},
|
||||
{
|
||||
id: '82934711',
|
||||
date: '2025-12-20 18:20',
|
||||
tenantName: '豫剧李大师',
|
||||
cover: 'https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
|
||||
title: '豫剧唱腔发音技巧专栏',
|
||||
type: 'article',
|
||||
typeLabel: '付费专栏',
|
||||
isVirtual: true,
|
||||
amount: '99.00',
|
||||
status: 'refunded' // or refunding
|
||||
}
|
||||
]);
|
||||
const orders = ref([]);
|
||||
|
||||
const filteredOrders = computed(() => {
|
||||
if (currentTab.value === 'all') return orders.value;
|
||||
if (currentTab.value === 'refund') return orders.value.filter(o => ['refunded', 'refunding'].includes(o.status));
|
||||
return orders.value.filter(o => o.status === currentTab.value);
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
let status = currentTab.value;
|
||||
// Map UI tab to Backend Status if needed
|
||||
// Assuming backend accepts 'all', 'created', 'paid', 'refunded'.
|
||||
const res = await userApi.getOrders(status);
|
||||
orders.value = res || [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders();
|
||||
});
|
||||
|
||||
watch(currentTab, () => {
|
||||
fetchOrders();
|
||||
});
|
||||
|
||||
// Use orders directly (filtered by backend)
|
||||
const filteredOrders = computed(() => orders.value);
|
||||
|
||||
const statusText = (status) => {
|
||||
const map = {
|
||||
unpaid: '待支付',
|
||||
created: '待支付',
|
||||
paid: '已支付',
|
||||
completed: '交易成功',
|
||||
refunding: '退款中',
|
||||
@@ -153,7 +138,7 @@ const statusText = (status) => {
|
||||
|
||||
const statusColor = (status) => {
|
||||
const map = {
|
||||
unpaid: 'text-orange-600',
|
||||
created: 'text-orange-600',
|
||||
paid: 'text-blue-600',
|
||||
completed: 'text-green-600',
|
||||
refunding: 'text-purple-600',
|
||||
|
||||
Reference in New Issue
Block a user