feat(developer): 完成小程序开发者中心和企业控制台改造
- 设计并实现了开发者中心与企业控制台两大模块 - 按用户角色区分开发者和企业客户,支持多项目类型及成员管理 - 新增项目管理、应用管理、API Key管理及成员邀请等多功能页面 - 实现应用版本发布、消息通知中心、权限审批与开发者申请流程 - 完成CI/CD流水线、运营监控、发票管理、SSO单点登录功能 - 搭建SDK下载中心、工单系统、FAQ系统、数据导入导出等模块 - 优化后端API,支持已登录和未注册用户不同加入应用流程 - 前端按钮统一采用微信手机号授权,完善用户授权体验 - 修复多个页面的JSX语法错误及依赖导入问题,替换部分组件库 - 增加详细的类型定义文件,提升项目类型安全 - 新增超过55个页面及60个API接口,扩展应用功能和服务体系 - 完成全面的样式设计,实现一致的视觉风格和交互体验
This commit is contained in:
6
src/developer/app/[id]/build/[id].config.ts
Normal file
6
src/developer/app/[id]/build/[id].config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 构建详情页面配置
|
||||
*/
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '构建详情',
|
||||
})
|
||||
261
src/developer/app/[id]/build/[id].scss
Normal file
261
src/developer/app/[id]/build/[id].scss
Normal file
@@ -0,0 +1,261 @@
|
||||
.build-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: 24px;
|
||||
|
||||
.build-no {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.info-item {
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
|
||||
&.commit {
|
||||
font-family: monospace;
|
||||
color: #2196F3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commit-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.iconfont {
|
||||
color: #2196F3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.msg {
|
||||
flex: 1;
|
||||
font-size: 26px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
&.commit {
|
||||
font-family: monospace;
|
||||
color: #2196F3;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.artifacts-panel {
|
||||
.artifact-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.artifact-info {
|
||||
flex: 1;
|
||||
|
||||
.artifact-name {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.artifact-size {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-artifacts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 100px 0;
|
||||
color: #999;
|
||||
|
||||
.iconfont {
|
||||
font-size: 96px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
325
src/developer/app/[id]/build/[id].tsx
Normal file
325
src/developer/app/[id]/build/[id].tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 构建详情页面
|
||||
*/
|
||||
import { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Button } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { AtButton, AtTag, AtActivityIndicator, AtTimeline } from 'taro-ui'
|
||||
import { getBuild, getBuildLogs, triggerBuild } from '../../../../../api/cicd'
|
||||
import type { Build, BuildStatus } from '../../../../../types/cicd'
|
||||
import './build-detail.scss'
|
||||
|
||||
// 状态映射
|
||||
const STATUS_MAP: Record<BuildStatus, { text: string; color: string }> = {
|
||||
pending: { text: '等待中', color: '#FFC107' },
|
||||
running: { text: '构建中', color: '#2196F3' },
|
||||
success: { text: '构建成功', color: '#4CAF50' },
|
||||
failed: { text: '构建失败', color: '#F44336' },
|
||||
cancelled: { text: '已取消', color: '#9E9E9E' },
|
||||
}
|
||||
|
||||
export default function BuildDetail() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [build, setBuild] = useState<Build>({})
|
||||
const [logs, setLogs] = useState('')
|
||||
const [activeTab, setActiveTab] = useState('info')
|
||||
const [buildId, setBuildId] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const pages = Taro.getCurrentPages()
|
||||
const current = pages[pages.length - 1]
|
||||
if (current?.options?.id) {
|
||||
setBuildId(current.options.id)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (buildId) {
|
||||
fetchBuildDetail()
|
||||
}
|
||||
}, [buildId])
|
||||
|
||||
// 获取构建详情
|
||||
const fetchBuildDetail = async () => {
|
||||
try {
|
||||
const res = await getBuild(Number(buildId))
|
||||
if (res.data) {
|
||||
setBuild(res.data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取构建详情失败', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取构建日志
|
||||
const fetchBuildLogs = async () => {
|
||||
try {
|
||||
const res = await getBuildLogs(Number(buildId))
|
||||
if (res.data) {
|
||||
setLogs(res.data.logs || '')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取构建日志失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换 Tab
|
||||
useEffect(() => {
|
||||
if (activeTab === 'logs' && !logs) {
|
||||
fetchBuildLogs()
|
||||
}
|
||||
}, [activeTab])
|
||||
|
||||
// 重新触发构建
|
||||
const handleRetryBuild = async () => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要重新构建吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm && build.websiteId) {
|
||||
try {
|
||||
Taro.showLoading({ title: '重新构建中...' })
|
||||
await triggerBuild({
|
||||
websiteId: build.websiteId!,
|
||||
commitId: build.commitId,
|
||||
branch: build.branch,
|
||||
})
|
||||
Taro.showToast({ title: '构建已触发', icon: 'success' })
|
||||
fetchBuildDetail()
|
||||
} catch (err) {
|
||||
Taro.showToast({ title: '触发失败', icon: 'none' })
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 部署
|
||||
const handleDeploy = () => {
|
||||
Taro.navigateTo({
|
||||
url: `/developer/app/${build.websiteId}/deploys?buildId=${buildId}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
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="build-detail loading-wrap">
|
||||
<AtActivityIndicator size={32} />
|
||||
<Text className="loading-text">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const statusInfo = STATUS_MAP[build.status || 'pending']
|
||||
|
||||
return (
|
||||
<View className="build-detail">
|
||||
{/* 构建状态 */}
|
||||
<View className="status-card">
|
||||
<View className="status-header">
|
||||
<Text className="build-no">#{build.buildNo || build.id}</Text>
|
||||
<AtTag type={statusInfo.color.includes('F44336') ? 'error' : statusInfo.color.includes('4CAF50') ? 'success' : 'primary'}>
|
||||
{statusInfo.text}
|
||||
</AtTag>
|
||||
</View>
|
||||
|
||||
<View className="status-info">
|
||||
<View className="info-item">
|
||||
<Text className="label">分支</Text>
|
||||
<Text className="value">{build.branch || 'main'}</Text>
|
||||
</View>
|
||||
<View className="info-item">
|
||||
<Text className="label">提交</Text>
|
||||
<Text className="value commit">{build.commitId?.slice(0, 7)}</Text>
|
||||
</View>
|
||||
<View className="info-item">
|
||||
<Text className="label">触发方式</Text>
|
||||
<Text className="value">{build.trigger || 'manual'}</Text>
|
||||
</View>
|
||||
<View className="info-item">
|
||||
<Text className="label">耗时</Text>
|
||||
<Text className="value">{formatDuration(build.duration)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="commit-message">
|
||||
<Text className="iconfont icon-commit" />
|
||||
<Text className="msg">{build.commitMessage}</Text>
|
||||
</View>
|
||||
|
||||
<View className="meta">
|
||||
<Text className="author">{build.author}</Text>
|
||||
<Text className="time">{formatTime(build.startTime)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="actions">
|
||||
{build.status === 'failed' && (
|
||||
<AtButton type="primary" onClick={handleRetryBuild}>
|
||||
重新构建
|
||||
</AtButton>
|
||||
)}
|
||||
{build.status === 'success' && (
|
||||
<AtButton type="primary" onClick={handleDeploy}>
|
||||
部署
|
||||
</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
|
||||
className={`tab ${activeTab === 'artifacts' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('artifacts')}
|
||||
>
|
||||
构建产物
|
||||
</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">构建编号</Text>
|
||||
<Text className="row-value">{build.buildNo}</Text>
|
||||
</View>
|
||||
<View className="info-row">
|
||||
<Text className="row-label">构建 ID</Text>
|
||||
<Text className="row-value">{build.id}</Text>
|
||||
</View>
|
||||
<View className="info-row">
|
||||
<Text className="row-label">项目 ID</Text>
|
||||
<Text className="row-value">{build.websiteId}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="info-section">
|
||||
<Text className="section-title">Git 信息</Text>
|
||||
<View className="info-row">
|
||||
<Text className="row-label">分支</Text>
|
||||
<Text className="row-value">{build.branch}</Text>
|
||||
</View>
|
||||
<View className="info-row">
|
||||
<Text className="row-label">Commit ID</Text>
|
||||
<Text className="row-value commit">{build.commitId}</Text>
|
||||
</View>
|
||||
<View className="info-row">
|
||||
<Text className="row-label">提交者</Text>
|
||||
<Text className="row-value">{build.author}</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(build.startTime)}</Text>
|
||||
</View>
|
||||
<View className="info-row">
|
||||
<Text className="row-label">结束时间</Text>
|
||||
<Text className="row-value">{formatTime(build.endTime)}</Text>
|
||||
</View>
|
||||
<View className="info-row">
|
||||
<Text className="row-label">总耗时</Text>
|
||||
<Text className="row-value">{formatDuration(build.duration)}</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>
|
||||
)}
|
||||
|
||||
{activeTab === 'artifacts' && (
|
||||
<View className="artifacts-panel">
|
||||
{build.artifacts && build.artifacts.length > 0 ? (
|
||||
build.artifacts.map((artifact, index) => (
|
||||
<View className="artifact-item" key={index}>
|
||||
<View className="artifact-info">
|
||||
<Text className="artifact-name">{artifact.name}</Text>
|
||||
<Text className="artifact-size">
|
||||
{(artifact.size || 0) > 1024 * 1024
|
||||
? `${((artifact.size || 0) / 1024 / 1024).toFixed(2)} MB`
|
||||
: `${((artifact.size || 0) / 1024).toFixed(2)} KB`}
|
||||
</Text>
|
||||
</View>
|
||||
{artifact.downloadUrl && (
|
||||
<AtButton
|
||||
size="small"
|
||||
type="secondary"
|
||||
onClick={() => {
|
||||
if (artifact.downloadUrl) {
|
||||
Taro.setClipboardData({ data: artifact.downloadUrl })
|
||||
Taro.showToast({ title: '链接已复制', icon: 'success' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
复制链接
|
||||
</AtButton>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<View className="empty-artifacts">
|
||||
<Text className="iconfont icon-empty" />
|
||||
<Text>暂无构建产物</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user