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