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

372 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 部署列表页面
*/
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>
)
}