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,3 @@
export default defineAppConfig({
navigationBarTitleText: '我的应用',
})

192
src/user/apps/index.tsx Normal file
View File

@@ -0,0 +1,192 @@
import { useCallback, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { Empty, InfiniteLoading, PullToRefresh, Tag } from '@nutui/nutui-react-taro'
import { useThemeStyles } from '@/hooks/useTheme'
import { listJoinedApps } from '@/api/app/appProduct'
import type { AppProduct } from '@/api/app/appProduct/model'
import { APP_TYPE_NAME, STATUS_NAME, STATUS_COLOR } from '@/api/app/appProduct/model'
import dayjs from 'dayjs'
const MyAppsPage = () => {
const themeStyles = useThemeStyles()
const [list, setList] = useState<AppProduct[]>([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [current, setCurrent] = useState(1)
const loadApps = useCallback(async (isRefresh = false) => {
if (loading) return
setLoading(true)
try {
const page = isRefresh ? 1 : current
const res = await listJoinedApps({ page, limit: 20 })
const newList = res?.list || []
if (isRefresh) {
setList(newList)
setCurrent(2)
} else {
setList(prev => [...prev, ...newList])
setCurrent(prev => prev + 1)
}
setHasMore(newList.length >= 20)
} catch (e) {
console.error('加载应用失败', e)
} finally {
setLoading(false)
}
}, [loading, current])
useDidShow(() => {
loadApps(true)
})
const handleEnterAdmin = (app: AppProduct) => {
if (!app.adminUrl) {
Taro.showToast({ title: '暂无管理入口', icon: 'none' })
return
}
const url = app.adminUrl.startsWith('http') ? app.adminUrl : `https://${app.adminUrl}`
Taro.navigateTo({ url: `/passport/webview/index?url=${encodeURIComponent(url)}` })
}
const handleEnterHome = (app: AppProduct) => {
if (!app.homeUrl && !app.domain) {
Taro.showToast({ title: '暂无访问地址', icon: 'none' })
return
}
const url = (app.homeUrl || app.domain || '').startsWith('http')
? (app.homeUrl || app.domain)
: `https://${app.homeUrl || app.domain}`
Taro.navigateTo({ url: `/passport/webview/index?url=${encodeURIComponent(url)}` })
}
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 className="mt-1">
<Text className="text-white text-sm opacity-80"></Text>
</View>
</View>
<PullToRefresh onRefresh={() => loadApps(true)} className="mt-4">
<View className="mx-4 rounded-xl overflow-hidden">
{list.length === 0 && !loading ? (
<View className="bg-white rounded-xl">
<Empty description="暂无应用请前往PC端开通" imageSize={80} className="py-16" />
<View className="px-4 pb-6">
<View
className="text-center text-sm text-blue-500"
onClick={() => Taro.navigateTo({
url: '/passport/webview/index?url=' + encodeURIComponent('https://websopy.websoft.top/products')
})}
>
>
</View>
</View>
</View>
) : (
list.map((app) => {
const statusName = STATUS_NAME[app.status ?? 0] || '未知'
const statusColor = STATUS_COLOR[app.status ?? 0] || '#6b7280'
const typeName = APP_TYPE_NAME[app.appType ?? 10] || '应用'
const isExpired = app.expirationTime && dayjs(app.expirationTime).isBefore(dayjs())
return (
<View
key={app.productId}
className="bg-white mx-0 mb-3 rounded-xl p-4"
>
<View className="flex items-start gap-3">
{/* 图标 */}
<View className="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center flex-shrink-0 overflow-hidden">
{app.icon ? (
<image src={app.icon} className="w-full h-full" mode="aspectFit" />
) : (
<Text className="text-lg text-gray-400">{typeName[0]}</Text>
)}
</View>
{/* 信息 */}
<View className="flex-1 min-w-0">
<View className="flex items-center gap-2 mb-1">
<Text className="font-semibold text-gray-900 text-sm truncate flex-1">
{app.productName || '未命名应用'}
</Text>
<Tag
plain
style={{
fontSize: '10px',
padding: '0 4px',
borderColor: statusColor,
color: statusColor,
}}
>
{isExpired ? '已过期' : statusName}
</Tag>
</View>
<View className="flex items-center gap-2 mb-1">
<Text className="text-xs text-gray-400">{typeName}</Text>
{app.version && (
<Text className="text-xs text-gray-300">v{app.version}</Text>
)}
</View>
{app.description && (
<Text className="text-xs text-gray-500 line-clamp-2 mb-2">{app.description}</Text>
)}
{/* 操作按钮 */}
<View className="flex gap-2 mt-2">
{app.adminUrl && (
<View
className="px-3 py-2 bg-blue-500 rounded-lg text-center"
onClick={() => handleEnterAdmin(app)}
>
<Text className="text-xs text-blue-600"></Text>
</View>
)}
{(app.homeUrl || app.domain) && (
<View
className="px-3 py-2 bg-green-50 rounded-lg text-center"
onClick={() => handleEnterHome(app)}
>
<Text className="text-xs text-green-600">访</Text>
</View>
)}
{app.expirationTime && (
<View className="flex items-center ml-auto">
<Text className={`text-xs ${isExpired ? 'text-red-400' : 'text-gray-300'}`}>
{dayjs(app.expirationTime).format('YYYY-MM-DD')}
</Text>
</View>
)}
</View>
</View>
</View>
</View>
)
})
)}
</View>
<InfiniteLoading
hasMore={hasMore}
onLoadMore={() => loadApps(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 MyAppsPage