refactor: 代码规范、Bug修复

This commit is contained in:
2025-09-26 09:10:37 +08:00
parent 3610f8f7db
commit dcc288acc4
572 changed files with 41415 additions and 44746 deletions
@@ -1,121 +1,124 @@
<!--Yaml编辑器 基于-->
<!--<a href="https://github.com/codemirror/CodeMirror" target="_blank">CodeMirror</a>,-->
<!--主题预览地址 <a href="https://codemirror.net/demo/theme.html#idea" target="_blank">Theme</a>-->
<!--</p>-->
<!-- mode取值如下:yaml、java、go、swift、dockerfile、groovy、lua、perl、python、ruby、sql、xml、vue -->
<template>
<textarea ref="textarea" />
</template>
<script>
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// 替换主题这里需修改名称
// 主题预览地址 https://blog.csdn.net/qq_41694291/article/details/106429772
import 'codemirror/theme/darcula.css'
import 'codemirror/mode/javascript/javascript'
import 'codemirror/mode/yaml/yaml'
import 'codemirror/mode/go/go'
import 'codemirror/mode/swift/swift'
import 'codemirror/mode/dockerfile/dockerfile'
import 'codemirror/mode/groovy/groovy'
import 'codemirror/mode/lua/lua'
import 'codemirror/mode/perl/perl'
import 'codemirror/mode/python/python'
import 'codemirror/mode/ruby/ruby'
import 'codemirror/mode/sql/sql'
import 'codemirror/mode/vue/vue'
import 'codemirror/mode/xml/xml'
import CsMessage from '@/utils/elementui/CsMessage'
const SupportModeList = ['yaml', 'java', 'go', 'swift', 'dockerfile', 'groovy', 'lua', 'perl', 'python', 'ruby', 'sql', 'xml', 'vue']
export default {
name: 'CuteCodeEditor',
props: {
content: {
type: String,
required: true,
default: ''
},
height: {
type: String,
required: false,
default: '500px'
},
readOnly: {
type: Boolean,
required: false,
default: false
},
mode: {
type: String,
required: false,
default: 'yaml'
}
},
data() {
return {
editor: false
}
},
watch: {
content(newVal, oldVal) {
const editorValue = this.editor.getValue()
if (newVal !== editorValue) {
this.editor.setValue(newVal)
}
},
height(newVal, oldVal) {
this.editor.setSize('auto', newVal)
}
},
mounted() {
if (this.mode && SupportModeList.includes(this.mode)) {
let mode = 'yaml'
if (this.mode === 'java' || this.mode === 'javascript') {
mode = 'javascript'
} else {
mode = this.mode
}
this.editor = CodeMirror.fromTextArea(this.$refs.textarea, {
mode: mode,
autoRefresh: true,
lineNumbers: true,
lint: true,
lineWrapping: true,
tabSize: 2,
cursorHeight: 1,
// 替换主题这里需修改名称
theme: 'darcula'
})
this.editor.setSize('auto', this.height)
if (this.content) {
this.editor.setValue(this.content)
}
this.editor.setOption('readOnly', this.readOnly)
this.editor.on('change', cm => {
this.$emit('change', cm.getValue())
})
return
}
CsMessage.Error('不支持的mode')
},
methods: {
getValue() {
if (this.editor == null) {
return ''
}
return this.editor.getValue()
},
setValue(val) {
if (this.editor == null) {
return
}
this.editor.setValue(val)
},
resetField() {
this.editor.setValue('')
}
}
}
</script>
<!--
* 代码编辑器
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
* @description 基于Yaml编辑器 https://github.com/codemirror/CodeMirror,主题预览地址 https://codemirror.net/demo/theme.html#idea
* @参数mode取值范围 yaml、java、go、swift、dockerfile、groovy、lua、perl、python、ruby、sql、xml、vue
-->
<template>
<textarea ref="textarea" />
</template>
<script>
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// 替换主题这里需修改名称
// 主题预览地址 https://blog.csdn.net/qq_41694291/article/details/106429772
import 'codemirror/theme/darcula.css'
import 'codemirror/mode/javascript/javascript'
import 'codemirror/mode/yaml/yaml'
import 'codemirror/mode/go/go'
import 'codemirror/mode/swift/swift'
import 'codemirror/mode/dockerfile/dockerfile'
import 'codemirror/mode/groovy/groovy'
import 'codemirror/mode/lua/lua'
import 'codemirror/mode/perl/perl'
import 'codemirror/mode/python/python'
import 'codemirror/mode/ruby/ruby'
import 'codemirror/mode/sql/sql'
import 'codemirror/mode/vue/vue'
import 'codemirror/mode/xml/xml'
import CsMessage from '@/utils/elementui/CsMessage'
const SupportModeList = ['yaml', 'java', 'go', 'swift', 'dockerfile', 'groovy', 'lua', 'perl', 'python', 'ruby', 'sql', 'xml', 'vue']
export default {
name: 'CuteCodeEditor',
props: {
content: {
type: String,
required: true,
default: ''
},
height: {
type: String,
required: false,
default: '500px'
},
readOnly: {
type: Boolean,
required: false,
default: false
},
mode: {
type: String,
required: false,
default: 'yaml'
}
},
data() {
return {
editor: false
}
},
watch: {
content(newVal, oldVal) {
const editorValue = this.editor.getValue()
if (newVal !== editorValue) {
this.editor.setValue(newVal)
}
},
height(newVal, oldVal) {
this.editor.setSize('auto', newVal)
}
},
mounted() {
if (this.mode && SupportModeList.includes(this.mode)) {
let mode = 'yaml'
if (this.mode === 'java' || this.mode === 'javascript') {
mode = 'javascript'
} else {
mode = this.mode
}
this.editor = CodeMirror.fromTextArea(this.$refs.textarea, {
mode: mode,
autoRefresh: true,
lineNumbers: true,
lint: true,
lineWrapping: true,
tabSize: 2,
cursorHeight: 1,
// 替换主题这里需修改名称
theme: 'darcula'
})
this.editor.setSize('auto', this.height)
if (this.content) {
this.editor.setValue(this.content)
}
this.editor.setOption('readOnly', this.readOnly)
this.editor.on('change', cm => {
this.$emit('change', cm.getValue())
})
return
}
CsMessage.Error('不支持的mode')
},
methods: {
getValue() {
if (this.editor == null) {
return ''
}
return this.editor.getValue()
},
setValue(val) {
if (this.editor == null) {
return
}
this.editor.setValue(val)
},
resetField() {
this.editor.setValue('')
}
}
}
</script>
@@ -1,158 +1,167 @@
<template>
<div>
<el-table
ref="table"
v-loading="crud.loading"
stripe
empty-text="暂无数据"
:row-key="crud.primaryKey"
fit
:data="crud.dataSource"
style="width: 100%;"
:height="height"
:max-height="height"
@selection-change="onTableSelectionChange"
>
<el-table-column v-if="mode === 'multi'" type="selection" width="55" />
<slot />
<el-pagination
:current-page="pageProps.current"
:page-sizes="[10, 20, 50]"
:page-size="pageProps.pageSize"
layout="total, sizes, prev, pager, next"
:total="pageProps.total"
@size-change="(size) => crud.onPageChange(pageProps.current, size, pageProps.total)"
@current-change="(current) => crud.onPageChange(current, pageProps.pageSize, pageProps.total)"
/>
</el-table></div>
</template>
<script>
import Sortable from 'sortablejs'
export default {
name: 'CuteDragTable',
props: {
primaryKey: {
type: String,
required: true,
default: null
},
fetch: {
type: Function,
required: false,
default: null
},
paramsTransform: {
type: Function,
required: false,
default: null
},
responseTransform: {
type: Function,
required: false,
default: null
},
mode: {
type: String,
required: false,
default: 'none'
},
pageProps: {
type: Object,
required: true,
default: function() {
return {
current: 1,
pageSize: 10,
total: 0
}
}
}
},
data() {
return {
crud: {
loading: false,
primaryKey: this.primaryKey ? this.primaryKey : 'id',
fetchUrl: null,
dataSource: [],
onPageChange: (currentPage, pageSize) => {
this.pageProps.current = currentPage
this.pageProps.pageSize = pageSize
console.log('on-page-change', currentPage, pageSize)
// this.$emit('onPageChange', currentPage, pageSize, total)
this.refresh()
}
}
}
},
computed: {
height() {
return document.documentElement.clientHeight - 300
}
},
mounted() {
this.initData()
},
methods: {
refresh() {
this.initData()
this.initDragTable()
},
async initData() {
let params = {}
// 自定义请求参数
if (this.paramsTransform) {
params = this.paramsTransform(this.pageProps)
}
if (this.fetch) {
try {
this.crud.loading = true
// 请求远程数据
const response = await this.fetch(params)
// console.error('response', response)
// { content: [], totalElements: 0 }
// 转换为表格所需要的数据
if (this.responseTransform) {
return this.responseTransform(response)
}
if (response && response.hasOwnProperty('totalElements') && response.hasOwnProperty('content')) {
this.pageProps.total = response.totalElements
this.crud.dataSource = response.content
}
return response
} finally {
this.crud.loading = false
}
}
},
onTableSelectionChange(selection) {
this.$emit('selection-change', selection)
},
initDragTable() {
const that = this
const table = that.$refs.table.$el.querySelectorAll('.el-table__body-wrapper > table > tbody')[0]
Sortable.create(table, {
animation: 1000,
onEnd({ newIndex, oldIndex }) {
// 拖拽排序数据
that.crud.dataSource.splice(newIndex, 0, that.crud.dataSource.splice(oldIndex, 1)[0])
const newArray = that.crud.dataSource.slice(0)
that.crud.dataSource = [] // 必须有此步骤,不然拖拽后回弹
that.$nextTick(function() {
that.crud.dataSource = newArray // 重新赋值,用新数据来刷新视图
this.updateOrderNum(that.crud.dataSource)// 更改列表中的序号,使序号1.2.3.4.....显示,不然就是行拖拽后乱序显示如:3.2.4.1...
})
}
})
},
updateOrderNum(data) {
data.forEach((item, index) => {
item.orderNum = index + 1
})
}
}
}
</script>
<!--
* 拖拽表格组件:封装自定义逻辑与参数,表格支持拖拽排序
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
-->
<template>
<div>
<el-table
ref="table"
v-loading="crud.loading"
stripe
empty-text="暂无数据"
:row-key="crud.primaryKey"
fit
:data="crud.dataSource"
style="width: 100%;"
:height="height"
:max-height="height"
@selection-change="onTableSelectionChange"
>
<el-table-column v-if="mode === 'multi'" type="selection" width="55" />
<slot />
<el-pagination
:current-page="pageProps.current"
:page-sizes="[10, 20, 50]"
:page-size="pageProps.pageSize"
layout="total, sizes, prev, pager, next"
:total="pageProps.total"
@size-change="(size) => crud.onPageChange(pageProps.current, size, pageProps.total)"
@current-change="(current) => crud.onPageChange(current, pageProps.pageSize, pageProps.total)"
/>
</el-table></div>
</template>
<script>
import Sortable from 'sortablejs'
export default {
name: 'CuteDragTable',
props: {
primaryKey: {
type: String,
required: true,
default: null
},
fetch: {
type: Function,
required: false,
default: null
},
paramsTransform: {
type: Function,
required: false,
default: null
},
responseTransform: {
type: Function,
required: false,
default: null
},
mode: {
type: String,
required: false,
default: 'none'
},
pageProps: {
type: Object,
required: true,
default: function() {
return {
current: 1,
pageSize: 10,
total: 0
}
}
}
},
data() {
return {
crud: {
loading: false,
primaryKey: this.primaryKey ? this.primaryKey : 'id',
fetchUrl: null,
dataSource: [],
onPageChange: (currentPage, pageSize) => {
this.pageProps.current = currentPage
this.pageProps.pageSize = pageSize
console.log('on-page-change', currentPage, pageSize)
// this.$emit('onPageChange', currentPage, pageSize, total)
this.refresh()
}
}
}
},
computed: {
height() {
return document.documentElement.clientHeight - 300
}
},
mounted() {
this.initData()
},
methods: {
refresh() {
this.initData()
this.initDragTable()
},
async initData() {
let params = {}
// 自定义请求参数
if (this.paramsTransform) {
params = this.paramsTransform(this.pageProps)
}
if (this.fetch) {
try {
this.crud.loading = true
// 请求远程数据
const response = await this.fetch(params)
// console.error('response', response)
// { content: [], totalElements: 0 }
// 转换为表格所需要的数据
if (this.responseTransform) {
return this.responseTransform(response)
}
if (response && response.hasOwnProperty('totalElements') && response.hasOwnProperty('content')) {
this.pageProps.total = response.totalElements
this.crud.dataSource = response.content
}
return response
} finally {
this.crud.loading = false
}
}
},
onTableSelectionChange(selection) {
this.$emit('selection-change', selection)
},
initDragTable() {
const that = this
const table = that.$refs.table.$el.querySelectorAll('.el-table__body-wrapper > table > tbody')[0]
Sortable.create(table, {
animation: 1000,
onEnd({ newIndex, oldIndex }) {
// 拖拽排序数据
that.crud.dataSource.splice(newIndex, 0, that.crud.dataSource.splice(oldIndex, 1)[0])
const newArray = that.crud.dataSource.slice(0)
that.crud.dataSource = [] // 必须有此步骤,不然拖拽后回弹
that.$nextTick(function() {
that.crud.dataSource = newArray // 重新赋值,用新数据来刷新视图
this.updateOrderNum(that.crud.dataSource)// 更改列表中的序号,使序号1.2.3.4.....显示,不然就是行拖拽后乱序显示如:3.2.4.1...
})
}
})
},
updateOrderNum(data) {
data.forEach((item, index) => {
item.orderNum = index + 1
})
},
getDataSource() {
return this.crud.dataSource
}
}
}
</script>
@@ -1,99 +1,105 @@
<template>
<el-dialog
:title="title"
:visible.sync="visible"
:width="width"
:fullscreen="fullscreen"
:before-close="beforeClose"
:modal="false"
>
<div class="container-form">
<el-form ref="form" :model="model" :rules="rules">
<!-- 这里是插槽,用于渲染传入的 el-form-item -->
<slot />
</el-form>
</div>
<el-divider />
<div class="dialog-footer" style="padding-right: 30px;text-align: right">
<el-button type="danger" @click="hidden">取 消</el-button>
<!--<el-button @click="resetForm('form')">重 置</el-button>-->
<el-button type="primary" @click="submitForm('form')">提 交</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'CuteFormDialog',
props: {
title: {
type: String,
required: true,
default: '默认标题'
},
width: {
type: String,
required: false,
default: '40%'
},
fullscreen: {
type: Boolean,
required: false,
default: false
},
model: {
type: Object,
required: true,
default: null
},
rules: {
type: Object,
required: true,
default: null
}
},
data() {
return {
visible: false
}
},
methods: {
show() {
this.visible = true
},
hidden() {
this.resetForm('form')
this.visible = false
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$emit('submit', this.model)
} else {
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
beforeClose() {
this.hidden()
}
}
}
</script>
<style scoped>
.container-form {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
padding-right: 20px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
.container-form::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera*/
}
</style>
<!--
* 简单表单弹窗:封装了常用方法和参数
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
-->
<template>
<el-dialog
:title="title"
:visible.sync="visible"
:width="width"
:fullscreen="fullscreen"
:before-close="beforeClose"
:modal="false"
>
<div class="container-form">
<el-form ref="form" :model="model" :rules="rules">
<!-- 这里是插槽,用于渲染传入的 el-form-item -->
<slot />
</el-form>
</div>
<el-divider />
<div class="dialog-footer" style="padding-right: 30px;text-align: right">
<el-button type="danger" @click="hidden">取 消</el-button>
<!--<el-button @click="resetForm('form')">重 置</el-button>-->
<el-button v-prevent-re-click type="primary" @click="submitForm('form')">提 交</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'CuteFormDialog',
props: {
title: {
type: String,
required: true,
default: '默认标题'
},
width: {
type: String,
required: false,
default: '40%'
},
fullscreen: {
type: Boolean,
required: false,
default: false
},
model: {
type: Object,
required: true,
default: null
},
rules: {
type: Object,
required: true,
default: null
}
},
data() {
return {
visible: false
}
},
methods: {
show() {
this.visible = true
},
hidden() {
this.resetForm('form')
this.visible = false
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$emit('submit', this.model)
} else {
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
beforeClose() {
this.hidden()
}
}
}
</script>
<style scoped>
.container-form {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
padding-right: 20px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
.container-form::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera*/
}
</style>
@@ -1,97 +1,104 @@
<template>
<el-drawer
:title="title"
:visible.sync="visible"
:size="width"
:before-close="beforeClose"
:append-to-body="true"
:close-on-press-escape="false"
:destroy-on-close="true"
:show-close="false"
:modal="false"
>
<div class="container-form">
<el-form ref="form" :model="model" :rules="rules">
<!-- 这里是插槽,用于渲染传入的 el-form-item -->
<slot />
</el-form>
</div>
<el-divider />
<div class="dialog-footer" style="padding-right: 30px;text-align: right">
<el-button type="danger" @click="hidden">取 消</el-button>
<el-button type="primary" @click="submitForm('form')">提 交</el-button>
</div>
</el-drawer>
</template>
<script>
export default {
name: 'CuteFormDrawer',
props: {
title: {
type: String,
required: true,
default: '默认标题'
},
width: {
type: String,
required: false,
default: '40%'
},
model: {
type: Object,
required: true,
default: null
},
rules: {
type: Object,
required: true,
default: null
}
},
data() {
return {
visible: false
}
},
methods: {
show() {
this.visible = true
},
hidden() {
this.resetForm('form')
this.visible = false
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$emit('submit', this.model)
} else {
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
beforeClose() {
this.hidden()
}
}
}
</script>
<style scoped>
.container-form {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
height: 85%;
padding-right: 20px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
.container-form::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera*/
}
</style>
<!--
* 简单表单抽屉:封装了常用方法和参数
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
-->
<template>
<el-drawer
:title="title"
:visible.sync="visible"
:size="width"
:before-close="beforeClose"
:append-to-body="true"
:close-on-press-escape="false"
:wrapper-closable="false"
:destroy-on-close="true"
:show-close="true"
:modal="false"
>
<div class="container-form">
<el-form ref="form" :model="model" :rules="rules">
<!-- 这里是插槽,用于渲染传入的 el-form-item -->
<slot />
</el-form>
</div>
<el-divider />
<div class="dialog-footer" style="padding-right: 30px;text-align: right">
<el-button type="danger" @click="hidden">取 消</el-button>
<el-button v-prevent-re-click type="primary" @click="submitForm('form')">提 交</el-button>
</div>
</el-drawer>
</template>
<script>
export default {
name: 'CuteFormDrawer',
props: {
title: {
type: String,
required: true,
default: '默认标题'
},
width: {
type: String,
required: false,
default: '40%'
},
model: {
type: Object,
required: true,
default: null
},
rules: {
type: Object,
required: true,
default: null
}
},
data() {
return {
visible: false
}
},
methods: {
show() {
this.visible = true
},
hidden() {
this.resetForm('form')
this.visible = false
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$emit('submit', this.model)
} else {
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
},
beforeClose() {
this.hidden()
}
}
}
</script>
<style scoped>
.container-form {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
height: 85%;
padding-right: 20px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
.container-form::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera*/
}
</style>
@@ -0,0 +1,40 @@
<!--
* 横向滚动条容器
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-20
-->
<template>
<div class="overflow-panel">
<slot />
</div>
</template>
<script>
export default {
name: 'CuteHorizontalScrollBar'
}
</script>
<style scoped>
.overflow-panel {
width: 1500px; /* 固定宽度 */
overflow-x: auto; /* 允许水平滚动 */
white-space: nowrap; /* 防止内容换行,使所有内容在一行显示 */
padding: 30px 20px 30px 20px;
}
.overflow-panel::-webkit-scrollbar {
height: 5px; /* 设置水平滚动条高度 */
}
.overflow-panel::-webkit-scrollbar-track {
background: #f1f1f1; /* 滚动条底色 */
}
.overflow-panel::-webkit-scrollbar-thumb {
background: #DCDFE6; /* 滚动条颜色 */
}
</style>
@@ -1,91 +1,93 @@
<!--Yaml编辑器 基于-->
<!--<a href="https://github.com/codemirror/CodeMirror" target="_blank">CodeMirror</a>,-->
<!--主题预览地址 <a href="https://codemirror.net/demo/theme.html#idea" target="_blank">Theme</a>-->
<!--</p>-->
<template>
<textarea ref="textarea" />
</template>
<script>
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// 替换主题这里需修改名称
// 主题预览地址 https://blog.csdn.net/qq_41694291/article/details/106429772
import 'codemirror/theme/darcula.css'
import 'codemirror/mode/javascript/javascript'
export default {
name: 'CuteJavaEditor',
props: {
content: {
type: String,
required: true,
default: ''
},
height: {
type: String,
required: false,
default: '500px'
},
readOnly: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
editor: false
}
},
watch: {
content(newVal, oldVal) {
const editorValue = this.editor.getValue()
if (newVal !== editorValue) {
this.editor.setValue(newVal)
}
},
height(newVal, oldVal) {
this.editor.setSize('auto', newVal)
}
},
mounted() {
this.editor = CodeMirror.fromTextArea(this.$refs.textarea, {
mode: 'javascript',
autoRefresh: true,
lineNumbers: true,
lint: true,
lineWrapping: true,
tabSize: 2,
cursorHeight: 1,
// 替换主题这里需修改名称
theme: 'darcula'
})
this.editor.setSize('auto', this.height)
if (this.content) {
this.editor.setValue(this.content)
}
this.editor.setOption('readOnly', this.readOnly)
this.editor.on('change', cm => {
this.$emit('change', cm.getValue())
})
},
methods: {
getValue() {
if (this.editor == null) {
return ''
}
return this.editor.getValue()
},
setValue(val) {
if (this.editor == null) {
return
}
this.editor.setValue(val)
},
resetField() {
this.editor.setValue('')
}
}
}
</script>
<!--
* Java代码编辑器
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
-->
<template>
<textarea ref="textarea" />
</template>
<script>
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// 替换主题这里需修改名称
// 主题预览地址 https://blog.csdn.net/qq_41694291/article/details/106429772
import 'codemirror/theme/darcula.css'
import 'codemirror/mode/javascript/javascript'
export default {
name: 'CuteJavaEditor',
props: {
content: {
type: String,
required: true,
default: ''
},
height: {
type: String,
required: false,
default: '500px'
},
readOnly: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
editor: false
}
},
watch: {
content(newVal, oldVal) {
const editorValue = this.editor.getValue()
if (newVal !== editorValue) {
this.editor.setValue(newVal)
}
},
height(newVal, oldVal) {
this.editor.setSize('auto', newVal)
}
},
mounted() {
this.editor = CodeMirror.fromTextArea(this.$refs.textarea, {
mode: 'javascript',
autoRefresh: true,
lineNumbers: true,
lint: true,
lineWrapping: true,
tabSize: 2,
cursorHeight: 1,
// 替换主题这里需修改名称
theme: 'darcula'
})
this.editor.setSize('auto', this.height)
if (this.content) {
this.editor.setValue(this.content)
}
this.editor.setOption('readOnly', this.readOnly)
this.editor.on('change', cm => {
this.$emit('change', cm.getValue())
})
},
methods: {
getValue() {
if (this.editor == null) {
return ''
}
return this.editor.getValue()
},
setValue(val) {
if (this.editor == null) {
return
}
this.editor.setValue(val)
},
resetField() {
this.editor.setValue('')
}
}
}
</script>
@@ -0,0 +1,34 @@
<!--
* 文字超链接
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-26
-->
<template>
<el-link
type="primary"
:underline="false"
:href="href"
target="_blank"
>{{ title }}</el-link>
</template>
<script>
export default {
name: 'CuteLink',
props: {
title: {
type: String,
required: true,
default: '默认标题'
},
href: {
type: String,
required: true,
default: 'https://github.com/odboy-tianjun'
}
}
}
</script>
@@ -1,76 +1,83 @@
<!--<p class="warn-content">-->
<!--Markdown 基于-->
<!--<el-link type="primary" href="https://github.com/hinesboy/mavonEditor" target="_blank">MavonEditor</el-link>-->
<!--</p>-->
<template>
<mavon-editor
ref="md"
v-model="content"
:style="`height: + ${height};overflow-y: hidden`"
:ishljs="true"
@imgAdd="imgAdd"
@change="onChange"
@save="onSave"
/>
</template>
<script>
import { mapGetters } from 'vuex'
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
import { UploadFile } from '@/utils/CsDomUtil'
const dynamicHeight = 160
export default {
name: 'CuteMarkdown',
components: {
mavonEditor
},
props: {
content: {
type: String,
required: true,
default: ''
}
},
data() {
return {
height: document.documentElement.clientHeight - dynamicHeight + 'px'
}
},
computed: {
...mapGetters([
'imagesUploadApi',
'baseApi'
])
},
mounted() {
const that = this
window.onresize = () => {
that.height = document.documentElement.clientHeight - dynamicHeight + 'px'
}
},
methods: {
imgAdd(pos, $file) {
const that = this
UploadFile(that.imagesUploadApi, $file).then(res => {
const data = res.data
const url = that.baseApi + '/file/' + data.type + '/' + data.realName
that.$refs.md.$img2Url(pos, url)
})
},
onChange(value, render) {
this.$emit('change', value, render)
},
onSave(value, render) {
this.$emit('save', value, render)
},
resetField() {
this.content = ''
}
}
}
</script>
<style scoped>
</style>
<!--<p class="warn-content">-->
<!--Markdown 基于-->
<!--<el-link type="primary" href="" target="_blank">MavonEditor</el-link>-->
<!--</p>-->
<!--
* Markdown编辑器
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
* @description 基于MavonEditor https://github.com/hinesboy/mavonEditor
-->
<template>
<mavon-editor
ref="md"
v-model="content"
:style="`height: + ${height};overflow-y: hidden`"
:ishljs="true"
@imgAdd="imgAdd"
@change="onChange"
@save="onSave"
/>
</template>
<script>
import { mapGetters } from 'vuex'
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
import { UploadFile } from '@/utils/CsDomUtil'
const dynamicHeight = 160
export default {
name: 'CuteMarkdown',
components: {
mavonEditor
},
props: {
content: {
type: String,
required: true,
default: ''
}
},
data() {
return {
height: document.documentElement.clientHeight - dynamicHeight + 'px'
}
},
computed: {
...mapGetters([
'imagesUploadApi',
'baseApi'
])
},
mounted() {
const that = this
window.onresize = () => {
that.height = document.documentElement.clientHeight - dynamicHeight + 'px'
}
},
methods: {
imgAdd(pos, $file) {
const that = this
UploadFile(that.imagesUploadApi, $file).then(res => {
const data = res.data
const url = that.baseApi + '/file/' + data.type + '/' + data.realName
that.$refs.md.$img2Url(pos, url)
})
},
onChange(value, render) {
this.$emit('change', value, render)
},
onSave(value, render) {
this.$emit('save', value, render)
},
resetField() {
this.content = ''
}
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,24 @@
<!--
* 无数据
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-20
-->
<template>
<el-empty :description="desc" />
</template>
<script>
export default {
name: 'CuteNoData',
props: {
desc: {
type: String,
required: true,
default: '暂无匹配数据'
}
}
}
</script>
@@ -1,44 +1,49 @@
<!-- 一键复制组件 -->
<!-- 使用方式: <one-key-copy style="margin-left: 10pt" :content.sync="data" /> -->
<template>
<el-button
v-clipboard:copy="content"
v-clipboard:success="onCopy"
v-clipboard:error="onError"
size="mini"
icon="el-icon-document-copy"
/>
</template>
<script>
export default {
name: 'CuteOneKeyCopy',
props: {
content: {
type: String,
required: true,
default: ''
}
},
methods: {
onCopy(row) {
const h = this.$createElement
this.$notify({
title: row.text + ' 复制成功',
message: h('i', { style: 'color: teal' }, '已复制到剪贴板,请直接去 Ctrl+V 粘贴吧!')
})
},
// 复制失败
onError(row) {
const h = this.$createElement
this.$notify({
title: row.text + ' 复制失败',
message: h('i', { style: 'color: teal' }, '联系技术吧!')
})
}
}
}
</script>
<style scoped>
</style>
<!--
* 一键复制组件
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
* @description 使用方式:<cute-one-key-copy style="margin-left: 10pt" :content.sync="data" />
-->
<template>
<el-button
v-clipboard:copy="content"
v-clipboard:success="onCopy"
v-clipboard:error="onError"
size="mini"
icon="el-icon-document-copy"
/>
</template>
<script>
export default {
name: 'CuteOneKeyCopy',
props: {
content: {
type: String,
required: true,
default: ''
}
},
methods: {
onCopy(row) {
const h = this.$createElement
this.$notify({
title: row.text + ' 复制成功',
message: h('i', { style: 'color: teal' }, '已复制到剪贴板,请直接去 Ctrl+V 粘贴吧!')
})
},
// 复制失败
onError(row) {
const h = this.$createElement
this.$notify({
title: row.text + ' 复制失败',
message: h('i', { style: 'color: teal' }, '联系技术吧!')
})
}
}
}
</script>
<style scoped>
</style>
@@ -1,355 +0,0 @@
<template>
<div
class="box-card"
:style="cardStyle"
>
<div>
<div style="float: left;">
<i :class="statusIconClass" :style="statusIconStyle" />
</div>
<div class="box-name">{{ currentTemplateData.name }}</div>
<div style="clear: both" />
</div>
<div class="box-current-node">
<a @click="onCurrentNodeClick">
{{ currentNodeDisplayText }}
</a>
</div>
<el-row>
<el-col :span="12" style="text-align: left">
<!-- 流水线节点状态为fail,且节点支持重试 -->
<div v-if="showRetryButton">
<div class="box-buttons">
<el-button
v-prevent-re-click="5000"
type="text"
size="medium"
:style="getButtonStyle()"
@click="onNodeRetryClick"
>
重试
</el-button>
</div>
</div>
<!-- 流水线节点状态为success,且有操作按钮 -->
<div v-else-if="showOperationButtons">
<div class="box-buttons">
<el-button
v-for="buttonItem in currentTemplateData.buttons"
:key="buttonItem.service"
size="medium"
type="text"
:style="getButtonStyle(buttonItem)"
@click="onNodeOperationClick(buttonItem)"
>
{{ buttonItem.title }}
</el-button>
</div>
</div>
<!-- 流水线节点状态为running,且有操作按钮 -->
<div v-else-if="showOperationButtons">
<div class="box-buttons">
<el-button
v-for="buttonItem in currentTemplateData.buttons"
:key="buttonItem.service"
size="medium"
type="text"
:style="getButtonStyle(buttonItem)"
@click="onNodeOperationClick(buttonItem)"
>
{{ buttonItem.title }}
</el-button>
</div>
</div>
<!-- 占位用,为了前端样式美观些 -->
<div v-else>
<div style="width: 110px;height: 30px" />
</div>
</el-col>
<el-col :span="12">
<div class="box-execute-time">
{{ executeTimeStr }}
</div>
</el-col>
<div style="clear: both" />
</el-row>
</div>
</template>
<script>
import dayjs from 'dayjs'
import { mapGetters } from 'vuex'
import CsMessage from '@/utils/elementui/CsMessage'
export default {
name: 'CutePipelineNode',
props: {
templateData: {
type: Object,
required: true,
default: function() {
return {}
}
},
instanceData: {
type: Object,
required: false,
default: function() {
return {}
}
}
},
data() {
return {
currentTemplateData: {},
currentInstanceData: {}
}
},
// 动态计算
computed: {
...mapGetters([
'statusConst'
]),
cardStyle() {
const statusKey = this.currentTemplateData.currentNodeStatus || this.statusConst.pending.code
const statusConfig = this.statusConst[statusKey] || this.statusConst.pending
return {
borderTop: `5px solid ${statusConfig.color}`
}
},
statusIconClass() {
const status = this.currentTemplateData.currentNodeStatus || this.statusConst.pending.code
return this.statusConst[status].icon || 'el-icon-time'
},
statusIconStyle() {
return {
fontSize: '22px',
color: this.getStatusColor()
}
},
currentNodeDisplayText() {
return this.currentTemplateData.currentNodeMsg || this.statusConst.pending.label
},
showRetryButton() {
return this.currentTemplateData.currentNodeStatus === this.statusConst.fail.code &&
this.currentTemplateData.retry === true
},
showOperationButtons() {
const status = this.currentTemplateData.currentNodeStatus
const hasButtons = this.currentTemplateData.buttons && this.currentTemplateData.buttons.length > 0
return hasButtons && (
status === this.statusConst.success.code ||
status === this.statusConst.running.code
)
},
executeTimeStr() {
return this.renderDateTimeStr(this.currentTemplateData)
}
},
watch: {
templateData: {
handler(newVal, oldVal) {
this.currentTemplateData = { ...newVal }
},
deep: true
},
instanceData: {
handler(newVal, oldVal) {
this.currentInstanceData = { ...newVal }
},
deep: true
}
},
mounted() {
// 初始化时,设置流水线模板
this.currentTemplateData = this.templateData
this.currentInstanceData = this.instanceData
},
methods: {
getStatusColor() {
const status = this.currentTemplateData.currentNodeStatus || this.statusConst.pending.code
const statusConfig = this.statusConst[status] || this.statusConst.pending
return statusConfig.color
},
getButtonStyle(buttonItem) {
if (buttonItem) {
const isPositiveAction = ['apply', 'agree', 'ok', 'success'].includes(buttonItem.code)
const status = this.currentTemplateData.currentNodeStatus
const color = isPositiveAction
? this.statusConst.running.color
: this.statusConst.fail.color
let display = ''
if (status === this.statusConst.running.code) {
display = this.getRunningNodeButtonVisibleStatus(buttonItem)
} else if (status === this.statusConst.success.code) {
display = this.getSuccessNodeButtonVisibleStatus(buttonItem)
}
return {
padding: 0,
margin: '0 10px 0 10px',
color,
display
}
}
return {
padding: 0,
margin: '0 10px 0 10px',
color: this.statusConst.running.color
}
},
/**
* 运行中节点的按钮显示
* @param buttonItem
* @returns {string}
*/
getRunningNodeButtonVisibleStatus(buttonItem) {
if (buttonItem.type === 'qrCodeDialog') {
return 'none'
}
return ''
},
/**
* 成功节点的按钮显示
* @param buttonItem
* @returns {string}
*/
getSuccessNodeButtonVisibleStatus(buttonItem) {
const that = this
// 根据按钮编码
if (buttonItem.code === that.currentTemplateData.currentNodeStatus) {
return ''
}
return 'none'
},
renderDateTimeStr(newVal) {
let executeTimeStr = ''
switch (newVal.status) {
case 'pending':
executeTimeStr = ''
break
case 'running':
case 'success':
case 'fail':
if (newVal.currentNodeStatus !== this.statusConst.pending.code) {
if (newVal.startTime && newVal.finishTime) {
executeTimeStr = this.formatTimeDifference(newVal.startTime, newVal.finishTime)
return executeTimeStr
}
if (newVal.startTime) {
executeTimeStr = this.formatTimeDifference(newVal.startTime, new Date())
return executeTimeStr
}
if (newVal.createTime) {
executeTimeStr = this.formatTimeDifference(newVal.createTime, new Date())
return executeTimeStr
}
}
break
default:
executeTimeStr = ''
break
}
return executeTimeStr
},
formatTimeDifference(createTimeStr, updateTimeStr) {
const startTime = dayjs(createTimeStr)
const endTime = dayjs(updateTimeStr)
// 计算两个时间点之间的差异(毫秒)
const diffInMilliseconds = endTime.diff(startTime)
// 将毫秒转换为秒
let totalSeconds = Math.floor(diffInMilliseconds / 1000)
const days = Math.floor(totalSeconds / (3600 * 24))
totalSeconds %= 3600 * 24
const hours = Math.floor(totalSeconds / 3600)
totalSeconds %= 3600
const minutes = Math.floor(totalSeconds / 60)
const seconds = totalSeconds % 60
// 格式化为字符串
if (days > 0) {
return `${days} 天 ${hours} 时 ${minutes} 分 ${seconds} 秒`
}
if (hours > 0) {
return `${hours} 时 ${minutes} 分 ${seconds} 秒`
}
if (minutes > 0) {
return `${minutes} 分 ${seconds} 秒`
}
if (seconds >= 0) {
return `${seconds} 秒`
}
return `${diffInMilliseconds} 毫秒`
},
onCurrentNodeClick() {
const currentTemplateData = this.currentTemplateData || { click: false }
if (!currentTemplateData.click) {
CsMessage.Warning('当前流水线节点不支持查看明细')
return
}
this.$emit('node-click', this.templateData)
},
onNodeRetryClick() {
const currentTemplateData = this.currentTemplateData || { retry: false }
if (!currentTemplateData.retry) {
CsMessage.Warning('当前流水线节点不支持重试')
return
}
const data = {
...this.currentInstanceData,
code: this.currentTemplateData.code
}
this.$emit('node-retry', data)
},
onNodeOperationClick(buttonInfo) {
const currentTemplateData = this.currentTemplateData || {}
this.$emit('node-opera', currentTemplateData, buttonInfo)
}
}
}
</script>
<style scoped>
.box-card {
width: 300px; /* 卡片宽度 */
max-width: 300px; /* 卡片宽度 */
padding: 10px;
background-color: #fff; /* 背景颜色 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 阴影效果 */
border-radius: 8px; /* 圆角效果 */
overflow: hidden; /* 防止子元素溢出 */
transition: transform 0.3s ease, box-shadow 0.3s ease; /* 过渡效果 */
display: inline-block;
margin-right: 30px;
}
.box-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.box-name {
float: left;
margin-left: 5px;
font-size: 16px;
line-height: 22px
}
.box-current-node {
padding: 20px;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
font-size: 15px;
text-align: center;
}
.box-buttons {
padding: 5px;
overflow: hidden;
text-align: center;
height: 30px;
}
.box-execute-time {
text-align: right;
font-size: 12px;
height: 30px;
line-height: 30px;
}
</style>
@@ -1,65 +1,77 @@
<template>
<el-drawer
:title="title"
:visible.sync="visible"
:size="width"
:before-close="beforeClose"
:append-to-body="true"
:close-on-press-escape="false"
:destroy-on-close="true"
:show-close="false"
:modal="false"
>
<!-- 这里是插槽,用于渲染传入的组件 -->
<slot />
</el-drawer>
</template>
<script>
export default {
name: 'CutePreviewDrawer',
props: {
title: {
type: String,
required: true,
default: '默认标题'
},
width: {
type: String,
required: false,
default: '40%'
}
},
data() {
return {
visible: false
}
},
methods: {
show() {
this.visible = true
},
hidden() {
this.visible = false
},
beforeClose() {
this.hidden()
}
}
}
</script>
<style scoped>
.container-form {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
height: 85%;
padding-right: 20px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
.container-form::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera*/
}
</style>
<!--
* 预览抽屉:封装了常用的方法和属性
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
-->
<template>
<el-drawer
:title="title"
:visible.sync="visible"
:size="width"
:before-close="beforeClose"
:append-to-body="true"
:close-on-press-escape="false"
:wrapper-closable="wrapperClosable"
:destroy-on-close="true"
:show-close="true"
:modal="false"
>
<!-- 这里是插槽,用于渲染传入的组件 -->
<slot />
</el-drawer>
</template>
<script>
export default {
name: 'CutePreviewDrawer',
props: {
title: {
type: String,
required: true,
default: '默认标题'
},
width: {
type: String,
required: false,
default: '40%'
},
wrapperClosable: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
visible: false
}
},
methods: {
show() {
this.visible = true
},
hidden() {
this.visible = false
},
beforeClose() {
this.hidden()
}
}
}
</script>
<style scoped>
.container-form {
overflow-y: scroll; /* 启用滚动 */
scrollbar-width: none; /* Firefox */
height: 85%;
padding-right: 20px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
.container-form::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera*/
}
</style>
@@ -1,298 +1,302 @@
<!--富文本基于-->
<!--<el-link type="primary" href="https://www.wangeditor.com/v5/getting-started.html" target="_blank">wangEditor</el-link>-->
<template>
<div ref="editor" style="border: 1px solid #ccc;">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editor"
:default-config="toolbarConfig"
:mode="editMode"
/>
<Editor
v-model="editValue"
:style="`height: 500px;overflow-y: hidden`"
:default-config="editorConfig"
:mode="editMode"
@onCreated="onCreated"
@onChange="onChange"
@onDestroyed="onDestroyed"
@onFocus="onFocus"
@onBlur="onBlur"
@customAlert="customAlert"
@customPaste="customPaste"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { UploadFile } from '@/utils/CsDomUtil'
import CsMessage from '@/utils/elementui/CsMessage'
export default {
name: 'CuteRichTextEditor',
components: { Toolbar, Editor },
props: {
readOnly: {
type: Boolean,
required: false,
default: false
},
content: {
type: String,
required: false,
default: ''
}
},
data() {
const that = this
return {
toolbarConfig: {},
editorConfig: {
placeholder: '请输入内容...',
readOnly: that.readOnly,
autoFocus: true,
scroll: false,
// 会造成卡顿
// maxLength: this.maxLength
MENU_CONF: {
'uploadImage': {
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['image/*'],
// 自定义上传
async customUpload(file, insertFn) { // JS 语法
UploadFile(that.imagesUploadApi, file).then(res => {
const data = res.data
const url = that.baseApi + '/file/' + data.dateGroup + '/' + data.realName
// 最后插入图片
insertFn(url, '', '')
})
}
}
}
},
// 'default' or 'simple'
editMode: 'simple',
editor: null,
editValue: ''
}
},
computed: {
...mapGetters([
'imagesUploadApi',
'baseApi'
])
},
methods: {
/**
* 编辑器创建完毕时的回调函数
* @param editor
*/
onCreated(editor) {
// 一定要用 Object.seal(), 否则会报错
this.editor = Object.seal(editor)
this.editValue = this.content
},
/**
* 编辑器内容、选区变化时的回调函数
* @param editor
*/
onChange(editor) {
if (editor == null) {
return
}
// 非格式化的 html
const html = editor.getHtml()
// 纯文本内容
const text = editor.getText()
this.$emit('change', html, text)
},
/**
* 编辑器销毁时的回调函数。调用 editor.destroy() 即可销毁编辑器
* @param editor
*/
onDestroyed(editor) {
if (editor == null) {
return
}
editor.destroy()
},
/**
* 编辑器内容达到最大长度时的回调函数(会导致卡顿)
* @param editor
*/
onMaxLength(editor) {
if (editor == null) {
return
}
},
/**
* 编辑器 focus 时的回调函数
* @param editor
*/
onFocus(editor) {
if (editor == null) {
return
}
this.$emit('focus', editor)
},
/**
* 编辑器 blur 时的回调函数
* @param editor
*/
onBlur(editor) {
if (editor == null) {
return
}
this.$emit('blur', editor)
},
/**
* 自定义编辑器 alert 。如想用 ElementUI 的 Message 功能
* @param editor
*/
customAlert(info, type) {
switch (type) {
case 'success':
CsMessage.Error(info)
break
case 'info':
CsMessage.Info(info)
break
case 'warning':
CsMessage.Warning(info)
break
case 'error':
CsMessage.Error(info)
break
default:
CsMessage.Info(info)
break
}
},
/**
* 自定义粘贴。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑
* @param editor
*/
customPaste(editor, event, callback) {
if (editor == null) {
callback(true)
}
// console.log('ClipboardEvent 粘贴事件对象', event)
// // const html = event.clipboardData.getData('text/html') // 获取粘贴的 html
// // const text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本
// // const rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
//
// // 自定义插入内容
// editor.insertText('xxx')
//
// // 返回 false ,阻止默认粘贴行为
// event.preventDefault()
// callback(false) // 返回值(注意,vue 事件的返回值,不能用 return)
//
// // 返回 true ,继续默认的粘贴行为
// // callback(true)
callback(true)
},
/**
* 设置编辑器html内容
* @param html
* <br/>
* 如果是 editor.getHtml() 获取的 HTML 格式,可以完美解析。
* 如果是其他的 HTML 格式,则不能保证语义正确 —— dangerously 。
*/
setHtml(html) {
if (this.editor == null) {
return
}
this.editor.dangerouslyInsertHtml(html)
},
/**
* 获取编辑器html内容
*/
getHtml() {
if (this.editor == null) {
return ''
}
this.editor.getHtml()
},
/**
* 清空编辑器内容
*/
clear() {
if (this.editor == null) {
return
}
this.editor.clear()
},
/**
* 撤销
*/
undo() {
if (this.editor == null) {
return
}
this.editor.undo()
},
/**
* 重做
*/
redo() {
if (this.editor == null) {
return
}
this.editor.redo()
},
/**
* 获取选中的文本
*/
getSelectionText() {
if (this.editor == null) {
return ''
}
return this.editor.getSelectionText()
},
/**
* 删除选中的内容
*/
deleteFragment() {
if (this.editor == null) {
return
}
this.editor.deleteFragment()
},
/**
* 获取选中的内容。JSON格式
*/
getFragment() {
if (this.editor == null) {
return {}
}
return this.editor.getFragment()
},
/**
* 获取文本内容
*/
getText() {
if (this.editor == null) {
return ''
}
return this.editor.getText()
},
/**
* 在选区插入文本
* @param text
*/
insertText(text) {
if (this.editor == null) {
return
}
this.editor.insertText(text)
},
resetField() {
this.setHtml('')
}
}
}
</script>
<!--
* 富文本编辑器
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
-->
<template>
<div ref="editor" style="border: 1px solid #ccc;">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editor"
:default-config="toolbarConfig"
:mode="editMode"
/>
<Editor
v-model="editValue"
:style="`height: 500px;overflow-y: hidden`"
:default-config="editorConfig"
:mode="editMode"
@onCreated="onCreated"
@onChange="onChange"
@onDestroyed="onDestroyed"
@onFocus="onFocus"
@onBlur="onBlur"
@customAlert="customAlert"
@customPaste="customPaste"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { UploadFile } from '@/utils/CsDomUtil'
import CsMessage from '@/utils/elementui/CsMessage'
export default {
name: 'CuteRichTextEditor',
components: { Toolbar, Editor },
props: {
readOnly: {
type: Boolean,
required: false,
default: false
},
content: {
type: String,
required: false,
default: ''
}
},
data() {
const that = this
return {
toolbarConfig: {},
editorConfig: {
placeholder: '请输入内容...',
readOnly: that.readOnly,
autoFocus: true,
scroll: false,
// 会造成卡顿
// maxLength: this.maxLength
MENU_CONF: {
'uploadImage': {
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['image/*'],
// 自定义上传
async customUpload(file, insertFn) { // JS 语法
UploadFile(that.imagesUploadApi, file).then(res => {
const data = res.data
const url = that.baseApi + '/file/' + data.dateGroup + '/' + data.realName
// 最后插入图片
insertFn(url, '', '')
})
}
}
}
},
// 'default' or 'simple'
editMode: 'simple',
editor: null,
editValue: ''
}
},
computed: {
...mapGetters([
'imagesUploadApi',
'baseApi'
])
},
methods: {
/**
* 编辑器创建完毕时的回调函数
* @param editor
*/
onCreated(editor) {
// 一定要用 Object.seal(), 否则会报错
this.editor = Object.seal(editor)
this.editValue = this.content
},
/**
* 编辑器内容、选区变化时的回调函数
* @param editor
*/
onChange(editor) {
if (editor == null) {
return
}
// 非格式化的 html
const html = editor.getHtml()
// 纯文本内容
const text = editor.getText()
this.$emit('change', html, text)
},
/**
* 编辑器销毁时的回调函数。调用 editor.destroy() 即可销毁编辑器
* @param editor
*/
onDestroyed(editor) {
if (editor == null) {
return
}
editor.destroy()
},
/**
* 编辑器内容达到最大长度时的回调函数(会导致卡顿)
* @param editor
*/
onMaxLength(editor) {
if (editor == null) {
return
}
},
/**
* 编辑器 focus 时的回调函数
* @param editor
*/
onFocus(editor) {
if (editor == null) {
return
}
this.$emit('focus', editor)
},
/**
* 编辑器 blur 时的回调函数
* @param editor
*/
onBlur(editor) {
if (editor == null) {
return
}
this.$emit('blur', editor)
},
/**
* 自定义编辑器 alert 。如想用 ElementUI 的 Message 功能
* @param editor
*/
customAlert(info, type) {
switch (type) {
case 'success':
CsMessage.Error(info)
break
case 'info':
CsMessage.Info(info)
break
case 'warning':
CsMessage.Warning(info)
break
case 'error':
CsMessage.Error(info)
break
default:
CsMessage.Info(info)
break
}
},
/**
* 自定义粘贴。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑
* @param editor
*/
customPaste(editor, event, callback) {
if (editor == null) {
callback(true)
}
// console.log('ClipboardEvent 粘贴事件对象', event)
// // const html = event.clipboardData.getData('text/html') // 获取粘贴的 html
// // const text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本
// // const rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
//
// // 自定义插入内容
// editor.insertText('xxx')
//
// // 返回 false ,阻止默认粘贴行为
// event.preventDefault()
// callback(false) // 返回值(注意,vue 事件的返回值,不能用 return)
//
// // 返回 true ,继续默认的粘贴行为
// // callback(true)
callback(true)
},
/**
* 设置编辑器html内容
* @param html
* <br/>
* 如果是 editor.getHtml() 获取的 HTML 格式,可以完美解析。
* 如果是其他的 HTML 格式,则不能保证语义正确 —— dangerously 。
*/
setHtml(html) {
if (this.editor == null) {
return
}
this.editor.dangerouslyInsertHtml(html)
},
/**
* 获取编辑器html内容
*/
getHtml() {
if (this.editor == null) {
return ''
}
this.editor.getHtml()
},
/**
* 清空编辑器内容
*/
clear() {
if (this.editor == null) {
return
}
this.editor.clear()
},
/**
* 撤销
*/
undo() {
if (this.editor == null) {
return
}
this.editor.undo()
},
/**
* 重做
*/
redo() {
if (this.editor == null) {
return
}
this.editor.redo()
},
/**
* 获取选中的文本
*/
getSelectionText() {
if (this.editor == null) {
return ''
}
return this.editor.getSelectionText()
},
/**
* 删除选中的内容
*/
deleteFragment() {
if (this.editor == null) {
return
}
this.editor.deleteFragment()
},
/**
* 获取选中的内容。JSON格式
*/
getFragment() {
if (this.editor == null) {
return {}
}
return this.editor.getFragment()
},
/**
* 获取文本内容
*/
getText() {
if (this.editor == null) {
return ''
}
return this.editor.getText()
},
/**
* 在选区插入文本
* @param text
*/
insertText(text) {
if (this.editor == null) {
return
}
this.editor.insertText(text)
},
resetField() {
this.setHtml('')
}
}
}
</script>
@@ -0,0 +1,59 @@
<!--
* 截屏工具
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-20
-->
<template>
<div>
<div style="text-align: right">
<el-button
type="primary"
icon="el-icon-camera"
:loading="capturing"
size="small"
@click="capture"
>
截图并下载
</el-button>
</div>
<div id="elementIdSelector">
<slot />
</div>
</div>
</template>
<script>
import CsMessage from '@/utils/elementui/CsMessage'
import { FormatDateTimeShortStr } from '@/utils/CsUtil'
import html2canvas from 'html2canvas'
export default {
name: 'CuteScreenShot',
data() {
return {
capturing: false
}
},
methods: {
async capture() {
this.capturing = true
const element = document.querySelector('#elementIdSelector')
if (!element) {
CsMessage.Error('未找到匹配的元素,请检查选择器是否正确')
this.capturing = false
return
}
const canvas = await html2canvas(element)
// const base64 = canvas.toDataURL('image/png')
const link = document.createElement('a')
link.download = `screenshot_${FormatDateTimeShortStr(new Date())}.png`
link.href = canvas.toDataURL('image/png')
link.click()
this.capturing = false
}
}
}
</script>
@@ -0,0 +1,57 @@
<!--
* 查询组件
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-20
-->
<template>
<el-form
ref="searchForm"
:inline="true"
:model.sync="model"
label-position="right"
label-width="90px"
>
<slot name="suffix" />
<el-form-item>
<el-button type="primary" @click="onFormSubmit('searchForm')">查询</el-button>
<el-button @click="onFormReset('searchForm')">重置</el-button>
</el-form-item>
<slot name="operation" />
</el-form>
</template>
<script>
export default {
name: 'CuteSearchForm',
components: {},
props: {
model: {
type: Object,
required: true
}
},
methods: {
onFormSubmit(formName) {
const that = this
that.$refs[formName].validate((valid) => {
if (valid) {
that.$emit('submit', that.model)
} else {
return false
}
})
},
onFormReset(formName) {
try {
this.$refs[formName].resetFields()
this.$emit('reset')
} catch (e) {
console.error(e)
}
}
}
}
</script>
@@ -0,0 +1,33 @@
<!--
* 段落标题组件:占一行,支持嵌套按钮
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-13
-->
<template>
<el-row>
<el-col :span="12">
<h4>
<i class="el-icon-s-help" style="color: #409EFF;font-size: 20px" />
<i style="color: #303133;">{{ title }}</i>
</h4>
</el-col>
<el-col :span="12" style="text-align: right;line-height: 64px;height: 64px;padding-right: 30px">
<slot />
</el-col>
</el-row>
</template>
<script>
export default {
name: 'CuteSectionTitle',
props: {
title: {
type: String,
required: true,
default: '默认标题'
}
}
}
</script>
@@ -1,135 +1,149 @@
<template>
<div>
<el-table
ref="table"
v-loading="crud.loading"
stripe
empty-text="暂无数据"
:row-key="crud.primaryKey"
fit
:data="crud.dataSource"
style="width: 100%;"
:height="height"
:max-height="height"
@selection-change="onTableSelectionChange"
>
<el-table-column v-if="mode === 'multi'" type="selection" width="55" />
<slot />
</el-table>
<el-pagination
:current-page="pageProps.current"
:page-sizes="[10, 20, 50]"
:page-size="pageProps.pageSize"
layout="total, sizes, prev, pager, next"
:total="pageProps.total"
@size-change="(size) => crud.onPageChange(pageProps.current, size, pageProps.total)"
@current-change="(current) => crud.onPageChange(current, pageProps.pageSize, pageProps.total)"
/>
</div>
</template>
<script>
export default {
name: 'CuteSimpleTable',
props: {
primaryKey: {
type: String,
required: true,
default: null
},
fetch: {
type: Function,
required: false,
default: null
},
paramsTransform: {
type: Function,
required: false,
default: null
},
responseTransform: {
type: Function,
required: false,
default: null
},
mode: {
type: String,
required: false,
default: 'none'
},
pageProps: {
type: Object,
required: true,
default: function() {
return {
current: 1,
pageSize: 10,
total: 0
}
}
}
},
data() {
return {
crud: {
loading: false,
primaryKey: this.primaryKey ? this.primaryKey : 'id',
fetchUrl: null,
dataSource: [],
onPageChange: (currentPage, pageSize) => {
this.pageProps.current = currentPage
this.pageProps.pageSize = pageSize
console.log('on-page-change', currentPage, pageSize)
// this.$emit('onPageChange', currentPage, pageSize, total)
this.refresh()
}
}
}
},
computed: {
height() {
return document.documentElement.clientHeight - 300
}
},
mounted() {
this.initData()
},
methods: {
refresh() {
this.initData()
},
async initData() {
let params = {}
// 自定义请求参数
if (this.paramsTransform) {
params = this.paramsTransform(this.pageProps)
}
if (this.fetch) {
try {
this.crud.loading = true
// 请求远程数据
const response = await this.fetch(params)
// console.error('response', response)
// { content: [], totalElements: 0 }
// 转换为表格所需要的数据
if (this.responseTransform) {
return this.responseTransform(response)
}
if (response && response.hasOwnProperty('totalElements') && response.hasOwnProperty('content')) {
this.pageProps.total = response.totalElements
this.crud.dataSource = response.content
}
return response
} finally {
this.crud.loading = false
}
}
},
onTableSelectionChange(selection) {
this.$emit('selection-change', selection)
}
}
}
</script>
<!--
* 简单表格组件:封装自定义逻辑与参数
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
-->
<template>
<div>
<el-table
ref="table"
v-loading="crud.loading"
stripe
empty-text="暂无数据"
:row-key="crud.primaryKey"
fit
:data="crud.dataSource"
style="width: 100%;"
:height="height"
:max-height="height"
@selection-change="onTableSelectionChange"
>
<el-table-column v-if="mode === 'multi'" type="selection" width="55" />
<slot />
</el-table>
<el-pagination
:current-page="pageProps.current"
:page-sizes="[10, 20, 50]"
:page-size="pageProps.pageSize"
layout="total, sizes, prev, pager, next"
:total="pageProps.total"
@size-change="(size) => crud.onPageChange(pageProps.current, size, pageProps.total)"
@current-change="(current) => crud.onPageChange(current, pageProps.pageSize, pageProps.total)"
/>
</div>
</template>
<script>
export default {
name: 'CuteSimpleTable',
props: {
primaryKey: {
type: String,
required: true,
default: null
},
fetch: {
type: Function,
required: false,
default: null
},
paramsTransform: {
type: Function,
required: false,
default: null
},
responseTransform: {
type: Function,
required: false,
default: null
},
mode: {
type: String,
required: false,
default: 'none'
},
pageProps: {
type: Object,
required: true,
default: function() {
return {
current: 1,
pageSize: 10,
total: 0
}
}
},
customHeight: {
type: Number,
required: false,
default: null
}
},
data() {
return {
crud: {
loading: false,
primaryKey: this.primaryKey ? this.primaryKey : 'id',
fetchUrl: null,
dataSource: [],
onPageChange: (currentPage, pageSize) => {
this.pageProps.current = currentPage
this.pageProps.pageSize = pageSize
console.log('on-page-change', currentPage, pageSize)
// this.$emit('onPageChange', currentPage, pageSize, total)
this.refresh()
}
}
}
},
computed: {
height() {
if (this.customHeight) {
return this.customHeight
}
return document.documentElement.clientHeight - 300
}
},
mounted() {
this.initData()
},
methods: {
refresh() {
this.initData()
},
async initData() {
let params = {}
// 自定义请求参数
if (this.paramsTransform) {
params = this.paramsTransform(this.pageProps)
}
if (this.fetch) {
try {
this.crud.loading = true
// 请求远程数据
const response = await this.fetch(params)
// console.error('response', response)
// { content: [], totalElements: 0 }
// 转换为表格所需要的数据
if (this.responseTransform) {
return this.responseTransform(response)
}
if (response && response.hasOwnProperty('totalElements') && response.hasOwnProperty('content')) {
this.pageProps.total = response.totalElements
this.crud.dataSource = response.content
}
return response
} finally {
this.crud.loading = false
}
}
},
onTableSelectionChange(selection) {
this.$emit('selection-change', selection)
}
}
}
</script>
@@ -0,0 +1,47 @@
<!--
* 纵向滚动条容器
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-20
-->
<template>
<div class="overflow-panel">
<slot />
</div>
</template>
<script>
export default {
name: 'CuteVerticalScrollBar'
}
</script>
<style scoped>
.overflow-panel {
height: 800px;
overflow-y: scroll;
}
/* 滚动条宽度 */
.overflow-panel::-webkit-scrollbar {
width: 5px;
}
/* 滚动条轨道 */
.overflow-panel::-webkit-scrollbar-track {
background: #f1f1f1;
}
/* 滚动条滑块 */
.overflow-panel::-webkit-scrollbar-thumb {
background: #409EFF;
border-radius: 6px;
}
/* 鼠标悬停在滑块上时 */
.overflow-panel::-webkit-scrollbar-thumb:hover {
background: #F56C6C;
}
</style>
@@ -1,92 +1,94 @@
<!--Yaml编辑器 基于-->
<!--<a href="https://github.com/codemirror/CodeMirror" target="_blank">CodeMirror</a>,-->
<!--主题预览地址 <a href="https://codemirror.net/demo/theme.html#idea" target="_blank">Theme</a>-->
<!--</p>-->
<template>
<textarea ref="textarea" />
</template>
<script>
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// 替换主题这里需修改名称
// 主题预览地址 https://blog.csdn.net/qq_41694291/article/details/106429772
import 'codemirror/theme/darcula.css'
import 'codemirror/mode/yaml/yaml'
export default {
name: 'CuteYamlEditor',
props: {
content: {
type: String,
required: true,
default: ''
},
height: {
type: String,
required: false,
default: '500px'
},
readOnly: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
editor: false
}
},
watch: {
content(newVal, oldVal) {
const editorValue = this.editor.getValue()
if (newVal !== editorValue) {
this.editor.setValue(newVal)
}
},
height(newVal, oldVal) {
this.editor.setSize('auto', newVal)
}
},
mounted() {
this.editor = CodeMirror.fromTextArea(this.$refs.textarea, {
mode: 'text/x-yaml',
autoRefresh: true,
lineNumbers: true,
lint: true,
lineWrapping: true,
tabSize: 2,
cursorHeight: 1,
// 替换主题这里需修改名称
theme: 'darcula'
})
this.editor.setSize('auto', this.height)
if (this.content) {
this.editor.setValue(this.content)
}
this.editor.setOption('readOnly', this.readOnly)
this.editor.on('change', cm => {
this.$emit('change', cm.getValue())
this.$emit('input', cm.getValue())
})
},
methods: {
getValue() {
if (this.editor == null) {
return ''
}
return this.editor.getValue()
},
setValue(val) {
if (this.editor == null) {
return
}
this.editor.setValue(val)
},
resetField() {
this.editor.setValue('')
}
}
}
</script>
<!--
* YAML编辑器
* @author odboy
* @email tianjun@odboy.cn
* @created 2025-08-01
-->
<template>
<textarea ref="textarea" />
</template>
<script>
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// 替换主题这里需修改名称
// 主题预览地址 https://blog.csdn.net/qq_41694291/article/details/106429772
import 'codemirror/theme/darcula.css'
import 'codemirror/mode/yaml/yaml'
export default {
name: 'CuteYamlEditor',
props: {
content: {
type: String,
required: true,
default: ''
},
height: {
type: String,
required: false,
default: '500px'
},
readOnly: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
editor: false
}
},
watch: {
content(newVal, oldVal) {
const editorValue = this.editor.getValue()
if (newVal !== editorValue) {
this.editor.setValue(newVal)
}
},
height(newVal, oldVal) {
this.editor.setSize('auto', newVal)
}
},
mounted() {
this.editor = CodeMirror.fromTextArea(this.$refs.textarea, {
mode: 'text/x-yaml',
autoRefresh: true,
lineNumbers: true,
lint: true,
lineWrapping: true,
tabSize: 2,
cursorHeight: 1,
// 替换主题这里需修改名称
theme: 'darcula'
})
this.editor.setSize('auto', this.height)
if (this.content) {
this.editor.setValue(this.content)
}
this.editor.setOption('readOnly', this.readOnly)
this.editor.on('change', cm => {
this.$emit('change', cm.getValue())
this.$emit('input', cm.getValue())
})
},
methods: {
getValue() {
if (this.editor == null) {
return ''
}
return this.editor.getValue()
},
setValue(val) {
if (this.editor == null) {
return
}
this.editor.setValue(val)
},
resetField() {
this.editor.setValue('')
}
}
}
</script>