ui库和web端产品库合并版本(还需修复细节)

This commit is contained in:
2022-11-29 18:27:14 +08:00
parent 5e4bd93238
commit 8bf6c57668
151 changed files with 28267 additions and 49 deletions

View 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>

View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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() {
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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支持jpgjpegpng格式</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>

View 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>