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