This commit is contained in:
李志鹏
2026-06-01 10:10:23 +08:00
15 changed files with 673 additions and 253 deletions

View File

@@ -1,3 +1,4 @@
VITE_APP_URL = http://192.168.31.82:10094
# VITE_APP_URL = http://192.168.31.82:10094
VITE_APP_URL = https://www.develop-ms.api.aida.com.hk
# WebSocket 主机地址
VITE_WS_HOST = 18.167.251.121:10094

View File

@@ -1,2 +1,4 @@
VITE_APP_URL = http://192.168.31.82:10094
VITE_APP_URL = https://www.develop-ms.api.aida.com.hk
# WebSocket 主机地址
VITE_WS_HOST = www.develop-ms.api.aida.com.hk

View File

@@ -7,13 +7,16 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref, onBeforeUnmount } from 'vue'
import { computed, onMounted, ref, onBeforeUnmount, watch } from 'vue'
import RouteCache from '@/components/RouteCache.vue'
import MainHeader from '@/views/main-header.vue'
import LoginDialog from '@/views/login/login-dialog.vue'
import { useGlobalStore } from '@/stores'
import { useGlobalStore, useUserInfoStore } from '@/stores'
import ShoppingDrawer from '@/views/shopping-drawer.vue'
import { wsManager } from '@/utils/websocket'
const globalStore = useGlobalStore()
const userInfoStore = useUserInfoStore()
const loading = computed(() => globalStore.state.loading)
globalStore.setLoading(false)
const viewRef = ref()
@@ -26,12 +29,41 @@
viewStyle.value['--app-view-width'] = width + 'px'
viewStyle.value['--app-view-height'] = height + 'px'
})
// 监听 token 变化,建立或关闭 WebSocket 连接
watch(
() => userInfoStore.state.token,
(newToken, oldToken) => {
if (newToken && newToken !== oldToken) {
// 用户登录,建立 WebSocket 连接
console.log('用户已登录,建立 WebSocket 连接')
wsManager.connect(newToken)
} else if (!newToken && oldToken) {
// 用户退出登录,关闭 WebSocket 连接
console.log('用户已退出,关闭 WebSocket 连接')
wsManager.close()
}
},
{ immediate: true }
)
onMounted(() => {
observer.observe(viewRef.value)
// 如果已经有 token立即建立连接
const token = userInfoStore.state.token
if (token) {
console.log('应用启动时检测到 token建立 WebSocket 连接')
wsManager.connect(token)
}
})
onBeforeUnmount(() => {
observer.disconnect()
// 组件卸载时关闭 WebSocket 连接
wsManager.close()
})
window['onClickPrivacy'] = () => {
const e = window.event || event
e.stopPropagation()

40
src/api/notification.ts Normal file
View File

@@ -0,0 +1,40 @@
import request from '@/utils/request'
interface Page {
page: number
size: number
type?: number
isRead?: 0 | 1 // 0未读1已读
keyword?: string // 关键词搜索标题Ï
}
export const fetchAllMessageList = (data) => {
return request({
url: '/buyer/buyer/message/page',
method: 'post',
data
})
}
// 获取所有未读消息数量
export const fetchAllUnreadMessage = () => {
return request({
url: '/buyer/buyer/message/unread-count',
method: 'get'
})
}
// 标记单条消息已读
export const markMessageAsRead = (id: number | string) => {
return request({
url: `/buyer/buyer/message/${id}/read`,
method: 'put'
})
}
// 标记全部消息已读
export const markAllMessagesAsRead = () => {
return request({
url: '/buyer/buyer/message/read-all',
method: 'put'
})
}

View File

@@ -22,7 +22,6 @@ const checkList = computed(()=>{
})
const handleChange = (val) => {
let data = val.filter(item => item !== 'all')
if(val.length == 0)return
if(data.length == props.list.length){
data = ['all']
}else{
@@ -39,9 +38,11 @@ const handleCheckAllChange = (val) => {
if(val && props.selected[0] !== 'all'){
data = ['all']
// data = props.list.map(item => item.value)
emit('update:selected', data)
emit('change', data)
}else{
data = []
}
emit('update:selected', data)
emit('change', data)
}
let data = reactive({
})

View File

@@ -1,11 +1,13 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useUserInfoStore } from '@/stores/userInfo'
import { useGlobalStore } from '@/stores/global'
import { getUserLanguage } from '@/api/user'
import { fetchAllUnreadMessage } from '@/api/notification'
import i18n from '@/lang/index'
// 语言映射:后端格式 -> i18n 格式
const backendToI18nLanguage: Record<string, string> = {
'en': 'ENGLISH',
en: 'ENGLISH',
'zh-CN': 'CHINESE_SIMPLIFIED'
}
@@ -19,122 +21,126 @@ let languageSynced = false
* 3. 路由的name默认是文件名,如果文件名与name不一致,通过defineOptions({ name: 'componentName' })来设置
*/
const router = createRouter({
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/home/index.vue')
},
{
path: '/collectionStory',
name: 'collectionStory',
component: () => import('../views/collectionStory/index.vue')
},
{
path: '/brand',
name: 'brand',
component: () => import('../views/brand/index.vue')
},
{
path: '/brand/:id',
name: 'brandDetail',
component: () => import('../views/brandDetail/index.vue')
},
{
path: '/digitalItem',
name: 'digitalItem',
component: () => import('../views/digitalItem/index.vue'),
meta: { cache: true }
},
{
path: '/digitalItem/:id',
name: 'digitalItemDetail',
component: () => import('../views/digitalDetail/index.vue')
},
{
path: '/settings',
name: 'settings',
component: () => import('@/views/setting/index.vue'),
meta: { cache: true }
},
{
path: '/shoppingCart', // 购物车
name: 'shoppingCart',
component: () => import('@/views/shoppingCart/index.vue')
},
{
path: '/notifications',
name: 'notifications',
component: () => import('@/views/notifications/index.vue')
},
{
path: '/wardrobe',
name: 'wardrobe',
component: () => import('@/views/wardrobe/index.vue')
},
{
path:'/account',
name:'account',
component:()=>import('@/views/account/index.vue')
},
{
path:'/pay',
name:'pay',
component:()=>import('@/views/pay/index.vue')
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('../views/404.vue')
}
],
history: createWebHistory('/')
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/home/index.vue')
},
{
path: '/collectionStory',
name: 'collectionStory',
component: () => import('../views/collectionStory/index.vue')
},
{
path: '/brand',
name: 'brand',
component: () => import('../views/brand/index.vue')
},
{
path: '/brand/:id',
name: 'brandDetail',
component: () => import('../views/brandDetail/index.vue')
},
{
path: '/digitalItem',
name: 'digitalItem',
component: () => import('../views/digitalItem/index.vue'),
meta: { cache: true }
},
{
path: '/digitalItem/:id',
name: 'digitalItemDetail',
component: () => import('../views/digitalDetail/index.vue')
},
{
path: '/settings',
name: 'settings',
component: () => import('@/views/setting/index.vue'),
meta: { cache: true }
},
{
path: '/shoppingCart', // 购物车
name: 'shoppingCart',
component: () => import('@/views/shoppingCart/index.vue')
},
{
path: '/notifications',
name: 'notifications',
component: () => import('@/views/notifications/index.vue')
},
{
path: '/wardrobe',
name: 'wardrobe',
component: () => import('@/views/wardrobe/index.vue')
},
{
path: '/account',
name: 'account',
component: () => import('@/views/account/index.vue')
},
{
path: '/pay',
name: 'pay',
component: () => import('@/views/pay/index.vue')
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('../views/404.vue')
}
],
history: createWebHistory('/')
})
router.beforeEach((to, from, next) => {
next()
next()
})
router.afterEach(async () => {
// 检查用户是否已登录
const userInfoStore = useUserInfoStore()
const token = userInfoStore.state.token
if (!token) {
languageSynced = false // 未登录时重置同步状态
languageSynced = false
return
}
// 如果已经同步过,跳过
const globalStore = useGlobalStore()
fetchAllUnreadMessage().then((res) => {
console.log(res)
globalStore.setUnredCount(res.totalUnread)
})
if (languageSynced) {
return
}
try {
// 获取用户语言设置
const response = await getUserLanguage()
const userLanguage = (response as any)?.language // 后端返回 'en' 或 'zh-CN'
if (!userLanguage) {
return
}
// 转换为 i18n 格式
const i18nLanguage = backendToI18nLanguage[userLanguage]
if (!i18nLanguage) {
return
}
// 获取当前 i18n 语言
const currentLocale = i18n.global.locale.value
// 如果用户语言和本地 i18n 不一致,更新 i18n
if (i18nLanguage !== currentLocale) {
i18n.global.locale.value = i18nLanguage as 'ENGLISH' | 'CHINESE_SIMPLIFIED'
localStorage.setItem('language', i18nLanguage)
}
// 标记已同步
languageSynced = true
} catch (error) {

View File

@@ -2,13 +2,20 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useGlobalStore = defineStore('global', () => {
const state = ref({
loading: false
loading: false,
unReadMessageCount: 0
})
const setLoading = (v: boolean) => { state.value.loading = v }
const setLoading = (v: boolean) => {
state.value.loading = v
}
const setUnredCount = (number: number) => {
state.value.unReadMessageCount = number
}
return {
state,
setLoading,
setUnredCount
}
})

252
src/utils/websocket.ts Normal file
View File

@@ -0,0 +1,252 @@
/**
* WebSocket 管理类
*/
import { useGlobalStore } from '@/stores'
class WebSocketManager {
private ws: WebSocket | null = null
private url: string = ''
private reconnectTimer: number | null = null
private reconnectAttempts: number = 0
private maxReconnectAttempts: number = 5
private reconnectInterval: number = 3000
private heartbeatTimer: number | null = null
private heartbeatInterval: number = 30000
private isManualClose: boolean = false
/**
* 连接 WebSocket
* @param token 用户 token
*/
connect(token: string) {
if (!token) {
console.warn('WebSocket: token 为空,无法建立连接')
return
}
// 如果已经有连接在进行中,先关闭
if (this.ws) {
if (this.ws.readyState === WebSocket.CONNECTING) {
console.log('WebSocket 正在连接中,跳过重复连接')
return
}
if (this.ws.readyState === WebSocket.OPEN) {
console.log('WebSocket 已连接,跳过重复连接')
return
}
}
// 根据当前页面协议自动选择 ws 或 wss
// const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const protocol = 'wss:'
// 从环境变量获取 WebSocket 主机地址
// const wsHost = import.meta.env.VITE_WS_HOST || '18.167.251.121:10094'
const wsHost = 'www.develop-ms.api.aida.com.hk'
this.url = `${protocol}//${wsHost}/ws?token=${token}`
this.isManualClose = false
console.log('WebSocket 开始连接:', this.url.replace(/token=.+/, 'token=***'))
try {
this.ws = new WebSocket(this.url)
this.initEventHandlers()
} catch (error) {
console.error('WebSocket 连接失败:', error)
this.reconnect(token)
}
}
/**
* 初始化事件处理器
*/
private initEventHandlers() {
if (!this.ws) return
this.ws.onopen = () => {
console.log('✅ WebSocket 连接成功')
this.reconnectAttempts = 0
this.startHeartbeat()
}
this.ws.onmessage = (event) => {
console.log('📨 WebSocket 收到消息:', event.data)
try {
const data = JSON.parse(event.data)
this.handleMessage(data)
} catch (error) {
console.error('WebSocket 消息解析失败:', error)
}
}
this.ws.onerror = (error) => {
console.error('❌ WebSocket 错误:', error)
console.error('可能的原因:')
console.error('1. 服务器未运行或地址错误')
console.error('2. Token 无效或已过期')
console.error('3. 网络连接问题')
console.error('4. 服务器拒绝连接')
}
this.ws.onclose = (event) => {
const closeReasons: Record<number, string> = {
1000: '正常关闭',
1001: '端点离开',
1002: '协议错误',
1003: '不支持的数据类型',
1006: '连接异常关闭(可能是网络问题或服务器未响应)',
1007: '数据格式错误',
1008: '违反策略',
1009: '消息过大',
1010: '扩展协商失败',
1011: '服务器错误',
1015: 'TLS 握手失败'
}
const reason = closeReasons[event.code] || '未知原因'
console.log(`🔌 WebSocket 连接关闭 [代码: ${event.code}] ${reason}`)
if (event.reason) {
console.log('关闭原因:', event.reason)
}
this.stopHeartbeat()
// 如果是异常关闭(非正常关闭码),才尝试重连
if (!this.isManualClose && event.code !== 1000) {
const token = this.extractTokenFromUrl()
if (token) {
this.reconnect(token)
}
} else if (event.code === 1000) {
console.log('连接正常关闭,不进行重连')
}
}
}
/**
* 处理接收到的消息
* @param data 消息数据
*/
private handleMessage(data: any) {
// 这里可以根据消息类型进行不同的处理
// 例如:通知、消息推送等
if (data.type === 'unread') {
// 处理通知消息
const info = data.data
// console.log('收到通知-----:', info)
useGlobalStore().setUnredCount(info.unreadCount)
// 可以触发自定义事件或调用回调函数
}
}
/**
* 发送消息
* @param data 要发送的数据
*/
send(data: any) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const message = typeof data === 'string' ? data : JSON.stringify(data)
this.ws.send(message)
} else {
console.warn('WebSocket 未连接,无法发送消息')
}
}
/**
* 重连
* @param token 用户 token
*/
private reconnect(token: string) {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('❌ WebSocket 重连次数已达上限,停止重连')
console.error('请检查:')
console.error('1. 服务器是否正常运行')
console.error('2. Token 是否有效')
console.error('3. 网络连接是否正常')
return
}
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
}
this.reconnectAttempts++
const delay = this.reconnectInterval * this.reconnectAttempts // 递增延迟
console.log(
`🔄 WebSocket 将在 ${delay / 1000} 秒后尝试重连 (${this.reconnectAttempts}/${
this.maxReconnectAttempts
})`
)
this.reconnectTimer = window.setTimeout(() => {
this.connect(token)
}, delay)
}
/**
* 开始心跳
*/
private startHeartbeat() {
this.stopHeartbeat()
this.heartbeatTimer = window.setInterval(() => {
this.send({ type: 'heartbeat', timestamp: Date.now() })
}, this.heartbeatInterval)
}
/**
* 停止心跳
*/
private stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
/**
* 从 URL 中提取 token
*/
private extractTokenFromUrl(): string | null {
if (!this.url) return null
const match = this.url.match(/token=([^&]+)/)
return match ? match[1] : null
}
/**
* 关闭连接
*/
close() {
this.isManualClose = true
this.stopHeartbeat()
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
if (this.ws) {
this.ws.close()
this.ws = null
}
console.log('WebSocket 已手动关闭')
}
/**
* 获取连接状态
*/
getReadyState(): number {
return this.ws ? this.ws.readyState : WebSocket.CLOSED
}
/**
* 是否已连接
*/
isConnected(): boolean {
return this.ws !== null && this.ws.readyState === WebSocket.OPEN
}
}
// 导出单例
export const wsManager = new WebSocketManager()

