ui库和web端产品库合并版本(还需修复细节)
This commit is contained in:
331
ui/packages/basic/AiArticle.vue
Normal file
331
ui/packages/basic/AiArticle.vue
Normal file
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div class="ai-article">
|
||||
<div v-html="value"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiArticle',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ai-article {
|
||||
width: 100%;
|
||||
line-height: 1.75;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
text-align: justify;
|
||||
overflow-x: auto;
|
||||
word-break: break-word;
|
||||
|
||||
::v-deep h1 {
|
||||
margin: 1.3rem 0;
|
||||
line-height: 1.2
|
||||
}
|
||||
|
||||
::v-deep p {
|
||||
line-height: 2.27rem
|
||||
}
|
||||
|
||||
::v-deep hr {
|
||||
border: none;
|
||||
border-top: 1px solid #ddd;
|
||||
margin-top: 2.7rem;
|
||||
margin-bottom: 2.7rem
|
||||
}
|
||||
|
||||
::v-deep img:not(.equation), ::v-deep iframe, ::v-deep embed, ::v-deep video {
|
||||
display: block;
|
||||
margin: 18px auto;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
::v-deep img.equation {
|
||||
margin: 0 .1em;
|
||||
max-width: 100% !important;
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
::v-deep figure {
|
||||
margin: 2.7rem auto;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
::v-deep img:not(.equation) {
|
||||
cursor: zoom-in
|
||||
}
|
||||
|
||||
::v-deep figure figcaption {
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
line-height: 2.7rem;
|
||||
color: #909090
|
||||
}
|
||||
|
||||
::v-deep pre {
|
||||
line-height: 1.93rem;
|
||||
overflow: auto
|
||||
}
|
||||
|
||||
::v-deep code,
|
||||
::v-deep pre {
|
||||
font-family: Menlo, Monaco, Consolas, Courier New, monospace
|
||||
}
|
||||
|
||||
::v-deep code {
|
||||
font-size: 1rem;
|
||||
padding: .26rem .53em;
|
||||
word-break: break-word;
|
||||
color: #4e5980;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 2px;
|
||||
overflow-x: auto
|
||||
}
|
||||
|
||||
::v-deep pre>code {
|
||||
font-size: 1rem;
|
||||
padding: .67rem 1.3rem;
|
||||
margin: 0;
|
||||
word-break: normal;
|
||||
display: block
|
||||
}
|
||||
|
||||
::v-deep a {
|
||||
color: #259
|
||||
}
|
||||
|
||||
::v-deep a:active,
|
||||
::v-deep a:hover {
|
||||
color: #275b8c
|
||||
}
|
||||
|
||||
::v-deep table {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 18px;
|
||||
overflow: auto;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
border-top: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
|
||||
::v-deep thead {
|
||||
background: #f6f6f6;
|
||||
color: #000;
|
||||
text-align: left
|
||||
}
|
||||
|
||||
::v-deep td,
|
||||
::v-deep th {
|
||||
padding: 3px 5px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
::v-deep td {
|
||||
min-width: 10rem
|
||||
}
|
||||
|
||||
::v-deep blockquote {
|
||||
margin: 1em 0;
|
||||
border-left: 4px solid #ddd;
|
||||
padding: 0 1.3rem
|
||||
}
|
||||
|
||||
::v-deep blockquote>p {
|
||||
margin: .6rem 0
|
||||
}
|
||||
|
||||
::v-deep ol,
|
||||
::v-deep ul {
|
||||
padding-left: 2.7rem
|
||||
}
|
||||
|
||||
::v-deep ol li,
|
||||
::v-deep ul li {
|
||||
margin-bottom: .6rem
|
||||
}
|
||||
|
||||
::v-deep ol ol,
|
||||
::v-deep ol ul,
|
||||
::v-deep ul ol,
|
||||
::v-deep ul ul {
|
||||
margin-top: .27rem
|
||||
}
|
||||
|
||||
::v-deep pre>code {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
color: #333;
|
||||
background: #f8f8f8
|
||||
}
|
||||
|
||||
::v-deep p {
|
||||
line-height: inherit;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 18px
|
||||
}
|
||||
|
||||
::v-deep img {
|
||||
max-height: none
|
||||
}
|
||||
|
||||
::v-deep a {
|
||||
color: #0269c8;
|
||||
border-bottom: 1px solid #d1e9ff
|
||||
}
|
||||
|
||||
::v-deep code {
|
||||
background-color: #fff5f5;
|
||||
color: #ff502c;
|
||||
font-size: .87em;
|
||||
padding: .065em .4em
|
||||
}
|
||||
|
||||
::v-deep blockquote {
|
||||
color: #666;
|
||||
padding: 1px 23px;
|
||||
margin: 18px 0;
|
||||
border-left: 4px solid #cbcbcb;
|
||||
background-color: #f8f8f8
|
||||
}
|
||||
|
||||
::v-deep blockquote:after {
|
||||
display: block;
|
||||
content: ""
|
||||
}
|
||||
|
||||
::v-deep blockquote>p {
|
||||
margin: 10px 0
|
||||
}
|
||||
|
||||
::v-deep blockquote.warning {
|
||||
position: relative;
|
||||
border-left-color: #f75151;
|
||||
margin-left: 8px
|
||||
}
|
||||
|
||||
::v-deep blockquote.warning:before {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: -12px;
|
||||
background: #f75151;
|
||||
border-radius: 50%;
|
||||
content: "!";
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
::v-deep ol,
|
||||
::v-deep ul {
|
||||
padding-left: 28px
|
||||
}
|
||||
|
||||
::v-deep ol li,
|
||||
::v-deep ul li {
|
||||
margin-bottom: 0;
|
||||
list-style: inherit
|
||||
}
|
||||
|
||||
::v-deep ol li.task-list-item,
|
||||
::v-deep ul li.task-list-item {
|
||||
list-style: none
|
||||
}
|
||||
|
||||
::v-deep ol li.task-list-item ol,
|
||||
::v-deep ol li.task-list-item ul,
|
||||
::v-deep ul li.task-list-item ol,
|
||||
::v-deep ul li.task-list-item ul {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
::v-deep ol li {
|
||||
padding-left: 6px
|
||||
}
|
||||
|
||||
::v-deep pre {
|
||||
position: relative;
|
||||
line-height: 1.75
|
||||
}
|
||||
|
||||
::v-deep pre>code {
|
||||
padding: 15px 12px
|
||||
}
|
||||
|
||||
::v-deep pre>code.hljs[lang] {
|
||||
padding: 18px 15px 12px
|
||||
}
|
||||
|
||||
::v-deep h1,
|
||||
::v-deep h2,
|
||||
::v-deep h3,
|
||||
::v-deep h4,
|
||||
::v-deep h5,
|
||||
::v-deep h6 {
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
margin-top: 35px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
::v-deep h1 {
|
||||
font-size: 30px;
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
::v-deep h2 {
|
||||
padding-bottom: 12px;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ececec
|
||||
}
|
||||
|
||||
::v-deep h3 {
|
||||
font-size: 18px;
|
||||
padding-bottom: 0
|
||||
}
|
||||
|
||||
::v-deep h4 {
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
::v-deep h5 {
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
::v-deep h6 {
|
||||
margin-top: 5px
|
||||
}
|
||||
|
||||
::v-deep h1.heading+h2.heading {
|
||||
margin-top: 20px
|
||||
}
|
||||
|
||||
::v-deep h1.heading+h3.heading {
|
||||
margin-top: 15px
|
||||
}
|
||||
|
||||
::v-deep .heading+.heading {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
::v-deep h1+:not(.heading) {
|
||||
margin-top: 25px
|
||||
}
|
||||
}
|
||||
</style>
|
||||
238
ui/packages/basic/AiAudio.vue
Normal file
238
ui/packages/basic/AiAudio.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<section class="ai-audio">
|
||||
<div class="controller" :class="skinClassName">
|
||||
<el-row :gutter="10" type="flex">
|
||||
<el-col>
|
||||
<i class="play-icon" :class="playIcon" @click="play"></i>
|
||||
</el-col>
|
||||
<el-col class="play-progress">
|
||||
<el-slider
|
||||
:format-tooltip="getDateFormat"
|
||||
v-model="currentTime"
|
||||
:max="totalDuration"
|
||||
@change="audio.currentTime = currentTime">
|
||||
</el-slider>
|
||||
<span class="total" v-if="skin === 'flat'">{{ getDateFormat(currentTime) }}</span>
|
||||
<span class="total" v-else>{{ [getDateFormat(currentTime), getDateFormat(totalDuration)].join('/') }}</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<audio
|
||||
ref="audio"
|
||||
style="display: none;"
|
||||
:src="src"
|
||||
preload="metadata"
|
||||
@pause="playState = false"
|
||||
@playing="playing"
|
||||
@loadedmetadata="loadedmetadata"
|
||||
@timeupdate="timeupdate"
|
||||
></audio>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'dayjs'
|
||||
|
||||
export default {
|
||||
name: 'AiAudio',
|
||||
props: {
|
||||
/**
|
||||
* 播放资源url
|
||||
*/
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* 同时只允许一个播放
|
||||
*/
|
||||
singlePlay: {type: Boolean, default: false},
|
||||
/**
|
||||
* 播放器风格
|
||||
* default 旧版样式,flat 新版样式
|
||||
* @values default,flat
|
||||
*/
|
||||
skin: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
audio: null,
|
||||
playState: false,
|
||||
currentTime: 0,
|
||||
totalTime: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
playIcon() {
|
||||
if (this.skin === 'flat') {
|
||||
return this.playState ? 'iconfont iconMediaPlayer_Stop' : 'iconfont iconMediaPlayer_Play'
|
||||
}
|
||||
|
||||
return this.playState ? 'el-icon-video-pause' : 'el-icon-video-play'
|
||||
},
|
||||
|
||||
totalDuration() {
|
||||
return Math.round(this.totalTime)
|
||||
},
|
||||
|
||||
skinClassName() {
|
||||
return this.skin === 'default' ? '' : this.skin
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.audio = this.$refs.audio
|
||||
},
|
||||
|
||||
methods: {
|
||||
play() {
|
||||
if (this.audio) {
|
||||
if (this.audio.paused) {
|
||||
if (this.singlePlay) this.stopAllAudio()
|
||||
this.audio.play()
|
||||
} else {
|
||||
this.audio.pause()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stopAllAudio() {
|
||||
let audios = document.getElementsByTagName('audio')
|
||||
for (let i = 0; i < audios.length; i++) {
|
||||
if (audios[i]) audios[i].pause()
|
||||
}
|
||||
},
|
||||
|
||||
playing() {
|
||||
this.playState = true
|
||||
},
|
||||
|
||||
loadedmetadata() {
|
||||
this.totalTime = this.$refs.audio.duration || 0
|
||||
},
|
||||
|
||||
getDateFormat(val) {
|
||||
const time = moment.duration(val, 'seconds')
|
||||
return [
|
||||
this.prefixNum(time.minutes(), 2),
|
||||
this.prefixNum(time.seconds(), 2),
|
||||
].join(':')
|
||||
},
|
||||
|
||||
prefixNum(val, num) {
|
||||
return (Array(num).join('0') + val).slice(-num)
|
||||
},
|
||||
|
||||
timeupdate() {
|
||||
this.currentTime = parseInt(this.audio.currentTime)
|
||||
}
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.audio.pause()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ai-audio {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
.controller {
|
||||
width: 205px;
|
||||
height: 40px;
|
||||
padding: 0 5px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
border-radius: 99px;
|
||||
|
||||
.el-col {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
color: #4c84ff;
|
||||
cursor: pointer;
|
||||
line-height: 40px;
|
||||
font-size: 32px;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #b3d8ff;
|
||||
}
|
||||
}
|
||||
|
||||
.play-progress {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
.el-slider {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-left: 15px;
|
||||
font-size: 12px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flat {
|
||||
height: 32px;
|
||||
background: rgba(239, 246, 255, 1);
|
||||
border: 1px solid rgba(132, 181, 255, 1);
|
||||
border-radius: 5px;
|
||||
box-shadow: none;
|
||||
|
||||
.play-icon {
|
||||
position: relative;
|
||||
padding-left: 3px;
|
||||
color: #4c84ff;
|
||||
cursor: pointer;
|
||||
line-height: 32px;
|
||||
font-size: 20px;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #b3d8ff;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep.el-slider__button {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #1365DD;
|
||||
}
|
||||
|
||||
::v-deep.el-slider__bar {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
::v-deep.el-slider__button-wrapper {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
::v-deep.el-slider__runway {
|
||||
height: 2px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.play-progress {
|
||||
display: flex;
|
||||
|
||||
.el-slider {
|
||||
width: 84px;
|
||||
}
|
||||
|
||||
.total {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
30
ui/packages/basic/AiBadge.vue
Normal file
30
ui/packages/basic/AiBadge.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<section class="AiBadge">
|
||||
<slot></slot>
|
||||
<sup v-if="isShow" class="badge">
|
||||
<span v-if="badge">{{badge}}</span>
|
||||
<slot v-else name="badge"></slot>
|
||||
</sup>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AiBadge",
|
||||
props: {
|
||||
badge: String,
|
||||
isShow: {type: Boolean, default: true}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.AiBadge {
|
||||
.badge {
|
||||
position: absolute;
|
||||
margin-left: -10px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
117
ui/packages/basic/AiBar.vue
Normal file
117
ui/packages/basic/AiBar.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="aibar" :style="{ marginBottom: marginBottom }" :class="[titlePosition === 'center' ? 'aibar-center' : '']">
|
||||
<div v-if="titlePosition === 'center'"></div>
|
||||
<div class="aibar-left" :class="[titlePosition === 'center' ? 'aibar-left__center' : '']">
|
||||
<template v-if="!isHasTitleSlot">{{ title }}</template>
|
||||
<slot name="title" v-else></slot>
|
||||
</div>
|
||||
<div class="aibar-right">
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiBar',
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
|
||||
customCliker: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
marginBottom: {
|
||||
type: String,
|
||||
default: '16px'
|
||||
},
|
||||
|
||||
titlePosition: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isHasTitleSlot () {
|
||||
return this.$slots.title
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.aibar {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #EEEEEE;
|
||||
|
||||
.aibar-left {
|
||||
color: #222;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.aibar-left__center {
|
||||
position: relative;
|
||||
width: 556px;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.aibar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #5088FF;
|
||||
font-size: 12px;
|
||||
|
||||
i {
|
||||
line-height: 1;
|
||||
color: #5088FF;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
& > div, & > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aibar-center {
|
||||
height: auto;
|
||||
padding: 10px 0;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
151
ui/packages/basic/AiBigTable.vue
Normal file
151
ui/packages/basic/AiBigTable.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<section class="AiBigTable"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import AiEmpty from "./AiEmpty";
|
||||
|
||||
export default {
|
||||
name: "AiBigTable",
|
||||
components: {AiEmpty},
|
||||
model: {
|
||||
prop: "data",
|
||||
event: "input"
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 表数据
|
||||
*/
|
||||
data: {default: () => []},
|
||||
/**
|
||||
* 表格配置
|
||||
*/
|
||||
colConfigs: {default: () => []},
|
||||
/**
|
||||
* 是否显示边框
|
||||
*/
|
||||
border: Boolean,
|
||||
/**
|
||||
* 是否启用dom对象
|
||||
*/
|
||||
isDom: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
html2canvas: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderTable() {
|
||||
const table = document.createElement("div")
|
||||
table.style.display = 'flex'
|
||||
table.style.flexDirection = 'column'
|
||||
table.appendChild(this.renderHead())
|
||||
if (this.data.length > 0) {
|
||||
this.data.map(e => table.appendChild(this.renderRow(e)))
|
||||
} else table.appendChild(this.renderEmpty())
|
||||
if (this.isDom) {
|
||||
this.$el.appendChild(table)
|
||||
} else {
|
||||
this.$el.appendChild(table)
|
||||
this.$load(this.html2canvas).then(() => this.html2canvas(table, {
|
||||
allowTaint: true,
|
||||
useCORS: true,
|
||||
height: this.$el.offsetHeight,
|
||||
width: this.$el.offsetWidth
|
||||
})).then(ctx => {
|
||||
this.$el.removeChild(table)
|
||||
this.$el.appendChild(ctx)
|
||||
})
|
||||
}
|
||||
},
|
||||
renderHead() {
|
||||
const head = document.createElement("div")
|
||||
head.style.display = 'flex'
|
||||
head.style.alignItems = 'center'
|
||||
head.style.fontWeight = 'bold'
|
||||
head.style.background = 'rgba(243, 246, 249, 1)'
|
||||
if (this.border) {
|
||||
head.style.borderLeft = '1px solid #eee'
|
||||
head.style.borderTop = '1px solid #eee'
|
||||
} else {
|
||||
head.style.borderBottom = '1px solid #eee'
|
||||
}
|
||||
this.colConfigs.map(e => {
|
||||
const cell = this.renderCell(e)
|
||||
head.appendChild(cell)
|
||||
})
|
||||
return head
|
||||
},
|
||||
renderRow(item) {
|
||||
const row = document.createElement("div")
|
||||
row.style.display = 'flex'
|
||||
row.style.alignItems = 'center'
|
||||
if (this.border) {
|
||||
row.style.borderLeft = '1px solid #eee'
|
||||
} else {
|
||||
row.style.borderBottom = '1px solid #eee'
|
||||
}
|
||||
this.colConfigs.map(e => {
|
||||
const cell = this.renderCell(e, item)
|
||||
row.appendChild(cell)
|
||||
})
|
||||
return row
|
||||
},
|
||||
renderCell(config, row) {
|
||||
const cell = document.createElement("div")
|
||||
cell.style.display = 'flex'
|
||||
cell.style.alignItems = 'center'
|
||||
cell.style.minheight = '32px'
|
||||
cell.style.padding = '0 8px'
|
||||
if (this.border) {
|
||||
cell.style.borderBottom = '1px solid #eee'
|
||||
cell.style.borderRight = '1px solid #eee'
|
||||
}
|
||||
if (config.align) {
|
||||
cell.style.justifyContent = config.align
|
||||
}
|
||||
if (config.width) {
|
||||
cell.style.width = config.width.toString().replace(/(\d+)/g, '$1px')
|
||||
cell.style.flexShrink = 0
|
||||
} else {
|
||||
cell.style.flex = 1
|
||||
cell.style.minWidth = 0
|
||||
}
|
||||
cell.innerHTML = row?.[config.prop] || config.label
|
||||
return cell
|
||||
},
|
||||
renderEmpty() {
|
||||
const empty = document.createElement("div")
|
||||
empty.style.background = 'url("https://cdn.cunwuyun.cn/dvcp/empty.svg") no-repeat'
|
||||
empty.style.width = '100%'
|
||||
empty.style.height = '140px'
|
||||
empty.style.backgroundPosition = 'center 0'
|
||||
empty.style.backgroundSize = '120px'
|
||||
empty.style.borderBottom = '1px solid #eee'
|
||||
empty.style.borderLeft = '1px solid #eee'
|
||||
empty.style.borderRight = '1px solid #eee'
|
||||
empty.style.textAlign = 'center'
|
||||
empty.style.color = '#999'
|
||||
empty.style.paddingTop = '110px'
|
||||
empty.innerHTML = "暂无数据"
|
||||
return empty
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$injectLib("https://cdn.cunwuyun.cn/html2canvas.min.js", () => {
|
||||
this.html2canvas = window?.html2canvas
|
||||
this.$nextTick(this.renderTable)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.AiBigTable {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
522
ui/packages/basic/AiCron.vue
Normal file
522
ui/packages/basic/AiCron.vue
Normal file
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<section class="AiCron">
|
||||
<div id="changeContab">
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane>
|
||||
<span slot="label"><i class="el-icon-date"></i>秒</span>
|
||||
<div class="tabBody">
|
||||
<el-row>
|
||||
<el-radio v-model="second.cronEvery" label="1">每一秒中</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="second.cronEvery" label="2">每隔
|
||||
<el-input-number size="small" v-model="second.incrementIncrement" :min="1" :max="60"></el-input-number>
|
||||
秒执行 从
|
||||
<el-input-number size="small" v-model="second.incrementStart" :min="0" :max="59"></el-input-number>
|
||||
秒开始
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio class="long" v-model="second.cronEvery" label="3">具体秒数(可多选)
|
||||
<el-select size="small" multiple v-model="second.specificSpecific">
|
||||
<el-option v-for="(val,i) in 60" :key="i" :value="val-1">{{val-1}}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="second.cronEvery" label="4">周期从
|
||||
<el-input-number size="small" v-model="second.rangeStart" :min="1" :max="60"></el-input-number>
|
||||
到
|
||||
<el-input-number size="small" v-model="second.rangeEnd" :min="0" :max="59"></el-input-number>
|
||||
秒
|
||||
</el-radio>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane>
|
||||
<span slot="label"><i class="el-icon-date"></i>分</span>
|
||||
<div class="tabBody">
|
||||
<el-row>
|
||||
<el-radio v-model="minute.cronEvery" label="1">每一分钟</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="minute.cronEvery" label="2">每隔
|
||||
<el-input-number size="small" v-model="minute.incrementIncrement" :min="1" :max="60"></el-input-number>
|
||||
分执行 从
|
||||
<el-input-number size="small" v-model="minute.incrementStart" :min="0" :max="59"></el-input-number>
|
||||
分开始
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio class="long" v-model="minute.cronEvery" label="3">具体分钟数(可多选)
|
||||
<el-select size="small" multiple v-model="minute.specificSpecific">
|
||||
<el-option v-for="(val,i) in 60" :key="i" :value="val-1">{{val-1}}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="minute.cronEvery" label="4">周期从
|
||||
<el-input-number size="small" v-model="minute.rangeStart" :min="1" :max="60"></el-input-number>
|
||||
到
|
||||
<el-input-number size="small" v-model="minute.rangeEnd" :min="0" :max="59"></el-input-number>
|
||||
分
|
||||
</el-radio>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane>
|
||||
<span slot="label"><i class="el-icon-date"></i>时</span>
|
||||
<div class="tabBody">
|
||||
<el-row>
|
||||
<el-radio v-model="hour.cronEvery" label="1">每一小时</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="hour.cronEvery" label="2">每隔
|
||||
<el-input-number size="small" v-model="hour.incrementIncrement" :min="0" :max="23"></el-input-number>
|
||||
小时执行 从
|
||||
<el-input-number size="small" v-model="hour.incrementStart" :min="0" :max="23"></el-input-number>
|
||||
小时开始
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio class="long" v-model="hour.cronEvery" label="3">具体小时数(可多选)
|
||||
<el-select size="small" multiple v-model="hour.specificSpecific">
|
||||
<el-option v-for="(val,i) in 24" :key="i" :value="val-1">{{val-1}}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="hour.cronEvery" label="4">周期从
|
||||
<el-input-number size="small" v-model="hour.rangeStart" :min="0" :max="23"></el-input-number>
|
||||
到
|
||||
<el-input-number size="small" v-model="hour.rangeEnd" :min="0" :max="23"></el-input-number>
|
||||
小时
|
||||
</el-radio>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane>
|
||||
<span slot="label"><i class="el-icon-date"></i>天</span>
|
||||
<div class="tabBody">
|
||||
<el-row>
|
||||
<el-radio v-model="day.cronEvery" label="1">每一天</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="day.cronEvery" label="2">每隔
|
||||
<el-input-number size="small" v-model="week.incrementIncrement" :min="1" :max="7"></el-input-number>
|
||||
周执行 从
|
||||
<el-select size="small" v-model="week.incrementStart">
|
||||
<el-option v-for="(val,i) in 7" :key="i" :label="weeks[val-1]" :value="val"></el-option>
|
||||
</el-select>
|
||||
开始
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="day.cronEvery" label="3">每隔
|
||||
<el-input-number size="small" v-model="day.incrementIncrement" :min="1" :max="31"></el-input-number>
|
||||
天执行 从
|
||||
<el-input-number size="small" v-model="day.incrementStart" :min="1" :max="31"></el-input-number>
|
||||
天开始
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio class="long" v-model="day.cronEvery" label="4">具体星期几(可多选)
|
||||
<el-select size="small" multiple v-model="week.specificSpecific">
|
||||
<el-option v-for="(val,i) in 7"
|
||||
:key="i"
|
||||
:label="weeks[val-1]"
|
||||
:value="['SUN','MON','TUE','WED','THU','FRI','SAT'][val-1]"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio class="long" v-model="day.cronEvery" label="5">具体天数(可多选)
|
||||
<el-select size="small" multiple v-model="day.specificSpecific">
|
||||
<el-option v-for="(val,i) in 31" :key="i" :value="val">{{val}}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="day.cronEvery" label="6">在这个月的最后一天</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="day.cronEvery" label="7">在这个月的最后一个工作日</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="day.cronEvery" label="8">在这个月的最后一个
|
||||
<el-select size="small" v-model="day.cronLastSpecificDomDay">
|
||||
<el-option v-for="(val,i) in 7" :key="i" :label="weeks[val-1]" :value="val"></el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="day.cronEvery" label="9">
|
||||
<el-input-number size="small" v-model="day.cronDaysBeforeEomMinus" :min="1" :max="31"></el-input-number>
|
||||
在本月底前
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="day.cronEvery" label="10">最近的工作日(周一至周五) 至本月
|
||||
<el-input-number size="small" v-model="day.cronDaysNearestWeekday" :min="1" :max="31"></el-input-number>
|
||||
日
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="day.cronEvery" label="11">在这个月的第
|
||||
<el-input-number size="small" v-model="week.cronNthDayNth" :min="1" :max="5"></el-input-number>
|
||||
个
|
||||
<el-select size="small" v-model="week.cronNthDayDay">
|
||||
<el-option v-for="(val,i) in 7" :key="i" :label="weeks[val-1]" :value="val"></el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane>
|
||||
<span slot="label"><i class="el-icon-date"></i> 月</span>
|
||||
<div class="tabBody">
|
||||
<el-row>
|
||||
<el-radio v-model="month.cronEvery" label="1">每个月</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="month.cronEvery" label="2">每隔
|
||||
<el-input-number size="small" v-model="month.incrementIncrement" :min="0" :max="12"></el-input-number>
|
||||
月执行 从
|
||||
<el-input-number size="small" v-model="month.incrementStart" :min="0" :max="12"></el-input-number>
|
||||
月开始
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio class="long" v-model="month.cronEvery" label="3">具体月数(可多选)
|
||||
<el-select size="small" multiple v-model="month.specificSpecific">
|
||||
<el-option v-for="(val,i) in 12" :key="i" :label="val" :value="val"></el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-radio v-model="month.cronEvery" label="4">从
|
||||
<el-input-number size="small" v-model="month.rangeStart" :min="1" :max="12"></el-input-number>
|
||||
到
|
||||
<el-input-number size="small" v-model="month.rangeEnd" :min="1" :max="12"></el-input-number>
|
||||
</el-radio>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="bottom">
|
||||
<span class="value">{{cron}}</span>
|
||||
<el-button type="primary" @click="change">保存</el-button>
|
||||
<el-button type="primary" @click="close">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AiCron",
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: "change"
|
||||
},
|
||||
props: {
|
||||
value: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
weeks: ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天"],
|
||||
second: {
|
||||
cronEvery: '',
|
||||
incrementStart: '3',
|
||||
incrementIncrement: '5',
|
||||
rangeStart: '',
|
||||
rangeEnd: '',
|
||||
specificSpecific: [],
|
||||
},
|
||||
minute: {
|
||||
cronEvery: '',
|
||||
incrementStart: '3',
|
||||
incrementIncrement: '5',
|
||||
rangeStart: '',
|
||||
rangeEnd: '',
|
||||
specificSpecific: [],
|
||||
},
|
||||
hour: {
|
||||
cronEvery: '',
|
||||
incrementStart: '3',
|
||||
incrementIncrement: '5',
|
||||
rangeStart: '',
|
||||
rangeEnd: '',
|
||||
specificSpecific: [],
|
||||
},
|
||||
day: {
|
||||
cronEvery: '',
|
||||
incrementStart: '1',
|
||||
incrementIncrement: '1',
|
||||
rangeStart: '',
|
||||
rangeEnd: '',
|
||||
specificSpecific: [],
|
||||
cronLastSpecificDomDay: 1,
|
||||
cronDaysBeforeEomMinus: '',
|
||||
cronDaysNearestWeekday: '',
|
||||
},
|
||||
week: {
|
||||
cronEvery: '',
|
||||
incrementStart: '1',
|
||||
incrementIncrement: '1',
|
||||
specificSpecific: [],
|
||||
cronNthDayDay: 1,
|
||||
cronNthDayNth: '1',
|
||||
},
|
||||
month: {
|
||||
cronEvery: '',
|
||||
incrementStart: '3',
|
||||
incrementIncrement: '5',
|
||||
rangeStart: '',
|
||||
rangeEnd: '',
|
||||
specificSpecific: [],
|
||||
},
|
||||
output: {
|
||||
second: '',
|
||||
minute: '',
|
||||
hour: '',
|
||||
day: '',
|
||||
month: '',
|
||||
Week: '',
|
||||
},
|
||||
visible: false,
|
||||
cronArr:['*','*','*','*','*','?']
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
cron(val) {
|
||||
this.value = val
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
secondsText() {
|
||||
let seconds = this.cronArr[0];
|
||||
let cronEvery = this.second.cronEvery;
|
||||
switch (cronEvery.toString()) {
|
||||
case '1':
|
||||
seconds = '*';
|
||||
break;
|
||||
case '2':
|
||||
seconds = this.second.incrementStart + '/' + this.second.incrementIncrement;
|
||||
break;
|
||||
case '3':
|
||||
this.second.specificSpecific.map(val => {
|
||||
seconds += val + ','
|
||||
});
|
||||
seconds = seconds.slice(0, -1);
|
||||
break;
|
||||
case '4':
|
||||
seconds = this.second.rangeStart + '-' + this.second.rangeEnd;
|
||||
break;
|
||||
}
|
||||
return seconds;
|
||||
},
|
||||
minutesText() {
|
||||
let minutes = this.cronArr[2];;
|
||||
let cronEvery = this.minute.cronEvery;
|
||||
switch (cronEvery.toString()) {
|
||||
case '1':
|
||||
minutes = '*';
|
||||
break;
|
||||
case '2':
|
||||
minutes = this.minute.incrementStart + '/' + this.minute.incrementIncrement;
|
||||
break;
|
||||
case '3':
|
||||
this.minute.specificSpecific.map(val => {
|
||||
minutes += val + ','
|
||||
});
|
||||
minutes = minutes.slice(0, -1);
|
||||
break;
|
||||
case '4':
|
||||
minutes = this.minute.rangeStart + '-' + this.minute.rangeEnd;
|
||||
break;
|
||||
}
|
||||
return minutes;
|
||||
},
|
||||
hoursText() {
|
||||
let hours = this.cronArr[2];;
|
||||
let cronEvery = this.hour.cronEvery;
|
||||
switch (cronEvery.toString()) {
|
||||
case '1':
|
||||
hours = '*';
|
||||
break;
|
||||
case '2':
|
||||
hours = this.hour.incrementStart + '/' + this.hour.incrementIncrement;
|
||||
break;
|
||||
case '3':
|
||||
this.hour.specificSpecific.map(val => {
|
||||
hours += val + ','
|
||||
});
|
||||
hours = hours.slice(0, -1);
|
||||
break;
|
||||
case '4':
|
||||
hours = this.hour.rangeStart + '-' + this.hour.rangeEnd;
|
||||
break;
|
||||
}
|
||||
return hours;
|
||||
},
|
||||
daysText() {
|
||||
let days = this.cronArr[3];;
|
||||
let cronEvery = this.day.cronEvery;
|
||||
switch (cronEvery.toString()) {
|
||||
case '1':
|
||||
break;
|
||||
case '2':
|
||||
case '4':
|
||||
case '11':
|
||||
days = '?';
|
||||
break;
|
||||
case '3':
|
||||
days = this.day.incrementStart + '/' + this.day.incrementIncrement;
|
||||
break;
|
||||
case '5':
|
||||
this.day.specificSpecific.map(val => {
|
||||
days += val + ','
|
||||
});
|
||||
days = days.slice(0, -1);
|
||||
break;
|
||||
case '6':
|
||||
days = "L";
|
||||
break;
|
||||
case '7':
|
||||
days = "LW";
|
||||
break;
|
||||
case '8':
|
||||
days = this.day.cronLastSpecificDomDay + 'L';
|
||||
break;
|
||||
case '9':
|
||||
days = 'L-' + this.day.cronDaysBeforeEomMinus;
|
||||
break;
|
||||
case '10':
|
||||
days = this.day.cronDaysNearestWeekday + "W";
|
||||
break
|
||||
}
|
||||
return days;
|
||||
},
|
||||
weeksText() {
|
||||
let weeks = this.cronArr[5];;
|
||||
let cronEvery = this.day.cronEvery;
|
||||
switch (cronEvery.toString()) {
|
||||
case '1':
|
||||
case '3':
|
||||
case '5':
|
||||
weeks = '?';
|
||||
break;
|
||||
case '2':
|
||||
weeks = this.week.incrementStart + '/' + this.week.incrementIncrement;
|
||||
break;
|
||||
case '4':
|
||||
this.week.specificSpecific.map(val => {
|
||||
weeks += val + ','
|
||||
});
|
||||
weeks = weeks.slice(0, -1);
|
||||
break;
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
case '10':
|
||||
weeks = "?";
|
||||
break;
|
||||
case '11':
|
||||
weeks = this.week.cronNthDayDay + "#" + this.week.cronNthDayNth;
|
||||
break;
|
||||
}
|
||||
return weeks;
|
||||
},
|
||||
monthsText() {
|
||||
let months = this.cronArr[4];
|
||||
let cronEvery = this.month.cronEvery;
|
||||
switch (cronEvery.toString()) {
|
||||
case '1':
|
||||
months = '*';
|
||||
break;
|
||||
case '2':
|
||||
months = this.month.incrementStart + '/' + this.month.incrementIncrement;
|
||||
break;
|
||||
case '3':
|
||||
this.month.specificSpecific.map(val => {
|
||||
months += val + ','
|
||||
});
|
||||
months = months.slice(0, -1);
|
||||
break;
|
||||
case '4':
|
||||
months = this.month.rangeStart + '-' + this.month.rangeEnd;
|
||||
break;
|
||||
}
|
||||
return months;
|
||||
},
|
||||
cron() {
|
||||
return `${this.secondsText || '*'} ${this.minutesText || '*'} ${this.hoursText || '*'} ${this.daysText || '*'} ${this.monthsText || '*'} ${this.weeksText || '?'} `
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getValue() {
|
||||
return this.cron;
|
||||
},
|
||||
change() {
|
||||
this.$emit('change', this.cron);
|
||||
this.close();
|
||||
},
|
||||
close() {
|
||||
this.$emit('close')
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if(this.value) this.cronArr = this.value.trim().split(" ")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.AiCron {
|
||||
|
||||
#changeContab {
|
||||
.language {
|
||||
position: absolute;
|
||||
right: 25px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tabBody {
|
||||
.el-row {
|
||||
margin: 10px 0;
|
||||
|
||||
.long {
|
||||
.el-select {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input-number {
|
||||
width: 110px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
|
||||
.value {
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
163
ui/packages/basic/AiDialog.vue
Normal file
163
ui/packages/basic/AiDialog.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<section class="ai-dialog__wrapper">
|
||||
<el-dialog
|
||||
custom-class="ai-dialog"
|
||||
v-on="$listeners"
|
||||
v-if="isEmpty"
|
||||
:close-on-click-modal="closeOnClickModal"
|
||||
v-bind="$attrs"
|
||||
:destroy-on-close="destroyOnClose"
|
||||
:visible.sync="dialogVisible">
|
||||
<div class="ai-dialog__header" slot="title">
|
||||
<h2>{{ title }}</h2>
|
||||
</div>
|
||||
<div class="ai-dialog__content" :style="{'max-height': isScrool ? '500px' : 'auto'}">
|
||||
<div class="ai-dialog__content--wrapper" :style="{'padding-right': isScrool ? '8px' : '0'}">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="customFooter" slot="footer">
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
<div v-else class="dialog-footer" slot="footer">
|
||||
<el-button @click="onCancel">取消</el-button>
|
||||
<el-button @click="onConfirm" type="primary" style="width: 92px;">确认</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customFooter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
'close-on-click-modal': {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
isScrool: true,
|
||||
isEmpty: true
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
visible: {
|
||||
handler (val) {
|
||||
this.dialogVisible = val
|
||||
|
||||
// if (val) {
|
||||
// this.$nextTick(() => {
|
||||
// setTimeout(() => {
|
||||
// this.isScrool = document.querySelector('.ai-dialog__content') && document.querySelector('.ai-dialog__content').clientHeight >= 500
|
||||
// }, 100)
|
||||
// })
|
||||
// }
|
||||
|
||||
if (this.destroyOnClose && !val) {
|
||||
setTimeout(() => {
|
||||
this.isEmpty = false
|
||||
|
||||
setTimeout(() => {
|
||||
this.isEmpty = true
|
||||
}, 50)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
},
|
||||
|
||||
methods: {
|
||||
onCancel () {
|
||||
this.$emit('update:visible', false)
|
||||
this.$emit('onCancel')
|
||||
},
|
||||
|
||||
onConfirm () {
|
||||
this.$emit('onConfirm')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.ai-dialog {
|
||||
margin: 0!important;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px 40px 20px;
|
||||
}
|
||||
|
||||
.ai-dialog__content {
|
||||
overflow-y: auto;
|
||||
padding-bottom: 4px;
|
||||
|
||||
.ai-dialog__content--wrapper {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-dialog__header {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 16px 20px;
|
||||
box-sizing: border-box;
|
||||
background: #F3F6F9;
|
||||
text-align: center;
|
||||
|
||||
& + .el-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 92px!important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 24px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
139
ui/packages/basic/AiDrawer.vue
Normal file
139
ui/packages/basic/AiDrawer.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<section class="AiDrawer" :style="{width:`${width}px`,height:`${height}px`}">
|
||||
<slot name="tools"/>
|
||||
<div class="resetSignature" @click.stop="handleClean">
|
||||
<ai-icon icon="iconResetting"/>
|
||||
<span>重写</span>
|
||||
</div>
|
||||
<div v-if="drawPlaceholder" class="placeholder">{{ placeholder }}</div>
|
||||
<canvas id="drawer" :width="width" :height="height" @mousedown.stop="handleDrawStart($event)"
|
||||
@mouseup.stop="handleDrawEnd" @mouseleave="handleDrawEnd"
|
||||
@mousemove.stop="handleDrawing($event)"
|
||||
@touchstart.stop="handleDrawStart" @touchend.stop="handleDrawEnd" @touchmove="handleDrawing"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AiDrawer",
|
||||
props: {
|
||||
seal: String,
|
||||
placeholder: {type: String, default: "请在此处清晰书写你的签名"},
|
||||
width: {type: Number, default: 640},
|
||||
height: {type: Number, default: 480}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawing: false,//判断是否处于绘画中
|
||||
drawer: {},
|
||||
drawPlaceholder: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
drawing() {
|
||||
this.drawPlaceholder = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initDrawer()
|
||||
},
|
||||
methods: {
|
||||
initDrawer() {
|
||||
this.$nextTick(() => {
|
||||
let canvas = document.querySelector("#drawer")
|
||||
if (canvas) {
|
||||
this.drawer = canvas?.getContext("2d")
|
||||
this.drawer.lineWidth = 3
|
||||
this.drawer.shadowColor = '#333'
|
||||
this.drawer.strokeStyle = '#333'
|
||||
this.drawer.shadowBlur = 2
|
||||
}
|
||||
})
|
||||
},
|
||||
handleDrawStart(e) {
|
||||
this.drawing = true
|
||||
const canvasX = e.offsetX
|
||||
const canvasY = e.offsetY
|
||||
this.drawer?.beginPath()
|
||||
this.drawer?.moveTo(canvasX, canvasY)
|
||||
},
|
||||
handleDrawEnd() {
|
||||
this.drawing = false
|
||||
this.$emit("update:seal", this.drawer.canvas.toDataURL("image/png", 1))
|
||||
},
|
||||
handleDrawing(e) {
|
||||
if (this.drawing) {
|
||||
const t = e.target?.getBoundingClientRect()
|
||||
let canvasX
|
||||
let canvasY
|
||||
// 根据触发事件类型来进行定位
|
||||
if (e.type == "touchmove") {
|
||||
canvasX = e.changedTouches[0].clientX - t.x
|
||||
canvasY = e.changedTouches[0].clientY - t.y
|
||||
} else {
|
||||
canvasX = e.offsetX
|
||||
canvasY = e.offsetY
|
||||
}
|
||||
// 连接到移动的位置并上色
|
||||
this.drawer?.lineTo(canvasX, canvasY)
|
||||
this.drawer?.stroke()
|
||||
}
|
||||
},
|
||||
handleClean() {
|
||||
this.drawer.clearRect(0, 0, this.drawer.canvas.width, this.drawer.canvas.height)
|
||||
this.drawPlaceholder = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.AiDrawer {
|
||||
margin: 0 40px;
|
||||
background: #F7F7F7;
|
||||
border: 1px solid #CCCCCC;
|
||||
position: relative;
|
||||
|
||||
#drawer {
|
||||
display: block;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.resetSignature {
|
||||
position: absolute;
|
||||
width: 76px;
|
||||
height: 32px;
|
||||
background: rgba(#000, .5);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: rgba(#fff, .6);
|
||||
cursor: pointer;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
|
||||
.AiIcon {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 407px;
|
||||
text-align: center;
|
||||
font-size: 64px;
|
||||
color: #DDDDDD;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
ui/packages/basic/AiEditor/AiEditor.vue
Normal file
129
ui/packages/basic/AiEditor/AiEditor.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<section class="AiEditor" :class="{invalid:valid&&!validateState}">
|
||||
<ck-editor v-if="sourceEditor" :editor="sourceEditor" v-model="content" :config="editorConfig" @ready="handleReady" @input="$emit('change',repairContent)"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 原组件ckeditor封装
|
||||
* 修改者:Kubbo
|
||||
*/
|
||||
import CKEditor from '@ckeditor/ckeditor5-vue2'
|
||||
import {UploadAdapter} from "./models";
|
||||
|
||||
export default {
|
||||
name: "AiEditor",
|
||||
components: {ckEditor: CKEditor.component},
|
||||
inject: {
|
||||
elFormItem: {default: ""},
|
||||
elForm: {default: ''},
|
||||
},
|
||||
model: {
|
||||
prop: "value",
|
||||
event: "change"
|
||||
},
|
||||
props: {
|
||||
value: {type: String, required: true, default: ""},
|
||||
placeholder: {type: String, default: '请输入正文'},
|
||||
conf: Object,
|
||||
instance: {type: Function, required: true},
|
||||
valid: {type: Boolean, default: true},
|
||||
text: {default: ""},
|
||||
action: {default: "/admin/file/add"},
|
||||
params: {default: () => ({})},
|
||||
mode: {default: "default"}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sourceEditor: null,
|
||||
editor: null,
|
||||
isPasteStyle: true,//粘贴是否携带格式
|
||||
content: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
validateState() {
|
||||
return ['', 'success'].includes(this.elFormItem?.validateState)
|
||||
},
|
||||
editorConfig() {
|
||||
return {
|
||||
mediaEmbed: {
|
||||
previewsInData: true
|
||||
},
|
||||
htmlSupport: {
|
||||
allow: [{name: /[\s\S]+/, styles: true, classes: true, attributes: true}]
|
||||
},
|
||||
...this.conf
|
||||
}
|
||||
},
|
||||
repairContent: v => `<section class="ck-content">${v.content?.replace(/\s0w"/g, '"')?.replace(/srcset/g, 'src')}</section>`
|
||||
},
|
||||
watch: {
|
||||
content(v) {
|
||||
v && this.dispatch('ElFormItem', 'el.form.change', [v])
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleReady(editor) {
|
||||
this.editor = editor
|
||||
const {instance, action, params} = this
|
||||
editor.plugins.get('FileRepository').createUploadAdapter = loader => new UploadAdapter(loader, instance, action, params)
|
||||
},
|
||||
/**
|
||||
* 表单验证
|
||||
* @param componentName
|
||||
* @param eventName
|
||||
* @param params
|
||||
*/
|
||||
dispatch(componentName, eventName, params) {
|
||||
let parent = this.$parent || this.$root;
|
||||
let name = parent.$options.componentName;
|
||||
|
||||
while (parent && (!name || name !== componentName)) {
|
||||
parent = parent.$parent;
|
||||
|
||||
if (parent) {
|
||||
name = parent.$options.componentName;
|
||||
}
|
||||
}
|
||||
if (parent) {
|
||||
parent.$emit.apply(parent, [eventName].concat(params));
|
||||
}
|
||||
},
|
||||
initValue() {
|
||||
let unwatch = this.$watch('value', (v) => {
|
||||
const init = v.replace(/<section class="ck-content">(.*)<\/section>/, '$1')
|
||||
if (!!this.content) unwatch && unwatch()
|
||||
else if (!!init) {
|
||||
this.content = init
|
||||
unwatch && unwatch()
|
||||
}
|
||||
}, {immediate: true})
|
||||
},
|
||||
loadEditor(count = 0) {
|
||||
if (!!window?.ClassicEditor) {
|
||||
this.sourceEditor = ClassicEditor
|
||||
this.initValue()
|
||||
} else if (count < 10) {
|
||||
setTimeout(() => this.loadEditor(++count), 50)
|
||||
} else {
|
||||
console.error("无法加载编辑器资源,请联系管理员")
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$injectLib("https://cdn.cunwuyun.cn/ckeditor.js", () => {
|
||||
this.loadEditor()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.AiEditor {
|
||||
::v-deep.ck-content {
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
ui/packages/basic/AiEditor/models.js
Normal file
24
ui/packages/basic/AiEditor/models.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import {Loading} from "element-ui"
|
||||
|
||||
export class UploadAdapter {
|
||||
constructor(loader, instance, action, params) {
|
||||
this.instance = instance
|
||||
this.action = action
|
||||
this.loader = loader
|
||||
this.params = params
|
||||
}
|
||||
|
||||
async upload() {
|
||||
const formData = new FormData()
|
||||
formData.append('file', await this.loader.file)
|
||||
const loading = Loading.service({})
|
||||
return this.instance.post(this.action, formData, {...this.params, returnError: true}).then(res => {
|
||||
if (res?.data) {
|
||||
return res.data.map(m => m.split(";")?.[0])
|
||||
} else return this.loader.status = "aborted" && Promise.reject()
|
||||
}).finally(() => loading.close())
|
||||
}
|
||||
|
||||
abort() {
|
||||
}
|
||||
}
|
||||
34
ui/packages/basic/AiEmpty.vue
Normal file
34
ui/packages/basic/AiEmpty.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="ai-empty">
|
||||
<div class="ai-empty__bg"></div>
|
||||
<template v-if="!isHasTitleSlot">暂无数据</template>
|
||||
<slot v-else></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiEmpty',
|
||||
|
||||
computed: {
|
||||
isHasTitleSlot () {
|
||||
return this.$slots.default
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ai-empty {
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
color: #acaaad;
|
||||
|
||||
.ai-empty__bg {
|
||||
background: url("https://cdn.cunwuyun.cn/dvcp/empty.svg") no-repeat center;
|
||||
background-size: 120px 120px;
|
||||
height: 120px;
|
||||
margin: 48px auto 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
118
ui/packages/basic/AiFileList.vue
Normal file
118
ui/packages/basic/AiFileList.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="ai-filelist">
|
||||
<div class="ai-flie__item" v-for="(item, index) in fileList" :key="index" @click="downFile(item)"
|
||||
:title="item.name">
|
||||
<div class="ai-flie__item--left flex1">
|
||||
<svg aria-hidden="true">
|
||||
<use xlink:href="#iconAppendix_UNdownload"></use>
|
||||
</svg>
|
||||
<span>{{ item[fileProps.name] }}</span>
|
||||
</div>
|
||||
<div class="ai-file__item--right">
|
||||
<span>{{ item[fileProps.size] }}</span>
|
||||
<i class="iconfont iconDownload"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!fileList.length" style="width: 120px" class="no-data"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiFileList',
|
||||
|
||||
props: {
|
||||
fileList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
fileOps: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fileProps() {
|
||||
const props = {
|
||||
name: 'name',
|
||||
size: 'size'
|
||||
}
|
||||
|
||||
return this.fileOps || props
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
downFile(item) {
|
||||
window.open(`${item.url}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ai-filelist {
|
||||
.ai-flie__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin-bottom: 16px;
|
||||
padding: 0 8px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d0d4dc;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f6f9;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-flie__item--left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-file__item--right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 1;
|
||||
|
||||
span {
|
||||
padding-right: 5px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
i {
|
||||
color: #5088FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
38
ui/packages/basic/AiIcon.vue
Normal file
38
ui/packages/basic/AiIcon.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="AiIcon">
|
||||
<div v-if="type=='icon'" class="iconfont" :class="icon"/>
|
||||
<div v-if="type=='logo'" class="logofont" :class="icon"/>
|
||||
<svg v-if="type=='svg'" class="icon" aria-hidden="true">
|
||||
<use :xlink:href="`#${icon}`"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "../../meta/styles/iconfont/iconfont";
|
||||
import "../../meta/styles/iconfont/iconfont.css";
|
||||
import "../../meta/styles/iconfont/logofont.css";
|
||||
|
||||
export default {
|
||||
name: "AiIcon",
|
||||
props: {
|
||||
type: {type: String, default: "icon"},
|
||||
icon: {type: String, required: true},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.AiIcon {
|
||||
box-sizing: border-box;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 16px;
|
||||
|
||||
.iconfont, .logofont, .icon {
|
||||
font-size: inherit;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
321
ui/packages/basic/AiImport.vue
Normal file
321
ui/packages/basic/AiImport.vue
Normal file
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<section class="ai-import">
|
||||
<a v-if="$slots.default" class="custom-clicker" @click="dialog = true">
|
||||
<slot/>
|
||||
</a>
|
||||
<el-button v-else size="small" @click="dialog=true" icon="iconfont iconImport">导入</el-button>
|
||||
<ai-dialog
|
||||
:title="dialogTitle"
|
||||
:visible.sync="dialog"
|
||||
:destroy-on-close="true"
|
||||
width="800px"
|
||||
customFooter
|
||||
:close-on-click-modal="false"
|
||||
@closed="onClose">
|
||||
<el-form size="small" ref="importForm" label-width="0" class="import-form" :model="fileForm"
|
||||
:rules="fileRules">
|
||||
<el-form-item class="ai-import__tips">
|
||||
<h2>导入说明:</h2>
|
||||
<p class="ai-import__content">
|
||||
1、您正在进行【{{ name }}】批量导入操作,请先
|
||||
<span @click="downloadFile" title="下载导入模板" class="ai-link" v-text="'【点击下载导入模板】'"/>
|
||||
并规范填写;</p>
|
||||
<p class="ai-import__content">2、填写完成后,上传您编辑完成的模板文件:</p>
|
||||
<div class="ai-import__text">
|
||||
<template v-if="$slots.tips">
|
||||
<div>请注意:</div>
|
||||
<slot name="tips"/>
|
||||
</template>
|
||||
<el-row type="flex" v-else-if="!!dict">
|
||||
<div v-text="'请注意:'"/>
|
||||
<span v-text="dict.getLabel('importTips',type)"/>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item prop="file" style="width: 100%">
|
||||
<ai-uploader isImport :instance="instance" v-model="fileForm.file" fileType="file" :limit="1"
|
||||
acceptType=".xls,.xlsx" @change="onChange" :clearable="false">
|
||||
<template #trigger>
|
||||
<el-button icon="iconfont iconfangda">选择文件</el-button>
|
||||
</template>
|
||||
<template #tips>最多上传1个文件,单个文件最大10MB,仅支持Excel格式</template>
|
||||
</ai-uploader>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" style="text-align: center;">
|
||||
<el-button @click="dialog=false">取消</el-button>
|
||||
<el-button type="primary" @click="onClick">立即导入</el-button>
|
||||
</div>
|
||||
</ai-dialog>
|
||||
<div class="ai-import__loading" v-if="isHasLoadingSlot && isLoading">
|
||||
<slot name="loading"/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiImport',
|
||||
props: {
|
||||
title: String,
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
instance: {
|
||||
type: Function
|
||||
},
|
||||
importUrl: {
|
||||
type: String,
|
||||
},
|
||||
importParams: {
|
||||
type: Object
|
||||
},
|
||||
suffixName: {
|
||||
type: String,
|
||||
default: 'xls'
|
||||
},
|
||||
tplParams: Object,
|
||||
url: {
|
||||
type: String,
|
||||
},
|
||||
timeout: {
|
||||
type: Number,
|
||||
default: 10 * 60 * 1000
|
||||
},
|
||||
customError: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: String,
|
||||
dict: Object
|
||||
},
|
||||
computed: {
|
||||
isHasLoadingSlot() {
|
||||
return this.$slots.loading
|
||||
},
|
||||
actions() {
|
||||
return {
|
||||
url: this.importUrl || `/app/${this.type}/import`,//导入接口
|
||||
tpl: this.url || `/app/${this.type}/downloadTemplate`,//下载模板接口
|
||||
}
|
||||
},
|
||||
dialogTitle() {
|
||||
return this.title || `${this.name}数据导入`
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
fileForm: {
|
||||
file: []
|
||||
},
|
||||
loading: null,
|
||||
isLoading: false,
|
||||
fileRules: {
|
||||
file: [{required: true, message: '请上传相关文件', trigger: 'change'}]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(e) {
|
||||
if (e.length) {
|
||||
this.$refs.importForm.clearValidate()
|
||||
} else {
|
||||
this.$refs.importForm.validate()
|
||||
}
|
||||
},
|
||||
onClick() {
|
||||
this.$refs.importForm.validate(v => {
|
||||
if (v) {
|
||||
const data = new FormData()
|
||||
data.append('file', this.fileForm.file[0].raw)
|
||||
if (!this.isHasLoadingSlot) {
|
||||
this.loading = this.$loading({
|
||||
lock: true,
|
||||
text: '导入中',
|
||||
background: 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
} else {
|
||||
this.isLoading = true
|
||||
}
|
||||
this.instance.post(this.actions.url, data, {
|
||||
params: this.importParams,
|
||||
timeout: this.timeout
|
||||
}).then(res => {
|
||||
if (!this.isHasLoadingSlot) {
|
||||
this.loading?.close()
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
if (res?.data?.importStatus == 1) {
|
||||
this.dialog = false
|
||||
const h = this.$createElement
|
||||
this.$emit('onSuccess', res)
|
||||
this.$emit('success', res)
|
||||
if (this.customError) {
|
||||
this.$emit('error')
|
||||
} else this.$confirm('导入失败数据具体原因请', {
|
||||
type: 'success',
|
||||
title: '数据导入完成',
|
||||
closeOnClickModal: false,
|
||||
customClass: 'message-wrapper',
|
||||
showConfirmButton: false,
|
||||
cancelButtonText: '关闭',
|
||||
message: h('div', {
|
||||
class: 'importResult'
|
||||
}, [
|
||||
h('span', null, '成功新增'),
|
||||
h('a', {
|
||||
style: 'color: #2EA222;'
|
||||
}, `${res.data.addCount}`),
|
||||
h('span', null, '条数据,更新'),
|
||||
h('a', {
|
||||
style: 'color: #26f;'
|
||||
}, `${res.data.updateCount}`),
|
||||
h('span', null, '条数据,导入失败'),
|
||||
h('a', {
|
||||
style: 'color: #f46;'
|
||||
}, `${res.data.failCount}`),
|
||||
h('span', null, '条数据。'),
|
||||
h('div', {class: 'gap'}),
|
||||
h('div', {style: `display:${res.data.errorFileURL ? 'block' : 'none'}`}, [
|
||||
h('span', null, '点此'),
|
||||
h('a', {
|
||||
class: 'tips-link',
|
||||
attrs: {
|
||||
href: res.data.errorFileURL
|
||||
}
|
||||
}, '下载异常数据')
|
||||
])
|
||||
])
|
||||
})
|
||||
} else if (res?.data?.importStatus == 0) {
|
||||
this.$message.error(res?.data?.errorMsg)
|
||||
}
|
||||
}).catch(() => {
|
||||
if (!this.isHasLoadingSlot) {
|
||||
this.loading?.close()
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onClose() {
|
||||
this.loading?.close()
|
||||
this.fileForm.file = []
|
||||
},
|
||||
downloadFile() {
|
||||
this.instance.post(this.actions.tpl, null, {
|
||||
responseType: 'blob',
|
||||
params: this.tplParams
|
||||
}).then((res) => {
|
||||
const link = document.createElement('a')
|
||||
let blob = new Blob([res], {type: 'application/vnd.ms-excel'})
|
||||
link.style.display = 'none'
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.setAttribute('download', this.name + '模板.' + this.suffixName)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
},
|
||||
hide() {
|
||||
this.dialog = false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.dict?.load("importTips")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ai-import {
|
||||
.empty-input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
.custom-clicker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ai-import__loading {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
::v-deep .el-message-box {
|
||||
width: 720px !important;
|
||||
}
|
||||
|
||||
.ai-import__content {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.ai-import__tips {
|
||||
line-height: 1;
|
||||
|
||||
::v-deep.el-form-item__content {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 4px;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 8px;
|
||||
color: #424242;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ai-link {
|
||||
cursor: pointer;
|
||||
color: $primaryColor;
|
||||
}
|
||||
|
||||
.ai-import__text {
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
line-height: 16px;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.message-wrapper {
|
||||
width: 560px !important;
|
||||
|
||||
.importResult {
|
||||
color: #222222;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: bold;
|
||||
|
||||
.gap {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.tips-link {
|
||||
color: $primaryColor;
|
||||
text-decoration: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
91
ui/packages/basic/AiInfoItem.vue
Normal file
91
ui/packages/basic/AiInfoItem.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="ai-info-item" :style="contentStyle">
|
||||
<label class="ai-info-item__left" :style="contentLabelStyle">
|
||||
<template v-if="$slots.label">
|
||||
<slot name="label"/>
|
||||
</template>
|
||||
<template v-else>{{ label }}</template>
|
||||
</label>
|
||||
<div class="ai-info-item__right">
|
||||
<slot v-if="$scopedSlots.default"/>
|
||||
<template v-else-if="!!openType">
|
||||
<ai-open-data :type="openType" :openid="value"/>
|
||||
</template>
|
||||
<template v-else>{{ value || '-' }}</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiInfoItem',
|
||||
|
||||
inject: ['AiWrapper'],
|
||||
|
||||
props: {
|
||||
label: {
|
||||
type: String
|
||||
},
|
||||
|
||||
value: {
|
||||
type: [String, Number]
|
||||
},
|
||||
|
||||
'label-width': {
|
||||
type: String
|
||||
},
|
||||
|
||||
isLine: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
openType: {default: ""}
|
||||
},
|
||||
|
||||
computed: {
|
||||
contentStyle() {
|
||||
let width = this.AiWrapper.autoWidth
|
||||
|
||||
if (this.isLine) {
|
||||
width = '100%'
|
||||
}
|
||||
|
||||
return {
|
||||
width
|
||||
}
|
||||
},
|
||||
|
||||
contentLabelStyle() {
|
||||
return {
|
||||
width: this.labelWidth || this.AiWrapper.autoLableWidth
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ai-info-item {
|
||||
display: flex;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
flex-shrink: 0;
|
||||
width: 96px;
|
||||
margin-right: 40px;
|
||||
text-align: right;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ai-info-item__right {
|
||||
flex: 1;
|
||||
color: #222;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
ui/packages/basic/AiNumber.vue
Normal file
58
ui/packages/basic/AiNumber.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<section class="AiNumber">
|
||||
<el-input v-model="num" :size="size" @input.native="validate" @focus="$emit('focus')" @blur="format" clearable
|
||||
@change="$emit('change')" :validate-event="isRule"></el-input>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AiNumber",
|
||||
model: {
|
||||
prop: "value",
|
||||
event: "change"
|
||||
},
|
||||
props: {
|
||||
value: String,
|
||||
size: String,
|
||||
decimal: {type: [String, Number], default: 2},
|
||||
isRule: {type: Boolean, default: true}
|
||||
},
|
||||
computed: {
|
||||
validRegex() {
|
||||
let dec = Number(this.decimal || 0),
|
||||
regex = `^(\\d*\\.?\\d{0,${dec}}).*`
|
||||
return new RegExp(regex)
|
||||
}
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
num: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validate() {
|
||||
let num = JSON.parse(JSON.stringify(this.num))
|
||||
this.num = num.replace(this.validRegex, '$1')
|
||||
},
|
||||
format() {
|
||||
this.num = Number(this.num||0).toString()
|
||||
this.$emit("blur")
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
num(v) {
|
||||
this.$emit("change", v)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.num = this.value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.AiNumber {
|
||||
}
|
||||
</style>
|
||||
98
ui/packages/basic/AiOperate.vue
Normal file
98
ui/packages/basic/AiOperate.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="ai-operate">
|
||||
<template v-if="filterBtns.length <= 3">
|
||||
<el-button
|
||||
v-for="(item, index) in filterBtns"
|
||||
:key="index"
|
||||
type="text"
|
||||
:title="item.text"
|
||||
@click="onClick(index)">
|
||||
{{ item.text }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
type="text"
|
||||
:title="item.text"
|
||||
@click="onClick(index)">
|
||||
{{ item.text }}
|
||||
</el-button>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="onDropClick">
|
||||
<el-button
|
||||
style="margin-left: 10px;"
|
||||
title="更多"
|
||||
type="text">
|
||||
更多
|
||||
</el-button>
|
||||
<el-dropdown-menu
|
||||
slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-for="(item, index) in moreList"
|
||||
:key="index"
|
||||
:command="index">
|
||||
{{ item.text }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiOperate',
|
||||
|
||||
props: {
|
||||
permissions: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
|
||||
btns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
filterBtns() {
|
||||
return this.btns.filter(e => this.permissions(e.permissions))
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
list: [],
|
||||
moreList: []
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.filterBtns.forEach((item, index) => {
|
||||
if (index <= 1) {
|
||||
this.list.push(item)
|
||||
} else {
|
||||
this.moreList.push(item)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick(index) {
|
||||
this.$emit('on-click', index)
|
||||
},
|
||||
|
||||
onDropClick(e) {
|
||||
this.onClick(e + 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
76
ui/packages/basic/AiPullDown.vue
Normal file
76
ui/packages/basic/AiPullDown.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<section class="AiPullDown">
|
||||
<div class="line"/>
|
||||
<div class="down-content" @click="handleExpand">
|
||||
<i :class="expandIcon"/>
|
||||
<span>{{ btnText }}</span>
|
||||
</div>
|
||||
<div class="line"/>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AiPullDown",
|
||||
props: {
|
||||
target: String,
|
||||
height: {default: 4},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expand: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleExpand() {
|
||||
this.expand = !this.expand
|
||||
if (this.target) {
|
||||
|
||||
} else this.$emit('change', this.expandStyle)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
btnText() {
|
||||
return this.expand ? '收起高级搜索' : '展开高级搜索'
|
||||
},
|
||||
expandStyle() {
|
||||
let initStyle = {overflow: 'hidden', height: `${this.height}px`}
|
||||
this.expand && (initStyle.height = "auto")
|
||||
return initStyle
|
||||
},
|
||||
expandIcon() {
|
||||
return this.expand ? 'iconfont iconDouble_Up' : 'iconfont iconDouble_Down'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$emit("change", this.expandStyle)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.AiPullDown {
|
||||
display: flex;
|
||||
|
||||
.line {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.down-content {
|
||||
cursor: pointer;
|
||||
padding: 0 8px;
|
||||
height: 24px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
border: 1px solid #eee;
|
||||
border-top: 0;
|
||||
box-sizing: border-box;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
101
ui/packages/basic/AiRange.vue
Normal file
101
ui/packages/basic/AiRange.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<section class="AiRange" :style="{width}">
|
||||
<el-input type="number" v-model.number="valueStart" size="small" :placeholder="startPlaceholder" @change="saveNum"/>
|
||||
<span class="separator">-</span>
|
||||
<el-input type="number" v-model.number="valueEnd" size="small" :placeholder="endPlaceholder" @change="saveNum"/>
|
||||
<span class="el-icon-circle-close" v-if="isUse" @click="shutoff"/>
|
||||
<div v-else/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AiRange",
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change'
|
||||
},
|
||||
props: {
|
||||
width: {type: String, default: '200px'},
|
||||
startPlaceholder: String,
|
||||
endPlaceholder: String,
|
||||
value: {default: () => []},
|
||||
},
|
||||
computed: {
|
||||
isUse() {
|
||||
return !!this.valueStart || !!this.valueEnd
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
valueStart: null,
|
||||
valueEnd: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shutoff() {
|
||||
this.valueStart = null
|
||||
this.valueEnd = null
|
||||
this.saveNum()
|
||||
},
|
||||
saveNum() {
|
||||
this.$emit('change', [this.valueStart, this.valueEnd])
|
||||
},
|
||||
initValue() {
|
||||
let unwatch = this.$watch('value', (v) => {
|
||||
if (this.isUse) unwatch && unwatch()
|
||||
else if (!!v) {
|
||||
this.valueStart = v?.[0] || ""
|
||||
this.valueEnd = v?.[1] || ""
|
||||
unwatch && unwatch()
|
||||
}
|
||||
}, {immediate: true})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initValue()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.AiRange {
|
||||
display: flex;
|
||||
border: 1px solid #D0D4DC;
|
||||
border-radius: 2px;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
|
||||
::v-deep.el-input {
|
||||
min-width: 80px;
|
||||
flex: 1;
|
||||
|
||||
.el-input__inner {
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon-circle-close {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
margin-right: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #5088FF;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #5088FF;
|
||||
|
||||
.el-icon-circle-close {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
ui/packages/basic/AiSelect.vue
Normal file
106
ui/packages/basic/AiSelect.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="ai-select">
|
||||
<el-select
|
||||
style="width: 100%;"
|
||||
clearable
|
||||
:value="value"
|
||||
:size="$attrs.size || 'small'"
|
||||
:filterable="isAction"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners">
|
||||
<template v-if="isAction">
|
||||
<el-option v-for="op in actionOps" :key="op.id"
|
||||
:label="op[actionProp.label]" :value="op[actionProp.value]"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-option
|
||||
v-for="(item, index) in selectList"
|
||||
:key="index"
|
||||
:label="item.dictName"
|
||||
:value="item.dictValue"/>
|
||||
</template>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiSelect',
|
||||
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change'
|
||||
},
|
||||
watch: {
|
||||
instance: {
|
||||
deep: true,
|
||||
handler(v) {
|
||||
v && this.isAction && !this.options.toString() && this.getOptions()
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {default: null},
|
||||
selectList: {
|
||||
type: Array
|
||||
},
|
||||
|
||||
width: {
|
||||
type: String,
|
||||
default: '216'
|
||||
},
|
||||
instance: Function,
|
||||
action: {default: ""},
|
||||
prop: {
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
options: [],
|
||||
filter: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectWidth() {
|
||||
if (this.width.indexOf('px') > -1) {
|
||||
return this.width
|
||||
}
|
||||
return `${this.width}px`
|
||||
},
|
||||
isAction() {
|
||||
return !!this.action
|
||||
},
|
||||
actionOps() {
|
||||
return this.options.filter(e => !this.filter || e[this.actionProp.label].indexOf(this.filter) > -1)
|
||||
},
|
||||
actionProp() {
|
||||
return {
|
||||
label: 'label',
|
||||
value: 'id',
|
||||
...this.prop
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getOptions() {
|
||||
this.instance?.post(this.action, null, {
|
||||
params: {size: 999}
|
||||
}).then(res => {
|
||||
if (res?.data) {
|
||||
this.options = res.data.records || res.data
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getOptions()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .ai-select .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
446
ui/packages/basic/AiTable.vue
Normal file
446
ui/packages/basic/AiTable.vue
Normal file
@@ -0,0 +1,446 @@
|
||||
<template>
|
||||
<div class="ai-table" :class="[isShowBorder ? 'ai-table__border' : 'ai-table__noborder']">
|
||||
<el-table
|
||||
:data="tableData"
|
||||
header-cell-class-name="ai-table__header"
|
||||
cell-class-name="ai-table__cell"
|
||||
row-class-name="ai-table__row"
|
||||
:class="{'ai-header__border': isShowBorder}"
|
||||
:ref="refName"
|
||||
:size="tableSize"
|
||||
:stripe="stripe"
|
||||
:tooltip-effect="tooltipEffect"
|
||||
@selection-change="handleSelectionChange"
|
||||
v-on="$listeners"
|
||||
v-bind="$attrs"
|
||||
v-loading="loading">
|
||||
<template v-for="colConfig in colConfigs.filter(e=>!e.hide)">
|
||||
<slot v-if="colConfig.slot && colConfig.slot !== 'options'" :name="colConfig.slot"/>
|
||||
<component
|
||||
:key="colConfig.id"
|
||||
v-else-if="colConfig.component"
|
||||
:is="colConfig.component"
|
||||
:col-config="colConfig">
|
||||
</component>
|
||||
<el-table-column
|
||||
v-else-if="colConfig.dict"
|
||||
:key="colConfig.id"
|
||||
v-bind="colConfig">
|
||||
<span slot-scope="{row}" :style="{color:colConfig.color||dict.getColor(colConfig.dict, row[colConfig.prop])}">
|
||||
{{ dict.getLabel(colConfig.dict, row[colConfig.prop]) }}
|
||||
</span>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-else-if="colConfig.openType"
|
||||
:key="colConfig.id"
|
||||
v-bind="colConfig">
|
||||
<template v-slot="{row}">
|
||||
<ai-open-data :type="colConfig.openType" :openid="row[colConfig.prop]"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-else-if="colConfig.type"
|
||||
:key="colConfig.id"
|
||||
v-bind="colConfig"
|
||||
:width="colConfig.width || 100"/>
|
||||
<el-table-column v-else v-bind="colConfig" :key="colConfig.id"
|
||||
:show-overflow-tooltip="colConfig['show-overflow-tooltip'] != false">
|
||||
<template slot-scope="scope">
|
||||
<render-slot v-if="colConfig.render" :render="colConfig.render" :row="scope.row" :index="scope.$index"
|
||||
:column="colConfig"/>
|
||||
<span v-else>{{ getValue(colConfig, scope.row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<slot class="table-options" name="options"></slot>
|
||||
<template #empty>
|
||||
<slot v-if="$scopedSlots.empty" name="empty"/>
|
||||
<div v-else class="no-data" style="height:160px;"/>
|
||||
</template>
|
||||
</el-table>
|
||||
<div class="pagination newPagination" v-if="isShowPagination">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page.sync="page.current"
|
||||
:total="page.total"
|
||||
:page-size="page.size"
|
||||
v-bind="$attrs"
|
||||
:page-sizes="pageSizes"
|
||||
:layout="layout"
|
||||
:pager-count="page.pagerCount"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleChange">
|
||||
<div class="paginationPre">
|
||||
<el-checkbox v-if="isHasPaginationBtnsSlot" :disabled="!tableData.length" :indeterminate="isIndeterminate"
|
||||
:value="checkAll"
|
||||
@click.native="toggleAllSelection">全选
|
||||
</el-checkbox>
|
||||
<slot name="pagination"/>
|
||||
<div class="pagination-btns">
|
||||
<slot name="paginationBtns"></slot>
|
||||
</div>
|
||||
<div class="paginationPre-total" :style="{marginLeft: isHasPaginationBtnsSlot ? '24px' : 0}">共<label class="color-primary">{{ page.total }}</label>条记录
|
||||
</div>
|
||||
</div>
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'dayjs'
|
||||
import dict from "../../meta/js/dict"
|
||||
|
||||
let renderSlot = {
|
||||
functional: true,
|
||||
props: {
|
||||
row: Object,
|
||||
render: Function,
|
||||
index: Number,
|
||||
column: {type: Object, default: null},
|
||||
},
|
||||
render: (h, data) => {
|
||||
const params = {
|
||||
row: data.props.row,
|
||||
index: data.props.index
|
||||
}
|
||||
|
||||
if (data.props.column) {
|
||||
params.column = data.props.column
|
||||
}
|
||||
|
||||
return data.props.render(h, params)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AiTable',
|
||||
props: {
|
||||
colConfigs: Array,
|
||||
tableData: Array,
|
||||
current: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
isShowPagination: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
total: {
|
||||
type: Number
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'slot,->, prev, pager, next, sizes, jumper'
|
||||
},
|
||||
stripe: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tooltipEffect: {
|
||||
type: String
|
||||
},
|
||||
tableSize: {
|
||||
type: String
|
||||
},
|
||||
tableRef: String,
|
||||
dict: {default: () => dict},
|
||||
pagerCount: {default: 5},
|
||||
pageSizes: {default: () => [10, 20, 50, 100]}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
chooseList: []
|
||||
}
|
||||
},
|
||||
components: {renderSlot},
|
||||
computed: {
|
||||
refName() {
|
||||
return this.tableRef || `aiTable${new Date().getTime()}`
|
||||
},
|
||||
isShowBorder() {
|
||||
return !!this.$attrs.border || this.$attrs.border === ''
|
||||
},
|
||||
|
||||
isHasPaginationBtnsSlot() {
|
||||
return this.$slots.paginationBtns
|
||||
},
|
||||
|
||||
page() {
|
||||
return {
|
||||
current: this.current,
|
||||
size: this.size,
|
||||
total: this.total,
|
||||
pagerCount: this.pagerCount
|
||||
}
|
||||
},
|
||||
isIndeterminate() {
|
||||
return this.chooseList.length > 0 && this.chooseList.length < this.tableData.length
|
||||
},
|
||||
checkAll() {
|
||||
return this.chooseList.length == this.tableData.length && this.tableData !== 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange(e) {
|
||||
this.$emit('update:current', e)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$emit('getList')
|
||||
})
|
||||
},
|
||||
handleSizeChange(e) {
|
||||
this.$emit('update:size', e)
|
||||
this.$nextTick(() => {
|
||||
this.$emit('getList')
|
||||
})
|
||||
},
|
||||
handleSelectionChange(e) {
|
||||
this.chooseList = e
|
||||
this.$emit('handleSelectionChange', e)
|
||||
},
|
||||
getValue(colConfig, row) {
|
||||
if (this.isFunction(colConfig.formart)) {
|
||||
return colConfig.formart.call(this, row[colConfig.prop])
|
||||
}
|
||||
|
||||
if (colConfig.dateFormat) {
|
||||
return moment(row[colConfig.prop]).format(colConfig.dateFormat)
|
||||
}
|
||||
|
||||
return this.isInvalidValue(row[colConfig.prop])
|
||||
},
|
||||
isInvalidValue(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
isFunction(fun) {
|
||||
return typeof fun === 'function'
|
||||
},
|
||||
/**
|
||||
* 表格方法代理
|
||||
*/
|
||||
clearSelection() {
|
||||
this.$refs[this.refName].clearSelection()
|
||||
},
|
||||
toggleRowSelection() {
|
||||
this.$refs[this.refName].toggleRowSelection(...arguments)
|
||||
},
|
||||
toggleAllSelection() {
|
||||
this.$refs[this.refName].toggleAllSelection()
|
||||
},
|
||||
toggleRowExpansion() {
|
||||
this.$refs[this.refName].toggleRowExpansion(...arguments)
|
||||
},
|
||||
setCurrentRow() {
|
||||
this.$refs[this.refName].setCurrentRow(...arguments)
|
||||
},
|
||||
clearSort() {
|
||||
this.$refs[this.refName].clearSort()
|
||||
},
|
||||
clearFilter() {
|
||||
this.$refs[this.refName].clearFilter(...arguments)
|
||||
},
|
||||
doLayout() {
|
||||
this.$refs[this.refName].doLayout()
|
||||
},
|
||||
sort() {
|
||||
this.$refs[this.refName].sort(...arguments)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ai-table {
|
||||
.color-primary {
|
||||
color: $primaryColor;
|
||||
}
|
||||
|
||||
::v-deep .ai-header__border .ai-table__header {
|
||||
border-bottom: 1px solid $borderColor !important;
|
||||
border-right: 1px solid $borderColor !important;
|
||||
}
|
||||
|
||||
::v-deep .el-table--border {
|
||||
border: 1px solid $borderColor;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
::v-deep .el-table {
|
||||
color: #222;
|
||||
|
||||
.caret-wrapper {
|
||||
height: 24px;
|
||||
|
||||
.ascending {
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.descending {
|
||||
bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #555
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .cell {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
::v-deep .el-table__header {
|
||||
th {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
tr {
|
||||
.cell {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
.cell {
|
||||
padding-left: 40px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-table__body {
|
||||
tr td:first-child .cell {
|
||||
padding-left: 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-table__fixed-right {
|
||||
.el-table__body {
|
||||
tr td:first-child .cell {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .ai-table__header {
|
||||
border-bottom: none;
|
||||
background: #F3F4F5;
|
||||
}
|
||||
|
||||
::v-deep.el-pager {
|
||||
li.active + li {
|
||||
border-left: 1px solid $borderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.newPagination {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
padding: 0 40px !important;
|
||||
|
||||
.el-pagination {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.paginationPre {
|
||||
display: flex;
|
||||
height: 28px;
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
align-items: center;
|
||||
|
||||
.pagination-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: $primaryColor !important;
|
||||
|
||||
::v-deep span, ::v-deep div {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: $primaryColor !important;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.paginationPre-total {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
|
||||
label {
|
||||
padding: 0 2px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
& > * + * {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination button, .el-pagination span:not([class*=suffix]) {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
::v-deep.el-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-checkbox__input, .el-checkbox__inner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
min-width: 0 !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.el-checkbox__label {
|
||||
font-size: 12px;
|
||||
color: #222222;
|
||||
height: auto !important;
|
||||
line-height: 1 !important;
|
||||
padding-left: 3px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai-table__noborder {
|
||||
::v-deep .el-table td, ::v-deep .el-table th.is-center {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-table::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background: #F5F6F9;
|
||||
}
|
||||
|
||||
::v-deep .el-table__fixed-right::before, ::v-deep .el-table__fixed::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
242
ui/packages/basic/AiTableSelect.vue
Normal file
242
ui/packages/basic/AiTableSelect.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<section class="AiTableSelect">
|
||||
<el-row type="flex">
|
||||
<ai-table v-if="isShowPagination" ref="PendingTable" :tableData="tableData" :total="page.total" :current.sync="page.current"
|
||||
:size.sync="page.size" class="fill" border height="330px" @getList="getTableData" tableSize="mini"
|
||||
:col-configs="[{slot: 'resident'}]" layout="slot,->, prev, pager, next, jumper" :pagerCount="5">
|
||||
<el-table-column slot="resident">
|
||||
<template #header>
|
||||
<b v-text="tableTitle"/>
|
||||
<el-input class="fill searchbar" v-model="search[searchKey]" size="small" placeholder="搜索" clearable
|
||||
@change="page.current=1,getTableData()"/>
|
||||
</template>
|
||||
<template slot-scope="{row}">
|
||||
<slot name="pending" v-if="$scopedSlots.pending" :row="row"/>
|
||||
<el-row v-else type="flex" justify="space-between" @click.native="handleSelect(row)" class="toggle"
|
||||
:class="{selected:findSelected(row)>-1}">
|
||||
<span v-text="row[nodeName]"/>
|
||||
<slot name="extra" v-if="$scopedSlots.extra" :row="row"/>
|
||||
<span v-else v-text="getExtra(row)"/>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ai-table>
|
||||
<ai-table v-else ref="PendingTable" :tableData="tableData" class="fill" border height="330px" @getList="getTableData" tableSize="mini"
|
||||
:col-configs="[{slot: 'resident'}]" :isShowPagination="false">
|
||||
<el-table-column slot="resident">
|
||||
<template #header>
|
||||
<b v-text="tableTitle"/>
|
||||
<el-input class="fill searchbar" v-model="search[searchKey]" size="small" placeholder="搜索" clearable
|
||||
@change="page.current=1,getTableData()"/>
|
||||
</template>
|
||||
<template slot-scope="{row}">
|
||||
<slot name="pending" v-if="$scopedSlots.pending" :row="row"/>
|
||||
<el-row v-else type="flex" justify="space-between" @click.native="handleSelect(row)" class="toggle"
|
||||
:class="{selected:findSelected(row)>-1}">
|
||||
<span v-text="row[nodeName]"/>
|
||||
<slot name="extra" v-if="$scopedSlots.extra" :row="row"/>
|
||||
<span v-else v-text="getExtra(row)"/>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ai-table>
|
||||
<ai-table :tableData="selected" :col-configs="[{slot:'resident'}]" :isShowPagination="false" border
|
||||
height="330px" tableSize="mini" class="el-table--scrollable-y">
|
||||
<el-table-column slot="resident">
|
||||
<template #header>
|
||||
<b v-text="`已选择`"/>
|
||||
<el-button type="text" @click="selected=[]">清空</el-button>
|
||||
</template>
|
||||
<template slot-scope="{row,$index}">
|
||||
<slot name="selected" v-if="$scopedSlots.selected" :row="row" :index="$index"/>
|
||||
<el-row v-else type="flex" align="middle" justify="space-between">
|
||||
<div v-text="[row[nodeName], getExtra(row)].join(' ')"/>
|
||||
<el-button type="text" @click="selected.splice($index,1)">删除</el-button>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ai-table>
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 智能列表选择器
|
||||
* @displayName AiTableSelect
|
||||
*/
|
||||
export default {
|
||||
name: "AiTableSelect",
|
||||
model: {
|
||||
event: "change",
|
||||
prop: "value"
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 接口方法类:必填
|
||||
*/
|
||||
instance: {type: Function, required: true},
|
||||
/**
|
||||
* 接口方法地址:必填
|
||||
*/
|
||||
action: {default: "", required: true},
|
||||
/**
|
||||
* 选择表格标题
|
||||
*/
|
||||
tableTitle: {default: "选择列表"},
|
||||
/**
|
||||
* 选择项
|
||||
* @model
|
||||
*/
|
||||
value: {default: ""},
|
||||
/**
|
||||
* 选择项绑定key
|
||||
*/
|
||||
nodeKey: {default: "id"},
|
||||
/**
|
||||
* 选择项绑定展示值:必填
|
||||
*/
|
||||
nodeName: {default: "name"},
|
||||
/**
|
||||
* 是否多选
|
||||
*/
|
||||
multiple: Boolean,
|
||||
/**
|
||||
* 返回值为选择的对象而不是id
|
||||
*/
|
||||
valueObj: Boolean,
|
||||
/**
|
||||
* 扩展字段,用于右侧展示
|
||||
*/
|
||||
extra: {default: ""},
|
||||
/**
|
||||
* 搜索字段,用于搜索框
|
||||
*/
|
||||
searchKey: {default: "con"},
|
||||
/**
|
||||
* 是否分页
|
||||
*/
|
||||
isShowPagination: {default: true}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: {total: 0, current: 1, size: 10},
|
||||
search: {},
|
||||
tableData: [],
|
||||
selected: [],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
action(v) {
|
||||
v && (this.page.current = 1, this.getTableData())
|
||||
},
|
||||
selected: {
|
||||
deep: true, handler(v) {
|
||||
let ids = v.map(e => e[this.nodeKey] || e).filter(e => !!e)
|
||||
this.$emit("change", this.valueObj ? v : this.multiple ? ids : ids?.toString())
|
||||
this.$emit("select", v)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getExtra(row) {
|
||||
let {extra} = this
|
||||
return extra ? row[extra] : this.idCardNoUtil.hideId(row.idNumber)
|
||||
},
|
||||
initValue() {
|
||||
let unwatch = this.$watch('value', (v) => {
|
||||
if (this.selected.length > 0) unwatch && unwatch()
|
||||
else if (!!v) {
|
||||
this.selected = this.multiple ? [v].flat().filter(e => !!e) || [] : (v?.split(",") || [])
|
||||
unwatch && unwatch()
|
||||
}
|
||||
}, {immediate: true})
|
||||
},
|
||||
getTableData() {
|
||||
let {page, search, action} = this
|
||||
this.instance?.post(action, null, {
|
||||
params: {...page, ...search}
|
||||
}).then(res => {
|
||||
if (res?.data) {
|
||||
this.tableData = res.data.records || res.data || []
|
||||
this.isShowPagination && (this.page.total = res.data.total)
|
||||
}
|
||||
})
|
||||
},
|
||||
handleSelect(row) {
|
||||
let index = this.findSelected(row)
|
||||
if (index > -1) {
|
||||
this.selected.splice(index, 1)
|
||||
} else {
|
||||
if (this.multiple) {
|
||||
this.selected.push(row)
|
||||
} else {
|
||||
this.selected = [row]
|
||||
}
|
||||
}
|
||||
this.$forceUpdate()
|
||||
},
|
||||
findSelected(item) {
|
||||
let {nodeKey} = this
|
||||
return this.selected?.findIndex(e => e[nodeKey] == item[nodeKey])
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$set(this.search, this.searchKey, "")
|
||||
this.initValue()
|
||||
this.getTableData()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep.AiTableSelect {
|
||||
.el-row {
|
||||
width: 100%;
|
||||
|
||||
.ai-table + .ai-table {
|
||||
margin-left: 16px;
|
||||
width: 400px;
|
||||
|
||||
.ai-table__header > .cell {
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
color: $primaryColor;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-table__header {
|
||||
padding: 0 !important;
|
||||
|
||||
& > .cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.searchbar {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.newPagination {
|
||||
height: 32px;
|
||||
padding: 0 !important;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.ai-table__cell {
|
||||
.el-button--text {
|
||||
padding: 0 8px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
163
ui/packages/basic/AiTitle.vue
Normal file
163
ui/packages/basic/AiTitle.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<section class="AiTitle" :class="{ 'bottomBorder': isShowBottomBorder, AiTitleSub: isHasSub}">
|
||||
<i class="iconfont iconBack_Large" v-if="isShowBack" @click="onBackBtnClick"/>
|
||||
<div class="fill">
|
||||
<div class="ailist-title">
|
||||
<div class="ailist-title__left">
|
||||
<h2>{{ title }}</h2>
|
||||
<div v-if="isShowIM" class="openIM iconfont iconGroup_IM" @click="openIM"></div>
|
||||
</div>
|
||||
<div class="ailist-title__right">
|
||||
<ai-area
|
||||
v-if="isShowArea"
|
||||
:instance="instance"
|
||||
v-bind="$attrs"
|
||||
:value="value"
|
||||
v-on="$listeners"
|
||||
:areaLevel="areaLevel"
|
||||
:hideLevel="hideLevel"
|
||||
:valueLevel="valueLevel"
|
||||
:disabled="disabled"/>
|
||||
<div class="aititle-right__btns">
|
||||
<slot name="rightBtn"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtitle" v-if="$scopedSlots.sub">
|
||||
<slot name="sub"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AiTitle',
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change'
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
instance: {
|
||||
type: Function
|
||||
},
|
||||
areaLevel: [String, Number],
|
||||
hideLevel: {default: 2},
|
||||
valueLevel: [String, Number],
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: String
|
||||
},
|
||||
isShowIM: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isShowArea: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
openIM: {
|
||||
type: Function
|
||||
},
|
||||
isShowBack: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isShowBottomBorder: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
classic: Boolean
|
||||
},
|
||||
|
||||
computed: {
|
||||
isHasSub() {
|
||||
return this.$slots.sub
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onBackBtnClick() {
|
||||
this.$emit('onBackClick')
|
||||
this.$emit('back')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.AiTitle {
|
||||
display: flex;
|
||||
|
||||
.ailist-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 48px;
|
||||
|
||||
.ailist-title__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > i {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #222;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.openIM {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ailist-title__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
::v-deep.el-button {
|
||||
margin-left: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.AiTitleSub {
|
||||
height: auto;
|
||||
padding: 16px 0;
|
||||
|
||||
.ailist-title {
|
||||
height: auto;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.bottomBorder {
|
||||
border-bottom: 1px solid #D8DCE3;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
width: 100%;
|
||||
color: #888888;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.iconBack_Large {
|
||||
line-height: 48px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
ui/packages/basic/AiTransSpeech.vue
Normal file
44
ui/packages/basic/AiTransSpeech.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<el-row type="flex" align="middle">
|
||||
<el-button type="text" size="small" :loading="loading" icon="iconfont iconClock" title="语音播报"
|
||||
@click="getSpeechByContent"/>
|
||||
<ai-audio v-if="speech" :src="speech" skin="flat"/>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AiTransSpeech",
|
||||
props: {
|
||||
instance: {type: Function, required: true},
|
||||
content: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
speech: "",
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getSpeechByContent() {
|
||||
this.loading = true
|
||||
this.instance.post("/app/msc/transToSpeech", null, {
|
||||
params: {
|
||||
fileName: "demo",
|
||||
words: this.content
|
||||
}
|
||||
}).then(res => {
|
||||
this.loading = false
|
||||
if (res && res.data) {
|
||||
let url = res.data.join("")
|
||||
this.speech = url.substring(0, url.indexOf(";"))
|
||||
}
|
||||
}).catch(() => this.loading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
514
ui/packages/basic/AiUploader.vue
Normal file
514
ui/packages/basic/AiUploader.vue
Normal file
@@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<section class="uploader">
|
||||
<el-upload
|
||||
action
|
||||
multiple
|
||||
ref="upload"
|
||||
:class="{validError:!validateState}"
|
||||
:http-request="submitUpload"
|
||||
:on-remove="handleRemove"
|
||||
:on-change="handleChange"
|
||||
:before-upload="onBeforeUpload"
|
||||
:file-list="fileList"
|
||||
:limit="limit"
|
||||
:disabled="disabled"
|
||||
:list-type="isImg ? 'picture-card' : 'text'"
|
||||
:accept="accept"
|
||||
:show-file-list="!isSingle"
|
||||
:on-preview="handlePictureCardPreview"
|
||||
:auto-upload="isAutoUpload"
|
||||
:on-exceed="handleExceed">
|
||||
<template v-if="!disabled">
|
||||
<template v-if="hasUploaded&&isSingle">
|
||||
<div class="fileItem">
|
||||
<div class="uploadFile" @click.stop>
|
||||
<ai-icon type="svg" :icon="uploadFile.icon"/>
|
||||
<div class="info">
|
||||
<span v-text="uploadFile.name"/>
|
||||
<span class="size" v-text="uploadFile.size"/>
|
||||
</div>
|
||||
</div>
|
||||
<el-button>重新选择</el-button>
|
||||
<el-button v-if="clearable" plain type="danger" @click.stop="handleClear">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="limit > fileList.length">
|
||||
<slot v-if="hasTriggerSlot" name="trigger"/>
|
||||
<div v-else class="uploaderBox">
|
||||
<span class="iconfont" :class="isImg ? 'iconPhoto' : 'iconAdd'"/>
|
||||
<p>上传{{ isImg ? '图片' : '附件' }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div slot="tip" class="el-upload__tip" v-if="showTips">
|
||||
<p v-if="fileType === 'img' && !acceptType && !hasTipsSlot">最多上传{{
|
||||
limit
|
||||
}}张图片,单个文件最大10MB,支持jpg、jpeg、png格式</p>
|
||||
<p v-if="fileType === 'file' && !acceptType && !hasTipsSlot">最多上传{{ limit }}个附件,单个文件最大10MB</p>
|
||||
<p v-if="fileType === 'file' && !acceptType && !hasTipsSlot">
|
||||
支持.zip、.rar、.doc、.docx、.xls、.xlsx、.ppt、.pptx、.pdf、.txt、.jpg、.png格式</p>
|
||||
<p>
|
||||
<slot name="tips" v-if="hasTipsSlot"></slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-dialog :visible.sync="dialog" title="图片预览编辑器" :modal="false" :show-close="false" append-to-body>
|
||||
<vue-cropper
|
||||
ref="cropper"
|
||||
style="height: 400px;"
|
||||
:img="fileList.length ? fileList[0].url : ''" v-bind="crop"/>
|
||||
<div style="text-align: center;margin-top: 10px;">
|
||||
<el-radio-group v-if="crop.fixed" size="small" v-model="currFixedIndex" @change="onFixedChange"
|
||||
style="margin-right: 8px;">
|
||||
<el-radio-button
|
||||
:label="0"
|
||||
size="small">
|
||||
1.6:1
|
||||
</el-radio-button>
|
||||
<el-radio-button
|
||||
:label="1"
|
||||
size="small">
|
||||
4:3
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button size="small" circle icon="el-icon-refresh-right" @click="$refs.cropper.rotateRight()"></el-button>
|
||||
<el-button size="small" circle icon="el-icon-refresh-left" @click="$refs.cropper.rotateLeft()"></el-button>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<el-popconfirm title="是否关闭图片预览器,并上传图片?" @confirm="previewCrop">
|
||||
<el-button slot="reference" type="primary">保存</el-button>
|
||||
</el-popconfirm>
|
||||
<el-button @click="onClose">关闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<div class="images" v-viewer="{movable: true}" v-show="false">
|
||||
<img v-for="(item, index) in imgList" :src="item" :key="index" alt="">
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {VueCropper} from 'vue-cropper'
|
||||
import 'viewerjs/dist/viewer.css'
|
||||
import Viewer from 'v-viewer'
|
||||
import Vue from "vue";
|
||||
|
||||
Viewer.setDefaults({
|
||||
zIndex: 20170
|
||||
})
|
||||
Vue.use(Viewer)
|
||||
export default {
|
||||
name: 'AiUploader',
|
||||
components: {VueCropper},
|
||||
inject: {
|
||||
elFormItem: {default: ""},
|
||||
elForm: {default: ''},
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change'
|
||||
},
|
||||
props: {
|
||||
value: {default: () => []},
|
||||
url: {
|
||||
type: String,
|
||||
default: '/admin/file/add'
|
||||
},
|
||||
isShowTip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isWechat: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
instance: Function,
|
||||
acceptType: {type: String},
|
||||
fileType: {type: String, default: 'img'},
|
||||
limit: {type: Number, default: 9},
|
||||
disabled: {type: Boolean, default: false},
|
||||
isCrop: {type: Boolean, default: false},
|
||||
cropOps: Object,
|
||||
isImport: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
clearable: {default: true},
|
||||
valueIsUrl: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fileList: [],
|
||||
dialog: false,
|
||||
currFixedIndex: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(v) {
|
||||
this.dispatch('ElFormItem', 'el.form.change', [v]);
|
||||
if (v?.length > 0) {
|
||||
this.fileList = this.valueIsUrl ? v?.split(",")?.map(url => ({url})) : [...v]
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isImg() {
|
||||
return this.fileType === 'img'
|
||||
},
|
||||
validateState() {
|
||||
return ['', 'success'].includes(this.elFormItem?.validateState)
|
||||
},
|
||||
isAutoUpload() {
|
||||
return !(this.isCrop || this.isImport);
|
||||
},
|
||||
hasTipsSlot() {
|
||||
return this.$slots.tips
|
||||
},
|
||||
hasTriggerSlot() {
|
||||
return this.$slots.trigger
|
||||
},
|
||||
accept() {
|
||||
if (this.acceptType) {
|
||||
return this.acceptType
|
||||
}
|
||||
|
||||
return this.isImg ? '.jpg,.png,.jpeg' : '.zip,.rar,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt,.jpg,.png,.mp4'
|
||||
},
|
||||
crop() {
|
||||
return {
|
||||
autoCrop: true,
|
||||
outputType: 'png',
|
||||
fixedBox: false,
|
||||
fixed: true,
|
||||
fixedNumber: [1.6, 1],
|
||||
width: 0,
|
||||
height: 0,
|
||||
...this.cropOps
|
||||
}
|
||||
},
|
||||
imgList() {
|
||||
return this.fileList.map(v => v.url)
|
||||
},
|
||||
isSingle() {
|
||||
return this.limit == 1 && this.isImport
|
||||
},
|
||||
showTips() {
|
||||
return this.isShowTip || this.$slots.tips
|
||||
},
|
||||
hasUploaded() {
|
||||
return this.fileList?.length > 0
|
||||
},
|
||||
uploadFile() {
|
||||
let file = this.fileList?.[0],
|
||||
size = Number(file.size),
|
||||
icon = "iconTxt"
|
||||
//显示大小
|
||||
if (size > Math.pow(1024, 2)) {
|
||||
size = (size / Math.pow(1024, 2)).toFixed(1) + 'MB'
|
||||
} else {
|
||||
size = (size / 1024).toFixed(1) + 'KB'
|
||||
}
|
||||
//显示图标
|
||||
if (/\.(xls|xlsx)$/.test(file.name)) {
|
||||
icon = "iconExcel"
|
||||
} else if (/\.(zip)$/.test(file.name)) {
|
||||
icon = "iconZip"
|
||||
} else if (/\.(rar)$/.test(file.name)) {
|
||||
icon = "iconRar"
|
||||
} else if (/\.(png)$/.test(file.name)) {
|
||||
icon = "iconPng"
|
||||
} else if (/\.(pptx|ppt)$/.test(file.name)) {
|
||||
icon = "iconPPT"
|
||||
} else if (/\.(doc|docx)$/.test(file.name)) {
|
||||
icon = "iconWord"
|
||||
}
|
||||
|
||||
return {...file, size, icon}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onFixedChange(e) {
|
||||
this.fixedNumber = e === 0 ? [1.6, 1] : [4, 3]
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.cropper.goAutoCrop()
|
||||
})
|
||||
},
|
||||
handleChange(file, fileList) {
|
||||
if (this.isImport) {
|
||||
if (!this.onOverSize(file)) {
|
||||
this.fileList = []
|
||||
return false
|
||||
}
|
||||
this.fileList = fileList
|
||||
this.emitChange(fileList)
|
||||
return false
|
||||
}
|
||||
if (this.isCrop) {
|
||||
if (file.raw.type === 'image/gif') {
|
||||
this.$message.error(`不支持gif格式的图片`)
|
||||
this.fileList = []
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (!this.onOverSize(file)) {
|
||||
this.fileList = []
|
||||
|
||||
return false
|
||||
}
|
||||
this.dialog = true
|
||||
this.fileList = fileList
|
||||
} else {
|
||||
this.fileList = fileList
|
||||
}
|
||||
},
|
||||
handleExceed(files) {
|
||||
if (this.isSingle && files[0]) {
|
||||
this.$refs.upload?.clearFiles()
|
||||
this.$refs.upload?.handleStart(files[0])
|
||||
} else this.$message.warning(`最多上传${this.limit}个${this.isImg ? '图片' : '文件'}`)
|
||||
},
|
||||
handlePictureCardPreview(file) {
|
||||
if (this.fileType !== 'img') return
|
||||
|
||||
const index = this.imgList.indexOf(file.url)
|
||||
const viewer = this.$el.querySelector('.images').$viewer
|
||||
viewer.view(index)
|
||||
},
|
||||
|
||||
handleRemove(file, fileList) {
|
||||
this.fileList = fileList
|
||||
this.emitChange(fileList)
|
||||
},
|
||||
emitChange(files) {
|
||||
this.$emit('change', this.valueIsUrl ? files?.map(e => e.url)?.toString() : files)
|
||||
},
|
||||
handleClear() {
|
||||
this.fileList = []
|
||||
},
|
||||
getExtension(name) {
|
||||
return name.substring(name.lastIndexOf('.'))
|
||||
},
|
||||
|
||||
onOverSize(e) {
|
||||
const isLt10M = e.size / 1024 / 1024 < this.maxSize
|
||||
const suffixName = this.getExtension(e.name)
|
||||
const suffixNameList = this.accept.split(',')
|
||||
|
||||
if (suffixNameList.indexOf(`${suffixName.toLowerCase()}`) === -1) {
|
||||
this.$message.error(`不支持该格式`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isLt10M) {
|
||||
this.$message.error(`${this.isImg ? '图片' : '文件'}大小不超过${this.maxSize}MB!`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
onBeforeUpload(event) {
|
||||
return this.onOverSize(event)
|
||||
},
|
||||
|
||||
onClose() {
|
||||
this.fileList = []
|
||||
this.dialog = false
|
||||
},
|
||||
|
||||
submitUpload(file) {
|
||||
let formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
this.instance.post(this.url, formData, {
|
||||
withCredentials: false
|
||||
}).then(res => {
|
||||
if (res?.code == 0) {
|
||||
if (this.isWechat) {
|
||||
this.emitChange([{
|
||||
...res.data.file,
|
||||
media: res.data.media
|
||||
}])
|
||||
this.fileList.forEach(item => {
|
||||
if (item.uid === file.file.uid) {
|
||||
item.id = res.data.file.id
|
||||
item.path = res.data.file.url
|
||||
item.url = res.data.file.url,
|
||||
item.media = res.data.media
|
||||
}
|
||||
})
|
||||
this.emitChange(this.fileList)
|
||||
this.$message.success('上传成功')
|
||||
return false
|
||||
}
|
||||
let data = res.data[0].split(';')
|
||||
this.fileList.forEach(item => {
|
||||
if (item.uid === file.file.uid) {
|
||||
item.id = data[1]
|
||||
item.path = data[0]
|
||||
item.url = data[0]
|
||||
}
|
||||
})
|
||||
this.emitChange(this.fileList)
|
||||
this.$message.success('上传成功')
|
||||
}
|
||||
})
|
||||
},
|
||||
previewCrop() {
|
||||
this.$refs.cropper.getCropBlob(data => {
|
||||
data.name = this.fileList[0].name;
|
||||
this.fileList[0].file = new window.File([data], data.name, {type: data.type})
|
||||
this.fileList[0].file.uid = this.fileList[0].uid
|
||||
this.submitUpload(this.fileList[0])
|
||||
this.dialog = false
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 表单验证
|
||||
* @param componentName
|
||||
* @param eventName
|
||||
* @param params
|
||||
*/
|
||||
dispatch(componentName, eventName, params) {
|
||||
let parent = this.$parent || this.$root;
|
||||
let name = parent.$options.componentName;
|
||||
|
||||
while (parent && (!name || name !== componentName)) {
|
||||
parent = parent.$parent;
|
||||
|
||||
if (parent) {
|
||||
name = parent.$options.componentName;
|
||||
}
|
||||
}
|
||||
if (parent) {
|
||||
parent.$emit.apply(parent, [eventName].concat(params));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uploader {
|
||||
line-height: 1;
|
||||
|
||||
::v-deep.el-upload {
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
::v-deep.validError {
|
||||
.el-button {
|
||||
border-color: #f46;
|
||||
color: #f46;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep.el-upload--picture-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
::v-deep .el-list-leave-active, ::v-deep .el-upload-list__item {
|
||||
transition: all 0s !important;
|
||||
}
|
||||
|
||||
::v-deep.el-upload-list--picture-card .el-upload-list__item {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::v-deep.el-upload--picture-card {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.el-upload__tip p {
|
||||
color: #999;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
::v-deep.fileItem {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #D0D4DC;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
cursor: default;
|
||||
|
||||
.uploadFile {
|
||||
text-align: start;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
color: #222;
|
||||
|
||||
.AiIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 8px;
|
||||
margin-right: 40px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.size {
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uploaderBox {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
line-height: 1;
|
||||
background: #F3F4F7;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #D0D4DC;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 32px;
|
||||
color: #8899bb;
|
||||
|
||||
&:hover {
|
||||
color: #8899bb;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding-top: 4px;
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
ui/packages/basic/AiWrapper.vue
Normal file
86
ui/packages/basic/AiWrapper.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="ai-wrapper">
|
||||
<ai-title class="w100" v-if="title" :title="title"/>
|
||||
<div class="ai-wrapper-content" :class="{border}">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiTitle from "./AiTitle";
|
||||
|
||||
export default {
|
||||
name: 'AiWrapper',
|
||||
components: {AiTitle},
|
||||
componentName: 'AiWrapper',
|
||||
|
||||
provide() {
|
||||
return {
|
||||
AiWrapper: this
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
'label-width': {
|
||||
type: String
|
||||
},
|
||||
|
||||
columnsNumber: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
border: Boolean,
|
||||
title: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
autoWidth() {
|
||||
return ((1 / this.columnsNumber) * 100).toFixed(2) + '%'
|
||||
},
|
||||
|
||||
autoLableWidth() {
|
||||
return this.labelWidth
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ai-wrapper {
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ai-wrapper-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
&.border {
|
||||
border-left: 1px solid $borderColor;
|
||||
border-top: 1px solid $borderColor;
|
||||
|
||||
.ai-info-item {
|
||||
border-bottom: 1px solid $borderColor;
|
||||
border-right: 1px solid $borderColor;
|
||||
margin-bottom: 0;
|
||||
line-height: 32px;
|
||||
|
||||
.ai-info-item__left {
|
||||
background: rgba(0, 0, 0, .03);
|
||||
padding-right: 16px;
|
||||
border-right: 1px solid $borderColor;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.el-textarea__inner {
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user