Files
websopy-mp/src/developer/notification/index.tsx
赵忠林 ffab0ec25c feat(developer): 完成小程序开发者中心和企业控制台改造
- 设计并实现了开发者中心与企业控制台两大模块
- 按用户角色区分开发者和企业客户,支持多项目类型及成员管理
- 新增项目管理、应用管理、API Key管理及成员邀请等多功能页面
- 实现应用版本发布、消息通知中心、权限审批与开发者申请流程
- 完成CI/CD流水线、运营监控、发票管理、SSO单点登录功能
- 搭建SDK下载中心、工单系统、FAQ系统、数据导入导出等模块
- 优化后端API,支持已登录和未注册用户不同加入应用流程
- 前端按钮统一采用微信手机号授权,完善用户授权体验
- 修复多个页面的JSX语法错误及依赖导入问题,替换部分组件库
- 增加详细的类型定义文件,提升项目类型安全
- 新增超过55个页面及60个API接口,扩展应用功能和服务体系
- 完成全面的样式设计,实现一致的视觉风格和交互体验
2026-04-13 02:26:46 +08:00

289 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import { View, Text, ActionSheet } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { usePullDownRefresh } from '@tarojs/taro'
import {
pageNotification,
getUnreadCount,
markAsRead,
markAllAsRead,
deleteNotification,
} from '@/api/developer/developer'
import type { Notification, NotificationParam, NotificationType } from '@/types/developer'
import './index.scss'
// 通知类型配置
const TYPE_CONFIG: Record<NotificationType, { label: string; icon: string; color: string }> = {
system: { label: '系统通知', icon: '🔔', color: '#1890ff' },
app: { label: '应用通知', icon: '📱', color: '#52c41a' },
member: { label: '成员通知', icon: '👥', color: '#722ed1' },
order: { label: '订单通知', icon: '💰', color: '#fa8c16' },
developer: { label: '开发通知', icon: '🛠️', color: '#13c2c2' },
}
const NotificationPage: React.FC = () => {
const [loading, setLoading] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [list, setList] = useState<Notification[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [unreadCount, setUnreadCount] = useState(0)
const [currentTab, setCurrentTab] = useState<NotificationType | 'all'>('all')
// 加载数据
const loadData = async (pageNum: number = 1, isRefresh = false) => {
if (loading) return
setLoading(true)
if (isRefresh) setRefreshing(true)
try {
const params: NotificationParam = {
page: pageNum,
limit: 20,
type: currentTab === 'all' ? undefined : currentTab,
}
const data = await pageNotification(params)
if (pageNum === 1) {
setList(data?.records || [])
} else {
setList(prev => [...prev, ...(data?.records || [])])
}
const total = data?.total || 0
const records = data?.records || []
setHasMore(records.length > 0 && (pageNum * 20) < total)
setPage(pageNum)
} catch (err) {
console.error('加载失败', err)
} finally {
setLoading(false)
setRefreshing(false)
}
}
// 加载未读数
const loadUnreadCount = async () => {
try {
const data = await getUnreadCount()
setUnreadCount(data?.count || 0)
} catch (err) {
console.error('获取未读数失败', err)
}
}
useEffect(() => {
loadData(1)
loadUnreadCount()
}, [currentTab])
// 下拉刷新
usePullDownRefresh(() => {
loadData(1, true)
loadUnreadCount()
})
// 加载更多
const onReachBottom = () => {
if (hasMore && !loading) {
loadData(page + 1)
}
}
// 标记已读
const handleRead = async (item: Notification) => {
if (item.isRead) return
try {
await markAsRead(item.id!)
setList(prev =>
prev.map(n => (n.id === item.id ? { ...n, isRead: true } : n))
)
setUnreadCount(prev => Math.max(0, prev - 1))
} catch (err) {
console.error('标记已读失败', err)
}
}
// 全部已读
const handleMarkAllRead = async () => {
try {
await markAllAsRead()
setList(prev => prev.map(n => ({ ...n, isRead: true })))
setUnreadCount(0)
Taro.showToast({ title: '已全部已读', icon: 'success' })
} catch (err) {
console.error('标记全部已读失败', err)
Taro.showToast({ title: '操作失败', icon: 'none' })
}
}
// 删除
const handleDelete = (item: Notification) => {
ActionSheet({
alertText: '确定删除这条通知吗?',
actions: [{ name: '删除', color: '#ff4d4f', type: 'warn' as const }],
confirmText: '取消',
}).then(res => {
if (res.confirm) {
doDelete(item)
}
}).catch(() => {})
}
const doDelete = async (item: Notification) => {
try {
await deleteNotification(item.id!)
setList(prev => prev.filter(n => n.id !== item.id))
if (!item.isRead) {
setUnreadCount(prev => Math.max(0, prev - 1))
}
Taro.showToast({ title: '已删除', icon: 'success' })
} catch (err) {
console.error('删除失败', err)
Taro.showToast({ title: '删除失败', icon: 'none' })
}
}
// 点击通知
const handleClick = (item: Notification) => {
handleRead(item)
// 根据通知类型和内容跳转
if (item.data?.url) {
Taro.navigateTo({ url: item.data.url })
} else if (item.data?.appId) {
Taro.navigateTo({ url: `/developer/app/${item.data.appId}/index` })
} else if (item.data?.projectId) {
Taro.navigateTo({ url: `/developer/project/${item.data.projectId}/index` })
}
}
// 格式化日期
const formatDate = (dateStr?: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) {
return `今天 ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
} else if (days === 1) {
return '昨天'
} else if (days < 7) {
return `${days}天前`
} else {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
}
// Tab 配置
const tabs: { key: NotificationType | 'all'; label: string }[] = [
{ key: 'all', label: '全部' },
{ key: 'system', label: '系统' },
{ key: 'app', label: '应用' },
{ key: 'member', label: '成员' },
{ key: 'order', label: '订单' },
]
return (
<View className="notification-page">
{/* 头部 */}
<View className="notification-page__header">
<View className="notification-page__header-left">
<Text className="notification-page__title">🔔 </Text>
{unreadCount > 0 && (
<View className="notification-page__badge">
<Text>{unreadCount > 99 ? '99+' : unreadCount}</Text>
</View>
)}
</View>
{unreadCount > 0 && (
<Text
className="notification-page__mark-read"
onClick={handleMarkAllRead}
>
</Text>
)}
</View>
{/* Tab */}
<View className="notification-page__tabs">
{tabs.map((tab) => (
<View
key={tab.key}
className={`notification-page__tab ${currentTab === tab.key ? 'active' : ''}`}
onClick={() => setCurrentTab(tab.key)}
>
{tab.label}
</View>
))}
</View>
{/* 列表 */}
<View className="notification-page__list">
{list.length === 0 && !loading ? (
<View className="notification-page__empty">
<Text className="notification-page__empty-icon">📭</Text>
<Text className="notification-page__empty-text"></Text>
</View>
) : (
list.map((item) => (
<View
key={item.id}
className={`notification-card ${item.isRead ? 'read' : 'unread'}`}
onClick={() => handleClick(item)}
>
<View className="notification-card__icon">
<Text style={{ color: TYPE_CONFIG[item.type || 'system']?.color }}>
{TYPE_CONFIG[item.type || 'system']?.icon}
</Text>
</View>
<View className="notification-card__content">
<View className="notification-card__header">
<Text className="notification-card__title">{item.title}</Text>
{!item.isRead && <View className="notification-card__dot" />}
</View>
<Text className="notification-card__body" numberOfLines={2}>
{item.content}
</Text>
<Text className="notification-card__time">
{formatDate(item.createTime)}
</Text>
</View>
<View
className="notification-card__delete"
onClick={(e) => {
e.stopPropagation()
handleDelete(item)
}}
>
<Text>×</Text>
</View>
</View>
))
)}
{/* 加载状态 */}
{loading && list.length > 0 && (
<View className="notification-page__loading">
<Text>...</Text>
</View>
)}
{!hasMore && list.length > 0 && (
<View className="notification-page__no-more">
<Text></Text>
</View>
)}
</View>
</View>
)
}
export default NotificationPage