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