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,371 @@
/**
* 部署列表页面
*/
import { useState, useEffect } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { AtButton, AtTabs, AtTabsPane, AtTag, AtActivityIndicator, AtModal } from 'taro-ui'
import { pageDeploy, triggerDeploy, rollbackDeploy } from '../../../../api/cicd'
import { pageBuild } from '../../../../api/cicd'
import type { Deploy, DeployStatus, DeployEnv, Build } from '../../../../types/cicd'
import './deploys.scss'
// 状态映射
const STATUS_MAP: Record<DeployStatus, { text: string; color: string }> = {
pending: { text: '等待中', color: '#FFC107' },
deploying: { text: '部署中', color: '#2196F3' },
success: { text: '部署成功', color: '#4CAF50' },
failed: { text: '部署失败', color: '#F44336' },
rollback: { text: '回滚中', color: '#FF9800' },
}
// 环境映射
const ENV_MAP: Record<DeployEnv, { text: string; color: string }> = {
development: { text: '开发', color: '#9E9E9E' },
staging: { text: '预发布', color: '#FF9800' },
production: { text: '生产', color: '#4CAF50' },
}
export default function Deploys() {
const [loading, setLoading] = useState(true)
const [deploys, setDeploys] = useState<Deploy[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [env, setEnv] = useState<DeployEnv | ''>('')
const [appId, setAppId] = useState('')
const [buildId, setBuildId] = useState('')
const [showDeployModal, setShowDeployModal] = useState(false)
const [selectBuild, setSelectBuild] = useState<Build | null>(null)
const [selectEnv, setSelectEnv] = useState<DeployEnv>('staging')
const [builds, setBuilds] = useState<Build[]>([])
// 获取部署列表
const fetchDeploys = async (pageNum = 1, reset = false) => {
try {
const res = await pageDeploy({
page: pageNum,
limit: 20,
websiteId: Number(appId),
env: env || undefined,
})
if (res.data) {
if (reset) {
setDeploys(res.data.list || [])
} else {
setDeploys(prev => [...prev, ...(res.data?.list || [])])
}
setTotal(res.data.total || 0)
setHasMore((res.data.list || []).length === 20)
setPage(pageNum)
}
} catch (err) {
console.error('获取部署列表失败', err)
} finally {
setLoading(false)
}
}
// 获取成功构建列表(用于部署选择)
const fetchBuilds = async () => {
try {
const res = await pageBuild({
page: 1,
limit: 20,
websiteId: Number(appId),
status: 'success',
})
if (res.data) {
setBuilds(res.data.list || [])
}
} catch (err) {
console.error('获取构建列表失败', err)
}
}
useEffect(() => {
const pages = Taro.getCurrentPages()
const current = pages[pages.length - 1]
if (current?.options?.appId) {
setAppId(current.options.appId)
}
if (current?.options?.buildId) {
setBuildId(current.options.buildId)
// 如果有 buildId自动打开部署弹窗
fetchBuilds().then(() => {
if (current.options?.buildId && builds.length > 0) {
const build = builds.find(b => b.id === Number(current.options.buildId))
if (build) {
setSelectBuild(build)
setShowDeployModal(true)
}
}
})
}
}, [])
useEffect(() => {
if (appId) {
fetchDeploys(1, true)
}
}, [appId, env])
// 触发部署
const handleTriggerDeploy = async () => {
if (!selectBuild || !appId) return
try {
Taro.showLoading({ title: '部署中...' })
await triggerDeploy({
websiteId: Number(appId),
buildId: selectBuild.id!,
env: selectEnv,
})
Taro.showToast({ title: '部署已触发', icon: 'success' })
setShowDeployModal(false)
fetchDeploys(1, true)
} catch (err) {
Taro.showToast({ title: '部署失败', icon: 'none' })
} finally {
Taro.hideLoading()
}
}
// 回滚部署
const handleRollback = async (deployId: number) => {
Taro.showModal({
title: '确认回滚',
content: '确定要回滚到上一个版本吗?',
success: async (res) => {
if (res.confirm) {
try {
Taro.showLoading({ title: '回滚中...' })
await rollbackDeploy(deployId)
Taro.showToast({ title: '回滚成功', icon: 'success' })
fetchDeploys(page, true)
} catch (err) {
Taro.showToast({ title: '回滚失败', icon: 'none' })
} finally {
Taro.hideLoading()
}
}
},
})
}
// 查看部署详情
const goToDeployDetail = (deployId: number) => {
Taro.navigateTo({
url: `/developer/app/${appId}/deploy/${deployId}`,
})
}
// 加载更多
const handleLoadMore = () => {
if (hasMore && !loading) {
fetchDeploys(page + 1)
}
}
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-'
const date = new Date(time)
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
}
// 格式化时长
const formatDuration = (seconds?: number) => {
if (!seconds) return '-'
if (seconds < 60) return `${seconds}s`
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}m ${secs}s`
}
const tabs = [
{ title: '全部' },
{ title: '开发' },
{ title: '预发布' },
{ title: '生产' },
]
const envList: (DeployEnv | '')[] = ['', 'development', 'staging', 'production']
return (
<View className="deploys-page">
{/* 顶部统计 */}
<View className="stats-bar">
<View className="stat-item">
<Text className="stat-num">{total}</Text>
<Text className="stat-label"></Text>
</View>
<View className="stat-item">
<Text className="stat-num success">{deploys.filter(d => d.status === 'success').length}</Text>
<Text className="stat-label"></Text>
</View>
<View className="stat-item">
<Text className="stat-num failed">{deploys.filter(d => d.status === 'failed').length}</Text>
<Text className="stat-label"></Text>
</View>
</View>
{/* 环境筛选 */}
<AtTabs
current={envList.indexOf(env)}
tabList={tabs}
scroll
onClick={(index) => setEnv(envList[index])}
>
<AtTabsPane current={0} index={0}>
<ScrollView
scrollY
className="deploy-list"
onScrollToLower={handleLoadMore}
>
{loading && deploys.length === 0 ? (
<View className="loading-wrap">
<AtActivityIndicator size={32} />
<Text className="loading-text">...</Text>
</View>
) : deploys.length === 0 ? (
<View className="empty-wrap">
<Text className="iconfont icon-empty" />
<Text className="empty-text"></Text>
<AtButton size="small" type="primary" onClick={() => {
fetchBuilds()
setShowDeployModal(true)
}}>
</AtButton>
</View>
) : (
deploys.map((deploy) => (
<View
className="deploy-item"
key={deploy.id}
onClick={() => deploy.id && goToDeployDetail(deploy.id)}
>
<View className="deploy-header">
<View className="deploy-version">
<Text className="version">v{deploy.version}</Text>
<AtTag
size="small"
type={ENV_MAP[deploy.env || 'staging'].color.includes('4CAF50') ? 'success' : 'primary'}
>
{ENV_MAP[deploy.env || 'staging'].text}
</AtTag>
</View>
<AtTag
type={STATUS_MAP[deploy.status || 'pending'].color.includes('F44336') ? 'error' : STATUS_MAP[deploy.status || 'pending'].color.includes('4CAF50') ? 'success' : 'primary'}
size="small"
>
{STATUS_MAP[deploy.status || 'pending'].text}
</AtTag>
</View>
<View className="deploy-info">
<View className="deploy-build">
<Text className="iconfont icon-build" />
<Text>#{deploy.buildNo}</Text>
</View>
{deploy.previousVersion && (
<View className="rollback-info">
<Text className="iconfont icon-rollback" />
<Text> v{deploy.previousVersion} </Text>
</View>
)}
</View>
<View className="deploy-footer">
<View className="deploy-meta">
<Text className="deployer">{deploy.deployer}</Text>
<Text className="time">{formatTime(deploy.startTime)}</Text>
<Text className="duration">{formatDuration(deploy.duration)}</Text>
</View>
{deploy.status === 'success' && (
<AtButton
size="small"
type="secondary"
onClick={(e) => {
e.stopPropagation()
deploy.id && handleRollback(deploy.id)
}}
>
</AtButton>
)}
</View>
</View>
))
)}
{loading && deploys.length > 0 && (
<View className="loading-more">
<AtActivityIndicator size={24} />
</View>
)}
{!hasMore && deploys.length > 0 && (
<View className="no-more"></View>
)}
</ScrollView>
</AtTabsPane>
</AtTabs>
{/* 部署弹窗 */}
<AtModal
isOpened={showDeployModal}
title="选择构建版本"
cancelText="取消"
confirmText="部署"
onClose={() => setShowDeployModal(false)}
onCancel={() => setShowDeployModal(false)}
onConfirm={handleTriggerDeploy}
content={
<View className="deploy-modal-content">
{/* 环境选择 */}
<View className="env-select">
<Text className="select-label"></Text>
<View className="env-options">
{(['development', 'staging', 'production'] as DeployEnv[]).map((e) => (
<View
key={e}
className={`env-option ${selectEnv === e ? 'active' : ''}`}
onClick={() => setSelectEnv(e)}
>
{ENV_MAP[e].text}
</View>
))}
</View>
</View>
{/* 构建选择 */}
<View className="build-select">
<Text className="select-label"></Text>
<ScrollView scrollY className="build-list">
{builds.map((build) => (
<View
key={build.id}
className={`build-option ${selectBuild?.id === build.id ? 'active' : ''}`}
onClick={() => setSelectBuild(build)}
>
<View className="build-info">
<Text className="build-no">#{build.buildNo}</Text>
<Text className="build-branch">{build.branch}</Text>
<Text className="build-commit">{build.commitId?.slice(0, 7)}</Text>
</View>
<View className="build-time">
{formatTime(build.endTime)}
</View>
</View>
))}
</ScrollView>
</View>
</View>
}
/>
</View>
)
}