- 添加编辑和新增收货地址页面,支持表单数据加载和提交 - 新增应用密钥凭证、新增应用操作动态、新增应用成员、新增应用版本页面配置 - 实现文章新增及编辑页面,包含图片上传及多种文章属性配置 - 增加注册会员页面,支持头像上传、手机号获取和邀请人关系处理 - 引入统一表单提交成功和失败处理,支持编辑模式数据回显 - 配置统一eslint和editorconfig规则,增强代码规范和编辑体验 - 新增.gitignore规则,屏蔽无关文件和目录,优化版本管理
207 lines
6.9 KiB
TypeScript
207 lines
6.9 KiB
TypeScript
import { useCallback, useState } from 'react'
|
|
import Taro, { useDidShow } from '@tarojs/taro'
|
|
import { View, Text } from '@tarojs/components'
|
|
import { Cell, CellGroup, Empty, InfiniteLoading, PullToRefresh, Tag, Button } from '@nutui/nutui-react-taro'
|
|
import { useThemeStyles } from '@/hooks/useTheme'
|
|
import { pageNotification, getUnreadCount, markNotificationRead, markAllNotificationRead } from '@/api/app/notification'
|
|
import type { Notification } from '@/api/app/notification/model'
|
|
import dayjs from 'dayjs'
|
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
import 'dayjs/locale/zh-cn'
|
|
|
|
dayjs.extend(relativeTime)
|
|
dayjs.locale('zh-cn')
|
|
|
|
/** 通知类型配置 */
|
|
const TYPE_CONFIG: Record<string, { label: string; color: string }> = {
|
|
system: { label: '系统', color: '#3b82f6' },
|
|
ticket: { label: '工单', color: '#f59e0b' },
|
|
review: { label: '审核', color: '#8b5cf6' },
|
|
resource: { label: '资源', color: '#10b981' },
|
|
permission: { label: '权限', color: '#ef4444' },
|
|
member: { label: '成员', color: '#06b6d4' },
|
|
payment: { label: '支付', color: '#ec4899' },
|
|
}
|
|
|
|
const NotificationPage = () => {
|
|
const themeStyles = useThemeStyles()
|
|
const [list, setList] = useState<Notification[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [hasMore, setHasMore] = useState(true)
|
|
const [page, setPage] = useState(1)
|
|
const [unreadTotal, setUnreadTotal] = useState(0)
|
|
|
|
const loadNotifications = useCallback(async (isRefresh = false) => {
|
|
if (loading) return
|
|
setLoading(true)
|
|
try {
|
|
const currentPage = isRefresh ? 1 : page
|
|
const res = await pageNotification({ page: currentPage, limit: 20 })
|
|
const newList = res?.list || []
|
|
if (isRefresh) {
|
|
setList(newList)
|
|
setPage(2)
|
|
} else {
|
|
setList(prev => [...prev, ...newList])
|
|
setPage(prev => prev + 1)
|
|
}
|
|
setHasMore(newList.length >= 20)
|
|
} catch (e) {
|
|
console.error('加载通知失败', e)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [loading, page])
|
|
|
|
const loadUnreadCount = useCallback(async () => {
|
|
try {
|
|
const res = await getUnreadCount()
|
|
setUnreadTotal(res?.total || 0)
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}, [])
|
|
|
|
useDidShow(() => {
|
|
loadNotifications(true)
|
|
loadUnreadCount()
|
|
})
|
|
|
|
const handleMarkRead = async (item: Notification) => {
|
|
if (item.isRead === 1 || !item.id) return
|
|
try {
|
|
await markNotificationRead(item.id)
|
|
setList(prev => prev.map(n => n.id === item.id ? { ...n, isRead: 1 } : n))
|
|
loadUnreadCount()
|
|
} catch (e) {
|
|
console.error('标记已读失败', e)
|
|
}
|
|
}
|
|
|
|
const handleMarkAllRead = async () => {
|
|
try {
|
|
await markAllNotificationRead()
|
|
setList(prev => prev.map(n => ({ ...n, isRead: 1 })))
|
|
setUnreadTotal(0)
|
|
Taro.showToast({ title: '已全部标记为已读', icon: 'success' })
|
|
} catch (e) {
|
|
console.error('标记全部已读失败', e)
|
|
}
|
|
}
|
|
|
|
const handleClick = (item: Notification) => {
|
|
handleMarkRead(item)
|
|
if (item.linkUrl) {
|
|
// 外链通过 WebView 打开
|
|
Taro.navigateTo({ url: `/passport/webview/index?url=${encodeURIComponent(item.linkUrl)}` })
|
|
}
|
|
}
|
|
|
|
const formatTime = (time?: string) => {
|
|
if (!time) return ''
|
|
return dayjs(time).fromNow()
|
|
}
|
|
|
|
if (list.length === 0 && !loading) {
|
|
return (
|
|
<View className="bg-gray-100 min-h-screen">
|
|
<View className="px-4 py-6" style={themeStyles.primaryBackground}>
|
|
<Text className="text-white text-lg font-bold">消息通知</Text>
|
|
</View>
|
|
<Empty description="暂无通知" imageSize={80} className="mt-20" />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<View className="bg-gray-100 min-h-screen">
|
|
{/* 头部 */}
|
|
<View className="px-4 py-6" style={themeStyles.primaryBackground}>
|
|
<View className="flex items-center justify-between">
|
|
<View>
|
|
<Text className="text-white text-lg font-bold">消息通知</Text>
|
|
{unreadTotal > 0 && (
|
|
<View className="mt-1">
|
|
<Text className="text-white text-sm opacity-80">{unreadTotal} 条未读</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
{unreadTotal > 0 && (
|
|
<Button
|
|
size="small"
|
|
style={{
|
|
background: 'rgba(255, 255, 255, 0.18)',
|
|
color: '#fff',
|
|
border: '1px solid rgba(255, 255, 255, 0.25)'
|
|
}}
|
|
onClick={handleMarkAllRead}
|
|
>
|
|
全部已读
|
|
</Button>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<PullToRefresh onRefresh={() => loadNotifications(true)}>
|
|
{/* 通知列表 */}
|
|
<View className="mx-4 mt-4 rounded-xl overflow-hidden bg-white">
|
|
{list.map((item) => {
|
|
const typeConf = TYPE_CONFIG[item.type || ''] || TYPE_CONFIG.system
|
|
return (
|
|
<View
|
|
key={item.id}
|
|
className={`px-4 py-3 border-b border-gray-50 ${item.isRead === 0 ? 'bg-blue-500' : ''}`}
|
|
onClick={() => handleClick(item)}
|
|
>
|
|
<View className="flex items-start gap-3">
|
|
{/* 未读标记 */}
|
|
<View className="mt-2">
|
|
{item.isRead === 0 ? (
|
|
<View className="w-2 h-2 bg-blue-500 rounded-full" />
|
|
) : (
|
|
<View className="w-2 h-2 bg-transparent" />
|
|
)}
|
|
</View>
|
|
|
|
{/* 内容 */}
|
|
<View className="flex-1 min-w-0">
|
|
<View className="flex items-center gap-2 mb-1">
|
|
<Tag type="primary" plain style={{ fontSize: '10px', padding: '0 4px' }}>
|
|
{typeConf.label}
|
|
</Tag>
|
|
<Text className={`text-sm ${item.isRead === 0 ? 'font-semibold text-gray-900' : 'text-gray-600'} truncate`}>
|
|
{item.title || '系统通知'}
|
|
</Text>
|
|
</View>
|
|
{item.content && (
|
|
<Text className="text-xs text-gray-400 line-clamp-2">{item.content}</Text>
|
|
)}
|
|
<View className="mt-1">
|
|
<Text className="text-xs text-gray-300">{formatTime(item.createTime)}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)
|
|
})}
|
|
</View>
|
|
|
|
{/* 加载更多 */}
|
|
<InfiniteLoading
|
|
hasMore={hasMore}
|
|
onLoadMore={() => loadNotifications(false)}
|
|
loading={loading}
|
|
>
|
|
<View className="py-4 text-center">
|
|
<Text className="text-xs text-gray-400">
|
|
{loading ? '加载中...' : hasMore ? '下拉加载更多' : '没有更多了'}
|
|
</Text>
|
|
</View>
|
|
</InfiniteLoading>
|
|
</PullToRefresh>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
export default NotificationPage
|