This commit is contained in:
fengchenchen
2023-08-14 17:10:11 +08:00
commit 37f6ec895d
1145 changed files with 437754 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
<template>
<tooltip v-if="tips !== ''">
<template slot="title">{{ tips }}</template>
<avatar :size="avatarSize" :src="src" />
</tooltip>
<avatar v-else :size="avatarSize" :src="src" />
</template>
<script>
import Avatar from 'ant-design-vue/es/avatar'
import Tooltip from 'ant-design-vue/es/tooltip'
export default {
name: 'AvatarItem',
components: {
Avatar,
Tooltip
},
props: {
tips: {
type: String,
default: '',
required: false
},
src: {
type: String,
default: ''
}
},
data () {
return {
size: this.$parent.size
}
},
computed: {
avatarSize () {
return ((this.size !== 'mini') && this.size) || 20
}
},
watch: {
'$parent.size' (val) {
this.size = val
}
}
}
</script>
+98
View File
@@ -0,0 +1,98 @@
<!--
<template>
<div :class="[prefixCls]">
<ul>
<slot></slot>
<template v-for="item in filterEmpty($slots.default).slice(0, 3)"></template>
<template v-if="maxLength > 0 && filterEmpty($slots.default).length > maxLength">
<avatar-item :size="size">
<avatar :size="size !== 'mini' && size || 20" :style="excessItemsStyle">{{ `+${maxLength}` }}</avatar>
</avatar-item>
</template>
</ul>
</div>
</template>
-->
<script>
import Avatar from 'ant-design-vue/es/avatar'
import AvatarItem from './Item'
import { filterEmpty } from '@/components/_util/util'
export default {
AvatarItem,
name: 'AvatarList',
components: {
Avatar,
AvatarItem
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-avatar-list'
},
/**
* 头像大小 类型: large、small 、mini, default
* 默认值: default
*/
size: {
type: [String, Number],
default: 'default'
},
/**
* 要显示的最大项目
*/
maxLength: {
type: Number,
default: 0
},
/**
* 多余的项目风格
*/
excessItemsStyle: {
type: Object,
default: () => {
return {
color: '#f56a00',
backgroundColor: '#fde3cf'
}
}
}
},
data () {
return {}
},
methods: {
getItems (items) {
const classString = {
[`${this.prefixCls}-item`]: true,
[`${this.size}`]: true
}
if (this.maxLength > 0) {
items = items.slice(0, this.maxLength)
items.push((<Avatar size={ this.size } style={ this.excessItemsStyle }>{`+${this.maxLength}`}</Avatar>))
}
return items.map((item) => (
<li class={ classString }>{ item }</li>
))
}
},
render () {
const { prefixCls, size } = this.$props
const classString = {
[`${prefixCls}`]: true,
[`${size}`]: true
}
const items = filterEmpty(this.$slots.default)
const itemsDom = items && items.length ? <ul class={`${prefixCls}-items`}>{ this.getItems(items) }</ul> : null
return (
<div class={ classString }>
{ itemsDom }
</div>
)
}
}
</script>
+4
View File
@@ -0,0 +1,4 @@
import AvatarList from './List'
import './index.less'
export default AvatarList
+60
View File
@@ -0,0 +1,60 @@
@import "../index";
@avatar-list-prefix-cls: ~"@{ant-pro-prefix}-avatar-list";
@avatar-list-item-prefix-cls: ~"@{ant-pro-prefix}-avatar-list-item";
.@{avatar-list-prefix-cls} {
display: inline-block;
ul {
list-style: none;
display: inline-block;
padding: 0;
margin: 0 0 0 8px;
font-size: 0;
}
}
.@{avatar-list-item-prefix-cls} {
display: inline-block;
font-size: @font-size-base;
margin-left: -8px;
width: @avatar-size-base;
height: @avatar-size-base;
:global {
.ant-avatar {
border: 1px solid #fff;
cursor: pointer;
}
}
&.large {
width: @avatar-size-lg;
height: @avatar-size-lg;
}
&.small {
width: @avatar-size-sm;
height: @avatar-size-sm;
}
&.mini {
width: 20px;
height: 20px;
:global {
.ant-avatar {
width: 20px;
height: 20px;
line-height: 20px;
.ant-avatar-string {
font-size: 12px;
line-height: 18px;
}
}
}
}
}
+111
View File
@@ -0,0 +1,111 @@
<template>
<a-card :loading="loading" :body-style="{ padding: '20px 24px 8px' }" :bordered="false">
<div class="chart-card-header">
<div class="meta">
<span class="chart-card-title">{{ title }}</span>
<span class="chart-card-action">
<slot name="action"></slot>
</span>
</div>
<div class="total"><span>{{ total }}</span></div>
</div>
<div class="chart-card-content">
<div class="content-fix">
<slot></slot>
</div>
</div>
<div class="chart-card-footer">
<div class="field">
<slot name="footer"></slot>
</div>
</div>
</a-card>
</template>
<script>
export default {
name: 'ChartCard',
props: {
title: {
type: String,
default: ''
},
total: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="less" scoped>
.chart-card-header {
position: relative;
overflow: hidden;
width: 100%;
.meta {
position: relative;
overflow: hidden;
width: 100%;
color: rgba(0, 0, 0, .45);
font-size: 14px;
line-height: 22px;
}
}
.chart-card-action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.chart-card-footer {
border-top: 1px solid #e8e8e8;
padding-top: 9px;
margin-top: 8px;
> * {
position: relative;
}
.field {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
}
}
.chart-card-content {
margin-bottom: 12px;
position: relative;
height: 46px;
width: 100%;
.content-fix {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: #000;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
line-height: 38px;
height: 38px;
}
</style>
+103
View File
@@ -0,0 +1,103 @@
<template>
<span>
{{ lastTime | format }}
</span>
</template>
<script>
function fixedZero (val) {
return val * 1 < 10 ? `0${val}` : val
}
export default {
name: 'CountDown',
props: {
format: {
type: Function,
default: undefined
},
target: {
type: [Date, Number],
required: true
},
onEnd: {
type: Function,
default: () => {
}
}
},
data () {
return {
dateTime: '0',
originTargetTime: 0,
lastTime: 0,
timer: 0,
interval: 1000
}
},
filters: {
format (time) {
const hours = 60 * 60 * 1000
const minutes = 60 * 1000
const h = Math.floor(time / hours)
const m = Math.floor((time - h * hours) / minutes)
const s = Math.floor((time - h * hours - m * minutes) / 1000)
return `${fixedZero(h)}:${fixedZero(m)}:${fixedZero(s)}`
}
},
created () {
this.initTime()
this.tick()
},
methods: {
initTime () {
let lastTime = 0
let targetTime = 0
this.originTargetTime = this.target
try {
if (Object.prototype.toString.call(this.target) === '[object Date]') {
targetTime = this.target
} else {
targetTime = new Date(this.target).getTime()
}
} catch (e) {
throw new Error('invalid target prop')
}
lastTime = targetTime - new Date().getTime()
this.lastTime = lastTime < 0 ? 0 : lastTime
},
tick () {
const { onEnd } = this
this.timer = setTimeout(() => {
if (this.lastTime < this.interval) {
clearTimeout(this.timer)
this.lastTime = 0
if (typeof onEnd === 'function') {
onEnd()
}
} else {
this.lastTime -= this.interval
this.tick()
}
}, this.interval)
}
},
beforeUpdate () {
if (this.originTargetTime !== this.target) {
this.initTime()
}
},
beforeDestroy () {
clearTimeout(this.timer)
}
}
</script>
<style scoped>
</style>
+3
View File
@@ -0,0 +1,3 @@
import CountDown from './CountDown'
export default CountDown
+49
View File
@@ -0,0 +1,49 @@
<script>
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/StringUtil'
export default {
name: 'Ellipsis',
props: {
prefixCls: {
type: String,
default: 'ant-pro-ellipsis'
},
tooltip: {
type: Boolean,
default: true
},
length: {
type: Number,
default: 25
},
lines: {
type: Number,
default: 1
},
fullWidthRecognition: {
type: Boolean,
default: false
}
},
methods: {},
render () {
const { tooltip, length } = this.$props
let text = ''
// 处理没有default插槽时的特殊情况
if (this.$slots.default) {
text = this.$slots.default.map(vNode => vNode.text).join('')
}
// 判断是否显示 tooltip
if (tooltip && getStrFullLength(text) > length) {
return (
<a-tooltip>
<template slot="title">{text}</template>
<span>{cutStrByFullLength(text, this.length) + '…'}</span>
</a-tooltip>
)
} else {
return (<span>{text}</span>)
}
}
}
</script>
+3
View File
@@ -0,0 +1,3 @@
import Ellipsis from './Ellipsis'
export default Ellipsis
+136
View File
@@ -0,0 +1,136 @@
<template>
<a-date-picker
:show-time="false"
:open="open"
dropdownClassName="j-multiple-date-picker"
@openChange="handleOpenChange"
style="width: 100%"
>
<template slot="dateRender" slot-scope="current">
<div class="ant-calendar-date" @click.stop="clickCalendarDate(current)" :class="{'blue': getCurrentStyle(current)}">
{{current.date()}}
</div>
</template>
<template slot="renderExtraFooter" slot-scope="">
<a class="ant-calendar-ok-btn" @click="handleOk">确定</a>
</template>
</a-date-picker>
</template>
<script>
export default {
name: 'JMultipleDatePicker',
props: {
value: {
type: String,
default: ''
}
},
watch: {
value (val) {
if (!val) {
this.checkedValue = []
document.getElementsByClassName('ant-calendar-picker-input')[0].value = ''
this.$emit('change', '')
}
}
},
data () {
return {
open: false,
checkedValue: []
}
},
methods: {
handleOpenChange (open) {
console.log(open)
if (open) {
this.open = open
setTimeout(() => {
document.getElementsByClassName('ant-calendar-input')[0].value = this.checkedValue.join(',')
}, 100)
} else {
this.open = open
this.$nextTick(() => {
document.getElementsByClassName('ant-calendar-picker-input')[0].value = this.checkedValue.join(',')
})
}
},
getCurrentStyle (current) {
const currentStr = current.format('YYYY-MM-DD')
if (this.checkedValue.includes(currentStr)) {
return true
}
},
clickCalendarDate (current) {
const currentStr = current.format('YYYY-MM-DD')
if (!this.checkedValue.includes(currentStr)) {
console.log('加日期')
this.checkedValue.push(currentStr)
// event.target.className += ' blue'
} else {
console.log('减日期')
this.checkedValue = this.checkedValue.filter(item => item !== currentStr)
// console.log(event.target.className)
// // 选中日期不存在该日期,该日期有blue类时删掉该类
// if (event.target.className.split(' ').includes('blue')) {
// event.target.className = event.target.className.split(' ').filter(item => item !== 'blue').join(' ')
// }
}
document.getElementsByClassName('ant-calendar-input')[0].value = this.checkedValue.join(',')
},
handleOk () {
this.open = false
this.$nextTick(() => {
document.getElementsByClassName('ant-calendar-picker-input')[0].value = this.checkedValue.join(',')
})
this.$emit('change', this.checkedValue.join(','))
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped lang="less">
// 去掉日期面板中原本选中的样式
.j-multiple-date-picker {
.ant-calendar-today {
.ant-calendar-date {
background: transparent;
border: none;
}
}
.ant-calendar-selected-day {
.ant-calendar-date{
background: transparent;
}
}
}
// 设置日期面板中选中日期的样式
.blue {
background: #bae7ff !important;
}
// 设置输入框中的文字显示
/deep/ .ant-calendar-picker-input {
padding-right: 30px;
text-overflow: ellipsis;
overflow: hidden;
}
// 设置弹出层footer的布局
:global(.ant-calendar-footer-btn) {
display: flex !important;
justify-content: space-between !important;
flex-direction: row-reverse !important;
}
// 删除弹出层的今天按钮
:global(.ant-calendar-today-btn) {
display: none !important;
}
</style>
+232
View File
@@ -0,0 +1,232 @@
<template>
<div>
<template v-if="hasFile">
<template v-for="(file, fileKey) of [innerFile || {}]">
<div :key="fileKey" style="position: relative;">
<a-tooltip v-if="file.status==='uploading'" :title="`上传中(${Math.floor(file.percent)}%)`">
<a-icon type="loading"/>
<span style="margin-left:5px">上传中</span>
</a-tooltip>
<a-tooltip v-else-if="file.status==='done'" :title="file.name">
<a-icon type="paper-clip"/>
<span style="margin-left:5px">{{ ellipsisFileName }}</span>
</a-tooltip>
<a-tooltip v-else :title="file.message||'上传失败'">
<a-icon type="exclamation-circle" style="color:red;"/>
<span style="margin-left:5px">{{ ellipsisFileName }}</span>
</a-tooltip>
<template style="width: 30px">
<a-dropdown :trigger="['click']" placement="bottomRight" style="margin-left: 10px;">
<a-tooltip title="操作">
<a-icon
v-if="file.status!=='uploading'"
type="setting"
style="cursor: pointer;"/>
</a-tooltip>
<a-menu slot="overlay">
<a-menu-item v-if="originColumn.allowDownload !== false" @click="handleClickDownloadFile">
<span><a-icon type="download"/>&nbsp;下载</span>
</a-menu-item>
<a-menu-item v-if="originColumn.allowRemove !== false" @click="handleClickDeleteFile">
<span><a-icon type="delete"/>&nbsp;删除</span>
</a-menu-item>
<a-menu-item @click="handleMoreOperation(originColumn)">
<span><a-icon type="bars"/> 更多</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</div>
</template>
</template>
<a-upload
v-show="!hasFile"
name="file"
:data="{'isup': 1}"
:multiple="false"
:action="uploadAction"
:headers="uploadHeaders"
:showUploadList="false"
v-bind="cellProps"
@change="handleChangeUpload"
>
<a-button icon="upload">{{originColumn.btnText || '上传文件'}}</a-button>
</a-upload>
<j-file-pop ref="filePop" @ok="handleFileSuccess" :number="number"/>
</div>
</template>
<script>
import { getFileAccessHttpUrl } from '@api/manage'
import JVxeCellMixins from '@/components/jero/JVxeTable/mixins/JVxeCellMixins'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import JFilePop from '@/components/jero/minipop/JFilePop'
import JVxeUploadCell from '@/components/jero/JVxeTable/components/cells/JVxeUploadCell'
export default {
name: 'JVxeFileCell',
mixins: [JVxeCellMixins],
components: { JFilePop },
props: {},
data () {
return {
innerFile: null,
number: 0
}
},
computed: {
/** upload headers */
uploadHeaders () {
const { originColumn: col } = this
const headers = {}
if (col.token === true) {
headers['X-Access-Token'] = this.$ls.get(ACCESS_TOKEN)
}
return headers
},
/** 上传请求地址 */
uploadAction () {
if (!this.originColumn.action) {
return window._CONFIG.domianURL + '/sys/common/upload'
} else {
return this.originColumn.action
}
},
hasFile () {
return this.innerFile != null
},
ellipsisFileName () {
const length = 5
const file = this.innerFile
if (!file || !file.name) {
return ''
}
if (file.name.length > length) {
return file.name.substr(0, length) + '…'
}
return file.name
},
responseName () {
if (this.originColumn.responseName) {
return this.originColumn.responseName
} else {
return 'message'
}
}
},
watch: {
innerValue: {
immediate: true,
handler () {
if (this.innerValue) {
this.innerFile = this.innerValue
} else {
this.innerFile = null
}
}
}
},
methods: {
// 点击更多按钮
handleMoreOperation (originColumn) {
// update-begin-author:wangshuai date:20201021 for:LOWCOD-969 判断传过来的字段是否存在number,用于控制上传文件
if (originColumn.number) {
this.number = originColumn.number
} else {
this.number = 0
}
// update-end-author:wangshuai date:20201021 for:LOWCOD-969 判断传过来的字段是否存在number,用于控制上传文件
if (originColumn && originColumn.fieldExtendJson) {
const json = JSON.parse(originColumn.fieldExtendJson)
this.number = json.uploadnum ? json.uploadnum : 0
}
let path = ''
if (this.innerFile) {
path = this.innerFile.path
}
this.$refs.filePop.show('', path)
},
// 更多上传回调
handleFileSuccess (file) {
if (file) {
this.innerFile.path = file.path
this.handleChangeCommon(this.innerFile)
}
},
handleChangeUpload (info) {
// const { originColumn: col } = this
const { file } = info
const value = {
name: file.name,
type: file.type,
size: file.size,
status: file.status,
percent: file.percent
}
if (file.response) {
value.responseName = file.response[this.responseName]
}
if (file.status === 'done') {
if (typeof file.response.success === 'boolean') {
if (file.response.success) {
value.path = file.response[this.responseName]
this.handleChangeCommon(value)
} else {
value.status = 'error'
value.message = file.response.message || '未知错误'
}
} else {
// 考虑到如果设置action上传路径为非jeecg-boot后台,可能不会返回 success 属性的情况,就默认为成功
value.path = file.response[this.responseName]
this.handleChangeCommon(value)
}
} else if (file.status === 'error') {
value.message = file.response.message || '未知错误'
}
this.innerFile = value
},
handleClickDownloadFile () {
let { url, path } = this.innerFile || {}
if (!url || url.length === 0) {
if (path && path.length > 0) {
url = getFileAccessHttpUrl(path.split(',')[0])
}
}
if (url) {
window.open(url)
}
},
handleClickDeleteFile () {
this.handleChangeCommon(null)
}
},
// 【组件增强】注释详见:JVxeCellMixins.js
enhanced: {
switches: { visible: true },
getValue: value => JVxeUploadCell.enhanced.getValue(value),
setValue: value => JVxeUploadCell.enhanced.setValue(value)
}
}
</script>
<style scoped lang="less">
</style>
+245
View File
@@ -0,0 +1,245 @@
<template>
<div>
<template v-if="hasFile">
<template v-for="(file, fileKey) of [innerFile || {}]">
<div :key="fileKey" style="position: relative;">
<template v-if="!file || !(file['url'] || file['path'] || file['message'])">
<a-tooltip :title="'请稍后: ' + JSON.stringify (file) + ((file['url'] || file['path'] || file['message']))">
<a-icon type="loading"/>
</a-tooltip>
</template>
<template v-else-if="file['path']">
<img class="j-editable-image" :src="imgSrc" alt="无图片" @click="handleMoreOperation"/>
</template>
<a-tooltip v-else :title="file.message||'上传失败'" @click="handleClickShowImageError">
<a-icon type="exclamation-circle" style="color:red;"/>
</a-tooltip>
<template style="width: 30px">
<a-dropdown :trigger="['click']" placement="bottomRight" style="margin-left: 10px;">
<a-tooltip title="操作">
<a-icon
v-if="file.status!=='uploading'"
type="setting"
style="cursor: pointer;"/>
</a-tooltip>
<a-menu slot="overlay">
<a-menu-item v-if="originColumn.allowDownload !== false" @click="handleClickDownloadFile">
<span><a-icon type="download"/>&nbsp;下载</span>
</a-menu-item>
<a-menu-item v-if="originColumn.allowRemove !== false" @click="handleClickDeleteFile">
<span><a-icon type="delete"/>&nbsp;删除</span>
</a-menu-item>
<a-menu-item @click="handleMoreOperation(originColumn)">
<span><a-icon type="bars"/> 更多</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</div>
</template>
</template>
<a-upload
v-show="!hasFile"
name="file"
:data="{'isup': 1}"
:multiple="false"
:action="uploadAction"
:headers="uploadHeaders"
:showUploadList="false"
v-bind="cellProps"
@change="handleChangeUpload"
>
<a-button icon="upload">{{originColumn.btnText || '上传图片'}}</a-button>
</a-upload>
<j-file-pop ref="filePop" @ok="handleFileSuccess" :number="number"/>
</div>
</template>
<script>
import { getFileAccessHttpUrl } from '@api/manage'
import JVxeCellMixins from '@/components/jero/JVxeTable/mixins/JVxeCellMixins'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import JFilePop from '@/components/jero/minipop/JFilePop'
import JVxeUploadCell from '@/components/jero/JVxeTable/components/cells/JVxeUploadCell'
export default {
name: 'JVxeImageCell',
mixins: [JVxeCellMixins],
components: { JFilePop },
props: {},
data () {
return {
innerFile: null,
number: 0
}
},
computed: {
/** upload headers */
uploadHeaders () {
const { originColumn: col } = this
const headers = {}
if (col.token === true) {
headers['X-Access-Token'] = this.$ls.get(ACCESS_TOKEN)
}
return headers
},
/** 上传请求地址 */
uploadAction () {
if (!this.originColumn.action) {
return window._CONFIG.domianURL + '/sys/common/upload'
} else {
return this.originColumn.action
}
},
hasFile () {
return this.innerFile != null
},
/** 预览图片地址 */
imgSrc () {
if (this.innerFile) {
if (this.innerFile.url) {
return this.innerFile.url
} else if (this.innerFile.path) {
const path = this.innerFile.path.split(',')[0]
return getFileAccessHttpUrl(path)
}
}
return ''
},
responseName () {
if (this.originColumn.responseName) {
return this.originColumn.responseName
} else {
return 'message'
}
}
},
watch: {
innerValue: {
immediate: true,
handler () {
if (this.innerValue) {
this.innerFile = this.innerValue
} else {
this.innerFile = null
}
}
}
},
methods: {
// 点击更多按钮
handleMoreOperation (originColumn) {
// update-begin-author:wangshuai date:20201021 for:LOWCOD-969 判断传过来的字段是否存在number,用于控制上传文件
if (originColumn.number) {
this.number = originColumn.number
} else {
this.number = 0
}
// update-end-author:wangshuai date:20201021 for:LOWCOD-969 判断传过来的字段是否存在number,用于控制上传文件
if (originColumn && originColumn.fieldExtendJson) {
const json = JSON.parse(originColumn.fieldExtendJson)
this.number = json.uploadnum ? json.uploadnum : 0
}
let path = ''
if (this.innerFile) {
path = this.innerFile.path
}
this.$refs.filePop.show('', path, 'img')
},
// 更多上传回调
handleFileSuccess (file) {
if (file) {
this.innerFile.path = file.path
this.handleChangeCommon(this.innerFile)
}
},
// 弹出上传出错详细信息
handleClickShowImageError () {
const file = this.innerFile || null
if (file && file.message) {
this.$error({ title: '上传出错', content: '错误信息:' + file.message, maskClosable: true })
}
},
handleChangeUpload (info) {
// const { originColumn: col } = this
const { file } = info
const value = {
name: file.name,
type: file.type,
size: file.size,
status: file.status,
percent: file.percent
}
if (file.response) {
value.responseName = file.response[this.responseName]
}
if (file.status === 'done') {
if (typeof file.response.success === 'boolean') {
if (file.response.success) {
value.path = file.response[this.responseName]
this.handleChangeCommon(value)
} else {
value.status = 'error'
value.message = file.response.message || '未知错误'
}
} else {
// 考虑到如果设置action上传路径为非jeecg-boot后台,可能不会返回 success 属性的情况,就默认为成功
value.path = file.response[this.responseName]
this.handleChangeCommon(value)
}
} else if (file.status === 'error') {
value.message = file.response.message || '未知错误'
}
this.innerFile = value
},
handleClickDownloadFile () {
if (this.imgSrc) {
window.open(this.imgSrc)
}
},
handleClickDeleteFile () {
this.handleChangeCommon(null)
}
},
// 【组件增强】注释详见:JVxeCellMixins.js
enhanced: {
switches: { visible: true },
getValue: value => JVxeUploadCell.enhanced.getValue(value),
setValue: value => JVxeUploadCell.enhanced.setValue(value)
}
}
</script>
<style scoped lang="less">
.j-editable-image {
height: 32px;
max-width: 100px !important;
cursor: pointer;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.6;
}
}
</style>
@@ -0,0 +1,63 @@
<template>
<j-popup
v-bind="popupProps"
@input="handlePopupInput"
/>
</template>
<script>
import JVxeCellMixins, { dispatchEvent, vModel } from '@/components/jero/JVxeTable/mixins/JVxeCellMixins'
export default {
name: 'JVxePopupCell',
mixins: [JVxeCellMixins],
computed: {
popupProps () {
const { innerValue, originColumn: col, caseId, cellProps } = this
return {
...cellProps,
value: innerValue,
field: col.field || col.key,
code: col.popupCode,
orgFields: col.orgFields,
destFields: col.destFields,
groupId: caseId,
param: col.param,
sorter: col.sorter
}
}
},
methods: {
/** popup回调 */
handlePopupInput (value, others) {
const { row, originColumn: col } = this
// 存储输入的值
let popupValue = value
if (others && Object.keys(others).length > 0) {
Object.keys(others).forEach(key => {
const currentValue = others[key]
// 当前列直接赋值,其他列通过vModel赋值
if (key === col.key) {
popupValue = currentValue
} else {
vModel.call(this, currentValue, row, key)
}
})
}
this.handleChangeCommon(popupValue)
}
},
// 【组件增强】注释详见:JVxeCellMixins.js
enhanced: {
aopEvents: {
editActived (event) {
dispatchEvent.call(this, event, 'ant-input')
}
}
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,60 @@
<template>
<a-radio-group
:class="clazz"
:value="innerValue"
v-bind="cellProps"
@change="(e)=>handleChangeCommon(e.target.value)"
>
<a-radio
v-for="item of originColumn.options"
:key="item.value"
:value="item.value"
@click="$event=>handleRadioClick(item,$event)"
>{{ item.text }}
</a-radio>
</a-radio-group>
</template>
<script>
import JVxeCellMixins from '@/components/jero/JVxeTable/mixins/JVxeCellMixins'
export default {
name: 'JVxeRadioCell',
mixins: [JVxeCellMixins],
computed: {
scrolling () {
return !!this.renderOptions.scrolling
},
clazz () {
return {
'j-vxe-radio': true,
'no-animation': this.scrolling
}
}
},
methods: {
handleRadioClick (item) {
if (this.originColumn.allowClear === true) {
// 取消选择
if (item.value === this.innerValue) {
this.handleChangeCommon(null)
}
}
}
},
// 【组件增强】注释详见:JVxeCellMixins.js
enhanced: {
switches: { visible: true }
}
}
</script>
<style lang="less">
// 关闭动画,防止滚动时动态赋值出现问题
.j-vxe-radio.no-animation {
.ant-radio-inner,
.ant-radio-inner::after {
transition: none !important;
}
}
</style>
@@ -0,0 +1,262 @@
import debounce from 'lodash/debounce'
import { getAction } from '@/api/manage'
import { cloneObject } from '@/utils/util'
import { filterDictText } from '@/components/dict/JDictSelectUtil'
import { ajaxGetDictItems, getDictItemsFromCache } from '@/api/api'
import JVxeCellMixins, { dispatchEvent } from '@/components/jero/JVxeTable/mixins/JVxeCellMixins'
/** 公共资源 */
const common = {
/** value - label map,防止重复查询(刷新清空缓存) */
labelMap: new Map(),
/** 公共data */
data () {
return {
loading: false,
innerSelectValue: null,
innerOptions: []
}
},
/** 公共计算属性 */
computed: {
dict () {
return this.originColumn.dict
},
options () {
if (this.isAsync) {
return this.innerOptions
} else {
return this.originColumn.options || []
}
},
// 是否是异步模式
isAsync () {
const isAsync = this.originColumn.async
return (isAsync != null && isAsync !== '') ? !!isAsync : true
}
},
/** 公共属性监听 */
watch: {
innerValue: {
immediate: true,
handler (value) {
if (value == null || value === '') {
this.innerSelectValue = null
} else {
this.loadDataByValue(value)
}
}
},
dict () {
this.loadDataByDict()
}
},
/** 公共方法 */
methods: {
// 根据 value 查询数据,用于回显
async loadDataByValue (value) {
if (this.isAsync) {
if (this.innerSelectValue !== value) {
if (common.labelMap.has(value)) {
this.innerOptions = cloneObject(common.labelMap.get(value))
} else {
const { success, result } = await getAction(`/sys/dict/loadDictItem/${this.dict}`, { key: value })
if (success && result && result.length > 0) {
this.innerOptions = [{ value: value, text: result[0] }]
common.labelMap.set(value, cloneObject(this.innerOptions))
}
}
}
}
this.innerSelectValue = (value || '').toString()
},
// 初始化字典
async loadDataByDict () {
if (!this.isAsync) {
// 如果字典项集合有数据
if (!this.originColumn.options || this.originColumn.options.length === 0) {
// 根据字典Code, 初始化字典数组
let dictStr = ''
if (this.dict) {
const arr = this.dict.split(',')
if (arr[0].indexOf('where') > 0) {
const tbInfo = arr[0].split('where')
dictStr = tbInfo[0].trim() + ',' + arr[1] + ',' + arr[2] + ',' + encodeURIComponent(tbInfo[1])
} else {
dictStr = this.dict
}
if (this.dict.indexOf(',') === -1) {
// 优先从缓存中读取字典配置
const cache = getDictItemsFromCache(this.dict)
if (cache) {
this.innerOptions = cache
return
}
}
const { success, result } = await ajaxGetDictItems(dictStr, null)
if (success) {
this.innerOptions = result
}
}
}
}
}
}
}
// 显示组件,自带翻译
export const DictSearchSpanCell = {
name: 'JVxeSelectSearchSpanCell',
mixins: [JVxeCellMixins],
data () {
return {
...common.data.apply(this)
}
},
computed: {
...common.computed
},
watch: {
...common.watch
},
methods: {
...common.methods
},
render (h) {
return h('span', {}, [
filterDictText(this.innerOptions, this.innerSelectValue || this.innerValue)
])
}
}
// 请求id
let requestId = 0
// 输入选择组件
export const DictSearchInputCell = {
name: 'JVxeSelectSearchInputCell',
mixins: [JVxeCellMixins],
data () {
return {
...common.data.apply(this),
hasRequest: false,
scopedSlots: {
notFoundContent: () => {
if (this.loading) {
return <a-spin size="small"/>
} else if (this.hasRequest) {
return <div>没有查询到任何数据</div>
} else {
return <div>{this.tipsContent}</div>
}
}
}
}
},
computed: {
...common.computed,
tipsContent () {
return this.originColumn.tipsContent || '请输入搜索内容'
},
filterOption () {
if (this.isAsync) {
return null
}
return (input, option) => option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
},
watch: {
...common.watch
},
created () {
this.loadData = debounce(this.loadData, 300)// 消抖
},
methods: {
...common.methods,
loadData (value) {
const currentRequestId = ++requestId
this.loading = true
this.innerOptions = []
if (value == null || value.trim() === '') {
this.loading = false
this.hasRequest = false
return
}
// 字典code格式:table,text,code
this.hasRequest = true
getAction(`/sys/dict/loadDict/${this.dict}`, { keyword: value }).then(res => {
if (currentRequestId !== requestId) {
return
}
const { success, result, message } = res
if (success) {
this.innerOptions = result
result.forEach((item) => {
common.labelMap.set(item.value, [item])
})
} else {
this.$message.warning(message)
}
}).finally(() => {
this.loading = false
})
},
handleChange (selectedValue) {
this.innerSelectValue = selectedValue
this.handleChangeCommon(this.innerSelectValue)
},
handleSearch (value) {
if (this.isAsync) {
// 在输入时也应该开启加载,因为loadData加了消抖,所以会有800ms的用户主观上认为的卡顿时间
this.loading = true
if (this.innerOptions.length > 0) {
this.innerOptions = []
}
this.loadData(value)
}
},
renderOptionItem () {
const options = []
this.options.forEach(({ value, text, label, title, disabled }) => {
options.push(
<a-select-option key={value} value={value} disabled={disabled}>{text || label || title}</a-select-option>
)
})
return options
}
},
render () {
return (
<a-select
showSearch
allowClear
value={this.innerSelectValue}
filterOption={this.filterOption}
style="width: 100%"
{...this.cellProps}
onSearch={this.handleSearch}
onChange={this.handleChange}
scopedSlots={this.scopedSlots}
>
{this.renderOptionItem()}
</a-select>
)
},
// 【组件增强】注释详见:JVxeCellMixins.js
enhanced: {
aopEvents: {
editActived (event) {
dispatchEvent.call(this, event, 'ant-select')
}
}
}
}
+36
View File
@@ -0,0 +1,36 @@
import { installCell, JVXETypes } from '@/components/jero/JVxeTable'
import JVxePopupCell from './JVxePopupCell'
import { DictSearchInputCell, DictSearchSpanCell } from './JVxeSelectDictSearchCell'
import JVxeFileCell from './JVxeFileCell'
import JVxeImageCell from './JVxeImageCell'
import JVxeRadioCell from './JVxeRadioCell'
import JVxeSelectCell from '@comp/jero/JVxeTable/components/cells/JVxeSelectCell'
import JVxeTextareaCell from '@comp/jero/JVxeTable/components/cells/JVxeTextareaCell'
// 注册online组件
JVXETypes.input_pop = 'input_pop'
JVXETypes.list_multi = 'list_multi'
JVXETypes.sel_search = 'sel_search'
installCell(JVXETypes.input_pop, JVxeTextareaCell)
installCell(JVXETypes.list_multi, JVxeSelectCell)
installCell(JVXETypes.sel_search, JVxeSelectCell)
// 注册【popup】组件(普通封装方式)
JVXETypes.popup = 'popup'
installCell(JVXETypes.popup, JVxePopupCell)
// 注册【字典搜索下拉】组件(高级封装方式)
JVXETypes.selectDictSearch = 'select-dict-search'
installCell(JVXETypes.selectDictSearch, DictSearchInputCell, DictSearchSpanCell)
// 注册【文件上传】组件
JVXETypes.file = 'file'
installCell(JVXETypes.file, JVxeFileCell)
// 注册【图片上传】组件
JVXETypes.image = 'image'
installCell(JVXETypes.image, JVxeImageCell)
// 注册【单选框】组件
JVXETypes.radio = 'radio'
installCell(JVXETypes.radio, JVxeRadioCell)
+41
View File
@@ -0,0 +1,41 @@
####1._util包:存放自定义函数 详细见代码注释
####2.AvatarList:显示头像群并支持tip,用法参考src\views\Home.vue(如下图)
![输入图片说明](https://static.oschina.net/uploads/img/201904/12181253_O0Xi.png "在这里输入图片标题")
####3.chart包:存放各种图表相关的组件,条形图柱形图折线图等等 具体用法参考首页
####4.countDown包:一个倒计时组件,用法参考home页,简单描述,该组件有3个属性,
target(时间/毫秒数)必填,
format(function,该方法接收一个毫秒数的参数,用于格式化显示当前倒计时时间)非必填,
onEnd倒计时结束触发函数
![输入图片说明](https://static.oschina.net/uploads/img/201904/12182046_mwqJ.png "在这里输入图片标题")
####5.dict包:数据字典专用,用法参考文件夹下readme文件
####6.Ellipsis包:字符串截取组件,可以指定字符串的显示长度,并将全部内容显示到tip中,简单使用参考src\views\system\PermissionList.vue
####7.jero包:该包下自定义了很多列表/表单中用到的组件 参考包下readme文件
####8.jerobiz包:该包下定义了一些业务相关的组件,比如选择用户弹框,根据部门选择用户等等
####9.layouts+page包:系统页面布局相关组件,比如登陆进去之后页面顶部显示什么,底部显示什么,菜单点击触发多个tab的布局等等 一般情况不需要修改
####10.menun包:菜单组件,俩个,一个折叠菜单一个正常显示的菜单
####12.online包:该包下封装了online表单的相关组件,用于展示表单各种控件,验证表单等等,相关用法参考readme
####13.setting包:该包下封装了首页风格切换等功能如下图
![输入图片说明](https://static.oschina.net/uploads/img/201904/12190520_jySG.png "在这里输入图片标题")
####14.table包:一个二次封装的table组件,用于展示列表,参考readme
####15.tools包:
Breadcrumb.vue:面包屑二次封装,支持路由跳转
DetailList.vue:详情展示用法参考src\views\profile\advanced\Advanced.vue(效果如下图)
![输入图片说明](https://static.oschina.net/uploads/img/201904/12193954_Uar6.png "在这里输入图片标题")
````
个人认为该页面代码有两点值得学习:
1.vue provide/inject的使用
2.该页面css定义方式,只定义一个顶层class,其余样式都定义在其下,这样只要顶层class不和别的页面冲突,整个页面的样式都是唯一生效的
````
FooterToolBar.vue:fixed定位的底部,通过是否定义内部控件的属性slot="extra"决定是左浮动或是右浮动
HeaderNotice.vue:首页通知(如下图)
![输入图片说明](https://static.oschina.net/uploads/img/201904/12195340_fPe0.png "在这里输入图片标题")
HeaderInfo.vue:上下文字布局(如下图)
![输入图片说明](https://static.oschina.net/uploads/img/201904/12195638_dG5o.png "在这里输入图片标题")
Logo.vue:首页左上侧的log图
![输入图片说明](https://static.oschina.net/uploads/img/201904/12200908_ihv3.png "在这里输入图片标题")
UserMenu.vue:首页右上侧的内容
![输入图片说明](https://static.oschina.net/uploads/img/201904/12201226_laQK.png "在这里输入图片标题")
####16.trend包 趋势显示组件(如下图)
![输入图片说明](https://static.oschina.net/uploads/img/201904/12201600_Wo8K.png "在这里输入图片标题")
![corn表达式](https://oscimg.oschina.net/oscnet/661f9ac09016395f9f49286143af3241623.jpg)
![corn控件添加清除按钮](https://oscimg.oschina.net/oscnet/15096e49f2e29bd829e304d56770025d03c.jpg)
+41
View File
@@ -0,0 +1,41 @@
<template>
<div :class="[prefixCls, reverseColor && 'reverse-color' ]">
<span>
<slot name="term"></slot>
<span class="item-text">
<slot></slot>
</span>
</span>
<span :class="[flag]"><a-icon :type="`caret-${flag}`"/></span>
</div>
</template>
<script>
export default {
name: 'Trend',
props: {
prefixCls: {
type: String,
default: 'ant-pro-trend'
},
/**
* 上升下降标识:up|down
*/
flag: {
type: String,
required: true
},
/**
* 颜色反转
*/
reverseColor: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>
+3
View File
@@ -0,0 +1,3 @@
import Trend from './Trend.vue'
export default Trend
+42
View File
@@ -0,0 +1,42 @@
@import "../index";
@trend-prefix-cls: ~"@{ant-pro-prefix}-trend";
.@{trend-prefix-cls} {
display: inline-block;
font-size: @font-size-base;
line-height: 22px;
.up,
.down {
margin-left: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(0.83);
}
}
.item-text {
display: inline-block;
margin-left: 8px;
color: rgba(0,0,0,.85);
}
.up {
color: @red-6;
}
.down {
color: @green-6;
top: -1px;
}
&.reverse-color .up {
color: @green-6;
}
&.reverse-color .down {
color: @red-6;
}
}
+441
View File
@@ -0,0 +1,441 @@
<template>
<j-modal
:title="$t('uploadFile.file')"
:width="width"
:visible="visible"
switchFullscreen
:maskClosable="false"
:confirmLoading="confirmLoading"
:footer="null"
@cancel="close">
<a-upload-dragger
name="file"
:multiple="multiple"
:action="uploadAction"
:headers="headers"
:data="{'biz':bizPath}"
:fileList="fileList"
:beforeUpload="doBeforeUpload"
@change="handleChange"
:disabled="disabled"
:returnUrl="returnUrl"
:listType="complistType"
@preview="handlePreview"
@download="handleDownload"
:showUploadList="{
showDownloadIcon: isDownload
}"
v-bind="$attrs"
v-on="childListeners"
:class="{'uploadty-disabled': disabled}"
>
<p class="upload-drag-icon">
<a-icon type="cloud-upload" />
</p>
<p class="ant-upload-text">
{{ $t('uploadFile.clickOrDragUpload') }}
</p>
</a-upload-dragger>
<div id="images">
<div class="image" v-viewer="{movable: false}">
<img v-show="image" :src="imageUrl">
</div>
</div>
<a-empty v-if="disabled && (!fileList || fileList.length === 0)" />
</j-modal>
</template>
<script>
import { ACCESS_TOKEN } from '@/store/mutation-types'
import { downloadFile, getFileAccessHttpUrl } from '@/api/manage'
import { getFileInfo } from '@/api/api'
import { previewPdf } from '@/utils/previewPdf'
import Vue from 'vue'
const FILE_TYPE_ALL = 'all'
const FILE_TYPE_IMG = 'image'
const FILE_TYPE_IMGS = ['jpg', 'jpeg', 'png', 'raw']
const FILE_TYPE_PDF = 'pdf'
// 本系统限制可以上传的文件格式
const CAN_UPLOAD_FILE_TYPE = 'doc,docx,xls,xlsx,pdf,png,jpg'
const Base64 = require('js-base64').Base64
// 支持预览的文件类型
const CAN_PREVIEW_FILE_TYPE = ['pdf', 'image', 'doc', 'docx']
// 不支持预览的文件后缀
const CAN_PREVIEW_FILE_SUFFIX = ['xls', 'xlsx', 'ppt', 'pptx', 'txt', 'mp3', 'mp4', 'flv']
export default {
name: 'UploadFile',
props: {
text: {
type: String,
required: false,
default: '点击上传'
},
fileType: {
type: String,
required: false,
default: CAN_UPLOAD_FILE_TYPE
},
/* 这个属性用于控制文件上传的业务路径 */
bizPath: {
type: String,
required: false,
default: 'temp'
},
value: {
type: [String, Array],
required: false
},
// update-begin- --- author:wangshuai ------ date:20190929 ---- for:Jupload组件增加是否能够点击
disabled: {
type: Boolean,
required: false,
default: false
},
// update-end- --- author:wangshuai ------ date:20190929 ---- for:Jupload组件增加是否能够点击
// 此属性被废弃了
triggerChange: {
type: Boolean,
required: false,
default: false
},
/**
* update -- author:lvdandan -- date:20190219 -- for:Jupload组件增加是否返回url
* true:仅返回url
* false:返回fileName filePath fileSize
*/
returnUrl: {
type: Boolean,
required: false,
default: true
},
number: {
type: Number,
required: false,
default: 0
},
multiple: {
type: Boolean,
default: true
},
beforeUpload: {
type: Function
},
isDownload: {
type: Boolean,
default: true
},
disabledUpload: {
type: Boolean,
default: false
}
},
data () {
return {
width: 600,
visible: false,
confirmLoading: false,
uploadAction: window._CONFIG.domianURL + '/sys/common/upload',
headers: {},
fileList: [],
fileIds: null, // 文件id,可以是数组,也可以是字符串
image: false,
imageUrl: null
}
},
computed: {
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
},
complistType () {
return this.fileType === FILE_TYPE_IMG ? 'picture-card' : 'text'
}
},
methods: {
open (fileIds) {
this.headers = { 'X-Access-Token': Vue.ls.get(ACCESS_TOKEN) }
this.fileIds = fileIds
this.initFileList()
this.visible = true
},
initFileList () {
if (!this.fileIds) {
return
}
let fileIdArr = []
if (Array.isArray(this.fileIds)) {
fileIdArr = this.fileIds
} else {
fileIdArr = this.fileIds.split(',')
}
const fileList = []
fileIdArr.forEach(async id => {
const fileInfo = await this.getFileInfoByFileId(id)
if (fileInfo) {
fileList.push(fileInfo)
}
})
this.fileList = fileList
console.log(this.fileList)
},
getFileInfoByFileId (id) {
return new Promise(resolve => {
let file
getFileInfo({ id: id }).then(res => {
if (res.success) {
const fileInfo = res.result || {}
file = {
uid: fileInfo.id,
name: fileInfo.fileName,
status: 'done',
url: fileInfo.url,
// response用于下载和预览
response: {
success: true,
result: {
id: fileInfo.id,
fileName: fileInfo.fileName
},
status: 'history'
}
}
}
}).finally(() => {
resolve(file)
})
})
},
close () {
this.visible = false
setTimeout(() => {
this.fileList = []
}, 500)
},
handleChange (info) {
console.log(info, this.uploadGoOn)
if (!info.file.status && this.uploadGoOn === false) {
info.fileList.pop()
}
console.log(info.fileList)
let fileList = info.fileList
if (info.file.status === 'done') {
console.log(this.number)
if (this.number > 0) {
console.log(fileList)
fileList = fileList.slice(-this.number)
console.log(fileList)
}
if (info.file.response.success) {
fileList = fileList.map((file) => {
console.log(file)
if (file.response) {
// const reUrl = `${file.response.result.id}?token=${Vue.ls.get(ACCESS_TOKEN)}&fullfilename=${file.response.result.fileName}`
// TODO getFileAccessHttpUrl方法会追加token,在之后拼参数
file.url = getFileAccessHttpUrl(file.response.result.id) + '&fullfilename=' + file.response.result.fileName
}
// 校验不通过的文件需要筛出去
if (file.uploadGoOn === false) {
return null
}
return file
})
} else {
this.$message.error(info.file.response.message)
}
// this.$message.success(`${info.file.name} 上传成功!`);
} else if (info.file.status === 'error') {
this.$message.error(`${info.file.name} ${this.$t('uploadFile.uploadFailed')}.`)
} else if (info.file.status === 'removed') {
this.handleDelete(info.file)
}
fileList = fileList.filter(tt => !!tt)
console.log(fileList)
// 二次过滤不符合要求的(uploadGoOn为false的),否则只上传多个不符合要求的只会有一个不出现在列表中
fileList = fileList.filter(tt => tt.uploadGoOn === undefined || tt.uploadGoOn === null || tt.uploadGoOn === true)
this.fileList = fileList
if (info.file.status === 'done' || info.file.status === 'removed') {
// returnUrl为true时仅返回文件路径
if (this.returnUrl) {
this.handlePathChange()
} else {
// returnUrl为false时返回文件名称、文件路径及文件大小
this.newFileList = []
for (let a = 0; a < fileList.length; a++) {
// update-begin-author:lvdandan date:20200603 for:【TESTA-514】【开源issue】多个文件同时上传时,控制台报错
if (fileList[a].status === 'done') {
const fileJson = {
id: fileList[a].response.result.id,
fileName: fileList[a].name,
filePath: fileList[a].url,
fileSize: fileList[a].size
}
this.newFileList.push(fileJson)
} else {
return
}
// update-end-author:lvdandan date:20200603 for:【TESTA-514】【开源issue】多个文件同时上传时,控制台报错
}
this.$emit('change', this.newFileList)
}
}
},
handlePreview (file) {
if (!file || !file.url) {
return
}
const fileType = file.type
// 截取文件后缀名
const fileSuffix = file.name ? file.name.split('.')[file.name.split('.').length - 1] : ''
const canPreview = fileType ? CAN_PREVIEW_FILE_TYPE.every(tt => fileType.indexOf(tt) === -1) : CAN_PREVIEW_FILE_SUFFIX.some(tt => fileSuffix === tt)
// 判断是否为可预览格式的文件
if (canPreview) {
this.$message.loading(this.$t('uploadFile.cannotPreview')).then(() => {
this.handleDownload(file)
})
return
}
const fileFullUrl = `${window._CONFIG.domianWebSocketURL}/sys/common/download/${file.response.result.id}?token=${sessionStorage.getItem(ACCESS_TOKEN)}&fullfilename=${file.name}`
console.log(fileSuffix)
// 图片预览,使用自己添加的组件
if (!canPreview && FILE_TYPE_IMGS.includes(fileSuffix)) {
this.imageUrl = getFileAccessHttpUrl(file.response.result.id)
// 获取viewer实例
const viewer = this.$el.querySelector('.image').$viewer
// 调用show方法进行显示预览图
viewer.show()
// this.$refs.imagePreviewModal.open(file)
return
}
// pdf预览
if (!canPreview && FILE_TYPE_PDF.includes(fileSuffix)) {
const url = previewPdf(file.response.result.id)
window.open(url)
return
}
// 其余可预览文件仍使用KKFile进行预览
const url = `${window._CONFIG.onlinePreviewDomainURL}?url=${encodeURIComponent(Base64.encode(fileFullUrl))}`
window.open(url)
},
handleDownload (file) {
// 下载文件
downloadFile(`/sys/common/download/${file.response.result.id}`, file.name)
},
doBeforeUpload (file) {
this.uploadGoOn = true
file.uploadGoOn = true
const fileSize = file.size // 上传的文件大小
if (fileSize === 0) {
this.$message.error(this.$t('uploadFile.cannotUploadEmpty'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (fileSize > 1024 * 1024 * 500) {
this.$message.error(this.$t('uploadFile.maxSize'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileType === FILE_TYPE_ALL) {
return true
}
const fileType = file.type
if (this.fileType === CAN_UPLOAD_FILE_TYPE && fileType.indexOf('image') !== -1) {
return true
}
// 截取文件后缀名
const fileSuffix = (file.name ? file.name.split('.')[file.name.split('.').length - 1] : '').toLowerCase()
if (this.fileType === FILE_TYPE_IMG && fileType.indexOf('image') < 0) {
this.$message.error(this.$t('uploadFile.onlyUploadPic'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileType === FILE_TYPE_IMG && (fileSuffix === 'png' || fileSuffix === 'jpg')) {
return true
}
if (this.fileType === FILE_TYPE_IMG && fileSuffix !== 'png' && fileSuffix !== 'jpg') {
this.$message.error(this.$t('uploadFile.pleaseUpload') + 'png、jpg' + this.$t('uploadFile.file'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileType.indexOf(fileSuffix) === -1) {
this.$message.error(this.$t('uploadFile.pleaseUpload') + `${this.fileType}` + this.$t('uploadFile.file'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
// 扩展 beforeUpload 验证
if (typeof this.beforeUpload === 'function') {
return this.beforeUpload(file)
}
return true
},
handlePathChange () {
const uploadFiles = this.fileList
let path = ''
if (!uploadFiles || uploadFiles.length === 0) {
path = ''
}
const arr = []
for (let a = 0; a < uploadFiles.length; a++) {
if (uploadFiles[a].status === 'done') {
arr.push(uploadFiles[a].url)
} else {
return
}
}
if (arr.length > 0) {
path = arr.join(',')
}
this.$emit('change', path)
},
handleDelete (file) {
// 如有需要新增 删除逻辑
console.log(file)
}
}
}
</script>
<style scoped lang="less">
.upload-drag-icon {
font-size: 70px;
color: #c0c4cc;
}
/*禁用情况下,不显示上传操作区域*/
/deep/ .ant-upload.ant-upload-disabled {
display: none;
}
// 索赔上传需要根据传入的值判断是否可以上传
.upload-disabled {
/deep/ .ant-upload{
display: none;
}
}
// 不可删除文件
.delete-disabled {
/deep/.ant-upload-list-item-card-actions {
a:nth-child(2){
display: none;
}
}
}
</style>
+83
View File
@@ -0,0 +1,83 @@
import Vue from 'vue'
/**
* 省市区
*/
export default class Area {
/**
* 构造器
* @param pcaa
*/
constructor (pcaa) {
if (!pcaa) {
pcaa = Vue.prototype.$Jpcaa
}
const arr = []
const province = pcaa['86']
Object.keys(province).map(key => {
arr.push({ id: key, text: province[key], pid: '86', index: 1 })
const city = pcaa[key]
Object.keys(city).map(key2 => {
arr.push({ id: key2, text: city[key2], pid: key, index: 2 })
const qu = pcaa[key2]
if (qu) {
Object.keys(qu).map(key3 => {
arr.push({ id: key3, text: qu[key3], pid: key2, index: 3 })
})
}
})
})
this.all = arr
}
get pca () {
return this.all
}
getCode (text) {
if (!text || text.length === 0) {
return ''
}
for (const item of this.all) {
if (item.text === text) {
return item.id
}
}
}
getText (code) {
if (!code || code.length === 0) {
return ''
}
const arr = []
this.getAreaBycode(code, arr, 3)
return arr.join('/')
}
getRealCode (code) {
const arr = []
this.getPcode(code, arr, 3)
return arr
}
getPcode (id, arr, index) {
for (const item of this.all) {
if (item.id === id && item.index === index) {
arr.unshift(id)
if (item.pid !== '86') {
this.getPcode(item.pid, arr, --index)
}
}
}
}
getAreaBycode (code, arr, index) {
for (const item of this.all) {
if (item.id === code && item.index === index) {
arr.unshift(item.text)
if (item.pid !== '86') {
this.getAreaBycode(item.pid, arr, --index)
}
}
}
}
}
+46
View File
@@ -0,0 +1,46 @@
/**
* 获取字符串的长度ascii长度为1 中文长度为2
* @param str
* @returns {number}
*/
export const getStrFullLength = (str = '') =>
str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
return pre + 1
}
return pre + 2
}, 0)
/**
* 给定一个字符串和一个长度,将此字符串按指定长度截取
* @param str
* @param maxLength
* @returns {string}
*/
export const cutStrByFullLength = (str = '', maxLength) => {
let showLength = 0
return str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0)
if (charCode >= 0 && charCode <= 128) {
showLength += 1
} else {
showLength += 2
}
if (showLength <= maxLength) {
return pre + cur
}
return pre
}, '')
}
// 下划线转换驼峰
export function underLinetoHump (name) {
return name.replace(/_(\w)/g, function (all, letter) {
return letter.toUpperCase()
})
}
// 驼峰转换下划线
export function humptoUnderLine (name) {
return name.replace(/([A-Z])/g, '_$1').toLowerCase()
}
+12
View File
@@ -0,0 +1,12 @@
/**
* components util
*/
/**
* 清理空值,对象
* @param children
* @returns {*[]}
*/
export function filterEmpty (children = []) {
return children.filter(c => c.tag || (c.text && c.text.trim() !== ''))
}
+88
View File
@@ -0,0 +1,88 @@
<template>
<div :style="{ padding: '0' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart ref="chart" :forceFit="true" :height="height" :data="dataSource" :scale="scale">
<v-tooltip :shared="false"/>
<v-axis/>
<v-line position="x*y" :size="lineSize" :color="lineColor"/>
<v-area position="x*y" :color="color"/>
</v-chart>
</div>
</template>
<script>
import { triggerWindowResizeEvent } from '@/utils/util'
export default {
name: 'AreaChartTy',
props: {
// 图表数据
dataSource: {
type: Array,
required: true
},
// 图表标题
title: {
type: String,
default: ''
},
// x 轴别名
x: {
type: String,
default: 'x'
},
// y 轴别名
y: {
type: String,
default: 'y'
},
// Y轴最小值
min: {
type: Number,
default: 0
},
// Y轴最大值
max: {
type: Number,
default: null
},
// 图表高度
height: {
type: Number,
default: 254
},
// 线的粗细
lineSize: {
type: Number,
default: 2
},
// 面积的颜色
color: {
type: String,
default: ''
},
// 线的颜色
lineColor: {
type: String,
default: ''
}
},
computed: {
scale () {
return [
{ dataKey: 'x', title: this.x, alias: this.x },
{ dataKey: 'y', title: this.y, alias: this.y, min: this.min, max: this.max }
]
}
},
mounted () {
triggerWindowResizeEvent()
}
}
</script>
<style lang="less" scoped>
@import "chart";
</style>
+50
View File
@@ -0,0 +1,50 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart :forceFit="true" :height="height" :data="dataSource" :scale="scale" :padding="padding">
<v-tooltip/>
<v-axis/>
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
import { triggerWindowResizeEvent } from '@/utils/util'
export default {
name: 'Bar',
props: {
dataSource: {
type: Array,
required: true
},
yaxisText: {
type: String,
default: 'y'
},
title: {
type: String,
default: ''
},
height: {
type: Number,
default: 254
}
},
data () {
return { padding: ['auto', 'auto', '40', '50'] }
},
computed: {
scale () {
return [{
dataKey: 'y',
alias: this.yaxisText
}]
}
},
mounted () {
triggerWindowResizeEvent()
}
}
</script>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div :style="{ padding: '0 50px 32px 0' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart :forceFit="true" :height="height" :data="data" :scale="scale" :padding=" padding" :onClick="handleClick">
<v-tooltip/>
<v-legend/>
<v-axis/>
<v-bar position="type*bar"/>
<v-line position="type*line" color="#2fc25b" :size="3"/>
</v-chart>
</div>
</template>
<script>
import { ChartEventMixins } from './mixins/ChartMixins'
export default {
name: 'BarAndLine',
mixins: [ChartEventMixins],
props: {
title: {
type: String,
default: ''
},
dataSource: {
type: Array,
default: () => [
{ type: '10:10', bar: 200, line: 1000 },
{ type: '10:15', bar: 600, line: 1000 },
{ type: '10:20', bar: 200, line: 1000 },
{ type: '10:25', bar: 900, line: 1000 },
{ type: '10:30', bar: 200, line: 1000 },
{ type: '10:35', bar: 200, line: 1000 },
{ type: '10:40', bar: 100, line: 1000 }
]
},
height: {
type: Number,
default: 400
}
},
data () {
return {
padding: { top: 50, right: 50, bottom: 100, left: 50 },
scale: [{
dataKey: 'bar',
min: 0
}, {
dataKey: 'line',
min: 0
}]
}
},
computed: {
data () {
return this.dataSource
}
}
}
</script>
+96
View File
@@ -0,0 +1,96 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart :data="data" :height="height" :force-fit="true" :scale="scale" :onClick="handleClick">
<v-tooltip/>
<v-axis/>
<v-legend/>
<v-bar position="x*y" color="type" :adjust="adjust"/>
</v-chart>
</div>
</template>
<script>
import { DataSet } from '@antv/data-set'
import { ChartEventMixins } from './mixins/ChartMixins'
export default {
name: 'BarMultid',
mixins: [ChartEventMixins],
props: {
title: {
type: String,
default: ''
},
dataSource: {
type: Array,
default: () => [
{ type: 'JeroOne', 'Jan.': 18.9, 'Feb.': 28.8, 'Mar.': 39.3, 'Apr.': 81.4, May: 47, 'Jun.': 20.3, 'Jul.': 24, 'Aug.': 35.6 },
{ type: 'JeroTwo', 'Jan.': 12.4, 'Feb.': 23.2, 'Mar.': 34.5, 'Apr.': 99.7, May: 52.6, 'Jun.': 35.5, 'Jul.': 37.4, 'Aug.': 42.4 }
]
},
fields: {
type: Array,
default: () => ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.']
},
// 别名,需要的格式:[{field:'name',alias:'姓名'}, {field:'sex',alias:'性别'}]
aliases: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 254
}
},
data () {
return {
adjust: [{
type: 'dodge',
marginRatio: 1 / 32
}]
}
},
computed: {
data () {
const dv = new DataSet.View().source(this.dataSource)
dv.transform({
type: 'fold',
fields: this.fields,
key: 'x',
value: 'y'
})
// bar 使用不了 - 和 / 所以替换下
const rows = dv.rows.map(row => {
if (typeof row.x === 'string') {
row.x = row.x.replace(/[-/]/g, '_')
}
return row
})
// 替换别名
rows.forEach(row => {
for (const item of this.aliases) {
if (item.field === row.type) {
row.type = item.alias
break
}
}
})
return rows
},
scale () {
return [
{
type: 'cat',
dataKey: 'x'
}
]
}
}
}
</script>
<style scoped>
</style>
+187
View File
@@ -0,0 +1,187 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<v-chart :forceFit="true" :height="300" :data="chartData" :scale="scale">
<v-coord type="polar" :startAngle="-202.5" :endAngle="22.5" :radius="0.75"></v-coord>
<v-axis
dataKey="value"
:zIndex="2"
:line="null"
:label="axisLabel"
:subTickCount="4"
:subTickLine="axisSubTickLine"
:tickLine="axisTickLine"
:grid="null"
></v-axis>
<v-axis dataKey="1" :show="false"></v-axis>
<v-series
gemo="point"
position="value*1"
shape="pointer"
color="#1890FF"
:active="false"
></v-series>
<v-guide
type="arc"
:zIndex="0"
:top="false"
:start="arcGuide1Start"
:end="arcGuide1End"
:vStyle="arcGuide1Style"
></v-guide>
<v-guide
type="arc"
:zIndex="1"
:start="arcGuide2Start"
:end="getArcGuide2End"
:vStyle="arcGuide2Style"
></v-guide>
<v-guide
type="html"
:position="htmlGuidePosition"
:html="getHtmlGuideHtml()"
></v-guide>
</v-chart>
</div>
</template>
<script>
import { registerShape } from 'viser-vue'
registerShape('point', 'pointer', {
draw (cfg, container) {
let point = cfg.points[0]
point = this.parsePoint(point)
const center = this.parsePoint({
x: 0,
y: 0
})
container.addShape('line', {
attrs: {
x1: center.x,
y1: center.y,
x2: point.x,
y2: point.y + 15,
stroke: cfg.color,
lineWidth: 5,
lineCap: 'round'
}
})
return container.addShape('circle', {
attrs: {
x: center.x,
y: center.y,
r: 9.75,
stroke: cfg.color,
lineWidth: 4.5,
fill: '#fff'
}
})
}
})
const scale = [{
dataKey: 'value',
min: 0,
max: 9,
tickInterval: 1,
nice: false
}]
const data = [
{ value: 7.0 }
]
export default {
name: 'DashChartDemo',
props: {
datasource: {
type: Number,
default: 7
},
title: {
type: String,
default: ''
}
},
created () {
if (!this.datasource) {
this.chartData = data
} else {
this.chartData = [
{ value: this.datasource }
]
}
this.getChartData()
},
watch: {
datasource: function (val) {
this.chartData = [
{ value: val }
]
this.getChartData()
}
},
methods: {
getChartData () {
if (this.chartData && this.chartData.length > 0) {
this.abcd = this.chartData[0].value * 10
} else {
this.abcd = 70
}
},
getHtmlGuideHtml () {
return '<div style="width: 300px;text-align: center;">\n' +
'<p style="font-size: 14px;color: #545454;margin: 0;">' + this.title + '</p>\n' +
'<p style="font-size: 36px;color: #545454;margin: 0;">' + this.abcd + '%</p>\n' +
'</div>'
},
getArcGuide2End () {
return [this.chartData[0].value, 0.945]
}
},
data () {
return {
chartData: [],
height: 400,
scale: scale,
abcd: 70,
axisLabel: {
offset: -16,
textStyle: {
fontSize: 18,
textAlign: 'center',
textBaseline: 'middle'
}
},
axisSubTickLine: {
length: -8,
stroke: '#fff',
strokeOpacity: 1
},
axisTickLine: {
length: -17,
stroke: '#fff',
strokeOpacity: 1
},
arcGuide1Start: [0, 0.945],
arcGuide1End: [9, 0.945],
arcGuide1Style: {
stroke: '#CBCBCB',
lineWidth: 18
},
arcGuide2Start: [0, 0.945],
arcGuide2Style: {
stroke: '#1890FF',
lineWidth: 18
},
htmlGuidePosition: ['50%', '100%'],
htmlGuideHtml: `
<div style="width: 300px;text-align: center;">
<p style="font-size: 14px;color: #545454;margin: 0;">${this.title}</p>
<p style="font-size: 36px;color: #545454;margin: 0;">${this.abcd}%</p>
</div>
`
}
}
}
</script>
+61
View File
@@ -0,0 +1,61 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart
height="254"
:data="datasource"
:forceFit="true"
:padding="['auto', 'auto', '40', '50']">
<v-tooltip />
<v-axis />
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
const data = []
for (let i = 0; i < 12; i += 1) {
data.push({
x: `${i + 1}`,
y: Math.floor(Math.random() * 1000) + 200
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
export default {
name: 'Bar',
props: {
title: {
type: String,
default: ''
}
},
mounted () {
this.datasource = data
},
data () {
return {
datasource: [],
scale,
tooltip
}
}
}
</script>
+95
View File
@@ -0,0 +1,95 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart :force-fit="true" :height="height" :data="data" :scale="scale" :onClick="handleClick">
<v-tooltip/>
<v-axis/>
<v-legend/>
<v-line position="type*y" color="x"/>
<v-point position="type*y" color="x" :size="4" :v-style="style" :shape="'circle'"/>
</v-chart>
</div>
</template>
<script>
import { DataSet } from '@antv/data-set'
import { ChartEventMixins } from './mixins/ChartMixins'
export default {
name: 'LineChartMultid',
mixins: [ChartEventMixins],
props: {
title: {
type: String,
default: ''
},
dataSource: {
type: Array,
default: () => [
{ type: 'Jan', jeroOne: 7.0, jeroTwo: 3.9 },
{ type: 'Feb', jeroOne: 6.9, jeroTwo: 4.2 },
{ type: 'Mar', jeroOne: 9.5, jeroTwo: 5.7 },
{ type: 'Apr', jeroOne: 14.5, jeroTwo: 8.5 },
{ type: 'May', jeroOne: 18.4, jeroTwo: 11.9 },
{ type: 'Jun', jeroOne: 21.5, jeroTwo: 15.2 },
{ type: 'Jul', jeroOne: 25.2, jeroTwo: 17.0 },
{ type: 'Aug', jeroOne: 26.5, jeroTwo: 16.6 },
{ type: 'Sep', jeroOne: 23.3, jeroTwo: 14.2 },
{ type: 'Oct', jeroOne: 18.3, jeroTwo: 10.3 },
{ type: 'Nov', jeroOne: 13.9, jeroTwo: 6.6 },
{ type: 'Dec', jeroOne: 9.6, jeroTwo: 4.8 }
]
},
fields: {
type: Array,
default: () => ['jeroOne', 'jeroTwo']
},
// 别名,需要的格式:[{field:'name',alias:'姓名'}, {field:'sex',alias:'性别'}]
aliases: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 254
}
},
data () {
return {
scale: [{
type: 'cat',
dataKey: 'x',
min: 0,
max: 1
}],
style: { stroke: '#fff', lineWidth: 1 }
}
},
computed: {
data () {
const dv = new DataSet.View().source(this.dataSource)
dv.transform({
type: 'fold',
fields: this.fields,
key: 'x',
value: 'y'
})
const rows = dv.rows
// 替换别名
rows.forEach(row => {
for (const item of this.aliases) {
if (item.field === row.x) {
row.x = item.alias
break
}
}
})
return rows
}
}
}
</script>
<style scoped>
</style>
+80
View File
@@ -0,0 +1,80 @@
<template>
<div>
<v-chart
:forceFit="true"
:height="height"
:width="width"
:data="data"
:scale="scale"
:padding="0">
<v-tooltip/>
<v-interval
:shape="['liquid-fill-gauge']"
position="transfer*value"
color=""
:v-style="{
lineWidth: 8,
opacity: 0.75
}"
:tooltip="[
'transfer*value',
(transfer, value) => {
return {
name: transfer,
value
};
},
]"
></v-interval>
<v-guide
v-for="(row, index) in data"
:key="index"
type="text"
:top="true"
:position="{
gender: row.transfer,
value: 45
}"
:content="row.value + '%'"
:v-style="{
fontSize: 100,
textAlign: 'center',
opacity: 0.75,
}"
/>
</v-chart>
</div>
</template>
<script>
const sourceDataConst = [
{ transfer: '一月', value: 813 },
{ transfer: '二月', value: 233 },
{ transfer: '三月', value: 561 }
]
export default {
name: 'Liquid',
props: {
height: {
type: Number,
default: 0
},
width: {
type: Number,
default: 0
}
},
data () {
return {
data: sourceDataConst,
scale: []
}
}
}
</script>
<style scoped>
</style>
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="antv-chart-mini">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="height" :data="data" :scale="scale" :padding="[36, 0, 18, 0]">
<v-tooltip/>
<v-smooth-area position="x*y"/>
</v-chart>
</div>
</div>
</template>
<script>
import moment from 'dayjs'
const sourceData = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
sourceData.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
export default {
name: 'MiniArea',
props: {
dataSource: {
type: Array,
default: () => []
},
// x 轴别名
x: {
type: String,
default: 'x'
},
// y 轴别名
y: {
type: String,
default: 'y'
}
},
data () {
return {
data: [],
height: 100
}
},
computed: {
scale () {
return [
{ dataKey: 'x', title: this.x, alias: this.x },
{ dataKey: 'y', title: this.y, alias: this.y }
]
}
},
created () {
if (this.dataSource.length === 0) {
this.data = sourceData
} else {
this.data = this.dataSource
}
}
}
</script>
<style lang="less" scoped>
@import "chart";
</style>
+76
View File
@@ -0,0 +1,76 @@
<template>
<div :style="{'width':width===null?'auto':width+'px'}">
<v-chart :forceFit="typeof width !== 'number'" :height="height" :data="data" padding="0">
<v-tooltip/>
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
import moment from 'dayjs'
const sourceData = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
sourceData.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 30
}]
export default {
name: 'MiniBar',
props: {
dataSource: {
type: Array,
default: () => []
},
width: {
type: Number,
default: null
},
height: {
type: Number,
default: 200
}
},
created () {
if (this.dataSource.length === 0) {
this.data = sourceData
} else {
this.data = this.dataSource
}
},
data () {
return {
tooltip,
data: [],
scale
}
}
}
</script>
<style lang="less" scoped>
@import "chart";
</style>
+75
View File
@@ -0,0 +1,75 @@
<template>
<div class="chart-mini-progress">
<div class="target" :style="{ left: target + '%'}">
<span :style="{ backgroundColor: color }"/>
<span :style="{ backgroundColor: color }"/>
</div>
<div class="progress-wrapper">
<div class="progress" :style="{ backgroundColor: color, width: percentage + '%', height: height+'px' }"></div>
</div>
</div>
</template>
<script>
export default {
name: 'MiniProgress',
props: {
target: {
type: Number,
default: 0
},
height: {
type: Number,
default: 10
},
color: {
type: String,
default: '#13C2C2'
},
percentage: {
type: Number,
default: 0
}
}
}
</script>
<style lang="less" scoped>
.chart-mini-progress {
padding: 5px 0;
position: relative;
width: 100%;
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
&:last-child {
top: auto;
bottom: 0;
}
}
}
.progress-wrapper {
background-color: #f5f5f5;
position: relative;
.progress {
transition: all .4s cubic-bezier(.08, .82, .17, 1) 0s;
border-radius: 1px 0 0 1px;
background-color: #1890ff;
width: 0;
height: 100%;
}
}
}
</style>
+70
View File
@@ -0,0 +1,70 @@
<template>
<v-chart :forceFit="true" :height="height" :data="data" :scale="scale" :onClick="handleClick">
<v-tooltip :showTitle="false" dataKey="item*percent"/>
<v-axis/>
<v-legend dataKey="item"/>
<v-pie position="percent" color="item" :v-style="pieStyle" :label="labelConfig"/>
<v-coord type="theta"/>
</v-chart>
</template>
<script>
import { ChartEventMixins } from './mixins/ChartMixins'
const DataSet = require('@antv/data-set')
export default {
name: 'Pie',
mixins: [ChartEventMixins],
props: {
title: {
type: String,
default: ''
},
height: {
type: Number,
default: 254
},
dataSource: {
type: Array,
default: () => [
{ item: '示例一', count: 40 },
{ item: '示例二', count: 21 },
{ item: '示例三', count: 17 },
{ item: '示例四', count: 13 },
{ item: '示例五', count: 9 }
]
}
},
data () {
return {
scale: [{
dataKey: 'percent',
min: 0,
formatter: '.0%'
}],
pieStyle: {
stroke: '#fff',
lineWidth: 1
},
labelConfig: ['percent', {
formatter: (val, item) => {
return item.point.item + ': ' + val
}
}]
}
},
computed: {
data () {
const dv = new DataSet.View().source(this.dataSource)
// 计算数据百分比
dv.transform({
type: 'percent',
field: 'count',
dimension: 'item',
as: 'percent'
})
return dv.rows
}
}
}
</script>
+367
View File
@@ -0,0 +1,367 @@
# 报表组件文档
## 柱状图
##### 引用方式
```js
import Bar from '@/components/chart/Bar'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| title | string | | 报表标题 |
| dataSource | array | ✔️ | 报表数据源 |
| height | number | | 报表高度,默认254 |
##### dataSource 示例
```json
[
{
"x": "1月",
"y": 320
},
{
"x": "2月",
"y": 457
},
{
"x": "3月",
"y": 182
}
]
```
##### 代码示例
```html
<template>
<bar title="柱状图" :dataSource="dataSource" :height="420"/>
</template>
<script>
import Bar from '@/components/chart/Bar'
export default {
name: 'ChartDemo',
components: {
Bar
},
data() {
return {
dataSource: [
{
"x": "1月",
"y": 320
},
{
"x": "2月",
"y": 457
},
{
"x": "3月",
"y": 182
}
]
}
}
}
</script>
<style></style>
```
## 多列柱状图
##### 引用方式
```js
import BarMultid from '@/components/chart/BarMultid'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| title | string | | 报表标题 |
| fields | array | | 主列字段列表 |
| dataSource | array | | 报表数据源 |
| height | number | | 报表高度,默认254 |
##### fields 示例
```json
["Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", "Jul.", "Aug."]
```
##### dataSource 示例
```json
[
{
"type": "jero", // 列名
"Jan.": 18.9,
"Feb.": 28.8,
"Mar.": 39.3,
"Apr.": 81.4,
"May": 47,
"Jun.": 20.3,
"Jul.": 24,
"Aug.": 35.6
},
{
"type": "jeroTwo",
"Jan.": 12.4,
"Feb.": 23.2,
"Mar.": 34.5,
"Apr.": 99.7,
"May": 52.6,
"Jun.": 35.5,
"Jul.": 37.4,
"Aug.": 42.4
}
]
```
## 迷你柱状图
不带标题和数据轴的柱状图
##### 引用方式
```js
import MiniBar from '@/components/chart/MiniBar'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|---------------|
| width | number | | 报表宽度度,默认自适应宽度 |
| height | number | | 报表高度,默认200 |
| dataSource | array | | 报表数据源 |
##### dataSource 示例
```json
[
{
"x": "1月",
"y": 320
},
{
"x": "2月",
"y": 457
},
{
"x": "3月",
"y": 182
}
]
```
## 面积图
##### 引用方式
```js
import AreaChartTy from '@/components/chart/AreaChartTy'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| title | string | | 报表标题 |
| dataSource | array | ✔️ | 报表数据源 |
| height | number | | 报表高度,默认254 |
| lineSize | number | | 线的粗细,默认2 |
##### dataSource 示例
```json
[
{
"x": "1月",
"y": 320
},
{
"x": "2月",
"y": 457
},
{
"x": "3月",
"y": 182
}
]
```
## 多行折线图
##### 引用方式
```js
import LineChartMultid from '@/components/chart/LineChartMultid'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| title | string | | 报表标题 |
| fields | array | | 主列字段列表 |
| dataSource | array | | 报表数据源 |
| height | number | | 报表高度,默认254 |
##### fields 示例
```json
["jero", "jeroTwo"]
```
##### dataSource 示例
```json
[
{
"type": "Jan", // 列名
"jero": 7,
"jeroTwo": 3.9
},
{ "type": "Feb", "jero": 6.9, "jeroTwo": 4.2 },
{ "type": "Mar", "jero": 9.5, "jeroTwo": 5.7 },
{ "type": "Apr", "jero": 14.5, "jeroTwo": 8.5 },
{ "type": "May", "jero": 18.4, "jeroTwo": 11.9 },
{ "type": "Jun", "jero": 21.5, "jeroTwo": 15.2 },
{ "type": "Jul", "jero": 25.2, "jeroTwo": 17 },
{ "type": "Aug", "jero": 26.5, "jeroTwo": 16.6 },
{ "type": "Sep", "jero": 23.3, "jeroTwo": 14.2 },
{ "type": "Oct", "jero": 18.3, "jeroTwo": 10.3 },
{ "type": "Nov", "jero": 13.9, "jeroTwo": 6.6 },
{ "type": "Dec", "jero": 9.6, "jeroTwo": 4.8 }
]
```
## 饼状图
##### 引用方式
```js
import Pie from '@/components/chart/Pie'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| dataSource | array | | 报表数据源 |
| height | number | | 报表高度,默认254 |
##### dataSource 示例
```json
[
// 所有的 percent 相加等于 100
{ "item": "一月", "percent": 40 },
{ "item": "二月", "percent": 21 },
{ "item": "三月", "percent": 17 },
{ "item": "四月", "percent": 13 },
{ "item": "五月", "percent": 9 }
]
```
## 雷达图
##### 引用方式
```js
import Radar from '@/components/chart/Radar'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|------------|
| dataSource | array | | 报表数据源 |
| height | number | | 报表高度,默认254 |
##### dataSource 示例
```json
[
// score 最小值为 0,最大值为 100
{ "item": "一月", "score": 40 },
{ "item": "二月", "score": 20 },
{ "item": "三月", "score": 67 },
{ "item": "四月", "score": 43 },
{ "item": "五月", "score": 90 }
]
```
## 进度条
##### 引用方式
```js
import MiniProgress from '@/components/chart/MiniProgress'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|------------|--------|----|-------------------|
| percentage | number | | 当前进度百分比,默认0,最高100 |
| target | number | | 目标值,默认10 |
| height | number | | 进度条高度,默认10 |
| color | string | | 进度条颜色,默认 #13C2C2 |
## 仪表盘
##### 引用方式
```js
import DashChartDemo from '@/components/chart/DashChartDemo'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|--------|--------|----|----------------|
| title | string | | 报表标题 |
| value | number | | 当前值,默认6.7,最大为9 |
| height | number | | 报表高度,默认254 |
## 排名列表
##### 引用方式
```js
import RankList from '@/components/chart/RankList'
```
##### 参数列表
| 参数名 | 类型 | 必填 | 说明 |
|--------|--------|----|--------------|
| title | string | | 报表标题 |
| list | array | | 排名列表数据 |
| height | number | | 报表高度,默认自适应高度 |
##### list 示例
```json
[
{
"name": "北京朝阳 1 号店",
"total": 1981
},
{ "name": "北京朝阳 2 号店", "total": 1359 },
{ "name": "北京朝阳 3 号店", "total": 1354 },
{ "name": "北京朝阳 4 号店", "total": 263 },
{ "name": "北京朝阳 5 号店", "total": 446 },
{ "name": "北京朝阳 6 号店", "total": 796 }
]
```
+90
View File
@@ -0,0 +1,90 @@
<template>
<v-chart :forceFit="true" :height="height" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
<v-tooltip></v-tooltip>
<v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid"/>
<v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid"/>
<v-legend dataKey="user" marker="circle" :offset="30"/>
<v-coord type="polar" radius="0.8"/>
<v-line position="item*score" color="user" :size="2"/>
<v-point position="item*score" color="user" :size="4" shape="circle"/>
</v-chart>
</template>
<script>
const axis1Opts = {
dataKey: 'item',
line: null,
tickLine: null,
grid: {
lineStyle: {
lineDash: null
},
hideFirstLine: false
}
}
const axis2Opts = {
dataKey: 'score',
line: null,
tickLine: null,
grid: {
type: 'polygon',
lineStyle: {
lineDash: null
}
}
}
const scale = [
{
dataKey: 'score',
min: 0,
max: 100
}, {
dataKey: 'user',
alias: '类型'
}
]
const sourceData = [
{ item: '示例一', score: 40 },
{ item: '示例二', score: 20 },
{ item: '示例三', score: 67 },
{ item: '示例四', score: 43 },
{ item: '示例五', score: 90 }
]
export default {
name: 'Radar',
props: {
height: {
type: Number,
default: 254
},
dataSource: {
type: Array,
default: () => []
}
},
data () {
return {
axis1Opts,
axis2Opts,
scale,
data: sourceData
}
},
watch: {
dataSource (newVal) {
if (newVal.length === 0) {
this.data = sourceData
} else {
this.data = newVal
}
}
}
}
</script>
<style scoped>
</style>
+81
View File
@@ -0,0 +1,81 @@
<template>
<div class="rank">
<h4 class="title">{{ title }}</h4>
<ul class="list" :style="{height:height?`${height}px`:'auto',overflow:'auto'}">
<li :key="index" v-for="(item, index) in list">
<span :class="index < 3 ? 'active' : null">{{ index + 1 }}</span>
<span>{{ item.name }}</span>
<span>{{ item.total }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'RankList',
// ['title', 'list']
props: {
title: {
type: String,
default: ''
},
list: {
type: Array,
default: null
},
height: {
type: Number,
default: null
}
}
}
</script>
<style lang="less" scoped>
.rank {
padding: 0 32px 32px 72px;
.list {
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
margin-top: 16px;
span {
color: rgba(0, 0, 0, .65);
font-size: 14px;
line-height: 22px;
&:first-child {
background-color: #f5f5f5;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 24px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
}
&.active {
background-color: #314659;
color: #fff;
}
&:last-child {
float: right;
}
}
}
}
}
.mobile .rank {
padding: 0 32px 32px 32px;
}
</style>
+54
View File
@@ -0,0 +1,54 @@
<template>
<div>
<v-chart :forceFit="true" :height="height" :data="data">
<v-coord type="rect" direction="LB" />
<v-tooltip />
<v-legend />
<v-axis dataKey="State" :label="label" />
<v-stack-bar position="State*流程数量" color="流程状态" />
</v-chart>
</div>
</template>
<script>
const DataSet = require('@antv/data-set')
export default {
name: 'StackBar',
props: {
dataSource: {
type: Array,
required: true,
default: () => [
{ State: '请假', 流转中: 25, 已归档: 18 },
{ State: '出差', 流转中: 30, 已归档: 20 },
{ State: '加班', 流转中: 38, 已归档: 42 },
{ State: '用车', 流转中: 51, 已归档: 67 }
]
},
height: {
type: Number,
default: 254
}
},
data () {
return {
label: { offset: 12 }
}
},
computed: {
data () {
const dv = new DataSet.View().source(this.dataSource)
dv.transform({
type: 'fold',
fields: ['流转中', '已归档'],
key: '流程状态',
value: '流程数量',
retains: ['State']
})
return dv.rows
}
}
}
</script>
+66
View File
@@ -0,0 +1,66 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart
:height="height"
:data="data"
:scale="scale"
:forceFit="true"
:padding="['auto', 'auto', '40', '50']">
<v-tooltip/>
<v-axis/>
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
export default {
name: 'Bar',
props: {
title: {
type: String,
default: ''
},
x: {
type: String,
default: 'x'
},
y: {
type: String,
default: 'y'
},
data: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 254
}
},
data () {
return {}
},
computed: {
scale () {
return [
{ dataKey: 'x', title: this.x, alias: this.x },
{ dataKey: 'y', title: this.y, alias: this.y }
]
}
},
created () {
// this.getMonthBar()
},
methods: {
// getMonthBar() {
// this.$http.get('/analysis/month-bar')
// .then(res => {
// this.data = res.result
// })
// }
}
}
</script>
+84
View File
@@ -0,0 +1,84 @@
<template>
<div class="chart-trend">
{{ term }}
<span>{{ rate }}%</span>
<span :class="['trend-icon', trend]"><a-icon :type="'caret-' + trend"/></span>
</div>
</template>
<script>
export default {
name: 'Trend',
props: {
// 同title
term: {
type: String,
default: '',
required: true
},
// 百分比
percentage: {
type: Number,
default: null
},
type: {
type: Boolean,
default: null
},
target: {
type: Number,
default: 0
},
value: {
type: Number,
default: 0
},
fixed: {
type: Number,
default: 2
}
},
data () {
return {
trend: (this.type && 'up') || 'down',
rate: this.percentage
}
},
created () {
const type = this.type === null ? this.value >= this.target : this.type
this.trend = type ? 'up' : 'down'
this.rate = (this.percentage === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percentage).toFixed(this.fixed)
}
}
</script>
<style lang="less" scoped>
.chart-trend {
display: inline-block;
font-size: 14px;
line-height: 22px;
.trend-icon {
font-size: 12px;
&.up, &.down {
margin-left: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(.83);
}
}
&.up {
color: #f5222d;
}
&.down {
color: #52c41a;
top: -1px;
}
}
}
</style>
+13
View File
@@ -0,0 +1,13 @@
.antv-chart-mini {
position: relative;
width: 100%;
.chart-wrapper {
position: absolute;
bottom: -28px;
width: 100%;
/* margin: 0 -5px;
overflow: hidden;*/
}
}
@@ -0,0 +1,10 @@
export const ChartEventMixins = {
methods: {
handleClick (event, chart) {
this.handleEvent('click', event, chart)
},
handleEvent (eventName, event, chart) {
this.$emit(eventName, event, chart)
}
}
}
+610
View File
@@ -0,0 +1,610 @@
<template>
<a-form-model :model="formInline" v-if="isFormInline" class="form-add" :rules="rules" ref="ruleForm">
<div v-for="(val,index) in dataList" :key="index">
<div v-for="(ol, index) in Object.keys(val)" :key="index">
<div class="header-text">
&nbsp;{{ol}}
</div>
<a-row :gutter="24">
<div v-for="(item, index) in val[ol]" :key="index">
<a-col :span="12" v-if="item.field_show_type === FieldType.TEXT_STRING.value || item.field_show_type === FieldType.TEXT_LINK.value">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<a-input class="box-input"
:disabled="disabled"
v-model="formInline[item.db_field_name]"
:maxLength="50"
:placeholder="$t('pleaseEnter')+item.db_field_txt"/>
</a-form-model-item>
</div>
</a-col>
<a-col :span="12" v-else-if="item.field_show_type === FieldType.TEXT_NUMBER.value">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<a-input-number class="box-input"
:placeholder="$t('pleaseEnter')+item.db_field_txt"
:disabled="disabled"
v-model="formInline[item.db_field_name]" :min="1" :max="99999999"/>
</a-form-model-item>
</div>
</a-col>
<a-col :span="12" v-else-if="item.field_show_type === FieldType.PULL_SINGLE.value">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<j-dict-select-tag class="box-input" v-model="formInline[item.db_field_name]"
:disabled="disabled"
@input="handleInput(item.db_field_name)"
:placeholder="$t('pleaseSelect')+item.db_field_txt"
:type="'select'"
:triggerChange="false" :dictCode="item.dict_field"/>
</a-form-model-item>
</div>
</a-col>
<a-col :span="12" v-else-if="item.field_show_type === FieldType.PULL_MORE.value">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model-multi" :prop="item.db_field_name">
<j-multi-select-tag class="box-input" v-model="formInline[item.db_field_name]"
:disabled="disabled"
:placeholder="$t('pleaseSelect')+item.db_field_txt"
:type="'select'"
:triggerChange="false" :dictCode="item.dict_field"/>
</a-form-model-item>
</div>
</a-col>
<a-col :span="12" v-else-if="item.field_show_type === FieldType.DATE_SINGLE.value">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<a-date-picker class="box-input"
:placeholder="$t('pleaseSelect')+item.db_field_txt"
@change="dateChange(item)"
:getCalendarContainer="(trigger) => trigger.parentNode"
format="YYYY-MM-DD"
v-model="formInline[item.db_field_name]"
:disabled="disabled"
style="width: 100%"/>
<!-- :disabledDate="disabledDate"-->
</a-form-model-item>
</div>
</a-col>
<a-col :span="12" v-else-if="item.field_show_type === FieldType.DATE_MORE.value">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<a-range-picker class="box-input" v-model="formInline[item.db_field_name]"
:disabled="disabled"
:getCalendarContainer="(trigger) => trigger.parentNode"
@change="onChange(item.db_field_name)"></a-range-picker>
</a-form-model-item>
</div>
</a-col>
<a-col :span="12" v-else-if="item.field_show_type === FieldType.FILE.value">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<a-button type="primary" class="button-text"
@click="clickButtonToUpload(item)">
{{
(formInline[item.db_field_name] === 'null' || formInline[item.db_field_name] === ''
|| formInline[item.db_field_name] === null || formInline[item.db_field_name] === undefined)
? $t('uploadFile.clickUpload')
: $t('uploadFile.viewUploadedFiles')
}}
</a-button>
</a-form-model-item>
</div>
</a-col>
<a-col :span="24" v-else-if="item.field_show_type === FieldType.TEXT.value || item.field_show_type === 'text'">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model-textarea" :prop="item.db_field_name">
<a-textarea
:placeholder="$t('pleaseEnter')+item.db_field_txt"
:disabled="disabled"
:maxLength="500"
v-model="formInline[item.db_field_name]" :rows="4"/>
</a-form-model-item>
</div>
</a-col>
<a-col :span="24" v-else-if="item.field_show_type === FieldType.PERSON.value || item.field_show_type == 'sel_user'">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<personnel-selection :query="item"
v-if="isFormInline"
:personneQuery="formInline"
@change="personnelSelectionChange"
:disabled="disabled"
v-model="formInline[item.db_field_name]"/>
</a-form-model-item>
</div>
</a-col>
<a-col :span="24" v-else-if="item.field_show_type === FieldType.STANDARD.value">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<standard-selection :query="item"
:disabled="disabled"
:standard="formInline"
@change="standardSelectionChange"
v-model="formInline[item.db_field_name]"/>
</a-form-model-item>
</div>
</a-col>
<a-col :span="12" v-else-if="item.field_show_type === 'RADIO'">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<j-dict-select-tag v-model="formInline[item.db_field_name]"
:disabled="disabled"
@input="handleInput(item.db_field_name)"
:placeholder="$t('selectStatus')"
:type="'radio'" :triggerChange="false" :dictCode="item.dict_field"/>
</a-form-model-item>
</div>
</a-col>
<a-col :span="12" v-else-if="item.field_show_type === FieldType.TREE.value">
<div class="box-title-text" :title="item.db_field_txt">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model" :prop="item.db_field_name">
<a-tree-select
tree-node-filter-prop="title"
v-model="formInline[item.db_field_name]"
:maxTagCount="1"
:getPopupContainer="triggerNode=> triggerNode.parentNode"
style="width: 100%"
:tree-data="item.tree"
tree-checkable
:placeholder="$t('PleaseSelect')+item.db_field_txt"
/>
</a-form-model-item>
</div>
</a-col>
<a-col :span="12" v-else-if="item.field_show_type === 'CHECKBOX'">
<div class="box-title-text">
<div class="title-text">
<span class="required" v-if="item.field_must_input === 1">*</span>
<span class="title-text-text" :title="item.db_field_txt">{{item.db_field_txt}}</span>
</div>
<a-form-model-item class="item-model-multi" :prop="item.db_field_name">
<j-multi-select-tag class="box-input" v-model="formInline[item.db_field_name]"
:disabled="disabled"
:placeholder="$t('selectStatus')" :type="'checkbox'"
:triggerChange="false" :dictCode="item.dict_field"/>
</a-form-model-item>
</div>
</a-col>
</div>
</a-row>
</div>
</div>
<upload-file ref="uploadFile" @change="uploadFileChange" :return-url="false"></upload-file>
</a-form-model>
</template>
<script>
import { getAction } from '@/api/manage'
import JDictSelectTag from '../dict/JDictSelectTag'
import JMultiSelectTag from '../dict/JMultiSelectTag'
import UploadFile from '../UploadFile'
// 选择标准选择器
import StandardSelection from '../selection/StandardSelection'
import { FieldType } from '../../enums/commonEnums'
export default {
name: 'AddForm',
components: { StandardSelection, JDictSelectTag, JMultiSelectTag, UploadFile },
props: {
url: {
type: Object,
default: () => {
return {}
}
},
flag: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
}
},
data () {
return {
FieldType,
loading: false,
formInline: {},
isFormInline: false,
rules: {},
confirmLoading: false,
type: 'add',
dataList: [],
ruleList: [],
uploadName: ''
}
},
methods: {
add () {
this.formInline = {}
this.type = 'add'
// this.getForm()
this.dataList = [
{
基本信息: [
{
area: '基本信息',
db_field_name: 'serial_number',
db_field_txt: '编号',
db_length: 200,
dict_field: '',
field_must_input: '1',
field_show_type: '1',
tree: []
},
{
area: '基本信息',
db_field_name: 'state',
db_field_txt: '状态',
db_length: 36,
dict_field: 'state',
field_must_input: '1',
field_show_type: '3',
tree: []
},
{
area: '基本信息',
db_field_name: 'standard_system',
db_field_txt: '标准体系',
db_length: 100,
dict_field: 'standard_system',
field_must_input: '0',
field_show_type: '3',
tree: []
},
{
area: '基本信息',
db_field_name: 'corresponding_standard',
db_field_txt: '对应标准',
db_length: 200,
dict_field: '',
field_must_input: '0',
field_show_type: '10',
tree: []
}
]
},
{
文本信息: [
{
area: '文本信息',
db_field_name: 'release_draft',
db_field_txt: '发言稿(必读)',
db_length: 3000,
dict_field: '',
field_must_input: '0',
field_show_type: '7',
flag: 1,
tree: []
},
{
area: '文本信息',
db_field_name: 'release_draft',
db_field_txt: '发言稿(必读)',
db_length: 3000,
dict_field: '',
field_must_input: '0',
field_show_type: '7',
flag: 1,
tree: []
}
]
}
]
this.ruleList = this.dataList
this.integrateData()
},
edit (item) {
this.type = 'edit'
this.loading = true
this.getForm(() => {
getAction(this.url.getDocumentInfo, { id: item.id }).then((res) => {
if (res.success) {
this.formInline = res.result[0]
this.loading = false
} else {
this.loading = false
}
})
})
},
// 获取表单列表
getForm (callBack) {
this.confirmLoading = true
const params = {
flag: this.flag,
type: this.type
}
getAction(this.url.getAddForm, params).then((res) => {
if (res.success) {
this.dataList = res.result
this.ruleList = res.result
this.integrateData()
callBack && callBack()
}
})
},
// 设置表单校验
integrateData () {
this.isFormInline = false
const ruleList = []
this.ruleList.forEach(res => {
Object.keys(res).forEach(val => {
res[val].forEach(ol => {
ruleList.push(ol)
})
})
})
const rules = {}
ruleList.forEach((res, index) => {
const rule = []
if (res.field_must_input === 1) {
if (res.field_show_type === '1') {
rule.push({
required: true,
message: res.db_field_txt + this.$t('cannotEmpty'),
trigger: 'blur'
})
} else if (res.field_show_type === '4' || res.field_show_type === '6' ||
res.field_show_type === '5' || res.field_show_type === '7'
) {
rule.push({
required: true,
message: res.db_field_txt + this.$t('cannotEmpty'),
trigger: 'change'
})
} else if (res.field_show_type === '2') {
rule.push({
required: true,
message: res.db_field_txt + this.$t('cannotEmpty'),
trigger: 'blur'
})
} else if (res.field_show_type === '8' || res.field_show_type === '9' || res.field_show_type === '10' || res.field_show_type === '11') {
rule.push({
required: true,
message: res.db_field_txt + this.$t('cannotEmpty'),
trigger: 'blur'
})
} else if (res.field_show_type === '3') {
rule.push({
required: true,
message: res.db_field_txt + this.$t('cannotEmpty'),
trigger: 'change'
})
}
}
if (res.field_show_type === '1' || res.field_show_type === '8' || res.field_show_type === '9' ||
res.field_show_type === '10' || res.field_show_type === '11') {
rule.push({
max: res.db_length,
message: res.db_field_txt + this.$t('cannotExceed') + res.db_length + this.$t('characters'),
trigger: 'blur'
})
}
if (rule.length > 0) {
rules[res.db_field_name] = rule
}
})
this.rules = rules
this.isFormInline = true
this.confirmLoading = false
},
// 点击点击上传按钮
clickButtonToUpload (item) {
this.$refs.uploadFile.open(this.formInline[item.db_field_name])
this.uploadName = item.db_field_name
},
// 文件上传改变后的回调
uploadFileChange (data) {
const attIdList = []
if (data && data.length > 0) {
data.map(item => {
attIdList.push(item.id || data.name)
})
}
/** 赋值给当前对应的表单文件 */
this.formInline[this.uploadName] = attIdList.join(',')
this.formInline = { ...this.formInline }
},
// 标准选择变化
standardSelectionChange (value, id) {
this.formInline[value + '_id'] = id
this.formInline = { ...this.formInline }
},
// 人员选择变化
personnelSelectionChange (value, id) {
this.formInline[value + '_id'] = id
this.formInline = { ...this.formInline }
},
submit () {
this.$refs.ruleForm.validate(valid => {
if (valid) {
const formInline = JSON.parse(JSON.stringify(this.formInline))
Object.keys(formInline).forEach(res => {
if (formInline[res] instanceof Array) {
formInline[res] = formInline[res].join(',')
}
})
this.$emit('submitFormData', formInline)
} else {
this.$emit('addFormWarning')
return false
}
})
}
}
}
</script>
<style scoped lang="less">
.form-add{
margin-bottom: 40px;
/deep/ .ant-form-item-label {
width: 130px;
}
/deep/ .ant-form-item-control-wrapper {
display: inline-block;
width: 100%;
}
}
.item-model{
width: calc(100% - 130px);
display: inline-block;
margin-top: 2px;
height: 40px;
margin-bottom: 24px;
/deep/ .ant-form-item-control-wrapper {
width: 100%;
}
}
.item-model-textarea {
width: calc(100% - 130px);
display: inline-block;
margin-top: 2px;
/deep/ .ant-form-item-control-wrapper {
width: 100% !important;
}
}
.box-input {
/deep/ .ant-select-selection--single {
height: 38px;
}
/deep/ .ant-select-selection--multiple {
height: 38px;
.ant-select-selection__rendered > ul > li{
margin-top: 6px;
}
}
/deep/ .ant-select-selection__rendered {
line-height: 38px;
height: 38px;
}
/deep/ .ant-calendar-picker {
line-height: 38px;
height: 38px;
}
/deep/ .ant-calendar-picker-input {
height: 38px;
}
/deep/ .ant-input-number-input-wrap {
line-height: 38px;
height: 38px;
}
}
.box-title-text {
line-height: 1.4;
display: flex;
/*align-items: center;*/
.title-text {
width: 114px;
text-align: right;
display: inline-block;
font-weight: 500;
font-size: 14px;
margin-right: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 42px;
line-height: 42px;
}
}
.box-input {
display: inline-block;
height: 38px;
width: 100%;
}
.required {
color: red;
margin-right: 4px;
}
.title-text-text {
margin-top: 9px;
}
.header-text {
font-size: 16px;
font-weight: bold;
margin-left: 15px;
/*border-bottom: 1px #d9d9d9 dashed;*/
height: 30px;
margin-bottom: 30px;
}
.formAdd {
margin-bottom: 40px;
}
.button-text {
height: 38px;
width: calc(100% - 100px);
line-height: 38px;
background: #fff;
border: 1px #1890ff solid;
color: #1890ff;
}
</style>
+394
View File
@@ -0,0 +1,394 @@
<template>
<a-form layout="inline" @keyup.enter.native="searchQuery">
<a-row :gutter="24">
<a-col :md="6" :sm="8" v-for="(item,index) in searchList" :key="index" style="line-height: 48px">
<template v-if="index < 3">
<!-- 树形选择器 -->
<div class="box-title-text" v-if="item.field_show_type === FieldType.TREE.value">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<a-tree-select
tree-node-filter-prop="title"
class="box-input"
v-model="queryParam[item.db_field_name]"
:maxTagCount="1"
:getPopupContainer="triggerNode=> triggerNode.parentNode"
:tree-data="item.tree"
tree-checkable
:placeholder="$t('pleaseSelect')+item.db_field_txt"
/>
</div>
<!-- 输入框 -->
<div class="box-title-text"
v-else-if="[FieldType.TEXT_STRING.value, FieldType.TEXT.value,FieldType.PERSON.value,
FieldType.STANDARD.value, FieldType.TEXT_LINK.value].includes(item.field_show_type)">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<a-input class="box-input" :placeholder="$t('pleaseEnter')+item.db_field_txt"
v-model="queryParam[item.db_field_name]"></a-input>
</div>
<!-- 数字输入框 -->
<div class="box-title-text" v-else-if="item.field_show_type === FieldType.TEXT_NUMBER.value">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<a-input-number class="box-input" :placeholder="$t('pleaseEnter')+item.db_field_txt"
v-model="queryParam[item.db_field_name]" :min="1" :max="99999999"/>
</div>
<!-- 字典多选框 -->
<div class="box-title-text" v-else-if="item.field_show_type === FieldType.PULL_MORE.value">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<j-multi-select-tag class="box-input" v-model="queryParam[item.db_field_name]"
:placeholder="$t('pleaseSelect')+item.db_field_txt" :type="'select'"
:triggerChange="false" :dictCode="item.dict_field"/>
</div>
<!-- 日期区间 -->
<div class="box-title-text" v-else-if="[FieldType.DATE_SINGLE.value, FieldType.DATE_MORE.value].includes(item.field_show_type)">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<a-range-picker class="box-input" v-model="queryParam[item.db_field_name]"
:placeholder="$t('pleaseSelect')+item.db_field_txt"
format="YYYY-MM-DD" value-format="YYYY-MM-DD"
@change="onChange(item.db_field_name)"></a-range-picker>
</div>
<!-- 字典单选框 -->
<div class="box-title-text" v-else-if="item.field_show_type === FieldType.PULL_SINGLE.value">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<j-dict-select-tag class="box-input" v-model="queryParam[item.db_field_name]"
:placeholder="$t('pleaseSelect')+item.db_field_txt"
:type="'select'"
:triggerChange="false" :dictCode="item.dict_field"/>
</div>
<!-- 单选框 -->
<!-- <div class="box-title-text" v-else-if="item.field_show_type === '12'">-->
<!-- <div class="title-text" :title="item.db_field_txt">-->
<!-- <span>{{item.db_field_txt}}</span>-->
<!-- </div>-->
<!-- <a-select :placeholder="$t('pleaseSelect')+item.db_field_txt"-->
<!-- class="box-input"-->
<!-- :getPopupContainer="triggerNode=> triggerNode.parentNode"-->
<!-- allowClear-->
<!-- v-model="queryParam[item.db_field_name]">-->
<!-- <a-select-option v-for="(item, key) in item.option"-->
<!-- :key="key"-->
<!-- :value="item.value">-->
<!-- <span style="display: inline-block;width: 100%" :title=" item.name">-->
<!-- {{ item.name }}-->
<!-- </span>-->
<!-- </a-select-option>-->
<!-- </a-select>-->
<!-- </div>-->
</template>
<template v-if="index >= 3 && toggleSearchStatus">
<div class="box-title-text" v-if="item.field_show_type === FieldType.TREE.value">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<a-tree-select
tree-node-filter-prop="title"
class="box-input"
v-model="queryParam[item.db_field_name]"
:maxTagCount="1"
:getPopupContainer="triggerNode=> triggerNode.parentNode"
:tree-data="item.tree"
tree-checkable
:placeholder="$t('pleaseSelect')+item.db_field_txt"
/>
</div>
<div class="box-title-text" v-else-if="[FieldType.TEXT_STRING.value, FieldType.TEXT.value,FieldType.PERSON.value,
FieldType.STANDARD.value, FieldType.TEXT_LINK.value].includes(item.field_show_type)">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<a-input class="box-input" :placeholder="$t('pleaseEnter')+item.db_field_txt"
v-model="queryParam[item.db_field_name]"></a-input>
</div>
<div class="box-title-text" v-else-if="item.field_show_type === FieldType.TEXT_NUMBER.value">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<a-input-number class="box-input" :placeholder="$t('pleaseEnter')+item.db_field_txt"
v-model="queryParam[item.db_field_name]" :min="1" :max="99999999"/>
</div>
<div class="box-title-text" v-else-if="item.field_show_type === FieldType.PULL_MORE.value">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<j-multi-select-tag class="box-input" v-model="queryParam[item.db_field_name]"
:placeholder="$t('pleaseSelect')+item.db_field_txt" :type="'select'"
:triggerChange="false" :dictCode="item.dict_field"/>
</div>
<div class="box-title-text" v-else-if="[FieldType.DATE_SINGLE.value, FieldType.DATE_MORE.value].includes(item.field_show_type)">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<a-range-picker class="box-input" v-model="queryParam[item.db_field_name]"
format="YYYY-MM-DD" value-format="YYYY-MM-DD"
@change="onChange(item.db_field_name)"></a-range-picker>
</div>
<div class="box-title-text" v-else-if="item.field_show_type === FieldType.PULL_SINGLE.value">
<div class="title-text" :title="item.db_field_txt">
<span>{{item.db_field_txt}}</span>
</div>
<j-dict-select-tag class="box-input" v-model="queryParam[item.db_field_name]"
:placeholder="$t('pleaseSelect')+item.db_field_txt"
:type="'select'"
:triggerChange="false" :dictCode="item.dict_field"/>
</div>
<!-- <div class="box-title-text" v-else-if="item.field_show_type === '12'">-->
<!-- <div class="title-text" :title="item.db_field_txt">-->
<!-- <span>{{item.db_field_txt}}</span>-->
<!-- </div>-->
<!-- <a-select :placeholder="$t('pleaseSelect')+item.db_field_txt"-->
<!-- class="box-input"-->
<!-- :getPopupContainer="triggerNode=> triggerNode.parentNode"-->
<!-- allowClear-->
<!-- v-model="queryParam[item.db_field_name]">-->
<!-- <a-select-option v-for="(item, key) in item.option"-->
<!-- :key="key"-->
<!-- :value="item.value">-->
<!-- <span style="display: inline-block;width: 100%" :title=" item.name">-->
<!-- {{ item.name }}-->
<!-- </span>-->
<!-- </a-select-option>-->
<!-- </a-select>-->
<!-- </div>-->
</template>
</a-col>
<span style="float: right;overflow: hidden;margin-right: 11px" class="table-page-search-submitButtons">
<a-col :md="6" :sm="24">
<a @click="handleToggleSearch" v-if="searchList.length > 3">
{{ !toggleSearchStatus ? $t('open') : $t('putAway') }}
<a-icon :type="toggleSearchStatus ? 'up' : 'down'"/>
</a>
<a-button class="box-button" style="margin-left: 8px" @click="searchReset">{{$t('reset')}}</a-button>
<a-button class="box-button" style="margin-left: 8px" type="primary" @click="searchQuery">{{$t('query')}}</a-button>
</a-col>
</span>
</a-row>
</a-form>
</template>
<script>
import { getAction } from '@/api/manage'
import JDictSelectTag from '../dict/JDictSelectTag'
import JMultiSelectTag from '../dict/JMultiSelectTag'
import { FieldType } from '../../enums/commonEnums'
export default {
name: 'Search',
components: { JDictSelectTag, JMultiSelectTag },
props: {
url: {
type: Object,
default: () => {
return {}
}
},
flag: { // 暂时不清楚flag的用处,是要查searchList的时候传给后端的
type: String,
default: ''
},
searchQueryList: {
type: Array,
default: () => {
return []
}
}
},
data () {
return {
FieldType,
queryParam: {},
toggleSearchStatus: false,
searchList: []
}
},
mounted () {
// this.getSearch()
this.searchList = [
{
db_field_name: 'serial_number',
db_field_txt: '编号',
dict_field: '',
field_show_type: '1',
tree: []
},
{
db_field_name: 'title',
db_field_txt: '标题',
dict_field: '',
field_show_type: '1',
tree: []
},
{
db_field_name: 'state',
db_field_txt: '状态',
dict_field: 'state',
field_show_type: '3',
tree: []
},
{
db_field_name: 'region',
db_field_txt: '适用地区',
dict_field: 'region',
field_show_type: '4',
tree: []
},
// {
// db_field_name: 'technology_territory',
// db_field_txt: '技术领域',
// dict_field: 'technology_territory',
// field_show_type: '0',
// tree: []
// },
{
db_field_name: 'issue_time',
db_field_txt: '发布日期',
dict_field: '',
field_show_type: '5',
tree: []
},
{
db_field_name: 'xin1_che1_xing2_shi2_shi1_ri4_qi1',
db_field_txt: '新车型实施日期',
dict_field: '',
field_show_type: '5',
tree: []
},
{
db_field_name: 'implement_time',
db_field_txt: '在产车实施日期',
dict_field: '',
field_show_type: '5',
tree: []
}
]
},
methods: {
searchQuery () {
this.$emit('searchQuery', JSON.parse(JSON.stringify(this.queryParam)))
},
searchReset () {
this.queryParam = {}
this.$emit('searchQuery', JSON.parse(JSON.stringify(this.queryParam)))
},
// 获取查询条件列表
getSearch () {
const params = {
flag: this.flag
}
getAction(this.url.getSearchList, params).then((res) => {
if (res.success) {
this.searchList = res.result
if (this.searchQueryList && this.searchQueryList.length > 0) {
for (let i = 0; i < this.searchList.length; i++) {
if (this.searchList[i].db_field_name === 'issue_time') {
this.searchList.splice(i, 1)
i--
}
// db_field_name 说明可以被代替位置??
if (this.searchList[i].db_field_name === 'title') {
const data = this.searchList[i + 1]
this.searchList[i + 1] = this.searchQueryList[0]
this.searchList.push(data)
}
}
}
}
})
},
onChange (item) {
if (this.queryParam[item] && this.queryParam[item].length > 0) {
const startDate = item + '_start'
const endDate = item + '_end'
this.queryParam[startDate] = this.queryParam[item][0]
this.queryParam[endDate] = this.queryParam[item][1]
} else {
this.queryParam[item] = []
}
},
handleToggleSearch () {
this.toggleSearchStatus = !this.toggleSearchStatus
}
}
}
</script>
<style scoped lang="less">
.box-title-text {
line-height: 1.4;
display: flex;
align-items: center;
margin-bottom: 10px;
.title-text {
width: 110px;
color: #000F16;
display: inline-block;
font-weight: 500;
font-size: 14px;
margin-right: 16px;
margin-top: 3px;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.box-input {
/*min-width: 200px;*/
display: inline-block;
width: calc(100% - 116px);
height: 38px;
margin-top: 2px;
/deep/ .ant-select-selection--single {
height: 38px;
}
/deep/ .ant-select-selection--multiple {
height: 38px;
.ant-select-selection__rendered > ul > li {
margin-top: 6px;
}
}
/deep/ .ant-select-selection__rendered {
line-height: 38px;
height: 38px;
}
/deep/ .ant-calendar-picker {
line-height: 38px;
height: 38px;
}
/deep/ .ant-calendar-picker-input {
height: 38px;
}
/deep/ .ant-input-number-input-wrap {
line-height: 38px;
height: 38px;
}
}
.box-button {
height: 38px;
/*margin-top: 2px;*/
}
/deep/ .ant-select-tree {
width: auto;
height: 300px;
overflow: auto;
position: absolute;
/*background: #fff;*/
}
/deep/ .ant-select-tree-dropdown {
min-height: 320px;
}
}
</style>
+328
View File
@@ -0,0 +1,328 @@
<template>
<div style="height: 100%">
<div class="box" v-if="isTrue">
<a-table
class="table"
rowKey="id"
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
:pagination="ipagination"
:scroll="{x: '100%'}"
:components="drag(columns,'columns')"
:data-source="dataSource"
:loading="loading"
:rowClassName="rowClassName"
:columns="columns"
@change="tableOnChange"
>
<span slot="operation" slot-scope="text, record">
<a v-for="(ol, index) in operationList" :key="index" @click="operationClick(ol,record)">
<span v-if="ol.text === $t('cancelCollection')" v-has="ol.has" class="text">
{{ !record.collectFlag || record.collectFlag === 0 ? $t('collection') :$t('cancelCollection') }}
</span>
<span v-else-if="ol.text === $t('cancelSubscribe')" v-has="ol.has" class="text">
{{ !record.subscribeFlag || record.subscribeFlag === 0 ? $t('subscribe') :$t('cancelSubscribe') }}
</span>
<span v-else v-has="ol.has" class="text">
{{ ol.text }}
</span>
</a>
</span>
<span slot="detailClick" slot-scope="text,record">
<a class="textName" :title="text" @click="detailClick(record)">
{{text}}
</a>
</span>
<!-- <span slot="urlClick" slot-scope="text,record">-->
<!-- <a class="text" :title="text" @click="urlClick(text)">-->
<!-- {{text}}-->
<!-- </a>-->
<!-- </span>-->
<!-- <span slot="detailText" slot-scope="text,record">-->
<!-- <span class="text" :title="text">-->
<!-- {{text}}-->
<!-- </span>-->
<!-- </span>-->
</a-table>
</div>
</div>
</template>
<script>
import { ResizeColumnProvide, ResizeHeader } from '@/mixins/header'
import { getAction, postAction } from '@/api/manage'
export default {
name: 'TableData',
mixins: [ResizeColumnProvide, ResizeHeader],
props: {
// 接口
url: {
type: Object,
default: () => {
return {}
}
},
// 操作按钮
operationList: {
type: Array,
default: () => {
return []
}
},
flag: { // flag获取数据时传给后端的,具体作用还不知道??
type: String,
default: ''
},
// 是否显示操作;默认不显示
showAction: {
type: Boolean,
default: false
}
},
data () {
return {
/* 分页参数 */
ipagination: {
current: 1,
pageSize: 50,
pageSizeOptions: ['10', '50', '100', '150', '200'],
showTotal: (total, range) => {
return range[0] + '-' + range[1] + ' ' + this.$t('total') + ' ' + total + ' ' + this.$t('strip')
},
showQuickJumper: true,
showSizeChanger: true,
total: 0
},
columns: [],
selectedRowKeys: [],
dataSource: [],
isTrue: false,
searchParams: {},
loading: false,
orderBy: '1',
orderByField: ''
}
},
mounted () {
this.$on('searchQuery', search => {
Object.keys(search).forEach(res => {
if (search[res] instanceof Array) {
search[res] = search[res].join(',')
}
})
this.searchParams = search
this.getData()
this.getTableList()
})
this.$on('searchReset', target => {
this.searchParams = {}
this.selectedRowKeys = []
this.getData()
this.getTableList()
})
this.$on('searchGetData', target => {
this.selectedRowKeys = []
this.getData()
this.getTableList()
})
// this.getData()
// this.getTableList()
this.columns = [
{
title: '编号',
dataIndex: 'serial_number',
align: 'left',
ellipsis: true,
sorter: 'true',
width: 215,
scopedSlots: {
customRender: 'detailClick'
}
},
{
title: '标题',
dataIndex: 'title',
align: 'left',
ellipsis: true,
sorter: 'true',
width: 215,
scopedSlots: {
customRender: 'detailClick'
}
},
{
title: '状态',
dataIndex: 'state',
align: 'left',
ellipsis: true,
sorter: 'true',
width: 215
},
{
title: '适用地区',
dataIndex: 'region',
align: 'left',
ellipsis: true,
width: 215
},
{
title: '技术领域',
dataIndex: 'technologyTerritory',
align: 'left',
ellipsis: true,
width: 215
},
{
title: '发布日期',
dataIndex: 'issue_time',
align: 'left',
ellipsis: true,
width: 215
},
{
title: this.$t('operation'),
align: 'center',
fixed: 'right',
width: 200,
scopedSlots: { customRender: 'operation' }
}
]
this.dataSource = [
{
collectFlag: '0',
id: 'fd045f5c5d734087a884a63664ee91f1',
issue_time: '',
region: '',
serial_number: '0621001编号',
state: '现行',
subscribeFlag: '0',
technology_territory: '',
title: '0621001标题',
title_en: '0621001Title'
}
]
this.isTrue = true
},
beforeDestroy () {
this.$off('searchQuery')
this.$off('searchReset')
this.$off('searchGetData')
},
methods: {
rowClassName (record) {
return record.state === '废止' ? 'rowClassNameBack' : ''
},
getTextWith (text, fontStyle) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.font = fontStyle || '14px' // 设置字体样式
const dimension = context.measureText(text)
return dimension.width + 40
},
getData () {
const params = {
flag: this.flag
}
getAction(this.url.tableHeader, params).then((res) => {
if (res.success) {
const jsonHead = res.result
this.columns = []
// res.db_field_txt
jsonHead.forEach((res, index) => {
this.columns.push({
title: res.db_field_txt,
dataIndex: res.db_field_name,
align: 'left',
ellipsis: true,
sorter: res.sort,
width: 215
})
if (res.click) {
this.columns[index].scopedSlots = {
customRender: 'detailClick'
}
this.columns[index].ellipsis = false
} else if (res.urlClick) {
this.columns[index].scopedSlots = {
customRender: 'urlClick'
}
} else {
this.columns[index].scopedSlots = {
customRender: 'detailText'
}
}
})
let width = 0
if (this.operationList.length > 0) {
for (let i = 0; i < this.operationList.length; i++) {
width += this.getTextWith(this.operationList[i].text)
}
}
if (this.showAction) {
this.columns.push({
title: this.$t('operation'),
align: 'center',
fixed: 'right',
width: width,
scopedSlots: { customRender: 'operation' }
})
}
this.isTrue = false
this.$nextTick(() => {
this.isTrue = true
})
}
})
},
tableOnChange (pagination, filters, sorter) {
this.orderBy = sorter.order === 'ascend' ? '1' : '2'
this.orderByField = sorter.columnKey
this.ipagination = pagination
this.getTableList()
},
getTableList () {
const pageNo = this.ipagination.current
const pageSize = this.ipagination.pageSize
const params = {
...this.searchParams,
orderBy: this.orderBy,
orderByField: this.orderByField,
pageNo: pageNo,
pageSize: pageSize
}
this.loading = true
postAction(this.url.tableList, params).then((res) => {
if (res.success) {
if (res.result.current > 1 && res.result.records.length === 0) {
this.ipagination.current = res.result.current - 1
this.getTableList()
return
}
this.dataSource = res.result.records
this.ipagination.total = res.result.total
this.loading = false
} else {
this.loading = false
}
})
},
onSelectChange (value) {
this.selectedRowKeys = value
this.$emit('onSelectChange', value)
},
operationClick (ol, item) {
this.$emit(ol.clickEvent, item)
},
detailClick (item, index) {
this.$emit('detailClick', item)
},
urlClick (item) {
window.open(item)
}
}
}
</script>
<style scoped>
</style>
+107
View File
@@ -0,0 +1,107 @@
<template>
<a-radio-group v-if="tagType==='radio'" @change="handleInput" :value="getValueSting" :disabled="disabled">
<a-radio v-for="(item, key) in dictOptions" :key="key" :value="item.value">{{ item.text }}</a-radio>
</a-radio-group>
<a-radio-group v-else-if="tagType==='radioButton'" buttonStyle="solid" @change="handleInput" :value="getValueSting" :disabled="disabled">
<a-radio-button v-for="(item, key) in dictOptions" :key="key" :value="item.value">{{ item.text }}</a-radio-button>
</a-radio-group>
<a-select v-else-if="tagType==='select'" :getPopupContainer = "getPopupContainer" :placeholder="placeholder" :disabled="disabled" :value="getValueSting" @change="handleInput">
<a-select-option :value="undefined">请选择</a-select-option>
<a-select-option v-for="(item, key) in dictOptions" :key="key" :value="item.value">
<span style="display: inline-block;width: 100%" :title=" item.text || item.label ">
{{ item.text || item.label }}
</span>
</a-select-option>
</a-select>
</template>
<script>
import { ajaxGetDictItems, getDictItemsFromCache } from '@/api/api'
export default {
name: 'JDictSelectTag',
props: {
dictCode: String,
placeholder: String,
disabled: Boolean,
value: [String, Number],
type: String,
getPopupContainer: {
type: Function,
default: (node) => node.parentNode
}
},
data () {
return {
dictOptions: [],
tagType: ''
}
},
watch: {
dictCode: {
immediate: true,
handler () {
this.initDictData()
}
}
},
created () {
if (!this.type || this.type === 'list') {
this.tagType = 'select'
} else {
this.tagType = this.type
}
},
computed: {
getValueSting () {
// update-begin author:wangshuai date:20200601 for: 不显示placeholder的文字 ------
// 当有null或“” placeholder不显示
return this.value != null ? this.value.toString() : undefined
// update-end author:wangshuai date:20200601 for: 不显示placeholder的文字 ------
}
},
methods: {
initDictData () {
// 优先从缓存中读取字典配置
if (getDictItemsFromCache(this.dictCode)) {
this.dictOptions = getDictItemsFromCache(this.dictCode)
return
}
// 根据字典Code, 初始化字典数组
ajaxGetDictItems(this.dictCode, null).then((res) => {
if (res.success) {
this.dictOptions = res.result
}
})
},
handleInput (e = '') {
let val
if (Object.keys(e).includes('target')) {
val = e.target.value
} else {
val = e
}
console.log(val)
this.$emit('change', val)
// 解决数据规则,选择自定义SQL 规则值无法输入空格
this.$emit('input', val)
},
setCurrentDictOptions (dictOptions) {
this.dictOptions = dictOptions
},
getCurrentDictOptions () {
return this.dictOptions
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
</style>
+146
View File
@@ -0,0 +1,146 @@
/**
* 字典 util
* author: scott
* date: 20190109
*/
import { ajaxGetDictItems, getDictItemsFromCache } from '@/api/api'
// import { getAction } from '@/api/manage'
/**
* 获取字典数组
* @param dictCode 字典Code
* @return List<Map>
*/
export async function initDictOptions (dictCode) {
if (!dictCode) {
return '字典Code不能为空!'
}
// 优先从缓存中读取字典配置
if (getDictItemsFromCache(dictCode)) {
const res = {}
res.result = getDictItemsFromCache(dictCode)
res.success = true
return res
}
// 获取字典数组
return (await ajaxGetDictItems(dictCode))
}
/**
* 字典值替换文本通用方法
* @param dictOptions 字典数组
* @param text 字典值
* @return String
*/
export function filterDictText (dictOptions, text) {
// --update-begin----author:sunjianlei---date:20200323------for: 字典翻译 text 允许逗号分隔 ---
if (text != null && Array.isArray(dictOptions)) {
const result = []
// 允许多个逗号分隔,允许传数组对象
let splitText
if (Array.isArray(text)) {
splitText = text
} else {
splitText = text.toString().trim().split(',')
}
for (const txt of splitText) {
let dictText = txt
for (const dictItem of dictOptions) {
if (txt.toString() === dictItem.value.toString()) {
dictText = (dictItem.text || dictItem.title || dictItem.label)
break
}
}
result.push(dictText)
}
return result.join(',')
}
return text
// --update-end----author:sunjianlei---date:20200323------for: 字典翻译 text 允许逗号分隔 ---
}
/**
* 字典值替换文本通用方法(多选)
* @param dictOptions 字典数组
* @param text 字典值
* @return String
*/
export function filterMultiDictText (dictOptions, text) {
// js “!text” 认为0为空,所以做提前处理
if (text === 0 || text === '0') {
if (dictOptions) {
for (const dictItem of dictOptions) {
if ((text + '') === (dictItem.value + '')) {
return dictItem.text
}
}
}
}
if (!text || text === 'null' || !dictOptions || dictOptions.length === 0) {
return ''
}
let re = ''
text = text.toString()
const arr = text.split(',')
dictOptions.forEach(function (option) {
if (option) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === option.value) {
re += option.text + ','
break
}
}
}
})
if (re === '') {
return text
}
return re.substring(0, re.length - 1)
}
/**
* 翻译字段值对应的文本
* @returns string
* @param dictCode
* @param key
*/
export function filterDictTextByCache (dictCode, key) {
if (key === null || key === undefined || key.length === 0) {
return ''
}
if (!dictCode) {
return '字典Code不能为空!'
}
// 优先从缓存中读取字典配置
if (getDictItemsFromCache(dictCode)) {
const item = getDictItemsFromCache(dictCode).filter(t => t.value + '' === key + '')
if (item && item.length > 0) {
return item[0].text
}
}
}
/** 通过code获取字典数组 */
export async function getDictItems (dictCode, params) {
// 优先从缓存中读取字典配置
if (getDictItemsFromCache(dictCode)) {
return getDictItemsFromCache(dictCode).map(item => ({ ...item, label: item.text }))
}
// 缓存中没有,就请求后台
return await ajaxGetDictItems(dictCode, params).then(({ success, result }) => {
if (success) {
const res = result.map(item => ({ ...item, label: item.text }))
console.log('------- 从DB中获取到了字典-------dictCode : ', dictCode, res)
return Promise.resolve(res)
} else {
console.error('getDictItems error: : ', result)
return Promise.resolve([])
}
}).catch((res) => {
console.error('getDictItems error: ', res)
return Promise.resolve([])
})
}
+121
View File
@@ -0,0 +1,121 @@
<template>
<a-checkbox-group v-if="tagType==='checkbox'" @change="onChange" :value="arrayValue" :disabled="disabled">
<a-checkbox v-for="(item, key) in dictOptions" :key="key" :value="item.value">{{ item.text || item.label }}</a-checkbox>
</a-checkbox-group>
<a-select
v-else-if="tagType==='select'"
:value="arrayValue"
@change="onChange"
:disabled="disabled"
mode="multiple"
:placeholder="placeholder"
:getPopupContainer="getParentContainer"
optionFilterProp="children"
:filterOption="filterOption"
allowClear
:options="dictOptions">
</a-select>
</template>
<script>
import { ajaxGetDictItems, getDictItemsFromCache } from '@/api/api'
export default {
name: 'JMultiSelectTag',
props: {
dictCode: String,
placeholder: String,
disabled: Boolean,
value: String,
type: String,
options: Array,
spliter: {
type: String,
required: false,
default: ','
},
popContainer: {
type: String,
default: '',
required: false
}
},
data () {
return {
dictOptions: [],
tagType: '',
arrayValue: !this.value ? [] : this.value.split(this.spliter)
}
},
created () {
if (!this.type || this.type === 'list_multi') {
this.tagType = 'select'
} else {
this.tagType = this.type
}
// 获取字典数据
// this.initDictData();
},
watch: {
options (val) {
val.forEach((item, index) => {
this.$set(this.dictOptions, index, item)
})
},
dictCode: {
immediate: true,
handler () {
this.initDictData()
}
},
value (val) {
if (!val) {
this.arrayValue = []
} else {
this.arrayValue = this.value.split(this.spliter)
}
}
},
methods: {
initDictData () {
if (this.options && this.options.length > 0) {
this.dictOptions = [...this.options]
} else {
// 优先从缓存中读取字典配置
const cacheOption = getDictItemsFromCache(this.dictCode)
if (cacheOption && cacheOption.length > 0) {
this.dictOptions = cacheOption
return
}
// 根据字典Code, 初始化字典数组
ajaxGetDictItems(this.dictCode, null).then((res) => {
if (res.success) {
this.dictOptions = res.result
}
})
}
},
onChange (selectedValue) {
this.arrayValue = selectedValue
this.$emit('change', selectedValue.join(this.spliter))
},
getParentContainer (node) {
if (!this.popContainer) {
return node.parentNode
} else {
return document.querySelector(this.popContainer)
}
},
// update--begin--autor:lvdandan-----date:20201120------forLOWCOD-1086 下拉多选框,搜索时只字典code进行搜索不能通过字典text搜索
filterOption (input, option) {
return option.componentOptions.children[0].children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
// update--end--autor:lvdandan-----date:20201120------forLOWCOD-1086 下拉多选框,搜索时只字典code进行搜索不能通过字典text搜索
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
+254
View File
@@ -0,0 +1,254 @@
<template>
<a-select
v-if="async"
showSearch
labelInValue
:disabled="disabled"
:getPopupContainer="getParentContainer"
@search="loadData"
:placeholder="placeholder"
v-model="selectedAsyncValue"
style="width: 100%"
:filterOption="false"
@change="handleAsyncChange"
allowClear
:notFoundContent="loading ? undefined : null"
>
<a-spin v-if="loading" slot="notFoundContent" size="small"/>
<a-select-option v-for="d in options" :key="d.value" :value="d.value">{{ d.text }}</a-select-option>
</a-select>
<a-select
v-else
:getPopupContainer="getParentContainer"
showSearch
:disabled="disabled"
:placeholder="placeholder"
optionFilterProp="children"
style="width: 100%"
@change="handleChange"
:filterOption="filterOption"
v-model="selectedValue"
allowClear
:notFoundContent="loading ? undefined : null">
<a-spin v-if="loading" slot="notFoundContent" size="small"/>
<a-select-option v-for="d in options" :key="d.value" :value="d.value">{{ d.text }}</a-select-option>
</a-select>
</template>
<script>
import { ajaxGetDictItems, getDictItemsFromCache } from '@/api/api'
import debounce from 'lodash/debounce'
import { getAction } from '@api/manage'
export default {
name: 'JSearchSelectTag',
props: {
disabled: Boolean,
value: [String, Number],
dict: String,
dictOptions: Array,
async: Boolean,
placeholder: {
type: String,
default: '请选择',
required: false
},
popContainer: {
type: String,
default: '',
required: false
},
pageSize: {
type: Number,
default: 10,
required: false
},
getPopupContainer: {
type: Function,
default: null
}
},
data () {
this.loadData = debounce(this.loadData, 800)// 消抖
this.lastLoad = 0
return {
loading: false,
selectedValue: [],
selectedAsyncValue: [],
options: []
}
},
created () {
this.initDictData()
},
watch: {
value: {
immediate: true,
handler (val) {
if (!val) {
if (val === 0) {
this.initSelectValue()
} else {
this.selectedValue = []
this.selectedAsyncValue = []
}
} else {
this.initSelectValue()
}
}
},
dict: {
handler () {
this.initDictData()
}
},
dictOptions: {
deep: true,
handler (val) {
if (val && val.length > 0) {
this.options = [...val]
}
}
}
},
methods: {
initSelectValue () {
if (this.async) {
if (!this.selectedAsyncValue || !this.selectedAsyncValue.key || (this.selectedAsyncValue.key + '') !== (this.value + '')) {
console.log('这才请求后台')
getAction(`/sys/dict/loadDictItem/${this.dict}`, { key: this.value }).then(res => {
if (res.success) {
const obj = {
key: this.value,
label: res.result
}
this.selectedAsyncValue = { ...obj }
}
})
}
} else {
this.selectedValue = this.value.toString()
}
},
loadData (value) {
console.log('数据加载', value)
this.lastLoad += 1
const currentLoad = this.lastLoad
this.options = []
this.loading = true
// 字典code格式:table,text,code
getAction(`/sys/dict/loadDict/${this.dict}`, { keyword: value, pageSize: this.pageSize }).then(res => {
this.loading = false
if (res.success) {
if (currentLoad !== this.lastLoad) {
return
}
this.options = res.result
console.log('我是第一个', res)
} else {
this.$message.warning(res.message)
}
})
},
initDictData () {
if (!this.async) {
// 如果字典项集合有数据
if (this.dictOptions && this.dictOptions.length > 0) {
this.options = [...this.dictOptions]
} else {
// 根据字典Code, 初始化字典数组
let dictStr = ''
if (this.dict) {
const arr = this.dict.split(',')
if (arr[0].indexOf('where') > 0) {
const tbInfo = arr[0].split('where')
dictStr = tbInfo[0].trim() + ',' + arr[1] + ',' + arr[2] + ',' + encodeURIComponent(tbInfo[1])
} else {
dictStr = this.dict
}
if (this.dict.indexOf(',') === -1) {
// 优先从缓存中读取字典配置
if (getDictItemsFromCache(this.dictCode)) {
this.options = getDictItemsFromCache(this.dictCode)
return
}
}
ajaxGetDictItems(dictStr, null).then((res) => {
if (res.success) {
this.options = res.result
}
})
}
}
} else {
if (!this.dict) {
console.error('搜索组件未配置字典项')
} else {
// 异步一开始也加载一点数据
this.loading = true
getAction(`/sys/dict/loadDict/${this.dict}`, { pageSize: this.pageSize, keyword: '' }).then(res => {
this.loading = false
if (res.success) {
this.options = res.result
} else {
this.$message.warning(res.message)
}
})
}
}
},
filterOption (input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
handleChange (selectedValue) {
console.log('selectedValue', selectedValue)
this.selectedValue = selectedValue
this.callback()
},
handleAsyncChange (selectedObj) {
// update-begin-author:scott date:20201222 for:【搜索】搜索查询组件,删除条件,默认下拉还是上次的缓存数据,不好 JT-191
if (selectedObj) {
this.selectedAsyncValue = selectedObj
this.selectedValue = selectedObj.key
} else {
this.selectedAsyncValue = null
this.selectedValue = null
this.options = null
this.loadData('')
}
this.callback()
// update-end-author:scott date:20201222 for:【搜索】搜索查询组件,删除条件,默认下拉还是上次的缓存数据,不好 JT-191
},
callback () {
this.$emit('change', this.selectedValue)
},
setCurrentDictOptions (dictOptions) {
this.options = dictOptions
},
getCurrentDictOptions () {
return this.options
},
getParentContainer (node) {
if (typeof this.getPopupContainer === 'function') {
return this.getPopupContainer(node)
} else if (!this.popContainer) {
return node.parentNode
} else {
return document.querySelector(this.popContainer)
}
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
</style>
+181
View File
@@ -0,0 +1,181 @@
# JDictSelectTag 组件用法
----
- 从字典表获取数据,dictCode格式说明: 字典code
```html
<j-dict-select-tag v-model="queryParam.sex" placeholder="请输入用户性别"
dictCode="sex"/>
```
v-decorator用法:
```html
<j-dict-select-tag v-decorator="['sex', {}]" :triggerChange="true" placeholder="请输入用户性别"
dictCode="sex"/>
```
- 从数据库表获取字典数据,dictCode格式说明: 表名,文本字段,取值字段
```html
<j-dict-select-tag v-model="queryParam.username" placeholder="请选择用户名称"
dictCode="sys_user,realname,id"/>
```
# JDictSelectUtil.js 列表字典函数用法
----
- 第一步: 引入依赖方法
```html
import {initDictOptions, filterDictText} from '@/components/dict/JDictSelectUtil'
```
- 第二步: 在created()初始化方法执行字典配置方法
```html
//初始化字典配置
this.initDictConfig();
```
- 第三步: 实现initDictConfig方法,加载列表所需要的字典(列表上有多个字典项,就执行多次initDictOptions方法)
```html
initDictConfig() {
//初始化字典 - 性别
initDictOptions('sex').then((res) => {
if (res.success) {
this.sexDictOptions = res.result;
}
});
},
```
- 第四步: 实现字段的customRender方法
```html
customRender: (text, record, index) => {
//字典值替换通用方法
return filterDictText(this.sexDictOptions, text);
}
```
# JMultiSelectTag 多选组件
下拉/checkbox
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| placeholder |string | | placeholder |
| disabled |Boolean | | 是否禁用 |
| type |string | | 多选类型 select/checkbox 默认是select |
| dictCode |string | | 数据字典编码或者表名,显示字段名,存储字段名拼接而成的字符串,如果提供了options参数 则此参数可不填|
| options |Array | | 多选项,如果dictCode参数未提供,可以设置此参数加载多选项 |
使用示例
----
```vue
<template>
<a-form>
<a-form-item label="下拉多选" style="width: 300px">
<j-multi-select-tag
v-model="selectValue"
:options="dictOptions"
placeholder="请做出你的选择">
</j-multi-select-tag>
{{ selectValue }}
</a-form-item>
<a-form-item label="checkbox">
<j-multi-select-tag
v-model="checkboxValue"
:options="dictOptions"
type="checkbox">
</j-multi-select-tag>
{{ checkboxValue }}
</a-form-item>
</a-form >
</template>
<script>
import JMultiSelectTag from '@/components/dict/JMultiSelectTag'
export default {
components: {JMultiSelectTag},
data() {
return {
selectValue:"",
checkboxValue:"",
dictOptions:[{
label:"选项一",
value:"1"
},{
label:"选项二",
value:"2"
},{
label:"选项三",
value:"3"
}]
}
}
}
</script>
```
# JSearchSelectTag 字典表的搜索组件
下拉搜索组件,支持异步加载,异步加载用于大数据量的字典表
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| placeholder |string | | placeholder |
| disabled |Boolean | | 是否禁用 |
| dict |string | | 表名,显示字段名,存储字段名拼接而成的字符串,如果提供了dictOptions参数 则此参数可不填|
| dictOptions |Array | | 多选项,如果dict参数未提供,可以设置此参数加载多选项 |
| async |Boolean | | 是否支持异步加载,设置成true,则通过输入的内容加载远程数据,否则在本地过滤数据,默认false|
使用示例
----
```vue
<template>
<a-form>
<a-form-item label="下拉搜索" style="width: 300px">
<j-search-select-tag
placeholder="请做出你的选择"
v-model="selectValue"
:dictOptions="dictOptions">
</j-search-select-tag>
{{ selectValue }}
</a-form-item>
<a-form-item label="异步加载" style="width: 300px">
<j-search-select-tag
placeholder="请做出你的选择"
v-model="asyncSelectValue"
dict="sys_depart,depart_name,id"
:async="true">
</j-search-select-tag>
{{ asyncSelectValue }}
</a-form-item>
</a-form >
</template>
<script>
import JSearchSelectTag from '@/components/dict/JSearchSelectTag'
export default {
components: {JSearchSelectTag},
data() {
return {
selectValue:"",
asyncSelectValue:"",
dictOptions:[{
text:"选项一",
value:"1"
},{
text:"选项二",
value:"2"
},{
text:"选项三",
value:"3"
}]
}
}
}
</script>
```
+16
View File
@@ -0,0 +1,16 @@
import JDictSelectTag from './JDictSelectTag.vue'
import JMultiSelectTag from './JMultiSelectTag.vue'
import JSearchSelectTag from './JSearchSelectTag.vue'
import { filterMultiDictText, filterDictText, initDictOptions, filterDictTextByCache } from './JDictSelectUtil'
export default {
install: function (Vue) {
Vue.component('JDictSelectTag', JDictSelectTag)
Vue.component('JMultiSelectTag', JMultiSelectTag)
Vue.component('JSearchSelectTag', JSearchSelectTag)
Vue.prototype.$initDictOptions = (dictCode) => initDictOptions(dictCode)
Vue.prototype.$filterMultiDictText = (dictOptions, text) => filterMultiDictText(dictOptions, text)
Vue.prototype.$filterDictText = (dictOptions, text) => filterDictText(dictOptions, text)
Vue.prototype.$filterDictTextByCache = (...param) => filterDictTextByCache(...param)
}
}
+4
View File
@@ -0,0 +1,4 @@
@import "~ant-design-vue/lib/style/index";
// The prefix to use on all css classes from ant-pro.
@ant-pro-prefix : ant-pro;
+164
View File
@@ -0,0 +1,164 @@
<template>
<div class="j-area-linkage">
<div v-if="reloading">
<span> Reloading... </span>
</div>
<area-cascader
v-else-if="_type === enums.type[0]"
:value="innerValue"
:data="pcaa"
:level="1"
:style="{width}"
v-bind="$attrs"
v-on="_listeners"
@change="handleChange"
/>
<area-select
v-else-if="_type === enums.type[1]"
:value="innerValue"
:data="pcaa"
:level="2"
v-bind="$attrs"
v-on="_listeners"
@change="handleChange"
/>
<div v-else>
<span style="color:red;"> Bad type value: {{ _type }}</span>
</div>
</div>
</template>
<script>
import Area from '@/components/_util/Area'
export default {
name: 'JAreaLinkage',
props: {
value: {
type: String,
required: false
},
// 组件的类型,可选值:
// select 下拉样式
// cascader 级联样式(默认)
type: {
type: String,
default: 'cascader'
},
width: {
type: String,
default: '100%'
}
},
data () {
return {
pcaa: this.$Jpcaa,
innerValue: [],
usedListeners: ['change'],
enums: {
type: ['cascader', 'select']
},
reloading: false,
areaData: ''
}
},
computed: {
_listeners () {
const listeners = { ...this.$listeners }
// 去掉已使用的事件,防止冲突
this.usedListeners.forEach(key => {
delete listeners[key]
})
return listeners
},
_type () {
if (this.enums.type.includes(this.type)) {
return this.type
} else {
console.error(`JAreaLinkage的type属性只能接收指定的值(${this.enums.type.join('|')}`)
return this.enums.type[0]
}
}
},
watch: {
value: {
immediate: true,
handler () {
this.loadDataByValue(this.value)
}
}
},
created () {
this.initAreaData()
},
methods: {
/** 重新加载组件 */
reload () {
this.reloading = true
this.$nextTick(() => {
this.reloading = false
})
},
/** 通过 value 反推 options */
loadDataByValue (value) {
if (!value || value.length === 0) {
this.innerValue = []
} else {
this.initAreaData()
this.innerValue = this.areaData.getRealCode(value)
}
this.reload()
},
/** 通过地区code获取子级 */
loadDataByCode (value) {
const options = []
const data = this.pcaa[value]
if (data) {
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
options.push({ value: key, label: data[key] })
}
}
return options
} else {
return []
}
},
/** 判断是否有子节点 */
hasChildren (options) {
options.forEach(option => {
const data = this.loadDataByCode(option.value)
option.isLeaf = data.length === 0
})
},
handleChange (values) {
const value = values[values.length - 1]
this.$emit('change', value)
},
initAreaData () {
if (!this.areaData) {
this.areaData = new Area(this.$Jpcaa)
}
}
},
model: { prop: 'value', event: 'change' }
}
</script>
<style lang="less" scoped>
.j-area-linkage {
height: 40px;
/deep/ .area-cascader-wrap .area-select {
width: 100%;
}
/deep/ .area-select .area-selected-trigger {
line-height: 1.15;
}
}
</style>
+266
View File
@@ -0,0 +1,266 @@
<template>
<a-tree-select
allowClear
labelInValue
style="width: 100%"
:disabled="disabled"
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="placeholder"
:loadData="asyncLoadTreeData"
:value="treeValue"
v-bind="_attrs"
v-on="childListeners"
:treeData="treeData"
:multiple="multiple"
@change="onChange">
</a-tree-select>
</template>
<script>
import { getAction } from '@/api/manage'
export default {
name: 'JCategorySelect',
props: {
value: {
// type: String,
required: false
},
placeholder: {
type: String,
default: '请选择',
required: false
},
disabled: {
type: Boolean,
default: false,
required: false
},
condition: {
type: String,
default: '',
required: false
},
// 是否支持多选
multiple: {
type: Boolean,
default: false
},
loadTriggleChange: {
type: Boolean,
default: false,
required: false
},
pid: {
type: String,
default: '',
required: false
},
pCode: {
type: String,
default: '',
required: false
},
back: {
type: String,
default: '',
required: false
}
},
data () {
return {
treeValue: '',
treeData: [],
url: '/sys/category/loadTreeData',
view: '/sys/category/loadDictItem/',
tableName: '',
text: '',
code: ''
}
},
computed: {
_attrs () {
return { ...this.$attrs }
},
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
}
},
watch: {
value () {
this.loadItemByCode()
},
pCode () {
this.loadRoot()
}
},
created () {
this.validateProp().then(() => {
this.loadRoot()
this.loadItemByCode()
})
},
methods: {
/** 加载一级节点 */
loadRoot () {
const param = {
pid: this.pid,
pcode: !this.pCode ? '0' : this.pCode,
condition: this.condition
}
getAction(this.url, param).then(res => {
if (res.success && res.result) {
for (const i of res.result) {
i.value = i.key
if (i.leaf === false) {
i.isLeaf = false
} else if (i.leaf === true) {
i.isLeaf = true
}
}
this.treeData = [...res.result]
} else {
console.log('树一级节点查询结果-else', res)
}
})
},
/** 数据回显 */
loadItemByCode () {
if (!this.value || this.value === '0') {
this.treeValue = []
} else {
getAction(this.view, { ids: this.value }).then(res => {
if (res.success) {
if (res.result && res.result.length > 0) {
const values = this.value.split(',')
this.treeValue = res.result.map((item, index) => ({
key: values[index],
value: values[index],
label: item
}))
this.onLoadTriggleChange(res.result[0])
}
}
})
}
},
onLoadTriggleChange (text) {
// 只有单选才会触发
if (!this.multiple && this.loadTriggleChange) {
this.backValue(this.value, text)
}
},
backValue (value, label) {
const obj = {}
if (this.back) {
obj[this.back] = label
}
/*
* 使用$listeners向上暴露事件---和$emit一起使用出现的问题:change事件会执行两遍
* 解决办法:改变选中时提交的事件名 */
this.$emit('change', value, obj)
},
asyncLoadTreeData (treeNode) {
return new Promise((resolve) => {
if (treeNode.$vnode.children) {
resolve()
return
}
const pid = treeNode.$vnode.key
const param = {
pid: pid,
condition: this.condition
}
getAction(this.url, param).then(res => {
if (res.success) {
for (const i of res.result) {
i.value = i.key
if (i.leaf === false) {
i.isLeaf = false
} else if (i.leaf === true) {
i.isLeaf = true
}
}
this.addChildren(pid, res.result, this.treeData)
this.treeData = [...this.treeData]
}
resolve()
})
})
},
addChildren (pid, children, treeArray) {
if (treeArray && treeArray.length > 0) {
for (const item of treeArray) {
if (item.key + '' === pid + '') {
if (!children || children.length === 0) {
item.isLeaf = true
} else {
item.children = children
}
break
} else {
this.addChildren(pid, children, item.children)
}
}
}
},
onChange (value) {
if (!value) {
/*
* 使用$listeners向上暴露事件---和$emit一起使用出现的问题:change事件会执行两遍
* 解决办法:改变选中时提交的事件名 */
this.$emit('change', '')
this.treeValue = ''
} else if (Array.isArray(value)) {
const labels = []
const values = value.map(item => {
labels.push(item.label)
return item.value
})
this.backValue(values.join(','), labels.join(','))
this.treeValue = value
} else {
this.backValue(value.value, value.label)
this.treeValue = value
}
},
getCurrTreeData () {
return this.treeData
},
validateProp () {
const myCondition = this.condition
return new Promise((resolve, reject) => {
if (!myCondition) {
resolve()
} else {
try {
const test = JSON.parse(myCondition)
if (typeof test === 'object' && test) {
resolve()
} else {
this.$message.error('组件JTreeSelect-condition传值有误,需要一个json字符串!')
reject()
}
} catch (e) {
this.$message.error('组件JTreeSelect-condition传值有误,需要一个json字符串!')
reject()
}
}
})
}
},
// 2.2新增 在组件内定义 指定父组件调用时候的传值属性和事件类型 这个牛逼
model: {
prop: 'value',
event: 'change'
}
}
</script>
+46
View File
@@ -0,0 +1,46 @@
<template>
<a-checkbox-group :options="options"
:value="checkboxArray"
v-bind="$attrs"
@change="onChange" />
</template>
<script>
export default {
name: 'JCheckbox',
props: {
value: {
type: String,
required: false
},
/* label value */
options: {
type: Array,
required: true
}
},
data () {
return {
checkboxArray: !this.value ? [] : this.value.split(',')
}
},
watch: {
value (val) {
if (!val) {
this.checkboxArray = []
} else {
this.checkboxArray = this.value.split(',')
}
}
},
methods: {
onChange (checkedValues) {
this.$emit('change', checkedValues.join(','))
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
+482
View File
@@ -0,0 +1,482 @@
<template>
<div v-bind="fullScreenParentProps">
<a-icon v-if="fullScreen" class="full-screen-icon" :type="iconType" @click="()=>fullCoder=!fullCoder" />
<div class="code-editor-cust full-screen-child">
<textarea ref="textarea"></textarea>
<span @click="nullTipClick" class="null-tip" :class="{'null-tip-hidden':hasCode}" :style="nullTipStyle">{{ placeholderShow }}</span>
<template v-if="languageChange">
<a-select v-model="mode" size="small" class="code-mode-select" @change="changeMode" placeholder="请选择主题">
<a-select-option
v-for="mode in modes"
:key="mode.value"
:value="mode.value">
{{ mode.label }}
</a-select-option>
</a-select>
</template>
</div>
</div>
</template>
<script type="text/ecmascript-6">
// 引入全局实例
import _CodeMirror from 'codemirror'
// 核心样式
import 'codemirror/lib/codemirror.css'
// 引入主题后还需要在 options 中指定主题才会生效 darcula gruvbox-dark hopscotch monokai
import 'codemirror/theme/panda-syntax.css'
// 提示css
import 'codemirror/addon/hint/show-hint.css'
// 需要引入具体的语法高亮库才会有对应的语法高亮效果
// codemirror 官方其实支持通过 /addon/mode/loadmode.js 和 /mode/meta.js 来实现动态加载对应语法高亮库
// 但 vue 貌似没有无法在实例初始化后再动态加载对应 JS ,所以此处才把对应的 JS 提前引入
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/mode/css/css.js'
import 'codemirror/mode/xml/xml.js'
import 'codemirror/mode/clike/clike.js'
import 'codemirror/mode/markdown/markdown.js'
import 'codemirror/mode/python/python.js'
import 'codemirror/mode/r/r.js'
import 'codemirror/mode/shell/shell.js'
import 'codemirror/mode/sql/sql.js'
import 'codemirror/mode/swift/swift.js'
import 'codemirror/mode/vue/vue.js'
import { isIE11, isIE } from '@/utils/browser'
// 尝试获取全局实例
const CodeMirror = window.CodeMirror || _CodeMirror
export default {
name: 'JCodeEditor',
props: {
// 外部传入的内容,用于实现双向绑定
value: {
type: String,
default: ''
},
// 外部传入的语法类型
language: {
type: String,
default: null
},
languageChange: {
type: Boolean,
default: false,
required: false
},
placeholder: {
type: String,
default: null
},
// 显示行号
lineNumbers: {
type: Boolean,
default: true
},
// 是否显示全屏按钮
fullScreen: {
type: Boolean,
default: false
},
// 全屏以后的z-index
zIndex: {
type: [Number, String],
default: 999
},
// 是否自适应高度,可以传String或Boolean
// 传 String 类型只能写"!ie"
// 填写这个字符串,代表其他浏览器自适应高度
// 唯独IE下不自适应高度,因为IE下不支持min、max-height样式
// 如果填写的不是"!ie"就视为true
autoHeight: {
type: [String, Boolean],
default: true
},
// 不自适应高度的情况下生效的固定高度
height: {
type: [String, Number],
default: '240px'
}
},
data () {
return {
// 内部真实的内容
code: '',
iconType: 'fullscreen',
hasCode: false,
// 默认的语法类型
mode: 'javascript',
// 编辑器实例
coder: null,
// 默认配置
options: {
// 缩进格式
tabSize: 2,
// 主题,对应主题库 JS 需要提前引入
theme: 'panda-syntax',
line: true,
// extraKeys: {'Ctrl': 'autocomplete'},//自定义快捷键
hintOptions: {
tables: {
users: ['name', 'score', 'birthDate'],
countries: ['name', 'population', 'size']
}
}
},
// 支持切换的语法高亮类型,对应 JS 已经提前引入
// 使用的是 MIME-TYPE ,不过作为前缀的 text/ 在后面指定时写死了
modes: [{
value: 'css',
label: 'CSS'
}, {
value: 'javascript',
label: 'Javascript'
}, {
value: 'html',
label: 'XML/HTML'
}, {
value: 'x-java',
label: 'Java'
}, {
value: 'x-objectivec',
label: 'Objective-C'
}, {
value: 'x-python',
label: 'Python'
}, {
value: 'x-rsrc',
label: 'R'
}, {
value: 'x-sh',
label: 'Shell'
}, {
value: 'x-sql',
label: 'SQL'
}, {
value: 'x-swift',
label: 'Swift'
}, {
value: 'x-vue',
label: 'Vue'
}, {
value: 'markdown',
label: 'Markdown'
}],
// code 编辑器 是否全屏
fullCoder: false
}
},
watch: {
fullCoder: {
handler (value) {
if (value) {
this.iconType = 'fullscreen-exit'
} else {
this.iconType = 'fullscreen'
}
}
},
// value: {
// immediate: false,
// handler(value) {
// this._getCoder().then(() => {
// this.coder.setValue(value)
// })
// }
// },
language: {
immediate: true,
handler (language) {
this._getCoder().then(() => {
// 尝试从父容器获取语法类型
if (language) {
// 获取具体的语法类型对象
const modeObj = this._getLanguage(language)
// 判断父容器传入的语法是否被支持
if (modeObj) {
this.mode = modeObj.label
this.coder.setOption('mode', `text/${modeObj.value}`)
}
}
})
}
}
},
computed: {
placeholderShow () {
if (this.placeholder == null) {
return `请在此输入${this.language}代码`
} else {
return this.placeholder
}
},
nullTipStyle () {
if (this.lineNumbers) {
return { left: '36px' }
} else {
return { left: '12px' }
}
},
// coder 配置
coderOptions () {
return {
tabSize: this.options.tabSize,
theme: this.options.theme,
lineNumbers: this.lineNumbers,
line: true,
hintOptions: this.options.hintOptions
}
},
isAutoHeight () {
let { autoHeight } = this
if (typeof autoHeight === 'string' && autoHeight.toLowerCase().trim() === '!ie') {
autoHeight = !(isIE() || isIE11())
} else {
autoHeight = true
}
return autoHeight
},
fullScreenParentProps () {
const props = {
class: {
'full-screen-parent': true,
'full-screen': this.fullCoder,
'auto-height': this.isAutoHeight
},
style: {}
}
if (isIE() || isIE11()) {
props.style.height = '240px'
}
if (this.fullCoder) {
props.style['z-index'] = this.zIndex
}
if (!this.isAutoHeight) {
props.style.height = (typeof this.height === 'number' ? this.height + 'px' : this.height)
}
return props
}
},
mounted () {
// 初始化
this._initialize()
},
methods: {
// 初始化
_initialize () {
// 初始化编辑器实例,传入需要被实例化的文本域对象和默认配置
this.coder = CodeMirror.fromTextArea(this.$refs.textarea, this.coderOptions)
// 编辑器赋值
if (this.value || this.code) {
this.hasCode = true
// this.coder.setValue(this.value || this.code)
this.setCodeContent(this.value || this.code)
} else {
this.coder.setValue('')
this.hasCode = false
}
// 支持双向绑定
this.coder.on('change', (coder) => {
this.code = coder.getValue()
this.hasCode = !!this.code
if (this.$emit) {
this.$emit('input', this.code)
}
})
this.coder.on('focus', () => {
this.hasCode = true
})
this.coder.on('blur', () => {
this.hasCode = !!this.code
})
/* this.coder.on('cursorActivity',()=>{
this.coder.showHint()
}) */
},
getCodeContent () {
return this.code
},
setCodeContent (val) {
setTimeout(() => {
if (!val) {
this.coder.setValue('')
} else {
this.coder.setValue(val)
}
}, 300)
},
// 获取当前语法类型
_getLanguage (language) {
// 在支持的语法类型列表中寻找传入的语法类型
return this.modes.find((mode) => {
// 所有的值都忽略大小写,方便比较
const currentLanguage = language.toLowerCase()
const currentLabel = mode.label.toLowerCase()
const currentValue = mode.value.toLowerCase()
// 由于真实值可能不规范,例如 java 的真实值是 x-java ,所以讲 value 和 label 同时和传入语法进行比较
return currentLabel === currentLanguage || currentValue === currentLanguage
})
},
_getCoder () {
const _this = this
return new Promise((resolve) => {
(function get () {
if (_this.coder) {
resolve(_this.coder)
} else {
setTimeout(get, 10)
}
})()
})
},
// 更改模式
changeMode (val) {
// 修改编辑器的语法配置
this.coder.setOption('mode', `text/${val}`)
// 获取修改后的语法
const label = this._getLanguage(val).label.toLowerCase()
// 允许父容器通过以下函数监听当前的语法值
this.$emit('language-change', label)
},
nullTipClick () {
this.coder.focus()
}
}
}
</script>
<style lang="less">
.code-editor-cust {
flex-grow: 1;
display: flex;
position: relative;
height: 100%;
.CodeMirror {
flex-grow: 1;
z-index: 1;
.CodeMirror-code {
line-height: 19px;
}
}
.code-mode-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 130px;
}
.CodeMirror {
height: auto;
min-height: 100%;
}
.null-tip {
position: absolute;
top: 4px;
left: 36px;
z-index: 10;
color: #ffffffc9;
line-height: initial;
}
.null-tip-hidden {
display: none;
}
/**选中样式偶然出现高度不够的情况*/
.CodeMirror-selected {
min-height: 19px !important;
}
}
/* 全屏样式 */
.full-screen-parent {
position: relative;
.full-screen-icon {
opacity: 0;
color: black;
width: 20px;
height: 20px;
line-height: 24px;
background-color: white;
position: absolute;
top: 2px;
right: 2px;
z-index: 9;
cursor: pointer;
transition: opacity 0.3s;
}
&:hover {
.full-screen-icon {
opacity: 1;
&:hover {
background-color: rgba(255, 255, 255, 0.88);
}
}
}
&.full-screen {
position: fixed;
top: 10px;
left: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
padding: 10px;
background-color: #f5f5f5;
.full-screen-icon {
top: 12px;
right: 12px;
}
.full-screen-child {
height: 100%;
max-height: 100%;
min-height: 100%;
}
}
.full-screen-child {
height: 100%;
}
&.auto-height {
.full-screen-child {
min-height: 120px;
max-height: 320px;
height: unset;
overflow: hidden;
}
&.full-screen .full-screen-child {
height: 100%;
max-height: 100%;
min-height: 100%;
}
}
}
.CodeMirror-cursor {
height: 18.4px !important;
}
</style>
+65
View File
@@ -0,0 +1,65 @@
<template>
<div class="components-input-demo-presuffix">
<a-input @click="openModal" placeholder="cron表达式" v-model="cron" @change="(e)=>handleOK(e.target.value)">
<a-icon slot="prefix" type="schedule" title="cron控件"/>
<a-icon v-if="cron" slot="suffix" type="close-circle" @click="handleEmpty" title="清空"/>
</a-input>
<JCronModal ref="innerVueCron" :data="cron" @ok="handleOK"></JCronModal>
</div>
</template>
<script>
import JCronModal from './modal/JCronModal'
export default {
name: 'JCron',
components: {
JCronModal
},
props: {
value: {
required: false,
type: String
}
},
data () {
return {
cron: this.value
}
},
watch: {
value (val) {
this.cron = val
}
},
methods: {
openModal () {
this.$refs.innerVueCron.show()
},
handleOK (val) {
this.cron = val
this.$emit('change', this.cron)
// this.$emit("change", Object.assign({}, this.cron));
},
handleEmpty () {
this.handleOK('')
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
.components-input-demo-presuffix .anticon-close-circle {
cursor: pointer;
color: #ccc;
transition: color 0.3s;
font-size: 12px;
}
.components-input-demo-presuffix .anticon-close-circle:hover {
color: #f5222d;
}
.components-input-demo-presuffix .anticon-close-circle:active {
color: #666;
}
</style>
+165
View File
@@ -0,0 +1,165 @@
<template>
<span>
<!--YYYY-MM-DD HH:mm:ss-->
<a-date-picker
v-if="showType === 'time'"
dropdownClassName="j-date-picker"
:disabled="disabled || readOnly"
:placeholder="placeholder"
@change="handleDateChange"
:value="momVal"
:showTime="true"
:format="dateFormat"
v-bind="$attrs"
v-on="childListeners"
:getCalendarContainer="getCalendarContainer">
</a-date-picker>
<!--YYYY-MM-DD-->
<a-date-picker
v-if="showType === 'day'"
dropdownClassName="j-date-picker"
:disabled="disabled || readOnly"
:placeholder="placeholder"
@change="handleDateChange"
:value="momVal"
:showTime="false"
:format="dateFormat"
v-bind="$attrs"
v-on="childListeners"
:getCalendarContainer="getCalendarContainer">
</a-date-picker>
<!--YYYY-MM-->
<a-month-picker
v-if="showType === 'month'"
:placeholder="placeholder"
:value="momVal"
:disabled="disabled || readOnly"
:format="dateFormat"
v-bind="$attrs"
v-on="childListeners"
@change="handleMonthChange">
</a-month-picker>
<!--YYYY-->
<a-date-picker
v-if="showType === 'year'"
:placeholder="placeholder"
mode="year"
:format="dateFormat"
:value="year"
:disabled="disabled || readOnly"
:open="yearShowOne"
v-bind="$attrs"
v-on="childListeners"
:getCalendarContainer="getCalendarContainer"
@openChange="openChangeOne"
@panelChange="panelChangeOne">
</a-date-picker>
</span>
</template>
<script>
import moment from 'moment'
export default {
name: 'JDate',
props: {
placeholder: {
type: String,
default: '请选择',
required: false
},
value: {
type: String,
required: false
},
dateFormat: {
type: String,
default: 'YYYY-MM-DD',
required: false
},
readOnly: {
type: Boolean,
required: false,
default: false
},
disabled: {
type: Boolean,
required: false,
default: false
},
// 控制选择时分秒
showType: {
type: String,
required: false,
default: 'time'
},
getCalendarContainer: {
type: Function,
default: (node) => node.parentNode
}
},
data () {
const dateStr = this.value
return {
yearShowOne: false, // 控制年份模态框的显示与否
year: '', // mode==“year” 专用
momVal: !dateStr ? null : moment(dateStr, this.dateFormat)
}
},
computed: {
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
}
},
watch: {
value (val) {
const fm = this.dateFormat
moment.prototype.toJSON = function () {
return moment(this).format(fm)
}
if (!val) {
this.momVal = null
} else {
this.momVal = moment(val, this.dateFormat)
}
}
},
methods: {
moment,
handleDateChange (mom, dateStr) {
this.$emit('change', dateStr)
},
handleMonthChange (mom, dateStr) {
this.$emit('change', dateStr)
},
/**
* 弹出日历和关闭日历的回调
* status:打开或关闭的状态
* */
openChangeOne (status) {
this.yearShowOne = status
},
// 得到年份选择器的值
panelChangeOne (value) {
this.yearShowOne = false
const year = moment(value, this.dateFormat).year().toString() // 处理成字符串
this.year = value // 组件显示的
this.$emit('change', year)
}
},
// 2.2新增 在组件内定义 指定父组件调用时候的传值属性和事件类型
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
.ant-calendar-picker {
min-width: 195px;
}
</style>
+317
View File
@@ -0,0 +1,317 @@
<template>
<div class="j-easy-cron">
<div class="content">
<div>
<a-tabs size="small" v-model="curtab">
<a-tab-pane tab="秒" key="second" v-if="!hideSecond">
<second-ui v-model="second" :disabled="disabled"></second-ui>
</a-tab-pane>
<a-tab-pane tab="分" key="minute">
<minute-ui v-model="minute" :disabled="disabled"></minute-ui>
</a-tab-pane>
<a-tab-pane tab="时" key="hour">
<hour-ui v-model="hour" :disabled="disabled"></hour-ui>
</a-tab-pane>
<a-tab-pane tab="日" key="day">
<day-ui v-model="day" :week="week" :disabled="disabled"></day-ui>
</a-tab-pane>
<a-tab-pane tab="月" key="month">
<month-ui v-model="month" :disabled="disabled"></month-ui>
</a-tab-pane>
<a-tab-pane tab="周" key="week">
<week-ui v-model="week" :day="day" :disabled="disabled"></week-ui>
</a-tab-pane>
<a-tab-pane tab="年" key="year" v-if="!hideYear && !hideSecond">
<year-ui v-model="year" :disabled="disabled"></year-ui>
</a-tab-pane>
</a-tabs>
</div>
<a-divider />
<!-- 执行时间预览 -->
<a-row :gutter="8">
<a-col :span="18" style="margin-top: 22px;">
<a-row :gutter="8">
<a-col :span="8" style="margin-bottom: 8px;">
<a-input addon-before="" v-model="inputValues.second" @blur="onInputBlur" />
</a-col>
<a-col :span="8" style="margin-bottom: 8px;">
<a-input addon-before="" v-model="inputValues.minute" @blur="onInputBlur" />
</a-col>
<a-col :span="8" style="margin-bottom: 8px;">
<a-input addon-before="" v-model="inputValues.hour" @blur="onInputBlur" />
</a-col>
<a-col :span="8" style="margin-bottom: 8px;">
<a-input addon-before="" v-model="inputValues.day" @blur="onInputBlur" />
</a-col>
<a-col :span="8" style="margin-bottom: 8px;">
<a-input addon-before="" v-model="inputValues.month" @blur="onInputBlur" />
</a-col>
<a-col :span="8" style="margin-bottom: 8px;">
<a-input addon-before="" v-model="inputValues.week" @blur="onInputBlur" />
</a-col>
<a-col :span="8" style="margin-bottom: 8px;">
<a-input addon-before="" v-model="inputValues.year" @blur="onInputBlur" />
</a-col>
<a-col :span="16" style="margin-bottom: 8px;">
<a-input addon-before="Cron" v-model="inputValues.cron" @blur="onInputCronBlur" />
</a-col>
</a-row>
</a-col>
<a-col :span="6">
<div>近十次执行时间不含年</div>
<a-textarea type="textarea" :value="preTimeList" :rows="5" />
</a-col>
</a-row>
</div>
</div>
</template>
<script>
import SecondUi from './tabs/second'
import MinuteUi from './tabs/minute'
import HourUi from './tabs/hour'
import DayUi from './tabs/day'
import WeekUi from './tabs/week'
import MonthUi from './tabs/month'
import YearUi from './tabs/year'
import CronParser from 'cron-parser'
import dateFormat from './format-date'
import { simpleDebounce } from '@/utils/util'
import ACol from 'ant-design-vue/es/grid/Col'
export default {
name: 'easy-cron',
components: {
ACol,
SecondUi,
MinuteUi,
HourUi,
DayUi,
WeekUi,
MonthUi,
YearUi
},
props: {
cronValue: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
hideSecond: {
type: Boolean,
default: false
},
hideYear: {
type: Boolean,
default: false
},
remote: {
type: Function,
default: null
}
},
data () {
return {
curtab: this.hideSecond ? 'minute' : 'second',
second: '*',
minute: '*',
hour: '*',
day: '*',
month: '*',
week: '?',
year: '*',
inputValues: { second: '', minute: '', hour: '', day: '', month: '', week: '', year: '', cron: '' },
preTimeList: '执行预览,会忽略年份参数'
}
},
computed: {
cronValue_c () {
const result = []
if (!this.hideSecond) result.push(this.second ? this.second : '*')
result.push(this.minute ? this.minute : '*')
result.push(this.hour ? this.hour : '*')
result.push(this.day ? this.day : '*')
result.push(this.month ? this.month : '*')
result.push(this.week ? this.week : '?')
if (!this.hideYear && !this.hideSecond) result.push(this.year ? this.year : '*')
return result.join(' ')
},
cronValue_c2 () {
const v = this.cronValue_c
if (this.hideYear || this.hideSecond) return v
const vs = v.split(' ')
if (vs.length >= 6) {
// 将 Quartz 星期 的规则转换为 CronParser 的规则
vs[5] = this.convertQuartzWeekToCParser(vs[5])
}
return vs.slice(0, vs.length - 1).join(' ')
}
},
watch: {
cronValue (newVal) {
if (newVal === this.cronValue_c) {
// console.info('same cron value: ' + newVal)
return
}
this.formatValue()
},
cronValue_c (newVal) {
this.calTriggerList()
this.$emit('change', newVal)
this.assignInput()
},
minute () {
if (this.second === '*') {
this.second = '0'
}
},
hour () {
if (this.minute === '*') {
this.minute = '0'
}
},
day (day) {
if (day !== '?' && this.hour === '*') {
this.hour = '0'
}
},
week (week) {
if (week !== '?' && this.hour === '*') {
this.hour = '0'
}
},
month () {
if (this.day === '?' && this.week === '*') {
this.week = '1'
} else if (this.week === '?' && this.day === '*') {
this.day = '1'
}
},
year () {
if (this.month === '*') {
this.month = '1'
}
}
},
created () {
this.formatValue()
this.$nextTick(() => {
this.calTriggerListInner()
})
},
methods: {
assignInput () {
Object.assign(this.inputValues, {
second: this.second,
minute: this.minute,
hour: this.hour,
day: this.day,
month: this.month,
week: this.week,
year: this.year,
cron: this.cronValue_c
})
},
formatValue () {
if (!this.cronValue) return
const values = this.cronValue.split(' ').filter(item => !!item)
if (!values || values.length <= 0) return
let i = 0
if (!this.hideSecond) this.second = values[i++]
if (values.length > i) this.minute = values[i++]
if (values.length > i) this.hour = values[i++]
if (values.length > i) this.day = values[i++]
if (values.length > i) this.month = values[i++]
if (values.length > i) this.week = values[i++]
if (values.length > i) this.year = values[i]
this.assignInput()
},
// 将 Quartz 星期 的规则转换为 CronParser 的规则:
// Quartz 的规则:1 = 周日,2 = 周一,3 = 周二,4 = 周三,5 = 周四,6 = 周五,7 = 周六
// CronParser 的规则: 0 = 周日,1 = 周一,2 = 周二,3 = 周三,4 = 周四,5 = 周五,6 = 周六,7 = 周日
convertQuartzWeekToCParser (week) {
const convert = (v) => {
if (v === '0') {
return '1'
}
if (v === '1') {
return '0'
}
return (Number.parseInt(v) - 1).toString()
}
// 匹配示例 1-7 or 1/7
const patten1 = /^([0-7])([-/])([0-7])$/
// 匹配示例 1,4,7
const patten2 = /^([0-7])(,[0-7])+$/
if (/^[0-7]$/.test(week)) {
return convert(week)
} else if (patten1.test(week)) {
return week.replace(patten1, ($0, before, separator, after) => {
if (separator === '/') {
return convert(before) + separator + after
} else {
return convert(before) + separator + convert(after)
}
})
} else if (patten2.test(week)) {
return week.split(',').map(v => convert(v)).join(',')
}
return week
},
calTriggerList: simpleDebounce(function () {
this.calTriggerListInner()
}, 500),
calTriggerListInner () {
// 设置了回调函数
if (this.remote) {
this.remote(this.cronValue_c, +new Date(), v => {
this.preTimeList = v
})
return
}
const format = 'yyyy-MM-dd hh:mm:ss'
const options = {
currentDate: dateFormat(new Date(), format)
}
const iter = CronParser.parseExpression(this.cronValue_c2, options)
const result = []
for (let i = 1; i <= 10; i++) {
result.push(dateFormat(new Date(iter.next()), format))
}
this.preTimeList = result.length > 0 ? result.join('\n') : '无执行时间'
},
onInputBlur () {
this.second = this.inputValues.second
this.minute = this.inputValues.minute
this.hour = this.inputValues.hour
this.day = this.inputValues.day
this.month = this.inputValues.month
this.week = this.inputValues.week
this.year = this.inputValues.year
},
onInputCronBlur (event) {
this.$emit('change', event.target.value)
}
},
model: {
prop: 'cronValue',
event: 'change'
}
}
</script>
<style scoped lang="less">
.j-easy-cron {
/deep/ .content {
.ant-checkbox-wrapper + .ant-checkbox-wrapper {
margin-left: 0;
}
}
}
</style>
@@ -0,0 +1,99 @@
<template>
<div class="input-cron">
<a-input :placeholder="placeholder" v-model="editCronValue" :disabled="disabled">
<a slot="addonAfter" @click="showConfigDlg" class="config-btn" :disabled="disabled">
<a-icon type="setting"></a-icon>
选择
</a>
</a-input>
<j-modal :visible.sync="show" title="Cron表达式" width="800px">
<easy-cron
v-model="editCronValue"
:exeStartTime="exeStartTime"
:hideYear="hideYear"
:remote="remote"
:hideSecond="hideSecond"
style="width: 100%"
></easy-cron>
</j-modal>
</div>
</template>
<script>
import EasyCron from './EasyCron.vue'
export default {
name: 'input-cron',
components: { EasyCron },
model: {
prop: 'cronValue',
event: 'change'
},
props: {
cronValue: {
type: String,
default: ''
},
width: {
type: String,
default: '800px'
},
placeholder: {
type: String,
default: '请输入cron表达式'
},
disabled: {
type: Boolean,
default: false
},
exeStartTime: {
type: [Number, String, Object],
default: 0
},
hideSecond: {
type: Boolean,
default: false
},
hideYear: {
type: Boolean,
default: false
},
remote: {
type: Function,
default: null
}
},
data () {
return {
editCronValue: this.cronValue,
show: false
}
},
watch: {
cronValue (newVal) {
if (newVal === this.editCronValue) {
return
}
this.editCronValue = newVal
},
editCronValue (newVal) {
this.$emit('change', newVal)
}
},
methods: {
showConfigDlg () {
if (!this.disabled) {
this.show = true
}
}
}
}
</script>
<style scoped>
.config-btn {
cursor: pointer;
}
</style>
@@ -0,0 +1,37 @@
const dateFormat = (date, block) => {
if (!date) {
return ''
}
let format = block || 'yyyy-MM-dd'
date = new Date(date)
const map = {
M: date.getMonth() + 1, // 月份
d: date.getDate(), // 日
h: date.getHours(), // 小时
m: date.getMinutes(), // 分
s: date.getSeconds(), // 秒
q: Math.floor((date.getMonth() + 3) / 3), // 季度
S: date.getMilliseconds() // 毫秒
}
format = format.replace(/([yMdhmsqS])+/g, (all, t) => {
let v = map[t]
if (v !== undefined) {
if (all.length > 1) {
v = `0${v}`
v = v.substr(v.length - 2)
}
return v
} else if (t === 'y') {
return (date.getFullYear().toString()).substr(4 - all.length)
}
return all
})
return format
}
export default dateFormat
+6
View File
@@ -0,0 +1,6 @@
// 原开源项目地址:https://gitee.com/toktok/easy-cron
import InputCron from './InputCron.vue'
InputCron.name = 'JEasyCron'
export default InputCron
@@ -0,0 +1,21 @@
export const WEEK_MAP_EN = {
SUN: '1',
MON: '2',
TUE: '3',
WED: '4',
THU: '5',
FRI: '6',
SAT: '7'
}
export const replaceWeekName = (c) => {
// console.info('after: ' + c)
if (c) {
Object.keys(WEEK_MAP_EN).forEach(k => {
c = c.replace(new RegExp(k, 'g'), WEEK_MAP_EN[k])
})
// c = c.replace(new RegExp('7', 'g'), '0')
}
// console.info('after: ' + c)
return c
}
+106
View File
@@ -0,0 +1,106 @@
<template>
<div class="config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio value="TYPE_NOT_SET" class="choice" :disabled="disableChoice">不设置</a-radio>
<span class="tip-info">日和周只能设置其中之一</span>
</div>
<div class="item">
<a-radio value="TYPE_EVERY" class="choice" :disabled="disableChoice">每日</a-radio>
</div>
<div class="item">
<a-radio value="TYPE_RANGE" class="choice" :disabled="disableChoice">区间</a-radio>
<a-input-number :disabled="type!==TYPE_RANGE || disableChoice" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueRange.start" />
<a-input-number :disabled="type!==TYPE_RANGE || disableChoice" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueRange.end" />
</div>
<div class="item">
<a-radio value="TYPE_LOOP" class="choice" :disabled="disableChoice">循环</a-radio>
<a-input-number :disabled="type!==TYPE_LOOP || disableChoice" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueLoop.start" />
日开始间隔
<a-input-number :disabled="type!==TYPE_LOOP || disableChoice" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueLoop.interval" />
</div>
<div class="item">
<a-radio value="TYPE_WORK" class="choice" :disabled="disableChoice">工作日</a-radio>
本月
<a-input-number :disabled="type!==TYPE_WORK || disableChoice" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueWork" />
最近的工作日
</div>
<div class="item">
<a-radio value="TYPE_LAST" class="choice" :disabled="disableChoice">最后一日</a-radio>
</div>
<div class="item">
<a-radio value="TYPE_SPECIFY" class="choice" :disabled="disableChoice">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<template v-for="i of specifyRange">
<a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{ i }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script>
import mixin from './mixin'
export default {
name: 'day',
mixins: [mixin],
props: {
week: {
type: String,
default: '?'
}
},
data () {
return {}
},
computed: {
disableChoice () {
return (this.week && this.week !== '?') || this.disabled
}
},
watch: {
value_c () {
// 数值变化
this.updateValue()
},
week () {
// console.info('new week: ' + newVal)
this.updateValue()
}
},
methods: {
updateValue () {
this.$emit('change', this.disableChoice ? '?' : this.value_c)
}
},
created () {
this.DEFAULT_VALUE = '*'
this.minValue = 1
this.maxValue = 31
this.valueRange.start = 1
this.valueRange.end = 31
this.valueLoop.start = 1
this.valueLoop.interval = 1
this.parseProp(this.prop)
}
}
</script>
<style lang="less" scoped>
@import "mixin.less";
</style>
@@ -0,0 +1,71 @@
<template>
<div class="config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio value="TYPE_EVERY" class="choice" :disabled="disabled">每时</a-radio>
</div>
<div class="item">
<a-radio value="TYPE_RANGE" class="choice" :disabled="disabled">区间</a-radio>
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueRange.start" />
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueRange.end" />
</div>
<div class="item">
<a-radio value="TYPE_LOOP" class="choice" :disabled="disabled">循环</a-radio>
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueLoop.start" />
时开始间隔
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueLoop.interval" />
</div>
<div class="item">
<a-radio value="TYPE_SPECIFY" class="choice" :disabled="disabled">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<template v-for="i in specifyRange">
<a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{ i }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script>
import mixin from './mixin'
export default {
name: 'minute',
mixins: [mixin],
data () {
return {}
},
watch: {
value_c (newVal) {
this.$emit('change', newVal)
}
},
created () {
this.DEFAULT_VALUE = '*'
this.minValue = 0
this.maxValue = 23
this.valueRange.start = 0
this.valueRange.end = 23
this.valueLoop.start = 0
this.valueLoop.interval = 1
this.parseProp(this.prop)
}
}
</script>
<style lang="less" scoped>
@import "mixin.less";
</style>
@@ -0,0 +1,71 @@
<template>
<div class="config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio value="TYPE_EVERY" class="choice" :disabled="disabled">每分</a-radio>
</div>
<div class="item">
<a-radio value="TYPE_RANGE" class="choice" :disabled="disabled">区间</a-radio>
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueRange.start" />
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueRange.end" />
</div>
<div class="item">
<a-radio value="TYPE_LOOP" class="choice" :disabled="disabled">循环</a-radio>
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueLoop.start" />
分开始间隔
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueLoop.interval" />
</div>
<div class="item">
<a-radio value="TYPE_SPECIFY" class="choice" :disabled="disabled">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<template v-for="i in specifyRange">
<a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{ i }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script>
import mixin from './mixin'
export default {
name: 'minute',
mixins: [mixin],
data () {
return {}
},
watch: {
value_c (newVal) {
this.$emit('change', newVal)
}
},
created () {
this.DEFAULT_VALUE = '*'
this.minValue = 0
this.maxValue = 59
this.valueRange.start = 0
this.valueRange.end = 59
this.valueLoop.start = 0
this.valueLoop.interval = 1
this.parseProp(this.prop)
}
}
</script>
<style lang="less" scoped>
@import "mixin.less";
</style>
+163
View File
@@ -0,0 +1,163 @@
// 主要用于日和星期的互斥使用
const TYPE_NOT_SET = 'TYPE_NOT_SET'
const TYPE_EVERY = 'TYPE_EVERY'
const TYPE_RANGE = 'TYPE_RANGE'
const TYPE_LOOP = 'TYPE_LOOP'
const TYPE_WORK = 'TYPE_WORK'
const TYPE_LAST = 'TYPE_LAST'
const TYPE_SPECIFY = 'TYPE_SPECIFY'
const DEFAULT_VALUE = '?'
export default {
model: {
prop: 'prop',
event: 'change'
},
props: {
prop: {
type: String,
default: DEFAULT_VALUE
},
disabled: {
type: Boolean,
default: false
}
},
data () {
const type = TYPE_EVERY
return {
DEFAULT_VALUE,
// 类型
type,
// 启用日或者星期互斥用
TYPE_NOT_SET,
TYPE_EVERY,
TYPE_RANGE,
TYPE_LOOP,
TYPE_WORK,
TYPE_LAST,
TYPE_SPECIFY,
// 对于不同的类型,所定义的值也有所不同
valueRange: {
start: 0,
end: 0
},
valueLoop: {
start: 0,
interval: 1
},
valueWeek: {
start: 0,
end: 0
},
valueList: [],
valueWork: 1,
maxValue: 0,
minValue: 0,
valueLast: null
}
},
watch: {
prop (newVal) {
if (newVal === this.value_c) {
// console.info('skip ' + newVal)
return
}
this.parseProp(newVal)
}
},
computed: {
value_c () {
const result = []
switch (this.type) {
case TYPE_NOT_SET:
result.push('?')
break
case TYPE_EVERY:
result.push('*')
break
case TYPE_RANGE:
result.push(`${this.valueRange.start}-${this.valueRange.end}`)
break
case TYPE_LOOP:
result.push(`${this.valueLoop.start}/${this.valueLoop.interval}`)
break
case TYPE_WORK:
result.push(`${this.valueWork}W`)
break
case TYPE_LAST:
result.push('L')
break
case TYPE_SPECIFY:
if (this.valueList.length === 0) {
this.valueList.push(this.minValue)
}
result.push(this.valueList.join(','))
break
default:
result.push(this.DEFAULT_VALUE)
break
}
return result.length > 0 ? result.join('') : this.DEFAULT_VALUE
},
// 指定值范围区间,介于最小值和最大值之间
specifyRange () {
const range = []
for (let i = this.minValue; i <= this.maxValue; i++) {
range.push(i)
}
return range
}
},
methods: {
parseProp (value) {
if (value === this.value_c) {
// console.info('same ' + value)
return
}
if (typeof (this.preProcessProp) === 'function') {
value = this.preProcessProp(value)
}
try {
if (!value || value === this.DEFAULT_VALUE) {
this.type = TYPE_EVERY
} else if (value.indexOf('?') >= 0) {
this.type = TYPE_NOT_SET
} else if (value.indexOf('-') >= 0) {
this.type = TYPE_RANGE
const values = value.split('-')
if (values.length >= 2) {
this.valueRange.start = parseInt(values[0])
this.valueRange.end = parseInt(values[1])
}
} else if (value.indexOf('/') >= 0) {
this.type = TYPE_LOOP
const values = value.split('/')
if (values.length >= 2) {
this.valueLoop.start = value[0] === '*' ? 0 : parseInt(values[0])
this.valueLoop.interval = parseInt(values[1])
}
} else if (value.indexOf('W') >= 0) {
this.type = TYPE_WORK
const values = value.split('W')
if (!values[0] && !isNaN(values[0])) {
this.valueWork = parseInt(values[0])
}
} else if (value.indexOf('L') >= 0) {
this.type = TYPE_LAST
const values = value.split('L')
this.valueLast = parseInt(values[0])
} else if (value.indexOf(',') >= 0 || !isNaN(value)) {
this.type = TYPE_SPECIFY
this.valueList = value.split(',').map(item => parseInt(item))
} else {
this.type = TYPE_EVERY
}
} catch (e) {
// console.info(e)
this.type = TYPE_EVERY
}
}
}
}
@@ -0,0 +1,35 @@
.config-list {
text-align: left;
margin: 0 10px 10px 10px;
}
.item {
margin-top: 5px;
}
.choice {
padding: 5px 8px;
}
.w60 {
width: 60px;
}
.w80 {
width: 80px;
}
.list {
margin: 0 20px;
}
.list-check-item {
padding: 1px 3px;
width: 4em;
}
.tip-info {
color: #999
}
@@ -0,0 +1,67 @@
<template>
<div class="config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio value="TYPE_EVERY" class="choice" :disabled="disabled">每月</a-radio>
</div>
<div class="item">
<a-radio value="TYPE_RANGE" class="choice" :disabled="disabled">区间</a-radio>
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60" v-model="valueRange.start"/>
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60" v-model="valueRange.end"/>
</div>
<div class="item">
<a-radio value="TYPE_LOOP" class="choice" :disabled="disabled">循环</a-radio>
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60" v-model="valueLoop.start"/>
月开始间隔
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60" v-model="valueLoop.interval"/>
</div>
<div class="item">
<a-radio value="TYPE_SPECIFY" class="choice" :disabled="disabled">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<template v-for="i of specifyRange">
<a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{i}}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script>
import mixin from './mixin'
export default {
name: 'month',
mixins: [mixin],
data () {
return {}
},
watch: {
value_c (newVal) {
this.$emit('change', newVal)
}
},
created () {
this.DEFAULT_VALUE = '*'
this.minValue = 1
this.maxValue = 12
this.valueRange.start = 1
this.valueRange.end = 12
this.valueLoop.start = 1
this.valueLoop.interval = 1
this.parseProp(this.prop)
}
}
</script>
<style lang="less" scoped>
@import "mixin.less";
</style>
@@ -0,0 +1,68 @@
<template>
<div class="config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio value="TYPE_EVERY" class="choice" :disabled="disabled">每秒</a-radio>
</div>
<div class="item">
<a-radio value="TYPE_RANGE" class="choice" :disabled="disabled">区间</a-radio>
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60" v-model="valueRange.start"/>
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60" v-model="valueRange.end"/>
</div>
<div class="item">
<a-radio value="TYPE_LOOP" class="choice" :disabled="disabled">循环</a-radio>
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60" v-model="valueLoop.start"/>
秒开始间隔
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :max="maxValue" :min="minValue" :precision="0" class="w60" v-model="valueLoop.interval"/>
</div>
<div class="item">
<a-radio value="TYPE_SPECIFY" class="choice" :disabled="disabled">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<template v-for="i in specifyRange">
<a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{i}}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script>
import mixin from './mixin'
export default {
name: 'second',
mixins: [mixin],
data () {
return {}
},
watch: {
value_c (newVal) {
this.$emit('change', newVal)
}
},
created () {
this.DEFAULT_VALUE = '*'
this.minValue = 0
this.maxValue = 59
this.valueRange.start = 0
this.valueRange.end = 59
this.valueLoop.start = 0
this.valueLoop.interval = 1
// console.info('created')
this.parseProp(this.prop)
}
}
</script>
<style lang="less" scoped>
@import "mixin.less";
</style>
+119
View File
@@ -0,0 +1,119 @@
<template>
<div class="config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio value="TYPE_NOT_SET" class="choice" :disabled="disableChoice">不设置</a-radio>
<span class="tip-info">日和周只能设置其中之一</span>
</div>
<div class="item">
<a-radio value="TYPE_RANGE" class="choice" :disabled="disableChoice">区间</a-radio>
<a-select v-model="valueRange.start" class="w80" :disabled="type!==TYPE_RANGE || disableChoice">
<template v-for="(v, k) of WEEK_MAP">
<a-select-option :value="v" :key="v">{{ k }}</a-select-option>
</template>
</a-select>
<a-select v-model="valueRange.end" class="w80" :disabled="type!==TYPE_RANGE || disableChoice">
<template v-for="(v, k) of WEEK_MAP">
<a-select-option :value="v" :key="v">{{ k }}</a-select-option>
</template>
</a-select>
</div>
<div class="item">
<a-radio value="TYPE_LOOP" class="choice" :disabled="disableChoice">循环</a-radio>
<a-select v-model="valueLoop.start" class="w80" :disabled="type!==TYPE_LOOP || disableChoice">
<template v-for="(v, k) of WEEK_MAP">
<a-select-option :value="v" :key="v">{{ k }}</a-select-option>
</template>
</a-select>
开始间隔
<a-input-number :disabled="type!==TYPE_LOOP || disableChoice" :max="maxValue" :min="minValue" :precision="0" class="w60"
v-model="valueLoop.interval" />
</div>
<div class="item">
<a-radio value="TYPE_SPECIFY" class="choice" :disabled="disableChoice">指定</a-radio>
<div class="list">
<a-checkbox-group v-model="valueList">
<template v-for="i in specifyRange">
<a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{ i }}</a-checkbox>
</template>
</a-checkbox-group>
</div>
</div>
</a-radio-group>
</div>
</template>
<script>
import mixin from './mixin'
import { replaceWeekName, WEEK_MAP_EN } from './const.js'
const WEEK_MAP = {
周一: 2,
周二: 3,
周三: 4,
周四: 5,
周五: 6,
周六: 7,
// 按照国人习惯,将周日放到每周的最后一天
周日: 1
}
export default {
name: 'week',
mixins: [mixin],
props: {
day: {
type: String,
default: '*'
}
},
data () {
return {
WEEK_MAP,
WEEK_MAP_EN
}
},
computed: {
disableChoice () {
return (this.day && this.day !== '?') || this.disabled
}
},
watch: {
value_c () {
// 如果设置日,那么星期就直接不设置
this.updateValue()
},
day () {
// console.info('new day: ' + newVal)
this.updateValue()
}
},
methods: {
updateValue () {
this.$emit('change', this.disableChoice ? '?' : this.value_c)
},
preProcessProp (c) {
return replaceWeekName(c)
}
},
created () {
this.DEFAULT_VALUE = '*'
// 0,7表示周日 1表示周一
this.minValue = 1
this.maxValue = 7
this.valueRange.start = 1
this.valueRange.end = 7
this.valueLoop.start = 2
this.valueLoop.interval = 1
this.parseProp(this.prop)
}
}
</script>
<style lang="less" scoped>
@import "mixin.less";
</style>
@@ -0,0 +1,60 @@
<template>
<div class="config-list">
<a-radio-group v-model="type">
<div class="item">
<a-radio value="TYPE_EVERY" class="choice" :disabled="disabled">每年</a-radio>
</div>
<div class="item">
<a-radio value="TYPE_RANGE" class="choice" :disabled="disabled">区间</a-radio>
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :min="0" :precision="0" class="w60" v-model="valueRange.start"/>
<a-input-number :disabled="type!==TYPE_RANGE || disabled" :min="1" :precision="0" class="w60" v-model="valueRange.end"/>
</div>
<div class="item">
<a-radio value="TYPE_LOOP" class="choice" :disabled="disabled">循环</a-radio>
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :min="0" :precision="0" class="w60" v-model="valueLoop.start"/>
年开始间隔
<a-input-number :disabled="type!==TYPE_LOOP || disabled" :min="1" :precision="0" class="w60" v-model="valueLoop.interval"/>
</div>
</a-radio-group>
</div>
</template>
<script>
import mixin from './mixin'
export default {
name: 'year',
mixins: [mixin],
data () {
return {}
},
watch: {
value_c (newVal) {
// console.info('change:' + newVal)
this.$emit('change', newVal)
}
},
created () {
const nowYear = (new Date()).getFullYear()
this.DEFAULT_VALUE = '*'
this.minValue = 0
this.maxValue = 0
this.valueRange.start = nowYear
this.valueRange.end = nowYear + 100
this.valueLoop.start = nowYear
this.valueLoop.interval = 1
// console.info('created')
this.parseProp(this.prop)
}
}
</script>
<style lang="less" scoped>
@import "mixin.less";
</style>
@@ -0,0 +1,51 @@
import CronParser from 'cron-parser'
import { replaceWeekName } from './tabs/const'
export default (rule, value, callback) => {
// 没填写就不校验
if (!value) {
callback()
return true
}
const values = value.split(' ').filter(item => !!item)
if (values.length > 7) {
callback(new Error('Cron表达式最多7项!'))
return false
}
// 检查第7项
let e = value
if (values.length === 7) {
const year = replaceWeekName(values[6])
if (year !== '*' && year !== '?') {
let yearValues
if (year.indexOf('-') >= 0) {
yearValues = year.split('-')
} else if (year.indexOf('/')) {
yearValues = year.split('/')
} else {
yearValues = [year]
}
// console.info(yearValues)
// 判断是否都是数字
const checkYear = yearValues.some(item => isNaN(item))
if (checkYear) {
callback(new Error('Cron表达式参数[年]错误:' + year))
return false
}
}
// 取其中的前六项
e = values.slice(0, 6).join(' ')
}
// 6位 没有年
// 5位没有秒、年
let result = true
try {
const iter = CronParser.parseExpression(e)
iter.next()
callback()
} catch (e) {
callback(new Error('Cron表达式错误:' + e))
result = false
}
return result
}
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
<template>
<div class="tinymce-editor">
<editor
v-if="!reloading"
v-model="myValue"
:init="init"
:disabled="disabled"
@onClick="onClick">
</editor>
</div>
</template>
<script>
import tinymce from 'tinymce/tinymce'
import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/silver/theme'
import 'tinymce/plugins/image'
import 'tinymce/plugins/link'
import 'tinymce/plugins/media'
import 'tinymce/plugins/table'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/contextmenu'
import 'tinymce/plugins/wordcount'
import 'tinymce/plugins/colorpicker'
import 'tinymce/plugins/textcolor'
import 'tinymce/plugins/fullscreen'
import 'tinymce/icons/default'
import { uploadAction, getFileAccessHttpUrl } from '@/api/manage'
import { getVmParentByName } from '@/utils/util'
export default {
components: {
Editor
},
props: {
value: {
type: String,
required: false
},
triggerChange: {
type: Boolean,
default: false,
required: false
},
disabled: {
type: Boolean,
default: false
},
plugins: {
type: [String, Array],
default: 'lists image link media table textcolor wordcount contextmenu fullscreen'
},
toolbar: {
type: [String, Array],
default: 'undo redo | formatselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | lists link unlink image media table | removeformat | fullscreen',
branding: false
}
},
data () {
return {
// 初始化配置
init: {
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/lightgray',
height: 300,
plugins: this.plugins,
toolbar: this.toolbar,
branding: false,
menubar: false,
toolbar_drawer: false,
images_upload_handler: (blobInfo, success) => {
const formData = new FormData()
formData.append('file', blobInfo.blob(), blobInfo.filename())
formData.append('biz', 'jeditor')
formData.append('jeditor', '1')
uploadAction(window._CONFIG.domianURL + '/sys/common/upload', formData).then((res) => {
if (res.success) {
if (res.message === 'local') {
const img = 'data:image/jpeg;base64,' + blobInfo.base64()
success(img)
} else {
const img = getFileAccessHttpUrl(res.message)
success(img)
}
}
})
}
},
myValue: this.value,
reloading: false
}
},
mounted () {
this.initATabsChangeAutoReload()
},
methods: {
reload () {
this.reloading = true
this.$nextTick(() => {
this.reloading = false
})
},
onClick (e) {
this.$emit('onClick', e, tinymce)
},
// 可以添加一些自己的自定义事件,如清空内容
clear () {
this.myValue = ''
},
/**
* 自动判断父级是否是 <a-tabs/> 组件,然后添加事件监听,自动触发reload()
*
* 由于 tabs 组件切换会导致 tinymce 无法输入,
* 只有重新加载才能使用(无论是vue版的还是jQuery版tinymce都有这个通病)
*/
initATabsChangeAutoReload () {
// 获取父级
const tabs = getVmParentByName(this, 'ATabs')
const tabPane = getVmParentByName(this, 'ATabPane')
if (tabs && tabPane) {
// 用户自定义的 key
const currentKey = tabPane.$vnode.key
// 添加事件监听
tabs.$on('change', (key) => {
// 切换到自己时执行reload
if (currentKey === key) {
this.reload()
}
})
// update--begin--autor:liusq-----date:20210316------for:富文本编辑器tab父组件可能导致的赋值问题------
this.reload()
// update--end--autor:liusq-----date:20210316------for:富文本编辑器tab父组件可能导致的赋值问题------
} else {
// update--begin--autor:wangshuai-----date:20200724------for:富文本编辑器切换tab无法修改------
const tabLayout = getVmParentByName(this, 'TabLayout')
// update--begin--autor:liusq-----date:20210713------for:处理特殊情况excuteCallback不能使用------
try {
tabLayout.excuteCallback(() => {
this.reload()
})
} catch (error) {
if (tabLayout) {
this.reload()
}
}
// update--end--autor:liusq-----date:20210713------for:处理特殊情况excuteCallback不能使用------
// update--begin--autor:wangshuai-----date:20200724------for:文本编辑器切换tab无法修改------
}
}
},
watch: {
value (newValue) {
this.myValue = (newValue == null ? '' : newValue)
},
myValue (newValue) {
if (this.triggerChange) {
this.$emit('change', newValue)
} else {
this.$emit('input', newValue)
}
}
}
}
</script>
<style scoped>
</style>
+32
View File
@@ -0,0 +1,32 @@
<template>
<a-tooltip
placement="topLeft"
v-bind="$attrs"
v-on="$listeners">
<template slot="title">
<span>{{value}}</span>
</template>
{{ value | ellipsis(length) }}
</a-tooltip>
</template>
<script>
export default {
name: 'JEllipsis',
props: {
value: {
type: String,
required: false
},
length: {
type: Number,
required: false,
default: 25
}
}
}
</script>
<style scoped>
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div :class="disabled?'jero-form-container-disabled':''">
<fieldset :disabled="disabled">
<slot name="detail"></slot>
</fieldset>
<slot name="edit"></slot>
<fieldset disabled>
<slot></slot>
</fieldset>
</div>
</template>
<script>
/**
* 使用方法
* 在form下直接写这个组件就行了,
*<a-form layout="inline" :form="form" >
* <j-form-container :disabled="true">
* <!-- 表单内容省略..... -->
* </j-form-container>
*</a-form>
*/
export default {
name: 'JFormContainer',
props: {
disabled: {
type: Boolean,
default: false,
required: false
}
},
mounted () {
console.log('我是表单禁用专用组件,但是我并不支持表单中iframe的内容禁用')
}
}
</script>
<style>
.jero-form-container-disabled{
cursor: not-allowed;
}
.jero-form-container-disabled fieldset[disabled] {
-ms-pointer-events: none;
pointer-events: none;
}
.jero-form-container-disabled .ant-select{
-ms-pointer-events: none;
pointer-events: none;
}
.jero-form-container-disabled .ant-upload-select{display:none}
.jero-form-container-disabled .ant-upload-list{cursor:grabbing}
.jero-form-container-disabled fieldset[disabled] .ant-upload-list{
-ms-pointer-events: auto !important;
pointer-events: auto !important;
}
/*.jero-form-container-disabled .ant-upload-list-item-actions .anticon-delete,*/
.jero-form-container-disabled .ant-upload-list-item .anticon-delete,
.jero-form-container-disabled .ant-upload-list-item .anticon-close{
display: none;
}
</style>
+256
View File
@@ -0,0 +1,256 @@
<template>
<div class="img">
<a-upload
name="file"
listType="picture-card"
:multiple="isMultiple"
:action="uploadAction"
:headers="headers"
:data="{biz:bizPath}"
:fileList="fileList"
:beforeUpload="beforeUpload"
:disabled="disabled"
v-bind="$attrs"
:accept="accept"
v-on="childListeners"
@change="handleChange"
@preview="handlePreview"
:class="[!isMultiple?'imgupload':'', (!isMultiple && picUrl)?'image-upload-single-over':'' ]">
<div>
<!--<img v-if="!isMultiple && picUrl" :src="getAvatarView()" style="width:100%;height:100%"/>-->
<div class="iconp">
<a-icon :type="uploadLoading ? 'loading' : 'plus'" />
<div class="ant-upload-text">{{ text }}</div>
</div>
</div>
</a-upload>
<div id="images">
<div class="image" v-viewer="{movable: false}">
<img v-show="image" :src="imageUrl">
</div>
</div>
<!-- <j-image-preview-modal ref="JImagePreviewModal"></j-image-preview-modal>-->
</div>
</template>
<script>
import Vue from 'vue'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import { getFileAccessHttpUrl } from '@/api/manage'
// import JImagePreviewModal from './modal/JImagePreviewModal'
// const Base64 = require('js-base64').Base64
const uidGenerator = () => {
return '-' + parseInt(Math.random() * 10000 + 1 + '', 10)
}
export default {
name: 'JImageUpload',
// components: { JImagePreviewModal },
data () {
return {
uploadAction: window._CONFIG.domianURL + '/sys/common/upload',
uploadLoading: false,
image: false,
picUrl: false,
headers: {},
fileList: [],
accept: 'image/png, image/jpeg',
previewImage: '',
imageUrl: null
}
},
props: {
text: {
type: String,
required: false,
default: '上传'
},
/* 这个属性用于控制文件上传的业务路径 */
bizPath: {
type: String,
required: false,
default: 'temp'
},
value: {
type: [String, Array],
required: false
},
disabled: {
type: Boolean,
required: false,
default: false
},
isMultiple: {
type: Boolean,
required: false,
default: false
},
// update-begin-author:wangshuai date:20201021 for:LOWCOD-969 新增number属性,用于判断上传数量
number: {
type: Number,
required: false,
default: 0
}
// update-end-author:wangshuai date:20201021 for:LOWCOD-969 新增number属性,用于判断上传数量
},
computed: {
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
}
},
watch: {
value: {
handler (val) {
if (val instanceof Array) {
this.initFileList(val.join(','))
} else {
this.initFileList(val)
}
if (!val || val.length === 0) {
this.picUrl = false
}
},
// 立刻执行handler
immediate: true
}
},
created () {
const token = Vue.ls.get(ACCESS_TOKEN)
this.headers = { 'X-Access-Token': token }
},
methods: {
initFileList (paths) {
if (!paths || paths.length === 0) {
this.fileList = []
return
}
this.picUrl = true
const fileList = []
const arr = paths.split(',')
for (let a = 0; a < arr.length; a++) {
const url = getFileAccessHttpUrl(arr[a])
fileList.push({
uid: uidGenerator(),
name: arr[a],
status: 'done',
url: url,
response: {
status: 'history',
message: arr[a]
}
})
}
this.fileList = fileList
},
beforeUpload: function (file) {
const fileType = file.type
if (fileType.indexOf('image') < 0) {
this.$message.warning('请上传图片')
return false
}
},
handleChange (info) {
this.picUrl = false
let fileList = info.fileList
// update-begin-author:wangshuai date:20201022 for:LOWCOD-969 判断number是否大于0和是否多选,返回选定的元素。
if (this.number > 0) {
fileList = fileList.slice(-this.number)
}
// update-end-author:wangshuai date:20201022 for:LOWCOD-969 判断number是否大于0和是否多选,返回选定的元素。
if (info.file.status === 'done') {
if (info.file.response.success) {
this.picUrl = true
fileList = fileList.map((file) => {
if (file.response) {
file.url = `${file.response.result.id}`
}
return file
})
}
// this.$message.success(`${info.file.name} 上传成功!`);
} else if (info.file.status === 'error') {
this.$message.error(`${info.file.name} 上传失败.`)
} else if (info.file.status === 'removed') {
this.handleDelete(info.file)
}
this.fileList = fileList
if (info.file.status === 'done' || info.file.status === 'removed') {
this.handlePathChange()
}
},
// 预览
handlePreview (file) {
if (file && file.url) {
console.log(file)
// const fileFullUrl = `${window._CONFIG.domianWebSocketURL}/${file.url}`
// const url = `${window._CONFIG.onlinePreviewDomainURL}?url=${encodeURIComponent(Base64.encode(fileFullUrl))}`
// window.open(url)
// this.$refs.JImagePreviewModal.open(file)
this.imageUrl = getFileAccessHttpUrl(file.name)
// 获取viewer实例
const viewer = this.$el.querySelector('.image').$viewer
// 调用show方法进行显示预览图
viewer.show()
}
},
getAvatarView () {
if (this.fileList.length > 0) {
const url = this.fileList[this.fileList.length - 1].url
return getFileAccessHttpUrl(url)
}
},
handlePathChange () {
const uploadFiles = this.fileList
let path = ''
if (!uploadFiles || uploadFiles.length === 0) {
path = ''
}
const arr = []
if (!this.isMultiple && uploadFiles && uploadFiles.length > 0) {
arr.push(uploadFiles[uploadFiles.length - 1].url)
} else {
for (let a = 0; a < uploadFiles.length; a++) {
// update-begin-author:taoyan date:20200819 for:【开源问题z】上传图片组件 LOWCOD-783
if (uploadFiles[a].status === 'done') {
arr.push(uploadFiles[a].url)
} else {
return
}
// update-end-author:taoyan date:20200819 for:【开源问题z】上传图片组件 LOWCOD-783
}
}
if (arr.length > 0) {
path = arr.join(',')
}
this.$emit('change', path)
},
handleDelete (file) {
// 如有需要新增 删除逻辑
},
handleCancel () {
this.close()
this.previewVisible = false
},
close () {
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
/deep/ .imgupload .ant-upload-select{display:block}
/deep/ .imgupload .ant-upload.ant-upload-select-picture-card{ width:120px;height: 120px;}
/deep/ .imgupload .iconp{padding:32px;}
/* update--end--autor:lvdandan-----date:20201016------forj-image-upload图片组件单张图片详情回显空白*/
</style>
+129
View File
@@ -0,0 +1,129 @@
<template>
<a-modal
title="导入EXCEL"
:width="600"
:visible="visible"
:confirmLoading="uploading"
@cancel="handleClose">
<a-upload
name="file"
:multiple="true"
accept=".xls,.xlsx"
:fileList="fileList"
:remove="handleRemove"
v-bind="$attrs"
v-on="$listeners"
:beforeUpload="beforeUpload">
<a-button>
<a-icon type="upload" />
选择导入文件
</a-button>
</a-upload>
<template slot="footer">
<a-button @click="handleClose">关闭</a-button>
<a-button
type="primary"
@click="handleImport"
:disabled="fileList.length === 0"
:loading="uploading">
{{ uploading ? '上传中...' : '开始上传' }}
</a-button>
</template>
</a-modal>
</template>
<script>
import { postAction } from '@/api/manage'
export default {
name: 'JImportModal',
props: {
url: {
type: String,
default: '',
required: false
},
visible: {
type: Boolean,
default: false,
required: false
},
biz: {
type: String,
default: '',
required: false
}
},
data () {
return {
uploading: false,
fileList: [],
uploadAction: '',
foreignKeys: ''
}
},
watch: {
url (val) {
if (val) {
this.uploadAction = window._CONFIG.domianURL + val
}
}
},
created () {
console.log(this.$attrs)
this.uploadAction = window._CONFIG.domianURL + this.url
},
methods: {
handleClose () {
this.visible = false
},
show (arg) {
this.fileList = []
this.uploading = false
this.visible = true
this.foreignKeys = arg
},
handleRemove (file) {
const index = this.fileList.indexOf(file)
const newFileList = this.fileList.slice()
newFileList.splice(index, 1)
this.fileList = newFileList
},
beforeUpload (file) {
this.fileList = [...this.fileList, file]
return false
},
handleImport () {
const { fileList } = this
const formData = new FormData()
if (this.biz) {
formData.append('isSingleTableImport', this.biz)
}
if (this.foreignKeys && this.foreignKeys.length > 0) {
formData.append('foreignKeys', this.foreignKeys)
}
fileList.forEach((file) => {
formData.append('files[]', file)
})
this.uploading = true
postAction(this.uploadAction, formData).then((res) => {
this.uploading = false
if (res.success) {
this.$message.success(res.message)
this.visible = false
this.$emit('ok')
} else {
this.$message.warning(res.message)
}
})
}
}
}
</script>
<style scoped>
</style>
+129
View File
@@ -0,0 +1,129 @@
<template>
<a-input :placeholder="placeholder"
:value="inputVal"
@input="backValue"
v-bind="$props"
v-on="childListeners"
></a-input>
</template>
<script>
import { Input } from 'ant-design-vue'
const JINPUT_QUERY_LIKE = 'like'
const JINPUT_QUERY_NE = 'ne'
const JINPUT_QUERY_GE = 'ge' // 大于等于
const JINPUT_QUERY_LE = 'le' // 小于等于
export default {
name: 'JInput',
props: {
...Input.props, // 拿到全部外部的参数
value: {
type: String,
required: false
},
type: {
type: String,
required: false,
default: JINPUT_QUERY_LIKE
},
placeholder: {
type: String,
required: false,
default: ''
},
trim: {
type: Boolean,
required: false,
default: true
}
},
data () {
return {
inputVal: ''
}
},
computed: {
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
}
},
watch: {
value: {
immediate: true,
handler: function () {
this.initVal()
}
},
// 当 type 变化的时候重新计算值
type () {
this.backValue({ target: { value: this.inputVal } })
}
},
methods: {
initVal () {
if (!this.value) {
this.inputVal = ''
} else {
let text = this.value
switch (this.type) {
case JINPUT_QUERY_LIKE:
// 修复路由传参的值传送到jinput框被前后各截取了一位 #1336
if (text.indexOf('*') !== -1) {
text = text.substring(1, text.length - 1)
}
break
case JINPUT_QUERY_NE:
text = text.substring(1)
break
case JINPUT_QUERY_GE:
text = text.substring(2)
break
case JINPUT_QUERY_LE:
text = text.substring(2)
break
default:
}
this.inputVal = text
}
},
backValue (e) {
let text = e.target.value
if (text && this.trim === true) {
text = text.trim()
}
switch (this.type) {
case JINPUT_QUERY_LIKE:
text = '*' + text + '*'
break
case JINPUT_QUERY_NE:
text = '!' + text
break
case JINPUT_QUERY_GE:
text = '>=' + text
break
case JINPUT_QUERY_LE:
text = '<=' + text
break
default:
}
this.$emit('change', text)
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,29 @@
export default {
minHeight: '200px',
previewStyle: 'vertical',
useCommandShortcut: true,
useDefaultHTMLSanitizer: true,
usageStatistics: false,
hideModeSwitch: false,
toolbarItems: [
'heading',
'bold',
'italic',
'strike',
'divider',
'hr',
'quote',
'divider',
'ul',
'ol',
'task',
'indent',
'outdent',
'divider',
'table',
'link',
'divider',
'code',
'codeblock'
]
}
@@ -0,0 +1,254 @@
<template>
<div>
<div class="j-markdown-editor" :id="id"/>
<div v-if="isShow">
<j-modal
title="图片上传"
:visible.sync="dialogVisible"
width="30%"
:before-close="handleClose"
@ok="handleOk">
<a-tabs default-active-key="1" @change="handleChange">
<a-tab-pane tab="本地图片上传" key="1" :forceRender="true">
<j-upload v-model="fileList" :number="1"></j-upload>
<div style="margin-top: 20px">
<a-input v-model="remark" placeholder="请填写备注"></a-input>
</div>
</a-tab-pane>
<a-tab-pane tab="网络图片地址" key="2" :forceRender="true">
<a-input v-model="networkPic" placeholder="请填写网络图片地址"></a-input>
<a-input style="margin-top: 20px" v-model="remark" placeholder="请填写备注"></a-input>
</a-tab-pane>
</a-tabs>
</j-modal>
</div>
</div>
</template>
<script>
import 'codemirror/lib/codemirror.css'
import '@toast-ui/editor/dist/toastui-editor.css'
import '@toast-ui/editor/dist/i18n/zh-cn'
import Editor from '@toast-ui/editor'
import defaultOptions from './default-options'
import JUpload from '@/components/jero/JUpload'
import { getFileAccessHttpUrl } from '@/api/manage'
export default {
name: 'JMarkdownEditor',
components: {
JUpload
},
props: {
value: {
type: String,
default: ''
},
id: {
type: String,
required: false,
default () {
return 'markdown-editor-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
options: {
type: Object,
default () {
return defaultOptions
}
},
mode: {
type: String,
default: 'markdown'
},
height: {
type: String,
required: false,
default: '300px'
},
language: {
type: String,
required: false,
default: 'zh-CN'
}
},
data () {
return {
editor: null,
isShow: false,
activeIndex: '1',
dialogVisible: false,
index: '1',
fileList: [],
remark: '',
imageName: '',
imageUrl: '',
networkPic: ''
}
},
computed: {
editorOptions () {
const options = Object.assign({}, defaultOptions, this.options)
options.initialEditType = this.mode
options.height = this.height
options.language = this.language
return options
}
},
watch: {
value (newValue, preValue) {
if (newValue !== preValue && newValue !== this.editor.getMarkdown()) {
this.editor.setMarkdown(newValue)
}
},
language () {
this.destroyEditor()
this.initEditor()
},
height (newValue) {
this.editor.height(newValue)
},
mode (newValue) {
this.editor.changeMode(newValue)
}
},
mounted () {
this.initEditor()
},
destroyed () {
this.destroyEditor()
},
methods: {
initEditor () {
this.editor = new Editor({
el: document.getElementById(this.id),
...this.editorOptions
})
if (this.value) {
this.editor.setMarkdown(this.value)
}
this.editor.on('change', () => {
this.$emit('change', this.editor.getMarkdown())
})
// --begin 添加自定义上传按钮
/*
* 添加自定义按钮
*/
// 获取编辑器上的功能条
const toolbar = this.editor.getUI().getToolbar()
// 添加图片点击事件
this.editor.eventManager.addEventType('isShowClickEvent')
this.editor.eventManager.listen('isShowClickEvent', () => {
this.isShow = true
this.dialogVisible = true
})
// addImageBlobHook图片上传、剪切、拖拽都会走此方法
// 删除默认监听事件
this.editor.eventManager.removeEventHandler('addImageBlobHook')
// 添加自定义监听事件
this.editor.eventManager.listen('addImageBlobHook', (blob, callback) => {
this.upload(blob, url => {
callback(url)
})
})
// 添加自定义按钮 第二个参数代表位置,不传默认放在最后
toolbar.insertItem(15, {
type: 'button',
options: {
name: 'customize',
className: 'tui-image tui-toolbar-icons',
event: 'isShowClickEvent',
tooltip: '上传图片'
}
//
})
// --end 添加自定义上传按钮
},
destroyEditor () {
if (!this.editor) return
this.editor.off('change')
this.editor.remove()
},
setMarkdown (value) {
this.editor.setMarkdown(value)
},
getMarkdown () {
return this.editor.getMarkdown()
},
setHtml (value) {
this.editor.setHtml(value)
},
getHtml () {
return this.editor.getHtml()
},
handleOk () {
if (this.index === '1') {
this.imageUrl = getFileAccessHttpUrl(this.fileList)
if (this.remark) {
this.addImgToMd(this.imageUrl, this.remark)
} else {
this.addImgToMd(this.imageUrl, '')
}
} else {
if (this.remark) {
this.addImgToMd(this.networkPic, this.remark)
} else {
this.addImgToMd(this.networkPic, '')
}
}
this.index = '1'
this.fileList = []
this.imageName = ''
this.imageUrl = ''
this.remark = ''
this.networkPic = ''
this.dialogVisible = false
this.isShow = false
},
handleClose (done) {
done()
},
handleChange (val) {
this.fileList = []
this.remark = ''
this.imageName = ''
this.imageUrl = ''
this.networkPic = ''
this.index = val
},
// 添加图片到markdown
addImgToMd (data, name) {
const editor = this.editor.getCodeMirror()
const editorHtml = this.editor.getCurrentModeEditor()
const isMarkdownMode = this.editor.isMarkdownMode()
if (isMarkdownMode) {
editor.replaceSelection(`![${name}](${data})`)
} else {
const range = editorHtml.getRange()
const img = document.createElement('img')
img.src = `${data}`
img.alt = name
range.insertNode(img)
}
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped lang="less">
.j-markdown-editor {
/deep/ .tui-editor-defaultUI {
.te-mode-switch,
.tui-scrollsync
{
line-height: 1.5;
}
}
}
</style>
+242
View File
@@ -0,0 +1,242 @@
<template>
<a-modal
ref="modal"
:class="getClass(modalClass)"
:style="getStyle(modalStyle)"
:visible="visible"
v-bind="_attrs"
v-on="$listeners"
@ok="handleOk"
@cancel="handleCancel"
destroyOnClose
>
<slot></slot>
<!--有设置标题-->
<template v-if="!isNoTitle" slot="title">
<a-row class="j-modal-title-row" type="flex">
<a-col class="left">
<slot name="title">{{ title }}</slot>
</a-col>
<a-col v-if="switchFullscreen" class="right" @click="toggleFullscreen">
<a-button class="ant-modal-close ant-modal-close-x" ghost type="link" :icon="fullscreenButtonIcon"/>
</a-col>
</a-row>
</template>
<!--没有设置标题-->
<template v-else slot="title">
<a-row class="j-modal-title-row" type="flex">
<a-col v-if="switchFullscreen" class="right" @click="toggleFullscreen">
<a-button class="ant-modal-close ant-modal-close-x" ghost type="link" :icon="fullscreenButtonIcon"/>
</a-col>
</a-row>
</template>
<!-- 处理 scopedSlots -->
<template v-for="slotName of scopedSlotsKeys" :slot="slotName">
<slot :name="slotName"></slot>
</template>
<!-- 处理 slots -->
<template v-for="slotName of slotsKeys" v-slot:[slotName]>
<slot :name="slotName"></slot>
</template>
</a-modal>
</template>
<script>
import { getClass, getStyle } from '@/utils/props-util'
import { triggerWindowResizeEvent } from '@/utils/util'
export default {
name: 'JModal',
props: {
title: String,
// 可使用 .sync 修饰符
visible: Boolean,
// 是否全屏弹窗,当全屏时无论如何都会禁止 body 滚动。可使用 .sync 修饰符
fullscreen: {
type: Boolean,
default: false
},
// 是否允许切换全屏(允许后右上角会出现一个按钮)
switchFullscreen: {
type: Boolean,
default: false
},
// 点击确定按钮的时候是否关闭弹窗
okClose: {
type: Boolean,
default: true
}
},
data () {
return {
// 内部使用的 slots ,不再处理
usedSlots: ['title'],
// 实际控制是否全屏的参数
innerFullscreen: this.fullscreen
}
},
computed: {
// 一些未处理的参数或特殊处理的参数绑定到 a-modal 上
_attrs () {
const attrs = { ...this.$attrs }
// 如果全屏就将宽度设为 100%
if (this.innerFullscreen) {
attrs.width = '100%'
}
return attrs
},
modalClass () {
return {
'j-modal-box': true,
fullscreen: this.innerFullscreen,
'no-title': this.isNoTitle,
'no-footer': this.isNoFooter
}
},
modalStyle () {
const style = {}
// 如果全屏就将top设为 0
if (this.innerFullscreen) {
style.top = '0'
}
return style
},
isNoTitle () {
return !this.title && !this.allSlotsKeys.includes('title')
},
isNoFooter () {
return this._attrs.footer === null
},
slotsKeys () {
return Object.keys(this.$slots).filter(key => !this.usedSlots.includes(key))
},
scopedSlotsKeys () {
return Object.keys(this.$scopedSlots).filter(key => !this.usedSlots.includes(key))
},
allSlotsKeys () {
return Object.keys(this.$slots).concat(Object.keys(this.$scopedSlots))
},
// 切换全屏的按钮图标
fullscreenButtonIcon () {
return this.innerFullscreen ? 'fullscreen-exit' : 'fullscreen'
}
},
watch: {
visible () {
if (this.visible) {
this.innerFullscreen = this.fullscreen
}
},
innerFullscreen (val) {
this.$emit('update:fullscreen', val)
}
},
methods: {
getClass (clazz) {
return { ...getClass(this), ...clazz }
},
getStyle (style) {
return { ...getStyle(this), ...style }
},
close () {
this.$emit('update:visible', false)
},
handleOk () {
if (this.okClose) {
this.close()
}
},
handleCancel () {
this.close()
},
/** 切换全屏 */
toggleFullscreen () {
this.innerFullscreen = !this.innerFullscreen
triggerWindowResizeEvent()
}
}
}
</script>
<style lang="less">
.j-modal-box {
&.fullscreen {
top: 0;
left: 0;
padding: 0;
// 兼容1.6.2版本的antdv
& .ant-modal {
top: 0;
padding: 0;
height: 100vh;
}
& .ant-modal-content {
height: 100vh;
border-radius: 0;
& .ant-modal-body {
/* title 和 footer 各占 55px */
height: calc(100% - 55px - 55px);
overflow: auto;
}
}
&.no-title, &.no-footer {
.ant-modal-body {
height: calc(100% - 55px);
}
}
&.no-title.no-footer {
.ant-modal-body {
height: 100%;
}
}
}
.j-modal-title-row {
.left {
width: calc(100% - 56px - 56px);
}
.right {
width: 56px;
position: inherit;
.ant-modal-close {
right: 56px;
color: rgba(0, 0, 0, 0.45);
&:hover {
color: rgba(0, 0, 0, 0.75);
}
}
}
}
&.no-title{
.ant-modal-header {
padding: 0 24px;
border-bottom: 0 !important;
}
}
}
@media (max-width: 767px) {
.j-modal-box.fullscreen {
margin: 0;
max-width: 100vw;
}
}
</style>
+124
View File
@@ -0,0 +1,124 @@
<template>
<j-modal :visible="visible" :confirmLoading="loading" :after-close="afterClose" v-bind="modalProps" @ok="onOk" @cancel="onCancel">
<a-spin :spinning="loading">
<div v-html="content"></div>
<a-form-model ref="form" :model="model" :rules="rules">
<a-form-model-item prop="input">
<a-input ref="input" v-model="model.input" v-bind="inputProps" @pressEnter="onInputPressEnter"/>
</a-form-model-item>
</a-form-model>
</a-spin>
</j-modal>
</template>
<script>
import pick from 'lodash.pick'
export default {
name: 'JPrompt',
data () {
return {
visible: false,
loading: false,
content: '',
// 弹窗参数
modalProps: {
title: ''
},
inputProps: {
placeholder: ''
},
// form model
model: {
input: ''
},
// 校验
rule: [],
// 回调函数
callback: {}
}
},
computed: {
rules () {
return {
input: this.rule
}
}
},
methods: {
show (options) {
this.content = options.content
if (Array.isArray(options.rule)) {
this.rule = options.rule
}
if (options.defaultValue != null) {
this.model.input = options.defaultValue
}
// 取出常用的弹窗参数
const pickModalProps = pick(options, 'title', 'centered', 'cancelText', 'closable', 'mask', 'maskClosable', 'okText', 'okType', 'okButtonProps', 'cancelButtonProps', 'width', 'wrapClassName', 'zIndex', 'dialogStyle', 'dialogClass')
this.modalProps = Object.assign({}, pickModalProps, options.modalProps)
// 取出常用的input参数
const pickInputProps = pick(options, 'placeholder', 'allowClear')
this.inputProps = Object.assign({}, pickInputProps, options.inputProps)
// 回调函数
this.callback = pick(options, 'onOk', 'onOkAsync', 'onCancel')
this.visible = true
this.$nextTick(() => this.$refs.input.focus())
},
onOk () {
this.$refs.form.validate((ok) => {
if (ok) {
const event = { value: this.model.input, target: this }
// 异步方法优先级高于同步方法
if (typeof this.callback.onOkAsync === 'function') {
this.callback.onOkAsync(event)
} else if (typeof this.callback.onOk === 'function') {
this.callback.onOk(event)
this.close()
} else {
this.close()
}
}
})
},
onCancel () {
if (typeof this.callback.onCancel === 'function') {
this.callback.onCancel(this.model.input)
}
this.close()
},
onInputPressEnter () {
this.onOk()
},
close () {
this.visible = this.loading ? this.visible : false
},
forceClose () {
this.visible = false
},
showLoading () {
this.loading = true
},
hideLoading () {
this.loading = false
},
afterClose (e) {
if (typeof this.modalProps.afterClose === 'function') {
this.modalProps.afterClose(e)
}
this.$emit('after-close', e)
}
}
}
</script>
<style scoped>
</style>
+18
View File
@@ -0,0 +1,18 @@
import JModal from './JModal'
import JPrompt from './JPrompt'
export default {
install (Vue) {
Vue.component(JModal.name, JModal)
const JPromptExtend = Vue.extend(JPrompt)
Vue.prototype.$JPrompt = function (options = {}) {
// 创建prompt实例
const vm = new JPromptExtend().$mount()
vm.show(options)
// 关闭后销毁
vm.$on('after-close', () => vm.$destroy())
return vm
}
}
}
+241
View File
@@ -0,0 +1,241 @@
<template>
<a-modal
ref="modal"
:class="getClass(modalClass)"
:style="getStyle(modalStyle)"
:visible="visible"
v-bind="_attrs"
v-on="$listeners"
@ok="handleOk"
@cancel="handleCancel"
destroyOnClose
>
<slot></slot>
<!--有设置标题-->
<template v-if="!isNoTitle" slot="title">
<a-row class="j-modal-title-row" type="flex">
<a-col class="left">
<slot name="title">{{ title }}</slot>
</a-col>
<a-col v-if="switchFullscreen" class="right" @click="toggleFullscreen">
<a-button class="ant-modal-close ant-modal-close-x" ghost type="link" :icon="fullscreenButtonIcon"/>
</a-col>
</a-row>
</template>
<!--没有设置标题-->
<template v-else slot="title">
<a-row class="j-modal-title-row" type="flex">
<a-col v-if="switchFullscreen" class="right" @click="toggleFullscreen">
<a-button class="ant-modal-close ant-modal-close-x" ghost type="link" :icon="fullscreenButtonIcon"/>
</a-col>
</a-row>
</template>
<!-- 处理 scopedSlots -->
<template v-for="slotName of scopedSlotsKeys" :slot="slotName">
<slot :name="slotName"></slot>
</template>
<!-- 处理 slots -->
<template v-for="slotName of slotsKeys" v-slot:[slotName]>
<slot :name="slotName"></slot>
</template>
</a-modal>
</template>
<script>
import { getClass, getStyle } from '@/utils/props-util'
import { triggerWindowResizeEvent } from '@/utils/util'
export default {
name: 'JModal',
props: {
title: String,
// 可使用 .sync 修饰符
visible: Boolean,
// 是否全屏弹窗,当全屏时无论如何都会禁止 body 滚动。可使用 .sync 修饰符
fullscreen: {
type: Boolean,
default: false
},
// 是否允许切换全屏(允许后右上角会出现一个按钮)
switchFullscreen: {
type: Boolean,
default: false
},
// 点击确定按钮的时候是否关闭弹窗
okClose: {
type: Boolean,
default: true
}
},
data () {
return {
// 内部使用的 slots ,不再处理
usedSlots: ['title'],
// 实际控制是否全屏的参数
innerFullscreen: this.fullscreen
}
},
computed: {
// 一些未处理的参数或特殊处理的参数绑定到 a-modal 上
_attrs () {
const attrs = { ...this.$attrs }
// 如果全屏就将宽度设为 100%
if (this.innerFullscreen) {
attrs.width = '100%'
}
return attrs
},
modalClass () {
return {
'j-modal-box': true,
fullscreen: this.innerFullscreen,
'no-title': this.isNoTitle,
'no-footer': this.isNoFooter
}
},
modalStyle () {
const style = {}
// 如果全屏就将top设为 0
if (this.innerFullscreen) {
style.top = '0'
}
return style
},
isNoTitle () {
return !this.title && !this.allSlotsKeys.includes('title')
},
isNoFooter () {
return this._attrs.footer === null
},
slotsKeys () {
return Object.keys(this.$slots).filter(key => !this.usedSlots.includes(key))
},
scopedSlotsKeys () {
return Object.keys(this.$scopedSlots).filter(key => !this.usedSlots.includes(key))
},
allSlotsKeys () {
return Object.keys(this.$slots).concat(Object.keys(this.$scopedSlots))
},
// 切换全屏的按钮图标
fullscreenButtonIcon () {
return this.innerFullscreen ? 'fullscreen-exit' : 'fullscreen'
}
},
watch: {
visible () {
if (this.visible) {
this.innerFullscreen = this.fullscreen
}
},
innerFullscreen (val) {
this.$emit('update:fullscreen', val)
}
},
methods: {
getClass (clazz) {
return { ...getClass(this), ...clazz }
},
getStyle (style) {
return { ...getStyle(this), ...style }
},
close () {
this.$emit('update:visible', false)
},
handleOk () {
if (this.okClose) {
this.close()
}
},
handleCancel () {
this.close()
},
/** 切换全屏 */
toggleFullscreen () {
this.innerFullscreen = !this.innerFullscreen
triggerWindowResizeEvent()
}
}
}
</script>
<style lang="less">
.j-modal-box {
&.fullscreen {
top: 0;
left: 0;
padding: 0;
// 兼容1.6.2版本的antdv
& .ant-modal {
top: 0;
padding: 0;
height: 100vh;
}
& .ant-modal-content {
height: 100vh;
border-radius: 0;
& .ant-modal-body {
/* title 和 footer 各占 55px */
height: calc(100% - 55px - 55px);
overflow: auto;
}
}
&.no-title, &.no-footer {
.ant-modal-body {
height: calc(100% - 55px);
}
}
&.no-title.no-footer {
.ant-modal-body {
height: 100%;
}
}
}
.j-modal-title-row {
.left {
width: calc(100% - 56px - 56px);
}
.right {
width: 56px;
position: inherit;
.ant-modal-close {
right: 56px;
color: rgba(0, 0, 0, 0.45);
&:hover {
color: rgba(0, 0, 0, 0.75);
}
}
}
}
&.no-title{
.ant-modal-header {
padding: 0 24px;
border-bottom: 0 !important;
}
}
}
@media (max-width: 767px) {
.j-modal-box.fullscreen {
margin: 0;
max-width: 100vw;
}
}
</style>
+230
View File
@@ -0,0 +1,230 @@
<template>
<div class="components-input-demo-presuffix" v-if="avalid">
<!---->
<a-input @click="openModal" :placeholder="placeholder" v-model="showText" readOnly :disabled="disabled">
<a-icon slot="prefix" type="cluster" :title="title"/>
<a-icon v-if="showText" slot="suffix" type="close-circle" @click="handleEmpty" title="清空"/>
</a-input>
<j-popup-onl-report
ref="jPopupOnlReport"
:code="code"
:multi="multi"
:sorter="sorter"
:groupId="uniqGroupId"
:param="param"
@ok="callBack"
/>
</div>
</template>
<script>
import JPopupOnlReport from './modal/JPopupOnlReport'
export default {
name: 'JPopup',
components: {
JPopupOnlReport
},
props: {
code: {
type: String,
default: '',
required: false
},
field: {
type: String,
default: '',
required: false
},
orgFields: {
type: String,
default: '',
required: false
},
destFields: {
type: String,
default: '',
required: false
},
/** 排序列,指定要排序的列,使用方式:列名=desc|asc */
sorter: {
type: String,
default: ''
},
width: {
type: Number,
default: 1200,
required: false
},
placeholder: {
type: String,
default: '请选择',
required: false
},
value: {
type: String,
required: false
},
triggerChange: {
type: Boolean,
required: false,
default: false
},
disabled: {
type: Boolean,
required: false,
default: false
},
multi: {
type: Boolean,
required: false,
default: false
},
// popup动态参数 支持系统变量语法
param: {
type: Object,
required: false,
default: () => {}
},
spliter: {
type: String,
required: false,
default: ','
},
/** 分组ID,用于将多个popup的请求合并到一起,不传不分组 */
groupId: String
},
data () {
return {
showText: '',
title: '',
avalid: true
}
},
computed: {
uniqGroupId () {
if (this.groupId) {
const { groupId, code, field, orgFields, destFields } = this
return `${groupId}_${code}_${field}_${orgFields}_${destFields}`
}
return null
}
},
watch: {
value: {
immediate: true,
handler: function (val) {
if (!val) {
this.showText = ''
} else {
this.showText = val.split(this.spliter).join(',')
}
}
}
},
created () {
},
mounted () {
if (!this.orgFields || !this.destFields || !this.code) {
this.$message.error('popup参数未正确配置!')
this.avalid = false
}
if (this.destFields.split(',').length !== this.orgFields.split(',').length) {
this.$message.error('popup参数未正确配置,原始值和目标值数量不一致!')
this.avalid = false
}
},
methods: {
openModal () {
if (this.disabled === false) {
this.$refs.jPopupOnlReport.show()
}
},
handleEmpty () {
this.showText = ''
const destFieldsArr = this.destFields.split(',')
if (destFieldsArr.length === 0) {
return
}
const res = {}
for (let i = 0; i < destFieldsArr.length; i++) {
res[destFieldsArr[i]] = ''
}
if (this.triggerChange) {
this.$emit('callback', res)
} else {
this.$emit('input', '', res)
}
},
callBack (rows) {
// update--begin--autor:lvdandan-----date:20200630------for:多选时未带回多个值------
const orgFieldsArr = this.orgFields.split(',')
const destFieldsArr = this.destFields.split(',')
let resetText = false
if (this.field && this.field.length > 0) {
this.showText = ''
resetText = true
}
const res = {}
if (orgFieldsArr.length > 0) {
for (let i = 0; i < orgFieldsArr.length; i++) {
const tempDestArr = []
for (const rw of rows) {
let val = rw[orgFieldsArr[i]]
// update--begin--autor:liusq-----date:20210713------for:处理val等于0的情况issues/I3ZL4T------
if (typeof val === 'undefined' || val == null || val.toString() === '') {
val = ''
}
// update--end--autor:liusq-----date:20210713------for:处理val等于0的情况issues/I3ZL4T------
tempDestArr.push(val)
}
res[destFieldsArr[i]] = tempDestArr.join(',')
}
if (resetText === true) {
const tempText = []
for (const rw of rows) {
let val = rw[orgFieldsArr[destFieldsArr.indexOf(this.field)]]
if (!val) {
val = ''
}
tempText.push(val)
}
this.showText = tempText.join(',')
}
// update--end--autor:lvdandan-----date:20200630------for:多选时未带回多个值------
}
if (this.triggerChange) {
// v-dec时即triggerChange为true时 将整个对象给form页面 让他自己setFieldsValue
this.$emit('callback', res)
} else {
// v-model时 需要传一个参数field 表示当前这个字段 从而根据这个字段的顺序找到原始值
// this.$emit("input",row[orgFieldsArr[destFieldsArr.indexOf(this.field)]])
let str = ''
if (this.showText) {
str = this.showText.split(',').join(this.spliter)
}
this.$emit('input', str, res)
}
}
}
}
</script>
<style scoped>
.components-input-demo-presuffix .anticon-close-circle {
cursor: pointer;
color: #ccc;
transition: color 0.3s;
font-size: 12px;
}
.components-input-demo-presuffix .anticon-close-circle:hover {
color: #f5222d;
}
.components-input-demo-presuffix .anticon-close-circle:active {
color: #666;
}
</style>
+94
View File
@@ -0,0 +1,94 @@
<template>
<a-select
mode="multiple"
:placeholder="placeholder"
:value="arrayValue"
@change="onChange"
option-filter-prop="children"
>
<a-select-option
v-for="(item,index) in newOptions"
:key="index"
:getPopupContainer="getParentContainer"
:value="item.value">
{{ item.text || item.label }}
</a-select-option>
</a-select>
</template>
<script>
// option {label:,value:}
export default {
name: 'JSelectMultiple',
props: {
placeholder: {
type: String,
default: '',
required: false
},
value: {
type: String,
required: false
},
readOnly: {
type: Boolean,
required: false,
default: false
},
options: {
type: Array,
required: true
},
triggerChange: {
type: Boolean,
required: false,
default: false
},
spliter: {
type: String,
required: false,
default: ','
},
popContainer: {
type: String,
default: '',
required: false
}
},
data () {
return {
newOptions: [],
arrayValue: !this.value ? [] : this.value.split(this.spliter) // arrayValue是已选中的数据
}
},
watch: {
value (val) {
if (!val) {
this.arrayValue = []
} else {
this.arrayValue = this.value.split(this.spliter)
}
}
},
created () {
this.newOptions = this.options
},
methods: {
onChange (selectedValue) {
this.newOptions = this.options
if (this.triggerChange) {
this.$emit('change', selectedValue.join(this.spliter))
} else {
this.$emit('input', selectedValue.join(this.spliter))
}
},
getParentContainer (node) {
if (!this.popContainer) {
return node.parentNode
} else {
return document.querySelector(this.popContainer)
}
}
}
}
</script>
+116
View File
@@ -0,0 +1,116 @@
<template>
<div class="drag" ref="dragDiv">
<div class="drag_bg"></div>
<div class="drag_text">{{confirmWords}}</div>
<div ref="moveDiv" @mousedown="mousedownFn($event)" :class="{'handler_ok_bg':confirmSuccess}" class="handler handler_bg" style="border: 0.5px solid #fff;height: 34px;position: absolute;top: 0;left: 0;"></div>
</div>
</template>
<script>
export default {
name: 'JSlider',
data () {
return {
beginClientX: 0, /* 距离屏幕左端距离 */
mouseMoveStata: false, /* 触发拖动状态 判断 */
maxwidth: '', /* 拖动最大宽度,依据滑块宽度算出来的 */
confirmWords: '拖动滑块验证', /* 滑块文字 */
confirmSuccess: false /* 验证成功判断 */
}
},
methods: {
isSuccess () {
return this.confirmSuccess
},
mousedownFn: function (e) {
if (!this.confirmSuccess) {
e.preventDefault && e.preventDefault() // 阻止文字选中等 浏览器默认事件
this.mouseMoveStata = true
this.beginClientX = e.clientX
}
}, // mousedoen 事件
successFunction () {
this.confirmSuccess = true
this.confirmWords = '验证通过'
if (window.addEventListener) {
document.getElementsByTagName('html')[0].removeEventListener('mousemove', this.mouseMoveFn)
document.getElementsByTagName('html')[0].removeEventListener('mouseup', this.moseUpFn)
} else {
document.getElementsByTagName('html')[0].removeEventListener('mouseup', () => {})
}
document.getElementsByClassName('drag_text')[0].style.color = '#fff'
document.getElementsByClassName('handler')[0].style.left = this.maxwidth + 'px'
document.getElementsByClassName('drag_bg')[0].style.width = this.maxwidth + 'px'
this.$emit('onSuccess', true)
}, // 验证成功函数
mouseMoveFn (e) {
if (this.mouseMoveStata) {
const width = e.clientX - this.beginClientX
if (width > 0 && width <= this.maxwidth) {
document.getElementsByClassName('handler')[0].style.left = width + 'px'
document.getElementsByClassName('drag_bg')[0].style.width = width + 'px'
} else if (width > this.maxwidth) {
this.successFunction()
}
}
}, // mousemove事件
moseUpFn (e) {
this.mouseMoveStata = false
var width = e.clientX - this.beginClientX
if (width < this.maxwidth) {
// ---- update-begin- author:sunjianlei --- date:20191009 --- for: 修复获取不到 handler 的时候报错 ----
const handler = document.getElementsByClassName('handler')[0]
if (handler) {
handler.style.left = 0 + 'px'
document.getElementsByClassName('drag_bg')[0].style.width = 0 + 'px'
}
// ---- update-end- author:sunjianlei --- date:20191009 --- for: 修复获取不到 handler 的时候报错 ----
}
} // mouseup事件
},
mounted () {
this.maxwidth = this.$refs.dragDiv.clientWidth - this.$refs.moveDiv.clientWidth
document.getElementsByTagName('html')[0].addEventListener('mousemove', this.mouseMoveFn)
document.getElementsByTagName('html')[0].addEventListener('mouseup', this.moseUpFn)
}
}
</script>
<style scoped>
.drag{
position: relative;
background-color: #e8e8e8;
width: 100%;
height: 34px;
line-height: 34px;
text-align: center;
}
.handler{
width: 40px;
height: 32px;
border: 1px solid #ccc;
cursor: move;
}
.handler_bg{
background: #fff url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0ZDhlNWY5My05NmI0LTRlNWQtOGFjYi03ZTY4OGYyMTU2ZTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NTEyNTVEMURGMkVFMTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTEyNTVEMUNGMkVFMTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2MTc5NzNmZS02OTQxLTQyOTYtYTIwNi02NDI2YTNkOWU5YmUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NGQ4ZTVmOTMtOTZiNC00ZTVkLThhY2ItN2U2ODhmMjE1NmU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+YiRG4AAAALFJREFUeNpi/P//PwMlgImBQkA9A+bOnfsIiBOxKcInh+yCaCDuByoswaIOpxwjciACFegBqZ1AvBSIS5OTk/8TkmNEjwWgQiUgtQuIjwAxUF3yX3xyGIEIFLwHpKyAWB+I1xGSwxULIGf9A7mQkBwTlhBXAFLHgPgqEAcTkmNCU6AL9d8WII4HOvk3ITkWJAXWUMlOoGQHmsE45ViQ2KuBuASoYC4Wf+OUYxz6mQkgwAAN9mIrUReCXgAAAABJRU5ErkJggg==") no-repeat center;
}
.handler_ok_bg{
background: #fff url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0ZDhlNWY5My05NmI0LTRlNWQtOGFjYi03ZTY4OGYyMTU2ZTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDlBRDI3NjVGMkQ2MTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDlBRDI3NjRGMkQ2MTFFNEI5NDBCMjQ2M0ExMDQ1OUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDphNWEzMWNhMC1hYmViLTQxNWEtYTEwZS04Y2U5NzRlN2Q4YTEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NGQ4ZTVmOTMtOTZiNC00ZTVkLThhY2ItN2U2ODhmMjE1NmU2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+k+sHwwAAASZJREFUeNpi/P//PwMyKD8uZw+kUoDYEYgloMIvgHg/EM/ptHx0EFk9I8wAoEZ+IDUPiIMY8IN1QJwENOgj3ACo5gNAbMBAHLgAxA4gQ5igAnNJ0MwAVTsX7IKyY7L2UNuJAf+AmAmJ78AEDTBiwGYg5gbifCSxFCZoaBMCy4A4GOjnH0D6DpK4IxNSVIHAfSDOAeLraJrjgJp/AwPbHMhejiQnwYRmUzNQ4VQgDQqXK0ia/0I17wJiPmQNTNBEAgMlQIWiQA2vgWw7QppBekGxsAjIiEUSBNnsBDWEAY9mEFgMMgBk00E0iZtA7AHEctDQ58MRuA6wlLgGFMoMpIG1QFeGwAIxGZo8GUhIysmwQGSAZgwHaEZhICIzOaBkJkqyM0CAAQDGx279Jf50AAAAAABJRU5ErkJggg==") no-repeat center;
}
.drag_bg{
background-color: #7ac23c;
height: 34px;
width: 0;
}
.drag_text{
position: absolute;
top: 0;
width: 100%;text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
-o-user-select:none;
-ms-user-select:none;
}
</style>
+678
View File
@@ -0,0 +1,678 @@
<template>
<div class="j-super-query-box">
<slot name="button" :isActive="superQueryFlag" :isMobile="izMobile" :open="handleOpen" :reset="handleReset">
<a-tooltip v-if="superQueryFlag" v-bind="tooltipProps" :mouseLeaveDelay="0.2">
<!-- begin 不知道为什么不加上这段代码就无法生效 -->
<span v-show="false">{{tooltipProps}}</span>
<!-- end 不知道为什么不加上这段代码就无法生效 -->
<template slot="title">
<span>{{ $t('superQuery.advancedQueryEffective') }}</span>
<a-divider type="vertical"/>
<a @click="handleReset">{{ $t('empty') }}</a>
</template>
<a-button-group>
<a-button type="primary" @click="handleOpen">
<a-icon type="appstore" theme="twoTone" spin/>
<span>{{ $t('superQuery.advancedQuery') }}</span>
</a-button>
<a-button v-if="izMobile" type="primary" icon="delete" @click="handleReset"/>
</a-button-group>
</a-tooltip>
<a-button v-else type="primary" icon="filter" @click="handleOpen">{{ $t('superQuery.advancedQuery') }}</a-button>
</slot>
<j-modal
:title="$t('superQuery.advancedQuery')+$t('superQuery.constructor')"
:width="1000"
:visible="visible"
@cancel="handleCancel"
:mask="false"
:fullscreen="izMobile"
class="j-super-query-modal"
style="top:5%;max-height: 95%;"
>
<template slot="footer">
<div style="float: left">
<a-button :loading="loading" @click="handleReset">{{ $t('reset') }}</a-button>
<a-button :loading="loading" @click="handleSave">{{ $t('superQuery.saveQueryCriteria') }}</a-button>
</div>
<a-button :loading="loading" @click="handleCancel">{{ $t('close') }}</a-button>
<a-button :loading="loading" type="primary" @click="handleOk">{{ $t('query') }}</a-button>
</template>
<a-spin :spinning="loading">
<a-row>
<a-col :sm="24" :md="24-5">
<a-empty v-if="queryParamsModel.length === 0" style="margin-bottom: 12px;">
<div slot="description">
<span>{{ $t('no') }}+{{ $t('query') }}+{{ $t('condition') }}</span>
<a-divider type="vertical"/>
<a @click="handleAdd">{{ $t('click') }}+{{ $t('newlyAdded') }}</a>
</div>
</a-empty>
<a-form v-else layout="inline">
<a-row style="margin-bottom: 12px;">
<a-col :md="12" :xs="24">
<a-form-item :label="$t('superQuery.filterMatching')" :labelCol="{md: 6,xs:24}" :wrapperCol="{md: 18,xs:24}" style="width: 100%;">
<a-select v-model="matchType" :getPopupContainer="node=>node.parentNode" style="width: 100%;">
<a-select-option value="and">AND{{$t('superQuery.allMatching')}}</a-select-option>
<a-select-option value="or">OR{{$t('superQuery.anyOneMatches')}}</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row type="flex" style="margin-bottom:10px" :gutter="16" v-for="(item, index) in queryParamsModel" :key="index">
<a-col :md="8" :xs="24" style="margin-bottom: 12px;">
<a-tree-select
:showSearch="true"
v-model="item.field"
:treeData="fieldTreeData"
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="$t('superQuery.selectQueryField')"
allowClear
treeDefaultExpandAll
:getPopupContainer="node=>node.parentNode"
style="width: 100%"
@select="(val,option)=>handleSelected(option,item)"
>
</a-tree-select>
</a-col>
<a-col :md="4" :xs="24" style="margin-bottom: 12px;">
<a-select :placeholder="$t('superQuery.matchRules')" :value="item.rule" :getPopupContainer="node=>node.parentNode" @change="handleRuleChange(item,$event)">
<a-select-option value="eq">{{ $t('superQuery.beEqualTo') }}</a-select-option>
<a-select-option value="like">{{ $t('superQuery.contain') }}</a-select-option>
<a-select-option value="right_like">{{ $t('superQuery.withStart') }}</a-select-option>
<a-select-option value="left_like">{{ $t('superQuery.withEnd') }}</a-select-option>
<a-select-option value="in">{{ $t('superQuery.in') }}</a-select-option>
<a-select-option value="ne">{{ $t('superQuery.notEqual') }}</a-select-option>
<a-select-option value="gt">{{ $t('superQuery.granter') }}</a-select-option>
<a-select-option value="ge">{{ $t('superQuery.greaterOrEqual') }}</a-select-option>
<a-select-option value="lt">{{ $t('superQuery.less') }}</a-select-option>
<a-select-option value="le">{{ $t('superQuery.lessOrEqual') }}</a-select-option>
</a-select>
</a-col>
<a-col :md="8" :xs="24" style="margin-bottom: 12px;">
<!-- 下拉搜索 -->
<j-search-select-tag v-if="item.type==='sel_search'" v-model="item.val" :dict="getDictInfo(item)" placeholder="请选择"/>
<!-- 下拉多选 -->
<template v-else-if="item.type==='list_multi'">
<j-multi-select-tag v-if="item.options" v-model="item.val" :options="item.options" :placeholder="$t('pleaseSelect')"/>
<j-multi-select-tag v-else v-model="item.val" :dictCode="getDictInfo(item)" :placeholder="$t('pleaseSelect')"/>
</template>
<template v-else-if="item.dictCode">
<template v-if="item.type === 'table-dict'">
<j-popup
v-model="item.val"
:code="item.dictTable"
:field="item.dictCode"
:orgFields="item.dictCode"
:destFields="item.dictCode"
:multi="true"
></j-popup>
</template>
<template v-else>
<j-multi-select-tag v-show="allowMultiple(item)" v-model="item.val" :dictCode="item.dictCode" :placeholder="$t('pleaseSelect')"/>
<j-dict-select-tag v-show="!allowMultiple(item)" v-model="item.val" :dictCode="item.dictCode" :placeholder="$t('pleaseSelect')"/>
</template>
</template>
<j-popup
v-else-if="item.type === 'popup'"
:value="item.val"
v-bind="item.popup"
group-id="superQuery"
@input="(e,v)=>handleChangeJPopup(item,e,v)"
:multi="true"/>
<j-select-multi-user
v-else-if="item.type === 'select-user' || item.type === 'sel_user'"
v-model="item.val"
:buttons="false"
:multiple="false"
:placeholder="$t('pleaseSelect')+$t('superQuery.user')"
:returnKeys="['id', item.customReturnField || 'username']"
/>
<j-select-depart
v-else-if="item.type === 'select-depart' || item.type === 'sel_depart'"
v-model="item.val"
:multi="false"
:placeholder="$t('pleaseSelect')+$t('department')"
:customReturnField="item.customReturnField || 'id'"
/>
<a-select
v-else-if="item.options instanceof Array"
v-model="item.val"
:options="item.options"
allowClear
:placeholder="$t('pleaseSelect')"
:mode="allowMultiple(item)?'multiple':''"
/>
<j-area-linkage v-model="item.val" v-else-if="item.type==='area-linkage' || item.type==='pca'" style="width: 100%"/>
<j-date v-else-if=" item.type + '' === 'date' " v-model="item.val" :placeholder="$t('pleaseSelect')+$t('date')" style="width: 100%"></j-date>
<j-date v-else-if=" item.type + '' ==='datetime' " v-model="item.val" :placeholder="$t('pleaseSelect')+$t('time')" :show-time="true" date-format="YYYY-MM-DD HH:mm:ss" style="width: 100%"></j-date>
<a-time-picker v-else-if="item.type + '' === 'time'" :value="item.val ? moment(item.val,'HH:mm:ss') : null" format="HH:mm:ss" style="width: 100%" @change="(time,value)=>item.val=value"/>
<a-input-number v-else-if=" item.type + '' === 'int'||item.type + '' === 'number' " style="width: 100%" :placeholder="$t('pleaseSelect')+$t('superQuery.numericalValue')" v-model="item.val"/>
<a-select v-else-if="item.type + '' === 'switch'" :placeholder="$t('pleaseSelect')" v-model="item.val">
<a-select-option value="Y">{{ $t('yes') }}</a-select-option>
<a-select-option value="N">{{ $t('not') }}</a-select-option>
</a-select>
<a-input v-else v-model="item.val" :placeholder="$t('superQuery.enterValue')"/>
</a-col>
<a-col :md="4" :xs="0" style="margin-bottom: 12px;">
<a-button @click="handleAdd" icon="plus"></a-button>&nbsp;
<a-button @click="handleDel( index )" icon="minus"></a-button>
</a-col>
<a-col :md="0" :xs="24" style="margin-bottom: 12px;text-align: right;">
<a-button @click="handleAdd" icon="plus"></a-button>&nbsp;
<a-button @click="handleDel( index )" icon="minus"></a-button>
</a-col>
</a-row>
</a-form>
</a-col>
<a-col :sm="24" :md="5">
<!-- 查询记录 -->
<a-card class="j-super-query-history-card" :bordered="true">
<div slot="title">
{{ $t('superQuery.savedQuery') }}
</div>
<a-empty v-if="saveTreeData.length === 0" class="j-super-query-history-empty" :description="$t('superQuery.noQueriesSaved')"/>
<a-tree
v-else
class="j-super-query-history-tree"
:showIcon="true"
:treeData="saveTreeData"
:selectedKeys="[]"
@select="handleTreeSelect"
>
</a-tree>
</a-card>
</a-col>
</a-row>
</a-spin>
<a-modal :title="$t('pleaseEnter')+$t('superQuery.savedName')" :visible="prompt.visible" @cancel="prompt.visible=false" @ok="handlePromptOk">
<a-input v-model="prompt.value"></a-input>
</a-modal>
</j-modal>
</div>
</template>
<script>
import moment from 'moment'
import * as utils from '@/utils/util'
import { mixinDevice } from '@/utils/mixin'
import JDate from '@/components/jero/JDate.vue'
import JSelectDepart from '@/components/jerobiz/JSelectDepart'
import JSelectMultiUser from '@/components/jerobiz/JSelectMultiUser'
import JMultiSelectTag from '@/components/dict/JMultiSelectTag'
import JAreaLinkage from '@comp/jero/JAreaLinkage'
export default {
name: 'JSuperQuery',
mixins: [mixinDevice],
components: { JAreaLinkage, JMultiSelectTag, JDate, JSelectDepart, JSelectMultiUser },
props: {
/*
fieldList: [{
value:'',
text:'',
type:'',
dictCode:'' // 只要 dictCode 有值,无论 type 是什么,都显示为字典下拉框
}]
type:date datetime int number string
* */
fieldList: {
type: Array,
required: true
},
/*
* 这个回调函数接收一个数组参数 即查询条件
* */
callback: {
type: String,
required: false,
default: 'handleSuperQuery'
},
// 当前是否在加载中
loading: {
type: Boolean,
default: false
},
// 保存查询条件的唯一 code,通过该 code 区分
// 默认为 null,代表以当前路由全路径为区分Code
saveCode: {
type: String,
default: null
}
},
data () {
return {
moment,
fieldTreeData: [],
prompt: {
visible: false,
value: ''
},
visible: false,
queryParamsModel: [],
treeIcon: <a-icon type="file-text"/>,
// 保存查询条件的treeData
saveTreeData: [],
// 保存查询条件的前缀名
saveCodeBefore: 'JSuperQuerySaved_',
// 查询类型,过滤条件匹配(and、or)
matchType: 'and',
superQueryFlag: false
}
},
computed: {
izMobile () {
return this.device === 'mobile'
},
tooltipProps () {
return this.izMobile ? { visible: false } : {}
},
fullSaveCode () {
let saveCode = this.saveCode
if (saveCode == null || saveCode === '') {
saveCode = this.$route.fullPath
}
return this.saveCodeBefore + saveCode
}
},
watch: {
// 当 saveCode 变化时,重新查询已保存的条件
fullSaveCode: {
immediate: true,
handler () {
const list = this.$ls.get(this.fullSaveCode)
if (list instanceof Array) {
this.saveTreeData = list.map(i => this.renderSaveTreeData(i))
}
}
},
fieldList: {
deep: true,
immediate: true,
handler (val) {
const mainData = []; const subData = []
val.forEach(item => {
const data = { ...item }
data.label = data.label || data.text
const hasChildren = (data.children instanceof Array)
data.disabled = hasChildren
data.selectable = !hasChildren
if (hasChildren) {
data.children = data.children.map(item2 => {
const child = { ...item2 }
child.label = child.label || child.text
child.label = data.label + '-' + child.label
child.value = data.value + ',' + child.value
child.val = ''
return child
})
data.val = ''
subData.push(data)
} else {
mainData.push(data)
}
})
this.fieldTreeData = mainData.concat(subData)
}
}
},
methods: {
show () {
if (!this.queryParamsModel || this.queryParamsModel.length === 0) {
this.resetLine()
}
this.visible = true
},
getDictInfo (item) {
let str = ''
if (!item.dictTable) {
str = item.dictCode
} else {
str = item.dictTable + ',' + item.dictText + ',' + item.dictCode
}
console.log('高级查询字典信息', str)
return str
},
handleOk () {
if (!this.isNullArray(this.queryParamsModel)) {
const event = {
matchType: this.matchType,
params: this.removeEmptyObject(this.queryParamsModel)
}
// 移动端模式下关闭弹窗
if (this.izMobile) {
this.visible = false
}
this.emitCallback(event)
} else {
this.$message.warn(this.$t('superQuery.cannotQueryEmpty'))
}
},
emitCallback (event = {}) {
const { params = [], matchType = this.matchType } = event
this.superQueryFlag = (params && params.length > 0)
for (const param of params) {
if (Array.isArray(param.val)) {
param.val = param.val.join(',')
}
}
console.debug('---高级查询参数--->', { params, matchType })
this.$emit(this.callback, params, matchType)
},
handleCancel () {
this.close()
},
close () {
this.$emit('close')
this.visible = false
},
handleAdd () {
this.addNewLine()
},
addNewLine () {
this.queryParamsModel.push({ rule: 'eq' })
},
resetLine () {
this.superQueryFlag = false
this.queryParamsModel = []
this.addNewLine()
},
handleDel (index) {
this.queryParamsModel.splice(index, 1)
},
handleSelected (node, item) {
const { type, dbType, options, dictCode, dictTable, dictText, customReturnField, popup } = node.dataRef
item.type = type
item.dbType = dbType
item.options = options
item.dictCode = dictCode
item.dictTable = dictTable
item.dictText = dictText
item.customReturnField = customReturnField
if (popup) {
item.popup = popup
}
this.$set(item, 'val', undefined)
},
handleOpen () {
this.show()
},
handleReset () {
this.resetLine()
this.emitCallback()
},
handleSave () {
const queryParams = this.removeEmptyObject(this.queryParamsModel)
if (this.isNullArray(queryParams)) {
this.$message.warning(this.$t('superQuery.emptyCannotSaved'))
} else {
this.prompt.value = ''
this.prompt.visible = true
}
},
handlePromptOk () {
const { value } = this.prompt
if (!value) {
this.$message.warning(this.$t('preservation') + this.$t('name') + this.$t('cannotEmpty'))
return
}
// 取出查询条件
const records = this.removeEmptyObject(this.queryParamsModel)
// 判断有没有重名的
const filterList = this.saveTreeData.filter(i => i.originTitle === value)
if (filterList.length > 0) {
this.$confirm({
content: `${value} ` + this.$t('superQuery.alreadyExists'),
onOk: () => {
this.prompt.visible = false
filterList[0].records = records
this.saveToLocalStore()
this.$message.success(this.$t('savedSuccessfully'))
}
})
} else {
// 没有重名的,直接添加
this.prompt.visible = false
// 添加到树列表中
this.saveTreeData.push(this.renderSaveTreeData({
title: value,
matchType: this.matchType,
records: records
}))
// 保存到 LocalStore
this.saveToLocalStore()
this.$message.success(this.$t('SavedSuccessfully'))
}
},
handleTreeSelect (idx, event) {
if (event.selectedNodes[0]) {
const { matchType, records } = event.selectedNodes[0].data.props
// 将保存的matchType取出,兼容旧数据,如果没有保存就还是使用原来的
this.matchType = matchType || this.matchType
this.queryParamsModel = utils.cloneObject(records)
}
},
handleRemoveSaveTreeItem (event, vNode) {
// 阻止事件冒泡
event.stopPropagation()
this.$confirm({
content: this.$t('superQuery.deleteQuery'),
onOk: () => {
const { eventKey } = vNode
this.saveTreeData.splice(Number.parseInt(eventKey.substring(2)), 1)
this.saveToLocalStore()
}
})
},
// 将查询保存到 LocalStore 里
saveToLocalStore () {
const saveValue = this.saveTreeData.map(({ originTitle, matchType, records }) => ({ title: originTitle, matchType, records }))
this.$ls.set(this.fullSaveCode, saveValue)
},
isNullArray (array) {
// 判断是不是空数组对象
if (!array || array.length === 0) {
return true
}
if (array.length === 1) {
const obj = array[0]
if (!obj.field || (obj.val == null || obj.val === '') || !obj.rule) {
return true
}
}
return false
},
// 去掉数组中的空对象
removeEmptyObject (arr) {
const array = utils.cloneObject(arr)
for (let i = 0; i < array.length; i++) {
const item = array[i]
if (item == null || Object.keys(item).length <= 0) {
array.splice(i--, 1)
} else {
if (Array.isArray(item.options)) {
// 如果有字典属性,就不需要保存 options 了
// update-begin-author:taoyan date:20200819 for:【开源问题】 高级查询 下拉框作为并且选项很多多多 LOWCOD-779
delete item.options
// update-end-author:taoyan date:20200819 for:【开源问题】 高级查询 下拉框作为并且选项很多多多 LOWCOD-779
}
}
}
return array
},
/** 渲染保存查询条件的 title(加个删除按钮) */
renderSaveTreeData (item) {
item.icon = this.treeIcon
item.originTitle = item.title
item.title = (arg1, arg2) => {
let vNode
// 兼容旧版的Antdv
if (arg1.dataRef) {
vNode = arg1
} else if (arg2.dataRef) {
vNode = arg2
} else {
return <span style="color:red;">Antdv版本不支持</span>
}
const { originTitle } = vNode.dataRef
return (
<div class="j-history-tree-title">
<span>{originTitle}</span>
<div class="j-history-tree-title-closer" onClick={e => this.handleRemoveSaveTreeItem(e, vNode)}>
<a-icon type="close-circle"/>
</div>
</div>
)
}
return item
},
/** 判断是否允许多选 */
allowMultiple (item) {
return item.rule === 'in'
},
handleRuleChange (item, newValue) {
const oldValue = item.rule
this.$set(item, 'rule', newValue)
// 上一个规则是否是 in,且type是字典或下拉
if (oldValue === 'in') {
if (item.dictCode || item.options instanceof Array) {
let value = item.val
if (typeof item.val === 'string') {
value = item.val.split(',')[0]
} else if (Array.isArray(item.val)) {
value = item.val[0]
}
this.$set(item, 'val', value)
}
}
},
handleChangeJPopup (item, e, values) {
item.val = values[item.popup.destFields]
}
}
}
</script>
<style lang="less" scoped>
.j-super-query-box {
display: inline-block;
}
.j-super-query-modal {
.j-super-query-history-card {
/deep/ .ant-card-body,
/deep/ .ant-card-head-title {
padding: 0;
}
/deep/ .ant-card-head {
padding: 4px 8px;
min-height: initial;
}
}
.j-super-query-history-empty {
/deep/ .ant-empty-image {
height: 80px;
line-height: 80px;
margin-bottom: 0;
}
/deep/ img {
width: 80px;
height: 65px;
}
/deep/ .ant-empty-description {
color: #afafaf;
margin: 8px 0;
}
}
.j-super-query-history-tree {
.j-history-tree-title {
width: calc(100% - 24px);
position: relative;
display: inline-block;
&-closer {
color: #999999;
position: absolute;
top: 0;
right: 0;
width: 24px;
height: 24px;
text-align: center;
opacity: 0;
transition: opacity 0.3s, color 0.3s;
&:hover {
color: #666666;
}
&:active {
color: #333333;
}
}
&:hover {
.j-history-tree-title-closer {
opacity: 1;
}
}
}
/deep/ .ant-tree-switcher {
display: none;
}
/deep/ .ant-tree-node-content-wrapper {
width: 100%;
}
}
}
</style>
+96
View File
@@ -0,0 +1,96 @@
<template>
<div>
<a-select
v-if="query"
style="width: 100%"
@change="handleSelectChange"
>
<a-select-option v-for="(item, index) in queryOption" :key="index" :value="item.value">
{{ item.text }}
</a-select-option>
</a-select>
<a-switch
v-else
v-model="checkStatus"
:disabled="disabled"
@change="handleChange"
v-bind="$attrs"
v-on="childListeners"/>
</div>
</template>
<script>
export default {
name: 'JSwitch',
props: {
value: {
type: [String, Number],
required: false
},
disabled: {
type: Boolean,
required: false,
default: false
},
options: {
type: Array,
required: false,
default: () => ['Y', 'N']
},
query: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
checkStatus: false
}
},
computed: {
queryOption () {
const arr = []
arr.push({ value: this.options[0], text: '是' })
arr.push({ value: this.options[1], text: '否' })
return arr
},
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
}
},
watch: {
value: {
immediate: true,
handler (val) {
if (!this.query) {
if (!val) {
this.checkStatus = false
this.$emit('change', this.options[1])
} else {
this.checkStatus = this.options[0] + '' === val + ''
}
}
}
}
},
methods: {
handleChange (checked) {
const flag = checked === false ? this.options[1] : this.options[0]
this.$emit('change', flag)
},
handleSelectChange (value) {
this.$emit('change', value)
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
+431
View File
@@ -0,0 +1,431 @@
<template>
<a-table
class="j-table"
ref="table"
:bordered="bordered"
:table-key="tableKey"
:columns="resultColumns"
:components="components"
:scroll="scroll || selfScroll"
v-bind="$attrs"
v-on="$listeners">
<!-- 如果插槽是作用域插槽将作用域插槽的props包装成对象传递给slot -->
<template v-for="(_, slotName) in $scopedSlots" :slot="slotName" slot-scope="text, record, index">
<slot :name="slotName" v-bind="{text, record, index}" />
</template>
<!-- 如果是普通插槽直接使用$slots渲染 -->
<slot v-for="(_, slotName) in $slots" :name="slotName" :slot="slotName"/>
<div slot="settingDropdown">
<a-card>
<a-table
rowKey="key"
:style="settingStyle"
tableLayout="fixed"
size="small"
bordered
:pagination="false"
:scroll="settingScroll"
:data-source="settingDataSource"
:columns="settingColumns">
<a-checkbox slot="hide" slot-scope="text, record"
:checked="text"
:disabled="record.disabledHide"
@change="changeSetting(text, 'hide', record)"></a-checkbox>
<a-checkbox slot="freeze" slot-scope="text, record"
:checked="text"
:disabled="record.disabledFreeze"
@change="changeSetting(text, 'freeze', record)"></a-checkbox>
</a-table>
</a-card>
</div>
<a-icon ref="settingBtn" slot="settingIcon" type="setting" :style="{ fontSize:'16px', color: '#108ee9' }" />
</a-table>
</template>
<script>
import Vue from 'vue'
import { cloneDeep } from 'lodash'
import VueDraggableResizable from 'vue-draggable-resizable'
Vue.component('vue-draggable-resizable', VueDraggableResizable)
const actionKey = 'action'
// 保存localStorage中的后缀
const saveSuffix = ':JTable'
// 拿到column的key
const getKey = col => col.key || col.dataIndex
// 所有JTable的缓存key
const J_TABLE_KEYS = 'J_TABLE_KEYS'
export default {
name: 'JTable',
props: {
// 用于持久化存储列配置,保证唯一
tableKey: {
type: String,
required: true
},
columns: {
type: Array,
required: true
},
// 配置表的样式
settingStyle: {
type: Object,
required: false,
default: () => ({ width: '300px' })
},
// 配置表的scroll
settingScroll: {
type: Object,
required: false,
default: () => ({ y: 300 })
},
// 如果需要拖拽后增加总列宽而不是占用其他列宽度:需要父组件 :scroll.sync="scroll"
scroll: {
type: Object,
required: false
},
// 列最小宽度
columnMinWidth: {
type: Number,
required: false,
default: 100
},
bordered: {
type: Boolean,
required: false
}
},
data () {
return {
// settingVisible: false,
// 设置表格
settingColumns: [
{ title: '列名', dataIndex: 'title', align: 'center', ellipsis: true, width: 150 },
{ title: '隐藏', dataIndex: 'hide', align: 'center', ellipsis: true, width: 60, scopedSlots: { customRender: 'hide' } },
{ title: '冻结', dataIndex: 'freeze', align: 'center', ellipsis: true, width: 60, scopedSlots: { customRender: 'freeze' } }
],
settingDataSource: [],
settingColumnsObj: { hide: [], freeze: [] }, // 本地存储的配置对象
resultColumns: [],
components: {
header: {
cell: this.initDrag(this.columns)
}
},
selfScroll: {}
}
},
watch: {
// 配置列改变,将配置转换成配置表格数据
settingColumnsObj: {
deep: true, // 深度监听
immediate: true, // 挂载先执行一次
handler () {
const dataSource = []
this.columns.forEach(col => {
const tempKey = getKey(col)
// 操作栏不可配置
if (tempKey === actionKey) {
return
}
// hideSettingColumn: true 可以让不显示字段控制
if (col.hideSettingColumn) {
return
}
dataSource.push({
title: col.title || col.columnTitle, // 用于显示的字段
key: tempKey, // 用于存储的字段
hide: this.settingColumnsObj.hide.includes(tempKey), // 是否隐藏
freeze: this.settingColumnsObj.freeze.includes(tempKey), // 是否冻结
disabledHide: col.disabledHide, // 禁用隐藏
disabledFreeze: col.disabledFreeze // 禁用冻结
})
})
this.settingDataSource = dataSource
// 自定义列配置修改后,更新表格字段
this.setResultColumns(this.columns)
}
}
},
methods: {
// 将父组件的columns进行处理:增加设置列按钮
setResultColumns (columns) {
// 深度克隆,防止直接修改父组件数据
columns = cloneDeep(columns)
// 设置按钮插槽配置
const settingOption = {
filterDropdown: 'settingDropdown',
filterIcon: 'settingIcon'
}
let findActionIndex = columns.findIndex(col => col && (getKey(col) === actionKey))
// 将设置按钮放到表格右上角,如果有操作栏就固定到右侧(操作栏固定右侧,没有的话就最后一列)
if (findActionIndex === -1) {
findActionIndex = columns.length - 1
}
if (columns[findActionIndex].scopedSlots instanceof Object) {
Object.assign(columns[findActionIndex].scopedSlots, settingOption)
} else {
columns[findActionIndex].scopedSlots = settingOption
}
// 自定义设置弹框(尝试解决表格刷新丢失弹框的问题)此方法会出现两个弹框
// columns[findActionIndex].filterDropdownVisible = this.settingVisible
// columns[findActionIndex].onFilterDropdownVisibleChange = (visible) => {
// this.settingVisible = visible
// }
// 最后过滤掉需要隐藏的字段,按定位进行排序
const startArr = [] // 冻结左侧的列
const midArr = [] // 没有冻结的列
const endArr = [] // 冻结右侧的列(只能是action,暂时不能自定义固定右侧)
columns.forEach(col => {
const key = getKey(col)
const fixed = this.settingColumnsObj.freeze.includes(key)
col.hide = this.settingColumnsObj.hide.includes(key)
col.fixed = key === actionKey ? 'right' : fixed
// 如果是字符串就转换成数字(因为实现拖拽,单位只能是px)
col.width = col.width && parseInt(col.width + '')
// 如果没有最小宽度就获取统一设置的最小宽度
col.minWidth = (col.minWidth && parseInt(col.minWidth + '')) || this.columnMinWidth
if (!col.hide) {
// 如果没有宽度就取minWidth(不添加原数据的width属性,这里只对需要固定的minWidth生效)
const width = col.width || col.minWidth
switch (col.fixed) {
case true:
case 'left':
startArr.push({ ...col, width })
break
case 'right':
endArr.push({ ...col, width })
break
default:
midArr.push(col)
}
}
})
// 更新父组件的数据,将排序后的进行渲染
this.$emit('update:columns', columns)
// 按冻结顺序排序
this.resultColumns = [
...startArr.sort((a, b) => {
const freezeArr = this.settingColumnsObj.freeze
return freezeArr.indexOf(getKey(a)) - freezeArr.indexOf(getKey(b))
}),
...midArr,
...endArr
]
return this.resultColumns
},
// 初始化表格拖拽
initDrag (columns) {
// 没有边框就不能拖拽
if (!this.bordered) {
return
}
// 如果没有任何列开启拖拽就不需要拖拽
if (!columns.some(col => col.resizable)) {
return
}
// 第一步:列宽映射
const draggingMap = {}
columns.forEach((col) => {
draggingMap[getKey(col)] = col.width
})
const draggingState = Vue.observable(draggingMap)
// 第二步:表头渲染
return (h, props, children) => {
// 表头DOM
let thDom = null
// 获取列的key值和特性
const { key, ...restProps } = props
// 获取最新列配置
const columns = cloneDeep(this.columns)
let col
if (key === 'selection-column') {
col = {}
} else {
col = columns.find(col => getKey(col) === key)
}
// 没有开启拖拽 或 没有宽度 或 有定位,都不能拖拽(防止布局异常,至少有一列不设width并且不能有fixed)
if (!col.resizable || !col.width || !!col.fixed) {
return <th {...restProps}>{children}</th>
}
// 开始拖拽监听
const onDragging = (x) => {
const beforeWidth = col.width
const scroll = cloneDeep(this.scroll) || {}
draggingState[key] = 0
col.width = Math.max(x, col.minWidth || this.columnMinWidth)
// 如果有scroll.x就计算差值,否则获取所有列的和
if (scroll.x) {
scroll.x = scroll.x + (col.width - beforeWidth)
} else {
scroll.x = 0
this.columns.forEach(col => (scroll.x += (col.width || col.minWidth || this.columnMinWidth)))
}
// 将计算好的宽度更新
if (this.scroll) {
this.$emit('update:scroll', scroll)
} else {
this.selfScroll = scroll
}
// 修改列宽后,更新表格字段
this.setResultColumns(columns)
}
// 停止拖拽监听
const onDragstop = () => {
draggingState[key] = thDom.getBoundingClientRect().width
}
// 控制最小拖拽宽度
const onDrag = (x) => {
return x >= (col.minWidth || this.columnMinWidth)
}
return (
<th
{...restProps}
v-ant-ref={(r) => (thDom = r)}
width={col.width}
class="resize-table-th"
>
{children}
<vue-draggable-resizable
key={getKey(col)}
class="table-draggable-handle"
minw={10}
w={10}
x={col.width || draggingState[key]}
z={1}
axis="x"
draggable={true}
resizable={false}
props={{ onDrag }}
onDragging={onDragging}
onDragstop={onDragstop}
></vue-draggable-resizable>
</th>
)
}
},
/**
* 配置表中复选框改变状态的事件
* @param text 修改前的状态
* @param type 隐藏还是冻结
* @param record 当前行数据
*/
changeSetting (text, type, record) {
const checked = !text // text是点击前的状态,取反就是要修改的状态
// console.log('修改了配置', checked, type, record)
// 修改当前行配置
record[type] = !checked
// 勾选就添加,取消就删除
if (checked) {
this.settingColumnsObj[type].push(record.key)
} else {
this.settingColumnsObj[type].splice(this.settingColumnsObj[type].findIndex(item => item === record.key), 1)
}
this.saveSetting()
/**
* 弹框消失问题 冻结列出现和消失会导致dom结构改变而从更新结构丢失弹框
* 解决方案,刷新前获取dom,刷新后再次获取,如果获取不一致就触发点击事件点开弹框
*/
const beforeIsFixed = !!this.$refs.table.$el.querySelector('.ant-table-fixed-left')
this.$nextTick(() => {
const afterIsFixed = !!this.$refs.table.$el.querySelector('.ant-table-fixed-left')
// console.log(beforeIsFixed !== afterIsFixed)
if (beforeIsFixed !== afterIsFixed) {
this.$refs.settingBtn.$el.click()
}
})
},
// 初始化配置,从columns里获取
initSetting (columns) {
const hide = []
const freeze = []
columns.forEach(col => {
const key = getKey(col)
// hide属性控制隐藏
if (col.hide) {
hide.push(key)
}
// fixed冻结到左侧
if (col.fixed === true || col.fixed === 'left') {
freeze.push(key)
}
})
this.settingColumnsObj = { hide, freeze }
this.saveSetting()
},
// 配置保存到本地
saveSetting () {
// 获取所有的jtable缓存的key数组(用于清除缓存,没有就初始化数组)
const jTableKeys = Vue.ls.get(J_TABLE_KEYS) || []
const saveKey = this.tableKey + saveSuffix
// 如果不在数组里面就追加进去
if (!jTableKeys.includes(saveKey)) {
jTableKeys.push(saveKey)
}
Vue.ls.set(J_TABLE_KEYS, jTableKeys, 7 * 24 * 60 * 60 * 10)
Vue.ls.set(saveKey, this.settingColumnsObj, 7 * 24 * 60 * 60 * 10)
},
// 还原默认配置
resteColumns () {
this.initSetting(this.columnsBak)
},
// 清除本地的配置(当前的JTable)
clearSetting () {
this.settingColumnsObj = { hide: [], freeze: [] }
Vue.ls.remove(this.tableKey + saveSuffix)
this.resteColumns()
this.$message.success('成功清除当前JTable的缓存!')
},
// 清除所有缓存
clearAllCacheSetting () {
// 获取到之后遍历删除,最后把keys数组删除
const jTableKeys = Vue.ls.get(J_TABLE_KEYS)
jTableKeys && jTableKeys.forEach(key => {
Vue.ls.remove(key)
})
Vue.ls.remove(J_TABLE_KEYS)
this.resteColumns() // 这里只能刷新当前的表
this.$message.success('成功清除全局JTable的缓存!')
}
},
mounted () {
// 备份默认配置
this.columnsBak = cloneDeep(this.columns)
const settingColumnsObj = Vue.ls.get(this.tableKey + saveSuffix)
// 第一次进页面或清空缓存进行初始化
if (settingColumnsObj) {
this.settingColumnsObj = settingColumnsObj
} else {
this.initSetting(this.columns)
}
},
created () {
}
}
</script>
<style lang="less">
.j-table {
.resize-table-th {
position: relative;
.table-draggable-handle {
transform: none !important;
position: absolute !important;
height: 100% !important;
bottom: 0;
left: auto !important;
right: -5px;
//width: 10px !important;
cursor: col-resize;
touch-action: none;
}
}
}
</style>
+89
View File
@@ -0,0 +1,89 @@
<template>
<a-time-picker
:disabled="disabled || readOnly"
:placeholder="placeholder"
:value="momVal"
:format="dateFormat"
v-bind="$attrs"
v-on="childListeners"
:getCalendarContainer="getCalendarContainer"
@change="handleTimeChange"/>
</template>
<script>
import moment from 'moment'
export default {
name: 'JTime',
props: {
placeholder: {
type: String,
default: '',
required: false
},
value: {
type: String,
required: false
},
dateFormat: {
type: String,
default: 'HH:mm:ss',
required: false
},
readOnly: {
type: Boolean,
required: false,
default: false
},
disabled: {
type: Boolean,
required: false,
default: false
},
getCalendarContainer: {
type: Function,
default: (node) => node.parentNode
}
},
data () {
const timeStr = this.value
return {
decorator: '',
momVal: !timeStr ? null : moment(timeStr, this.dateFormat)
}
},
computed: {
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
}
},
watch: {
value (val) {
if (!val) {
this.momVal = null
} else {
this.momVal = moment(val, this.dateFormat)
}
}
},
methods: {
moment,
handleTimeChange (mom, timeStr) {
this.$emit('change', timeStr)
}
},
// 2.2新增 在组件内定义 指定父组件调用时候的传值属性和事件类型 这个牛逼
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped>
</style>
+215
View File
@@ -0,0 +1,215 @@
<template>
<a-tree-select
allowClear
labelInValue
style="width: 100%"
:disabled="disabled"
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="placeholder"
:loadData="asyncLoadTreeData"
:value="treeValue"
:treeData="treeData"
@change="onChange"
@search="onSearch"
v-bind="_attrs"
v-on="childListeners">
</a-tree-select>
</template>
<script>
import { getAction } from '@/api/manage'
export default {
name: 'JTreeDict',
data () {
return {
treeData: [],
treeValue: null,
url_root: '/sys/category/loadTreeRoot',
url_children: '/sys/category/loadTreeChildren',
url_view: '/sys/category/loadOne'
}
},
props: {
value: {
// type: String,
required: false
},
placeholder: {
type: String,
default: '请选择',
required: false
},
parentCode: {
type: String,
default: '',
required: false
},
field: {
type: String,
default: 'id',
required: false
},
root: {
type: Object,
required: false,
default: () => {
return {
pid: '0'
}
}
},
async: {
type: Boolean,
default: false,
required: false
},
disabled: {
type: Boolean,
default: false,
required: false
}
},
watch: {
root: {
handler (val) {
console.log('root-change', val)
},
deep: true
},
parentCode: {
handler () {
this.loadRoot()
}
},
value: {
handler () {
this.loadViewInfo()
}
}
},
computed: {
_attrs () {
return { ...this.$attrs }
},
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
}
},
created () {
this.loadRoot()
this.loadViewInfo()
},
model: {
prop: 'value',
event: 'change'
},
methods: {
loadViewInfo () {
if (!this.value || this.value + '' === '0') {
this.treeValue = null
} else {
const param = {
field: this.field,
val: this.value
}
getAction(this.url_view, param).then(res => {
if (res.success) {
this.treeValue = {
value: this.value,
label: res.result.name
}
}
})
}
},
loadRoot () {
const param = {
async: this.async,
pcode: this.parentCode
}
getAction(this.url_root, param).then(res => {
if (res.success) {
this.handleTreeNodeValue(res.result)
this.treeData = [...res.result]
} else {
this.$message.error(res.message)
}
})
},
asyncLoadTreeData (treeNode) {
return new Promise((resolve) => {
if (!this.async) {
resolve()
return
}
if (treeNode.$vnode.children) {
resolve()
return
}
const pid = treeNode.$vnode.key
const param = {
pid: pid
}
getAction(this.url_children, param).then(res => {
if (res.success) {
this.handleTreeNodeValue(res.result)
this.addChildren(pid, res.result, this.treeData)
this.treeData = [...this.treeData]
}
resolve()
})
})
},
addChildren (pid, children, treeArray) {
if (treeArray && treeArray.length > 0) {
for (const item of treeArray) {
if (item.key + '' === pid + '') {
if (!children || children.length === 0) {
item.leaf = true
} else {
item.children = children
}
break
} else {
this.addChildren(pid, children, item.children)
}
}
}
},
handleTreeNodeValue (result) {
const storeField = this.field + '' === 'code' ? 'code' : 'key'
for (const i of result) {
i.value = i[storeField]
i.isLeaf = !(!i.leaf)
if (i.children && i.children.length > 0) {
this.handleTreeNodeValue(i.children)
}
}
},
onChange (value) {
if (!value) {
/*
* 使用$listeners向上暴露事件---和$emit一起使用出现的问题:change事件会执行两遍
* 解决办法:改变选中时提交的事件名 */
this.$emit('change', '')
} else {
this.$emit('change', value.value)
}
this.treeValue = value
},
onSearch (value) {
console.log(value)
},
getCurrTreeData () {
return this.treeData
}
}
}
</script>
+335
View File
@@ -0,0 +1,335 @@
<template>
<div>
<a-tree-select
v-if="isAsync"
allowClear
tree-node-filter-prop="title"
:getPopupContainer="(node) => node.parentNode"
style="width: 100%"
:disabled="disabled"
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="placeholder"
:loadData="asyncLoadTreeData"
:value="treeValue"
:treeData="treeData"
:multiple="multiple"
:show-search="!multiple"
v-bind="_attrs"
v-on="childListeners"
@change="onChange">
</a-tree-select>
<a-tree-select
v-if="!isAsync"
allowClear
tree-node-filter-prop="title"
:getPopupContainer="(node) => node.parentNode"
style="width: 100%"
:disabled="disabled"
:dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="placeholder"
:value="treeValue"
:treeData="treeData"
:multiple="multiple"
:show-search="!multiple"
v-bind="_attrs"
v-on="$listeners"
@change="onChange">
</a-tree-select>
</div>
</template>
<script>
/*
* 异步树加载组件 通过传入表名 显示字段 存储字段 加载一个树控件
* <j-tree-select dict="aa_tree_test,aad,id" pid-field="pid" ></j-tree-select>
* */
import { getAction } from '@/api/manage'
export default {
name: 'JTreeSelect',
props: {
isAsync: { // 是否采用异步方式初始化树
type: Boolean,
required: false,
default: true
},
value: {
// type: String,
required: false
},
placeholder: {
type: String,
default: '请选择',
required: false
},
dict: {
type: String,
default: '',
required: false
},
pidField: {
type: String,
default: 'pid',
required: false
},
pidValue: {
type: String,
default: '',
required: false
},
disabled: {
type: Boolean,
default: false,
required: false
},
hasChildField: {
type: String,
default: '',
required: false
},
condition: {
type: String,
default: '',
required: false
},
// 是否支持多选
multiple: {
type: Boolean,
default: false
},
loadTriggleChange: {
type: Boolean,
default: false,
required: false
}
},
data () {
return {
treeValue: null, // 选中的数据
treeData: [], // 渲染树的数组
url: '/sys/dict/loadTreeData',
view: '/sys/dict/loadDictItem/',
tableName: '',
text: '',
code: ''
}
},
watch: {
value () {
this.loadItemByCode()
},
dict () {
this.initDictInfo()
this.loadRoot()
}
},
computed: {
_attrs () {
return { ...this.$attrs }
},
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
}
},
created () {
this.validateProp().then(() => {
this.initDictInfo()
this.loadRoot()
this.loadItemByCode()
})
},
mounted () {
window.that = this
},
methods: {
loadItemByCode () {
if (!this.value || this.value + '' === '0') {
this.treeValue = null
} else {
getAction(`${this.view}${this.dict}`, { key: this.value }).then(res => {
if (res.success) {
this.treeValue = this.value.split(',') // v-model接收的是string || string[]
// 将节点名称显示在选择框上
this.treeValue = res.result[0]
this.onLoadTriggleChange(res.result[0])
}
})
}
},
onLoadTriggleChange (text) {
// 只有单选才会触发
if (!this.multiple && this.loadTriggleChange) {
this.$emit('change', this.value, text)
}
},
initDictInfo () { // 取出父组件传过来的codetexttableName
const arr = this.dict.split(',')
this.tableName = arr[0]
this.text = arr[1]
this.code = arr[2]
},
// 异步加载树节点
asyncLoadTreeData (treeNode) {
debugger
return new Promise((resolve) => {
if (treeNode.$vnode.children) {
resolve()
return
}
const pid = treeNode.$vnode.key
const param = {
pid: pid,
tableName: this.tableName,
text: this.text,
code: this.code,
pidField: this.pidField,
hasChildField: this.hasChildField,
condition: this.condition
}
getAction(this.url, param).then(res => {
if (res.success) {
for (const i of res.result) {
i.value = i.key
if (i.leaf === false) {
i.isLeaf = false
} else if (i.leaf === true) {
i.isLeaf = true
}
}
this.addChildren(pid, res.result, this.treeData)
this.treeData = [...this.treeData]
}
resolve()
})
})
},
addChildren (pid, children, treeArray) {
if (treeArray && treeArray.length > 0) {
for (const item of treeArray) {
if (item.key + '' === pid + '') { // 找到当前元素所在的父节点
if (!children || children.length === 0) {
item.isLeaf = true
} else {
item.children = children // 搜索出来的children添加到原树子children
}
break
} else {
this.addChildren(pid, children, item.children)
}
}
}
},
/**
* 初始化树
* isAsynctrue===异步获取 false===同步获取数据 默认异步 */
loadRoot () {
if (this.isAsync) {
const param = {
pid: this.pidValue,
tableName: this.tableName,
text: this.text,
code: this.code,
pidField: this.pidField,
hasChildField: this.hasChildField,
condition: this.condition
}
getAction(this.url, param).then(res => {
if (res.success && res.result) {
for (const i of res.result) {
i.value = i.key
if (i.leaf === false) {
i.isLeaf = false
} else if (i.leaf === true) {
i.isLeaf = true
}
}
this.treeData = [...res.result]
} else {
console.log('数根节点查询结果-else', res)
}
})
} else {
// 同步
const param = {
code: this.code,
pidField: this.pidField,
tableName: this.tableName,
text: this.text
}
getAction('/sys/dict/queryAllTreeData', param).then((res) => {
if (res.success) {
this.treeData = deepFor(res.result)
}
})
}
},
onChange (value) {
if (!value) {
/*
* 使用$listeners向上暴露事件---和$emit一起使用出现的问题:change事件会执行两遍
* 解决办法:改变选中时提交的事件名 */
this.$emit('change', '')
this.treeValue = null
} else if (value instanceof Array) {
// 多选
this.$emit('change', value.join(',').toString()) // 修改第一次选中时,选中为空的情况
this.treeValue = value
} else {
// 单选
this.$emit('change', value)
this.treeValue = value
}
},
getCurrTreeData () {
return this.treeData
},
validateProp () {
const myCondition = this.condition
return new Promise((resolve, reject) => {
if (!myCondition) {
resolve()
} else {
try {
const test = JSON.parse(myCondition)
if (typeof test === 'object' && test) {
resolve()
} else {
this.$message.error('组件JTreeSelect-condition传值有误,需要一个json字符串!')
reject()
}
} catch (e) {
this.$message.error('组件JTreeSelect-condition传值有误,需要一个json字符串!')
reject()
}
}
})
}
},
// 2.2新增 在组件内定义 指定父组件调用时候的传值属性和事件类型
model: {
prop: 'value',
event: 'change'
}
}
// 递归整个树,判断是否是叶子节点----同步获取数据时用到
function deepFor (source) {
for (const i of source) {
i.value = i.key
if (i.leaf === false) {
i.isLeaf = false
} else if (i.leaf === true) {
i.isLeaf = true
}
if (i.children && i.children.length > 0) {
deepFor(i.children)
}
}
return source
}
</script>
+180
View File
@@ -0,0 +1,180 @@
<template>
<a-table
:rowKey="rowKey"
:columns="columns"
:dataSource="dataSource"
:expandedRowKeys="expandedRowKeys"
v-bind="tableAttrs"
v-on="$listeners"
@expand="handleExpand"
@expandedRowsChange="expandedRowKeys=$event">
<template v-for="(slotItem) of slots" :slot="slotItem" slot-scope="text, record, index">
<slot :name="slotItem" v-bind="{text,record,index}"></slot>
</template>
</a-table>
</template>
<script>
import { getAction } from '@/api/manage'
export default {
name: 'JTreeTable',
props: {
rowKey: {
type: String,
default: 'id'
},
// 根据什么查询,如果传递 id 就根据 id 查询
queryKey: {
type: String,
default: 'parentId'
},
queryParams: {
type: Object,
default: () => ({})
},
// 查询顶级时的值,如果顶级为0,则传0
topValue: {
type: String,
default: null
},
columns: {
type: Array,
required: true
},
url: {
type: String,
required: true
},
childrenUrl: {
type: String,
default: null
},
tableProps: {
type: Object,
default: () => ({})
},
/** 是否在创建组件的时候就查询数据 */
immediateRequest: {
type: Boolean,
default: true
},
condition: {
type: String,
default: '',
required: false
}
},
data () {
return {
dataSource: [],
expandedRowKeys: []
}
},
computed: {
getChildrenUrl () {
if (this.childrenUrl) {
return this.childrenUrl
} else {
return this.url
}
},
slots () {
const slots = []
for (const column of this.columns) {
if (column.scopedSlots && column.scopedSlots.customRender) {
slots.push(column.scopedSlots.customRender)
}
}
return slots
},
tableAttrs () {
return Object.assign(this.$attrs, this.tableProps)
}
},
watch: {
queryParams: {
deep: true,
handler () {
this.loadData()
}
}
},
created () {
if (this.immediateRequest) this.loadData()
},
methods: {
/** 加载数据 */
loadData (id = this.topValue, first = true, url = this.url) {
this.$emit('requestBefore', { first })
if (first) {
this.expandedRowKeys = []
}
const params = Object.assign({}, this.queryParams || {})
params[this.queryKey] = id
if (this.condition && this.condition.length > 0) {
params.condition = this.condition
}
return getAction(url, params).then(res => {
let list = []
if (res.result instanceof Array) {
list = res.result
} else if (res.result.records instanceof Array) {
list = res.result.records
} else {
throw new Error('返回数据类型不识别')
}
const dataSource = list.map(item => {
// 判断是否标记了带有子级
if (item.hasChildren === true) {
// 查找第一个带有dataIndex的值的列
let firstColumn
for (const column of this.columns) {
firstColumn = column.dataIndex
if (firstColumn) break
}
// 定义默认展开时显示的loading子级,实际子级数据只在展开时加载
const loadChild = { id: `${item.id}_loadChild`, [firstColumn]: 'loading...', isLoading: true }
item.children = [loadChild]
}
return item
})
if (first) {
this.dataSource = dataSource
}
this.$emit('requestSuccess', { first, dataSource, res })
return Promise.resolve(dataSource)
}).finally(() => this.$emit('requestFinally', { first }))
},
/** 点击展开图标时触发 */
handleExpand (expanded, record) {
// 判断是否是展开状态
if (expanded) {
// 判断子级的首个项的标记是否是“正在加载中”,如果是就加载数据
if (record.children[0].isLoading === true) {
this.loadData(record.id, false, this.getChildrenUrl).then(dataSource => {
// 处理好的数据可直接赋值给children
if (dataSource.length === 0) {
record.children = null
} else {
record.children = dataSource
}
})
}
}
}
}
}
</script>
<style scoped>
</style>
+402
View File
@@ -0,0 +1,402 @@
<template>
<div :id="containerId" style="position: relative">
<a-upload
name="file"
:multiple="multiple"
:action="uploadAction"
:headers="headers"
:data="{'biz':bizPath}"
:fileList="fileList"
:beforeUpload="doBeforeUpload"
@change="handleChange"
:disabled="disabled"
:returnUrl="returnUrl"
:listType="complistType"
@preview="handlePreview"
@download="handleDownload"
:showUploadList="{
showDownloadIcon: isDownload
}"
v-bind="$attrs"
v-on="childListeners"
:class="{'uploadty-disabled':disabled}">
<template>
<div v-if="isImageComp">
<a-icon type="plus" />
<div class="ant-upload-text">{{ text }}</div>
</div>
<a-button v-else-if="buttonVisible">
<a-icon type="upload" />
{{ text }}
</a-button>
</template>
</a-upload>
<div id="images">
<div class="image" v-viewer="{movable: false}">
<img v-show="image" :src="imageUrl">
</div>
</div>
<j-image-preview-modal ref="imagePreviewModal" />
</div>
</template>
<script>
import Vue from 'vue'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import { getFileAccessHttpUrl, downloadFile } from '@/api/manage'
import { previewPdf } from '@/utils/previewPdf'
import JImagePreviewModal from '@comp/jero/modal/JImagePreviewModal.vue'
const FILE_TYPE_ALL = 'all'
const FILE_TYPE_IMG = 'image'
const FILE_TYPE_IMGS = ['jpg', 'jpeg', 'png', 'raw']
const FILE_TYPE_PDF = 'pdf'
// const uidGenerator = () => {
// return '-' + parseInt(Math.random() * 10000 + 1 + '', 10)
// }
const Base64 = require('js-base64').Base64
// 支持预览的文件类型
const CAN_PREVIEW_FILE_TYPE = ['pdf', 'image']
// 不支持预览的文件后缀
const CAN_PREVIEW_FILE_SUFFIX = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'mp3', 'mp4', 'flv']
export default {
name: 'JUpload',
components: { JImagePreviewModal },
data () {
return {
uploadAction: window._CONFIG.domianURL + '/sys/common/upload',
headers: {},
fileList: [],
newFileList: [],
image: false,
uploadGoOn: true,
containerId: null,
imageUrl: null
}
},
props: {
text: {
type: String,
required: false,
default: '点击上传'
},
fileType: {
type: String,
required: false,
default: FILE_TYPE_ALL
},
/* 这个属性用于控制文件上传的业务路径 */
bizPath: {
type: String,
required: false,
default: 'temp'
},
value: {
type: [String, Array],
required: false
},
// update-begin- --- author:wangshuai ------ date:20190929 ---- for:Jupload组件增加是否能够点击
disabled: {
type: Boolean,
required: false,
default: false
},
// update-end- --- author:wangshuai ------ date:20190929 ---- for:Jupload组件增加是否能够点击
// 此属性被废弃了
triggerChange: {
type: Boolean,
required: false,
default: false
},
/**
* update -- author:lvdandan -- date:20190219 -- for:Jupload组件增加是否返回url
* true:仅返回url
* false:返回fileName filePath fileSize
*/
returnUrl: {
type: Boolean,
required: false,
default: true
},
number: {
type: Number,
required: false,
default: 0
},
buttonVisible: {
type: Boolean,
required: false,
default: true
},
multiple: {
type: Boolean,
default: true
},
beforeUpload: {
type: Function
},
isDownload: {
type: Boolean,
default: true
}
},
watch: {
value: {
immediate: true,
handler () {
const val = this.value
this.initFileList(val)
// if (val instanceof Array) {
// if (this.returnUrl) {
// this.initFileList(val.join(','))
// } else {
// this.initFileListArr(val)
// }
// } else {
// this.initFileList(val)
// }
}
}
},
computed: {
// 透传给下级组件的事件,需要排除本组件使用的change事件
childListeners () {
const result = Object.assign({},
this.$listeners
)
delete result.change
return result
},
isImageComp () {
return this.fileType === FILE_TYPE_IMG
},
complistType () {
return this.fileType === FILE_TYPE_IMG ? 'picture-card' : 'text'
}
},
created () {
const token = Vue.ls.get(ACCESS_TOKEN)
// ---------------------------- begin 图片左右换位置 -------------------------------------
this.headers = { 'X-Access-Token': token }
this.containerId = 'container-ty-' + new Date().getTime()
// ---------------------------- end 图片左右换位置 -------------------------------------
},
methods: {
// 将url的参数拆分成对象
urlToParams (url) {
const commonUrl = window._CONFIG.staticDomainURL
// url截取参数的部分
const paramsStr = url.slice(url.indexOf('?') + 1)
const paramsObj = {
url,
id: url.slice(url.indexOf(commonUrl) + commonUrl.length + 1, url.indexOf('?'))
}
paramsStr.split('&').forEach(item => {
const arr = item.split('=')
paramsObj[arr[0]] = arr[1]
})
return paramsObj
},
initFileList (val) {
if (!val || val.length === 0) {
this.fileList = []
return
}
// 所有文件url的数组
let arr = []
// 用于临时存储文件的数组,最终会被赋值到this.fileList
const fileList = []
if (val instanceof Array) {
// url数组直接返回,如果是对象数组,就将每个对象的url取出
arr = this.returnUrl ? val : val.map(item => item.filePath)
} else {
// 将字符串拆分数组(props声明value只能是Array或String)
arr = val.split(',')
}
arr.forEach(url => {
if (url) {
const params = this.urlToParams(url)
fileList.push({
uid: params.id,
name: params.fullfilename,
status: 'done',
url,
// response用于下载和预览
response: {
success: true,
result: {
id: params.id,
fileName: params.fullfilename
},
status: 'history'
}
})
}
})
// 将处理好的数据回显
this.fileList = fileList
},
handlePathChange () {
const uploadFiles = this.fileList
let path = ''
if (!uploadFiles || uploadFiles.length === 0) {
path = ''
}
const arr = []
for (let a = 0; a < uploadFiles.length; a++) {
if (uploadFiles[a].status === 'done') {
arr.push(uploadFiles[a].url)
} else {
return
}
}
if (arr.length > 0) {
path = arr.join(',')
}
this.$emit('change', path)
},
doBeforeUpload (file) {
this.uploadGoOn = true
const fileType = file.type
if (this.fileType === FILE_TYPE_IMG) {
if (fileType.indexOf('image') < 0) {
this.$message.warning('请上传图片')
this.uploadGoOn = false
return false
}
}
// 扩展 beforeUpload 验证
if (typeof this.beforeUpload === 'function') {
return this.beforeUpload(file)
}
return true
},
handleChange (info) {
if (!info.file.status && this.uploadGoOn === false) {
info.fileList.pop()
}
let fileList = info.fileList
if (info.file.status === 'done') {
if (this.number > 0) {
fileList = fileList.slice(-this.number)
}
if (info.file.response.success) {
fileList = fileList.map((file) => {
if (file.response) {
// const reUrl = `${file.response.result.id}?token=${Vue.ls.get(ACCESS_TOKEN)}&fullfilename=${file.response.result.fileName}`
// TODO getFileAccessHttpUrl方法会追加token,在之后拼参数
file.url = getFileAccessHttpUrl(file.response.result.id) + '&fullfilename=' + file.response.result.fileName
}
return file
})
} else {
this.$message.error(info.file.response.message)
}
// this.$message.success(`${info.file.name} 上传成功!`);
} else if (info.file.status === 'error') {
this.$message.error(`${info.file.name} 上传失败.`)
} else if (info.file.status === 'removed') {
this.handleDelete(info.file)
}
this.fileList = fileList
if (info.file.status === 'done' || info.file.status === 'removed') {
// returnUrl为true时仅返回文件路径
if (this.returnUrl) {
this.handlePathChange()
} else {
// returnUrl为false时返回文件名称、文件路径及文件大小
this.newFileList = []
for (let a = 0; a < fileList.length; a++) {
// update-begin-author:lvdandan date:20200603 for:【TESTA-514】【开源issue】多个文件同时上传时,控制台报错
if (fileList[a].status === 'done') {
const fileJson = {
fileName: fileList[a].name,
filePath: fileList[a].url,
fileSize: fileList[a].size
}
this.newFileList.push(fileJson)
} else {
return
}
// update-end-author:lvdandan date:20200603 for:【TESTA-514】【开源issue】多个文件同时上传时,控制台报错
}
this.$emit('change', this.newFileList)
}
}
},
handleDelete (file) {
// 如有需要新增 删除逻辑
console.log(file)
},
handlePreview (file) {
if (!file || !file.url) {
return
}
const fileType = file.type
// 截取文件后缀名
const fileSuffix = file.name ? file.name.split('.')[file.name.split('.').length - 1] : ''
const canPreview = fileType ? CAN_PREVIEW_FILE_TYPE.some(tt => fileType.indexOf(tt) !== -1) : CAN_PREVIEW_FILE_SUFFIX.some(tt => fileSuffix === tt)
// 判断是否为可预览格式的文件
if (!canPreview) {
this.$message.loading('该文件类型不支持预览,正在为您准备下载...').then(() => {
this.handleDownload(file)
})
return
}
const fileFullUrl = `${window._CONFIG.domianWebSocketURL}/sys/common/download/${file.response.result.id}?token=${Vue.ls.get(ACCESS_TOKEN)}&fullfilename=${file.name}`
// 图片预览,使用自己添加的组件
if (canPreview && FILE_TYPE_IMGS.includes(fileSuffix)) {
this.imageUrl = getFileAccessHttpUrl(file.response.result.id)
// 获取viewer实例
const viewer = this.$el.querySelector('.image').$viewer
// 调用show方法进行显示预览图
viewer.show()
// this.$refs.imagePreviewModal.open(file)
return
}
// pdf预览
if (canPreview && FILE_TYPE_PDF.includes(fileSuffix)) {
const url = previewPdf(file.response.result.id)
window.open(url)
return
}
// 其余可预览文件仍使用KKFile进行预览
const url = `${window._CONFIG.onlinePreviewDomainURL}?url=${encodeURIComponent(Base64.encode(fileFullUrl))}`
window.open(url)
},
handleDownload (file) {
// 下载文件
downloadFile(`/sys/common/download/${file.response.result.id}`, file.name)
}
},
mounted () {
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style lang="less">
.uploadty-disabled {
.ant-upload-list-item {
.anticon-close {
display: none;
}
.anticon-delete {
display: none;
}
}
}
</style>
@@ -0,0 +1,75 @@
<template>
<j-modal
title="详细信息"
:width="1200"
:visible="visible"
@ok="handleOk"
@cancel="close"
switch-fullscreen
:fullscreen.sync="fullscreen"
>
<transition name="fade">
<div v-if="visible">
<slot name="mainForm" :row="row" :column="column"/>
<slot name="subForm" :row="row" :column="column"/>
</div>
</transition>
</j-modal>
</template>
<script>
import { cloneObject } from '@/utils/util'
export default {
name: 'JVxeDetailsModal',
inject: ['superTrigger'],
data () {
return {
visible: false,
fullscreen: false,
row: null,
column: null
}
},
created () {
},
methods: {
open (event) {
const { row, column } = event
this.row = cloneObject(row)
this.column = column
this.visible = true
},
close () {
this.visible = false
},
handleOk () {
this.superTrigger('detailsConfirm', {
row: this.row,
column: this.column,
callback: (success) => {
this.visible = !success
}
})
}
}
}
</script>
<style lang="less">
.fade-enter-active,
.fade-leave-active {
opacity: 1;
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

Some files were not shown because too many files have changed in this diff Show More