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,6 @@
/**
* 部署详情页面配置
*/
export default definePageConfig({
navigationBarTitleText: '部署详情',
})

View File

@@ -0,0 +1,237 @@
.deploy-detail {
min-height: 100vh;
background: #f5f5f5;
padding: 24px;
&.loading-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 300px;
.loading-text {
margin-top: 16px;
font-size: 28px;
color: #999;
}
}
}
.status-card {
background: #fff;
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
.version-info {
display: flex;
align-items: center;
gap: 12px;
.version {
font-size: 40px;
font-weight: 600;
color: #333;
}
}
}
.status-progress {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32px;
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
.step-dot {
width: 24px;
height: 24px;
border-radius: 50%;
background: #e0e0e0;
margin-bottom: 8px;
&.active {
background: #4CAF50;
}
}
.step-label {
font-size: 24px;
color: #999;
}
}
.progress-line {
width: 100px;
height: 4px;
background: #e0e0e0;
margin: 0 16px;
margin-bottom: 32px;
&.active {
background: #4CAF50;
}
}
}
.deploy-meta {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
.meta-item {
.label {
display: block;
font-size: 24px;
color: #999;
margin-bottom: 4px;
}
.value {
font-size: 28px;
color: #333;
}
}
}
.rollback-info {
display: flex;
align-items: center;
gap: 8px;
margin-top: 24px;
padding: 16px;
background: #FFF3E0;
border-radius: 8px;
font-size: 26px;
color: #FF9800;
.iconfont {
font-size: 28px;
}
}
}
.actions {
display: flex;
gap: 16px;
margin-bottom: 24px;
.at-button {
flex: 1;
}
}
.tabs {
display: flex;
background: #fff;
border-radius: 16px;
overflow: hidden;
margin-bottom: 24px;
.tab {
flex: 1;
text-align: center;
padding: 24px;
font-size: 28px;
color: #666;
position: relative;
&.active {
color: #1890ff;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 4px;
background: #1890ff;
border-radius: 2px;
}
}
}
}
.tab-content {
height: calc(100vh - 600px);
}
.info-panel {
.info-section {
background: #fff;
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
.section-title {
display: block;
font-size: 30px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
.row-label {
font-size: 26px;
color: #666;
}
.row-value {
font-size: 26px;
color: #333;
}
}
}
}
.logs-panel {
background: #1e1e1e;
border-radius: 16px;
padding: 24px;
min-height: 500px;
.logs-content {
.log-line {
display: block;
font-family: 'Courier New', monospace;
font-size: 22px;
color: #d4d4d4;
line-height: 1.8;
word-break: break-all;
}
}
.loading-logs {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
color: #d4d4d4;
font-size: 26px;
gap: 16px;
}
}

View File

