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