fix: resolve frontend build error and order refund bug, add member price filter

This commit is contained in:
2026-01-07 21:49:04 +08:00
parent 5b45f7d5c4
commit a1de16bc01
18 changed files with 772 additions and 282 deletions

View File

@@ -105,7 +105,8 @@ const fetchContents = async (append = false) => {
const params = {
page: page.value,
limit: 12,
sort: sort.value
sort: sort.value,
price_type: selectedPrice.value
};
if (selectedGenre.value !== '全部') params.genre = selectedGenre.value;
if (keyword.value) params.keyword = keyword.value;
@@ -127,7 +128,7 @@ const loadMore = () => {
fetchContents(true);
};
watch([selectedGenre, sort], () => {
watch([selectedGenre, selectedPrice, sort], () => {
page.value = 1;
fetchContents();
});

View File

@@ -1,29 +1,40 @@
<script setup>
import { ref, onMounted } from 'vue';
import { contentApi } from '../../api/content';
import { tenantApi } from '../../api/tenant';
const contents = ref([]);
const bannerItems = ref([]);
const trendingItems = ref([]);
const recommendedCreators = ref([]);
const matchedCreators = ref([]);
const searchKeyword = ref('');
const loading = ref(true);
const page = ref(1);
const hasMore = ref(false);
const activeBannerIndex = ref(0);
const fetchContents = async (append = false) => {
const fetchData = async () => {
loading.value = true;
try {
const params = {
page: page.value,
limit: 10,
sort: 'latest',
keyword: searchKeyword.value
};
const res = await contentApi.list(params);
if (append) {
contents.value.push(...(res.items || []));
} else {
contents.value = res.items || [];
const [bannerRes, trendingRes, creatorsRes, feedRes] = await Promise.all([
contentApi.list({ is_pinned: true, limit: 5 }),
contentApi.list({ sort: 'hot', limit: 3 }),
tenantApi.list({ limit: 5 }),
contentApi.list({ page: 1, limit: 10, sort: 'latest' })
]);
if (bannerRes.items && bannerRes.items.length > 0) {
bannerItems.value = bannerRes.items;
} else if (feedRes.items && feedRes.items.length > 0) {
bannerItems.value = feedRes.items.slice(0, 5);
}
hasMore.value = (res.total > contents.value.length);
trendingItems.value = trendingRes.items || [];
recommendedCreators.value = creatorsRes.items || [];
contents.value = feedRes.items || [];
hasMore.value = (feedRes.total > contents.value.length);
} catch (e) {
console.error(e);
} finally {
@@ -31,46 +42,78 @@ const fetchContents = async (append = false) => {
}
};
const handleSearch = () => {
const handleSearch = async () => {
page.value = 1;
fetchContents();
loading.value = true;
matchedCreators.value = [];
try {
const promises = [
contentApi.list({ page: 1, limit: 10, keyword: searchKeyword.value })
];
if (searchKeyword.value) {
promises.push(tenantApi.list({ keyword: searchKeyword.value, limit: 5 }));
}
const results = await Promise.all(promises);
const contentRes = results[0];
contents.value = contentRes.items || [];
hasMore.value = (contentRes.total > contents.value.length);
if (results[1]) {
matchedCreators.value = results[1].items || [];
}
} finally {
loading.value = false;
}
};
const loadMore = () => {
const loadMore = async () => {
page.value++;
fetchContents(true);
const res = await contentApi.list({
page: page.value,
limit: 10,
keyword: searchKeyword.value
});
if (res.items) {
contents.value.push(...res.items);
hasMore.value = (res.total > contents.value.length);
}
};
onMounted(() => fetchContents());
onMounted(fetchData);
</script>
<template>
<div class="mx-auto max-w-screen-xl py-8">
<!-- Hero Banner -->
<div class="relative w-full h-[400px] rounded-2xl overflow-hidden bg-slate-900 mb-8 group">
<!-- Mock Carousel Image -->
<img
src="https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80"
class="w-full h-full object-cover opacity-80 transition-transform duration-700 group-hover:scale-105"
alt="Banner">
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
<div class="absolute bottom-0 left-0 p-10 max-w-2xl text-white">
<div class="inline-block px-3 py-1 bg-red-600 text-white text-xs font-bold rounded mb-3">置顶推荐</div>
<h2 class="text-4xl font-bold mb-4 leading-tight">京剧霸王别姬全本实录程派艺术的巅峰演绎</h2>
<p class="text-lg text-slate-200 line-clamp-2">梅兰芳大师经典之作高清修复版独家上线感受国粹魅力重温梨园风华</p>
<div class="relative w-full h-[400px] rounded-2xl overflow-hidden bg-slate-900 mb-8 group" v-if="bannerItems.length > 0">
<div v-for="(item, index) in bannerItems" :key="item.id"
class="absolute inset-0 transition-opacity duration-700"
:class="{ 'opacity-100 z-10': activeBannerIndex === index, 'opacity-0 z-0': activeBannerIndex !== index }">
<img :src="item.cover" class="w-full h-full object-cover opacity-80" alt="Banner">
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
<div class="absolute bottom-0 left-0 p-10 max-w-2xl text-white">
<div class="inline-block px-3 py-1 bg-red-600 text-white text-xs font-bold rounded mb-3">置顶推荐</div>
<h2 class="text-4xl font-bold mb-4 leading-tight cursor-pointer hover:underline" @click="$router.push(`/contents/${item.id}`)">{{ item.title }}</h2>
<p class="text-lg text-slate-200 line-clamp-2">{{ item.description || item.title }}</p>
</div>
</div>
<!-- Arrows (Always visible as per spec) -->
<button
class="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/50 text-white rounded-full flex items-center justify-center backdrop-blur-sm transition-all"><i
<!-- Arrows -->
<button @click="activeBannerIndex = (activeBannerIndex - 1 + bannerItems.length) % bannerItems.length"
class="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/50 text-white rounded-full flex items-center justify-center backdrop-blur-sm transition-all z-20"><i
class="pi pi-chevron-left text-xl"></i></button>
<button
class="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/50 text-white rounded-full flex items-center justify-center backdrop-blur-sm transition-all"><i
<button @click="activeBannerIndex = (activeBannerIndex + 1) % bannerItems.length"
class="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/50 text-white rounded-full flex items-center justify-center backdrop-blur-sm transition-all z-20"><i
class="pi pi-chevron-right text-xl"></i></button>
<!-- Indicators -->
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
<span class="w-2 h-2 rounded-full bg-white"></span>
<span class="w-2 h-2 rounded-full bg-white/50"></span>
<span class="w-2 h-2 rounded-full bg-white/50"></span>
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 z-20">
<span v-for="(item, index) in bannerItems" :key="index"
class="w-2 h-2 rounded-full cursor-pointer transition-colors"
:class="activeBannerIndex === index ? 'bg-white' : 'bg-white/50'"
@click="activeBannerIndex = index"></span>
</div>
</div>
@@ -111,6 +154,22 @@ onMounted(() => fetchContents());
<!-- Main Feed (Left 9) -->
<div class="col-span-12 lg:col-span-8 xl:col-span-9 space-y-6">
<!-- Matched Creators (Search Result) -->
<div v-if="searchKeyword && matchedCreators.length > 0" class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
<h3 class="font-bold text-slate-900 mb-4">相关频道</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div v-for="creator in matchedCreators" :key="creator.id"
class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 transition-colors cursor-pointer border border-transparent hover:border-slate-200"
@click="$router.push(`/creators/${creator.id}`)">
<img :src="creator.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${creator.id}`" class="w-12 h-12 rounded-full border border-slate-100">
<div class="flex-1 min-w-0">
<div class="font-bold text-slate-900 truncate">{{ creator.name }}</div>
<div class="text-xs text-slate-500 truncate">{{ creator.bio || '暂无简介' }}</div>
</div>
</div>
</div>
</div>
<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">
@@ -173,32 +232,15 @@ onMounted(() => fetchContents());
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h3 class="font-bold text-slate-900 mb-4">推荐名家</h3>
<div class="space-y-4">
<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">
<div v-for="creator in recommendedCreators" :key="creator.id" class="flex items-center gap-3">
<img :src="creator.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${creator.id}`" class="w-10 h-10 rounded-full cursor-pointer" @click="$router.push(`/creators/${creator.id}`)">
<div class="flex-1 min-w-0">
<div class="font-bold text-slate-900 text-sm truncate">梅派传人小林</div>
<div class="text-xs text-slate-500 truncate">粉丝 12.5</div>
<div class="font-bold text-slate-900 text-sm truncate hover:text-primary-600 cursor-pointer" @click="$router.push(`/creators/${creator.id}`)">{{ creator.name }}</div>
<div class="text-xs text-slate-500 truncate">粉丝 {{ creator.stats?.followers || 0 }}</div>
</div>
<button
class="px-3 py-1 bg-primary-50 text-primary-600 text-xs font-bold rounded-full hover:bg-primary-100">关注</button>
</div>
<div class="flex items-center gap-3">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Master2" class="w-10 h-10 rounded-full">
<div class="flex-1 min-w-0">
<div class="font-bold text-slate-900 text-sm truncate">豫剧李大师</div>
<div class="text-xs text-slate-500 truncate">粉丝 8.9</div>
</div>
<button
class="px-3 py-1 bg-primary-50 text-primary-600 text-xs font-bold rounded-full hover:bg-primary-100">关注</button>
</div>
<div class="flex items-center gap-3">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Master3" class="w-10 h-10 rounded-full">
<div class="flex-1 min-w-0">
<div class="font-bold text-slate-900 text-sm truncate">越剧小生阿强</div>
<div class="text-xs text-slate-500 truncate">粉丝 5.2</div>
</div>
<button class="px-3 py-1 bg-slate-100 text-slate-400 text-xs font-bold rounded-full">已关注</button>
<button class="px-3 py-1 bg-primary-50 text-primary-600 text-xs font-bold rounded-full hover:bg-primary-100">关注</button>
</div>
<div v-if="recommendedCreators.length === 0" class="text-center text-slate-400 text-sm">暂无推荐</div>
</div>
</div>
@@ -208,33 +250,16 @@ onMounted(() => fetchContents());
<i class="pi pi-chart-line text-red-500"></i> 本周热门
</h3>
<ul class="space-y-4">
<li class="flex gap-3 items-start">
<span class="text-red-500 font-bold italic text-lg w-4">1</span>
<li v-for="(item, index) in trendingItems" :key="item.id" class="flex gap-3 items-start">
<span class="font-bold italic text-lg w-4" :class="index === 0 ? 'text-red-500' : (index === 1 ? 'text-orange-500' : 'text-yellow-500')">{{ index + 1 }}</span>
<div class="flex-1">
<h4
<h4 @click="$router.push(`/contents/${item.id}`)"
class="text-sm font-medium text-slate-800 line-clamp-2 hover:text-primary-600 cursor-pointer">
智取威虎山选段今日痛饮庆功酒</h4>
<span class="text-xs text-slate-400 mt-1 block">15.2 阅读</span>
</div>
</li>
<li class="flex gap-3 items-start">
<span class="text-orange-500 font-bold italic text-lg w-4">2</span>
<div class="flex-1">
<h4
class="text-sm font-medium text-slate-800 line-clamp-2 hover:text-primary-600 cursor-pointer">
深度解析京剧脸谱颜色的含义</h4>
<span class="text-xs text-slate-400 mt-1 block">9.8 阅读</span>
</div>
</li>
<li class="flex gap-3 items-start">
<span class="text-yellow-500 font-bold italic text-lg w-4">3</span>
<div class="flex-1">
<h4
class="text-sm font-medium text-slate-800 line-clamp-2 hover:text-primary-600 cursor-pointer">
黄梅戏女驸马全场高清</h4>
<span class="text-xs text-slate-400 mt-1 block">7.5 阅读</span>
{{ item.title }}</h4>
<span class="text-xs text-slate-400 mt-1 block">{{ item.views }} 阅读</span>
</div>
</li>
<div v-if="trendingItems.length === 0" class="text-center text-slate-400 text-sm">暂无热门</div>
</ul>
</div>
@@ -251,34 +276,4 @@ onMounted(() => fetchContents());
</div>
</div>
</div>
<script setup>
import { ref, onMounted } from 'vue';
import { contentApi } from '../api/content';
const contents = ref([]);
const searchKeyword = ref('');
const loading = ref(true);
const fetchContents = async () => {
loading.value = true;
try {
const params = {
limit: 20,
sort: 'latest',
keyword: searchKeyword.value
};
const res = await contentApi.list(params);
contents.value = res.items || [];
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
};
const handleSearch = () => {
fetchContents();
};
onMounted(fetchContents);
</script>

View File

@@ -24,7 +24,7 @@
</div>
<div class="ml-auto relative 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="搜索订单号或买家..."
<input type="text" v-model="searchKeyword" @keyup.enter="fetchOrders" placeholder="搜索订单号或买家..."
class="w-full h-9 pl-9 pr-4 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none transition-all" />
</div>
</div>
@@ -165,7 +165,7 @@
</label>
</div>
<div v-if="refundAction === 'reject'" class="mt-4">
<textarea class="w-full p-2 border border-slate-200 rounded text-sm focus:border-red-500 outline-none"
<textarea v-model="refundReason" class="w-full p-2 border border-slate-200 rounded text-sm focus:border-red-500 outline-none"
rows="2" placeholder="请输入拒绝理由..."></textarea>
</div>
<template #footer>
@@ -188,51 +188,54 @@ import Dialog from "primevue/dialog";
import RadioButton from "primevue/radiobutton";
import Toast from "primevue/toast";
import { useToast } from "primevue/usetoast";
import { computed, ref } from "vue";
import { computed, ref, onMounted, watch } from "vue";
import { creatorApi } from "../../api/creator";
const toast = useToast();
const filterStatus = ref("all");
const searchKeyword = ref("");
const detailDialog = ref(false);
const refundDialog = ref(false);
const selectedOrder = ref(null);
const refundAction = ref("accept");
const refundReason = ref("");
const orders = ref([]);
const loading = ref(false);
const orders = ref([
{
id: "82934712",
title: "《霸王别姬》全本实录珍藏版",
type: "视频",
cover:
"https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60",
buyerName: "戏迷小张",
buyerAvatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang",
buyerId: "9527",
amount: "9.90",
date: "2025-12-24 14:30",
status: "completed",
},
{
id: "82934715",
title: "京剧打击乐基础教程",
type: "视频",
cover:
"https://images.unsplash.com/photo-1533174072545-e8d4aa97edf9?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60",
buyerName: "票友老李",
buyerAvatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Li",
buyerId: "8848",
amount: "19.90",
date: "2025-12-25 09:15",
status: "refunding",
},
]);
const fetchOrders = async () => {
loading.value = true;
try {
const params = {
status: filterStatus.value === 'all' ? '' : filterStatus.value,
keyword: searchKeyword.value
};
const res = await creatorApi.listOrders(params);
orders.value = (res || []).map(o => ({
id: o.id,
title: o.title || '未知内容',
type: '数字内容',
cover: o.cover,
buyerName: o.buyer_name,
buyerAvatar: o.buyer_avatar,
amount: o.amount,
date: o.create_time,
status: o.status
}));
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
};
const filteredOrders = computed(() => {
if (filterStatus.value === "all") return orders.value;
return orders.value.filter((o) => o.status === filterStatus.value);
});
onMounted(fetchOrders);
watch(filterStatus, fetchOrders);
const filteredOrders = computed(() => orders.value);
const statusStyle = (status) => {
switch (status) {
case "paid":
case "completed":
return { bg: "bg-green-50", text: "text-green-600", label: "已完成" };
case "refunding":
@@ -252,18 +255,21 @@ const viewDetail = (order) => {
const handleRefund = (order) => {
selectedOrder.value = order;
refundAction.value = "accept";
refundReason.value = "";
refundDialog.value = true;
};
const confirmRefund = () => {
// Mock API
refundDialog.value = false;
toast.add({
severity: "success",
summary: "处理完成",
detail: refundAction.value === "accept" ? "已同意退款" : "已拒绝退款申请",
life: 3000,
});
// In real app, refresh list
const confirmRefund = async () => {
try {
const res = await creatorApi.refundOrder(selectedOrder.value.id, {
action: refundAction.value,
reason: refundReason.value
});
refundDialog.value = false;
toast.add({ severity: "success", summary: "处理成功", life: 3000 });
fetchOrders();
} catch (e) {
toast.add({ severity: "error", summary: "处理失败", detail: e.message, life: 3000 });
}
};
</script>

View File

@@ -0,0 +1,81 @@
<script setup>
import { ref, onMounted } from 'vue';
import { userApi } from '../../api/user';
const currentTab = ref('unused');
const coupons = ref([]);
const loading = ref(false);
const tabs = [
{ label: '未使用', value: 'unused' },
{ label: '已使用', value: 'used' },
{ label: '已过期', value: 'expired' }
];
const fetchCoupons = async () => {
loading.value = true;
try {
const res = await userApi.getCoupons(currentTab.value);
coupons.value = res || [];
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
};
onMounted(fetchCoupons);
</script>
<template>
<div class="bg-white rounded-xl shadow-sm border border-slate-100 min-h-[600px] p-8">
<h1 class="text-2xl font-bold text-slate-900 mb-8">我的优惠券</h1>
<!-- Tabs -->
<div class="flex items-center gap-8 mb-8 border-b border-slate-100">
<button
v-for="tab in tabs"
:key="tab.value"
@click="currentTab = tab.value; fetchCoupons()"
class="pb-4 text-sm font-bold transition-colors border-b-2 cursor-pointer focus:outline-none"
:class="currentTab === tab.value ? 'text-primary-600 border-primary-600' : 'text-slate-500 border-transparent hover:text-slate-700'"
>
{{ tab.label }}
</button>
</div>
<!-- List -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div v-for="coupon in coupons" :key="coupon.id"
class="flex bg-white border border-slate-200 rounded-xl overflow-hidden group hover:border-primary-300 transition-all"
:class="{ 'opacity-60 grayscale': currentTab !== 'unused' }">
<!-- Left: Amount -->
<div class="w-32 bg-primary-50 flex flex-col items-center justify-center border-r border-dashed border-slate-200 p-4">
<div class="text-primary-600 font-bold">
<span class="text-sm">¥</span>
<span class="text-3xl">{{ coupon.value / 100 }}</span>
</div>
<div class="text-[10px] text-primary-500 mt-1">{{ coupon.min_order_amount / 100 }}可用</div>
</div>
<!-- Right: Info -->
<div class="flex-1 p-4 relative">
<h3 class="font-bold text-slate-900 mb-1">{{ coupon.title }}</h3>
<p class="text-xs text-slate-500 mb-4">{{ coupon.description }}</p>
<div class="text-[10px] text-slate-400">有效期至: {{ coupon.end_at }}</div>
<!-- Status Badge -->
<div v-if="currentTab === 'used'" class="absolute top-2 right-2 border-2 border-slate-300 text-slate-400 text-[10px] font-bold px-1 py-0.5 rounded rotate-12">已使用</div>
<div v-if="currentTab === 'expired'" class="absolute top-2 right-2 border-2 border-red-300 text-red-400 text-[10px] font-bold px-1 py-0.5 rounded rotate-12">已过期</div>
</div>
</div>
</div>
<!-- Empty -->
<div v-if="!loading && coupons.length === 0" class="text-center py-20 text-slate-400">
<div class="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="pi pi-ticket text-2xl text-slate-300"></i>
</div>
<p>暂无相关优惠券</p>
</div>
</div>
</template>

View File

@@ -1,3 +1,83 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
import { userApi } from '../../api/user';
const router = useRouter();
const currentTab = ref('all');
const dialogVisible = ref(false);
const selectedNotification = ref(null);
const loading = ref(false);
const page = ref(1);
const tabs = ref([
{ label: '全部', value: 'all', count: 0 },
{ label: '系统通知', value: 'system', count: 0 },
{ label: '订单通知', value: 'order', count: 0 },
{ label: '审核通知', value: 'audit', count: 0 },
{ label: '互动消息', value: 'interaction', count: 0 }
]);
const notifications = ref([]);
const fetchNotifications = async () => {
loading.value = true;
try {
const res = await userApi.getNotifications(currentTab.value, page.value);
notifications.value = res.items || [];
} catch (e) {
console.error("Fetch notifications failed:", e);
} finally {
loading.value = false;
}
};
onMounted(fetchNotifications);
watch(currentTab, () => {
page.value = 1;
fetchNotifications();
});
const getIconStyle = (type) => {
switch(type) {
case 'system': return { bg: 'bg-blue-50', color: 'text-blue-600', icon: 'pi-megaphone' };
case 'order': return { bg: 'bg-green-50', color: 'text-green-600', icon: 'pi-shopping-bag' };
case 'audit': return { bg: 'bg-orange-50', color: 'text-orange-600', icon: 'pi-file-edit' };
case 'interaction': return { bg: 'bg-purple-50', color: 'text-purple-600', icon: 'pi-comments' };
default: return { bg: 'bg-slate-100', color: 'text-slate-500', icon: 'pi-bell' };
}
};
const handleNotificationClick = async (item) => {
if (!item.read) {
try {
await userApi.markNotificationRead(item.id);
item.read = true;
} catch (e) {
console.error("Mark read failed:", e);
}
}
if (item.type === 'system') {
selectedNotification.value = item;
dialogVisible.value = true;
}
};
const handleMarkAllRead = async () => {
try {
await userApi.markAllNotificationsRead();
notifications.value.forEach(n => n.read = true);
tabs.value.forEach(t => t.count = 0);
} catch (e) {
console.error("Mark all read failed:", e);
}
}
</script>
<template>
<div class="bg-white rounded-xl shadow-sm border border-slate-100 min-h-[600px]">
<!-- Header & Tabs -->
@@ -14,16 +94,16 @@
<span v-if="tab.count > 0" class="absolute -top-1 -right-4 min-w-[1.25rem] h-5 px-1.5 bg-red-500 text-white text-[10px] rounded-full flex items-center justify-center">{{ tab.count }}</span>
</button>
</div>
<button class="mb-4 text-base font-medium text-slate-500 hover:text-primary-600 cursor-pointer flex items-center gap-1">
<button @click="handleMarkAllRead" class="mb-4 text-base font-medium text-slate-500 hover:text-primary-600 cursor-pointer flex items-center gap-1">
<i class="pi pi-check-circle"></i> 全部已读
</button>
</div>
<!-- Notification List -->
<div class="p-0">
<div v-if="filteredNotifications.length > 0">
<div v-if="notifications.length > 0">
<div
v-for="item in filteredNotifications"
v-for="item in notifications"
:key="item.id"
@click="handleNotificationClick(item)"
class="flex items-start gap-4 p-5 border-b border-slate-50 hover:bg-slate-50 transition-colors cursor-pointer group"
@@ -62,114 +142,10 @@
<div class="p-4">
<div class="text-slate-500 text-sm mb-4">{{ selectedNotification?.time }}</div>
<div class="text-slate-700 leading-relaxed whitespace-pre-wrap">{{ selectedNotification?.content }}</div>
<!-- Mock rich content / image -->
<div v-if="selectedNotification?.id === 1" class="mt-4 p-4 bg-slate-50 rounded text-sm text-slate-500">
(此处为富文本内容展示区可能包含图片链接等)
</div>
</div>
<template #footer>
<Button label="关闭" icon="pi pi-check" @click="dialogVisible = false" autofocus />
</template>
</Dialog>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
const router = useRouter();
const currentTab = ref('all');
const dialogVisible = ref(false);
const selectedNotification = ref(null);
const tabs = [
{ label: '全部', value: 'all', count: 3 },
{ label: '系统通知', value: 'system', count: 1 },
{ label: '订单通知', value: 'order', count: 1 },
{ label: '审核通知', value: 'audit', count: 0 },
{ label: '互动消息', value: 'interaction', count: 1 }
];
const notifications = ref([
{
id: 1,
type: 'system',
title: '平台服务协议更新通知',
content: '为了更好地保障您的权益,我们更新了《用户服务协议》和《隐私政策》,主要变更涉及账户安全与数据保护。\n\n具体变更内容如下\n1. 明确了数据采集范围...\n2. 优化了账号注销流程...',
time: '10分钟前',
read: false,
link: null
},
{
id: 2,
type: 'order',
title: '订单支付成功',
content: '您购买的《霸王别姬》全本实录珍藏版已支付成功订单号82934712请前往已购内容查看。',
time: '2小时前',
read: false,
link: '/me/orders/82934712'
},
{
id: 3,
type: 'interaction',
title: '收到新的评论回复',
content: '梅派传人小林 回复了您的评论:“感谢您的支持,这版录音确实非常珍贵...”。',
time: '昨天 14:30',
read: false,
link: '/contents/1'
},
{
id: 4,
type: 'audit',
title: '内容审核通过',
content: '恭喜!您发布的文章《京剧脸谱赏析》已通过审核并发布上线。',
time: '3天前',
read: true,
link: '/creator/contents'
},
{
id: 5,
type: 'system',
title: '春节期间服务调整公告',
content: '春节期间2月9日-2月17日提现申请处理时效将有所延迟敬请谅解。',
time: '5天前',
read: true,
link: null
}
]);
const filteredNotifications = computed(() => {
if (currentTab.value === 'all') return notifications.value;
return notifications.value.filter(n => n.type === currentTab.value);
});
const getIconStyle = (type) => {
switch(type) {
case 'system': return { bg: 'bg-blue-50', color: 'text-blue-600', icon: 'pi-megaphone' };
case 'order': return { bg: 'bg-green-50', color: 'text-green-600', icon: 'pi-shopping-bag' };
case 'audit': return { bg: 'bg-orange-50', color: 'text-orange-600', icon: 'pi-file-edit' };
case 'interaction': return { bg: 'bg-purple-50', color: 'text-purple-600', icon: 'pi-comments' };
default: return { bg: 'bg-slate-100', color: 'text-slate-500', icon: 'pi-bell' };
}
};
const handleNotificationClick = (item) => {
// 1. Mark as read
item.read = true;
// 2. Handle System type separately
if (item.type === 'system') {
selectedNotification.value = item;
dialogVisible.value = true;
return;
}
// 3. Navigate if link exists
if (item.link) {
router.push(item.link);
}
};
</script>
</template>