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