View File

@@ -44,19 +44,19 @@ const {} = toRefs(data);
<div class="icon">
<svg-icon name="brand-email" size="24" />
</div>
<div>{{ designerDetail.email }}</div>
<div class="text">{{ designerDetail.email }}</div>
</div>
<div class="phone label">
<div class="icon">
<svg-icon name="brand-call" size="24" />
</div>
<div>{{ designerDetail.mobile }}</div>
<div class="text">{{ designerDetail.mobile }}</div>
</div>
<div class="address label" v-for="value in JSON.parse(designerDetail.socialLinks)">
<div class="icon">
<svg-icon name="brand-link" size="24" />
</div>
<div>{{value}}</div>
<div class="text">{{value}}</div>
</div>
</div>
<div class="about">
@@ -118,9 +118,20 @@ const {} = toRefs(data);
font-size: 1.4rem;
line-height: 100%;
color: #585858;
align-items: center;
&:last-child{
margin-bottom: 0;
}
> .text{
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
// word-break: break-word;
// white-space: normal;
}
}
}
> .about{

View File

@@ -49,6 +49,8 @@ const addShopping = () => {
const goShopping = () => {
if(!detail.value.price) return ElMessage.warning(t('brandDetail.addShoppingTip'))
// console.log(detail.value)
// return
let data = {
listingId: detail.value.id, //资产ID
title: detail.value.title, //标题
@@ -56,7 +58,7 @@ const goShopping = () => {
sellerId: detail.value.sellerId, //店铺ID
cover: detail.value.images.cover[0], //封面
amount: detail.value.price, //价格
// status: v.status, //状态
status: 1, //状态
// date: v.addTime, //添加时间
// tags: v.productCategory, //标签
// salesVolume: v.salesVolume, //销售量
@@ -163,7 +165,7 @@ defineExpose({})
<div class="detail">
<div class="name" @click="gobrand">{{ detail.shopName }}</div>
<div class="release-time">
<span>{{ $t('digitalDetail.ReleaseIn') }} {{ detail.updateTime }}</span>
<span>{{ $t('digitalDetail.ReleaseIn') }} {{ new Date(detail.updateTime).toLocaleString() }}</span>
</div>
</div>
</div>
@@ -332,6 +334,7 @@ defineExpose({})
gap: 1.4rem;
align-items: center;
width: min-content;
white-space: nowrap;
cursor: pointer;
> span{
font-weight: 500;

View File

@@ -41,6 +41,11 @@ const reset = () => {
getListingListData.isShowMark = false
getListingListData.isNoData = false
}
const clearData = () => {
commodityList.value = []
getListingListData.pageNum = 1
getListingListData.isNoData = true
}
const getListingMallList = ()=>{
getListingListData.isShowMark = true
@@ -86,7 +91,7 @@ onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({reset,commodityList,getListingListData})
defineExpose({reset,clearData,commodityList,getListingListData})
const {} = toRefs(data);
</script>
<template>

View File

@@ -56,7 +56,11 @@ const openDetail = (item) => {
const handleChange = (val) => {
categories.value = val.categories
gender.value = val.gender
commodityListRef.value.reset()
if(categories.value.length == 0 || gender.value.length == 0){
commodityListRef.value.clearData()
}else{
commodityListRef.value.reset()
}
}
const updateSort = () => {
commodityListRef.value.reset()

View File

@@ -38,7 +38,9 @@
v-if="userInfo.userId"
>
<template #reference>
<div class="icon"><svg-icon name="user_0" size="22" /></div>
<el-badge :value="unReadCount" :hidden="unReadCount < 1">
<div class="icon"><svg-icon name="user_0" size="22" /></div>
</el-badge>
</template>
<template #default>
<div class="profile-content">
@@ -55,10 +57,17 @@
<div class="icon"><svg-icon name="my_wardrobe" size="18" /></div>
<div class="label">{{ $t('MainHeader.MyWardrobe') }}</div>
</div>
<div class="nav-item" @click="onNotifications">
<div class="icon"><svg-icon name="notifications" size="14" /></div>
<div class="label">{{ $t('MainHeader.Notifications') }}</div>
</div>
<el-badge
:value="unReadCount"
class="nav-item badge"
:offset="[10, 0]"
:hidden="unReadCount < 1"
>
<div class="nav-item flex" @click="onNotifications">
<div class="icon"><svg-icon name="notifications" size="14" /></div>
<div class="label">{{ $t('MainHeader.Notifications') }}</div>
</div>
</el-badge>
<div class="nav-item" @click="onSettings">
<div class="icon"><svg-icon name="settings" size="16" /></div>
<div class="label">{{ $t('MainHeader.Settings') }}</div>
@@ -87,14 +96,17 @@
import myEvent from '@/utils/myEvent'
import { useI18n } from 'vue-i18n'
import { useUserInfoStore } from '@/stores/userInfo'
import { useGlobalStore } from '@/stores/global'
import { setUserLanguage } from '@/api/user'
const { t, locale } = useI18n()
const userInfoStore = useUserInfoStore()
const GLOABL = useGlobalStore()
const router = useRouter()
const route = useRoute()
const userInfo = computed(() => userInfoStore.state.userInfo)
const activePath = computed(() => route.path)
const unReadCount = computed(() => GLOABL.state.unReadMessageCount)
const navList1 = ref([
{
name: 'MainHeader.Home',
@@ -221,7 +233,7 @@
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
> .nav-item {
.nav-item {
height: 100%;
cursor: pointer;
display: flex;
@@ -278,7 +290,7 @@
margin: 1.2rem 1rem;
border-top: 0.1rem solid #c4c4c4;
}
> .nav-item {
.nav-item {
margin-left: 2rem;
margin-bottom: 1.2rem;
display: flex;
@@ -286,10 +298,14 @@
height: 2rem;
user-select: none;
cursor: pointer;
column-gap: 1rem;
&.badge {
margin: 0;
}
> .icon {
width: 2rem;
height: 2rem;
margin-right: 1rem;
// margin-right: 1rem;
}
> .label {
font-family: KaiseiOpti-Regular;

View File

@@ -1,166 +1,206 @@
<template>
<div class="notifications-view mini-scrollbar">
<section class="notifications-hero">
<div class="notifications-hero__content">
<h1 class="notifications-hero__title">Notifications</h1>
<p class="notifications-hero__subtitle">System announcements and updates</p>
</div>
</section>
<div class="notifications-view mini-scrollbar" @scroll="handleScroll">
<section class="notifications-hero">
<div class="notifications-hero__content">
<h1 class="notifications-hero__title">Notifications</h1>
<p class="notifications-hero__subtitle">System announcements and updates</p>
</div>
</section>
<section class="notifications-content">
<NotificationsList
:items="notifications"
:unread-count="unreadCount"
@toggle-item="handleToggleItem"
@mark-all-as-read="handleMarkAllAsRead"
/>
</section>
</div>
<section class="notifications-content">
<NotificationsList
:items="notifications"
:unread-count="unreadCount"
@toggle-item="handleToggleItem"
@mark-all-as-read="handleMarkAllAsRead"
/>
<div v-if="loading" class="loading-indicator">Loading...</div>
<div v-if="!hasMore && notifications.length > 0" class="no-more-data">No more notifications</div>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import NotificationsList from './components/NotificationsList.vue'
import type { NotificationRecord } from './types'
import { computed, ref, onMounted } from 'vue'
import NotificationsList from './components/NotificationsList.vue'
import type { NotificationRecord } from './types'
import { fetchAllMessageList, markMessageAsRead, markAllMessagesAsRead } from '@/api/notification'
const notifications = ref<NotificationRecord[]>([
{
id: 'maintenance-mar-10-primary',
title: 'Platform Maintenance Notice - Mar 10, 2026',
date: 'Mar 6, 2026',
content:
'We will perform scheduled platform maintenance on Mar 10, 2026 to improve checkout stability and notification delivery. During this window, a few account features may respond more slowly than usual.',
isUnread: true,
isExpanded: false
},
{
id: 'maintenance-mar-10-reminder',
title: 'Platform Maintenance Notice - Mar 10, 2026',
date: 'Feb 28, 2026',
content:
'This is an early reminder for the Mar 10 maintenance window. Please avoid making urgent profile or order updates right before the scheduled service period.',
isUnread: true,
isExpanded: false
},
{
id: 'terms-mar-1',
title: 'Updated Terms of Service - Effective Mar 1, 2026',
date: 'Feb 20, 2026',
content:
'We updated our Terms of Service to clarify digital item ownership, payment processing responsibilities, and account conduct expectations. Please review the new terms before your next purchase.',
isUnread: true,
isExpanded: false
},
{
id: 'welcome',
title: 'Welcome to Stylish Parade',
date: 'Jan 4, 2026',
content:
'Thanks for joining Stylish Parade. Explore brand stories, save your favorite pieces, and keep your profile updated so we can recommend the right collections for you.',
isUnread: false,
isExpanded: false
},
{
id: 'holiday-support',
title: 'Platform Maintenance Notice - Mar 10, 2026',
date: 'Dec 20, 2025',
content:
'Our customer support team will have limited availability during the holiday season from Dec 24 to Jan 2. Response times may be longer than usual. The platform will remain fully operational throughout this period. We wish you a wonderful holiday season.',
isUnread: false,
isExpanded: true
}
])
const notifications = ref<NotificationRecord[]>([])
const loading = ref(false)
const hasMore = ref(true)
const unreadCount = computed(() => notifications.value.filter((item) => item.isUnread).length)
const unreadCount = computed(() => notifications.value.filter((item) => item.isUnread).length)
const handleToggleItem = (id: NotificationRecord['id']) => {
const targetItem = notifications.value.find((item) => item.id === id)
const handleToggleItem = async (id: NotificationRecord['id']) => {
const targetItem = notifications.value.find((item) => item.id === id)
if (!targetItem) return
if (!targetItem) return
const nextExpanded = !targetItem.isExpanded
const nextExpanded = !targetItem.isExpanded
notifications.value = notifications.value.map((item) => ({
...item,
isUnread: item.id === id ? false : item.isUnread,
isExpanded: item.id === id ? nextExpanded : false
}))
}
// 如果消息是未读状态调用API标记为已读
if (targetItem.isUnread) {
try {
await markMessageAsRead(id)
} catch (error) {
console.error('Failed to mark message as read:', error)
}
}
const handleMarkAllAsRead = () => {
notifications.value = notifications.value.map((item) => ({
...item,
isUnread: false
}))
}
notifications.value = notifications.value.map((item) => ({
...item,
isUnread: item.id === id ? false : item.isUnread,
isExpanded: item.id === id ? nextExpanded : false
}))
}
const handleMarkAllAsRead = async () => {
try {
await markAllMessagesAsRead()
notifications.value = notifications.value.map((item) => ({
...item,
isUnread: false
}))
} catch (error) {
console.error('Failed to mark all messages as read:', error)
}
}
const params = ref({
page: 1,
size: 15
})
const handleFetchMessageList = async () => {
if (loading.value || !hasMore.value) return
loading.value = true
try {
const res = await fetchAllMessageList(params.value)
// Transform API data to match NotificationRecord interface
const newNotifications: NotificationRecord[] = res.content.map((item: any) => ({
id: String(item.id),
title: item.title,
date: item.createTime,
content: item.content,
isUnread: item.isRead === 0,
isExpanded: false
}))
if (params.value.page === 1) {
notifications.value = newNotifications
} else {
notifications.value = [...notifications.value, ...newNotifications]
}
// Check if there are more pages
hasMore.value = params.value.page < res.pages
} catch (error) {
console.error('Failed to fetch notifications:', error)
} finally {
loading.value = false
}
}
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement
const scrollTop = target.scrollTop
const scrollHeight = target.scrollHeight
const clientHeight = target.clientHeight
// Load more when scrolled to 80% of the content
if (scrollTop + clientHeight >= scrollHeight * 0.8 && !loading.value && hasMore.value) {
params.value.page++
handleFetchMessageList()
}
}
onMounted(() => {
handleFetchMessageList()
})
</script>
<style lang="less" scoped>
.notifications-view {
height: 100%;
overflow-y: auto;
background: #ffffff;
}
.notifications-view {
height: 100%;
overflow-y: auto;
background: #ffffff;
}
.notifications-hero {
min-height: 14.8rem;
padding: 3.2rem 2rem;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, rgba(245, 243, 240, 0.92) 0%, rgba(250, 249, 246, 0.95) 100%),
linear-gradient(
90deg,
rgba(227, 221, 212, 0.28) 0%,
rgba(255, 255, 255, 0.24) 50%,
rgba(227, 221, 212, 0.28) 100%
);
border-bottom: 0.05rem solid #dfd8d1;
}
.notifications-hero {
min-height: 14.8rem;
padding: 3.2rem 2rem;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
180deg,
rgba(245, 243, 240, 0.92) 0%,
rgba(250, 249, 246, 0.95) 100%
),
linear-gradient(
90deg,
rgba(227, 221, 212, 0.28) 0%,
rgba(255, 255, 255, 0.24) 50%,
rgba(227, 221, 212, 0.28) 100%
);
border-bottom: 0.05rem solid #dfd8d1;
}
.notifications-hero__content {
text-align: center;
}
.notifications-hero__content {
text-align: center;
}
.notifications-hero__title {
margin: 0;
color: #232323;
font-size: 4rem;
line-height: 1;
font-family: 'KaiseiOpti-Bold';
}
.notifications-hero__title {
margin: 0;
color: #232323;
font-size: 4rem;
line-height: 1;
font-family: 'KaiseiOpti-Bold';
}
.notifications-hero__subtitle {
margin: 1.2rem 0 0;
color: #585858;
font-size: 1.6rem;
line-height: 1.4;
font-family: 'KaiseiOpti-Regular';
}
.notifications-hero__subtitle {
margin: 1.2rem 0 0;
color: #585858;
font-size: 1.6rem;
line-height: 1.4;
font-family: 'KaiseiOpti-Regular';
}
.notifications-content {
width: min(108rem, calc(100% - 4rem));
margin: 0 auto;
padding: 4rem 0 8rem;
}
.notifications-content {
width: min(108rem, calc(100% - 4rem));
margin: 0 auto;
padding: 4rem 0 8rem;
}
@media (max-width: 768px) {
.notifications-hero {
min-height: 12rem;
padding: 2.8rem 1.6rem;
}
.loading-indicator,
.no-more-data {
text-align: center;
padding: 2rem;
color: #979797;
font-size: 1.4rem;
font-family: 'KaiseiOpti-Regular';
}
.notifications-hero__title {
font-size: 3rem;
}
@media (max-width: 768px) {
.notifications-hero {
min-height: 12rem;
padding: 2.8rem 1.6rem;
}
.notifications-hero__subtitle {
margin-top: 0.8rem;
font-size: 1.4rem;
}
.notifications-hero__title {
font-size: 3rem;
}
.notifications-content {
width: calc(100% - 3.2rem);
padding: 3.2rem 0 4.8rem;
}
}
.notifications-hero__subtitle {
margin-top: 0.8rem;
font-size: 1.4rem;
}
.notifications-content {
width: calc(100% - 3.2rem);
padding: 3.2rem 0 4.8rem;
}
}
</style>

View File

@@ -289,15 +289,15 @@
title: item.listingName,
brand: order.shopName,
cover: item.thumbnailUrl,
amount: item.price
// status:order
amount: item.price,
status:1
})
})
// 2060253365078691800
// "https://checkout.stripe.com/c/pay/cs_test_a1zdyl7iR9sIEWArSQyUaOFIax6Pia0S7GJNXqvLOFfy2w57JVlVAV5Jlm#fidnandhYHdWcXxpYCc%2FJ2FgY2RwaXEnKSdpamZkaWAnPydgaycpJ2JwZGZkaGppYFNkd2xka3EnPydmamtxd2ppJyknZHVsTmB8Jz8ndW5acWB2cVowNFUxX19JNTdrNFFAfGF8S2lwbXEzU0F9a0dIakdyPVYwNzFqcX9pXVM2RkhGX0w9TWhuZlB3NkZOfD1fNWBUN1J2dlZPPHZEZF9rTFRPTDUxY0RXTU1PbDU1dWZMRzF2TlAnKSdjd2poVmB3c2B3Jz9xd3BgKSdnZGZuYndqcGthRmppancnPycmY2NjY2NjJyknaWR8anBxUXx1YCc%2FJ3Zsa2JpYFpscWBoJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl"
console.log(list)
const params = btoa(encodeURIComponent(JSON.stringify({ list })))
const params = btoa(encodeURIComponent(JSON.stringify(list)))
ROUTER.push({
name: 'pay',
query: {