@@ -0,0 +1,304 @@
/**
* 部署详情页面
*/
import { useState, useEffect } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { AtButton, AtTag, AtActivityIndicator } from 'taro-ui'
import { getDeploy, getDeployLogs, rollbackDeploy } from '../../../../../api/cicd'
import type { Deploy, DeployStatus, DeployEnv } from '../../../../../types/cicd'
import './deploy-detail.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 DeployDetail() {
const [loading, setLoading] = useState(true)
const [deploy, setDeploy] = useState<Deploy>({})
const [logs, setLogs] = useState('')
const [activeTab, setActiveTab] = useState('info')
const [deployId, setDeployId] = useState('')
useEffect(() => {
const pages = Taro.getCurrentPages()
const current = pages[pages.length - 1]
if (current?.options?.id) {
setDeployId(current.options.id)
}
}, [])
useEffect(() => {
if (deployId) {
fetchDeployDetail()
}
}, [deployId])
// 获取部署详情
const fetchDeployDetail = async () => {
try {
const res = await getDeploy(Number(deployId))
if (res.data) {
setDeploy(res.data)
}
} catch (err) {
console.error('获取部署详情失败', err)
} finally {
setLoading(false)
}
}
// 获取部署日志
const fetchDeployLogs = async () => {
try {
const res = await getDeployLogs(Number(deployId))
if (res.data) {
setLogs(res.data.logs || '')
}
} catch (err) {
console.error('获取部署日志失败', err)
}
}
// 切换 Tab
useEffect(() => {
if (activeTab === 'logs' && !logs) {
fetchDeployLogs()
}
}, [activeTab])
// 回滚部署
const handleRollback = async () => {
Taro.showModal({
title: '确认回滚',
content: `确定要回滚到版本 ${deploy.previousVersion || '上一个版本'} 吗?`,
success: async (res) => {
if (res.confirm) {
try {
Taro.showLoading({ title: '回滚中...' })
await rollbackDeploy(Number(deployId))
Taro.showToast({ title: '回滚成功', icon: 'success' })
fetchDeployDetail()
} catch (err) {
Taro.showToast({ title: '回滚失败', icon: 'none' })
} finally {
Taro.hideLoading()
}
}
},
})
}
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-'
const date = new Date(time)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
// 格式化时长
const formatDuration = (seconds?: number) => {
if (!seconds) return '-'
if (seconds < 60) return `${seconds}`
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}${secs}`
}
if (loading) {
return (
<View className="deploy-detail loading-wrap">
<AtActivityIndicator size={32} />
<Text className="loading-text">...</Text>
</View>
)
}
const statusInfo = STATUS_MAP[deploy.status || 'pending']
const envInfo = ENV_MAP[deploy.env || 'staging']
return (
<View className="deploy-detail">
{/* 部署状态卡片 */}
<View className="status-card">
<View className="status-header">
<View className="version-info">
<Text className="version">v{deploy.version}</Text>
<AtTag type={envInfo.color.includes('4CAF50') ? 'success' : 'primary'}>
{envInfo.text}
</AtTag>
</View>
<AtTag
type={statusInfo.color.includes('F44336') ? 'error' : statusInfo.color.includes('4CAF50') ? 'success' : 'primary'}
>
{statusInfo.text}
</AtTag>
</View>
<View className="status-progress">
<View className="progress-step">
<View className={`step-dot ${deploy.status !== 'pending' ? 'active' : ''}`} />
<Text className="step-label"></Text>
</View>
<View className={`progress-line ${deploy.status === 'deploying' || deploy.status === 'success' ? 'active' : ''}`} />
<View className="progress-step">
<View className={`step-dot ${deploy.status === 'deploying' || deploy.status === 'success' ? 'active' : ''}`} />
<Text className="step-label"></Text>
</View>
<View className={`progress-line ${deploy.status === 'success' ? 'active' : ''}`} />
<View className="progress-step">
<View className={`step-dot ${deploy.status === 'success' ? 'active' : ''}`} />
<Text className="step-label"></Text>
</View>
</View>
<View className="deploy-meta">
<View className="meta-item">
<Text className="label"></Text>
<Text className="value">#{deploy.buildNo}</Text>
</View>
<View className="meta-item">
<Text className="label"></Text>
<Text className="value">{deploy.deployer}</Text>
</View>
<View className="meta-item">
<Text className="label"></Text>
<Text className="value">{formatTime(deploy.startTime)}</Text>
</View>
<View className="meta-item">
<Text className="label"></Text>
<Text className="value">{formatDuration(deploy.duration)}</Text>
</View>
</View>
{deploy.previousVersion && (
<View className="rollback-info">
<Text className="iconfont icon-rollback" />
<Text> v{deploy.previousVersion} </Text>
</View>
)}
</View>
{/* 操作按钮 */}
<View className="actions">
{deploy.status === 'success' && (
<AtButton type="primary" onClick={handleRollback}>
</AtButton>
)}
<AtButton
type="secondary"
onClick={() => {
Taro.navigateBack()
}}
>
</AtButton>
</View>
{/* Tab 切换 */}
<View className="tabs">
<View
className={`tab ${activeTab === 'info' ? 'active' : ''}`}
onClick={() => setActiveTab('info')}
>
</View>
<View
className={`tab ${activeTab === 'logs' ? 'active' : ''}`}
onClick={() => setActiveTab('logs')}
>
</View>
</View>
{/* Tab 内容 */}
<ScrollView scrollY className="tab-content">
{activeTab === 'info' && (
<View className="info-panel">
<View className="info-section">
<Text className="section-title"></Text>
<View className="info-row">
<Text className="row-label"> ID</Text>
<Text className="row-value">{deploy.id}</Text>
</View>
<View className="info-row">
<Text className="row-label"> ID</Text>
<Text className="row-value">{deploy.websiteId}</Text>
</View>
<View className="info-row">
<Text className="row-label"> ID</Text>
<Text className="row-value">{deploy.buildId}</Text>
</View>
<View className="info-row">
<Text className="row-label"></Text>
<Text className="row-value">v{deploy.version}</Text>
</View>
</View>
<View className="info-section">
<Text className="section-title"></Text>
<View className="info-row">
<Text className="row-label"></Text>
<Text className="row-value">{formatTime(deploy.startTime)}</Text>
</View>
<View className="info-row">
<Text className="row-label"></Text>
<Text className="row-value">{formatTime(deploy.endTime)}</Text>
</View>
<View className="info-row">
<Text className="row-label"></Text>
<Text className="row-value">{formatDuration(deploy.duration)}</Text>
</View>
</View>
{deploy.previousVersion && (
<View className="info-section">
<Text className="section-title"></Text>
<View className="info-row">
<Text className="row-label"></Text>
<Text className="row-value">v{deploy.previousVersion}</Text>
</View>
<View className="info-row">
<Text className="row-label"></Text>
<Text className="row-value">v{deploy.version}</Text>
</View>
</View>
)}
</View>
)}
{activeTab === 'logs' && (
<View className="logs-panel">
{logs ? (
<View className="logs-content">
{logs.split('\n').map((line, index) => (
<Text key={index} className="log-line">
{line}
</Text>
))}
</View>
) : (
<View className="loading-logs">
<AtActivityIndicator size={24} />
<Text>...</Text>
</View>
)}
</View>
)}
</ScrollView>
</View>
)
}