feat(add): 新增多页面新增和编辑表单功能

- 添加编辑和新增收货地址页面,支持表单数据加载和提交
- 新增应用密钥凭证、新增应用操作动态、新增应用成员、新增应用版本页面配置
- 实现文章新增及编辑页面,包含图片上传及多种文章属性配置
- 增加注册会员页面,支持头像上传、手机号获取和邀请人关系处理
- 引入统一表单提交成功和失败处理,支持编辑模式数据回显
- 配置统一eslint和editorconfig规则,增强代码规范和编辑体验
- 新增.gitignore规则,屏蔽无关文件和目录,优化版本管理
This commit is contained in:
2026-04-11 12:22:29 +08:00
commit 07f5c92f4b
627 changed files with 85725 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
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