feat(constool): 添加数据对比同步工具并重构动态表格构建器

- 新增 KitDiffSyncConstTool 工具类,支持集合数据的对比同步功能
- 添加数据同步回调接口和完整的增删改查批量操作支持
- 重命名 KitDynamicTableUtil 为 KitDynamicTableBuilder 并调整方法名称
- 修改 renderOrder 方法为 ofOrder,renderAll 方法为 ofAll
- 更新前端 FormatDuration 函数,支持年、天、时、分、秒格式化显示
- 添加字节格式化方法 renderBytes 到 KitStringUtil 工具类
- 在首页视图中集成 Pipeline 组件用于展示任务流程
- 添加窗口打开工具函数 OpenWindowInCurrent 和 OpenWindowInNew
- 创建 MybatisMapperBatchOpRender 批量操作生成器
This commit is contained in:
2026-03-06 19:19:34 +08:00
parent afbdbbdea8
commit a14809e203
22 changed files with 1749 additions and 275 deletions
+8
View File
@@ -273,3 +273,11 @@ export function IsNotBlank(str) {
export function IsBlank(str) {
return str == null || String(str).trim().length === 0
}
export function OpenWindowInCurrent(url) {
window.location.href = url
}
export function OpenWindowInNew(url) {
window.open(url, '_blank')
}
+16 -7
View File
@@ -390,22 +390,31 @@ export function FormatEnabled(value) {
*/
export function FormatDuration(value) {
if (value === null || value === undefined || value === '') {
return '-'
return ''
}
const seconds = Number(value)
const seconds = Number(value / 1000)
if (isNaN(seconds)) {
return '-'
return ''
}
const hours = Math.floor(seconds / 3600)
const years = Math.floor(seconds / (365 * 24 * 3600))
const days = Math.floor((seconds % (365 * 24 * 3600)) / (24 * 3600))
const hours = Math.floor((seconds % (24 * 3600)) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
const secs = (seconds % 60).toFixed(0)
if (hours > 0) {
return `${hours}小时${minutes}分${secs}秒`
if (years > 0) {
return `${years}年${days}天${hours}时${minutes}分${secs}秒`
} else if (days > 0) {
return `${days}天${hours}时${minutes}分${secs}秒`
} else if (hours > 0) {
return `${hours}时${minutes}分${secs}秒`
} else if (minutes > 0) {
return `${minutes}分${secs}秒`
} else {
if (secs === '0') {
return ``
}
return `${secs}秒`
}
}
@@ -0,0 +1,317 @@
<template>
<div :style="taskWindowStyle">
<div :style="taskHeaderStyle" />
<div :style="taskBodyStyle">
<!-- 标题和图标 -->
<el-row :gutter="2">
<el-col :span="16">{{ name }}</el-col>
<el-col :span="8" style="text-align: right">
<i :class="getStatusIconClassByStatus(status)" :style="getStatusIconColorByStatus(status)" />
</el-col>
</el-row>
<!-- 分割线 -->
<div style="padding-left: 10px; padding-right: 10px; padding-top: 2px; border-bottom: 1px solid #EBEEF5" />
<!-- 任务执行信息 -->
<div
v-if="click === false"
style="width: 100%; height: 40px; line-height: 30px; font-size: 12px; color: #606266;
text-align: center;padding: 9px 0;font-family: Arial, sans-serif"
>
{{ desc }}
</div>
<el-button
v-else
v-prevent-re-click
type="text"
style="width: 100%; height: 40px; line-height: 30px; font-size: 12px; color: #606266"
@click="onTaskDetailClick"
>
{{ desc }}
</el-button>
<el-row :gutter="2">
<!-- 持续时间 -->
<el-col :span="15" style="font-size: 12px;color: #606266;height: 32px;max-height: 32px;line-height: 32px">
{{ FormatDuration(status !== statusMap.running ? durationMillis : new Date().getTime() - startTimeMillis) }}
</el-col>
<!-- 任务失败,且支持重试 -->
<el-col v-if="status === statusMap.failed && retry === true" :span="9" style="font-size: 12px;color: #606266;height: 32px;max-height: 32px">
<el-button v-prevent-re-click type="text" @click="onRetryClick">重试</el-button>
</el-col>
<!-- 任务运行中,且包含按钮组 -->
<el-col v-if="buttonList && buttonList.length > 0" :span="9" style="font-size: 12px;color: #606266;height: 32px;max-height: 32px;padding-right: 5px">
<el-button
v-for="(item, index) in buttonList"
:key="index"
v-prevent-re-click
type="text"
:style="`width: ${item.text && item.text.length >= 4 ? '74px': '26px'
};color: ${item.color ? item.color : statusColorMap.running};display: ${item.condition ===
'all' ?
'inline-block':
(item.condition === status ? 'inline-block': 'none')};overflow: hidden;text-overflow: ellipsis;`"
@click="onButtonNodeClick(item)"
>{{ item.text }}</el-button>
</el-col>
</el-row>
</div>
<!-- 任务节点明细 -->
<cute-preview-drawer ref="taskDetailDrawer" :title="name">
<el-timeline>
<el-timeline-item
v-for="(taskDetail, index) in taskDetails"
:key="index"
:color="getColorHexByStatus(taskDetail.status)"
:timestamp="taskDetail.timestamp"
>
{{ taskDetail.content }}
</el-timeline-item>
</el-timeline>
</cute-preview-drawer>
</div>
</template>
<script>
import KitMessage from '@/utils/elementui/KitMessage'
import CutePreviewDrawer from '@/views/components/dev/CutePreviewDrawer.vue'
import { OpenWindowInCurrent, OpenWindowInNew } from '@/utils/KitUtil'
import request from '@/utils/request'
import { FormatDuration } from '@/utils/filters'
export default {
name: 'PipeTask',
components: { CutePreviewDrawer },
props: {
/**
* 任务名称
*/
name: {
type: String,
required: true
},
/**
* 任务编码
*/
code: {
type: String,
required: true
},
/**
* 开始时间戳。单位:毫秒
*/
startTimeMillis: {
type: Number,
required: true
},
/**
* 持续时间。单位:秒
*/
durationMillis: {
type: Number,
required: true
},
/**
* 状态
*/
status: {
type: String,
required: true
},
/**
* 状态(翻译)
*/
statusDesc: {
type: String,
required: true
},
/**
* 任务执行信息
*/
desc: {
type: String,
required: true
},
/**
* 是否可以点击
*/
click: {
type: Boolean,
required: false,
default: false
},
/**
* 是否可以重试
*/
retry: {
type: Boolean,
required: false,
default: false
},
/**
* 任务明细类型(gitlab、jenkins等)
*/
detailType: {
type: String,
required: false,
default: null
},
/**
* 按钮组
* @property type: execute(请求系统api)、link(调用三方链接跳转)
* @property text: 按钮标题
* @property color: 按钮颜色
* @property condition: 符合条件才展示,当值为`all`时,非pending状态都展示
* @property method: 当type=execute时,配置请求方式:GET/POST
* @property requestUrl: 当type=execute时,配置请求路径和参数
* @property linkUrl: 当type=link时,配置跳转路径和参数
* @property isBlank: 当type=link时,判断是否在新页面中打开
*/
buttonList: {
type: Array,
required: false,
default: function() {
return []
}
}
},
data() {
return {
statusMap: {
pending: 'pending',
running: 'running',
success: 'success',
failed: 'failed'
},
statusColorMap: {
pending: '#EBEEF5',
success: '#67C23A',
failed: '#F56C6C',
running: '#409EFF'
},
// 任务明细
taskDetails: [
// { timestamp: '2018-04-03 20:46', content: '开始初始化', status: 'success' },
// { timestamp: '2018-04-03 20:46', content: '开始初始化', status: 'success' },
// { timestamp: '2018-04-03 20:46', content: '开始初始化', status: 'failed' }
]
}
},
computed: {
taskWindowStyle() {
const colorHex = this.getColorHexByStatus(this.status)
return {
width: '240px',
maxWidth: '240px',
height: '100px',
maxHeight: '100px',
border: `1px solid ${colorHex}`,
borderRadius: '6px',
overflow: 'hidden',
display: 'inline-block'
}
},
taskHeaderStyle() {
const colorHex = this.getColorHexByStatus(this.status)
return {
width: '100%',
height: '5px',
backgroundColor: colorHex
}
},
taskBodyStyle() {
return {
padding: '5px',
fontSize: '14px',
backgroundColor: '#FFFFFF',
color: '#606266',
height: '95px'
}
}
},
watch: {},
mounted() {
},
methods: {
FormatDuration,
/**
* 根据任务状态获取颜色Hex值
*/
getColorHexByStatus(status) {
const colorHex = this.statusColorMap[status]
if (colorHex) {
return colorHex
}
return this.statusColorMap.pending
},
/**
* 根据任务状态渲染状态图标样式
* @param status
*/
getStatusIconClassByStatus(status) {
if (this.statusMap.success === status) {
return 'el-icon-circle-check'
}
if (this.statusMap.failed === status) {
return 'el-icon-circle-close'
}
if (this.statusMap.running === status) {
return 'el-icon-loading'
}
return 'el-icon-time'
},
/**
* 根据任务状态渲染状态图标颜色
* @param status
*/
getStatusIconColorByStatus(status) {
const styles = {
width: '14px',
height: '14px'
}
styles.color = this.getColorHexByStatus(status)
return styles
},
onTaskDetailClick() {
console.error('当任务卡片可点击时,查看明细记录')
if (this.statusMap.pending === this.status) {
KitMessage.Error('节点任务未开始')
return
}
if (this.detailType === 'gitlab') {
// 获取gitlab任务明细
} else if (this.detailType === 'jenkins') {
// 获取jenkins任务明细
} else {
// 获取业务服务明细
}
this.$refs.taskDetailDrawer.show()
},
onRetryClick() {
console.error('当任务卡片可重试时,任务重试', this.code)
this.$emit('retry', this.code)
},
async onButtonNodeClick(buttonProps) {
console.error('当任务卡片包含按钮组时,某个按钮被单击', buttonProps)
if (buttonProps.type === 'execute') {
const { method, requestUrl } = buttonProps
if (method === 'get' || method === 'post') {
const response = await request({ url: requestUrl, method: method })
console.error('请求系统内置接口结果:', response)
} else {
KitMessage.Error(`'${buttonProps.text}' 按钮异常,不支持的请求方式`)
}
} else if (buttonProps.type === 'link') {
const { blank, linkUrl } = buttonProps
if (blank) {
OpenWindowInNew(linkUrl)
} else {
OpenWindowInCurrent(linkUrl)
}
} else {
KitMessage.Error(`'${buttonProps.text}' 按钮异常,不支持的类型`)
}
}
}
}
</script>
@@ -0,0 +1,70 @@
<template>
<cute-horizontal-scroll-bar :width="maxWidth">
<i v-for="(item, index) in taskNodes" :key="index">
<!-- 最后一个元素如何渲染 -->
<i v-if="index === taskNodes.length - 1">
<pipe-task v-bind="item" />
</i>
<!-- 其他元素如何渲染 -->
<i v-else class="pipeline-card">
<pipe-task v-bind="item" />
<div class="pipeline-link" />
</i>
</i>
</cute-horizontal-scroll-bar>
</template>
<script>
import PipeTask from '@/views/components/devops/pipeline/PipeTask.vue'
import CuteHorizontalScrollBar from '@/views/components/dev/CuteHorizontalScrollBar.vue'
export default {
name: 'Pipeline',
components: { CuteHorizontalScrollBar, PipeTask },
props: {
/**
* 流水线全局唯一实例ID
*/
instanceId: {
type: String,
required: true
},
/**
* 流水线任务节点明细
*/
taskNodes: {
type: Array,
required: true
}
},
data() {
return {}
},
computed: {
maxWidth() {
return `${document.body.clientWidth - 310}px`
}
},
watch: {},
mounted() {
},
methods: {}
}
</script>
<style lang="scss" scoped>
.pipeline-card {
width: 240px;
max-width: 240px;
}
.pipeline-link {
height: 3px;
width: 30px;
display: inline-block;
background-color: #E4E7ED;
margin-bottom: 3em;
margin-right: 4px;
}
</style>
+397 -2
View File
@@ -3,11 +3,20 @@
<github-corner class="github-corner" />
<el-row :gutter="40" style="background-color: white;margin-left: 2px;margin-right: 2px;padding: 6px">
<el-col>系统API文档:<el-link type="primary" :href="swaggerApi" target="_blank">{{ swaggerApi }}</el-link></el-col>
<el-col>系统API文档:
<el-link type="primary" :href="swaggerApi" target="_blank">{{ swaggerApi }}</el-link>
</el-col>
</el-row>
<panel-group @handleSetLineChartData="handleSetLineChartData" />
<div style="border: 1px solid #E4E7ED;margin-bottom: 10px">
<Pipeline instance-id="instanceId1" :task-nodes="task1Nodes" />
<Pipeline instance-id="instanceId2" :task-nodes="task2Nodes" />
<Pipeline instance-id="instanceId3" :task-nodes="task3Nodes" />
<Pipeline instance-id="instanceId4" :task-nodes="task4Nodes" />
</div>
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
<line-chart :chart-data="lineChartData" />
</el-row>
@@ -39,6 +48,7 @@ import RadarChart from '@/components/Echarts/RadarChart'
import PieChart from '@/components/Echarts/PieChart'
import BarChart from '@/components/Echarts/BarChart'
import store from '@/store'
import Pipeline from '@/views/components/devops/pipeline/Pipeline.vue'
const lineChartData = {
newVisitis: {
@@ -62,6 +72,7 @@ const lineChartData = {
export default {
name: 'Dashboard',
components: {
Pipeline,
GithubCorner,
PanelGroup,
LineChart,
@@ -72,7 +83,391 @@ export default {
data() {
return {
swaggerApi: store.getters.swaggerApi,
lineChartData: lineChartData.newVisitis
lineChartData: lineChartData.newVisitis,
task1Nodes: [
{
click: false,
code: 'release',
desc: '执行成功',
durationMillis: 7446,
name: '环境准备',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'merge',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 50331,
name: '合并代码',
startTimeMillis: 1652343110691,
status: 'success',
statusDesc: '成功'
},
{
click: false,
code: 'stc_upload',
desc: '代码上传成功',
durationMillis: 16123,
name: 'STC扫描(代码上传)',
startTimeMillis: 1652343161022,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'build',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 104692,
name: '构建',
startTimeMillis: 1652343177145,
status: 'success',
statusDesc: '成功'
},
{
buttonList: [
{
condition: 'all',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看扫描结果',
type: 'link'
}
],
click: false,
code: 'stc_result',
desc: 'STC扫描通过',
durationMillis: 18971,
name: 'STC扫描(扫描结果)',
startTimeMillis: 1652343281837,
status: 'success',
statusDesc: '成功'
},
{
click: false,
code: 'doPreCheck',
desc: '【田俊】确认通过',
durationMillis: 69937,
name: '发布前确认',
startTimeMillis: 1652343300808,
status: 'success',
statusDesc: '成功'
},
{
buttonList: [
{
condition: 'success',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看部署详情',
type: 'link'
}
],
click: false,
code: 'deploy',
desc: '成功',
durationMillis: 161975,
name: '部署',
startTimeMillis: 1652343370745,
status: 'success',
statusDesc: '成功'
},
{
buttonList: [
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/doCheck?id=60012',
text: '确认合并',
type: 'execute'
}
],
click: false,
code: 'mergeMaster',
detailType: 'gitlab',
durationMillis: 0,
name: '合并主干',
desc: '合并master确认',
startTimeMillis: 0,
status: 'running',
statusDesc: '执行中'
}
],
task2Nodes: [
{
click: false,
code: 'release',
desc: '执行成功',
durationMillis: 7446,
name: '环境准备',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'merge',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 50331,
name: '合并代码',
startTimeMillis: 1652343110691,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'build',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 104692,
name: '构建',
startTimeMillis: 1652343177145,
status: 'success',
statusDesc: '成功'
},
{
buttonList: [
{
condition: 'success',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看部署详情',
type: 'link'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doAgree?id=60012',
text: '同意',
type: 'execute'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doRefuse?id=60012',
text: '拒绝',
color: '#ff0000',
type: 'execute'
}
],
click: false,
code: 'deploy',
desc: '等待部署审批',
durationMillis: 161975,
name: '部署',
startTimeMillis: 1652343370745,
status: 'running',
statusDesc: '执行中'
},
{
buttonList: [
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/doCheck?id=60012',
text: '确认合并',
type: 'execute'
}
],
click: false,
code: 'mergeMaster',
detailType: 'gitlab',
durationMillis: 7446,
name: '合并主干',
desc: '合并master确认',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
}
],
task3Nodes: [
{
click: false,
code: 'release',
desc: '执行成功',
durationMillis: 7446,
name: '环境准备',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'merge',
desc: '执行失败',
detailType: 'gitlab',
durationMillis: 50331,
name: '合并代码',
startTimeMillis: 1652343110691,
status: 'failed',
statusDesc: '失败'
},
{
click: true,
retry: true,
code: 'build',
desc: '待执行',
detailType: 'gitlab',
durationMillis: 0,
name: '构建',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
},
{
buttonList: [
{
condition: 'success',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看部署详情',
type: 'link'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doAgree?id=60012',
text: '同意',
type: 'execute'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doRefuse?id=60012',
text: '拒绝',
color: '#ff0000',
type: 'execute'
}
],
click: false,
code: 'deploy',
desc: '待执行',
durationMillis: 0,
name: '部署',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
},
{
buttonList: [
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/doCheck?id=60012',
text: '确认合并',
type: 'execute'
}
],
click: false,
code: 'mergeMaster',
detailType: 'gitlab',
durationMillis: 0,
name: '合并主干',
desc: '待执行',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
}
],
task4Nodes: [
{
click: false,
code: 'release',
desc: '执行成功',
durationMillis: 7446,
name: '环境准备',
startTimeMillis: 1652343103245,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'merge',
desc: '执行成功',
detailType: 'gitlab',
durationMillis: 50331,
name: '合并代码',
startTimeMillis: 1652343110691,
status: 'success',
statusDesc: '成功'
},
{
click: true,
retry: true,
code: 'build',
desc: '执行失败',
detailType: 'gitlab',
durationMillis: 50331,
name: '构建',
startTimeMillis: 1652343110691,
status: 'failed',
statusDesc: '失败'
},
{
buttonList: [
{
condition: 'success',
blank: true,
linkUrl: 'https://devops.odboy.cn/depolyHistory/release?appName=kenaito-pilot&id=1',
text: '查看部署详情',
type: 'link'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doAgree?id=60012',
text: '同意',
type: 'execute'
},
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/apply/doRefuse?id=60012',
text: '拒绝',
color: '#ff0000',
type: 'execute'
}
],
click: false,
code: 'deploy',
desc: '待执行',
durationMillis: 0,
name: '部署',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
},
{
buttonList: [
{
condition: 'running',
method: 'get',
requestUrl: 'api/devops/ci/doCheck?id=60012',
text: '确认合并',
type: 'execute'
}
],
click: false,
code: 'mergeMaster',
detailType: 'gitlab',
durationMillis: 0,
name: '合并主干',
desc: '待执行',
startTimeMillis: 0,
status: 'pending',
statusDesc: '待执行'
}
]
}
},
mounted() {