feat(developer): 完成小程序开发者中心和企业控制台改造

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

View 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