fix: resolve frontend build error and order refund bug, add member price filter
This commit is contained in:
81
frontend/portal/src/views/user/CouponsView.vue
Normal file
81
frontend/portal/src/views/user/CouponsView.vue
Normal 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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user