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 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 = http://192.168.31.82:10094
VITE_APP_URL = https://www.develop-ms.api.aida.com.hk 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> </template>
<script setup lang="ts"> <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 RouteCache from '@/components/RouteCache.vue'
import MainHeader from '@/views/main-header.vue' import MainHeader from '@/views/main-header.vue'
import LoginDialog from '@/views/login/login-dialog.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 ShoppingDrawer from '@/views/shopping-drawer.vue'
import { wsManager } from '@/utils/websocket'
const globalStore = useGlobalStore() const globalStore = useGlobalStore()
const userInfoStore = useUserInfoStore()
const loading = computed(() => globalStore.state.loading) const loading = computed(() => globalStore.state.loading)
globalStore.setLoading(false) globalStore.setLoading(false)
const viewRef = ref() const viewRef = ref()
@@ -26,12 +29,41 @@
viewStyle.value['--app-view-width'] = width + 'px' viewStyle.value['--app-view-width'] = width + 'px'
viewStyle.value['--app-view-height'] = height + '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(() => { onMounted(() => {
observer.observe(viewRef.value) observer.observe(viewRef.value)
// 如果已经有 token立即建立连接
const token = userInfoStore.state.token
if (token) {
console.log('应用启动时检测到 token建立 WebSocket 连接')
wsManager.connect(token)
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
observer.disconnect() observer.disconnect()
// 组件卸载时关闭 WebSocket 连接
wsManager.close()
}) })
window['onClickPrivacy'] = () => { window['onClickPrivacy'] = () => {
const e = window.event || event const e = window.event || event
e.stopPropagation() 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) => { const handleChange = (val) => {
let data = val.filter(item => item !== 'all') let data = val.filter(item => item !== 'all')
if(val.length == 0)return
if(data.length == props.list.length){ if(data.length == props.list.length){
data = ['all'] data = ['all']
}else{ }else{
@@ -39,9 +38,11 @@ const handleCheckAllChange = (val) => {
if(val && props.selected[0] !== 'all'){ if(val && props.selected[0] !== 'all'){
data = ['all'] data = ['all']
// data = props.list.map(item => item.value) // data = props.list.map(item => item.value)
}else{
data = []
}
emit('update:selected', data) emit('update:selected', data)
emit('change', data) emit('change', data)
}
} }
let data = reactive({ let data = reactive({
}) })

View File

@@ -1,11 +1,13 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { useUserInfoStore } from '@/stores/userInfo' import { useUserInfoStore } from '@/stores/userInfo'
import { useGlobalStore } from '@/stores/global'
import { getUserLanguage } from '@/api/user' import { getUserLanguage } from '@/api/user'
import { fetchAllUnreadMessage } from '@/api/notification'
import i18n from '@/lang/index' import i18n from '@/lang/index'
// 语言映射:后端格式 -> i18n 格式 // 语言映射:后端格式 -> i18n 格式
const backendToI18nLanguage: Record<string, string> = { const backendToI18nLanguage: Record<string, string> = {
'en': 'ENGLISH', en: 'ENGLISH',
'zh-CN': 'CHINESE_SIMPLIFIED' 'zh-CN': 'CHINESE_SIMPLIFIED'
} }
@@ -73,14 +75,14 @@ const router = createRouter({
component: () => import('@/views/wardrobe/index.vue') component: () => import('@/views/wardrobe/index.vue')
}, },
{ {
path:'/account', path: '/account',
name:'account', name: 'account',
component:()=>import('@/views/account/index.vue') component: () => import('@/views/account/index.vue')
}, },
{ {
path:'/pay', path: '/pay',
name:'pay', name: 'pay',
component:()=>import('@/views/pay/index.vue') component: () => import('@/views/pay/index.vue')
}, },
{ {
path: '/:pathMatch(.*)', path: '/:pathMatch(.*)',
@@ -96,16 +98,20 @@ router.beforeEach((to, from, next) => {
}) })
router.afterEach(async () => { router.afterEach(async () => {
// 检查用户是否已登录
const userInfoStore = useUserInfoStore() const userInfoStore = useUserInfoStore()
const token = userInfoStore.state.token const token = userInfoStore.state.token
if (!token) { if (!token) {
languageSynced = false // 未登录时重置同步状态 languageSynced = false
return return
} }
// 如果已经同步过,跳过 const globalStore = useGlobalStore()
fetchAllUnreadMessage().then((res) => {
console.log(res)
globalStore.setUnredCount(res.totalUnread)
})
if (languageSynced) { if (languageSynced) {
return return
} }

View File

@@ -2,13 +2,20 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
export const useGlobalStore = defineStore('global', () => { export const useGlobalStore = defineStore('global', () => {
const state = ref({ 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 { return {
state, state,
setLoading, 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"> <div class="icon">
<svg-icon name="brand-email" size="24" /> <svg-icon name="brand-email" size="24" />
</div> </div>
<div>{{ designerDetail.email }}</div> <div class="text">{{ designerDetail.email }}</div>
</div> </div>
<div class="phone label"> <div class="phone label">
<div class="icon"> <div class="icon">
<svg-icon name="brand-call" size="24" /> <svg-icon name="brand-call" size="24" />
</div> </div>
<div>{{ designerDetail.mobile }}</div> <div class="text">{{ designerDetail.mobile }}</div>
</div> </div>
<div class="address label" v-for="value in JSON.parse(designerDetail.socialLinks)"> <div class="address label" v-for="value in JSON.parse(designerDetail.socialLinks)">
<div class="icon"> <div class="icon">
<svg-icon name="brand-link" size="24" /> <svg-icon name="brand-link" size="24" />
</div> </div>
<div>{{value}}</div> <div class="text">{{value}}</div>
</div> </div>
</div> </div>
<div class="about"> <div class="about">
@@ -118,9 +118,20 @@ const {} = toRefs(data);
font-size: 1.4rem; font-size: 1.4rem;
line-height: 100%; line-height: 100%;
color: #585858; color: #585858;
align-items: center;
&:last-child{ &:last-child{
margin-bottom: 0; margin-bottom: 0;
} }
> .text{
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
// word-break: break-word;
// white-space: normal;
}
} }
} }
> .about{ > .about{

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="notifications-view mini-scrollbar"> <div class="notifications-view mini-scrollbar" @scroll="handleScroll">
<section class="notifications-hero"> <section class="notifications-hero">
<div class="notifications-hero__content"> <div class="notifications-hero__content">
<h1 class="notifications-hero__title">Notifications</h1> <h1 class="notifications-hero__title">Notifications</h1>
@@ -14,100 +14,131 @@
@toggle-item="handleToggleItem" @toggle-item="handleToggleItem"
@mark-all-as-read="handleMarkAllAsRead" @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> </section>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref, onMounted } from 'vue'
import NotificationsList from './components/NotificationsList.vue' import NotificationsList from './components/NotificationsList.vue'
import type { NotificationRecord } from './types' import type { NotificationRecord } from './types'
import { fetchAllMessageList, markMessageAsRead, markAllMessagesAsRead } from '@/api/notification'
const notifications = ref<NotificationRecord[]>([ const notifications = ref<NotificationRecord[]>([])
{ const loading = ref(false)
id: 'maintenance-mar-10-primary', const hasMore = ref(true)
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 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 handleToggleItem = async (id: NotificationRecord['id']) => {
const targetItem = notifications.value.find((item) => item.id === id) const targetItem = notifications.value.find((item) => item.id === id)
if (!targetItem) return if (!targetItem) return
const nextExpanded = !targetItem.isExpanded const nextExpanded = !targetItem.isExpanded
// 如果消息是未读状态调用API标记为已读
if (targetItem.isUnread) {
try {
await markMessageAsRead(id)
} catch (error) {
console.error('Failed to mark message as read:', error)
}
}
notifications.value = notifications.value.map((item) => ({ notifications.value = notifications.value.map((item) => ({
...item, ...item,
isUnread: item.id === id ? false : item.isUnread, isUnread: item.id === id ? false : item.isUnread,
isExpanded: item.id === id ? nextExpanded : false isExpanded: item.id === id ? nextExpanded : false
})) }))
} }
const handleMarkAllAsRead = () => { const handleMarkAllAsRead = async () => {
try {
await markAllMessagesAsRead()
notifications.value = notifications.value.map((item) => ({ notifications.value = notifications.value.map((item) => ({
...item, ...item,
isUnread: false 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> </script>
<style lang="less" scoped> <style lang="less" scoped>
.notifications-view { .notifications-view {
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
background: #ffffff; background: #ffffff;
} }
.notifications-hero { .notifications-hero {
min-height: 14.8rem; min-height: 14.8rem;
padding: 3.2rem 2rem; padding: 3.2rem 2rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(180deg, rgba(245, 243, 240, 0.92) 0%, rgba(250, 249, 246, 0.95) 100%), background: linear-gradient(
180deg,
rgba(245, 243, 240, 0.92) 0%,
rgba(250, 249, 246, 0.95) 100%
),
linear-gradient( linear-gradient(
90deg, 90deg,
rgba(227, 221, 212, 0.28) 0%, rgba(227, 221, 212, 0.28) 0%,
@@ -115,35 +146,44 @@ const handleMarkAllAsRead = () => {
rgba(227, 221, 212, 0.28) 100% rgba(227, 221, 212, 0.28) 100%
); );
border-bottom: 0.05rem solid #dfd8d1; border-bottom: 0.05rem solid #dfd8d1;
} }
.notifications-hero__content { .notifications-hero__content {
text-align: center; text-align: center;
} }
.notifications-hero__title { .notifications-hero__title {
margin: 0; margin: 0;
color: #232323; color: #232323;
font-size: 4rem; font-size: 4rem;
line-height: 1; line-height: 1;
font-family: 'KaiseiOpti-Bold'; font-family: 'KaiseiOpti-Bold';
} }
.notifications-hero__subtitle { .notifications-hero__subtitle {
margin: 1.2rem 0 0; margin: 1.2rem 0 0;
color: #585858; color: #585858;
font-size: 1.6rem; font-size: 1.6rem;
line-height: 1.4; line-height: 1.4;
font-family: 'KaiseiOpti-Regular'; font-family: 'KaiseiOpti-Regular';
} }
.notifications-content { .notifications-content {
width: min(108rem, calc(100% - 4rem)); width: min(108rem, calc(100% - 4rem));
margin: 0 auto; margin: 0 auto;
padding: 4rem 0 8rem; padding: 4rem 0 8rem;
} }
@media (max-width: 768px) { .loading-indicator,
.no-more-data {
text-align: center;
padding: 2rem;
color: #979797;
font-size: 1.4rem;
font-family: 'KaiseiOpti-Regular';
}
@media (max-width: 768px) {
.notifications-hero { .notifications-hero {
min-height: 12rem; min-height: 12rem;
padding: 2.8rem 1.6rem; padding: 2.8rem 1.6rem;
@@ -162,5 +202,5 @@ const handleMarkAllAsRead = () => {
width: calc(100% - 3.2rem); width: calc(100% - 3.2rem);
padding: 3.2rem 0 4.8rem; padding: 3.2rem 0 4.8rem;
} }
} }
</style> </style>

View File

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