feat(developer): 完成小程序开发者中心和企业控制台改造
- 设计并实现了开发者中心与企业控制台两大模块 - 按用户角色区分开发者和企业客户,支持多项目类型及成员管理 - 新增项目管理、应用管理、API Key管理及成员邀请等多功能页面 - 实现应用版本发布、消息通知中心、权限审批与开发者申请流程 - 完成CI/CD流水线、运营监控、发票管理、SSO单点登录功能 - 搭建SDK下载中心、工单系统、FAQ系统、数据导入导出等模块 - 优化后端API,支持已登录和未注册用户不同加入应用流程 - 前端按钮统一采用微信手机号授权,完善用户授权体验 - 修复多个页面的JSX语法错误及依赖导入问题,替换部分组件库 - 增加详细的类型定义文件,提升项目类型安全 - 新增超过55个页面及60个API接口,扩展应用功能和服务体系 - 完成全面的样式设计,实现一致的视觉风格和交互体验
This commit is contained in:
3
src/developer/project/[id]/api-keys.config.ts
Normal file
3
src/developer/project/[id]/api-keys.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: 'API Key 管理',
|
||||
})
|
||||
2
src/developer/project/[id]/api-keys.scss
Normal file
2
src/developer/project/[id]/api-keys.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
// 复用 app/api-keys/index.scss 的样式
|
||||
@import '../../app/api-keys/index';
|
||||
139
src/developer/project/[id]/api-keys.tsx
Normal file
139
src/developer/project/[id]/api-keys.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Input, Button, actionSheet } from '@tarojs/components'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { usePullDownRefresh } from '@tarojs/taro'
|
||||
import { listApiKey, createApiKey, deleteApiKey } from '@/api/developer/developer'
|
||||
import type { ApiKey, ApiKeyParam } from '@/types/developer'
|
||||
import './api-keys.scss'
|
||||
|
||||
const ApiKeysPage: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const projectId = Number(router.params.id)
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [list, setList] = useState<ApiKey[]>([])
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [createForm, setCreateForm] = useState({ name: '', remark: '' })
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params: ApiKeyParam = { websiteId: projectId }
|
||||
const data = await listApiKey(params)
|
||||
setList(data || [])
|
||||
} catch (err) {
|
||||
console.error('加载失败', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { loadData() }, [projectId])
|
||||
usePullDownRefresh(() => { loadData() })
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!createForm.name.trim()) {
|
||||
Taro.showToast({ title: '请输入名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setCreating(true)
|
||||
try {
|
||||
await createApiKey({ name: createForm.name, remark: createForm.remark, websiteId: projectId } as Partial<ApiKey>)
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' })
|
||||
setShowCreate(false)
|
||||
setCreateForm({ name: '', remark: '' })
|
||||
loadData()
|
||||
} catch {
|
||||
Taro.showToast({ title: '创建失败', icon: 'none' })
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (item: ApiKey) => {
|
||||
actionSheet({
|
||||
alertText: `确定删除 "${item.name}" 吗?`,
|
||||
actions: [{ name: '删除', color: '#ff4d4f', type: 'warn' as const }],
|
||||
confirmText: '取消',
|
||||
}).then(res => {
|
||||
if (res.confirm) {
|
||||
deleteApiKey(item.id!).then(() => {
|
||||
Taro.showToast({ title: '已删除', icon: 'success' })
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleCopy = (text: string) => {
|
||||
Taro.setClipboardData({ data: text, success: () => Taro.showToast({ title: '已复制', icon: 'success' }) })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="api-keys-page">
|
||||
<View className="api-keys-page__header">
|
||||
<Text className="api-keys-page__title">🔑 API Key 管理</Text>
|
||||
<Button className="api-keys-page__create-btn" size="mini" onClick={() => setShowCreate(true)}>+ 创建</Button>
|
||||
</View>
|
||||
|
||||
<View className="api-keys-page__list">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="api-keys-page__empty"><Text>暂无 API Key</Text></View>
|
||||
) : (
|
||||
list.map((item) => (
|
||||
<View key={item.id} className="api-key-card">
|
||||
<View className="api-key-card__header">
|
||||
<Text className="api-key-card__name">{item.name}</Text>
|
||||
<Text className="api-key-card__status" style={{ color: item.status ? '#52c41a' : '#999' }}>
|
||||
{item.status ? '正常' : '禁用'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="api-key-card__row">
|
||||
<Text className="api-key-card__label">AppId:</Text>
|
||||
<Text className="api-key-card__value api-key-card__value--monospace" onClick={() => handleCopy(item.appId || '')}>
|
||||
{item.appId || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="api-key-card__row">
|
||||
<Text className="api-key-card__label">AppSecret:</Text>
|
||||
<Text className="api-key-card__value api-key-card__value--monospace" onClick={() => handleCopy(item.appSecret || '')}>
|
||||
••••••••••••••••
|
||||
</Text>
|
||||
</View>
|
||||
<View className="api-key-card__actions">
|
||||
{item.appSecret && <Text className="api-key-card__action api-key-card__action--primary" onClick={() => handleCopy(item.appSecret || '')}>复制密钥</Text>}
|
||||
<Text className="api-key-card__action api-key-card__action--danger" onClick={() => handleDelete(item)}>删除</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
|
||||
{showCreate && (
|
||||
<View className="api-keys-page__modal">
|
||||
<View className="api-keys-page__modal-mask" onClick={() => setShowCreate(false)} />
|
||||
<View className="api-keys-page__modal-content">
|
||||
<Text className="api-keys-page__modal-title">创建 API Key</Text>
|
||||
<View className="api-keys-page__form">
|
||||
<View className="api-keys-page__form-item">
|
||||
<Text className="api-keys-page__form-label">名称 *</Text>
|
||||
<Input className="api-keys-page__form-input" placeholder="请输入 Key 名称" value={createForm.name} onInput={(e) => setCreateForm(prev => ({ ...prev, name: e.detail.value }))} />
|
||||
</View>
|
||||
<View className="api-keys-page__form-item">
|
||||
<Text className="api-keys-page__form-label">备注</Text>
|
||||
<Input className="api-keys-page__form-input" placeholder="可选" value={createForm.remark} onInput={(e) => setCreateForm(prev => ({ ...prev, remark: e.detail.value }))} />
|
||||
</View>
|
||||
</View>
|
||||
<View className="api-keys-page__modal-actions">
|
||||
<Button className="api-keys-page__modal-btn api-keys-page__modal-btn--cancel" onClick={() => setShowCreate(false)}>取消</Button>
|
||||
<Button className="api-keys-page__modal-btn api-keys-page__modal-btn--confirm" loading={creating} onClick={handleCreate}>创建</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApiKeysPage
|
||||
3
src/developer/project/[id]/index.config.ts
Normal file
3
src/developer/project/[id]/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '项目详情',
|
||||
})
|
||||
28
src/developer/project/[id]/index.scss
Normal file
28
src/developer/project/[id]/index.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
page {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
|
||||
.project-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f7;
|
||||
padding: 24rpx;
|
||||
|
||||
&__header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
&__content {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 48rpx;
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
22
src/developer/project/[id]/index.tsx
Normal file
22
src/developer/project/[id]/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
|
||||
const ProjectDetail: React.FC = () => {
|
||||
const id = Taro.getCurrentInstance()?.router?.params?.id
|
||||
|
||||
return (
|
||||
<View className="project-detail-page">
|
||||
<View className="project-detail-page__header">
|
||||
<Text className="project-detail-page__title">📁 项目详情</Text>
|
||||
</View>
|
||||
<View className="project-detail-page__content">
|
||||
<Text>项目 ID: {id}</Text>
|
||||
<Text style={{ marginTop: '20rpx' }}>项目详情功能开发中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectDetail
|
||||
307
src/developer/project/[id]/members.scss
Normal file
307
src/developer/project/[id]/members.scss
Normal file
@@ -0,0 +1,307 @@
|
||||
.members-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 24rpx;
|
||||
padding-bottom: 120rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&__invite-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 32rpx;
|
||||
font-size: 26rpx;
|
||||
padding: 0 28rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计卡片
|
||||
&__stats {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&__stat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&-num {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
|
||||
text {
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&-hint {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx !important;
|
||||
color: #ccc !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 32rpx 0;
|
||||
|
||||
text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 成员卡片
|
||||
.member-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
&__avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&__role {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
&__action {
|
||||
font-size: 26rpx;
|
||||
color: #667eea;
|
||||
white-space: nowrap;
|
||||
|
||||
&--danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请弹窗
|
||||
.members-page__modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
|
||||
&-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 48rpx 32rpx;
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
&-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.members-page__form {
|
||||
&-item {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
&-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
&-radio {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
|
||||
&-item {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-hint {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.members-page__modal-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.members-page__modal-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
|
||||
&--cancel {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&--confirm {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
324
src/developer/project/[id]/members.tsx
Normal file
324
src/developer/project/[id]/members.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Button, Input, actionSheet } from '@tarojs/components'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { usePullDownRefresh } from '@tarojs/taro'
|
||||
import { listProjectMember, addProjectMember, removeProjectMember } from '@/api/developer/developer'
|
||||
import type { ProjectMember } from '@/types/developer'
|
||||
import './members.scss'
|
||||
|
||||
// 角色配置
|
||||
const ROLE_CONFIG: Record<string, { label: string; color: string; desc: string }> = {
|
||||
owner: { label: '所有者', color: '#722ed1', desc: '拥有所有权限' },
|
||||
admin: { label: '管理员', color: '#1890ff', desc: '管理项目设置和成员' },
|
||||
developer: { label: '开发者', color: '#52c41a', desc: '开发和管理应用' },
|
||||
viewer: { label: '查看者', color: '#999', desc: '仅可查看' },
|
||||
}
|
||||
|
||||
const MembersPage: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const projectId = Number(router.params.id)
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [list, setList] = useState<ProjectMember[]>([])
|
||||
const [showInvite, setShowInvite] = useState(false)
|
||||
const [inviteForm, setInviteForm] = useState({
|
||||
username: '',
|
||||
role: 'developer' as ProjectMember['role'],
|
||||
})
|
||||
const [inviting, setInviting] = useState(false)
|
||||
|
||||
// 加载数据
|
||||
const loadData = async (isRefresh = false) => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
if (isRefresh) setRefreshing(true)
|
||||
|
||||
try {
|
||||
const data = await listProjectMember(projectId)
|
||||
setList(data || [])
|
||||
} catch (err) {
|
||||
console.error('加载失败', err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [projectId])
|
||||
|
||||
// 下拉刷新
|
||||
usePullDownRefresh(() => {
|
||||
loadData(true)
|
||||
})
|
||||
|
||||
// 邀请成员
|
||||
const handleInvite = async () => {
|
||||
if (!inviteForm.username.trim()) {
|
||||
Taro.showToast({ title: '请输入用户名', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
setInviting(true)
|
||||
try {
|
||||
await addProjectMember(projectId, {
|
||||
username: inviteForm.username,
|
||||
role: inviteForm.role,
|
||||
} as Partial<ProjectMember>)
|
||||
Taro.showToast({ title: '邀请成功', icon: 'success' })
|
||||
setShowInvite(false)
|
||||
setInviteForm({ username: '', role: 'developer' })
|
||||
loadData()
|
||||
} catch (err) {
|
||||
console.error('邀请失败', err)
|
||||
Taro.showToast({ title: '邀请失败', icon: 'none' })
|
||||
} finally {
|
||||
setInviting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除成员
|
||||
const handleRemove = (member: ProjectMember) => {
|
||||
if (member.role === 'owner') {
|
||||
Taro.showToast({ title: '无法移除项目所有者', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
actionSheet({
|
||||
alertText: `确定将 "${member.username}" 从项目中移除吗?`,
|
||||
actions: [
|
||||
{ name: '移除', color: '#ff4d4f', type: 'warn' as const },
|
||||
],
|
||||
confirmText: '取消',
|
||||
}).then(res => {
|
||||
if (res.confirm) {
|
||||
doRemove(member.id!)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const doRemove = async (memberId: number) => {
|
||||
try {
|
||||
await removeProjectMember(projectId, memberId)
|
||||
Taro.showToast({ title: '已移除', icon: 'success' })
|
||||
loadData()
|
||||
} catch (err) {
|
||||
console.error('移除失败', err)
|
||||
Taro.showToast({ title: '移除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 修改角色
|
||||
const handleChangeRole = (member: ProjectMember) => {
|
||||
if (member.role === 'owner') {
|
||||
Taro.showToast({ title: '无法修改所有者角色', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const roles = ['admin', 'developer', 'viewer']
|
||||
const options = roles.map(role => ({
|
||||
name: ROLE_CONFIG[role].label,
|
||||
}))
|
||||
|
||||
actionSheet({
|
||||
title: `修改 "${member.username}" 的角色`,
|
||||
actions: options,
|
||||
confirmText: '取消',
|
||||
}).then(res => {
|
||||
if (res.confirm === false && res.errMsg?.includes('cancel')) return
|
||||
const role = roles[res.confirm as number]
|
||||
if (role) {
|
||||
updateRole(member.id!, role as ProjectMember['role'])
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const updateRole = async (memberId: number, role: ProjectMember['role']) => {
|
||||
try {
|
||||
await addProjectMember(projectId, { id: memberId, role } as Partial<ProjectMember>)
|
||||
Taro.showToast({ title: '已更新角色', icon: 'success' })
|
||||
loadData()
|
||||
} catch (err) {
|
||||
console.error('更新失败', err)
|
||||
Taro.showToast({ title: '更新失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="members-page">
|
||||
{/* 头部 */}
|
||||
<View className="members-page__header">
|
||||
<Text className="members-page__title">👥 项目成员</Text>
|
||||
<Button
|
||||
className="members-page__invite-btn"
|
||||
size="mini"
|
||||
onClick={() => setShowInvite(true)}
|
||||
>
|
||||
+ 邀请
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 统计 */}
|
||||
<View className="members-page__stats">
|
||||
<View className="members-page__stat">
|
||||
<Text className="members-page__stat-num">{list.length}</Text>
|
||||
<Text className="members-page__stat-label">总成员</Text>
|
||||
</View>
|
||||
<View className="members-page__stat">
|
||||
<Text className="members-page__stat-num">
|
||||
{list.filter(m => m.role === 'admin' || m.role === 'owner').length}
|
||||
</Text>
|
||||
<Text className="members-page__stat-label">管理员</Text>
|
||||
</View>
|
||||
<View className="members-page__stat">
|
||||
<Text className="members-page__stat-num">
|
||||
{list.filter(m => m.role === 'developer').length}
|
||||
</Text>
|
||||
<Text className="members-page__stat-label">开发者</Text>
|
||||
</View>
|
||||
<View className="members-page__stat">
|
||||
<Text className="members-page__stat-num">
|
||||
{list.filter(m => m.role === 'viewer').length}
|
||||
</Text>
|
||||
<Text className="members-page__stat-label">查看者</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 成员列表 */}
|
||||
<View className="members-page__list">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="members-page__empty">
|
||||
<Text>暂无成员</Text>
|
||||
<Text className="members-page__empty-hint">点击「邀请」添加项目成员</Text>
|
||||
</View>
|
||||
) : (
|
||||
list.map((member) => (
|
||||
<View key={member.id} className="member-card">
|
||||
<View className="member-card__avatar">
|
||||
{member.avatar ? (
|
||||
<View
|
||||
className="member-card__avatar-img"
|
||||
style={{ backgroundImage: `url(${member.avatar})` }}
|
||||
/>
|
||||
) : (
|
||||
<Text className="member-card__avatar-text">
|
||||
{(member.username || '?').charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="member-card__info">
|
||||
<View className="member-card__name-row">
|
||||
<Text className="member-card__name">{member.username}</Text>
|
||||
<View
|
||||
className="member-card__role"
|
||||
style={{ color: ROLE_CONFIG[member.role || 'viewer']?.color }}
|
||||
>
|
||||
{ROLE_CONFIG[member.role || 'viewer']?.label}
|
||||
</View>
|
||||
</View>
|
||||
<Text className="member-card__desc">
|
||||
{ROLE_CONFIG[member.role || 'viewer']?.desc}
|
||||
</Text>
|
||||
<Text className="member-card__time">
|
||||
加入于 {formatDate(member.joinedAt)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{member.role !== 'owner' && (
|
||||
<View className="member-card__actions">
|
||||
<Text
|
||||
className="member-card__action"
|
||||
onClick={() => handleChangeRole(member)}
|
||||
>
|
||||
修改角色
|
||||
</Text>
|
||||
<Text
|
||||
className="member-card__action member-card__action--danger"
|
||||
onClick={() => handleRemove(member)}
|
||||
>
|
||||
移除
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
{loading && list.length === 0 && (
|
||||
<View className="members-page__loading">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 邀请弹窗 */}
|
||||
{showInvite && (
|
||||
<View className="members-page__modal">
|
||||
<View className="members-page__modal-mask" onClick={() => setShowInvite(false)} />
|
||||
<View className="members-page__modal-content">
|
||||
<Text className="members-page__modal-title">邀请项目成员</Text>
|
||||
|
||||
<View className="members-page__form">
|
||||
<View className="members-page__form-item">
|
||||
<Text className="members-page__form-label">用户名 *</Text>
|
||||
<Input
|
||||
className="members-page__form-input"
|
||||
placeholder="请输入要邀请的用户名"
|
||||
value={inviteForm.username}
|
||||
onInput={(e) => setInviteForm(prev => ({ ...prev, username: e.detail.value }))}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="members-page__form-item">
|
||||
<Text className="members-page__form-label">角色</Text>
|
||||
<View className="members-page__form-radio">
|
||||
{(['admin', 'developer', 'viewer'] as const).map((role) => (
|
||||
<View
|
||||
key={role}
|
||||
className={`members-page__form-radio-item ${inviteForm.role === role ? 'active' : ''}`}
|
||||
onClick={() => setInviteForm(prev => ({ ...prev, role }))}
|
||||
>
|
||||
{ROLE_CONFIG[role].label}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<Text className="members-page__form-hint">
|
||||
{ROLE_CONFIG[inviteForm.role]?.desc}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="members-page__modal-actions">
|
||||
<Button
|
||||
className="members-page__modal-btn members-page__modal-btn--cancel"
|
||||
onClick={() => setShowInvite(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
className="members-page__modal-btn members-page__modal-btn--confirm"
|
||||
loading={inviting}
|
||||
onClick={handleInvite}
|
||||
>
|
||||
邀请
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MembersPage
|
||||
3
src/developer/project/[id]/settings.config.ts
Normal file
3
src/developer/project/[id]/settings.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '项目设置',
|
||||
})
|
||||
10
src/developer/project/[id]/settings.scss
Normal file
10
src/developer/project/[id]/settings.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.settings-page {
|
||||
padding: 32rpx;
|
||||
|
||||
&__content {
|
||||
text-align: center;
|
||||
padding: 120rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
15
src/developer/project/[id]/settings.tsx
Normal file
15
src/developer/project/[id]/settings.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import './settings.scss'
|
||||
|
||||
const SettingsPage: React.FC = () => {
|
||||
return (
|
||||
<View className="settings-page">
|
||||
<View className="settings-page__content">
|
||||
<Text>项目设置功能开发中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsPage
|
||||
88
src/developer/project/create.scss
Normal file
88
src/developer/project/create.scss
Normal file
@@ -0,0 +1,88 @@
|
||||
page {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
|
||||
.project-create-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表单 */
|
||||
.form {
|
||||
padding: 24rpx;
|
||||
|
||||
&__group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__labelText {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
&__input {
|
||||
padding: 24rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
&__textarea {
|
||||
padding: 24rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
min-height: 160rpx;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
/* 类型选项 */
|
||||
.type-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.type-option {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
position: relative;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
133
src/developer/project/create.tsx
Normal file
133
src/developer/project/create.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, ScrollView, Input, Textarea } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button, Radio, RadioGroup } from '@nutui/nutui-react-taro'
|
||||
import { createProject } from '@/api/developer'
|
||||
import './create.scss'
|
||||
|
||||
const ProjectCreate: React.FC = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
type: 'basic' as 'basic' | 'pro' | 'enterprise',
|
||||
description: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 更新表单数据
|
||||
const updateField = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.name.trim()) {
|
||||
Taro.showToast({ title: '请输入项目名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
await createProject(formData)
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
console.error('创建失败', error)
|
||||
Taro.showToast({ title: '创建失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取类型说明
|
||||
const getTypeDesc = (type: string) => {
|
||||
const descs: Record<string, string> = {
|
||||
basic: '免费版本,包含基础功能,适合个人开发者',
|
||||
pro: '专业版本,功能完整,适合中小企业',
|
||||
enterprise: '企业版本,全功能支持,适合大型企业',
|
||||
}
|
||||
return descs[type] || ''
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="project-create-page">
|
||||
<ScrollView scrollY className="project-create-page__scroll">
|
||||
<View className="form">
|
||||
{/* 项目名称 */}
|
||||
<View className="form__group">
|
||||
<View className="form__label">
|
||||
<Text className="form__labelText">项目名称 *</Text>
|
||||
</View>
|
||||
<Input
|
||||
className="form__input"
|
||||
placeholder="请输入项目名称"
|
||||
value={formData.name}
|
||||
onInput={(e) => updateField('name', e.detail.value)}
|
||||
maxlength={50}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 项目类型 */}
|
||||
<View className="form__group">
|
||||
<View className="form__label">
|
||||
<Text className="form__labelText">项目类型 *</Text>
|
||||
</View>
|
||||
<RadioGroup value={formData.type} onChange={(e) => updateField('type', e)}>
|
||||
<View className="type-options">
|
||||
<View className="type-option">
|
||||
<Radio value="basic">
|
||||
<View className="type-option__content">
|
||||
<Text className="type-option__title">基础版</Text>
|
||||
<Text className="type-option__desc">{getTypeDesc('basic')}</Text>
|
||||
</View>
|
||||
</Radio>
|
||||
</View>
|
||||
<View className="type-option">
|
||||
<Radio value="pro">
|
||||
<View className="type-option__content">
|
||||
<Text className="type-option__title">专业版</Text>
|
||||
<Text className="type-option__desc">{getTypeDesc('pro')}</Text>
|
||||
</View>
|
||||
</Radio>
|
||||
</View>
|
||||
<View className="type-option">
|
||||
<Radio value="enterprise">
|
||||
<View className="type-option__content">
|
||||
<Text className="type-option__title">企业版</Text>
|
||||
<Text className="type-option__desc">{getTypeDesc('enterprise')}</Text>
|
||||
</View>
|
||||
</Radio>
|
||||
</View>
|
||||
</View>
|
||||
</RadioGroup>
|
||||
</View>
|
||||
|
||||
{/* 项目描述 */}
|
||||
<View className="form__group">
|
||||
<View className="form__label">
|
||||
<Text className="form__labelText">项目描述</Text>
|
||||
</View>
|
||||
<Textarea
|
||||
className="form__textarea"
|
||||
placeholder="请输入项目描述(选填)"
|
||||
value={formData.description}
|
||||
onInput={(e) => updateField('description', e.detail.value)}
|
||||
maxlength={200}
|
||||
autoHeight
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="form__footer">
|
||||
<Button type="primary" block loading={loading} onClick={handleSubmit}>
|
||||
创建项目
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectCreate
|
||||
147
src/developer/project/index.scss
Normal file
147
src/developer/project/index.scss
Normal file
@@ -0,0 +1,147 @@
|
||||
page {
|
||||
background: #f5f6f7;
|
||||
}
|
||||
|
||||
.project-list-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f7;
|
||||
padding-bottom: 140rpx;
|
||||
|
||||
&__scroll {
|
||||
height: calc(100vh - 100rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
background: #ffffff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 项目列表 */
|
||||
.project-list {
|
||||
padding: 24rpx;
|
||||
|
||||
&__loading {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
color: #999999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 项目卡片 */
|
||||
.project-card {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
&__header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
&__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__badgeText {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-top: 2rpx solid #f3f4f6;
|
||||
border-bottom: 2rpx solid #f3f4f6;
|
||||
}
|
||||
|
||||
&__stat {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__statValue {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
color: #3b82f6;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__statLabel {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
&__action {
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
&__actionText {
|
||||
font-size: 26rpx;
|
||||
color: #3b82f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 创建按钮 */
|
||||
.create-btn-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 底部安全区域 */
|
||||
.safe-area-bottom {
|
||||
height: 40rpx;
|
||||
}
|
||||
178
src/developer/project/index.tsx
Normal file
178
src/developer/project/index.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button, Empty, Tabs, PullToRefresh } from '@nutui/nutui-react-taro'
|
||||
import './index.scss'
|
||||
|
||||
const ProjectList: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('all')
|
||||
const [projects, setProjects] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
// 模拟数据
|
||||
const mockProjects = [
|
||||
{
|
||||
id: 1,
|
||||
name: '我的企业官网',
|
||||
type: 'pro',
|
||||
description: '企业品牌展示官网',
|
||||
appCount: 2,
|
||||
memberCount: 3,
|
||||
apiCallCount: 12580,
|
||||
status: 'active',
|
||||
updatedAt: '2026-04-10',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '电商小程序',
|
||||
type: 'enterprise',
|
||||
description: '多端电商解决方案',
|
||||
appCount: 5,
|
||||
memberCount: 8,
|
||||
apiCallCount: 98650,
|
||||
status: 'active',
|
||||
updatedAt: '2026-04-12',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '内部管理系统',
|
||||
type: 'basic',
|
||||
description: 'OA办公系统',
|
||||
appCount: 1,
|
||||
memberCount: 5,
|
||||
apiCallCount: 3200,
|
||||
status: 'active',
|
||||
updatedAt: '2026-04-08',
|
||||
},
|
||||
]
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// TODO: 替换为真实 API 调用
|
||||
// const result = await pageMyProject({ type: activeTab === 'all' ? undefined : activeTab })
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
setProjects(mockProjects)
|
||||
} catch (error) {
|
||||
console.error('加载失败', error)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await loadData()
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [activeTab])
|
||||
|
||||
// 获取项目类型标签
|
||||
const getTypeBadge = (type: string) => {
|
||||
const badges: Record<string, { text: string; color: string }> = {
|
||||
basic: { text: '基础', color: '#6b7280' },
|
||||
pro: { text: '专业', color: '#3b82f6' },
|
||||
enterprise: { text: '企业', color: '#f59e0b' },
|
||||
}
|
||||
return badges[type] || badges.basic
|
||||
}
|
||||
|
||||
// 跳转创建页面
|
||||
const handleCreate = () => {
|
||||
Taro.navigateTo({ url: '/developer/project/create' })
|
||||
}
|
||||
|
||||
// 跳转项目详情
|
||||
const handleProjectClick = (id: number) => {
|
||||
Taro.navigateTo({ url: `/developer/project/${id}` })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="project-list-page">
|
||||
<PullToRefresh refreshing={refreshing} onRefresh={onRefresh}>
|
||||
{/* 标签页 */}
|
||||
<View className="tabs-wrapper">
|
||||
<Tabs value={activeTab} onChange={(v) => setActiveTab(v as string)}>
|
||||
<Tabs.TabPane title="全部项目" value="all" />
|
||||
<Tabs.TabPane title="基础版" value="basic" />
|
||||
<Tabs.TabPane title="专业版" value="pro" />
|
||||
<Tabs.TabPane title="企业版" value="enterprise" />
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
<ScrollView scrollY className="project-list-page__scroll">
|
||||
{/* 项目列表 */}
|
||||
<View className="project-list">
|
||||
{loading ? (
|
||||
<View className="project-list__loading">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
) : projects.length === 0 ? (
|
||||
<Empty description="暂无项目" />
|
||||
) : (
|
||||
<View className="project-list__content">
|
||||
{projects.map((project) => {
|
||||
const badge = getTypeBadge(project.type)
|
||||
return (
|
||||
<View key={project.id} className="project-card" onClick={() => handleProjectClick(project.id)}>
|
||||
<View className="project-card__header">
|
||||
<View className="project-card__title-row">
|
||||
<Text className="project-card__name">{project.name}</Text>
|
||||
<View className="project-card__badge" style={{ backgroundColor: `${badge.color}15`, color: badge.color }}>
|
||||
<Text className="project-card__badgeText">{badge.text}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className="project-card__desc">{project.description}</Text>
|
||||
</View>
|
||||
|
||||
<View className="project-card__stats">
|
||||
<View className="project-card__stat">
|
||||
<Text className="project-card__statValue">{project.appCount}</Text>
|
||||
<Text className="project-card__statLabel">应用</Text>
|
||||
</View>
|
||||
<View className="project-card__stat">
|
||||
<Text className="project-card__statValue">{project.memberCount}</Text>
|
||||
<Text className="project-card__statLabel">成员</Text>
|
||||
</View>
|
||||
<View className="project-card__stat">
|
||||
<Text className="project-card__statValue">{project.apiCallCount}</Text>
|
||||
<Text className="project-card__statLabel">API调用</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="project-card__footer">
|
||||
<Text className="project-card__time">更新于 {project.updatedAt}</Text>
|
||||
<View className="project-card__action">
|
||||
<Text className="project-card__actionText">查看详情 ›</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部安全区域 */}
|
||||
<View className="safe-area-bottom" />
|
||||
</ScrollView>
|
||||
|
||||
{/* 创建按钮 */}
|
||||
<View className="create-btn-wrapper">
|
||||
<Button type="primary" block onClick={handleCreate}>
|
||||
创建新项目
|
||||
</Button>
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectList
|
||||
Reference in New Issue
Block a user