Initial commit

This commit is contained in:
danshihao
2024-06-11 15:39:01 +08:00
commit 00acf31aa3
1589 changed files with 520803 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
+198
View File
@@ -0,0 +1,198 @@
<template>
<div>
<template v-if="fileList && fileList.length > 0">
<div class="file" :class="{'operate-right': operateFixedRight}" v-for="file in fileList" :key="file.id">
<div class="file-info" :class="{'can-download-file-info': canDownload}">
<a-icon type="link" class="file-info-icon" />
<div class="file-name" @click="handlePreview(file)">{{ file.fileName }}</div>
</div>
<div class="file-operate">
<!-- 下载-->
<a-button type="link" icon="download" @click="handleDownload(file)" v-if="canDownload">{{ $t('download') }}</a-button>
</div>
</div>
</template>
<div id="images">
<div class="image" v-viewer="{movable: false}">
<img v-show="image" :src="imageUrl" alt="">
</div>
</div>
</div>
</template>
<script>
import { getFileInfo } from '../api/api.js'
import Vue from 'vue'
import { ACCESS_TOKEN } from '../store/mutation-types.js'
import { downloadFile, getFileAccessHttpUrl } from '../api/manage.js'
import { previewPdf } from '../utils/previewPdf.js'
import { Base64 } from 'js-base64'
import { kkFilePreview } from '../utils/kkFilePreview'
// 支持预览的文件类型
const CAN_PREVIEW_FILE_TYPE = ['pdf', 'image']
// 支持预览的文件后缀
const CAN_PREVIEW_FILE_SUFFIX = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
const FILE_TYPE_IMGS = ['jpg', 'jpeg', 'png', 'raw']
const FILE_TYPE_PDF = 'pdf'
export default {
name: 'FileEcho',
props: {
// 文件id
fileIds: {
type: String,
required: false,
default: null
},
// 操作按钮固定在右侧
operateFixedRight: {
type: Boolean,
required: false,
default: true
},
// 是否可以下载
canDownload: {
type: Boolean,
required: false,
default: true
}
},
watch: {
fileIds: {
handler (value) {
if (value) {
this.getFileList()
}
},
immediate: true,
deep: true
}
},
data () {
return {
fileList: [],
image: false,
imageUrl: null
}
},
methods: {
getFileList () {
const fileIdsList = this.fileIds.split(',')
for (let i = 0; i < fileIdsList.length; i++) {
const id = fileIdsList[i]
getFileInfo({ id }).then(res => {
if (res.success) {
this.fileList.push(res.result)
}
})
}
},
handlePreview (file) {
if (!file || !file.url) {
return
}
// 截取文件后缀名
const fileSuffix = file.fileName ? file.fileName.split('.')[file.fileName.split('.').length - 1] : ''
const canPreview = CAN_PREVIEW_FILE_SUFFIX.some(tt => fileSuffix.toLowerCase() === tt)
// 判断是否为可预览格式的文件
if (!canPreview) {
this.$message.loading('该文件类型不支持预览,正在为您准备下载...').then(() => {
this.handleDownload(file)
})
return
}
const fileFullUrl = `${window._CONFIG.domianWebSocketURL}/sys/common/view/${file.id}?at=${Vue.ls.get(ACCESS_TOKEN)}&fullfilename=${file.fileName}`
// 图片预览,使用自己添加的组件
if (canPreview && FILE_TYPE_IMGS.includes(fileSuffix.toLowerCase())) {
this.imageUrl = getFileAccessHttpUrl(file.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.toLowerCase())) {
const url = previewPdf(file.id)
window.open(url)
return
}
// 其余可预览文件仍使用KKFile进行预览
kkFilePreview(fileFullUrl)
},
/**
* 单个文件的下载功能
* @param file
*/
handleDownload (file) {
// 下载文件
downloadFile(`/sys/common/download/${file.id}`, file.fileName)
}
}
}
</script>
<style scoped lang="less">
.operate-right {
justify-content: space-between;
.file-info {
flex: 1;
width: 0;
&-icon {
cursor: default;
}
}
.file-name {
flex: 1;
width: 0;
}
}
.file {
display: flex;
align-items: center;
width: 100%;
}
.file:last-child {
margin-bottom: 0;
}
.file-info {
display: flex;
align-items: center;
color: @primary-color;
cursor: pointer;
max-width: 100%;
}
.file-info > .anticon {
margin-right: 8px;
}
.can-download-file-info {
max-width: calc(100% - 97px);
}
.file-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-operate {
.ant-btn > .anticon + span, .ant-btn > .iconfont + span {
margin-left: 10px;
}
.ant-btn:first-child {
margin-left: 15px;
}
}
</style>
+360
View File
@@ -0,0 +1,360 @@
<template>
<div :id="domId" class="chart-dom"></div>
</template>
<script>
import insertCss from 'insert-css'
import G6 from '@antv/g6'
import DeleteIcon from '../assets/image/iconDelete.svg'
// 流程图用到的属性
const ChartAttrs = {
nodeStrokeColor: '#739FC8', // 节点边框颜色
nodeFillColor: '#EEF4F8', // 节点背景颜色
nodeTextColor: '#227CCF', // 节点文字颜色
nodeWidth: 210, // 节点宽度
nodeHeight: 40, // 节点高度
ellipseLength: 12, // 文本显示最大长度
returnLineSpacing: 30, // 文本左右绕的加宽
edgeColor: '#86909c', // 线的颜色
edgeWidth: 1,
btnColor: '#1f1f1f', // 按钮颜色
btnSize: 20, // 按钮的宽高
btnPadding: 2 // 按钮边框和内部图标的距离
}
// 添加图标
const AddIcon = function EXPAND_ICON (x, y, r) {
return [['M', x + 2, y], ['L', x + 2 * r - 2, y], ['M', x + r, y - r + 2], ['L', x + r, y + r - 2]]
}
// 处理图的tooltip样式
insertCss(`
.g6-tooltip {
border-radius: 6px;
font-size: 12px;
color: #fff;
background-color: #000;
padding: 2px 8px;
text-align: center;
}
`)
export default {
name: 'FlowChart',
props: {
// 元素的id
domId: {
type: String,
required: true,
default: ''
},
// 流程图用到的数据
chartData: {
type: Object,
required: false,
default: () => {
return {}
}
},
// 是否仅查看
isViewOnly: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
isLoadEnd: false, // 页面是否加载完成
graph: null,
observer: null // 图形容器大小变化监听器
}
},
watch: {
chartData: {
handler () {
if (this.isLoadEnd) {
if (this.graph) {
this.graph.destroy()
this.graph = null
this.initChart()
} else {
this.initChart()
}
}
},
deep: true,
immediate: true
},
isLoadEnd () {
if (this.isLoadEnd && Object.keys(this.chartData).length > 0) {
this.initChart()
}
}
},
mounted () {
this.$nextTick(() => {
this.isLoadEnd = true
const dom = document.getElementById(this.domId)
this.observer = new ResizeObserver(this.handleResize)
this.observer.observe(dom, { box: 'border-box' })
})
},
methods: {
initChart () {
const canOperate = !this.isViewOnly
// 自定义节点
G6.registerNode('default-node', {
drawShape (cfg, group) {
const rect = group.addShape('rect', {
attrs: {
x: -(ChartAttrs.nodeWidth / 2),
y: -(ChartAttrs.nodeHeight / 2),
width: ChartAttrs.nodeWidth,
height: ChartAttrs.nodeHeight,
radius: ChartAttrs.nodeHeight / 2,
fill: ChartAttrs.nodeFillColor,
stroke: ChartAttrs.nodeStrokeColor
},
name: 'default-node-box'
})
// 处理文本显示及文本超长
if (cfg.name) {
const text = cfg.name.length > ChartAttrs.ellipseLength ? cfg.name.substring(0, ChartAttrs.ellipseLength) + '...' : cfg.name
group.addShape('text', {
attrs: {
text,
fill: ChartAttrs.nodeTextColor,
fontSize: 14,
textAlign: 'center',
textBaseline: 'middle',
fontWeight: 'bold'
},
name: 'default-node-text'
})
}
// 可以添加,添加虽然在线上,但代码要写在自定义点里,不然获取不到节点id
if (cfg.isApprovalNode === 1 && canOperate) {
group.addShape('rect', {
attrs: {
x: -(ChartAttrs.btnSize / 2),
y: 1.5 * ChartAttrs.btnSize,
width: ChartAttrs.btnSize,
height: ChartAttrs.btnSize,
radius: 4,
symbol: AddIcon,
fill: 'white',
cursor: 'pointer',
stroke: ChartAttrs.btnColor,
lineWidth: 1
},
name: 'add-btn'
})
group.addShape('marker', {
attrs: {
x: -(ChartAttrs.btnSize / 2) + ChartAttrs.btnPadding,
y: 2 * ChartAttrs.btnSize,
r: (ChartAttrs.btnSize / 2) - ChartAttrs.btnPadding,
symbol: AddIcon,
stroke: ChartAttrs.btnColor,
fill: 'white',
cursor: 'pointer',
lineWidth: 1
},
name: 'add-btn-icon'
})
}
// 可以删除
if (cfg.isCanDeleted === 1 && canOperate) {
group.addShape('rect', {
attrs: {
x: ChartAttrs.nodeWidth / 2 + ChartAttrs.btnSize / 2,
y: -ChartAttrs.btnSize / 2,
width: ChartAttrs.btnSize,
height: ChartAttrs.btnSize,
radius: 4,
symbol: AddIcon,
fill: 'white',
cursor: 'pointer',
stroke: ChartAttrs.btnColor
},
name: 'delete-btn'
})
group.addShape('image', {
attrs: {
x: ChartAttrs.nodeWidth / 2 + ChartAttrs.btnSize / 2 + ChartAttrs.btnPadding,
y: -ChartAttrs.btnSize / 2 + ChartAttrs.btnPadding,
img: DeleteIcon,
width: ChartAttrs.btnSize - ChartAttrs.btnPadding * 2,
height: ChartAttrs.btnSize - ChartAttrs.btnPadding * 2
},
name: 'delete-btn-icon'
})
}
return rect
},
// 设置锚点,上下中心
getAnchorPoints () {
return [
[0.5, 0],
[0.5, 1]
]
}
}, 'single-node')
// 自定义边
G6.registerEdge('default-edge', {
// 处理线条
draw (cfg, group) {
const startPoint = cfg.startPoint
const endPoint = cfg.endPoint
let path = []
// 如果起始点在终点的右下方
if (startPoint.y > endPoint.y && startPoint.x >= endPoint.x) {
path = [
['M', startPoint.x, startPoint.y],
['L', startPoint.x, startPoint.y + ChartAttrs.nodeHeight], // 从下面出来
['L', startPoint.x + ChartAttrs.nodeWidth / 2 + ChartAttrs.returnLineSpacing, startPoint.y + ChartAttrs.nodeHeight], // 右侧绕一下
['L', startPoint.x + ChartAttrs.nodeWidth / 2 + ChartAttrs.returnLineSpacing, endPoint.y - ChartAttrs.nodeHeight], // 右侧绕一下
['L', endPoint.x, endPoint.y - ChartAttrs.nodeHeight], // 到入点的上方
['L', endPoint.x, endPoint.y]
]
} else if (startPoint.y > endPoint.y && startPoint.x < endPoint.x) { // 起始点在终点的左下方
path = [
['M', startPoint.x, startPoint.y],
['L', startPoint.x, startPoint.y + ChartAttrs.nodeHeight], // 从下面出来
['L', startPoint.x - ChartAttrs.nodeWidth - ChartAttrs.returnLineSpacing, startPoint.y + ChartAttrs.nodeHeight], // 左侧绕一下
['L', startPoint.x - ChartAttrs.nodeWidth - ChartAttrs.returnLineSpacing, endPoint.y - ChartAttrs.nodeHeight], // 右侧绕一下
['L', endPoint.x, endPoint.y - ChartAttrs.nodeHeight], // 到入点的上方
['L', endPoint.x, endPoint.y]
]
} else {
path = [
['M', startPoint.x, startPoint.y],
['L', startPoint.x, (endPoint.y - startPoint.y) / 2 + startPoint.y],
['L', endPoint.x, (endPoint.y - startPoint.y) / 2 + startPoint.y],
['L', endPoint.x, endPoint.y]
]
}
const shape = group.addShape('path', {
attrs: {
path,
stroke: ChartAttrs.edgeColor,
lineWidth: ChartAttrs.edgeWidth,
endArrow: true
},
className: 'edge-shape',
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
name: 'edge-shape'
})
// 处理label
if (cfg.label && cfg.label.length > 0) {
console.log(cfg.label)
group.addShape('text', {
attrs: {
text: cfg.label,
textAlign: 'center',
x: endPoint.x,
y: (endPoint.y - startPoint.y) / 2 + startPoint.y,
fill: 'black'
},
name: 'edge-label'
})
}
return shape
}
})
this.graph = new G6.Graph({
container: this.domId,
width: document.getElementById(this.domId).clientWidth,
height: document.getElementById(this.domId).clientHeight,
fitView: true,
layout: {
type: 'dagre',
nodesep: 70,
ranksep: 30,
controlPoints: true,
preventOverlap: true // 防止节点重叠
},
defaultNode: {
type: 'default-node',
sortByCombo: true,
size: [210, 40]
},
defaultEdge: {
type: 'default-edge' // 在数据中已经指定 type,这里无需再次指定
},
modes: {
default: [
'drag-canvas',
'zoom-canvas',
{
type: 'tooltip',
formatText (model) {
return model.name
},
offset: 10
}
]
}
})
this.graph.data(this.chartData)
this.graph.render()
console.log(document.getElementById(this.domId).clientWidth,
document.getElementById(this.domId).clientHeight)
this.graph.on('node:click', event => {
this.handleNodeClick(event)
})
},
/**
* 节点点击事件
* @param target
* @param item
*/
handleNodeClick ({ target, item }) {
// 处理新增
if (target.cfg.name === 'add-btn' || target.cfg.name === 'add-btn-icon') {
this.handleAdd(item)
return
}
// 处理删除
if (target.cfg.name === 'delete-btn' || target.cfg.name === 'delete-btn-icon') {
this.handleDelete(item)
}
},
handleAdd (item) {
this.$emit('add', item)
},
handleDelete (item) {
this.$emit('delete', item)
},
/**
* 处理元素大小变化
*/
handleResize () {
if (!this.graph || this.graph.get('destroyed')) {
return
}
this.graph.changeSize(document.getElementById(this.domId).clientWidth, document.getElementById(this.domId).clientHeight)
this.graph.fitCenter()
}
},
beforeDestroy () {
this.observer.disconnect()
}
}
</script>
<style scoped lang="less">
.chart-dom {
position: relative;
}
/deep/ .g6-tooltip {
max-width: 200px;
padding: 8px 10px;
}
</style>
+69
View File
@@ -0,0 +1,69 @@
<template>
<j-modal
:title="$t('workCenter.processDetail.flowChart')"
:width="width"
:visible="visible"
switchFullscreen
:maskClosable="false"
:confirmLoading="confirmLoading"
:footer="null"
@cancel="close">
<a-spin :spinning="confirmLoading">
<flow-chart dom-id="flowChart" :chart-data="chartData" is-view-only />
</a-spin>
</j-modal>
</template>
<script>
import FlowChart from './FlowChart.vue'
import { getFlowNodeList } from '../api/workCenter.js'
export default {
name: 'FlowChartModal',
components: { FlowChart },
data () {
return {
width: 1000,
visible: false,
confirmLoading: false,
chartData: {},
processKey: null // 流程key
}
},
methods: {
open () {
this.visible = true
this.initChartData()
},
close () {
this.visible = false
},
initChartData () {
this.confirmLoading = true
getFlowNodeList({ key: this.processKey }).then(res => {
if (res.success) {
this.chartData = Object.assign({}, res.result || {})
} else {
this.$message.warning(res.message)
}
}).finally(() => {
this.confirmLoading = false
})
}
}
}
</script>
<style scoped lang="less">
#flowChart {
width: 100%;
height: calc(100vh - 200px - 55px - 48px);
}
.fullscreen {
/deep/ #flowChart {
height: calc(100vh - 55px - 48px);
}
}
</style>
+121
View File
@@ -0,0 +1,121 @@
<template>
<a-modal
:title="$t('import')"
:maskClosable="false"
:width="600"
:closable="true"
:confirm-loading="confirmLoading"
@ok="handleOk"
@cancel="handleCancel"
:visible="visible">
<a-form layout="inline">
<a-form-item :label="$t('importWithTemplate.chooseFile')">
<div class="form-content">
<a-input v-model="fileName" />
<j-upload type="primary" :number="1" @change="handleFileChange" :show-upload-list="false" :return-url="false" v-bind="$attrs">
<template v-slot:customButton>
<a-button type="primary">{{ $t('importWithTemplate.chooseFile') }}</a-button>
</template>
</j-upload>
</div>
<a-button type="link" class="down-template-btn" @click="downTemplate">{{ $t('importWithTemplate.downTemplate') }}</a-button>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
import '@assets/less/common.less'
import { urlToParams } from '../utils/util'
import { downloadFile } from '../api/manage'
export default {
name: 'ImportWithTemplate',
props: {
// 下载模板的地址
downTemplateUrl: {
type: String,
required: false,
default: null
},
// 导入模板文件名
templateName: {
type: String,
required: false,
default: null
}
},
data () {
return {
visible: false,
confirmLoading: false,
fileId: null,
fileName: null
}
},
methods: {
open () {
this.visible = true
},
handleOk () {
if (this.fileId) {
this.$emit('ok', this.fileId)
this.close()
} else {
this.$message.warn(this.$t('importWithTemplate.noFileMsg'))
}
},
handleCancel () {
this.close()
},
close () {
this.visible = false
this.fileId = null
this.fileName = null
},
handleFileChange (fileList) {
this.fileName = fileList[0].fileName
const file = urlToParams(fileList[0].filePath)
this.fileId = file.id
},
// 下载导入模板
downTemplate () {
if (!this.downTemplateUrl) {
this.$message.warn('请设置downTemplateUrl属性')
return
}
const fileName = this.templateName || '导入模板'
downloadFile(this.downTemplateUrl, fileName + '.xlsx')
}
}
}
</script>
<style scoped lang="less">
.form-content {
display: flex;
margin-top: 5px;
.ant-input {
pointer-events: none;
}
.ant-btn {
margin-left: 16px;
}
}
.ant-form-inline .ant-form-item {
width: 100%;
display: flex;
/deep/ .ant-form-item-control-wrapper {
flex: 1;
width: 0;
}
}
/deep/ .ant-form-item-control {
line-height: 32px;
}
</style>
+135
View File
@@ -0,0 +1,135 @@
<template>
<a-spin :spinning="loading">
<div class="detail-container">
<div class="page-title">
<slot name="customIcon" v-if="customIcon" />
<i class="iconfont icon-chexiao" v-else @click="handleBack" />
<div class="page-title-text">
{{ title }}
<a-tooltip v-if="subTitleTooltip">
<template slot="title">{{ subTitle }}</template>
<div class="page-title-sub-title">{{ subTitle }}</div>
</a-tooltip>
<div class="page-title-sub-title" v-else>{{ subTitle }}</div>
</div>
<div class="custom-operate">
<slot name="titleRightCustom"></slot>
</div>
</div>
<div class="detail-content" :style="{padding: contentPadding + 'px'}">
<slot></slot>
</div>
</div>
</a-spin>
</template>
<script>
export default {
name: 'InternalDetailPage',
props: {
// 页面标题
title: {
type: String,
required: false,
default: null
},
// 是否需要自定义返回方法
needCustomBackFunc: {
type: Boolean,
required: false,
default: false
},
// 内容的内边距,默认是24
contentPadding: {
type: Number,
required: false,
default: 24
},
loading: {
type: Boolean,
required: false,
default: false
},
// 自定义图标
customIcon: {
type: Boolean,
required: false,
default: false
},
// 副标题
subTitle: {
type: String,
required: false,
default: null
},
// 副标题是否需要tooltip
subTitleTooltip: {
type: Boolean,
required: false,
default: false
}
},
methods: {
handleBack () {
if (this.needCustomBackFunc) {
this.$emit('back')
return
}
this.$router.go(-1)
}
}
}
</script>
<style scoped lang="less">
@import "~@/assets/less/common";
.detail-container {
height: @page-content-h;
background-color: white;
}
.page-title {
height: 56px;
border-bottom: 1px solid #E5E6EB;
padding: 0 24px;
display: flex;
align-items: center;
.iconfont {
font-size: 24px;
color: @primary-color;
cursor: pointer;
margin-right: 12px;
}
&-text {
font-size: 16px;
font-family: PingFang SC-Medium, PingFang SC, sans-serif;
font-weight: bold;
color: #1D2129;
flex: 1;
width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
}
&-sub-title {
font-size: 16px;
font-family: PingFang SC-Regular, PingFang SC, sans-serif;
font-weight: 400;
color: #4E5969;
margin-left: 12px;
display: inline-block;
}
}
.detail-content {
height: calc(100% - 56px);
overflow: auto;
}
</style>
+379
View File
@@ -0,0 +1,379 @@
<template>
<a-table
v-bind="$attrs"
v-on="$listeners"
:row-key="rowKey"
:bordered="bordered"
:scroll="{x: '100%'}"
:row-class-name="lineCanExpand"
:columns="columns"
:expandedRowKeys.sync="expandedRowKeys"
:pagination="ipagination"
:data-source="dataSource">
<!-- 如果插槽是作用域插槽将作用域插槽的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" />
<!-- 文本悬浮插槽 -->
<template slot="text" slot-scope="text">
<a-tooltip overlay-class-name="tooltip-style">
<template slot="title">{{ text || text === 0 ? text : global.emptyLine }}</template>
<div class="table-text">{{ text || text === 0 ? text : global.emptyLine }}</div>
</a-tooltip>
</template>
<!-- 表格扩展插槽 -->
<div slot="expandedRowRender" slot-scope="record, index" style="margin: 0">
<!-- 如果表单会依赖某个字段变化就需要加判断循环判断extraForm中的表单 -->
<template v-if="formIsDepend">
<template v-for="(value, key) in extraForm">
<template v-if="record[formIsDepend] === key">
<a-form-model :key="key" :model="record" :rules="value.rules" :ref="`ruleForm${record[rowKey]}`">
<a-form-model-item v-for="form in value.formArr"
:key="form.dbFieldName"
:labelCol="labelCol"
:wrapperCol="wrapperCol"
:label="form.dbFieldTxt"
:prop="form.dbFieldName">
<template v-if="form.fieldShowType === 1">
<a-input
type="textarea"
:maxLength="500"
:placeholder="$t('pleaseEnter')+form.dbFieldTxt"
:disabled="allDisabled || form.isDisabled || (form.dynamicIsDisabled && form.dynamicIsDisabled(record)) || false"
v-model="record[form.dbFieldName]" />
</template>
<template v-else-if="form.fieldShowType === 2">
<j-upload
return-id
multiple
v-model="record[form.dbFieldName]"
:disabled="allDisabled || form.isDisabled || (form.dynamicIsDisabled && form.dynamicIsDisabled(record)) || false"
:file-type="form.fileType || canUploadType">
</j-upload>
</template>
<template v-else-if="form.fieldShowType === 3">
<a-date-picker
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
:disabled="allDisabled || form.isDisabled || (form.dynamicIsDisabled && form.dynamicIsDisabled(record)) || false"
:disabled-date="form.disabledDate || undefined"
v-model="record[form.dbFieldName]">
</a-date-picker>
</template>
<template v-else-if="form.fieldShowType === 4">
<a-input
:maxLength="50"
:placeholder="$t('pleaseEnter')+form.dbFieldTxt"
:disabled="allDisabled || form.isDisabled || (form.dynamicIsDisabled && form.dynamicIsDisabled(record)) || false"
v-model="record[form.dbFieldName]" />
</template>
</a-form-model-item>
</a-form-model>
</template>
</template>
</template>
<!-- 没有依赖的字段就直接把传入的extraForm数组渲染成表单 -->
<template v-else>
<a-form-model :model="record" :rules="extraForm.rules" :ref="`ruleForm${index}`">
<a-form-model-item v-for="form in extraForm.formArr"
:key="form.dbFieldName"
:labelCol="labelCol"
:wrapperCol="wrapperCol"
:label="form.dbFieldTxt"
:prop="form.dbFieldName">
<template v-if="form.fieldShowType === 1">
<a-input
type="textarea"
:maxLength="500"
:disabled="allDisabled || form.isDisabled || (form.dynamicIsDisabled && form.dynamicIsDisabled(record)) || false"
:placeholder="$t('pleaseEnter')+form.dbFieldTxt"
v-model="record[form.dbFieldName]" />
</template>
<template v-else-if="form.fieldShowType === 2">
<j-upload
return-id
multiple
v-model="record[form.dbFieldName]"
:disabled="allDisabled || form.isDisabled || (form.dynamicIsDisabled && form.dynamicIsDisabled(record)) || false"
:file-type="form.fileType || canUploadType">
</j-upload>
</template>
<template v-else-if="form.fieldShowType === 3">
<a-date-picker
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
:disabled="allDisabled || form.isDisabled || (form.dynamicIsDisabled && form.dynamicIsDisabled(record)) || false"
:disabled-date="form.disabledDate || undefined"
v-model="record[form.dbFieldName]">
</a-date-picker>
</template>
<template v-else-if="form.fieldShowType === 4">
<a-input
:maxLength="50"
:placeholder="$t('pleaseEnter')+form.dbFieldTxt"
:disabled="allDisabled || form.isDisabled || (form.dynamicIsDisabled && form.dynamicIsDisabled(record)) || false"
v-model="record[form.dbFieldName]" />
</template>
</a-form-model-item>
</a-form-model>
</template>
</div>
</a-table>
</template>
<!--
使用方法:
html:
<j-expand-table :formIsDepend="formIsDepend" :extraForm="extraForm"></j-expand-table>
1如果扩展中的表单会根据某个字段变化就传formIsDepend=随之变化的字段名extraForm就传
extraForm: {
1: { // 1表示formIsDepend对应的字段值是1
rules: { // formIsDepend对应的字段值是1时的表单验证
desc: { required: true, message: '请输入依据描述', trigger: 'blur' },
...
},
formArr: [// formIsDepend对应的字段值是1时的表单数据
{
fieldShowType: 1,
dbFieldTxt: this.$t(''), // 标签文本
dbFieldName: 'desc', // dataSource中对应的字段名
isDisabled: true, // false的话可以不传
dynamicIsDisabled: (record) => {return true} // 可以通过行数据动态设置是否可以编辑
},
...
]
},
},
2如果扩展中的表单是固定的extraForm就传
extraForm: {
rules: {
desc: { required: false, message: '请输入依据描述', trigger: 'blur' },
...
},
formArr: [
{
fieldShowType: 1,
dbFieldTxt: this.$t(''), // 标签文本
dbFieldName: 'desc', // dataSource中对应的字段名
isDisabled: true // false的话可以不传
},
...
]
}
3提交时调用该组件中的submit方法获取返回值
返回值为false说明没有通过表单校验为true说明通过了校验直接通过绑定的dataSource获取数据即可示例如下
// 获取表格扩展部分的校验结果
const result = this.$refs.jExpandTable.submit()
if (result) {
// 每个表单校验都通过了
console.log('通过了', this.dataSource)
} else {
// 有表单没有通过
this.$message.warning('请输入必填项')
}
-->
<script>
import JUpload from './jero/JUpload'
export default {
name: 'JExpandTable',
components: { JUpload },
props: {
columns: {
type: Array,
default: () => {
return []
}
},
dataSource: {
type: Array,
default: () => {
return []
}
},
bordered: {
type: Boolean,
default: false
},
/**
* 扩展区表单, 表单需要依赖某个字段判断的就用对象,key是字段对应值,value是字段值对应下的表单数组
* 表单数组格式如下:
* [
* {
* fieldShowType: // 字段类型: 1,多行文本;2, 上传文件; 3, 日期选择; 4, 单行文本,
* dbFieldTxt: // 标签文本,例如:依据描述,
* dbFieldName: // 字段名
* isDisabled: // 是否可编辑
* }
* ]
*/
extraForm: {
type: Object,
default: () => {
return {}
}
},
/**
* 表单是否有依赖,传false或者依赖字段名
*/
formIsDepend: {
type: [Boolean, String],
default: false
},
// 全部禁用
allDisabled: {
type: Boolean,
required: false,
default: false
},
// 控制该行是否可展开的字段
lineCanExpandField: {
type: String,
required: false,
default: null
},
// 控制该行是否可以展开的字段值
lineCanExpandValue: {
type: [String, Number],
required: false,
default: null
},
// 表格行数据的主键
rowKey: {
type: String,
required: false,
default: 'id'
},
// 分页参数
pagination: {
type: [Boolean, Object],
required: false,
default: false
}
},
watch: {
pagination: {
handler (val) {
this.ipagination = Object.assign({}, val)
},
deep: true,
immediate: true
}
},
data () {
return {
labelCol: {
sm: 4
},
wrapperCol: {
sm: 16
},
// 本系统限制可以上传的文件格式
canUploadType: 'pdf,docx,doc,xlsx,xls,ppt,pptx,rar,zip,jpg,jpeg,png,avi,wmv,mov,rm,mp4,cad',
ipagination: {},
expandedRowKeys: []
}
},
methods: {
submit () {
console.log(this.rowKey)
const arr = []
for (const i in this.dataSource) {
if (this.$refs[`ruleForm${i}`] && this.$refs[`ruleForm${i}`][0]) {
this.$refs[`ruleForm${i}`][0].validate(valid => {
arr[i] = { valid, rowKey: this.dataSource[i][this.rowKey], index: i }
})
} else {
// 表单都没有渲染,说明没有展开过,也就是未填状态
if (this.formIsDepend) {
if (this.dataSource[i][this.formIsDepend]) {
// 需要依赖某个字段,获取当前行该字段时,扩展表单是否有必填项
const currRowExtraFormFormArr = this.extraForm[this.dataSource[i][this.formIsDepend]].formArr
const currRowExtraFormRules = this.extraForm[this.dataSource[i][this.formIsDepend]].rules
let isHaveRequired = false
for (const item of currRowExtraFormFormArr) {
if (currRowExtraFormRules[item.dbFieldName].required === true && !this.dataSource[i][item.dbFieldName]) {
// 有必填但没有填内容
isHaveRequired = true
}
}
if (isHaveRequired) {
// 任意一个字段有必填,她又没展开过,校验不通过
arr[i] = { valid: false, rowKey: this.dataSource[i][this.rowKey], index: i }
} else {
arr[i] = { valid: true, rowKey: this.dataSource[i][this.rowKey], index: i }
}
} else if (this.lineCanExpandField && this.dataSource[i][this.lineCanExpandField] !== this.lineCanExpandValue) {
// 如果这行数据本身不需要填写
arr[i] = { valid: true, rowKey: this.dataSource[i][this.rowKey], index: i }
} else {
// 依赖的字段都没有填
arr[i] = { valid: false, rowKey: this.dataSource[i][this.rowKey], index: i }
}
} else {
// 没有依赖的字段
let isHaveRequired = false
for (const item of this.extraForm.formArr) {
if (this.extraForm.rules[item.dbFieldName].required === true && !this.dataSource[i][item.dbFieldName]) {
// 有字段有必填,但没有内容
isHaveRequired = true
}
}
if (isHaveRequired) {
// 任意一个字段有必填,她又没展开过,校验不通过
arr[i] = { valid: false, rowKey: this.dataSource[i][this.rowKey], index: i }
} else {
arr[i] = { valid: true, rowKey: this.dataSource[i][this.rowKey], index: i }
}
}
}
}
if (arr.some(tt => !tt.valid)) {
const errorRows = arr.filter(tt => !tt.valid)
const errorIndex = errorRows.map(tt => tt.index)
const firstIndex = errorRows[0].index
const firstPage = parseInt((Number(firstIndex) + 1) / this.ipagination.pageSize) + 1
this.ipagination.current = firstPage
this.$nextTick(() => {
this.expandedRowKeys = [...this.expandedRowKeys, ...errorRows.map(tt => tt.rowKey)]
let timer = setTimeout(() => {
this.$nextTick(() => {
errorIndex.forEach(index => {
if (this.$refs[`ruleForm${index}`] && this.$refs[`ruleForm${index}`][0]) {
this.$refs[`ruleForm${index}`][0].validate()
}
})
})
clearTimeout(timer)
timer = null
}, 100)
})
return false
} else {
return true
}
},
lineCanExpand (record) {
console.log(this.lineCanExpandField, typeof this.lineCanExpandValue, record[this.lineCanExpandField] === this.lineCanExpandValue)
if (!this.lineCanExpandField || (!this.lineCanExpandValue && this.lineCanExpandValue !== 0 && this.lineCanExpandValue !== false)) {
return 'can-expend'
}
if (record[this.lineCanExpandField] === this.lineCanExpandValue) {
return 'can-expend'
}
return 'cannot-expend'
}
}
}
</script>
<style scoped lang="less">
/deep/ .cannot-expend {
.ant-table-row-expand-icon {
display: none;
}
}
</style>
+220
View File
@@ -0,0 +1,220 @@
<template>
<a-date-picker
:id="fieldName"
:show-time="false"
:open="open"
:ref="fieldName"
:dropdownClassName="'j-multiple-date-picker ' + fieldName + '-drop'"
@openChange="handleOpenChange"
@change="handleChange"
style="width: 100%"
:value="firstValue"
v-bind="$attrs"
>
<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: ''
},
fieldName: {
type: String,
default: ''
}
},
watch: {
value (val) {
console.log(val, '----watch--value-------')
if (!val) {
this.checkedValue = []
this.firstValue = null
document.getElementById(this.fieldName).children[0].children[0].value = ''
this.$emit('change', '')
} else {
this.checkedValue = val.split(',')
this.firstValue = this.checkedValue[0]
this.$nextTick(() => {
document.getElementById(this.fieldName).children[0].children[0].value = val
})
}
}
},
data () {
return {
open: false,
checkedValue: [],
firstValue: null // 多选中第一个日期,用于触发显示删除按钮
}
},
methods: {
// 面板头部切换日期的按钮部分点击事件,切换时会刷新dom,这里每次把刷新后的dom给改回多日期显示
calendarHeaderFn () {
this.firstValue = this.checkedValue[0]
this.$nextTick(() => {
document.getElementsByClassName(this.fieldName + '-drop')[0].children[0].children[0].children[0].children[0].children[0].value = this.checkedValue.join(',')
})
},
handleOpenChange (open) {
console.log(open)
if (open) {
this.open = open
this.$nextTick(() => {
// 为了固定弹出层,把弹出层dom元素移动
const drop = document.getElementsByClassName(this.fieldName + '-drop')[0]
const picker = document.getElementById(this.fieldName)
// 创建第二层外层元素
console.log(drop)
if (drop) {
const outerWrapperTwo = document.createElement('div')
outerWrapperTwo.appendChild(drop)
// 创建新的外层元素
const outerWrapper = document.createElement('div')
outerWrapper.style.position = 'absolute'
outerWrapper.style.top = '0px'
outerWrapper.style.left = '0px'
outerWrapper.style.width = '100%'
outerWrapper.appendChild(outerWrapperTwo)
picker.appendChild(outerWrapper)
}
setTimeout(() => {
// 日期面板头部点击事件(头部切换日期会重新渲染dom,这里绑定事件,头部切换的时候就把值再改回多日期)
console.log(`#${this.fieldName} .ant-calendar-header`, document.querySelector(`#${this.fieldName} .ant-calendar-header`))
document.querySelector(`#${this.fieldName} .ant-calendar-header`).addEventListener('click', this.calendarHeaderFn)
document.getElementsByClassName(this.fieldName + '-drop')[0].children[0].children[0].children[0].children[0].children[0].value = this.checkedValue.join(',')
}, 100)
})
} else {
const newVal = document.getElementsByClassName(this.fieldName + '-drop')[0].children[0].children[0].children[0].children[0].children[0].value
const oldVal = this.checkedValue.join(',')
if (newVal !== oldVal) {
if (newVal) {
this.checkedValue = newVal.split(',')
} else {
this.checkedValue = []
}
this.$emit('change', this.checkedValue.join(','))
}
this.$refs[this.fieldName].blur()
this.open = open
this.$nextTick(() => {
// console.log('------this.checkedValue.join(\',\')----------', this.checkedValue.join(','))
// this.$emit('change', lastCheckedValue.join(','))
document.getElementById(this.fieldName).children[0].children[0].value = this.checkedValue.join(',')
})
}
},
handleChange (val) {
console.log('-handleChange-----', val)
if (!val) {
this.firstValue = null
this.checkedValue = []
this.$emit('change', this.checkedValue.join(','))
}
},
getCurrentStyle (current) {
const currentStr = current.format('YYYY-MM-DD')
if (this.checkedValue.includes(currentStr)) {
return true
}
},
// 日期面板中input绑定事件
focusInput (e) {
e.target.value = this.checkedValue.join(',')
},
// 点击日期,加日期或者减日期
clickCalendarDate (current) {
const currentStr = current.format('YYYY-MM-DD')
if (!this.checkedValue.includes(currentStr)) {
console.log('加日期')
const checkedValue = this.checkedValue
checkedValue.push(currentStr)
// 对日期进行排序
this.checkedValue = checkedValue.sort((date1, date2) => {
return new Date(date1) - new Date(date2)
})
} else {
console.log('减日期')
this.checkedValue = this.checkedValue.filter(item => item !== currentStr)
}
this.firstValue = this.checkedValue[0]
this.$nextTick(() => {
setTimeout(() => {
const inputDom = document.getElementsByClassName(this.fieldName + '-drop')[0].children[0].children[0].children[0].children[0].children[0]
inputDom.value = this.checkedValue.join(',')
inputDom.addEventListener('focus', this.focusInput)
})
})
this.$emit('change', this.checkedValue.join(','))
}
// 点击日期弹出框中的确定按钮
// handleOk () {
// this.open = false
// this.$nextTick(() => {
// // 改变外侧日期显示框的数据
// document.getElementById(this.fieldName).children[0].children[0].value = this.checkedValue.join(',')
// // document.getElementsByClassName(this.fieldName)[0].children[0].children[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: #ffd5d3 !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;
align-items: center !important;
}
// 删除弹出层的今天按钮
:global(.ant-calendar-today-btn) {
display: none !important;
}
</style>
+388
View File
@@ -0,0 +1,388 @@
<template>
<div :id="domId" class="mind-map"></div>
</template>
<script>
import G6 from '@antv/g6'
import insertCss from 'insert-css'
// 收起图标
const COLLAPSE_ICON = function COLLAPSE_ICON (x, y, r) {
return [['M', x, y], ['a', r, r, 0, 1, 0, r * 2, 0], ['a', r, r, 0, 1, 0, -r * 2, 0], ['M', x + 2, y], ['L', x + 2 * r - 2, y]]
}
// 展开图标
const EXPAND_ICON = function EXPAND_ICON (x, y, r) {
return [['M', x, y], ['a', r, r, 0, 1, 0, r * 2, 0], ['a', r, r, 0, 1, 0, -r * 2, 0], ['M', x + 2, y], ['L', x + 2 * r - 2, y], ['M', x + r, y - r + 2], ['L', x + r, y + r - 2]]
}
// 节点的颜色
const NODE_COLOR = [
{
bg: '#E9ECF0',
stroke: '#3D557B',
color: '#2F466B'
},
{
bg: '#EEF4F8',
stroke: '#739FC8',
color: '#227CCF'
},
{
bg: '#FEF1EA',
stroke: '#F9964F',
color: '#DF7426'
},
{
bg: '#E5F2F4',
stroke: '#2596A1',
color: '#2596A1'
}
]
// 节点和收起展开之间的间距
const NODE_ICON_MARGIN = 8
// 最长文本显示长度
const ELLIPSE_LENGTH = 30
// 处理图的tooltip样式
insertCss(`
.g6-tooltip {
border-radius: 6px;
font-size: 12px;
color: #fff;
background-color: #000;
padding: 2px 8px;
text-align: center;
}
`)
export default {
name: 'MindMap',
props: {
// 页面元素的id
domId: {
type: String,
required: false,
default: 'mountNode'
},
// 图表的数据
mapData: {
type: Object,
required: false,
default: () => {
return {}
}
},
// 图数据的key字段
mapDataKeyField: {
type: String,
required: false,
default: 'id'
},
// 回显文本取的字段
labelField: {
type: String,
required: false,
default: 'name'
},
// 节点的宽度
nodeWidth: {
type: Number,
required: false,
default: 400
},
// 节点高度
nodeHeight: {
type: Number,
required: false,
default: 46
},
// 收起展开节点图标的半径
expendCollapseIconR: {
type: Number,
required: false,
default: 8
}
},
data () {
return {
data: {},
graph: null,
loadEnd: false,
selectedNodeId: null,
lastQueryId: null // 记录上次查询的节点id
}
},
watch: {
mapData: {
handler () {
this.data = JSON.parse(JSON.stringify(this.mapData))
if (this.loadEnd && this.data.id) {
this.initMindMap()
}
},
deep: true
},
loadEnd () {
if (this.loadEnd && this.data.id) {
this.initMindMap()
}
}
},
mounted () {
this.$nextTick(() => {
this.loadEnd = true
window.onresize = () => {
const width = document.getElementById(this.domId).clientWidth
const height = document.getElementById(this.domId).clientHeight
this.graph.changeSize(width, height)
this.graph.fitCenter()
}
})
},
methods: {
initMindMap () {
if (this.graph) {
this.graph.destroy()
this.graph = null
}
G6.registerNode('tree-node', {
drawShape: (cfg, group) => {
const nodeColorIndex = cfg.depth % NODE_COLOR.length
const nodeStyle = NODE_COLOR[nodeColorIndex]
// 节点整体的内容
const nodeContainer = group.addShape('rect', {
attrs: {
x: this.nodeHeight / 2,
y: this.nodeHeight / 2,
width: this.nodeWidth + NODE_ICON_MARGIN + this.expendCollapseIconR,
height: this.nodeHeight
},
name: 'rect-shape'
})
// 有边框和背景色的box
group.addShape('rect', {
attrs: {
x: 0,
y: 0,
width: this.nodeWidth,
height: this.nodeHeight,
radius: this.nodeHeight / 2,
fill: nodeStyle.bg,
stroke: nodeStyle.stroke
},
name: 'rect-content'
})
// 文本的部分
let textContent = (cfg.num || '') + ' ' + (cfg.name || '')
const text = textContent.length > ELLIPSE_LENGTH ? textContent.substring(0, ELLIPSE_LENGTH) + '...' : textContent
group.addShape('text', {
attrs: {
text: text,
x: this.nodeWidth / 2,
y: this.nodeHeight / 2,
textAlign: 'center',
textBaseline: 'middle',
fill: nodeStyle.color
},
name: 'rect-text'
})
// 处理展开收起图标
const hasChildren = cfg.children && cfg.children.length > 0
if (hasChildren) {
group.addShape('marker', {
attrs: {
x: this.nodeWidth + NODE_ICON_MARGIN,
y: this.nodeHeight / 2,
r: this.expendCollapseIconR,
symbol: COLLAPSE_ICON,
stroke: '#666',
lineWidth: 2
},
name: 'expend-collapse',
className: 'collapse-icon'
})
}
nodeContainer.attr({
x: 0,
y: 0,
width: this.nodeWidth + this.expendCollapseIconR + NODE_ICON_MARGIN + this.expendCollapseIconR,
height: this.nodeHeight
})
return nodeContainer
},
setState (name, value, item) {
const group = item.getContainer()
const nodeDom = group.get('children')
const nodeBox = nodeDom[1]
const nodeText = nodeDom[2]
if (name === 'selected') {
if (value) {
nodeBox.attr('fill', '#D52C26')
nodeBox.attr('stroke', '#D52C26')
nodeText.attr('fill', '#FFFFFF')
} else {
const nodeColorIndex = item._cfg.model.depth % NODE_COLOR.length
const nodeStyle = NODE_COLOR[nodeColorIndex]
nodeBox.attr('fill', nodeStyle.bg)
nodeBox.attr('stroke', nodeStyle.stroke)
nodeText.attr('fill', nodeStyle.color)
}
}
}
}, 'single-shape')
this.graph = new G6.TreeGraph({
container: this.domId,
width: document.getElementById(this.domId).clientWidth,
height: document.getElementById(this.domId).clientHeight,
modes: {
default: [
'drag-canvas',
'zoom-canvas',
{
type: 'tooltip',
formatText (model) {
return (model.num || '') + '&emsp;' + (model.name || '')
},
offset: 10
}
]
},
defaultNode: {
type: 'tree-node',
anchorPoints: [[0, 0.5], [1, 0.5]]
},
defaultEdge: {
type: 'cubic-horizontal',
style: {
stroke: '#A3B1BF',
endArrow: {
path: G6.Arrow.circle(2),
d: 0,
fill: '#A3B1BF'
}
}
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: (d) => {
return d.id
},
getVGap: () => {
return 20
},
getHGap: () => {
return 300
}
}
})
// G6.Util.traverseTree(this.data, (item) => {
// item.id = item[this.mapDataKeyField]
// })
this.graph.data(this.data)
this.graph.render()
this.graph.fitView()
// 展开收起事件
this.graph.on('expend-collapse:click', (event) => {
const { item } = event
item.getModel().collapsed = !item.getModel().collapsed
// this.graph.setItemState(item, 'collapsed', item.getModel().collapsed)
this.graph.refreshItem(item)
this.graph.layout()
})
// 点击选中
this.graph.on('rect-content:click', (event) => {
this.handleSelectedNode(event)
})
this.graph.on('rect-text:click', (event) => {
this.handleSelectedNode(event)
})
},
handleSelectedNode (event) {
const { item } = event
if (this.selectedNodeId && this.selectedNodeId !== item._cfg.id) {
this.graph.findById(this.selectedNodeId).getModel().selected = false
this.graph.setItemState(this.selectedNodeId, 'selected', false)
}
item.getModel().selected = !item.getModel().selected
this.graph.setItemState(item._cfg.id, 'selected', item.getModel().selected)
this.selectedNodeId = item._cfg.id
this.graph.refreshItem(item._cfg.id)
this.graph.layout()
// 选中抛出节点信息,取消选中抛出一个空对象
if (item.getModel().selected) {
this.$emit('select', item._cfg.model)
} else {
this.$emit('select', {})
}
},
/**
* 查询,定位高亮
* @param nodeId
*/
searchNode (nodeId) {
if (this.lastQueryId) {
this.graph.setItemState(this.lastQueryId, 'selected', false)
}
if (this.selectedNodeId) {
this.graph.setItemState(this.selectedNodeId, 'selected', false)
}
if (!nodeId) {
return
}
const item = this.graph.findById(nodeId)
this.graph.setItemState(nodeId, 'selected', true)
const node = this.graph.findById(nodeId)
this.selectedNodeId = nodeId
this.$emit('select', node._cfg.model)
this.lastQueryId = nodeId
const matrix = item.get('group').getMatrix()
const point = {
x: matrix[6],
y: matrix[7]
}
const width = this.graph.get('width')
const height = this.graph.get('height')
// 找到视口中心
const viewCenter = {
x: width / 2,
y: height / 2
}
const modelCenter = this.graph.getPointByCanvas(viewCenter.x, viewCenter.y)
const viewportMatrix = this.graph.get('group').getMatrix()
// 画布平移的目标位置,最终目标是graph.translate(dx, dy);
const dx = (modelCenter.x - point.x) * viewportMatrix[0]
const dy = (modelCenter.y - point.y) * viewportMatrix[4]
let lastX = 0
let lastY = 0
let newX = void 0
let newY = void 0
// 动画每次平移一点,直到目标位置
this.graph.get('canvas').animate({
onFrame: (ratio) => {
newX = dx * ratio
newY = dy * ratio
this.graph.translate(newX - lastX, newY - lastY)
lastX = newX
lastY = newY
}
}, 300, 'easeCubic')
}
}
}
</script>
<style scoped lang="less">
.mind-map {
width: 100%;
height: 100%;
position: relative;
}
/deep/ .g6-tooltip {
max-width: 200px;
padding: 8px 10px;
}
</style>
+323
View File
@@ -0,0 +1,323 @@
<template>
<div class="person-comp-box">
<a-steps direction="vertical">
<!-- 没有currentNode说明流程已经结束了图标和线都显示已经走过的样式 -->
<a-step v-for="(step, index) in stepsData" :key="step.id" :status="step.nodeName === currentNode ? 'process' : 'wait'"
:class="{'red-tail': currentNode ? index < currentNodeIndex : true}">
<template #icon>
<div class="step-cus-icon"
:class="{'step-cus-icon-done': currentNode ? index < currentNodeIndex : true, 'step-cus-icon-now': index === currentNodeIndex}">
<i class="iconfont icon-user" />
</div>
</template>
<!-- 流程节点标题-->
<!--<template #title>-->
<!-- <a-tooltip>-->
<!-- <template #title>{{ step.nodeName }}</template>-->
<!-- <div class="step-title">{{ step.nodeName }}</div>-->
<!-- </a-tooltip>-->
<!--</template>-->
<!-- 流程节点选人内容-->
<template #description>
<div class="personnel-container">
<div class="personnel-container-title" :class="{'personnel-container-select-title': index === 0}">
<a-tooltip>
<template #title>{{ step.nodeName }}</template>
<div class="personnel-container-title-text">{{ step.nodeName }}</div>
</a-tooltip>
</div>
<div class="personnel-container-content">
<div class="personnel-container-content-text">
<template v-if="step.canChoose && index >= currentNodeIndex">
<user-select-by-work-group v-if="step.chooseSource === 'workGroup'"
v-model="step.linkUserIds"
:type="step.chooseType"
:name-str="step.linkUserIds_dictText"
:filedName="step.variableName"
@nameChange="nameChange"
input-type="textarea" />
<user-select-by-contact v-else-if="step.chooseSource === 'contactManage'"
v-model="step.linkUserIds"
:type="step.chooseType"
:name-str="step.linkUserIds_dictText"
:filedName="step.variableName"
@nameChange="nameChange"
input-type="textarea" />
<user-selection v-else-if="step.chooseSource === 'user'"
:type="step.chooseType"
v-model="step.linkUserIds"
:name-str="step.linkUserIds_dictText"
:filedName="step.variableName"
@nameChange="nameChange"
input-type="textarea" />
</template>
<div class="personnel-container-content-echo-text" v-else>{{ step.userList }}</div>
</div>
<a-icon type="clock-circle" theme="filled" v-if="index === currentNodeIndex" class="personnel-container-content-icon" />
<a-icon type="check"
v-if="index < currentNodeIndex"
class="personnel-container-content-icon personnel-container-content-icon-now" />
</div>
</div>
</template>
</a-step>
</a-steps>
</div>
</template>
<script>
import '@assets/less/common.less'
import UserSelection from './selection/UserSelection.vue'
import UserSelectByWorkGroup from './selection/UserSelectByWorkGroup'
import UserSelectByContact from './selection/UserSelectByContact'
export default {
name: 'PersonnelInfo',
components: { UserSelectByContact, UserSelectByWorkGroup, UserSelection },
props: {
// 数据
data: {
type: Array,
required: true,
default: () => {
return []
}
},
// 当前节点
currentNode: {
type: String,
required: false,
default: null
},
// 特殊流程需要用indexOf判断的前缀
specialProcessNodePrefix: {
type: String,
required: false,
default: null
},
// 不需要做必填校验的字段,用于应对第一个节点选没选人决定节点跳不跳过的问题
notPermissField: {
type: Array,
required: false,
default: () => {
return []
}
}
},
data () {
return {
stepsData: []
}
},
computed: {
// 当前节点的位置
currentNodeIndex () {
if (this.specialProcessNodePrefix) {
return this.stepsData.findIndex(tt => tt.xmlNodeId.indexOf(this.specialProcessNodePrefix) === 0)
}
return this.stepsData.findIndex(tt => tt.xmlNodeId === this.currentNode)
}
},
watch: {
data: {
handler () {
this.stepsData = JSON.parse(JSON.stringify(this.data))
},
deep: true,
immediate: true
}
},
methods: {
// 获取人员对象,无校验
getDataObj () {
const personnelObj = {}
// for (let i = 0; i < this.stepsData.length; i++) {
// // 该属性还没有人就给他赋值
// if ((this.stepsData[i].canChoose || i === 0) && !personnelObj[this.stepsData[i].variableName]) {
// personnelObj[this.stepsData[i].variableName] = this.stepsData[i].linkUserIds
// personnelObj[this.stepsData[i].variableName + '_dictText'] = this.stepsData[i].linkUserIds_dictText || this.stepsData[i].userList
// }
// }
for (const item of this.stepsData) {
console.log(item)
// 该属性还没有人就给他赋值
if (!personnelObj[item.variableName]) {
personnelObj[item.variableName] = item.linkUserIds
personnelObj[item.variableName + '_dictText'] = item.linkUserIds_dictText || item.userList
}
// if ((item.canChoose || item.sort === 1) && !personnelObj[item.variableName]) {
// personnelObj[item.variableName] = item.linkUserIds
// personnelObj[item.variableName + '_dictText'] = item.linkUserIds_dictText || item.userList
// }
}
return personnelObj
},
// 有校验的获取人员对象
getDataObjVerify () {
let personnelObj = {}
console.log(this.stepsData)
for (let i = 0; i < this.stepsData.length; i++) {
// 该属性还没有人就给他赋值,并且如果minLength有值的话,需要判断选的人数过了这个限制
if ((this.stepsData[i].canChoose || i === 0) && !personnelObj[this.stepsData[i].variableName]) {
if (this.stepsData[i].minLength) {
// 有设置至少选几人
if (this.stepsData[i].linkUserIds && this.stepsData[i].linkUserIds.split(',').length >= this.stepsData[i].minLength) {
// 说明人数通过了限制
personnelObj[this.stepsData[i].variableName] = this.stepsData[i].linkUserIds
personnelObj[this.stepsData[i].variableName + '_dictText'] = this.stepsData[i].linkUserIds_dictText || this.stepsData[i].userList
} else {
// 提示XX节点至少选择XX人
this.$message.warning(this.stepsData[i].nodeName + this.$t('node') + this.$t('atLeastSelect') + this.stepsData[i].minLength + this.$t('personnal'))
personnelObj[this.stepsData[i].variableName] = ''
return
}
} else {
personnelObj[this.stepsData[i].variableName] = this.stepsData[i].linkUserIds
personnelObj[this.stepsData[i].variableName + '_dictText'] = this.stepsData[i].linkUserIds_dictText || this.stepsData[i].userList
}
}
}
console.log(personnelObj)
for (const item in personnelObj) {
if (!this.notPermissField.includes(item) && !personnelObj[item]) {
// 不在不需要做必填校验的字段里,说明是必填并且没有数据
this.$message.warning(this.$t('pleaseEnterPersonnelInfo'))
personnelObj = false
break
}
}
return personnelObj
},
nameChange (value, fieldName) {
this.stepsData.find(item => item.variableName === fieldName).linkUserIds_dictText = value
this.stepsData.find(item => item.variableName === fieldName).userList = value
}
}
}
</script>
<style scoped lang="less">
.person-comp-box {
height: 100%;
}
/deep/ .ant-steps-vertical .ant-steps-item-icon {
margin-right: 0;
}
/deep/ .ant-steps-item-title {
max-width: 100%;
}
/deep/ .ant-steps-item-content {
padding: 0 12px 30px 12px;
}
.step-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
font-family: PingFang SC-Regular, PingFang SC, sans-serif;
color: #1D2129;
font-weight: 550;
}
.personnel-container {
&-title {
height: 36px;
background: #6A7484;
border-radius: 8px 8px 0 0;
padding: 0 16px;
&-text {
font-size: 16px;
font-family: PingFang SC-Regular, PingFang SC, sans-serif;
color: #FFFFFF;
line-height: 36px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
&-select-title {
background: #9F5258;
}
&-content {
background: #FFFFFF;
border-radius: 0 0 8px 8px;
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.1);
padding: 16px;
min-height: 52px;
display: flex;
align-items: center;
&-text {
flex: 1;
width: 0;
}
&-icon {
margin-left: 16px;
color: @primary-color;
font-size: 20px;
}
&-icon-now {
font-size: 10px;
width: 20px;
height: 20px;
background: #FAE6E5;
border-radius: 10px 10px 10px 10px;
opacity: 1;
line-height: 20px;
text-align: center;
}
&-echo-text {
font-size: 14px;
font-family: PingFang SC-Regular, PingFang SC, sans-serif;
font-weight: 400;
color: #4E5969;
line-height: 22px;
word-break: break-all;
}
}
}
/deep/ .user-organ-wrap .button-box {
height: 32px;
}
/deep/ .ant-steps-vertical > .ant-steps-item > .ant-steps-item-container > .ant-steps-item-tail {
left: 14px;
padding: 31px 0 4px;
}
.step-cus-icon {
width: 28px;
height: 28px;
background: #F2F3F5;
border-radius: 32px 32px 32px 32px;
display: flex;
align-items: center;
justify-content: center;
&-done {
background: rgba(213, 44, 38, 0.08);
color: @primary-color;
}
&-now {
color: white;
background: @primary-color;
}
}
/deep/ .red-tail > .ant-steps-item-container > .ant-steps-item-tail::after {
background-color: @primary-color;
}
</style>
+420
View File
@@ -0,0 +1,420 @@
<template>
<div class="page-box">
<div class="personnel-info">
<slot name="personnelInfo"></slot>
</div>
<div class="approval-list">
<!--查询区域-->
<div class="table-page-search-wrapper">
<a-form layout="inline" @keyup.enter.native="searchQuery">
<a-row :gutter="24">
<a-col :md="8" :sm="8">
<!--状态-->
<a-form-item :label="$t('status')" :labelCol="labelCol" :wrapperCol="wrapperCol">
<j-dict-select-tag
:placeholder="$t('pleaseEnter') + $t('status')"
dict-code="prc_node_status"
v-model="queryParam.finishFlag"></j-dict-select-tag>
</a-form-item>
</a-col>
<div style="float: right;overflow: hidden;margin-right: 11px" class="table-page-search-submitButtons">
<a-button class="box-button" type="primary" icon="search" @click="searchQuery">{{ $t('query') }}</a-button>
<a-button class="box-button" icon="reload" style="margin-left: 8px" @click="searchReset">{{ $t('reset') }}</a-button>
</div>
</a-row>
</a-form>
</div>
<div class="table-page-oper-submitButtons">
<!--催办-->
<a-button type="primary" @click="handleUrge" icon="bell" v-if="showUrgeButton">
{{ $t('workCenter.processDetail.urgeHandle') }}
</a-button>
<!--流程图-->
<!--<a-button type="primary" @click="viewFlowChart" icon="apartment" ghost>{{ $t('workCenter.processDetail.flowChart') }}</a-button>-->
</div>
<div>
<!-- rowKey加了id是因为强制撤回的记录没有taskId -->
<j-table
:columns="columns"
:dataSource="dataSource"
:can-drag="true"
:loading="loading"
:rowKey="record => {return record.id + ',' + (record.taskId || '')}"
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
:pagination="ipagination"
:scroll="{x: '100%', y: yScrollHeight}"
@change="handleTableChange">
<!-- 耗时 -->
<!--<template v-slot:timeConsuming="{text, record}">-->
<!-- <a-tooltip overlay-class-name="tooltip-style">-->
<!-- <template slot="title">{{ record.endTime && record.createTime ? computedTimeConsuming(record) : global.emptyLine }}</template>-->
<!-- <div class="table-text">-->
<!-- {{ record.endTime && record.createTime ? computedTimeConsuming(record) : global.emptyLine }}-->
<!-- </div>-->
<!-- </a-tooltip>-->
<!--</template>-->
<!--审批意见-->
<template v-slot:approvalOpinion="{text, record}">
<a-tooltip overlay-class-name="tooltip-style">
<template slot="title">{{ text || text === 0 ? record.commitFlag_dictText : global.emptyLine }}</template>
<div class="table-text" :class="{'agree-color': record.commitFlag + '' === '1', 'reject-color': record.commitFlag + '' === '2'}">
{{ text || text === 0 ? record.commitFlag_dictText : global.emptyLine }}
</div>
</a-tooltip>
</template>
<!--附件-->
<template v-slot:file="{text, record}">
<a v-if="text" @click="fileLook(record)">{{ $t('view') }}</a>
<div v-else class="table-text">{{ global.emptyLine }}</div>
<!--<a-tooltip overlay-class-name="tooltip-style">-->
<!-- <template slot="title">{{ text || text === 0 ? text : global.emptyLine }}</template>-->
<!-- <div class="table-text can-click-table-text" v-if="text || text === 0" @click="handlePreview(record)">{{ text }}</div>-->
<!-- <div v-else class="table-text">{{ global.emptyLine }}</div>-->
<!--</a-tooltip>-->
</template>
</j-table>
</div>
<div id="images">
<div class="image" v-viewer="{movable: false}">
<img v-show="image" :src="imageUrl" alt="">
</div>
</div>
</div>
<flow-chart-modal ref="flowChartModal" />
<upload-file ref="uploadFile" disabled></upload-file>
</div>
</template>
<script>
import { JeroListMixin } from '../mixins/JeroListMixin.js'
import JTable from './jero/JTable.vue'
import Vue from 'vue'
import { ACCESS_TOKEN } from '../store/mutation-types.js'
import { downloadFile, getFileAccessHttpUrl, getAction } from '../api/manage.js'
import { previewPdf } from '../utils/previewPdf.js'
import FlowChartModal from './FlowChartModal.vue'
import UploadFile from './UploadFile'
import { filterObj } from '../utils/util'
import { kkFilePreview } from '../utils/kkFilePreview'
import { urgeProcessNode } from '../api/workCenter'
// 支持预览的文件后缀
const CAN_PREVIEW_FILE_SUFFIX = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
const FILE_TYPE_IMGS = ['jpg', 'jpeg', 'png', 'raw']
const FILE_TYPE_PDF = 'pdf'
export default {
name: 'ProcessDetailList',
components: { UploadFile, JTable, FlowChartModal },
mixins: [JeroListMixin],
props: {
// 需要有list属性,用于混入里的接口获取
url: {
type: Object,
required: false,
default: () => {
return {
list: '/workCenter/getApprovalRecordList'
}
}
},
// 催办按钮的权限
urgePermissionFlag: {
type: String,
required: false,
default: null
},
// 流程图按钮的权限
flowChartPermissionFlag: {
type: String,
required: false,
default: null
},
// 流程key
processKey: {
type: String,
required: false,
default: null
},
// 是否展示催办按钮
showUrgeButton: {
type: Boolean,
required: false,
default: false
}
},
computed: {
yScrollHeight () {
/**
* 前几个使用变量是为了配合iframe嵌套,算出页面显示范围的高度
* 48px:分页器的高度
* 48pxa-card的padding(上24+下24
* 54px:表头行的高度
*/
return `calc(100vh - ${this.defaultHeight['user-info-height']} - ${this.defaultHeight['breadcrumb-height']} - ${this.defaultHeight.oneLineSearchHeight} - ${this.defaultHeight.oneLineOperationHeight} - 48px - 48px - 54px - 56px)`
}
},
data () {
return {
labelCol: {
span: 1
},
wrapperCol: {
span: 14
},
columns: [
// 任务节点
{
dataIndex: 'taskName',
title: this.$t('workCenter.processDetail.taskNode'),
width: 150
},
// 任务受理人
{
dataIndex: 'taskAcceptor',
title: this.$t('workCenter.processDetail.taskHandler'),
width: 120
},
// 创建时间
{
dataIndex: 'createTime',
title: this.$t('createTime'),
width: 150
},
// 完成时间
{
dataIndex: 'endTime',
title: this.$t('workCenter.processDetail.completionTime'),
width: 150
},
// 耗时
{
title: this.$t('workCenter.processDetail.timeConsuming'),
width: 150,
dataIndex: 'overTime',
scopedSlots: { customRender: 'text' }
},
// 审批意见
{
dataIndex: 'commitFlag',
title: this.$t('workCenter.processDetail.approvalOpinion'),
width: 100,
scopedSlots: { customRender: 'approvalOpinion' }
},
// 意见内容
{
dataIndex: 'commitText',
title: this.$t('workCenter.processDetail.opinionContent'),
width: 120
},
// 附件
{
dataIndex: 'commitFile',
title: this.$t('businessSupport.questionAnswer.attach'),
width: 160,
scopedSlots: { customRender: 'file' }
},
// 状态
{
dataIndex: 'finishFlag_dictText',
title: this.$t('status'),
width: 100
}
],
image: false,
imageUrl: null,
/* 排序参数 */
// 后端需要去掉
isorter: null
}
},
methods: {
loadData (arg) {
if (!this.url.list) {
this.$message.warning('请设置url.list属性!')
return
}
if (arg === 1) {
this.ipagination.current = 1
}
const params = this.getQueryParams()// 查询条件
// 流程监控进详情,并且是已撤回的流程不查状态为进行中的任务
if (this.$route.query.source === 'monitorList' && this.$route.query.prcStatus === '3') {
params.finishFlagSearch = '2,3,4'
}
if (this.$route.query.processInstanceId) {
params.processInstanceId = this.$route.query.processInstanceId
} else {
// 没有流程实例id,那就是新增,不用查
return
}
this.loading = true
getAction(this.url.list, params).then((res) => {
if (res.success) {
// update-begin---author:zhangyafei Date:20201118 for:适配不分页的数据列表------------
this.dataSource = res.result.records || res.result
if (res.result.total) {
this.ipagination.total = res.result.total
} else {
this.ipagination.total = 0
}
// update-end---author:zhangyafei Date:20201118 for:适配不分页的数据列表------------
} else {
this.$message.warning(res.message)
}
}).finally(() => {
this.loading = false
})
},
getQueryParams () {
// 获取查询条件
const sqp = {}
if (this.superQueryParams) {
sqp.superQueryParams = encodeURI(this.superQueryParams)
sqp.superQueryMatchType = this.superQueryMatchType
}
const param = Object.assign(sqp, this.queryParam, this.isorter, this.filters)
param.field = this.getQueryField()
param.pageNo = this.ipagination.current
param.pageSize = this.ipagination.pageSize
return filterObj(param)
},
// 计算耗时
computedTimeConsuming (record) {
// 结束时间和创建时间
const endTime = new Date(record.endTime)
const createTime = new Date(record.createTime)
// 计算时间差 转化为秒数
const time = parseInt((endTime.getTime() - createTime.getTime()) / 1000)
// 总秒数转化为对应的天数
const days = parseInt(time / (24 * 60 * 60))
// 计算小时
const hours = parseInt((time % (24 * 60 * 60)) / (60 * 60))
// 计算分钟
const minutes = parseInt((time % (60 * 60)) / 60)
// 计算秒
const seconds = time % 60
return (days ? days + '天 ' : '') + (hours ? hours + '小时 ' : '') +
(minutes ? minutes + '分钟 ' : '') + (seconds ? seconds + '秒' : '')
},
// 附件列的查看点击
fileLook (record) {
this.$refs.uploadFile.open(record.commitFile)
},
/**
* 催办
*/
handleUrge () {
if (!this.selectedRowKeys || this.selectedRowKeys.length === 0) {
this.$message.warning(this.$t('selectAtLeastOne'))
return
}
this.loading = true
const taskIds = this.selectedRowKeys.map(item => {
return item.split(',')[1]
})
urgeProcessNode({ taskId: taskIds.join(',') }).then(res => {
if (res.success) {
this.$message.success(res.message)
} else {
this.$message.warning(res.message)
}
}).finally(() => {
this.loading = false
})
},
/**
* 跳转流程图页面
*/
viewFlowChart () {
if (!this.processKey) {
return
}
this.$refs.flowChartModal.processKey = this.processKey
this.$refs.flowChartModal.open()
},
handlePreview (file) {
if (!file || !file.url) {
return
}
// 截取文件后缀名
const fileSuffix = file.fileName ? file.fileName.split('.')[file.fileName.split('.').length - 1] : ''
const canPreview = CAN_PREVIEW_FILE_SUFFIX.some(tt => fileSuffix.toLowerCase() === tt)
// 判断是否为可预览格式的文件
if (!canPreview) {
this.$message.loading('该文件类型不支持预览,正在为您准备下载...').then(() => {
this.handleDownload(file)
})
return
}
const fileFullUrl = `${window._CONFIG.domianWebSocketURL}/sys/common/view/${file.id}?at=${Vue.ls.get(ACCESS_TOKEN)}&fullfilename=${file.fileName}`
// 图片预览,使用自己添加的组件
if (canPreview && FILE_TYPE_IMGS.includes(fileSuffix.toLowerCase())) {
this.imageUrl = getFileAccessHttpUrl(file.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.toLowerCase())) {
const url = previewPdf(file.id)
window.open(url)
return
}
// 其余可预览文件仍使用KKFile进行预览
kkFilePreview(fileFullUrl)
}
}
}
</script>
<style scoped lang="less">
.page-box {
height: 100%;
display: flex;
}
.personnel-info {
height: 100%;
width: 400px;
padding: 32px;
overflow: auto;
border-right: 1px solid #E5E6EB;
}
.approval-list {
height: 100%;
flex: 1;
width: 0;
overflow: auto;
padding: 32px 32px 0;
}
.agree-color {
color: #229B1F;
}
// 这个地方的颜色不用primary-color是因为拒绝永远是红色的
.reject-color {
color: #D52C26;
}
.table-page-oper-submitButtons {
.ant-btn {
margin-bottom: 20px;
}
}
/deep/ .ant-form-item-label {
min-width: 90px !important;
}
</style>
+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)
+96
View File
@@ -0,0 +1,96 @@
<template>
<a-tree-select
v-bind="$attrs"
v-on="$listeners"
:value="treeValue"
:getPopupContainer="(node) => node.parentNode"
style="width: 100%"
:tree-checkable="treeCheckable"
:show-search="canSearch"
:search-value.sync="searchValue"
:filterTreeNode="filterTreeOption"
@change="handleChange"
@search="handleSearch">
</a-tree-select>
</template>
<script>
export default {
name: 'STreeSelect',
props: {
value: {
required: false
},
// 是否可以搜索
canSearch: {
type: Boolean,
required: false,
default: true
},
// 是否多选
treeCheckable: {
type: Boolean,
required: false,
default: false
}
},
watch: {
value: {
handler () {
if (typeof this.value !== 'string' && this.value) {
this.treeValue = JSON.parse(JSON.stringify(this.value))
} else {
this.treeValue = this.value
}
},
immediate: true
}
},
data () {
return {
treeValue: null, // 选中的数据
searchValue: null // 查询的字符串
}
},
mounted () {
if (this.treeCheckable) {
this.$nextTick(() => {
document.addEventListener('click', this.clearSearchValue)
})
}
},
methods: {
filterTreeOption (input, option) {
const text = option.componentOptions.propsData.title || option.componentOptions.propsData.label
return text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
handleChange (value) {
this.treeValue = value
this.$emit('change', value)
},
handleSearch (value) {
this.searchValue = value
},
clearSearchValue (event) {
event.stopPropagation()
this.searchValue = null
}
},
beforeDestroy () {
if (this.treeCheckable) {
document.removeEventListener('click', this.clearSearchValue)
}
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style scoped lang="less">
/deep/ .ant-select-tree-dropdown {
max-height: 50vh !important;
}
</style>
+231
View File
@@ -0,0 +1,231 @@
<template>
<div>
<a-drawer
:title="title"
:maskClosable="false"
:width="600"
placement="right"
:closable="true"
@close="handleCancel"
:visible="visible"
style="height: 100%;overflow: auto;padding-bottom: 53px;">
<a-spin :spinning="confirmLoading">
<a-input-search style="margin-bottom: 8px" v-model="searchModel"
@search="onSearch"
allowClear
:placeholder="$t('NodeQuickLookup')" />
<div class="drawer-content">
<a-tree
v-if="visibleTree"
style="margin-bottom: 60px;height: 600px"
checkable
:autoExpandParent="false"
:tree-data="gData"
@select="onSelect"
@check="checkChange"
@expand="onExpand"
:defaultExpandedKeys="defaultExpandedKeys"
>
</a-tree>
</div>
</a-spin>
<div class="drawer-bootom-button">
<a-button @click="handleCancel" style="margin-right: 16px">{{ $t('cancel') }}</a-button>
<a-button @click="handleSubmit" type="primary" :loading="submitLoading">{{ $t('submit') }}</a-button>
</div>
</a-drawer>
</div>
</template>
<script>
import { getAction, postAction } from '@/api/manage'
export default {
name: 'index',
props: {
title: {
type: String,
default: ''
},
isSingleChoice: {
type: Boolean,
default: false
},
flag: {
type: Number,
default: 0
}
},
data () {
return {
gData: [],
searchModel: '',
autoExpandParent: true,
expandedKeys: [],
visible: false,
confirmLoading: false,
selectedKey: [],
tableKey: [],
userIds: [],
departId: '',
defaultExpandedKeys: [],
submitLoading: false,
visibleTree: false,
userName: []
}
},
mounted () {
},
methods: {
getPush () {
this.searchModel = ''
this.visible = true
this.visibleTree = false
this.$nextTick(() => {
this.departId = ''
this.queryDepartUserTreeList()
})
},
onSelect (selectedKeys) {
this.selectedKey = selectedKeys
},
onExpand (selectedKeys, val) {
if (this.searchModel === '' || !this.searchModel) {
if (val.expanded) {
if (this.departId === val.node.value) {
} else {
this.departId = val.node.value
this.queryDepartUserTreeList()
}
}
}
},
handleCancel () {
this.visible = false
this.visibleTree = false
},
handleSubmit () {
if (this.isSingleChoice) {
if (this.userIds.length > 1) {
this.$message.warning(this.$t('onlyOnePersonCanBeSelected'))
return
}
}
this.confirmLoading = true
if (this.userIds && this.userIds.length > 0) {
this.submitLoading = true
this.$emit('SelectedByForm', this.userIds.join(','), this.userName.join(','))
} else {
this.$message.warning(this.$t('selectLeastOne'))
}
},
onSearch (e) {
this.gData = []
this.departId = ''
this.visibleTree = false
if (e) {
this.getUserAndDepart()
} else {
this.queryDepartUserTreeList()
}
},
getUserAndDepart () {
getAction('sys/user/getUserAndDepart', { name: this.searchModel }).then((res) => {
if (res) {
this.gData = res.filter(ele => ele.flag === 'DEPART')
} else {
this.gData = []
}
this.visibleTree = true
})
},
queryDepartUserTreeList () {
let gData = JSON.parse(JSON.stringify(this.gData))
if (gData && gData.length > 0) {
gData = JSON.stringify(gData)
} else {
gData = ''
}
let query = {}
if (this.flag === 1) {
query = {
departId: this.departId,
json: gData
}
} else {
query = {
departId: this.departId,
json: gData,
flag: '1'
}
}
this.confirmLoading = true
postAction('sys/user/queryDepartUserTreeList', query).then((res) => {
this.confirmLoading = false
if (res.success) {
this.gData = res.result
this.gData = [...this.gData]
this.defaultExpandedKeys = [this.departId]
this.visibleTree = true
} else {
this.gData = []
}
})
},
checkChange (e, name) {
this.userName = []
this.userIds = e
if (name.checkedNodes && name.checkedNodes.length > 0) {
name.checkedNodes.forEach(res => {
this.userName.push(res.data.props.dataRef.title)
})
}
if (this.isSingleChoice) {
if (this.userIds.length > 1) {
this.userIds.shift()
this.userName.shift()
}
}
}
}
}
</script>
<style scoped>
.box-title-text {
line-height: 1.4;
display: flex;
}
.box-input {
display: inline-block;
height: 38px;
width: 100%;
}
.button-box {
margin-left: 10px;
height: 38px;
line-height: 38px;
}
.drawer-bootom-button {
position: absolute;
bottom: 0;
z-index: 100;
width: 100%;
border-top: 1px solid #e8e8e8;
padding: 10px 16px;
text-align: right;
left: 0;
background: #fff;
border-radius: 0 0 2px 2px;
}
.drawer-content {
height: calc(100% - 60px);
overflow: auto;
}
</style>
+611
View File
@@ -0,0 +1,611 @@
<template>
<a-spin :spinning="loading">
<!-- 拖拽中不能选中内容 -->
<div class="sheet-box" :class="{ disabledSelect: isDragging }" ref="sheetBoxRef">
<!--标准目录-->
<div class="standard-catalogue part-common" ref="standardCatalogueRef" :style="`width: ${standardCatalogueRefX}px`"
:class="isCatalogueExpand ? 'expand-catalogue' : 'no-expand-catalogue'">
<div class="control-standard-catalogue-expand" @click="expandCatalogue">
<a-icon :type="isCatalogueExpand ? 'left' : 'right'" />
</div>
<div class="part-title" v-show="isCatalogueExpand">{{ $t('standardRegulationLibrary.standardCatalogue') }}</div>
<div class="has-title-part-content" v-show="isCatalogueExpand">
<a-tree :tree-data="treeData"
:selected-keys="selectedTreeKeys"
@select="handleTreeSelected"
:replaceFields="{children: 'children', key: 'id'}">
<a-icon slot="switcherIcon" type="down" :style="{ fontSize: '12px', color: '#103770' }" />
<template slot="title" slot-scope="{name, itemName}">
<span :style="name && name.indexOf('.')===-1?'font-weight: 600;':''">{{ `${name} ${itemName || ''}` }}</span>
</template>
</a-tree>
</div>
<vue-draggable-resizable v-show="isCatalogueExpand" :draggable="true" :resizable="false" :w="6" axis="x" :x="standardCatalogueRefX" :z="2" class="draggable-border"
:onDrag="(...args) => onDragCallback(100, 500, ...args)" class-name-dragging="dragging"
@dragging="onDragging"
@dragstop="(...args) => onDragstop('standardCatalogueRef', ...args)"></vue-draggable-resizable>
</div>
<!--标准内容-->
<div class="sheet-container part-ml" ref="sheetContainerRef" :style="`width: ${sheetContainerRefX}px;`">
<!--标准名称-->
<div class="standard-name">
<a-tooltip placement="topLeft">
<template #title>
{{ spiltInfo.serialNumber }}
</template>
<span> {{ spiltInfo.serialNumber }}{{ spiltInfo.fileName }}</span>
</a-tooltip>
<a-button type="link" :icon="isContainerExpand ? 'fullscreen-exit' : 'fullscreen'" @click="handleContainerExpand">
{{ isContainerExpand ? $t('putAway') : $t('open') }}
</a-button>
</div>
<!--条文内容-->
<div class="standard-content">
<div class="clause-content-item"
v-for="clause in clauseContentList"
:key="clause.id"
@click="handleClickClauseContentItem(clause)">
<div class="clause-content-item-title">{{ clause.item_num }} {{ clause.item_title }}</div>
<div class="clause-content-item-content"
:class="{'clause-content-item-content-hight': clause.menu_id === hightMenuId}"
v-html="clause.item_content"></div>
</div>
</div>
<vue-draggable-resizable :draggable="true" :resizable="false" :w="6" axis="x" :x="sheetContainerRefX" :z="2" class="draggable-border"
:onDrag="(...args) => onDragCallback(100, null, ...args)" class-name-dragging="dragging"
@dragging="onDragging"
@dragstop="(...args) => onDragstop('sheetContainerRef', ...args)"></vue-draggable-resizable>
</div>
<!--条文标签-->
<div class="clause-label part-ml part-common" ref="clauseLabelRef" :style="`width: ${clauseLabelRefX}px`" :class="{'hide-clause-label': !showClauseLabel || isContainerExpand}">
<div class="part-title">{{ $t('standardRegulationLibrary.clauseLabel') }}</div>
<div class="has-title-part-content">
<template v-for="clause in clauseField">
<div class="clause-item" v-if="!clauseLabelHideField.includes(clause.dbFieldName)" :key="clause.id">
<label>{{ clause.dbFieldTxt }}</label>
<div class="clause-item-content"
v-html="clauseLabelInfo[needDictField.includes(clause.fieldShowType) ? clause.dbFieldName + '_dictText' : clause.dbFieldName]"></div>
</div>
</template>
</div>
<vue-draggable-resizable :draggable="true" :resizable="false" :w="6" axis="x" :x="clauseLabelRefX" :z="2" class="draggable-border"
:onDrag="(...args) => onDragCallback(100, 500, ...args)" class-name-dragging="dragging"
@dragging="onDragging"
@dragstop="(...args) => onDragstop('clauseLabelRef', ...args)"></vue-draggable-resizable>
</div>
<!--条文条文标签切换-->
<div class="clause-label-change part-ml part-common" ref="clauseLabelChangeRef" :class="{'no-expand-clause-label': !isClauseLabelExpand}">
<div class="control-clause-label-expand" @click="expandClauseLabel">
<a-icon :type="isClauseLabelExpand ? 'right' : 'left'" />
</div>
<!--条文-->
<a-tooltip placement="left">
<template #title>{{ $t('docTool.split.article') }}</template>
<div class="clause-label-change-item"
:class="{'clause-label-change-item-selected': !showClauseLabel}"
@click="handleClauseChange(false)">
<i class="iconfont icon-ios-list-box" />
<span v-show="isClauseLabelExpand">{{ $t('docTool.split.article') }}</span>
</div>
</a-tooltip>
<!--条文标签-->
<a-tooltip placement="left">
<template #title>{{ $t('docTool.split.clauseLabel') }}</template>
<div class="clause-label-change-item"
:class="{'clause-label-change-item-selected': showClauseLabel}"
@click="handleClauseChange(true)">
<i class="iconfont icon-biaoqian-mianxing" />
<span v-show="isClauseLabelExpand">{{ $t('docTool.split.clauseLabel') }}</span>
</div>
</a-tooltip>
</div>
</div>
</a-spin>
</template>
<script>
import { getDocSplitEditForm, previewDocSplit } from '@api/documentToolApi'
import { FieldType } from '@/enums/commonEnums'
import VueDraggableResizable from 'vue-draggable-resizable'
export default {
name: 'StandardResolutionSheet',
components: {
VueDraggableResizable
},
props: {
// 是否文档拆分处查看
horizontalOverstep: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
showClauseLabel: true, // 是否展示条文标签
// 条文标签字段
clauseField: [],
// 目录数据
treeData: [],
selectedTreeKeys: [],
hightMenuId: null, // 高亮的menuId
isClauseLabelExpand: true, // 条文/条文标签切换部分是否展开,控制是否只显示图标
isCatalogueExpand: true, // 标准目录收起
isContainerExpand: false, // 条文是否展开
loading: false,
spiltInfo: {}, // 拆分的整体信息
clauseContentList: [], // 条文内容的数据
clauseLabelInfo: {}, // 条文标签数据
clauseLabelHideField: ['item_content'],
needDictField: [FieldType.USER_SINGLE.value, FieldType.ORGAN_SINGLE.value, FieldType.STANDARD_MORE.value, FieldType.ORGAN_MORE.value, FieldType.OPTION_SINGLE.value, FieldType.OPTION_MORE.value, FieldType.TREE.value],
queryParams: {},
isDragging: false, // 是否拖拽中
standardCatalogueRefX: 280, // 标准目录默认宽度
sheetContainerRefX: 0, // 标准内容默认宽度
clauseLabelRefX: 400 // 条文标签默认宽度
}
},
mounted () {
// 容器实际剩余宽度 = 右侧内容宽度 - 内容中的间距
const boxWidth = this.$refs.sheetBoxRef.getBoundingClientRect().width - this.$refs.clauseLabelChangeRef.getBoundingClientRect().width - (16 * 3)
this.sheetContainerRefX = boxWidth - this.standardCatalogueRefX - this.clauseLabelRefX
},
methods: {
/**
* 拖拽回调
* @param min - 拖拽最小宽度
* @param max - 拖拽最大宽度
* @param x - 当前宽度
* @return {boolean}
*/
onDragCallback (min, max, x) {
if ((min !== null) && (x < min)) {
return false
}
if ((max !== null) && (x > max)) {
return false
}
return true
},
/**
* 拖拽中
*/
onDragging () {
this.isDragging = true
},
/**
* 拖拽结束
* @param refName - ref名字用于获取dom改变宽度
* @param x - 当前宽度
*/
onDragstop (refName, x) {
this.isDragging = false
// console.log(this.$refs[refName])
this[refName + 'X'] = x
// this.$refs[refName].style.width = x + 'px'
},
/**
* 获取条文标签字段
*/
initClauseFieldList () {
getDocSplitEditForm({ type: 1 }).then(res => {
if (res.success) {
this.clauseField = res.result || []
}
})
},
initDetailInfo () {
this.loading = true
const params = {
...this.queryParams
}
if (this.horizontalOverstep) {
params.horizontalOverstep = true
}
previewDocSplit(params).then(res => {
if (res.success) {
const data = res.result || {}
this.treeData = data.sarFileSplitMenuEO || []
this.spiltInfo = data.documentSplitInfo || {}
this.clauseContentList = data.documentSplitDetail || []
}
}).finally(() => {
this.loading = false
})
},
/**
* 树节点选中
* @param selectedKeys
*/
handleTreeSelected (selectedKeys) {
this.selectedTreeKeys = selectedKeys
this.hightMenuId = this.selectedTreeKeys[0]
if (this.hightMenuId) {
this.handleJump()
} else {
this.clauseLabelInfo = {}
}
},
/**
* 内容跳转,处理条文标签数据
*/
handleJump () {
const contentItemDomList = document.getElementsByClassName('clause-content-item')
const contentDom = document.getElementsByClassName('standard-content')[0]
const contentItemIndex = this.clauseContentList.findIndex(tt => tt.menu_id === this.hightMenuId)
this.handleProcessClauseLabelInfo(contentItemIndex)
contentDom.scrollTop = contentItemDomList[contentItemIndex].offsetTop - 150
},
/**
* 处理条文标签的数据
* @param contentItemIndex
*/
handleProcessClauseLabelInfo (contentItemIndex) {
this.clauseLabelInfo = Object.assign({}, this.clauseContentList[contentItemIndex])
},
/**
* 点击条文内容
*/
handleClickClauseContentItem ({ menu_id: menuId }) {
this.selectedTreeKeys = [menuId]
this.hightMenuId = menuId
const contentItemIndex = this.clauseContentList.findIndex(tt => tt.menu_id === this.hightMenuId)
this.handleProcessClauseLabelInfo(contentItemIndex)
},
/**
* 标准内容展开
*/
handleContainerExpand () {
this.isContainerExpand = !this.isContainerExpand
if (this.isContainerExpand) {
// 展开主要内容前:主要内容宽 = 当前宽度 + 条文标签宽度(未隐藏时) + 标准目录宽度(未隐藏时)
this.sheetContainerRefX = this.sheetContainerRefX + (this.showClauseLabel ? this.clauseLabelRefX : 0) + (this.isCatalogueExpand ? this.standardCatalogueRefX : 0)
this.isCatalogueExpand = false
} else {
// 收起主要内容前:主要内容宽 = 当前宽度 - 条文标签宽度(隐藏时) - 标准目录宽度(隐藏时)
this.sheetContainerRefX = this.sheetContainerRefX - (this.showClauseLabel ? this.clauseLabelRefX : 0) - (!this.isCatalogueExpand ? this.standardCatalogueRefX : 0)
this.isCatalogueExpand = true
}
},
/**
* 条文标签切换
* @param flag
*/
handleClauseChange (flag) {
const beforeFlag = this.showClauseLabel
this.showClauseLabel = flag
// 如果值改变了,就进行宽度调整
if (beforeFlag !== flag) {
// 主要内容宽 = 当前宽度 +(收起)|-(展开) 条文标签宽度
this.sheetContainerRefX = this.sheetContainerRefX + (this.clauseLabelRefX * (this.showClauseLabel ? -1 : 1))
}
},
/**
* 展开或收起条文/条文标签面板
*/
expandClauseLabel () {
this.isClauseLabelExpand = !this.isClauseLabelExpand
// TODO 右侧切换条文和条文标签折叠宽度有改的话,需要修改这里的差值
// 根据展开或收起,给主要内容减少或增加宽度
const diff = Math.abs(130 - 45)
this.sheetContainerRefX = this.sheetContainerRefX + (diff * (this.isClauseLabelExpand ? -1 : 1))
},
/**
* 展开或收起标准目录
*/
expandCatalogue () {
this.isCatalogueExpand = !this.isCatalogueExpand
// 主要内容宽 = 当前宽度 +(收起)|-(展开) 标准目录宽度
this.sheetContainerRefX = this.sheetContainerRefX + (this.standardCatalogueRefX * (this.isCatalogueExpand ? -1 : 1))
}
}
}
</script>
<style scoped lang="less">
@import '~@assets/less/common.less';
.sheet-box {
overflow-x: auto;
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
// 禁止flex自动扩充或收缩,拖拽必须要绝对单位
& > div {
flex-grow: 0;
flex-shrink: 0;
}
}
.part-ml {
margin-left: 16px;
}
.part-common {
border-radius: 8px;
background: #FFFFFF;
}
.standard-catalogue {
height: 100%;
position: relative;
transition: width .2s linear;
/deep/ .ant-tree li .ant-tree-node-content-wrapper {
max-width: calc(100% - 24px);
&:hover {
max-width: 100%;
}
}
/deep/ .ant-tree li .ant-tree-node-content-wrapper.ant-tree-node-selected {
background-color: transparent !important;
color: @primary-color;
}
}
.control-standard-catalogue-expand {
width: 12px;
height: 24px;
border-radius: 0 12px 12px 0;
background-color: @primary-color;
color: white;
position: absolute;
z-index: 3;
left: 100%;
top: calc(50% - 52px);
.anticon {
position: absolute;
top: 50%;
left: -2px;
transform: translateY(-50%);
cursor: pointer;
}
}
.expand-catalogue {
width: 280px;
}
// 拖拽会设置行内样式,这里要覆盖行内样式
.no-expand-catalogue {
width: 0 !important;
}
.sheet-container {
position: relative;
height: 100%;
width: 100%;
//flex: 1;
//width: 0;
transition: width .2s linear;
}
.clause-label {
position: relative;
width: 400px;
height: 100%;
//overflow: hidden;
transition: width .2s linear;
.clause-item {
display: flex;
margin-bottom: 20px;
label {
width: 7.5em;
font-size: 14px;
font-family: PingFang SC-Regular, PingFang SC, sans-serif;
color: #86909C;
margin-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&-content {
flex: 1;
width: 0;
white-space: pre-wrap;
word-break: break-all;
font-size: 14px;
font-family: PingFang SC-Regular, PingFang SC, sans-serif;
color: #1D2129;
}
}
}
.hide-clause-label {
width: 0 !important;
overflow: hidden;
margin-left: 0;
}
.clause-label-change {
width: 130px;
height: 100%;
padding-top: 40px;
position: relative;
transition: width .2s linear;
&-item {
height: 40px;
display: flex;
align-items: center;
padding: 0 16px;
font-size: 16px;
font-family: PingFang SC-Semibold, PingFang SC, sans-serif;
font-weight: 600;
color: #1D2129;
cursor: pointer;
.iconfont {
color: @primary-color;
}
span {
margin-left: 8px;
white-space: nowrap;
overflow: hidden;
}
}
&-item-selected {
background: #FAE6E5;
}
}
.no-expand-clause-label {
width: 45px;
min-width: 45px;
}
.part-title {
height: 40px;
background: rgba(213, 44, 38, 0.12);
border-radius: 8px 8px 0 0;
line-height: 40px;
padding: 0 24px;
font-size: 16px;
font-family: PingFang SC-Semibold, PingFang SC, sans-serif;
font-weight: 600;
color: #1D2129;
white-space: nowrap;
overflow: hidden;
}
.has-title-part-content {
height: calc(100% - 40px);
overflow: auto;
padding: 24px;
}
.standard-name {
height: 40px;
background: #FFFFFF;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
margin-bottom: 5px;
}
.standard-name > span {
font-size: 16px;
font-family: PingFang SC-Semibold, PingFang SC, sans-serif;
font-weight: 600;
color: #1D2129;
flex: 1;
width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.standard-content {
height: calc(100% - 45px);
background: #FFFFFF;
border-radius: 8px;
overflow: auto;
padding: 24px;
}
.clause-content-item {
margin-bottom: 20px;
&-title {
font-size: 18px;
font-family: PingFang SC-Medium, PingFang SC, sans-serif;
font-weight: 600;
color: #1D2129;
margin-bottom: 12px;
}
&-content {
font-size: 14px;
font-family: PingFang SC-Regular, PingFang SC, sans-serif;
font-weight: 500;
color: #1D2129;
line-height: 24px;
/deep/ table {
width: 100%;
border: 1px solid;
td {
border: 1px solid;
}
}
/deep/ img.wordImg {
width: auto !important;
max-width: 100%;
}
}
&-content-hight {
color: @primary-color;
}
}
.control-clause-label-expand {
position: absolute;
width: 12px;
height: 24px;
background-color: @primary-color;
border-radius: 12px 0 0 12px;
left: -12px;
top: calc(50% - 52px);
color: white;
cursor: pointer;
.anticon {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
}
/deep/ .ant-tree li span.ant-tree-switcher {
float: right;
}
.ant-spin-nested-loading {
height: 100%;
}
/deep/ .ant-spin-container {
height: 100%;
}
// 拖拽相关样式
.draggable-border {
position: absolute;
top: 0;
left: -3px;
height: 100% !important;
cursor: col-resize;
}
// 拖拽的线
.draggable-border.dragging::after {
content: '';
display: block;
position: absolute;
z-index: 9;
left: 0;
top: 0;
width: 1px;
height: 100%;
background-color: @primary-color;
}
// 拖拽过程中禁止选中内容,防止拖拽过程中选中内容
.disabledSelect {
user-select: none;
}
</style>
+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;
}
}
+457
View File
@@ -0,0 +1,457 @@
<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'
import { kkFilePreview } from '../utils/kkFilePreview'
const FILE_TYPE_ALL = 'all'
const FILE_TYPE_IMG = 'image'
const FILE_TYPE_IMGS = ['jpg', 'jpeg', 'png']
const FILE_TYPE_PDF = 'pdf'
// 本系统限制可以上传的文件格式
const CAN_UPLOAD_FILE_TYPE = 'pdf,docx,doc,xlsx,xls,ppt,pptx,rar,zip,jpg,jpeg,png,avi,wmv,mov,rm,mp4,cad'
const Base64 = require('js-base64').Base64
// 支持预览的文件后缀
const CAN_PREVIEW_FILE_SUFFIX = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
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
},
fileMaxSize: {
type: Number,
required: false,
default: 500
}
},
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
},
async initFileList () {
if (!this.fileIds) {
return
}
let fileIdArr = []
if (Array.isArray(this.fileIds)) {
fileIdArr = this.fileIds
} else {
fileIdArr = this.fileIds.split(',')
}
const fileList = []
for (const id of fileIdArr) {
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,
pdfId: fileInfo.pdfId,
// 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.warning(info.file.response.message)
}
// this.$message.success(`${info.file.name} 上传成功!`);
} else if (info.file.status === 'error') {
this.$message.warning(`${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
}
console.log(file)
if (file.pdfId) {
// 说明是onlyOffice生成的
const url = previewPdf(file.pdfId)
window.open(url)
return
}
// 截取文件后缀名
const fileSuffix = (file.name ? file.name.split('.')[file.name.split('.').length - 1] : '').toLowerCase()
const canPreview = 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/view/${file.response.result.id}?at=${Vue.ls.get(ACCESS_TOKEN)}&fullfilename=${file.name}`
console.log(fileSuffix)
// 图片预览,使用自己添加的组件
if (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 (FILE_TYPE_PDF.includes(fileSuffix)) {
const url = previewPdf(file.response.result.id)
window.open(url)
return
}
// 其余可预览文件仍使用KKFile进行预览
kkFilePreview(fileFullUrl)
},
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.warning(this.$t('uploadFile.cannotUploadEmpty'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileMaxSize && fileSize > 1024 * 1024 * this.fileMaxSize) {
this.$message.warning(this.$t('uploadFile.pleaseUpload') + this.fileMaxSize + this.$t('uploadFile.theFollowingDocuments'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (fileSize > 1024 * 1024 * 500) {
this.$message.warning(this.$t('uploadFile.maxSize'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileType === FILE_TYPE_ALL) {
return true
}
const fileType = file.type
// 截取文件后缀名
const fileSuffix = (file.name ? file.name.split('.')[file.name.split('.').length - 1] : '').toLowerCase()
if (this.fileType === CAN_UPLOAD_FILE_TYPE && CAN_UPLOAD_FILE_TYPE.split(',').includes(fileSuffix)) {
return true
}
if (this.fileType === FILE_TYPE_IMG && fileType.indexOf('image') < 0) {
this.$message.warning(this.$t('uploadFile.onlyUploadPic'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileType === FILE_TYPE_IMG && FILE_TYPE_IMGS.includes(fileSuffix)) {
return true
}
if (this.fileType === FILE_TYPE_IMG && !FILE_TYPE_IMGS.includes(fileSuffix)) {
this.$message.warning(this.$t('uploadFile.pleaseUpload') + FILE_TYPE_IMGS.join('、') + this.$t('uploadFile.file'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileType.indexOf(fileSuffix) === -1) {
this.$message.warning(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() !== ''))
}
+890
View File
@@ -0,0 +1,890 @@
<template>
<a-spin :spinning="loading">
<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-if="val && val.length > 0" class="header-text">{{ index }}</div>
<div v-if="val && val.length > 0" class="form-flex-box">
<template v-for="(item, index) in val">
<div :key="index"
class="box-title-text form-flex-item-half"
:class="{'form-flex-item-line': item.fieldShowType === FieldType.STANDARD_MORE.value}"
v-if="!hiddenFieldList.includes(item.dbFieldName)">
<div class="title-text">
<span class="required" v-if="item.fieldMustInput === '1' || rules[item.dbFieldName][0].required">*</span>
<span class="title-text-text" :title="item.dbFieldTxt">{{ item.dbFieldTxt }}</span>
</div>
<a-form-model-item class="item-model" :prop="item.dbFieldName">
<!-- 1, 日期单选 -->
<a-date-picker v-if="item.fieldShowType === FieldType.DATE_SINGLE.value"
class="box-input"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
:getCalendarContainer="(trigger) => trigger.parentNode"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
v-model="formInline[item.dbFieldName]"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)"
style="width: 100%" />
<!-- 2, 单选用户 -->
<user-selection v-else-if="item.fieldShowType === FieldType.USER_SINGLE.value"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
v-model="formInline[item.dbFieldName]"
:filedName="item.dbFieldName"
@nameChange="userSelectNameChange"
:nameStr="formInline[item.dbFieldName + '_dictText']"
type="radio"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)" />
<!-- 3, 单选组织机构 -->
<j-select-depart v-else-if="item.fieldShowType === FieldType.ORGAN_SINGLE.value"
v-model="formInline[item.dbFieldName]"
:backDepart="true" />
<!-- 4, 日期多选 -->
<j-multiple-date-picker v-else-if="item.fieldShowType === FieldType.DATE_MORE.value"
class="box-input"
v-model="formInline[item.dbFieldName]"
:fieldName="item.dbFieldName"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)" />
<!-- 5, 多行文本 -->
<a-textarea v-else-if="item.fieldShowType === FieldType.TEXTAREA.value"
:placeholder="$t('pleaseEnter')+item.dbFieldTxt"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)"
:maxLength="500"
v-model="formInline[item.dbFieldName]" :rows="4" />
<!-- 6, 标准多选 -->
<standard-selection v-else-if="item.fieldShowType === FieldType.STANDARD_MORE.value"
:placeholder="specialPlaceholder[item.dbFieldName] || $t('pleaseSelectStandard')"
:source="getFlag(item.dbFieldName)"
:disabledSourceSearch="!selectStandardCanUpdateTypeField.includes(item.dbFieldName)"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)"
:exclude-standard-numbers="excludeStandardFields.includes(item.dbFieldName) ? excludeStandardNumbers : null"
v-model="formInline[item.dbFieldName]" />
<!-- 7, 多选组织机构 -->
<j-select-depart v-else-if="item.fieldShowType === FieldType.ORGAN_MORE.value"
v-model="formInline[item.dbFieldName]"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName) || innerDisabledField.includes(item.dbFieldName)"
:selectRange="item.dbFieldName === 'auth_dept' ? departSelectRange : []"
:multi="true"
:backDepart="true"
:treeOpera="true" />
<!-- 8, 上传文件 -->
<a-button v-else-if="item.fieldShowType === FieldType.FILE_UP.value" type="primary" class="button-text"
@click="clickButtonToUpload(item)">
{{
(formInline[item.dbFieldName] === 'null' || formInline[item.dbFieldName] === ''
|| formInline[item.dbFieldName] === null || formInline[item.dbFieldName] === undefined)
? $t('uploadFile.clickUpload')
: $t('uploadFile.viewUploadedFiles')
}}
</a-button>
<!-- 9, 文本框 -->
<a-input v-else-if="item.fieldShowType === FieldType.TEXT_BOX.value"
class="box-input"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)"
v-model="formInline[item.dbFieldName]"
:maxLength="item.dbLength"
:placeholder="specialPlaceholder[item.dbFieldName] || $t('pleaseEnter')+item.dbFieldTxt" />
<!-- 10, 下拉单选数据字典 -->
<j-dict-select-tag v-else-if="item.fieldShowType === FieldType.OPTION_SINGLE.value"
class="box-input"
v-model="formInline[item.dbFieldName]"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName) || innerDisabledField.includes(item.dbFieldName)"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
:type="'select'"
:triggerChange="false"
:options="optionsData[item.dbFieldName]"
:dictCode="item.fieldHref || dictNotCodes.includes(item.dbFieldName) ? null : item.dictField" />
<!-- 11, 下拉多选数据字典 -->
<j-multi-select-tag v-else-if="item.fieldShowType === FieldType.OPTION_MORE.value"
class="box-input"
v-model="formInline[item.dbFieldName]"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
:type="'select'"
:triggerChange="false"
:options="optionsData[item.dbFieldName]"
:openSelectedAll="openSelectedAllFieldList.includes(item.dbFieldName)"
:dictCode="item.fieldHref ? null : item.dictField" />
<!-- 12, 树选择 -->
<s-tree-select
v-else-if="item.fieldShowType === FieldType.TREE.value"
v-model="formInline[item.dbFieldName]"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)"
:tree-data="optionsData[item.dbFieldName]"
tree-checkable
treeCheckStrictly
:placeholder="$t('pleaseSelect')+item.dbFieldTxt" />
<!-- 14, 年份选择 -->
<j-date v-else-if="item.fieldShowType === FieldType.YEAR_PICKER.value"
show-type="year"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)"
v-model="formInline[item.dbFieldName]"
:default-value="defaultValue[item.dbFieldName]"
date-format="YYYY"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt" />
<!-- 15, 树选择单选 -->
<s-tree-select v-else-if="item.fieldShowType === FieldType.TREE_SINGLE.value"
v-model="formInline[item.dbFieldName]"
:getPopupContainer="triggerNode=> triggerNode.parentNode"
style="width: 100%"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)"
:tree-data="optionsData[item.dbFieldName]"
:label-in-value="false"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt" />
<!--16树形弹框选择-->
<tree-selection v-else-if="item.fieldShowType === FieldType.TREE_MODAL_SELECT.value"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
v-model="formInline[item.dbFieldName]"
@nameChange="treeSelectNameChange"
:filedName="item.dbFieldName"
type="checkbox"
:options-href="item.fieldHref"
:nameStr="formInline[item.dbFieldName + '_dictText']"
:dict-id="item.dictId"
:disabled="disabled || disabledFieldList.includes(item.dbFieldName)" />
</a-form-model-item>
</div>
</template>
</div>
</div>
<upload-file ref="uploadFile" @change="uploadFileChange" :return-url="false"></upload-file>
</a-form-model>
</a-spin>
</template>
<script>
import '@assets/less/common.less'
import JDictSelectTag from '../dict/JDictSelectTag'
import JMultiSelectTag from '../dict/JMultiSelectTag'
import UploadFile from '../UploadFile'
// 选择标准选择器
import StandardSelection from '../selection/StandardSelection'
import {
FieldType,
StandardGradeClassify,
StandardProperty,
IsTrial,
EnterpriseStandardStatus,
StandardSource,
StandardStatus, EnterpriseStandardCode, EnterpriseNameCode
} from '../../enums/commonEnums'
import JMultipleDatePicker from '../JMultipleDatePicker'
import UserSelection from '../selection/UserSelection'
import { getAction } from '../../api/manage.js'
import { ajaxGetDictItems, getDictByCode, getFormDictTreeList } from '../../api/api.js'
import TreeSelection from '../selection/TreeSelection'
import { getStandardGradeClassification } from '../../api/enterpriseStandardLibraryApi'
import STreeSelect from '../STreeSelect'
export default {
name: 'AddForm',
components: {
UserSelection,
JMultipleDatePicker,
StandardSelection,
JDictSelectTag,
JMultiSelectTag,
UploadFile,
TreeSelection,
STreeSelect
},
props: {
url: {
type: Object,
default: () => {
return {}
}
},
// 获取表单数据的方法
getFormFunc: {
type: Function,
required: false,
default: () => {
return function () {
}
}
},
// 获取详细信息的方法
getDocumentInfoFunc: {
type: Function,
required: false,
default: () => {
return function () {
}
}
},
// 标准模块的区分:1,国内; 2,国外;3,企标
flag: {
type: String,
default: null
},
disabled: {
type: Boolean,
default: false
},
extraParams: {
type: Object,
default: () => {
return {}
}
},
// 特殊的placeholder,目前只处理了输入框,有需要再处理别的
specialPlaceholder: {
type: Object,
required: false,
default: () => {
return {}
}
},
// 默认值
defaultValue: {
type: Object,
required: false,
default: () => {
return {}
}
},
// 不可修改值
disabledFieldList: {
type: Array,
required: false,
default: () => {
return []
}
},
// 数据字典多选,需要全选的字段列表
openSelectedAllFieldList: {
type: Array,
required: false,
default: () => {
return []
}
},
// 需要隐藏的字段
hiddenFieldList: {
type: Array,
required: false,
default: () => {
return []
}
},
// 不包含的标准从哪个字段取值
excludeStandardValFormFields: {
type: String,
required: false,
default: 'standard_number'
},
// 需要处理不包含标准的字段
excludeStandardFields: {
type: [Array],
required: false,
default: () => {
return []
}
},
// 是否需要进行国内标准的特殊校验
domesticSpecialCheck: {
type: Boolean,
required: false,
default: false
},
// 选择标准字段和选择来源的对应关系
flagByField: {
type: Object,
required: false,
default: () => {
return {}
}
},
// 选择标准组件可以修改来源的字段(暂时只有企标的引用标准用到)
selectStandardCanUpdateTypeField: {
type: Array,
required: false,
default: () => {
return []
}
}
},
mounted () {
// 如果是企标就获取企标级别映射数据
if (this.flag === StandardSource.ENTERPRISE.value) {
getStandardGradeClassification().then(res => {
if (res.success) {
this.standardGradeClassification = res.result
}
})
}
},
data () {
return {
FieldType,
StandardSource,
loading: false,
formInline: {},
isFormInline: false,
rules: {},
confirmLoading: false,
type: 2,
dataList: [],
ruleList: [],
uploadName: '',
optionsData: {}, // 用接口获取到的下拉数据
excludeStandardNumbers: '', // 不包含的标准号
standardGradeClassification: [], // 企标级别映射
innerDisabledField: [], // 内部不可编辑字段
departSelectRange: [], // 选择部门的选择范围,传了就只能从这些里面选,不传就都能选
isEditSetData: false, // 正在编辑回显数据
oldMainDraftingUnit: '', // 上一次的主起草单位
dictNotCodes: ['name_code'] // 用到字典的字段不传dictCode的字段
}
},
watch: {
// 国内的零部件分类、适用认证、适用车型和标准性质关联,准入标准必填,非准入标准不必填
'formInline.standard_property' () {
this.handleDomesticSpecialCheck()
this.handleStatePropertyCheck()
},
// 标准等级分类改变后授权部门需要自动带出
'formInline.grade_classification' (val) {
if (val && this.flag === StandardSource.ENTERPRISE.value && !this.isEditSetData) {
if (val === StandardGradeClassify.ZERO_LEVEL.value) {
// 选的是0级,授权部门带出不可编辑
this.formInline.auth_dept = this.standardGradeClassification.find(item => item.level + '' === '0').deptIds
if (!this.innerDisabledField.includes('auth_dept')) {
this.innerDisabledField.push('auth_dept')
}
} else if (val === StandardGradeClassify.ONE_LEVEL.value) {
// 选的是1级
// // 带出3级的部门:起草部门
// this.formInline.auth_dept = this.standardGradeClassification.find(item => item.level + '' === '3').deptIds
// 带出主起草单位
this.formInline.auth_dept = this.formInline.main_drafting_unit
// 设置可选择项
this.departSelectRange = this.standardGradeClassification.find(item => item.level + '' === '1').deptIds.split(',')
this.formInline = JSON.parse(JSON.stringify(this.formInline))
// 设置授权部门可编辑
if (this.innerDisabledField.includes('auth_dept')) {
this.innerDisabledField = this.innerDisabledField.filter(item => item !== 'auth_dept')
}
} else if (val === StandardGradeClassify.TWO_LEVEL.value) {
// 选的是2级
// // 带出3级的部门:起草部门
// this.formInline.auth_dept = this.standardGradeClassification.find(item => item.level + '' === '3').deptIds
// 带出主起草单位
this.formInline.auth_dept = this.formInline.main_drafting_unit
// 设置可选择项
this.departSelectRange = this.standardGradeClassification.find(item => item.level + '' === '2').deptIds.split(',')
this.formInline = JSON.parse(JSON.stringify(this.formInline))
// 设置授权部门可编辑
if (this.innerDisabledField.includes('auth_dept')) {
this.innerDisabledField = this.innerDisabledField.filter(item => item !== 'auth_dept')
}
} else if (val === StandardGradeClassify.THREE_LEVEL.value) {
// // 选的是3级,授权部门带出不可编辑
// this.formInline.auth_dept = this.standardGradeClassification.find(item => item.level + '' === '3').deptIds
// 选的是3级,带出主起草单位
this.formInline.auth_dept = this.formInline.main_drafting_unit
if (!this.innerDisabledField.includes('auth_dept')) {
this.innerDisabledField.push('auth_dept')
}
}
} else if (val && this.flag === StandardSource.ENTERPRISE.value && this.isEditSetData) {
if (val === StandardGradeClassify.ZERO_LEVEL.value) {
// 选的是0级,授权部门带出不可编辑
if (!this.innerDisabledField.includes('auth_dept')) {
this.innerDisabledField.push('auth_dept')
}
} else if (val === StandardGradeClassify.ONE_LEVEL.value) {
// 选的是1级
// 设置可选择项
this.departSelectRange = this.standardGradeClassification.find(item => item.level + '' === '1').deptIds.split(',')
// 设置授权部门可编辑
if (this.innerDisabledField.includes('auth_dept')) {
this.innerDisabledField = this.innerDisabledField.filter(item => item !== 'auth_dept')
}
} else if (val === StandardGradeClassify.TWO_LEVEL.value) {
// 选的是2级
// 设置可选择项
this.departSelectRange = this.standardGradeClassification.find(item => item.level + '' === '2').deptIds.split(',')
// 设置授权部门可编辑
if (this.innerDisabledField.includes('auth_dept')) {
this.innerDisabledField = this.innerDisabledField.filter(item => item !== 'auth_dept')
}
} else if (val === StandardGradeClassify.THREE_LEVEL.value) {
// 选的是3级,授权部门带出不可编辑
if (!this.innerDisabledField.includes('auth_dept')) {
this.innerDisabledField.push('auth_dept')
}
}
} else if (!val && this.flag === StandardSource.ENTERPRISE.value && !this.isEditSetData) {
// 清空授权部门的可选项
this.departSelectRange = []
// 设置授权部门可编辑
if (this.innerDisabledField.includes('auth_dept')) {
this.innerDisabledField = this.innerDisabledField.filter(item => item !== 'auth_dept')
}
// 清空授权部门
this.formInline.auth_dept = ''
}
},
// 主起草单位改变后,需要将选中的主起草单位增加到授权部门
'formInline.main_drafting_unit' (val) {
if (val) {
if (this.formInline.grade_classification === StandardGradeClassify.ONE_LEVEL.value ||
this.formInline.grade_classification === StandardGradeClassify.TWO_LEVEL.value) {
// 标准等级分类是1级或者2级需要,把主起草单位追加到授权部门
let authDeptArr = (this.formInline.auth_dept || '').split(',')
// 把旧的主起草部门刨出去,否则每换一次主起草单位都会累计到授权部门
authDeptArr = authDeptArr.filter(item => item !== this.oldMainDraftingUnit)
authDeptArr.push(val)
this.formInline.auth_dept = authDeptArr.join(',')
} else if (this.formInline.grade_classification === StandardGradeClassify.THREE_LEVEL.value) {
// 标准等级分类是3级,把授权部门替换成主起草单位
this.formInline.auth_dept = val
}
this.oldMainDraftingUnit = val
}
},
// 企标是否试行改变,标准状态需要跟着变,试行周期必填校验也跟着变
'formInline.try_flag' (val) {
if (val && this.flag === StandardSource.ENTERPRISE.value && !this.isEditSetData) {
if (val === IsTrial.YES.value) {
// 试行,标准状态改为试运行,
this.formInline.standard_state = EnterpriseStandardStatus.TRIAL_OPERATION.value
// 试行周期给默认值24
this.formInline.trial_period = '24'
this.formInline = JSON.parse(JSON.stringify(this.formInline))
// 试行,标准状态不可修改
if (!this.innerDisabledField.includes('standard_state')) {
this.innerDisabledField.push('standard_state')
}
// 试行,试行周期必填
this.rules.trial_period[0].required = true
} else {
// 试行周期置空
this.formInline.trial_period = undefined
// 不试行,标准状态可修改
if (this.innerDisabledField.includes('standard_state')) {
this.innerDisabledField = this.innerDisabledField.filter(item => item !== 'standard_state')
}
// 不试行,试行周期非必填
this.rules.trial_period[0].required = false
}
} else if (val && this.flag === StandardSource.ENTERPRISE.value && this.isEditSetData) {
if (val === IsTrial.YES.value) {
// 编辑回显得时候改变,设置标准状态不可编辑,试行周期必填
// 试行,试行周期必填
this.rules.trial_period[0].required = true
} else {
// 不试行,试行周期非必填
this.rules.trial_period[0].required = false
}
}
},
// 编辑回显标准状态是废止,标准状态不可更改
'formInline.standard_state' (val) {
if (val && this.isEditSetData && (
(this.flag === StandardSource.ENTERPRISE.value && val === EnterpriseStandardStatus.ABOLISH.value) ||
(this.flag === StandardSource.DOMESTIC.value && val === StandardStatus.ABOLISH.value) ||
(this.flag === StandardSource.OVERSEAS.value && val === StandardStatus.ABOLISH.value))) {
// 状态是废止
if (!this.innerDisabledField.includes('standard_state')) {
this.innerDisabledField.push('standard_state')
}
} else if (this.formInline.try_flag === IsTrial.YES.value && this.flag === StandardSource.ENTERPRISE.value && this.isEditSetData) {
// 企标,是否试行选的是,需要标准状态不可编辑
if (!this.innerDisabledField.includes('standard_state')) {
this.innerDisabledField.push('standard_state')
}
} else {
// 状态不是废止
if (this.innerDisabledField.includes('standard_state')) {
this.innerDisabledField = this.innerDisabledField.filter(item => item !== 'standard_state')
}
}
// 起草、征求依据、报批、废止状态时发布日期、实施日期不必填
this.handleRequiredByState(val)
this.handleStatePropertyCheck()
},
// 企标:企业标准代号选Q,企业名称代号只能选ZZ\ZR;标准代号选TS,企业名称代号只能选ZZ
'formInline.standard_code' (val) {
if (this.flag === StandardSource.ENTERPRISE.value && !this.isEditSetData) {
if (val === EnterpriseStandardCode.Q.value) {
// 选的Q,企业名称代号只能选ZZ\ZR
this.optionsData.name_code.map(item => {
if (![EnterpriseNameCode.ZZ.value, EnterpriseNameCode.ZR.value].includes(item.value)) {
item.disabled = true
} else {
item.disabled = false
}
})
} else if (val === EnterpriseStandardCode.TS.value) {
// 选的TS,企业名称代号只能选ZZ
this.optionsData.name_code.map(item => {
if (![EnterpriseNameCode.ZZ.value].includes(item.value)) {
item.disabled = true
} else {
item.disabled = false
}
})
}
if (this.formInline.name_code !== EnterpriseNameCode.ZZ.value) {
this.formInline.name_code = undefined
this.formInline = Object.assign({}, this.formInline)
}
}
}
},
methods: {
add () {
this.formInline = Object.assign({}, this.defaultValue)
this.$forceUpdate()
this.getForm()
},
edit (item, callback) {
this.loading = true
this.getForm(() => {
if (!this.getDocumentInfoFunc || typeof this.getDocumentInfoFunc !== 'function') {
this.loading = false
return
}
// type标识是新增表单还是详情页
this.getDocumentInfoFunc({ id: item.id, type: this.type }).then((res) => {
if (res.success) {
// 设置正在编辑回显值
this.isEditSetData = true
// 处理树结构多选回显
const formList = Object.values(this.dataList).reduce((acc, cur) => acc.concat(cur), [])
const treeMultipleField = formList.filter(tt => tt.fieldShowType === FieldType.TREE.value)
this.formInline = res.result
callback && callback(this.formInline)
treeMultipleField.forEach(field => {
const valueArr = []
if (this.formInline[field.dbFieldName]) {
const values = this.formInline[field.dbFieldName].split(',')
for (let i = 0; i < values.length; i++) {
valueArr[i] = {
value: values[i]
}
}
this.formInline[field.dbFieldName] = valueArr
console.log(this.formInline[field.dbFieldName])
} else {
this.formInline[field.dbFieldName] = undefined
}
})
const dictField = formList.filter(tt => tt.fieldShowType === FieldType.OPTION_SINGLE.value)
dictField.forEach(field => {
this.formInline[field.dbFieldName] = this.formInline[field.dbFieldName] || null
})
// 处理标准选择不能选自己
this.excludeStandardNumbers = this.formInline[this.excludeStandardValFormFields]
this.handleDomesticSpecialCheck()
this.loading = false
} else {
this.loading = false
}
}).finally(() => {
// 设置编辑回显值完成
this.isEditSetData = false
})
})
},
// 获取表单列表
getForm (callBack) {
if (!this.getFormFunc || typeof this.getFormFunc !== 'function') {
return
}
this.confirmLoading = true
const params = {
module: this.flag,
type: this.type
}
this.getFormFunc(params).then((res) => {
if (res.success) {
this.dataList = res.result
this.ruleList = res.result
this.integrateData()
this.getOptionsData() // 获取下拉数据
callBack && callBack()
}
})
},
// 设置表单校验
integrateData () {
this.isFormInline = false
const ruleList = []
for (const item in this.ruleList) {
this.ruleList[item].forEach(ol => {
ruleList.push(ol)
})
}
const rules = {}
ruleList.forEach((res, index) => {
const rule = []
if (res.fieldMustInput === '1') {
if ([FieldType.TEXT_BOX.value, FieldType.TEXTAREA.value].includes(res.fieldShowType)) {
rule.push({
required: true,
message: res.dbFieldTxt + this.$t('cannotEmpty'),
trigger: 'blur'
})
} else {
rule.push({
required: true,
message: res.dbFieldTxt + this.$t('cannotEmpty'),
trigger: 'change'
})
}
} else {
if ([FieldType.TEXT_BOX.value, FieldType.TEXTAREA.value].includes(res.fieldShowType)) {
rule.push({
required: false,
message: res.dbFieldTxt + this.$t('cannotEmpty'),
trigger: 'blur'
})
} else {
rule.push({
required: false,
message: res.dbFieldTxt + this.$t('cannotEmpty'),
trigger: 'change'
})
}
}
if (rule.length > 0) {
rules[res.dbFieldName] = rule
}
})
this.rules = rules
this.isFormInline = true
this.confirmLoading = false
},
/**
* 处理需要通过接口获取的下拉数据
*/
getOptionsData () {
const formList = Object.values(this.dataList).reduce((acc, cur) => acc.concat(cur), [])
formList.forEach(item => {
// 如果接口这个字段有值
if (item.fieldHref) {
getAction(item.fieldHref).then(res => {
if (res.success) {
this.$set(this.optionsData, item.dbFieldName, res.result)
}
})
} else if ((item.fieldShowType === FieldType.TREE.value || item.fieldShowType === FieldType.TREE_SINGLE.value) && item.dictId) {
getFormDictTreeList({ dictId: item.dictId }).then(res => {
if (res.success) {
this.$set(this.optionsData, item.dbFieldName, res.result)
}
})
} else if (item.fieldShowType === FieldType.OPTION_SINGLE.value && this.dictNotCodes.includes(item.dbFieldName)) {
ajaxGetDictItems(item.dictField).then(res => {
if (res.success) {
const result = JSON.parse(JSON.stringify(res.result))
if (item.dbFieldName === 'name_code') {
for (const dictItem of result) {
if ([EnterpriseNameCode.WQ.value, EnterpriseNameCode.JC.value, EnterpriseNameCode.JHK.value].includes(dictItem.value)) {
dictItem.disabled = true
} else {
dictItem.disabled = false
}
}
}
console.log('获取企业名称代号下拉框数据成功了', result)
this.$set(this.optionsData, item.dbFieldName, result)
}
})
}
})
},
// 点击点击上传按钮
clickButtonToUpload (item) {
this.$refs.uploadFile.open(this.formInline[item.dbFieldName])
this.uploadName = item.dbFieldName
},
// 文件上传改变后的回调
uploadFileChange (data) {
const attIdList = []
const attNameList = []
if (data && data.length > 0) {
data.map(item => {
attIdList.push(item.id)
})
data.map(item => {
attNameList.push(item.fileName)
})
this.$refs.ruleForm.clearValidate(this.uploadName)
}
console.log(data)
/** 赋值给当前对应的表单文件 */
this.formInline[this.uploadName] = attIdList.join(',')
this.formInline[this.uploadName + '_dictText'] = attNameList.join(',')
this.formInline = { ...this.formInline }
},
// 标准选择变化
standardSelectionChange (value, id) {
// this.formInline[value + '_by'] = id
this.formInline = { ...this.formInline }
},
// 选择用户得到用户名
userSelectNameChange (value, fieldName) {
this.formInline[fieldName + '_dictText'] = value
},
treeSelectNameChange (value, fieldName) {
this.formInline[fieldName + '_dictText'] = value
},
submit () {
this.$refs.ruleForm.validate(valid => {
if (valid) {
const formInline = { ...this.formInline }
Object.keys(formInline).forEach(res => {
if (formInline[res] instanceof Array) {
if (formInline[res][0] instanceof Object) {
// 说明是树选择
formInline[res] = formInline[res].map(item => item.value).join(',')
} else {
formInline[res] = formInline[res].join(',')
}
} else if (typeof formInline[res] === 'undefined') {
// 说明是undefined,改成空字符串,为了解决下拉框改成undefined时,不会传给后端该字段,改不了下拉框值的问题
formInline[res] = ''
}
})
this.$emit('submitFormData', formInline)
} else {
this.$emit('addFormWarning')
return false
}
})
},
clearValidate (fieldNameList) {
this.$refs.ruleForm.clearValidate(fieldNameList)
},
/**
* 标准性质是准入,状态为发布、即将实施和现行有效的时候 新车型实施日期和在产车的实施日期 都改成必填
*/
handleStatePropertyCheck () {
const mustFillFields = ['new_model_implementation_date', 'on_the_production_date']
const rules = JSON.parse(JSON.stringify(this.rules))
if (this.formInline.standard_property === StandardProperty.admittance.value && (
this.formInline.standard_state === StandardStatus.RELEASE.value ||
this.formInline.standard_state === StandardStatus.BE_IMPLEMENTED_SOON.value ||
this.formInline.standard_state === StandardStatus.CURRENTLY_EFFECTIVE.value)) {
mustFillFields.forEach(key => {
if (rules[key] && rules[key].length > 0) {
rules[key][0].required = true
const dataPart = Object.keys(this.dataList).find(tt => this.dataList[tt].some(item => item.dbFieldName === key))
const index = this.dataList[dataPart].findIndex(tt => tt.dbFieldName === key)
this.dataList[dataPart][index].fieldMustInput = '1'
}
})
} else {
mustFillFields.forEach(key => {
if (rules[key] && rules[key].length > 0) {
rules[key][0].required = false
const dataPart = Object.keys(this.dataList).find(tt => this.dataList[tt].some(item => item.dbFieldName === key))
const index = this.dataList[dataPart].findIndex(tt => tt.dbFieldName === key)
this.dataList[dataPart][index].fieldMustInput = '0'
}
})
}
this.rules = Object.assign({}, rules)
this.clearValidate(mustFillFields)
},
/**
* 国内海外的特殊处理
*/
handleDomesticSpecialCheck () {
if (this.domesticSpecialCheck) {
const fieldNameList = ['part_name', 'applicable_certification', 'applicable_vehicle', 'responsible_department']
const rules = JSON.parse(JSON.stringify(this.rules))
if (this.formInline.standard_property === StandardProperty.admittance.value) {
fieldNameList.forEach(key => {
if (rules[key] && rules[key].length > 0) {
rules[key][0].required = true
const dataPart = Object.keys(this.dataList).find(tt => this.dataList[tt].some(item => item.dbFieldName === key))
const index = this.dataList[dataPart].findIndex(tt => tt.dbFieldName === key)
this.dataList[dataPart][index].fieldMustInput = '1'
}
})
} else {
fieldNameList.forEach(key => {
if (rules[key] && rules[key].length > 0) {
rules[key][0].required = false
const dataPart = Object.keys(this.dataList).find(tt => this.dataList[tt].some(item => item.dbFieldName === key))
const index = this.dataList[dataPart].findIndex(tt => tt.dbFieldName === key)
this.dataList[dataPart][index].fieldMustInput = '0'
}
})
}
this.rules = Object.assign({}, rules)
this.clearValidate(fieldNameList)
}
},
getFlag (fieldName) {
return this.flagByField[fieldName] || this.flag
},
filterTreeOption (input, option) {
const text = option.componentOptions.propsData.title || option.componentOptions.propsData.label
return text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
/**
* 标准状态为起草、征求意见、报批等状态时发布日期、实施日期不应是必填项
* @param val
*/
handleRequiredByState (val) {
let notRequiredStatus = [], fields = []
// 国内和海外的标准状态是同一个数据字典
if (this.flag === StandardSource.DOMESTIC.value || this.flag === StandardSource.OVERSEAS.value) {
notRequiredStatus = [StandardStatus.DRAFT.value, StandardStatus.SEEK_FOR_OPTIONS.value, StandardStatus.EXAMINE.value, StandardStatus.APPROVAL.value, StandardStatus.ABOLISH.value]
fields = ['release_date', 'implementation_date']
} else { // 处理企标
notRequiredStatus = [EnterpriseStandardStatus.DRAFT.value, EnterpriseStandardStatus.SEEK_FOR_OPTIONS.value, EnterpriseStandardStatus.EXAMINE.value, EnterpriseStandardStatus.APPROVAL.value]
fields = ['release_date', 'implementation_date']
}
const rules = JSON.parse(JSON.stringify(this.rules))
if (notRequiredStatus.includes(val)) {
fields.forEach(key => {
if (rules[key] && rules[key].length > 0) {
rules[key][0].required = false
const dataPart = Object.keys(this.dataList).find(tt => this.dataList[tt].some(item => item.dbFieldName === key))
const index = this.dataList[dataPart].findIndex(tt => tt.dbFieldName === key)
this.dataList[dataPart][index].fieldMustInput = '0'
}
})
} else {
fields.forEach(key => {
if (rules[key] && rules[key].length > 0) {
rules[key][0].required = true
const dataPart = Object.keys(this.dataList).find(tt => this.dataList[tt].some(item => item.dbFieldName === key))
const index = this.dataList[dataPart].findIndex(tt => tt.dbFieldName === key)
this.dataList[dataPart][index].fieldMustInput = '1'
}
})
}
this.rules = Object.assign({}, rules)
this.clearValidate(fields)
}
}
}
</script>
<style scoped lang="less">
/deep/ .ant-form-item-children {
width: 100%;
display: inline-block;
}
.form-flex-box {
display: flex;
flex-wrap: wrap;
}
.form-flex-item-half {
width: 50%;
}
.form-flex-item-line {
width: 100%;
}
</style>
+447
View File
@@ -0,0 +1,447 @@
<template>
<a-form layout="inline" @keyup.enter.native="searchQuery">
<a-row :gutter="24">
<a-col :md="(24 / (minLength + 1))"
:sm="(24 / minLength)"
v-for="(item) in searchList.slice(0, minLength)"
:key="item.id"
style="line-height: 48px">
<!-- 树形选择器 -->
<template v-if="item.fieldShowType === FieldType.TREE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-tree-select
tree-node-filter-prop="title"
v-model="queryParam[item.dbFieldName]"
:maxTagCount="1"
:getPopupContainer="triggerNode=> triggerNode.parentNode"
:tree-data="optionsData[item.dbFieldName]"
tree-checkable
show-search
allowClear
:filterTreeNode="filterTreeOption"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
/>
</a-form-item>
</template>
<!-- 树形选择器单选 -->
<template v-if="item.fieldShowType === FieldType.TREE_SINGLE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-tree-select
tree-node-filter-prop="title"
v-model="queryParam[item.dbFieldName]"
:maxTagCount="1"
:getPopupContainer="triggerNode=> triggerNode.parentNode"
:tree-data="optionsData[item.dbFieldName]"
show-search
allowClear
:filterTreeNode="filterTreeOption"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
/>
</a-form-item>
</template>
<!-- 输入框 -->
<template
v-if="[FieldType.TEXT_BOX.value, FieldType.TEXTAREA.value,FieldType.USER_SINGLE.value,
FieldType.STANDARD_MORE.value,FieldType.TREE_MODAL_SELECT.value]
.includes(item.fieldShowType)">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-input :placeholder="$t('pleaseEnter')+item.dbFieldTxt"
v-model="queryParam[item.dbFieldName]"></a-input>
</a-form-item>
</template>
<!-- 数字输入框 -->
<!-- <template v-else-if="item.fieldShowType === FieldType.TEXT_NUMBER.value">-->
<!-- <a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">-->
<!-- <a-input-number :placeholder="$t('pleaseEnter')+item.dbFieldTxt"-->
<!-- v-model="queryParam[item.dbFieldName]" :min="1" :max="99999999"/>-->
<!-- </a-form-item>-->
<!-- </template>-->
<!-- 日期区间 -->
<template v-else-if="[FieldType.DATE_SINGLE.value, FieldType.DATE_MORE.value].includes(item.fieldShowType)">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-range-picker v-model="queryParam[item.dbFieldName]"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
format="YYYY-MM-DD" value-format="YYYY-MM-DD"
@change="onChange(item.dbFieldName)"></a-range-picker>
</a-form-item>
</template>
<!-- 字典单选框 -->
<template v-else-if="item.fieldShowType === FieldType.OPTION_SINGLE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<j-dict-select-tag v-model="queryParam[item.dbFieldName]"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
:type="'select'"
:triggerChange="false" :dictCode="item.dictField" />
</a-form-item>
</template>
<!-- 字典多选框 -->
<template v-else-if="item.fieldShowType === FieldType.OPTION_MORE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<j-multi-select-tag v-model="queryParam[item.dbFieldName]"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
:type="'select'" :maxTagCount="1"
:triggerChange="false" :dictCode="item.dictField" />
</a-form-item>
</template>
<!-- 组织机构选择 -->
<template v-else-if="item.fieldShowType === FieldType.ORGAN_SINGLE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-tree-select
tree-node-filter-prop="title"
v-model="queryParam[item.dbFieldName]"
:maxTagCount="1"
allowClear
:getPopupContainer="triggerNode=> triggerNode.parentNode"
class="box-input"
style="width: 100%"
:tree-data="categoryTreeList"
show-search
:filterTreeNode="filterTreeOption"
:placeholder="$t('pleaseSelect') + item.dbFieldTxt"
/>
</a-form-item>
</template>
<!-- 组织机构多选 -->
<template v-else-if="item.fieldShowType === FieldType.ORGAN_MORE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-tree-select
tree-node-filter-prop="title"
v-model="queryParam[item.dbFieldName]"
:maxTagCount="1"
allowClear
:getPopupContainer="triggerNode=> triggerNode.parentNode"
class="box-input"
style="width: 100%"
:tree-data="categoryTreeList"
:placeholder="$t('pleaseSelect') + item.dbFieldTxt"
treeCheckable
treeCheckStrictly
show-search
:filterTreeNode="filterTreeOption"
/>
</a-form-item>
</template>
<!-- 年份选择 -->
<template
v-if="item.fieldShowType === FieldType.YEAR_PICKER.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<j-date show-type="year"
:allowClear="false"
v-model="queryParam[item.dbFieldName]"
date-format="YYYY"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt" />
</a-form-item>
</template>
</a-col>
<template v-if="searchList.length > minLength && toggleSearchStatus">
<a-col :md="(24 / (minLength + 1))"
:sm="(24 / minLength)"
v-for="(item) in searchList.slice(minLength)"
:key="item.id"
style="line-height: 48px">
<!-- 树形选择器 -->
<template v-if="item.fieldShowType === FieldType.TREE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-tree-select
tree-node-filter-prop="title"
v-model="queryParam[item.dbFieldName]"
:maxTagCount="1"
:getPopupContainer="triggerNode=> triggerNode.parentNode"
:tree-data="optionsData[item.dbFieldName]"
tree-checkable
treeCheckStrictly
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
show-search
allowClear
:filterTreeNode="filterTreeOption"
/>
</a-form-item>
</template>
<!-- 树形选择器(单选) -->
<template v-if="item.fieldShowType === FieldType.TREE_SINGLE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-tree-select
tree-node-filter-prop="title"
v-model="queryParam[item.dbFieldName]"
:maxTagCount="1"
:getPopupContainer="triggerNode=> triggerNode.parentNode"
:tree-data="optionsData[item.dbFieldName]"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
show-search
allowClear
:filterTreeNode="filterTreeOption"
/>
</a-form-item>
</template>
<!-- 输入框 -->
<template v-if="[FieldType.TEXT_BOX.value, FieldType.TEXTAREA.value,FieldType.USER_SINGLE.value,
FieldType.STANDARD_MORE.value,FieldType.TREE_MODAL_SELECT.value]
.includes(item.fieldShowType)">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-input :placeholder="$t('pleaseEnter')+item.dbFieldTxt"
v-model="queryParam[item.dbFieldName]"></a-input>
</a-form-item>
</template>
<!-- 组织机构单选 -->
<template v-else-if="item.fieldShowType === FieldType.ORGAN_SINGLE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-tree-select
tree-node-filter-prop="title"
v-model="queryParam[item.dbFieldName]"
:maxTagCount="1"
:getPopupContainer="triggerNode=> triggerNode.parentNode"
class="box-input"
style="width: 100%"
:tree-data="categoryTreeList"
:placeholder="$t('pleaseSelect') + item.dbFieldTxt"
show-search
allowClear
:filterTreeNode="filterTreeOption"
/>
</a-form-item>
</template>
<!-- 组织机构多选 -->
<template v-else-if="item.fieldShowType === FieldType.ORGAN_MORE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-tree-select
tree-node-filter-prop="title"
v-model="queryParam[item.dbFieldName]"
:maxTagCount="1"
:show-search="true"
allowClear
:getPopupContainer="triggerNode=> triggerNode.parentNode"
class="box-input"
style="width: 100%"
:tree-data="categoryTreeList"
:placeholder="$t('pleaseSelect') + item.dbFieldTxt"
treeCheckable
treeCheckStrictly
:filterTreeNode="filterTreeOption"
/>
</a-form-item>
</template>
<!-- 日期区间选择器 -->
<template v-else-if="[FieldType.DATE_SINGLE.value, FieldType.DATE_MORE.value].includes(item.fieldShowType)">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-range-picker v-model="queryParam[item.dbFieldName]"
format="YYYY-MM-DD" value-format="YYYY-MM-DD"
@change="onChange(item.dbFieldName)"></a-range-picker>
</a-form-item>
</template>
<!-- 字典单选 -->
<template v-else-if="item.fieldShowType === FieldType.OPTION_SINGLE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<j-dict-select-tag v-model="queryParam[item.dbFieldName]"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
:type="'select'"
:triggerChange="false" :dictCode="item.dictField" />
</a-form-item>
</template>
<!-- 字典多选框 -->
<template v-else-if="item.fieldShowType === FieldType.OPTION_MORE.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<j-multi-select-tag v-model="queryParam[item.dbFieldName]"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt"
:type="'select'" :maxTagCount="1"
:triggerChange="false" :dictCode="item.dictField" />
</a-form-item>
</template>
<!-- 年份选择 -->
<template
v-if="item.fieldShowType === FieldType.YEAR_PICKER.value">
<a-form-item :label="item.dbFieldTxt" :labelCol="labelCol" :wrapperCol="wrapperCol">
<j-date show-type="year"
:allowClear="false"
v-model="queryParam[item.dbFieldName]"
date-format="YYYY"
:placeholder="$t('pleaseSelect')+item.dbFieldTxt" />
</a-form-item>
</template>
</a-col>
</template>
<div style="float: right;overflow: hidden;margin-right: 11px" class="table-page-search-submitButtons">
<a @click="handleToggleSearch" v-if="searchList.length > minLength">
{{ !toggleSearchStatus ? $t('open') : $t('putAway') }}
<a-icon :type="toggleSearchStatus ? 'up' : 'down'" />
</a>
<a-button icon="search" type="primary" @click="searchQuery" style="margin-left: 8px" v-has="searchHas">{{
$t('query')
}}
</a-button>
<a-button icon="reload" type="primary" ghost style="margin-left: 8px" @click="searchReset">{{ $t('reset') }}</a-button>
</div>
</a-row>
</a-form>
</template>
<script>
import JDictSelectTag from '../dict/JDictSelectTag'
import { FieldType } from '../../enums/commonEnums'
import eventBus from '@/common/event'
import { getFormDictTreeList, queryDepartTreeList } from '../../api/api'
import { getAction } from '../../api/manage'
export default {
name: 'Search',
components: { JDictSelectTag },
props: {
// 获取查询条件的方法
getQueryConditionFunc: {
type: Function,
required: false,
default: () => {
return function () {
}
}
},
flag: { // 暂时不清楚flag的用处,是要查searchList的时候传给后端的
type: String,
default: ''
},
minLength: {
type: Number,
default: 2
},
// 查询权限
searchHas: {
type: String,
required: false,
default: null
},
// 默认的查询条件
defaultSearchParams: {
type: Object,
required: false,
default: () => {
return {}
}
}
// searchQueryList: {
// type: Array,
// default: () => {
// return []
// }
// }
},
data () {
return {
FieldType,
categoryTreeList: [], // 组织机构下拉框数据
queryParam: {},
toggleSearchStatus: false,
searchList: [],
labelCol: {
span: 6
},
wrapperCol: {
span: 14
},
optionsData: {} // 用接口获取到的下拉数据
}
},
created () {
this.queryParam = Object.assign({}, this.defaultSearchParams)
},
mounted () {
this.getSysCategoryTree()
this.getSearch()
},
methods: {
searchQuery () {
const formInline = JSON.parse(JSON.stringify(this.queryParam))
Object.keys(formInline).forEach(res => {
if (formInline[res] instanceof Array) {
if (formInline[res][0] instanceof Object) {
// 说明是树选择
formInline[res] = formInline[res].map(item => item.value).join(',')
} else {
formInline[res] = formInline[res].join(',')
}
}
})
this.$emit('search', JSON.parse(JSON.stringify(formInline)))
eventBus.$emit('searchQuery', JSON.parse(JSON.stringify(this.queryParam)))
},
searchReset () {
this.queryParam = Object.assign({}, this.defaultSearchParams)
console.log(this.defaultSearchParams, this.queryParam)
this.$emit('reset', JSON.parse(JSON.stringify(this.queryParam)))
eventBus.$emit('searchQuery', JSON.parse(JSON.stringify(this.queryParam)))
},
// 获取组织机构树
getSysCategoryTree () {
queryDepartTreeList().then((res) => {
if (res.success) {
this.categoryTreeList = res.result
} else {
this.categoryTreeList = []
}
})
},
// 获取查询条件列表
getSearch () {
console.log(this.getQueryConditionFunc)
if (!this.getQueryConditionFunc || typeof this.getQueryConditionFunc !== 'function') {
return
}
const params = {
module: this.flag
}
this.getQueryConditionFunc(params).then((res) => {
if (res.success) {
this.searchList = res.result
// 标准状态特殊处理成多选
const standardStateIndex = this.searchList.findIndex(item => item.dbFieldName === 'standard_state')
this.searchList[standardStateIndex].fieldShowType = FieldType.OPTION_MORE.value
this.getOptionsData() // 获取下拉数据
console.log(this.searchList)
}
})
},
/**
* 处理需要通过接口获取的下拉数据
*/
getOptionsData () {
const formList = Object.values(this.searchList).reduce((acc, cur) => acc.concat(cur), [])
formList.forEach(item => {
if (item.fieldShowType === FieldType.TREE.value || item.fieldShowType === FieldType.TREE_SINGLE.value) {
// 展示类型是树选择,且有接口的
if (item.fieldHref) {
getAction(item.fieldHref).then(res => {
if (res.success) {
this.$set(this.optionsData, item.dbFieldName, res.result)
}
})
} else if (item.dictId) {
getFormDictTreeList({ dictId: item.dictId }).then(res => {
if (res.success) {
this.$set(this.optionsData, item.dbFieldName, res.result)
}
})
}
}
})
},
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
},
filterTreeOption (input, option) {
const text = option.componentOptions.propsData.title || option.componentOptions.propsData.label
return text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
}
}
</script>
<style scoped lang="less">
</style>
+389
View File
@@ -0,0 +1,389 @@
<template>
<div class="box" v-if="isTrue">
<a-table
class="table"
rowKey="id"
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange, type: selectionType }"
:pagination="ipagination"
:scroll="{x: '100%'}"
:components="drag(columns,'columns')"
:data-source="dataSource"
:loading="loading"
:rowClassName="rowClassName"
:columns="columns"
@change="tableOnChange"
>
<tempalte 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>
<!-- v-has="ol.has"-->
<span v-else class="text">
{{ ol.text }}
</span>
</a>
</tempalte>
<span slot="detailClick" slot-scope="text, record">
<a class="text-name" :title="text" @click="detailClick(record)">
{{text}}
</a>
</span>
<span slot="detail" slot-scope="text">
<span class="text-name" :title="text">
{{text}}
</span>
</span>
<span slot="urlClick" slot-scope="text">
<a class="text" :title="text" @click="urlClick(text)">
{{text}}
</a>
</span>
<span slot="detailText" slot-scope="text">
<span class="text" :title="text">
{{text}}
</span>
</span>
<span slot="showContent" slot-scope="text, record" class="item-solt">
<span class="content-show" href="javascript:;" @click="showContent(text,record)">{{record.iterms_conditions&&record.iterms_conditions!=='null'?record.iterms_conditions.replace(/<.*?>/ig, ' ') : ''}}</span>
</span>
</a-table>
</div>
</template>
<script>
import { ResizeColumnProvide, ResizeHeader } from '@/mixins/header'
import { getAction, postAction } from '@/api/manage'
import eventBus from '@/common/event'
export default {
name: 'TableData',
mixins: [ResizeColumnProvide, ResizeHeader],
props: {
// 接口
url: {
type: Object,
default: () => {
return {}
}
},
// 操作按钮
operationList: {
type: Array,
default: () => {
return []
}
},
flag: { // flag获取数据时传给后端的,具体作用还不知道??
type: String,
default: ''
},
// 表格选择是单选还是多选
selectionType: {
type: String,
default: 'checkbox'
},
// 是否显示操作;默认不显示
showAction: {
type: Boolean,
default: false
},
type: {
type: String,
default: ''
}
},
data () {
return {
/* 分页参数 */
ipagination: {
current: 1,
pageSize: 50,
pageSizeOptions: ['10', '30', '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 () {
eventBus.$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()
})
eventBus.$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' }
// }
]
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.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 () {
eventBus.$off('searchQuery')
eventBus.$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 if (res.db_field_name === 'iterms_conditions') {
// 拆分文档详情页的条款内容点击可预览
this.columns[index].width = 500
this.columns[index].scopedSlots = {
customRender: 'showContent'
}
} 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,
type: this.type,
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 (selectedRowKeys, selectedRows) {
this.selectedRowKeys = selectedRowKeys
this.$emit('onSelectChange', selectedRowKeys, selectedRows)
},
operationClick (ol, item) {
this.$emit(ol.clickEvent, item)
},
detailClick (item, index) {
this.$emit('detailClick', item)
},
urlClick (item) {
window.open(item)
},
showContent (item) {
this.$emit('showContent', item)
}
}
}
</script>
<style scoped lang="less">
.text {
margin-right: 10px;
}
.text-name {
width: 100%;
overflow: hidden;
display: -webkit-box;
text-overflow: ellipsis;
/*! autoprefixer: off */
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
/*! autoprefixer: on;*/
text-justify: inter-ideograph;
word-break: break-all;
}
.item-solt {
cursor: pointer;
}
</style>
+145
View File
@@ -0,0 +1,145 @@
<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"
v-bind="$attrs"
show-search
:filter-option="filterOption"
@change="handleInput"
>
<a-select-option :value="undefined">请选择</a-select-option>
<a-select-option v-for="(item, key) in dictOptions" :key="key" :value="item.value" :disabled="item.disabled || false">
<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
},
options: {
type: Array,
required: false,
default: () => {
return []
}
}
},
data () {
return {
dictOptions: [],
tagType: ''
}
},
watch: {
dictCode: {
immediate: true,
handler () {
if (this.dictCode) {
this.initDictData()
} else {
this.dictOptions = this.options
}
}
},
options: {
handler () {
if (!this.dictCode) {
this.dictOptions = this.options
}
},
immediate: true,
deep: true
}
},
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: {
filterOption (input, option) {
const text = option.componentOptions.children[0].text || option.componentOptions.children[0].data.attrs.title
return text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
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 (e !== undefined && 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([])
})
}
+211
View File
@@ -0,0 +1,211 @@
<template>
<a-checkbox-group v-if="tagType==='checkbox'" @change="onChange" :value="arrayValue" :disabled="disabled">
<a-checkbox :key="-1" :value="selectedAllValue" v-if="openSelectedAll && dictOptions.length > 0">{{ $t('checkedAll') }}</a-checkbox>
<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"
@select="onSelect"
@deselect="onDeselect"
:disabled="disabled"
mode="multiple"
:placeholder="placeholder"
:getPopupContainer="getParentContainer"
optionFilterProp="children"
:filterOption="filterOption"
allowClear
:class="(openSelectedAll && arrayValue[0] === selectedAllValue) ? 'multi-select-tag-check-all' : ''"
v-bind="$attrs">
<a-select-option :key="-1" :value="selectedAllValue" v-if="openSelectedAll && dictOptions.length > 0">{{ $t('checkedAll') }}</a-select-option>
<a-select-option v-for="(item, key) in dictOptions" :key="key" :value="item.value">{{ item.text || item.label }}</a-select-option>
</a-select>
</template>
<script>
import { ajaxGetDictItems, getDictItemsFromCache } from '@/api/api'
// 全选的value
const selectedAllValue = '__all'
export default {
name: 'JMultiSelectTag',
props: {
dictCode: String,
placeholder: String,
disabled: Boolean,
value: String,
type: {
type: String,
default: 'select'
},
options: Array,
// 是否需要全选
openSelectedAll: {
type: Boolean,
default: false
},
spliter: {
type: String,
required: false,
default: ','
},
popContainer: {
type: String,
default: '',
required: false
}
},
data () {
return {
// 全选的value
selectedAllValue,
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 () {
if (this.dictCode) {
this.initDictData()
}
}
},
value: {
immediate: true,
handler (val) {
if (!val) {
this.arrayValue = []
} else {
const list = this.value.split(this.spliter)
// 开启全选后如果所有项都被勾选要回显全选
if (this.openSelectedAll && (list.length === this.dictOptions.length)) {
list.unshift(selectedAllValue)
}
this.arrayValue = list
}
}
}
},
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) {
// 如果开启全选,并且是复选框时,判断是全选还是取消勾选
if (this.openSelectedAll && this.tagType === 'checkbox') {
const findAllBefore = this.arrayValue.includes(selectedAllValue)
const findAllAfter = selectedValue.includes(selectedAllValue)
// 之前选了,之后没选就是取消所有勾选
if (findAllBefore && !findAllAfter) {
selectedValue = []
} else if (!findAllBefore && findAllAfter) {
// 之前没选,之后选了就是全部勾选
selectedValue = this.dictOptions.map(item => item.value)
}
}
this.arrayValue = selectedValue
// 开启全选时,不把全选的value进行返回
if (this.openSelectedAll) {
this.$emit('change', selectedValue.filter(item => item !== selectedAllValue).join(this.spliter))
} else {
this.$emit('change', selectedValue.join(this.spliter))
}
},
// 选中的回调
onSelect (value) {
// 没有开启全选就直接退出
if (!this.openSelectedAll) {
return
}
this.$nextTick(() => {
// 勾选全选就把所有项选中
if (value === selectedAllValue) {
this.onChange([selectedAllValue, ...this.dictOptions.map(item => item.value)])
} else if (this.arrayValue.length === this.dictOptions.length) {
// 勾选其他就判断是否全部勾选,如果已经全部勾选,把全选勾上
this.arrayValue.unshift(selectedAllValue)
this.onChange(this.arrayValue)
}
})
},
// 取消选中的回调
onDeselect (value) {
// 没有开启全选就直接退出
if (!this.openSelectedAll) {
return
}
this.$nextTick(() => {
// 如果取消全选就把所有项取消勾选
if (value === selectedAllValue) {
this.onChange([])
} else {
// 如果取消了任意一个,就把全选给取消勾选
this.onChange(this.arrayValue.filter(item => item !== selectedAllValue))
}
})
},
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].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
// update--end--autor:lvdandan-----date:20201120------forLOWCOD-1086 下拉多选框,搜索时只字典code进行搜索不能通过字典text搜索
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style lang="less" scoped>
// 全选是第一个,全选这个样式把第一个选项隐藏
.multi-select-tag-check-all {
/deep/ .ant-select-selection__rendered ul li:first-child {
display: none;
}
}
</style>
+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.warning('组件JTreeSelect-condition传值有误,需要一个json字符串!')
reject()
}
} catch (e) {
this.$message.warning('组件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>
+130
View File
@@ -0,0 +1,130 @@
<template>
<a-date-picker
:disabledDate="disabledDate"
:disabled="disabled || readOnly"
:placeholder="placeholder"
@change="handleDateChange"
@panelChange="panelChange"
@openChange="handOpenChange"
:open="isOpen"
:value="momVal"
:showTime="showTime"
:format="dateFormat"
:mode="mode"
:getCalendarContainer="getCalendarContainer"
style="width: 100%" />
</template>
<script>
import moment from 'moment'
export default {
name: 'JDate',
props: {
placeholder: {
type: String,
default: '',
required: false
},
value: {
type: String,
required: false
},
disabledDate: {
type: Function
},
dateFormat: {
type: String,
default: 'YYYY-MM-DD',
required: false
},
// 此属性可以被废弃了
triggerChange: {
type: Boolean,
required: false,
default: false
},
readOnly: {
type: Boolean,
required: false,
default: false
},
disabled: {
type: Boolean,
required: false,
default: false
},
showTime: {
type: Boolean,
required: false,
default: false
},
getCalendarContainer: {
type: Function,
default: (node) => node.parentNode
},
showType: {
type: String,
required: false,
default: 'time'
}
},
data () {
const dateStr = this.value
return {
decorator: '',
momVal: !dateStr ? null : moment(dateStr, this.dateFormat),
isOpen: false
}
},
computed: {
mode () {
if (this.showType === 'time') {
return 'time'
}
if (this.showType === 'day') {
return 'date'
}
if (this.showType === 'month') {
return 'month'
}
if (this.showType === 'year') {
return 'year'
}
return 'date'
}
},
watch: {
value (val) {
if (!val) {
this.momVal = null
} else {
this.momVal = moment(val, this.dateFormat)
}
}
},
methods: {
moment,
handleDateChange (mom, dateStr) {
this.$emit('change', dateStr)
},
panelChange (value) {
const dateStr = moment(value).format(this.dateFormat)
this.isOpen = false
this.$emit('change', dateStr)
},
handOpenChange (open) {
this.isOpen = !!open
}
},
// 2.2新增 在组件内定义 指定父组件调用时候的传值属性和事件类型 这个牛逼
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style lang="less" scoped>
/deep/ .ant-input {
padding: 4px 6px;
}
</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.warning(`${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>
+87
View File
@@ -0,0 +1,87 @@
<template>
<div class="loading-box" :style="style" v-if="loading">
<div class="loading-content">
<a-icon class="loading" type="loading" />
<div class="loading-tips">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'JLoading',
props: {
// 是否显示隐藏
loading: {
type: Boolean,
default: false
},
// 是否固定
fixed: {
type: Boolean,
default: false
}
},
data () {
return {
style: {}
}
},
mounted () {
if (this.fixed) {
const el = $(this.el)
const top = `${el.offset().top}px`
const left = `${el.offset().left}px`
const width = `${el.outerWidth()}px`
const height = `${el.outerHeight()}px`
this.style = {
position: 'fixed',
top,
left,
width,
height,
'z-index': 1000
}
}
}
}
</script>
<style lang="less">
.loading-box {
position: absolute;
top: 0;
left: 0;
z-index: 1009;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 8px;
user-select: none;
.loading-content {
position: absolute;
top: 50%;
left: 50%;
color: #21c9cc;
transform: translate(-50%, -50%);
text-align: center;
.loading {
font-size: 28px;
}
.spin-loading {
animation: rotating 2s linear infinite;
}
.loading-tips {
margin-top: 5px;
font-size: 16px;
color: #21c9cc;
}
}
}
</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.warning('popup参数未正确配置!')
this.avalid = false
}
if (this.destFields.split(',').length !== this.orgFields.split(',').length) {
this.$message.warning('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>
+674
View File
@@ -0,0 +1,674 @@
<template>
<a-table
class="j-table"
ref="table"
:bordered="bordered"
: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" />
<template slot="text" slot-scope="text">
<a-tooltip overlay-class-name="tooltip-style">
<template slot="title">{{ text || text === 0 ? text : global.emptyLine }}</template>
<div class="table-text">{{ text || text === 0 ? text : global.emptyLine }}</div>
</a-tooltip>
</template>
<!-- 操作栏 -->
<template slot="action" slot-scope="text, record, index">
<slot name="action" v-bind="{text, record, index}">
<div class="action-span-cell">
<!-- 传了operSpecialJudgeField需要特殊判断操作列是否显示更多 -->
<template v-if="operSpecialJudgeField">
<template v-if="operSpecialJudge(record).length > operateMaxNum + 1">
<!-- 需要更多按钮 -->
<a @click="operationClick(operSpecialJudge(record)[0],record)">{{ operSpecialJudge(record)[0].text }}</a>
<template v-if="operSpecialJudge(record).length > operateMaxNum">
<a-divider type="vertical" />
<a-dropdown placement="bottomCenter">
<a-icon type="more" class="operate-icon-btn operate-icon-more" />
<a-menu slot="overlay">
<a-menu-item v-for="(operation, index) in operSpecialJudge(record).slice(operateMaxNum)" :key="index">
<a @click="operationClick(operation,record)">
<div class="operate-btn">
{{ operation.text }}
</div>
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</template>
<template v-else>
<!-- 不需要更多按钮 -->
<template v-for="(operation, index) in operSpecialJudge(record)">
<a :key="index" @click="operationClick(operation,record)">{{ operation.text }}</a>
<a-divider type="vertical" v-if="index !== operSpecialJudge(record).length - 1" />
</template>
</template>
</template>
<template v-else>
<!-- 没有特殊判断直接渲染operationList -->
<!-- 需要更多按钮的情况 -->
<template v-if="getLineOperationList(record).length > operateMaxNum + 1">
<a @click="operationClick(getLineOperationList(record)[0],record)">{{ getLineOperationList(record)[0].text }}</a>
<template v-if="getLineOperationList(record).length > operateMaxNum">
<a-divider type="vertical" />
<a-dropdown placement="bottomCenter">
<a-icon type="more" class="operate-icon-btn operate-icon-more" />
<a-menu slot="overlay">
<a-menu-item v-for="(operation, index) in getLineOperationList(record).slice(operateMaxNum)" :key="index">
<a @click="operationClick(operation,record)">
<div class="operate-btn">
{{ operation.text }}
</div>
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</template>
<template v-else-if="getLineOperationList(record).length > 0">
<template v-for="(operation, index) in getLineOperationList(record)">
<a :key="index" @click="operationClick(operation,record)">{{ operation.text }}</a>
<a-divider type="vertical" v-if="index !== getLineOperationList(record).length - 1" />
</template>
</template>
</template>
</div>
</slot>
</template>
<div slot="settingDropdown">
<a-card v-if="needSettingColumnHideOrFreeze">
<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' }"
v-if="needSettingColumnHideOrFreeze" />
</a-table>
</template>
<script>
import '@assets/less/common.less'
import Vue from 'vue'
import { cloneDeep } from 'lodash'
import VueDraggableResizable from 'vue-draggable-resizable'
import { isHasPermission } from '@/utils/hasPermission'
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: false,
default: 'j-table'
},
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
},
// 是否需要设置冻结列和显隐列
needSettingColumnHideOrFreeze: {
type: Boolean,
required: false,
default: false
},
// 是否需要将冻结和显隐的设置存储在localStorage中
needSaveSettingInLocal: {
type: Boolean,
required: false,
default: false
},
// 是否可拖拽
canDrag: {
type: Boolean,
required: false,
default: false
},
// 操作列数据
operationList: {
type: Array,
required: false,
default: () => {
return []
}
},
// 操作列显示更多前的最大个数
operateMaxNum: {
type: Number,
required: false,
default: 1
},
// 操作列对于哪个字段有特殊判断
operSpecialJudgeField: {
type: String,
required: false,
default: ''
},
// 操作列需要特殊判断的字段是哪些值时,过滤不符合条件按钮
operSpecialJudgeValue: {
type: [Array, String, Boolean],
required: false,
default: true
},
// 操作列特殊判断时需要删掉的按钮
operSpecialJudgeDelBtn: {
type: [String, Array],
required: false,
default: ''
}
},
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: {},
resizing: false
}
},
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)
}
},
columns: {
handler () {
this.setResultColumns(this.columns)
},
deep: true
}
},
methods: {
isHasPermission,
// 操作列特殊判断,返回经过特殊判断后的操作列数据
operSpecialJudge (record) {
let operationList = []
if (typeof this.operSpecialJudgeValue === 'boolean' || typeof this.operSpecialJudgeValue === 'string') {
// 只需要判断某个字段是否符合某一个值
if (record[this.operSpecialJudgeField] === this.operSpecialJudgeValue) {
// 符合特殊条件,需要过滤操作按钮
if (this.operSpecialJudgeDelBtn && typeof this.operSpecialJudgeDelBtn === 'string') {
// 是字符串类型,只需要过滤掉一个操作按钮
operationList = this.operationList.filter(item => item.text !== this.operSpecialJudgeDelBtn)
} else if (this.operSpecialJudgeDelBtn && typeof this.operSpecialJudgeDelBtn === 'object') {
// 是数组类型需要过滤掉多个操作按钮
operationList = this.operationList.filter(item => !this.operSpecialJudgeDelBtn.includes(item.text))
} else {
operationList = this.operationList
}
} else {
// 不符合条件,不需要过滤
operationList = this.operationList
}
} else if (typeof this.operSpecialJudgeValue === 'object') {
// 需要判断某个字段是否符合某些值
if (this.operSpecialJudgeValue.includes(record[this.operSpecialJudgeField])) {
// 符合特殊条件,需要过滤操作按钮
if (this.operSpecialJudgeDelBtn && typeof this.operSpecialJudgeDelBtn === 'string') {
// 是字符串类型,只需要过滤掉一个操作按钮
operationList = this.operationList.filter(item => item.text !== this.operSpecialJudgeDelBtn)
} else if (this.operSpecialJudgeDelBtn && typeof this.operSpecialJudgeDelBtn === 'object') {
// 师叔祖类型需要过滤掉多个操作按钮
operationList = this.operationList.filter(item => !this.operSpecialJudgeDelBtn.includes(item.text))
} else {
operationList = this.operationList
}
} else {
// 不符合条件,不需要过滤
operationList = this.operationList
}
}
return operationList
},
// 操作栏按钮点击
operationClick (operation, record) {
this.$emit('operationClick', operation, record)
},
// 将父组件的columns进行处理:增加设置列按钮
setResultColumns (columns) {
// 深度克隆,防止直接修改父组件数据
columns = cloneDeep(columns)
// 有设置功能再进行这个插槽的配置
if (this.needSettingColumnHideOrFreeze) {
// 设置按钮插槽配置
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.needSettingColumnHideOrFreeze ? this.settingColumnsObj.freeze.includes(key) : col.fixed
col.hide = this.settingColumnsObj.hide.includes(key)
col.fixed = fixed
// 如果是字符串就转换成数字(因为实现拖拽,单位只能是px)
col.width = col.width && parseInt(col.width + '')
// 如果没有最小宽度就获取统一设置的最小宽度
col.minWidth = (col.minWidth && parseInt(col.minWidth + '')) || this.columnMinWidth
// 如果没有配置align,就默认居中
col.align = col.align || 'center'
if (!col.scopedSlots) {
col.scopedSlots = {
customRender: 'text'
}
}
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.canDrag) {
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.width || !!col.fixed) {
return <th {...restProps}>{children}</th>
}
// 开始拖拽监听
const onDragging = (x) => {
this.resizing = true
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
}
// 解决拖拽重置其他单元格宽度的问题
const colIndex = this.resultColumns.findIndex(tt => getKey(tt) === key)
const nowResultColumns = cloneDeep(this.resultColumns)
nowResultColumns.splice(colIndex, 1, col)
// 修改列宽后,更新表格字段
this.setResultColumns(nowResultColumns)
}
// 停止拖拽监听
const onDragstop = () => {
draggingState[key] = thDom.getBoundingClientRect().width
setTimeout(() => {
this.resizing = false
}, 200)
}
// 控制最小拖拽宽度
const onDrag = (x) => {
return x >= (col.minWidth || this.columnMinWidth)
}
// 解决都有dataIndex时会有横向滚动条的问题,让最后一列的拖拽容器宽度为0
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 () {
if (!this.needSaveSettingInLocal) {
return
}
// 获取所有的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的缓存!')
},
getLineOperationList (record) {
const hasPermissionList = this.operationList.filter(tt => !tt.has || isHasPermission(tt.has))
const lineOperateList = []
hasPermissionList.forEach(item => {
if (!item.show) {
lineOperateList.push(item)
} else if (typeof item.show === 'function') {
if (item.show(record)) {
lineOperateList.push(item)
}
}
})
return lineOperateList
}
},
mounted () {
// 备份默认配置
this.columnsBak = cloneDeep(this.columns)
let settingColumnsObj
if (this.needSaveSettingInLocal) {
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;
// 让拖动的地方有个线
border-right: 1px #e8e8e8 solid;
.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;
}
}
// 解决每行都有dataIndex时会有横向滚动条的问题
.resize-table-th:last-child {
border-right: none;
.table-draggable-handle {
width: 0px !important;
min-width: 0px !important;
}
}
}
</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.warning(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.warning('组件JTreeSelect-condition传值有误,需要一个json字符串!')
reject()
}
} catch (e) {
this.$message.warning('组件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>
+512
View File
@@ -0,0 +1,512 @@
<template>
<div :id="containerId" style="position: relative">
<a-upload
name="file"
:action="uploadAction"
:headers="headers"
:data="{'biz':bizPath}"
:fileList="fileList"
:beforeUpload="doBeforeUpload"
@change="handleChange"
:disabled="disabled"
:returnUrl="returnUrl"
:listType="complistType"
@preview="handlePreview"
@download="handleDownload"
v-bind="$attrs"
v-on="childListeners"
:showUploadList="showUploadList ? {showDownloadIcon: isDownload} : false"
:class="{'uploadty-disabled':disabled}">
<template>
<slot name="customButton">
<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>
</slot>
</template>
</a-upload>
<div id="images">
<div class="image" v-viewer="{movable: false}">
<img v-show="image" :src="imageUrl" alt="">
</div>
</div>
</div>
</template>
<script>
import Vue from 'vue'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import { getFileAccessHttpUrl, downloadFile } from '@/api/manage'
import { getFileInfo } from '@/api/api'
import { previewPdf } from '../../utils/previewPdf'
import { kkFilePreview } from '../../utils/kkFilePreview'
// eslint-disable-next-line no-undef
// const Base64 = require('js-base64').Base64
const FILE_TYPE_ALL = 'all'
const FILE_TYPE_IMG = 'image'
// const FILE_TYPE_TXT = 'file'
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_UPLOAD_FILE_TYPE = 'pdf,docx,doc,xlsx,xls,ppt,pptx,rar,zip,jpg,jpeg,png,avi,wmv,mov,rm,mp4,cad'
// 支持预览的文件后缀
const CAN_PREVIEW_FILE_SUFFIX = ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
const uidGenerator = () => {
return '-' + parseInt(Math.random() * 10000 + 1, 10)
}
export default {
name: 'JUpload',
data () {
return {
uploadAction: window._CONFIG.domianURL + '/sys/common/upload',
headers: {},
fileList: [],
newFileList: [],
uploadGoOn: true,
fileTypes: this.fileType,
image: false,
imageUrl: null
}
},
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
},
/**
* 仅返回文件id
* true 仅返回文件id
* false 返回reurl
* */
returnId: {
type: Boolean,
required: false,
default: false
},
number: {
type: Number,
required: false,
default: 0
},
buttonVisible: {
type: Boolean,
required: false,
default: true
},
showUploadList: {
type: Boolean,
required: false,
default: true
},
isDownload: {
type: Boolean,
required: false,
default: true
},
beforeUpload: {
type: Function
},
fileMaxSize: {
type: Number,
required: false,
default: 500
}
},
watch: {
value: {
immediate: true,
handler () {
const val = this.value
if (val instanceof Array) {
if (this.returnUrl) {
this.initFileList(val.join(','))
} else {
this.initFileListArr(val)
}
} else {
this.initFileList(val)
}
}
},
fileType () {
this.fileTypes = this.fileType
}
},
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: {
initFileListArr (val) {
if (!val || val.length === 0) {
this.fileList = []
return
}
for (let a = 0; a < val.length; a++) {
this.fileList.forEach((item) => {
if (item.url === val[a]) {
item.uid = uidGenerator()
item.response.status = 'done'
}
})
}
},
async initFileList (paths) {
if (!paths || paths.length === 0) {
this.fileList = []
return
}
let arr = paths.split(',')
// 返回路径的时候,对路径进行操作,取出所属id
if (!this.returnId && arr.length > 0) {
const newArr = []
arr.map(item => {
const itemArr = item.split('/')
newArr.push(itemArr[itemArr.length - 1])
})
arr = newArr
}
const fileList = []
for (let a = 0; a < arr.length; a++) {
// 获取每一个文件的信息,组成fileList
// 突然想判断现在的fileList 中是否是历史的信息,如果是的话则先进行匹配
const isHistory = this.fileList.find(file => file.response.status === 'history' && (file.response.result || {}).id === arr[a])
console.log('isHistory===', isHistory)
if (isHistory) {
fileList.push(isHistory)
} else {
const fileObj = await this.getFileInfo(arr[a])
const url = getFileAccessHttpUrl(arr[a])
const fileName = (fileObj || {}).fileName
const fileNameNotType = fileName.substring(0, fileName.lastIndexOf('.'))
const previewUrl = (fileObj || {}).url || ''
const previewName = previewUrl.substring(previewUrl.lastIndexOf(fileNameNotType))
fileList.push({
uid: uidGenerator(),
name: (fileObj || {}).fileName,
status: 'done',
url: url,
previewName,
response: {
status: 'history',
message: arr[a],
result: fileObj
}
})
}
}
this.fileList = [...fileList]
},
handlePathChange () {
const uploadFiles = this.fileList
let path = ''
// 文件id
let fileIds = ''
if (!uploadFiles || uploadFiles.length === 0) {
path = ''
}
const arr = []
// 如果returnid为true 只返回id
if (this.returnId) {
for (let a = 0; a < uploadFiles.length; a++) {
if (uploadFiles[a].status === 'done') {
arr.push(uploadFiles[a].response.result.id)
} else {
return
}
}
if (arr.length > 0) {
fileIds = arr.join(',')
}
this.$emit('change', fileIds)
return
}
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
file.uploadGoOn = true
const fileSize = file.size // 上传的文件大小
if (fileSize === 0) {
this.$message.warning(this.$t('uploadFile.cannotUploadEmpty'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileMaxSize && fileSize > 1024 * 1024 * this.fileMaxSize) {
this.$message.warning(this.$t('uploadFile.pleaseUpload') + this.fileMaxSize + this.$t('uploadFile.theFollowingDocuments'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (fileSize > 1024 * 1024 * 500) {
this.$message.warning(this.$t('uploadFile.maxSize'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileType === FILE_TYPE_ALL) {
return true
}
const fileType = file.type
// 截取文件后缀名
const fileSuffix = (file.name ? file.name.split('.')[file.name.split('.').length - 1] : '').toLowerCase()
if (this.fileType === CAN_UPLOAD_FILE_TYPE && CAN_UPLOAD_FILE_TYPE.split(',').includes(fileSuffix)) {
return true
}
if (this.fileType === FILE_TYPE_IMG && fileType.indexOf('image') < 0) {
this.$message.warning(this.$t('uploadFile.onlyUploadPic'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileType === FILE_TYPE_IMG && FILE_TYPE_IMGS.includes(fileSuffix)) {
return true
}
if (this.fileType === FILE_TYPE_IMG && !FILE_TYPE_IMGS.includes(fileSuffix)) {
this.$message.warning(this.$t('uploadFile.pleaseUpload') + FILE_TYPE_IMGS.join('、') + this.$t('uploadFile.file'))
this.uploadGoOn = false
file.uploadGoOn = false
return false
}
if (this.fileType.indexOf(fileSuffix) === -1) {
this.$message.warning(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
},
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) {
// let reUrl = `${file.response.result.id}?token=${Vue.ls.get(ACCESS_TOKEN)}&fullfilename=${file.response.result.fileName}`;
file.url = getFileAccessHttpUrl(file.response.result.id)
}
return file
})
} else {
info.fileList.pop()
this.uploadGoOn = false
this.$message.warn(info.file.response.message)
}
// this.$message.success(`${info.file.name} 上传成功!`);
} else if (info.file.status === 'error') {
this.$message.warning(`${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 fileSuffix = (file.name ? file.name.split('.')[file.name.split('.').length - 1] : '').toLowerCase()
const canPreview = 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/view/${file.response.result.id}?at=${Vue.ls.get(ACCESS_TOKEN)}&fullfilename=${file.name}`
console.log(fileSuffix)
// 图片预览,使用自己添加的组件
if (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 (FILE_TYPE_PDF.includes(fileSuffix)) {
const url = previewPdf(file.response.result.id)
window.open(url)
return
}
// 其余可预览文件仍使用KKFile进行预览
kkFilePreview(fileFullUrl)
},
getFileUrl (fileId) {
return `${window._CONFIG.domianURL}/sys/common/download/${fileId}`
},
/**
* 通过文件的id获取文件的相关信息
* @param id
*/
getFileInfo (id) {
return new Promise(resolve => {
let result = {}
getFileInfo({ id: id }).then(res => {
if (res.success) {
result = res.result
}
}).finally(() => {
resolve(result)
})
})
},
/**
* 单个文件的下载功能
* @param file
*/
handleDownload (file) {
// 下载文件
downloadFile(`/sys/common/download/${file.response.result.id}`, file.name)
}
},
mounted () {
},
model: {
prop: 'value',
event: 'change'
}
}
</script>
<style lang="less" scoped>
.uploadty-disabled {
.ant-upload-list-item {
.anticon-close {
display: none;
}
.anticon-delete {
display: none;
}
}
}
</style>
+512
View File
@@ -0,0 +1,512 @@
# JDate 日期组件 使用文档
###### 说明: antd-vue日期组件需要用moment中转一下,用起来不是很方便,特二次封装,使用时只需要传字符串即可
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| placeholder |string | | placeholder |
| readOnly | boolean | | true/false 默认false |
| value | string | | 绑定v-model或是v-decorator后不需要设置 |
| showTime | boolean | | 是否展示时间true/false 默认false |
| dateFormat | string | |日期格式 默认'YYYY-MM-DD' 若showTime设置为true则需要将其设置成对应的时间格式(如:YYYY-MM-DD HH:mm:ss) |
| triggerChange | string | |触发组件值改变的事件是否是change,当使用v-decorator时且没有设置decorator的option.trigger为input需要设置该值为true |
使用示例
----
1.组件带有v-model的使用方法
```vue
<j-date v-model="dateStr"></j-date>
```
2.组件带有v-decorator的使用方法
a).设置trigger-change属性为true
```vue
<j-date :trigger-change="true" v-decorator="['dateStr',{}]"></j-date>
```
b).设置decorator的option.trigger为input
```vue
<j-date v-decorator="['dateStr',{trigger:'input'}]"></j-date>
```
3.其他使用
添加style
```vue
<j-date v-model="dateStr" style="width:100%"></j-date>
```
添加placeholder
```vue
<j-date v-model="dateStr" placeholder="请输入dateStr"></j-date>
```
添加readOnly
```vue
<j-date v-model="dateStr" :read-only="true"></j-date>
```
备注:
script内需引入jdate
```vue
<script>
import JDate from '@/components/jero/JDate'
export default {
name: "demo",
components: {
JDate
}
//...
}
</script>
```
---
# JSuperQuery 高级查询 使用文档
## 参数配置
| 参数 | 类型 | 必填 | 说明 |
|--------------|---------|----|----------------------|
| fieldList | array |✔| 需要查询的列集合示例如下,type类型有:date/datetime/string/int/number |
| callback | array | | 回调函数名称(非必须)默认handleSuperQuery |
fieldList结构示例:
```vue
const superQueryFieldList=[{
type:"date",
value:"birthday",
text:"生日"
},{
type:"string",
value:"name",
text:"用户名"
},{
type:"int",
value:"age",
text:"年龄"
}]
```
页面代码概述:
----
1.import之后再components之内声明
```vue
import JSuperQuery from '@/components/jero/JSuperQuery.vue';
export default {
name: "JeroDemoList",
components: {
JSuperQuery
},
```
2.页面引用
```vue
<!-- 高级查询区域 -->
<j-super-query :fieldList="fieldList" ref="superQueryModal" @handleSuperQuery="handleSuperQuery"></j-super-query>
```
3.list页面data中需要定义三个属性:
```vue
fieldList:superQueryFieldList,
superQueryFlag:false,
superQueryParams:""
```
4.list页面声明回调事件handleSuperQuery(与组件的callback对应即可)
```vue
//高级查询方法
handleSuperQuery(arg) {
if(!arg){
this.superQueryParams=''
this.superQueryFlag = false
}else{
this.superQueryFlag = true
this.superQueryParams=JSON.stringify(arg)
}
this.loadData()
},
```
5.改造list页面方法
```vue
// 获取查询条件
getQueryParams() {
let sqp = {}
if(this.superQueryParams){
sqp['superQueryParams']=encodeURI(this.superQueryParams)
}
var param = Object.assign(sqp, this.queryParam, this.isorter);
param.field = this.getQueryField();
param.pageNo = this.ipagination.current;
param.pageSize = this.ipagination.pageSize;
return filterObj(param);
},
```
6.打开弹框调用show方法:
```vue
this.$refs.superQueryModal.show();
```
# JEllipsis 字符串超长截取省略号显示
###### 说明: 遇到超长文本展示,通过此标签可以截取省略号显示,鼠标放置会提示全文本
## 参数配置
| 参数 | 类型 | 必填 | 说明 |
|--------|---------|----|----------------|
| value |string | 必填 | 字符串文本|
| length | number | 非必填 | 默认25 |
使用示例
----
1.组件带有v-model的使用方法
```vue
<j-ellipsis :value="text"/>
# Modal弹框实现最大化功能
1.定义modal的宽度:
```vue
<a-modal
:width="modalWidth"
/>
```
2.自定义modal的title,居右显示切换图标
```vue
<template slot="title">
<div style="width: 100%;">
<span>{{ title }}</span>
<span style="display:inline-block;width:calc(100% - 51px);padding-right:10px;text-align: right">
<a-button @click="toggleScreen" icon="appstore" style="height:20px;width:20px;border:0px"></a-button>
</span>
</div>
</template>
```
3.定义toggleScreen事件,用于切换modal宽度
```vue
toggleScreen(){
if(this.modaltoggleFlag){
this.modalWidth = window.innerWidth;
}else{
this.modalWidth = 800;
}
this.modaltoggleFlag = !this.modaltoggleFlag;
},
```
4.data中声明上述用到的属性
```vue
data () {
return {
modalWidth:800,
modaltoggleFlag:true,
```
# <a-select/> 下拉选项滚动错位的解决方法
## 问题描述
当使用了 `a-modal` 或其他带有滚动条的组件时,使用`a-select`组件并打开下拉框时滚动滚动条,就会导致错位的问题产生。
## 解决方法
大多数情况下,在 `a-select` 上添加一个 `getPopupContainer` 属性,值为`node => node.parentNode`即可解决。
但是如果遇到 `a-select` 标签层级过深的情况,可能仍然会显示异常,只需要多加几个`.parentNode` (例:node => node.parentNode.parentNode.parentNode)多尝试几次直到解决问题即可。
### 代码示例
```html
<a-select
placeholder="请选择展示模板"
:options="dicts.displayTemplate"
:getPopupContainer="node => node.parentNode"
/>
```
# JAsyncTreeList 异步数列表组件使用说明
## 引入组件
```js
import JTreeTable from '@/components/jero/JTreeTable'
export default {
components: { JTreeTable }
}
```
## 所需参数
| 参数 | 类型 | 必填 | 说明 |
|-------------|--------|--------|--------------------------------------------------------------|
| rowKey | String | 非必填 | 表格行 key 的取值,默认为"id" |
| columns | Array | 必填 | 表格列的配置描述,具体见Antd官方文档 |
| url | String | 必填 | 数据查询url |
| childrenUrl | String | 非必填 | 查询子级时的url,若不填则使用url参数查询子级 |
| queryKey | String | 非必填 | 根据某个字段查询,如果传递 id 就根据 id 查询,默认为parentId |
| queryParams | Object | 非必填 | 查询参数,当查询参数改变的时候会自动重新查询,默认为{} |
| topValue | String | 非必填 | 查询顶级时的值,如果顶级为0,则传0,默认为null |
| tableProps | Object | 非必填 | 自定义给内部table绑定的props |
## 代码示例
```html
<template>
<a-card :bordered="false">
<j-tree-table :url="url" :columns="columns" :tableProps="tableProps"/>
</a-card>
</template>
<script>
import JTreeTable from '@/components/jero/JTreeTable'
export default {
name: 'AsyncTreeTable',
components: { JTreeTable },
data() {
return {
url: '/mock/api/asynTreeList',
columns: [
{ title: '菜单名称', dataIndex: 'name' },
{ title: '组件', dataIndex: 'component' },
{ title: '排序', dataIndex: 'orderNum' }
],
selectedRowKeys: []
}
},
computed: {
tableProps() {
let _this = this
return {
// 列表项是否可选择
// 配置项见:https://vue.ant.design/components/table-cn/#rowSelection
rowSelection: {
selectedRowKeys: _this.selectedRowKeys,
onChange: (selectedRowKeys) => _this.selectedRowKeys = selectedRowKeys
}
}
}
}
}
</script>
```
# JCheckbox 使用文档
###### 说明: antd-vue checkbox组件处理的是数组,用起来不是很方便,特二次封装,使用时只需处理字符串即可
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| options |array |✔| checkbox需要配置的项,是个数组,数组中每个对象包含两个属性:label(用于显示)和value(用于存储) |
使用示例
----
```vue
<template>
<a-form :form="form">
<a-form-item label="v-model式用法">
<j-checkbox v-model="sport" :options="sportOptions"></j-checkbox><span>{{ sport }}</span>
</a-form-item>
<a-form-item label="v-decorator式用法">
<j-checkbox v-decorator="['sport']" :options="sportOptions"></j-checkbox><span>{{ getFormFieldValue('sport') }}</span>
</a-form-item>
</a-form>
</template>
<script>
import JCheckbox from '@/components/jero/JCheckbox'
export default {
components: {JCheckbox},
data() {
return {
form: this.$form.createForm(this),
sport:'',
sportOptions:[
{
label:"足球",
value:"1"
},{
label:"篮球",
value:"2"
},{
label:"乒乓球",
value:"3"
}]
}
},
methods: {
getFormFieldValue(field){
return this.form.getFieldValue(field)
}
}
}
</script>
```
# JCodeEditor 使用文档
###### 说明: 一个简易版的代码编辑器,支持语法高亮
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| language |string | | 表示当前编写代码的类型 javascript/html/css/sql |
| placeholder |string | | placeholder |
| lineNumbers |Boolean | | 是否显示行号 |
| fullScreen |Boolean | | 是否显示全屏按钮 |
| zIndex |string | | 全屏以后的z-index |
使用示例
----
```vue
<template>
<div>
<j-code-editor
language="javascript"
v-model="editorValue"
:fullScreen="true"
style="min-height: 100px"/>
{{ editorValue }}
</div>
</template>
<script>
import JCodeEditor from '@/components/jero/JCodeEditor'
export default {
components: {JCodeEditor},
data() {
return {
form: this.$form.createForm(this),
editorValue:'',
}
}
}
</script>
```
# JFormContainer 使用文档
###### 说明: 暂用于表单禁用
使用示例
----
```vue
<!-- 在form下直接写这个组件,设置disabled为true就能将此form中的控件禁用 -->
<a-form layout="inline" :form="form" >
<j-form-container disabled>
<!-- 表单内容省略..... -->
</j-form-container>
</a-form>
```
# JImportModal 使用文档
###### 说明: 用于列表页面导入excel功能
使用示例
----
```vue
<template>
<!-- 此处省略部分代码...... -->
<a-button @click="handleImportXls" type="primary" icon="upload">导入</a-button>
<!-- 此处省略部分代码...... -->
<j-import-modal ref="importModal" :url="getImportUrl()" @ok="importOk"></j-import-modal>
<!-- 此处省略部分代码...... -->
</template>
<script>
import JCodeEditor from '@/components/jero/JCodeEditor'
export default {
components: {JCodeEditor},
data() {
return {
//省略代码......
}
},
methods:{
//省略部分代码......
handleImportXls(){
this.$refs.importModal.show()
},
getImportUrl(){
return '你自己处理上传业务的后台地址'
},
importOk(){
this.loadData(1)
}
}
}
</script>
```
# JSelectMultiple 多选下拉组件
online用 实际开发请使用components/dict/JMultiSelectTag
# JSlider 滑块验证码
使用示例
----
```vue
<template>
<div style="width: 300px">
<j-slider @onSuccess="sliderSuccess"></j-slider>
</div>
</template>
<script>
import JSlider from '@/components/jero/JSlider'
export default {
components: {JSlider},
data() {
return {
form: this.$form.createForm(this),
editorValue:'',
}
},
methods:{
sliderSuccess(){
console.log("验证完成")
}
}
}
</script>
```
# JTreeSelect 树形下拉组件
异步加载的树形下拉组件
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| placeholder |string | | placeholder |
| dict |string | ✔| 表名,显示字段名,存储字段名拼接的字符串 |
| pidField |string | ✔| 父ID的字段名 |
| pidValue |string | | 根节点父ID的值 默认'0' 不可以设置为空,如果想使用此组件,而数据库根节点父ID为空,请修改之 |
| multiple |boolean | |是否支持多选 |
使用示例
----
```vue
<template>
<a-form>
<a-form-item label="树形下拉测试" style="width: 300px">
<j-tree-select
v-model="departId"
placeholder="请选择部门"
dict="sys_depart,depart_name,id"
pidField="parent_id">
</j-tree-select>
{{ departId }}
</a-form-item>
</a-form >
</template>
<script>
import JTreeSelect from '@/components/jero/JTreeSelect'
export default {
components: {JTreeSelect},
data() {
return {
departId:""
}
}
}
</script>
```
@@ -0,0 +1,577 @@
# JEditableTable 帮助文档
## 参数配置
| 参数 | 类型 | 必填 | 说明 |
|--------------|---------|------|---------------------------------------------------------------------------------|
| columns | array | ✔️ | 表格列的配置描述,具体项见下表 |
| dataSource | array | ✔️ | 表格数据 |
| loading | boolean | | 是否正在加载,加载中不会显示任何行,默认false |
| actionButton | boolean | | 是否显示操作按钮,包括"新增"、"删除",默认false |
| rowNumber | boolean | | 是否显示行号,默认false |
| rowSelection | boolean | | 是否可选择行,默认false |
| dragSort | boolean | | 是否可拖动排序,默认false |
| dragSortKey | string | | 拖动排序存储的Key,无需定义在columns内也能在getValues()时获取到值,默认orderNum |
| maxHeight | number | | 设定最大高度(px),默认400 |
| disabledRows | object | | 设定禁用的行,被禁用的行无法被选择和编辑,配置方法可以查看示例 |
| disabled | boolean | | 是否禁用所有行,默认false |
### columns 参数详解
| 参数 | 类型 | 必填 | 说明 |
|---------------|---------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| title | string | ✔️ | 表格列头显示的问题 |
| key | string | ✔️ | 列数据在数据项中对应的 key,必须是唯一的 |
| type | string | ✔️ | 表单的类型,可以通过`JEditableTableUtil.FormTypes`赋值 |
| width | string | | 列的宽度,可以是百分比,也可以是`px`或其他单位,建议设置为百分比,且每一列的宽度加起来不应超过100%,否则可能会不能达到预期的效果。留空会自动计算百分比 |
| placeholder | string | | 表单预期值的提示信息,可以使用`${...}`变量替换文本(详见`${...} 变量使用方式`) |
| defaultValue | string | | 默认值,在新增一行时生效 |
| validateRules | array | | 表单验证规则,配置方式见[validateRules 配置规则](#validaterules-配置规则) |
| props | object | | 设置添加给表单元素的自定义属性,例如:`props:{title: 'show title'}` |
| disabled | boolean | | 是否禁用当前列,默认false |
#### 当 type=checkbox 时所需的参数
| 参数 | 类型 | 必填 | 说明 |
|----------------|---------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| defaultChecked | boolean | | 默认值是否选中 |
| customValue | array | | 自定义值,checkbox需要的是boolean值,如果数据是其他值(例如`'Y' or 'N'`)时,就会导致错误,所以提供了该属性进行转换,例:`customValue: ['Y','N']`,会将`true`转换为`'Y'``false`转换为`'N'`,反之亦然 |
#### 当 type=select 时所需的参数
| 参数 | 类型 | 必填 | 说明 |
|------------|---------|------|----------------------------------------------------|
| options | array | ✔️ | 下拉选项列表,详见下表 |
| allowInput | boolean | | 是否允许用户输入内容,并创建新的内容 |
| dictCode | String | | 数据字典Code,若options也有值,则拼接在options后面 |
##### options 所需参数
| 参数 | 类型 | 必填 | 说明 |
|-----------|------------|------|----------------------------------------------------------------------|
| text | string | ✔️ | 显示标题 |
| value | string | ✔️ | 真实值 |
| ~~title~~ | ~~string~~ | | ~~显示标题(已废弃,若同时填写了 title 和 text 那么优先使用 text~~ |
#### 当 type=upload 时所需的参数
| 参数 | 类型 | 必填 | 说明 |
|--------------|---------|------|--------------------------------------------------------------------------------------|
| action | string | ✔️ | 上传文件路径 |
| token | boolean | | 上传的时候是否传递token |
| responseName | string | ✔️ | 若要从上传成功后从response中取出返回的文件名,那么这里填后台返回的包含文件名的字段名 |
#### 当 type=slot 时所需的参数
| 参数 | 类型 | 必填 | 说明 |
|----------|--------|------|------------|
| slotName | string | ✔️ | slot的名称 |
### validateRules 配置规则
`validateRules` 需要的是一个数组,数组里每项都是一个规则,规则是object类型,规则的各个参数如下
- `required` 是否必填,可选值为`true`or`false`
- `pattern` 正则表达式验证,只有成功匹配该正则的值才能成功通过验证
- `handler` 自定义函数校验,使用方法请见[示例五](#示例五)
- `message` 当验证未通过时显示的提示文本,可以使用`${...}`变量替换文本(详见`${...} 变量使用方式`
- 配置示例请看[示例二](#示例二)
## 事件
| 事件名 | 触发时机 | 参数 |
|-----------------|----------------------------------------------------|--------------------------------------------------|
| added | 当添加行操作完成后触发 | |
| deleted | 当删除行操作完成后触发(批量删除操作只会触发一次) | `deleteIds` 被逻辑删除的id |
| selectRowChange | 当行被选中或取消选中时触发 | `selectedRowIds` 被选中行的id |
| valueChange | 当数据发生改变的时候触发的事件 | `{ type, row, column, value, target }` Event对象 |
## 方法
关于方法的如何调用的问题,请在**FAQ**中查看[方法如何调用](#方法如何调用)
### initialize
用于初始化表格(清空表格)
- `参数:`
- `返回值:`
### resetScrollTop
重置滚动条Top位置
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|--------|--------|------|--------------------------------------------------------------------------------------------------------|
| top | number | | 新top位置,留空则滚动到上次记录的位置,用于解决切换tab选项卡时导致白屏以及自动将滚动条滚动到顶部的问题 |
- `返回值:`
### add
主动添加行,默认情况下,当用户的滚动条已经在底部的时候,会将滚动条固定在底部,即添加后无需用户手动滚动,而会自动滚动到底部
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|---------------------|---------|------|---------------------------------------------------------------------|
| num | number | | 添加几行,默认为1 |
| forceScrollToBottom | boolean | | 是否在添加后无论用户的滚动条在什么位置都强制滚动到底部,默认为false |
- `返回值:`
### removeRows
主动删除一行或多行
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|--------|-----------------|------|--------------------------------------------------------------------------------------------|
| id | string 或 array | ✔️ | 被删除行的id。如果要删除一个,可以直接传id,如果要删除多个,需要将多个id封装成一个数组传入 |
- `返回值:`
### removeSelectedRows
主动删除被选中的行
- `参数:`
- `返回值:`
### getValues
用于获取表格里所有表单的值,可进行表单验证
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|----------|----------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| callback | function | ✔️ | 获取值的回调方法,会传入`error``values`两个参数。`error`:未通过验证的数量,当等于`0`时代表验证通过;`values`:获取的值(即使未通过验证该字段也有数据) |
| validate | boolean | | 是否进行表单验证,默认为`true`,设为`false`则代表忽略表单验证 |
| rowIds | array | | 默认返回所有行的数据,如果传入了`rowIds`,那么就会只返回与该`rowIds`相匹配的数据,如果没有匹配的数据,就会返回空数组 |
- `返回值:`
### getValuesSync
`getValues`的同步版,会直接将获取到的数据返回
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|---------|--------|------|------------------------|
| options | object | | 选项,详见下方所需参数 |
- - `options` 所需参数
| 参数名 | 类型 | 必填 | 说明 |
|----------|---------|------|----------------------------------------------------------------------------------------------------------------------|
| validate | boolean | | 是否进行表单验证,默认为`true`,设为`false`则代表忽略表单验证 |
| rowIds | array | | 默认返回所有行的数据,如果传入了`rowIds`,那么就会只返回与该`rowIds`相匹配的数据,如果没有匹配的数据,就会返回空数组 |
- `返回值:` object
- `error` 未通过验证的数量,当等于`0`时代表验证通过
- `values` 获取的值(即使未通过验证该字段也有数据)
- `使用示例`
```js
let { error, values } = this.$refs.editableTable.getValuesSync({ validate: true, rowIds: ['rowId1', 'rowId2'] })
if (error === 0) {
console.log('表单验证通过,数据:', values);
} else {
console.log('未通过表单验证,数据:', values);
}
```
### getValuesPromise
`getValues`的promise版,会在`resolve`中传入获取到的值,会在`reject`中传入失败原因,例如`VALIDATE_NO_PASSED`
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|----------|---------|------|----------------------------------------------------------------------------------------------------------------------|
| validate | boolean | | 同`getValues``validate`参数 |
| rowIds | array | | 默认返回所有行的数据,如果传入了`rowIds`,那么就会只返回与该`rowIds`相匹配的数据,如果没有匹配的数据,就会返回空数组 |
- `返回值:` Promise
### getDeleteIds
用于获取被逻辑删除的行的id,返回一个数组,用户可将该数组传入后台,并进行批量删除
- `参数:`
- `返回值:` array
### getAll
获取所有的数据,包括values、deleteIds
会在`resolve`中传入获取到的值:`{values, deleteIds}`
会在`reject`中传入失败原因,例如`VALIDATE_NO_PASSED`
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|----------|---------|------|-------------------------------|
| validate | boolean | | 同`getValues``validate`参数 |
- `返回值:` Promise
### setValues
主动设置表格中某行某列的值
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|--------|-------|------|------------------------------------------------------------|
| values | array | | 传入一个数组,数组中的每项都是一行的新值,具体见下面的示例 |
- `返回值:`
- `示例:`
```js
setValues([
{
rowKey: id1, // 行的id
values: { // 在这里 values 中的 name 是你 columns 中配置的 key
'name': 'zhangsan',
'age': '20'
}
},
{
rowKey: id2,
values: {
'name': 'lisi',
'age': '23'
}
}
])
```
### clearSelection
主动清空选择的行
- `参数:`
- `返回值:`
## 内置插槽
| 插槽名 | 说明 |
|--------------|------------------------------------------------------|
| buttonBefore | 在操作按钮的**前面**插入插槽,不受`actionButton`属性的影响 |
| buttonAfter | 在操作按钮的**后面**插入插槽,不受`actionButton`属性的影响 |
## ${...} 变量使用方式
`placeholder``message`这两个属性中可以使用`${...}`变量来替换文本
在[示例二](#示例二)中,配置了`title``名称`的一列,而`placeholder`配置成了`请输入${title}`,那么最终显示效果为`请输入名称`
这就是`${...}`变量的使用方式,在`${}`中可以使用的变量有`title``key``defaultValue`这三个属性的值
## JEditableTableUtil 使用说明
在之前配置`columns`时提到过`JEditableTableUtil`这个工具类,那么如果想要知道详细的使用说明就请看这里
### export 的常量
#### FormTypes
这是配置`columns.type`时用到的常量值,其中包括
- `normal` 默认,直接显示值,不渲染表单
- `input` 显示输入框
- `inputNumber` 显示数字输入框
- `checkbox` 显示多选框
- `select` 显示选择器(下拉框)
- `date` 日期选择器
- `datetime` 日期时间选择器
- `upload` 上传组件(文件域)
- `slot` 自定义插槽
### VALIDATE_NO_PASSED
在判断表单验证是否通过时使用,如果 reject 的值 === VALIDATE_NO_PASSED 则代表表单验证未通过,你可以做相应的其他处理,反之则可能是发生了报错,可以使用 `console.error` 输出
### 封装的方法
#### validateTables
当你的页面中存在多个JEditableTable实例的时候,如果要获取每个实例的值、判断表单验证是否通过,就会让代码变得极其冗余、繁琐,于是我们就将该操作封装成了一个函数供你调用,它可以同时获取并验证多个JEditableTable实例的值,只有当所有实例的表单验证都通过后才会返回值,否则将会告诉你具体哪个实例没有通过验证。具体使用方法请看下面的示例
- `参数:`
| 参数名 | 类型 | 必填 | 说明 |
|--------|-------|------|--------------------------------------------------------|
| cases | array | | 传入一个数组,数组中的每项都是一个JEditableTable的实例 |
- `返回值:` Promise
- `示例:`
```js
import { validateTables, VALIDATE_NO_PASSED } from '@/utils/JEditableTableUtil'
// 封装cases
let cases = []
cases.push(this.$refs.editableTable1)
cases.push(this.$refs.editableTable2)
cases.push(this.$refs.editableTable3)
cases.push(this.$refs.editableTable4)
cases.push(this.$refs.editableTable5)
// 同时验证并获取多个实例的值
validateTables(cases).then((all) => {
// all 是一个数组,每项都对应传入cases的下标,包含values和deleteIds
console.log('所有实例的值:', all)
}).catch((e = {}) => {
// 判断表单验证是否未通过
if (e.error === VALIDATE_NO_PASSED) {
console.log('未通过验证的实例下标:', e.index)
} else {
console.error('发生异常:', e)
}
})
```
## FAQ
### 方法如何调用?
在[示例一](#示例一)中,设定了一个 `ref="editableTable"` 的属性,那么在vue中就可以使用`this.$refs.editableTable`获取到该表格的实例,并调取其中的方法。
假如我要调取`initialize`方法,就可以这么写:`this.$refs.editableTable.initialize()`
### 如何获取表单的值?
使用`getValue`方法进行获取,详见[示例三](#示例三)
### 如何进行表单验证?
在获取值的时候默认会进行表单验证操作,用户在输入的时候也会对正在输入的表单进行验证,只要配置好规则就可以了
### 如何添加或删除一行?
该功能已封装到组件中,你只需要将 `actionButton` 设置为 `true` 即可,当然你也可以在代码中主动调用新增方法或修改,具体见上方的方法介绍。
### 为什么使用了ATab组件后,切换选项卡会导致白屏或滚动条位置会归零?
在ATab组件中确实会导致滚动条位置归零,且不会触发`onscroll`方法,所以无法动态加载行,导致白屏的问题出现。
解决方法是在ATab组件的`onChange`事件触发时执行实例提供的`resetScrollTop()`方法即可,但是需要注意的是:代码主动改变ATab的`activeKey`不会触发`onChange`事件,还需要你手动调用下。
- `示例`
```html
<template>
<a-tabs @change="handleChangeTab">
<a-tab-pane tab="表格1" :forceRender="true" key="1">
<j-editable-table
ref="editableTable1"
:loading="tab1.loading"
:columns="tab1.columns"
:dataSource="tab1.dataSource"/>
</a-tab-pane>
<a-tab-pane tab="表格2" :forceRender="true" key="2">
<j-editable-table
ref="editableTable2"
:loading="tab2.loading"
:columns="tab2.columns"
:dataSource="tab2.dataSource"/>
</a-tab-pane>
</a-tabs>
</template>
```
```js
/*--- 忽略部分代码片段 ---*/
methods: {
/** 切换tab选项卡的时候重置editableTable的滚动条状态 */
handleChangeTab(key) {
this.$refs[`editableTable${key}`].resetScrollTop()
}
}
/*--- 忽略部分代码片段 ---*/
```
### slot(自定义插槽)如何使用?
代码示例请看:[示例四(slot)](#示例四(slot))
----------------------------------------------------------------------------------------
## 示例一
```html
<j-editable-table
ref="editableTable"
:loading="loading"
:columns="columns"
:dataSource="dataSource"
:rowNumber="true"
:rowSelection="true"
:actionButton="true"
style="margin-top: 8px;"
@selectRowChange="handleSelectRowChange"/>
```
## 示例二
```js
import { FormTypes } from '@/utils/JEditableTableUtil'
/*--- 忽略部分代码片断 ---*/
columns: [
{
title: '名称',
key: 'name',
type: FormTypes.input,
placeholder: '请输入${title}',
defaultValue: '称名',
// 表单验证规则
validateRules: [
{
required: true, // 必填
message: '${title}不能为空' // 提示的文本
},
{
pattern: /^[a-z|A-Z][a-z|A-Z\d_-]{0,}$/, // 正则
message: '${title}必须以字母开头,可包含数字、下划线、横杠'
}
]
},
{
title: '年龄',
key: 'age',
type: FormTypes.inputNumber,
placeholder: '请输入${title}',
defaultValue: 18,
validateRules: [{required: true, message: '${title}不能为空'}]
}
]
/*--- 忽略部分代码片断 ---*/
```
## 示例三
```js
// 获取被逻辑删除的字段id
let deleteIds = this.$refs.editableTable.getDeleteIds();
// 获取所有表单的值,并进行验证
this.$refs.editableTable.getValues((error, values) => {
// 错误数 = 0 则代表验证通过
if (error === 0) {
this.$message.success('验证通过')
// 将通过后的数组提交到后台或自行进行其他处理
console.log(deleteIds, values)
} else {
this.$message.warning('验证未通过')
}
})
```
## 示例四(slot)
```html
<template>
<j-editable-table :columns="columns" :dataSource="dataSource">
<!-- 定义插槽 -->
<!-- 这种定义插槽的写法是vue推荐的新版写法(https://cn.vuejs.org/v2/guide/components-slots.html#具名插槽),旧版已被废弃的写法不再支持 -->
<!-- 若webstorm这样写报错,请看这篇文章:https://blog.csdn.net/lxq_9532/article/details/81870651 -->
<template v-slot:action="props">
<a @click="handleDelete(props)">删除</a>
</template>
</j-editable-table>
</template>
<script>
import { FormTypes } from '@/utils/JEditableTableUtil'
import JEditableTable from '@/components/jero/JEditableTable'
export default {
components: { JEditableTable },
data() {
return {
columns: [
// ...
{
title: '操作',
key: 'action',
width: '8%',
type: FormTypes.slot, // 定义该列为 自定义插值列
slotName: 'action' // slot 的名称,对应 v-slot 冒号后面和等号前面的内容
}
]
}
},
methods: {
/* a 标签的点击事件,删除当前选中的行 */
handleDelete(props) {
// 参数解释
// props.index :当前行的下标
// props.text :当前值,可能是defaultValue定义的值,也可能是从dataSource中取出的值
// props.rowId :当前选中行的id,如果是新增行则是临时id
// props.column :当前操作的列
// props.getValue :这是一个function,执行后可以获取当前行的所有值(禁止在template中使用)
// 例:const value = props.getValue()
// props.target :触发当前事件的实例,可直接调用该实例内的方法(禁止在template中使用)
// 例:target.add()
// 使用实例:删除当前操作的行
let { rowId, target } = props
target.removeRows(rowId)
}
}
}
</script>
```
## 示例五
```js
// 该示例是自定义函数校验
columns: [
{
title: '字段名称',
key: 'dbFieldName',
type: FormTypes.input,
defaultValue: '',
validateRules: [
{
// 自定义函数校验 handler
handler(type, value, row, column, callback, target) {
// type 触发校验的类型(input、change、blur
// value 当前校验的值
// callback(flag, message) 方法必须执行且只能执行一次
// flag = 是否通过了校验,不填写或者填写 null 代表不进行任何操作
// message = 提示的类型,默认使用配置的 message
// target 行编辑的实例对象
if (type === 'blur') {
if (value === 'abc') {
callback(false, '${title}不能是abc') // false = 未通过,可以跟自定义提示
return
}
let { values } = target.getValuesSync({ validate: false })
let count = 0
for (let val of values) {
if (val['dbFieldName'] === value) {
if (++count >= 2) {
callback(false, '${title}不能重复')
return
}
}
}
callback(true) // true = 通过验证
} else {
callback() // 不填写或者填写 null 代表不进行任何操作
}
},
message: '${title}默认提示'
}
]
},
]
```
+65
View File
@@ -0,0 +1,65 @@
# JPopup 弹窗选择组件
## 参数配置
| 参数 | 类型 | 必填 |说明|
|--------------|---------|----|---------|
| placeholder |string | | placeholder |
| code |string | | online报表编码 |
| orgFields |string | | online报表中显示的列,多个以逗号隔开 |
| destFields |string | | 回调对象的属性,多个以逗号隔开,其顺序和orgFields一一对应 |
| field |string | | v-model模式专用,表示从destFields中选择一个属性的值返回给当前组件 |
| triggerChange |Boolean | | v-decorator模式下需设置成true |
| callback(事件) |function | | 回调事件,v-decorator模式下用到,用于设置form控件的值 |
使用示例
----
```vue
<template>
<a-form :form="form">
<a-form-item label="v-model模式指定一个值返回至当前组件" style="width: 300px">
<j-popup
v-model="selectValue"
code="user_msg"
org-fields="username,realname"
dest-fields="popup,other"
field="popup"/>
{{ selectValue }}
</a-form-item>
<a-form-item label="v-decorator模式支持回调多个值至当前表单" style="width: 300px">
<j-popup
v-decorator="['one']"
:trigger-change="true"
code="user_msg"
org-fields="username,realname"
dest-fields="one,two"
@callback="popupCallback"/>
{{ getFormFieldValue('one') }}
</a-form-item>
<a-form-item label="v-decorator模式被回调的值" style="width: 300px">
<a-input v-decorator="['two']"></a-input>
</a-form-item>
</a-form >
</template>
<script>
export default {
data() {
return {
form: this.$form.createForm(this),
selectValue:"",
}
},
methods:{
getFormFieldValue(field){
return this.form.getFieldValue(field)
},
popupCallback(row){
this.form.setFieldsValue(row)
}
}
}
</script>
+283
View File
@@ -0,0 +1,283 @@
# JTable 支持列自定义及可拖拽列宽的表格
## JTable参数配置
| 参数 | 类型 | 必填 | 说明 |
|--------------|--------|----|---------------------------------------------------------------|
| tableKey | String | | 全局`JTable`唯一,持久化存储自定义列配置 |
| columns | Array | ✔ | **需要配合`.sync`获取最新的数据**,具体项见下表 |
| settingStyle | Object | | 自定义列配置表的样式 |
| settingScroll | Object | | 自定义列配置表的滚动配置 |
| scroll | Object | | 表格滚动配置,建议使用拖拽属性`resizable`时设置`scroll.x`**需要配合`.sync`获取最新的数据** |
| columnMinWidth | Number | | 所有列共用的最小宽度,在没有`width``minWidth`时的冻结列和拖拽列的最小宽度 |
| needSettingColumnHideOrFreeze | Boolean | | 是否需要设置冻结列和显隐列 |
| needSaveSettingInLocal | Boolean | | 是否需要将冻结和显隐的设置存储在localStorage中 |
| canDrag | Boolean | | 是否可拖拽 |
## columns参数配置
| 参数 | 类型 | 必填 | 说明 |
|-------------------|----------------|----|------------------------------------------------------|
| hideSettingColumn | Boolean | | 是否在自定义列配置表中隐藏 |
| disabledHide | Boolean | | 是否禁用自定义列配置表中的隐藏复选框 |
| disabledFreeze | Boolean | | 是否禁用自定义列配置表中的冻结复选框 |
| fixed | String、Boolean | | 默认的冻结列,只能为`true``'left'`其他不生效 |
| width | Number | | 列宽,只能是数字 |
| minWidth | Number | | 最小列宽,只能是数字,在冻结列和拖拽列时生效 |
| resizable | Boolean | | 是否启用拖拽,需要表格显示边框`bordered`(这个属性因为加了总体的参数canDrag,暂时无效) |
## JTable的方法
### clearAllCacheSetting
用于清理所有JTable的缓存
- `参数:`
- `返回值:`
### clearSetting
用于清理当前表的缓存
- `参数:`
- `返回值:`
### resteColumns
还原初始的配置
- `参数:`
- `返回值:`
## FAQ
### 方法如何调用?
在[示例](#示例)中,设定了一个 `ref="table"` 的属性,那么在vue中就可以使用`this.$refs.table`获取到该表格的实例,并调取其中的方法。
假如我要调取`resteColumns`方法,就可以这么写:`this.$refs.table.resteColumns()`
### `columns`和`scroll`为什么要使用`.sync`
保证父组件和子组件数据同步
`columns`自定义列配置后会同步,
`scroll`拖拽后会把最新的总宽度同步
### 对原`a-table`的使用有哪些限制
- 操作列的`dataIndex``key`必须为`action`
- 带有设置的列不能使用`scopedSlots.filterIcon``scopedSlots.filterDropdown`(没有操作列时,设置在最后一列)
- `scroll.x``width`必须是数字,不支持百分比
- 使用作用域插槽,需要使用解构赋值,里面有三个参数`{text, record, index}`,可参考[示例](#示例)写法
### 拖拽后没有设置宽度的列会被缩小
- 设置scroll.x配置一个默认宽度即可
### 出现空白列
- 设置至少一列没有width(没有width不能被拖拽)
- 建议没有`width`的列不能手动控制隐藏和冻结,使用`disabledHide``disabledFreeze`或者`hideSettingColumn`
### `resizable`没有效果
- 需要设置`resizable: true`,要有`width`,且不能有`fixed`
### 自定义列的弹框会消失
- 因为冻结列的出现或消失会刷新表格结构导致弹框丢失
- 设置一个冻结列始终存在,不可被手动控制隐藏和取消冻结即可
### 注意事项
- `table-key`该属性为全局每个表的唯一属性,建议使用路由+命名的方式,如[示例](#示例)中设置
- 固定头和列(ant-design-vue自带的问题) :若列头与内容不对齐或出现列重复,请指定固定列的宽度 width。如果指定 width 不生效或出现白色垂直空隙,请尝试建议留一列不设宽度以适应弹性布局,或者检查是否有超长连续字段破坏布局。
建议指定 scroll.x 为大于表格宽度的固定值。注意,且非固定列宽度之和不要超过 `scroll.x`
- 如果表头是从后端获取的,需要写v-if="columns && columns.length > 0",引入ConfigurableTableMixin混入
## 示例
```vue
<template>
<div>
<div style="margin-bottom: 5px;">
<a-button @click="resetTable">重置表格配置</a-button>
<a-button @click="clearTableCache" style="margin-left: 5px;">清空本表缓存</a-button>
<a-button @click="clearAllTableCache" style="margin-left: 5px;">清空全局JTable缓存</a-button>
</div>
<j-table bordered
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
:table-key="$route.name + '_table1'"
ref="table"
rowKey="key"
:data-source="dataSource"
:scroll.sync="scroll"
:columns.sync="columns">
<!-- 使用插槽 -->
<a slot="name" slot-scope="{text}">{{ text }}</a>
<span slot="customTitle"><a-icon type="smile-o" /> Name</span>
<span slot="tags" slot-scope="{text: tags}">
<a-tag
v-for="tag in tags"
:key="tag"
:color="tag === 'loser' ? 'volcano' : tag.length > 5 ? 'geekblue' : 'green'"
>
{{ tag.toUpperCase() }}
</a-tag>
</span>
<span slot="action" slot-scope="{record}">
<a-popconfirm
v-if="dataSource.length"
title="是否删除?"
@confirm="() => onDelete(record.key)"
>
<a href="javascript:">Delete</a>
</a-popconfirm>
</span>
<template slot="footer" slot-scope="currentPageData">
Footer{{ currentPageData }}
</template>
</j-table>
</div>
</template>
<script>
import JTable from '@comp/jero/JTable'
export default {
name: 'Demo',
components: { JTable },
data () {
const columns = [
{
// 是否在配置表中隐藏
// hideSettingColumn: true,
// 尽量都设置最小宽度,冻结列与拖拽时生效
minWidth: 100,
width: 100,
// 禁用配置表中的隐藏或冻结
disabledHide: true,
disabledFreeze: true,
// 可拖拽属性(不能fixed一起用)
// resizable: true,
// 默认冻结,值可以是 'left' 或 true
fixed: true,
title: '#',
ellipsis: true,
dataIndex: 'key'
},
{
resizable: true,
title: 'Date',
dataIndex: 'date',
minWidth: 200,
width: 200
},
{
// 标题使用插槽使用这个代替title(用于设置里显示列名)
columnTitle: 'Name',
dataIndex: 'name',
key: 'name',
minWidth: 100,
slots: { title: 'customTitle' },
scopedSlots: { customRender: 'name' }
},
{
title: 'Tags',
key: 'tags',
dataIndex: 'tags',
minWidth: 200,
scopedSlots: { customRender: 'tags' }
},
{
resizable: true, // 可拖拽属性
title: 'Amount',
dataIndex: 'amount',
minWidth: 100,
width: 100
},
{
title: 'Type',
dataIndex: 'type',
width: 100
},
{
title: 'Note',
dataIndex: 'note',
// 保留一列自适应
// width: 100
// 建议:没有宽度的列至少一列禁止自定义(解决冻结或隐藏列导致出现空列)
// disabledHide: true,
// disabledFreeze: true,
hideSettingColumn: true
},
{
title: 'Action',
key: 'action',
width: 200,
// filterIcon 和 filterDropdown 不能使用
scopedSlots: { customRender: 'action' }
}
]
const dataSource = [
{
key: 0,
name: 'John Brown',
tags: ['nice', 'developer'],
date: '2018-02-11',
amount: 120,
type: 'income',
note: 'transfer'
},
{
key: 1,
name: 'Jim Green',
tags: ['loser'],
date: '2018-03-11',
amount: 243,
type: 'income',
note: 'transfer'
},
{
key: 2,
name: 'Joe Black',
tags: ['cool', 'teacher'],
date: '2018-04-11',
amount: 98,
type: 'income',
note: 'transfer'
}
]
return {
scroll: { x: 1400 },
columns: columns,
dataSource: dataSource,
selectedRowKeys: []
}
},
methods: {
// 还原表格初始配置
resetTable () {
this.$refs.table.resteColumns()
},
// 清除当前表格缓存
clearTableCache () {
this.$refs.table.clearSetting()
},
// 清除所有缓存
clearAllTableCache () {
this.$refs.table.clearAllCacheSetting()
},
onDelete (key) {
this.$message.info(`del:${key}`)
},
onSelectChange (selectedRowKeys) {
console.log('selectedRowKeys changed: ', selectedRowKeys)
this.selectedRowKeys = selectedRowKeys
}
}
}
</script>
```
+92
View File
@@ -0,0 +1,92 @@
import JModal from './JModal'
import JFormContainer from './JFormContainer.vue'
import JPopup from './JPopup.vue'
import JMarkdownEditor from './JMarkdownEditor'
import JCodeEditor from './JCodeEditor.vue'
import JEditor from './JEditor.vue'
import JEditableTable from './JEditableTable.vue'
import JAreaLinkage from './JAreaLinkage.vue'
import JSuperQuery from './JSuperQuery.vue'
import JUpload from './JUpload.vue'
import JTreeSelect from './JTreeSelect.vue'
import JCategorySelect from './JCategorySelect.vue'
import JImageUpload from './JImageUpload.vue'
import JImportModal from './JImportModal.vue'
import JTreeDict from './JTreeDict.vue'
import JCheckbox from './JCheckbox.vue'
import JCron from './JCron.vue'
import JDate from './JDate.vue'
import JEllipsis from './JEllipsis.vue'
import JInput from './JInput.vue'
import JPopupOnlReport from './modal/JPopupOnlReport.vue'
import JFilePop from './minipop/JFilePop.vue'
import JInputPop from './minipop/JInputPop.vue'
import JSelectMultiple from './JSelectMultiple.vue'
import JSlider from './JSlider.vue'
import JSwitch from './JSwitch.vue'
import JTime from './JTime.vue'
import JTreeTable from './JTreeTable.vue'
import JEasyCron from '@/components/jero/JEasyCron'
// jerobiz
import JSelectDepart from '../jerobiz/JSelectDepart.vue'
import JSelectMultiUser from '../jerobiz/JSelectMultiUser.vue'
import JSelectRole from '../jerobiz/JSelectRole.vue'
import JSelectUserByDep from '../jerobiz/JSelectUserByDep.vue'
// 引入需要全局注册的js函数和变量
import { Modal, notification, message } from 'ant-design-vue'
// eslint-disable-next-line camelcase
import lodash_object from 'lodash'
import debounce from 'lodash/debounce'
import pick from 'lodash.pick'
import data from 'china-area-data'
export default {
install (Vue) {
Vue.use(JModal)
Vue.component('JMarkdownEditor', JMarkdownEditor)
Vue.component('JPopupOnlReport', JPopupOnlReport)
Vue.component('JFilePop', JFilePop)
Vue.component('JInputPop', JInputPop)
Vue.component('JAreaLinkage', JAreaLinkage)
Vue.component('JCategorySelect', JCategorySelect)
Vue.component('JCheckbox', JCheckbox)
Vue.component('JCodeEditor', JCodeEditor)
Vue.component('JCron', JCron)
Vue.component('JDate', JDate)
Vue.component('JEditableTable', JEditableTable)
Vue.component('JEditor', JEditor)
Vue.component('JEllipsis', JEllipsis)
Vue.component('JFormContainer', JFormContainer)
Vue.component('JImageUpload', JImageUpload)
Vue.component('JImportModal', JImportModal)
Vue.component('JInput', JInput)
Vue.component('JPopup', JPopup)
Vue.component('JSelectMultiple', JSelectMultiple)
Vue.component('JSlider', JSlider)
Vue.component('JSuperQuery', JSuperQuery)
Vue.component('JSwitch', JSwitch)
Vue.component('JTime', JTime)
Vue.component('JTreeDict', JTreeDict)
Vue.component('JTreeSelect', JTreeSelect)
Vue.component('JTreeTable', JTreeTable)
Vue.component('JUpload', JUpload)
// jerobiz
Vue.component('JSelectDepart', JSelectDepart)
Vue.component('JSelectMultiUser', JSelectMultiUser)
Vue.component('JSelectRole', JSelectRole)
Vue.component('JSelectUserByDep', JSelectUserByDep)
Vue.component(JEasyCron.name, JEasyCron)
// 注册全局js函数和变量
Vue.prototype.$Jnotification = notification
Vue.prototype.$Jmodal = Modal
Vue.prototype.$Jmessage = message
// eslint-disable-next-line camelcase
Vue.prototype.$Jlodash = lodash_object
Vue.prototype.$Jdebounce = debounce
Vue.prototype.$Jpick = pick
Vue.prototype.$Jpcaa = data
}
}
+116
View File
@@ -0,0 +1,116 @@
<template>
<div>
<a-modal
:title="fileType === 'image' ? '图片上传' : '文件上传'"
:width="width"
:visible="visible"
@ok="ok"
cancelText="取消"
@cancel="close">
<!--style="top: 20px;"-->
<j-upload :file-type="fileType" :value="filePath" @change="handleChange" :disabled="disabled" :number="number"></j-upload>
</a-modal>
</div>
</template>
<script>
import { getFileAccessHttpUrl } from '@/api/manage'
const getFileName = (path) => {
if (path.lastIndexOf('\\') >= 0) {
const reg = new RegExp('\\\\', 'g')
path = path.replace(reg, '/')
}
return path.substring(path.lastIndexOf('/') + 1)
}
export default {
name: 'JFilePop',
components: { },
props: {
title: {
type: String,
default: '',
required: false
},
position: {
type: String,
default: 'right',
required: false
},
height: {
type: Number,
default: 200,
required: false
},
width: {
type: Number,
default: 520,
required: false
},
popContainer: {
type: String,
default: '',
required: false
},
disabled: {
type: Boolean,
default: false,
required: false
},
number: {
type: Number,
required: false,
default: 0
}
},
data () {
return {
visible: false,
filePath: '',
id: '',
fileType: 'file'
}
},
methods: {
handleChange (value) {
this.filePath = value
},
show (id, value, flag) {
this.id = id
this.filePath = value
this.visible = true
if (flag === 'img') {
this.fileType = 'image'
} else {
this.fileType = 'file'
}
},
ok () {
if (!this.filePath) {
this.$message.warning('未上传任何文件')
return false
}
const arr = this.filePath.split(',')
const obj = {
name: getFileName(arr[0]),
url: getFileAccessHttpUrl(arr[0]),
path: this.filePath,
status: 'done',
id: this.id
}
this.$emit('ok', obj)
this.visible = false
},
close () {
this.visible = false
}
}
}
</script>
<style scoped>
</style>
+104
View File
@@ -0,0 +1,104 @@
<template>
<a-popover trigger="contextmenu" v-model="visible" :placement="position" overlayClassName="j-input-pop">
<!--"(node) => node.parentNode.parentNode"-->
<div slot="title">
<span>{{ title }}</span>
<span style="float: right" title="关闭">
<a-icon type="close" @click="visible=false"/>
</span>
</div>
<a-input :value="inputContent" :disabled="disabled" @change="handleInputChange">
<a-icon slot="suffix" type="fullscreen" @click.stop="pop" />
</a-input>
<div slot="content">
<a-textarea ref="textarea" :value="inputContent" :disabled="disabled" @input="handleInputChange" :style="{ height: height + 'px', width: width + 'px' }"/>
</div>
</a-popover>
</template>
<script>
export default {
name: 'JInputPop',
props: {
title: {
type: String,
default: '',
required: false
},
position: {
type: String,
default: 'right',
required: false
},
height: {
type: Number,
default: 200,
required: false
},
width: {
type: Number,
default: 150,
required: false
},
value: {
type: String,
required: false
},
popContainer: {
type: String,
default: '',
required: false
},
disabled: {
type: Boolean,
default: false
}
},
data () {
return {
visible: false,
inputContent: ''
}
},
watch: {
value: {
immediate: true,
handler: function () {
if (this.value && this.value.length > 0) {
this.inputContent = this.value
}
}
}
},
model: {
prop: 'value',
event: 'change'
},
methods: {
handleInputChange (event) {
this.inputContent = event.target.value
this.$emit('change', this.inputContent)
},
pop () {
this.visible = true
this.$nextTick(() => {
this.$refs.textarea.focus()
})
},
getPopupContainer (node) {
if (!this.popContainer) {
return node.parentNode
} else {
return document.getElementById(this.popContainer)
}
}
}
}
</script>
<style scoped>
</style>
+925
View File
@@ -0,0 +1,925 @@
<template>
<a-modal
title="cron表达式"
:width="modalWidth"
:visible="visible"
:confirmLoading="confirmLoading"
@ok="handleSubmit"
@cancel="close"
cancelText="关闭">
<div class="card-container">
<a-tabs type="card">
<a-tab-pane key="1" type="card">
<span slot="tab"><a-icon type="schedule" /> </span>
<a-radio-group v-model="result.second.cronEvery">
<a-row>
<a-radio value="1">每一秒钟</a-radio>
</a-row>
<a-row>
<a-radio value="2">每隔
<a-input-number size="small" v-model="result.second.incrementIncrement" :min="1" :max="59"></a-input-number>
秒执行
<a-input-number size="small" v-model="result.second.incrementStart" :min="0" :max="59"></a-input-number>
秒开始
</a-radio>
</a-row>
<a-row>
<a-radio value="3">具体秒数(可多选)</a-radio>
<a-select style="width:354px;" size="small" mode="multiple" v-model="result.second.specificSpecific">
<a-select-option v-for="(val,index) in 60" :key="index" :value="index">{{ index }}</a-select-option>
</a-select>
</a-row>
<a-row>
<a-radio value="4">周期从
<a-input-number size="small" v-model="result.second.rangeStart" :min="1" :max="59"></a-input-number>
<a-input-number size="small" v-model="result.second.rangeEnd" :min="0" :max="59"></a-input-number>
</a-radio>
</a-row>
</a-radio-group>
</a-tab-pane>
<a-tab-pane key="2">
<span slot="tab"><a-icon type="schedule" /></span>
<div class="tabBody">
<a-radio-group v-model="result.minute.cronEvery">
<a-row>
<a-radio value="1">每一分钟</a-radio>
</a-row>
<a-row>
<a-radio value="2">每隔
<a-input-number size="small" v-model="result.minute.incrementIncrement" :min="1" :max="60"></a-input-number>
分执行
<a-input-number size="small" v-model="result.minute.incrementStart" :min="0" :max="59"></a-input-number>
分开始
</a-radio>
</a-row>
<a-row>
<a-radio value="3">具体分钟数(可多选)</a-radio>
<a-select style="width:340px;" size="small" mode="multiple" v-model="result.minute.specificSpecific">
<a-select-option v-for="(val,index) in Array(60)" :key="index" :value="index"> {{ index }}</a-select-option>
</a-select>
</a-row>
<a-row>
<a-radio value="4">周期从
<a-input-number size="small" v-model="result.minute.rangeStart" :min="1" :max="60"></a-input-number>
<a-input-number size="small" v-model="result.minute.rangeEnd" :min="0" :max="59"></a-input-number>
</a-radio>
</a-row>
</a-radio-group>
</div>
</a-tab-pane>
<a-tab-pane key="3">
<span slot="tab"><a-icon type="schedule" /> </span>
<div class="tabBody">
<a-radio-group v-model="result.hour.cronEvery">
<a-row>
<a-radio value="1">每一小时</a-radio>
</a-row>
<a-row>
<a-radio value="2">每隔
<a-input-number size="small" v-model="result.hour.incrementIncrement" :min="0" :max="23"></a-input-number>
小时执行
<a-input-number size="small" v-model="result.hour.incrementStart" :min="0" :max="23"></a-input-number>
小时开始
</a-radio>
</a-row>
<a-row>
<a-radio class="long" value="3">具体小时数(可多选)</a-radio>
<a-select style="width:340px;" size="small" mode="multiple" v-model="result.hour.specificSpecific">
<a-select-option v-for="(val,index) in Array(24)" :key="index" >{{ index }}</a-select-option>
</a-select>
</a-row>
<a-row>
<a-radio value="4">周期从
<a-input-number size="small" v-model="result.hour.rangeStart" :min="0" :max="23"></a-input-number>
<a-input-number size="small" v-model="result.hour.rangeEnd" :min="0" :max="23"></a-input-number>
小时
</a-radio>
</a-row>
</a-radio-group>
</div>
</a-tab-pane>
<a-tab-pane key="4">
<span slot="tab"><a-icon type="schedule" /> </span>
<div class="tabBody">
<a-radio-group v-model="result.day.cronEvery">
<a-row>
<a-radio value="1">每一天</a-radio>
</a-row>
<a-row>
<a-radio value="2">每隔
<a-input-number size="small" v-model="result.week.incrementIncrement" :min="1" :max="7"></a-input-number>
周执行
<a-select size="small" v-model="result.week.incrementStart">
<a-select-option v-for="(val,index) in Array(7)" :key="index" :value="index+1">{{ weekDays[index] }}</a-select-option>
</a-select>
开始
</a-radio>
</a-row>
<a-row>
<a-radio value="3">每隔
<a-input-number size="small" v-model="result.day.incrementIncrement" :min="1" :max="31"></a-input-number>
天执行
<a-input-number size="small" v-model="result.day.incrementStart" :min="1" :max="31"></a-input-number>
天开始
</a-radio>
</a-row>
<a-row>
<a-radio class="long" value="4">具体星期几(可多选)</a-radio>
<a-select style="width:340px;" size="small" mode="multiple" v-model="result.week.specificSpecific">
<a-select-option v-for="(val,index) in Array(7)" :key="index" :value="index+1">{{ weekDays[index] }}</a-select-option>
</a-select>
</a-row>
<a-row>
<a-radio class="long" value="5">具体天数(可多选)</a-radio>
<a-select style="width:354px;" size="small" mode="multiple" v-model="result.day.specificSpecific">
<a-select-option v-for="(val,index) in Array(31)" :key="index" :value="index+1">{{ index+1 }}</a-select-option>
</a-select>
</a-row>
<a-row>
<a-radio value="6">在这个月的最后一天</a-radio>
</a-row>
<a-row>
<a-radio value="7">在这个月的最后一个工作日</a-radio>
</a-row>
<a-row>
<a-radio value="8">在这个月的最后一个
<a-select size="small" v-model="result.day.cronLastSpecificDomDay">
<a-select-option v-for="(val,index) in Array(7)" :key="index" :value="index+1">{{ weekDays[index] }}</a-select-option>
</a-select>
</a-radio>
</a-row>
<a-row>
<a-radio value="9">
在本月底前
<a-input-number size="small" v-model="result.day.cronDaysBeforeEomMinus" :min="1" :max="31"></a-input-number>
</a-radio>
</a-row>
<a-row>
<a-radio value="10">最近的工作日周一至周五至本月
<a-input-number size="small" v-model="result.day.cronDaysNearestWeekday" :min="1" :max="31"></a-input-number>
</a-radio>
</a-row>
<a-row>
<a-radio value="11">在这个月的第
<a-input-number size="small" v-model="result.week.cronNthDayNth" :min="1" :max="5"></a-input-number>
<a-select size="small" v-model="result.week.cronNthDayDay">
<a-select-option v-for="(val,index) in Array(7)" :key="index" :value="index+1">{{ weekDays[index] }}</a-select-option>
</a-select>
</a-radio>
</a-row>
</a-radio-group>
</div>
</a-tab-pane>
<a-tab-pane key="5">
<span slot="tab"><a-icon type="schedule" /> </span>
<div class="tabBody">
<a-radio-group v-model="result.month.cronEvery">
<a-row>
<a-radio value="1">每一月</a-radio>
</a-row>
<a-row>
<a-radio value="2">每隔
<a-input-number size="small" v-model="result.month.incrementIncrement" :min="0" :max="12"></a-input-number>
月执行
<a-input-number size="small" v-model="result.month.incrementStart" :min="0" :max="12"></a-input-number>
月开始
</a-radio>
</a-row>
<a-row>
<a-radio class="long" value="3">具体月数(可多选)</a-radio>
<a-select style="width:354px;" size="small" filterable mode="multiple" v-model="result.month.specificSpecific">
<a-select-option v-for="(val,index) in Array(12)" :key="index" :value="index+1">{{ index+1 }}</a-select-option>
</a-select>
</a-row>
<a-row>
<a-radio value="4">
<a-input-number size="small" v-model="result.month.rangeStart" :min="1" :max="12"></a-input-number>
<a-input-number size="small" v-model="result.month.rangeEnd" :min="1" :max="12"></a-input-number>
月之间的每个月
</a-radio>
</a-row>
</a-radio-group>
</div>
</a-tab-pane>
<a-tab-pane key="6">
<span slot="tab"><a-icon type="schedule" /> </span>
<div class="tabBody">
<a-radio-group v-model="result.year.cronEvery">
<a-row>
<a-radio value="1">每一年</a-radio>
</a-row>
<a-row>
<a-radio value="2">每隔
<a-input-number size="small" v-model="result.year.incrementIncrement" :min="1" :max="99"></a-input-number>
年执行
<a-input-number size="small" v-model="result.year.incrementStart" :min="2019" :max="2119"></a-input-number>
年开始
</a-radio>
</a-row>
<a-row>
<a-radio class="long" value="3">具体年份(可多选)</a-radio>
<a-select style="width:354px;" size="small" filterable mode="multiple" v-model="result.year.specificSpecific">
<a-select-option v-for="(val,index) in Array(100)" :key="index" :value="2019+index">{{ 2019+index }}</a-select-option>
</a-select>
</a-row>
<a-row>
<a-radio value="4">
<a-input-number size="small" v-model="result.year.rangeStart" :min="2019" :max="2119"></a-input-number>
<a-input-number size="small" v-model="result.year.rangeEnd" :min="2019" :max="2119"></a-input-number>
年之间的每一年
</a-radio>
</a-row>
</a-radio-group>
</div>
</a-tab-pane>
</a-tabs>
<div class="bottom">
<span class="value">{{this.cron }}</span>
</div>
</div>
</a-modal>
</template>
<script>
export default {
name: 'VueCron',
props: ['data'],
data () {
return {
visible: false,
confirmLoading: false,
size: 'large',
weekDays: ['天', '一', '二', '三', '四', '五', '六'].map(val => '星期' + val),
result: {
second: {},
minute: {},
hour: {},
day: {},
week: {},
month: {},
year: {}
},
defaultValue: {
second: {
cronEvery: '',
incrementStart: 3,
incrementIncrement: 5,
rangeStart: 1,
rangeEnd: 0,
specificSpecific: []
},
minute: {
cronEvery: '',
incrementStart: 3,
incrementIncrement: 5,
rangeStart: 1,
rangeEnd: '0',
specificSpecific: []
},
hour: {
cronEvery: '',
incrementStart: 3,
incrementIncrement: 5,
rangeStart: '0',
rangeEnd: '0',
specificSpecific: []
},
day: {
cronEvery: '',
incrementStart: 1,
incrementIncrement: '1',
rangeStart: '',
rangeEnd: '',
specificSpecific: [],
cronLastSpecificDomDay: 1,
cronDaysBeforeEomMinus: 1,
cronDaysNearestWeekday: 1
},
week: {
cronEvery: '',
incrementStart: 1,
incrementIncrement: 1,
specificSpecific: [],
cronNthDayDay: 1,
cronNthDayNth: 1
},
month: {
cronEvery: '',
incrementStart: 3,
incrementIncrement: 5,
rangeStart: 1,
rangeEnd: 1,
specificSpecific: []
},
year: {
cronEvery: '',
incrementStart: 2017,
incrementIncrement: 1,
rangeStart: 2019,
rangeEnd: 2019,
specificSpecific: []
},
label: ''
}
}
},
computed: {
modalWidth () {
return 608
},
secondsText () {
let seconds = ''
const cronEvery = this.result.second.cronEvery || ''
switch (cronEvery.toString()) {
case '1':
seconds = '*'
break
case '2':
seconds = this.result.second.incrementStart + '/' + this.result.second.incrementIncrement
break
case '3':
this.result.second.specificSpecific.map(val => { seconds += val + ',' })
seconds = seconds.slice(0, -1)
break
case '4':
seconds = this.result.second.rangeStart + '-' + this.result.second.rangeEnd
break
}
return seconds
},
minutesText () {
let minutes = ''
const cronEvery = this.result.minute.cronEvery || ''
switch (cronEvery.toString()) {
case '1':
minutes = '*'
break
case '2':
minutes = this.result.minute.incrementStart + '/' + this.result.minute.incrementIncrement
break
case '3':
this.result.minute.specificSpecific.map(val => {
minutes += val + ','
})
minutes = minutes.slice(0, -1)
break
case '4':
minutes = this.result.minute.rangeStart + '-' + this.result.minute.rangeEnd
break
}
return minutes
},
hoursText () {
let hours = ''
const cronEvery = this.result.hour.cronEvery || ''
switch (cronEvery.toString()) {
case '1':
hours = '*'
break
case '2':
hours = this.result.hour.incrementStart + '/' + this.result.hour.incrementIncrement
break
case '3':
this.result.hour.specificSpecific.map(val => {
hours += val + ','
})
hours = hours.slice(0, -1)
break
case '4':
hours = this.result.hour.rangeStart + '-' + this.result.hour.rangeEnd
break
}
return hours
},
daysText () {
let days = ''
const cronEvery = this.result.day.cronEvery || ''
switch (cronEvery.toString()) {
case '1':
break
case '2':
case '4':
case '11':
days = '?'
break
case '3':
days = this.result.day.incrementStart + '/' + this.result.day.incrementIncrement
break
case '5':
this.result.day.specificSpecific.map(val => {
days += val + ','
})
days = days.slice(0, -1)
break
case '6':
days = 'L'
break
case '7':
days = 'LW'
break
case '8':
days = this.result.day.cronLastSpecificDomDay + 'L'
break
case '9':
days = 'L-' + this.result.day.cronDaysBeforeEomMinus
break
case '10':
days = this.result.day.cronDaysNearestWeekday + 'W'
break
}
return days
},
weeksText () {
let weeks = ''
const cronEvery = this.result.day.cronEvery || ''
switch (cronEvery.toString()) {
case '1':
case '3':
case '5':
weeks = '?'
break
case '2':
weeks = this.result.week.incrementStart + '/' + this.result.week.incrementIncrement
break
case '4':
this.result.week.specificSpecific.map(val => {
weeks += val + ','
})
weeks = weeks.slice(0, -1)
break
case '6':
case '7':
case '8':
case '9':
case '10':
weeks = '?'
break
case '11':
weeks = this.result.week.cronNthDayDay + '#' + this.result.week.cronNthDayNth
break
}
return weeks
},
monthsText () {
let months = ''
const cronEvery = this.result.month.cronEvery || ''
switch (cronEvery.toString()) {
case '1':
months = '*'
break
case '2':
months = this.result.month.incrementStart + '/' + this.result.month.incrementIncrement
break
case '3':
this.result.month.specificSpecific.map(val => {
months += val + ','
})
months = months.slice(0, -1)
break
case '4':
months = this.result.month.rangeStart + '-' + this.result.month.rangeEnd
break
}
return months
},
yearsText () {
let years = ''
const cronEvery = this.result.year.cronEvery || ''
switch (cronEvery.toString()) {
case '1':
years = '*'
break
case '2':
years = this.result.year.incrementStart + '/' + this.result.year.incrementIncrement
break
case '3':
this.result.year.specificSpecific.map(val => {
years += val + ','
})
years = years.slice(0, -1)
break
case '4':
years = this.result.year.rangeStart + '-' + this.result.year.rangeEnd
break
}
return years
},
cron () {
return `${this.secondsText || '*'} ${this.minutesText || '*'} ${this.hoursText || '*'} ${this.daysText || '*'} ${this.monthsText || '*'} ${this.weeksText || '?'} ${this.yearsText || '*'}`
}
},
watch: {
visible: {
handler () {
// if(this.data){
// //this. result = Object.keys(this.data.value).length>0?this.deepCopy(this.data.value):this.deepCopy(this.defaultValue);
// //this.result = Object.keys(this.data.value).length>0?clone(this.data.value):clone(this.defaultValue);
// //this.result = Object.keys(this.data.value).length>0?clone(JSON.parse(this.data.value)):clone(this.defaultValue);
// this.result = Object.keys(this.data.value).length>0?JSON.parse(this.data.value):JSON.parse(JSON.stringify(this.defaultValue));
// }else{
// //this.result = this.deepCopy(this.defaultValue);
// //this.result = clone(this.defaultValue);
// this.result = JSON.parse(JSON.stringify(this.defaultValue));
// }
const label = this.data
if (label) {
this.secondsReverseExp(label)
this.minutesReverseExp(label)
this.hoursReverseExp(label)
this.daysReverseExp(label)
this.daysReverseExp(label)
this.monthsReverseExp(label)
this.yearReverseExp(label)
JSON.parse(JSON.stringify(label))
} else {
this.result = JSON.parse(JSON.stringify(this.defaultValue))
}
}
}
},
methods: {
show () {
this.visible = true
// console.log('secondsReverseExp',this.secondsReverseExp(this.data));
// console.log('minutesReverseExp',this.minutesReverseExp(this.data));
// console.log('hoursReverseExp',this.hoursReverseExp(this.data));
// console.log('daysReverseExp',this.daysReverseExp(this.data));
// console.log('monthsReverseExp',this.monthsReverseExp(this.data));
// console.log('yearReverseExp',this.yearReverseExp(this.data));
},
handleSubmit () {
this.$emit('ok', this.cron)
this.close()
this.visible = false
},
close () {
this.visible = false
},
secondsReverseExp (seconds) {
const val = seconds.split(' ')[0]
// alert(val);
const second = {
cronEvery: '',
incrementStart: 3,
incrementIncrement: 5,
rangeStart: 1,
rangeEnd: 0,
specificSpecific: []
}
switch (true) {
case val.includes('*'):
second.cronEvery = '1'
break
case val.includes('/'):
second.cronEvery = '2'
second.incrementStart = val.split('/')[0]
second.incrementIncrement = val.split('/')[1]
break
case val.includes(','):
second.cronEvery = '3'
second.specificSpecific = val.split(',').map(Number).sort()
break
case val.includes('-'):
second.cronEvery = '4'
second.rangeStart = val.split('-')[0]
second.rangeEnd = val.split('-')[1]
break
default:
second.cronEvery = '1'
}
this.result.second = second
},
minutesReverseExp (minutes) {
const val = minutes.split(' ')[1]
const minute = {
cronEvery: '',
incrementStart: 3,
incrementIncrement: 5,
rangeStart: 1,
rangeEnd: 0,
specificSpecific: []
}
switch (true) {
case val.includes('*'):
minute.cronEvery = '1'
break
case val.includes('/'):
minute.cronEvery = '2'
minute.incrementStart = val.split('/')[0]
minute.incrementIncrement = val.split('/')[1]
break
case val.includes(','):
minute.cronEvery = '3'
minute.specificSpecific = val.split(',').map(Number).sort()
break
case val.includes('-'):
minute.cronEvery = '4'
minute.rangeStart = val.split('-')[0]
minute.rangeEnd = val.split('-')[1]
break
default:
minute.cronEvery = '1'
}
this.result.minute = minute
},
hoursReverseExp (hours) {
const val = hours.split(' ')[2]
const hour = {
cronEvery: '',
incrementStart: 3,
incrementIncrement: 5,
rangeStart: 1,
rangeEnd: '0',
specificSpecific: []
}
switch (true) {
case val.includes('*'):
hour.cronEvery = '1'
break
case val.includes('/'):
hour.cronEvery = '2'
hour.incrementStart = val.split('/')[0]
hour.incrementIncrement = val.split('/')[1]
break
case val.includes(','):
hour.cronEvery = '3'
hour.specificSpecific = val.split(',').map(Number).sort()
break
case val.includes('-'):
hour.cronEvery = '4'
hour.rangeStart = val.split('-')[0]
hour.rangeEnd = val.split('-')[1]
break
default:
hour.cronEvery = '1'
}
this.result.hour = hour
},
daysReverseExp (cron) {
const days = cron.split(' ')[3]
const weeks = cron.split(' ')[5]
const day = {
cronEvery: '',
incrementStart: 1,
incrementIncrement: 1,
rangeStart: 1,
rangeEnd: 1,
specificSpecific: [],
cronLastSpecificDomDay: 1,
cronDaysBeforeEomMinus: 1,
cronDaysNearestWeekday: 1
}
const week = {
cronEvery: '',
incrementStart: 1,
incrementIncrement: 1,
specificSpecific: [],
cronNthDayDay: 1,
cronNthDayNth: '1'
}
if (!days.includes('?')) {
switch (true) {
case days.includes('*'):
day.cronEvery = '1'
break
case days.includes('?'):
// 2、4、11
break
case days.includes('/'):
day.cronEvery = '3'
day.incrementStart = days.split('/')[0]
day.incrementIncrement = days.split('/')[1]
break
case days.includes(','):
day.cronEvery = '5'
day.specificSpecific = days.split(',').map(Number).sort()
// day.specificSpecific.forEach(function (value, index) {
// day.specificSpecific[index] = value -1;
// });
break
case days.includes('LW'):
day.cronEvery = '7'
break
case days.includes('L-'):
day.cronEvery = '9'
day.cronDaysBeforeEomMinus = days.split('L-')[1]
break
case days.includes('L'):
// alert(days);
if (days.len + '' === '1') {
day.cronEvery = '6'
day.cronLastSpecificDomDay = '1'
} else {
day.cronEvery = '8'
day.cronLastSpecificDomDay = Number(days.split('L')[0])
}
break
case days.includes('W'):
day.cronEvery = '10'
day.cronDaysNearestWeekday = days.split('W')[0]
break
default:
day.cronEvery = '1'
}
} else {
switch (true) {
case weeks.includes('/'):
day.cronEvery = '2'
week.incrementStart = weeks.split('/')[0]
week.incrementIncrement = weeks.split('/')[1]
break
case weeks.includes(','):
day.cronEvery = '4'
week.specificSpecific = weeks.split(',').map(Number).sort()
break
case '#':
day.cronEvery = '11'
week.cronNthDayDay = weeks.split('#')[0]
week.cronNthDayNth = weeks.split('#')[1]
break
default:
day.cronEvery = '1'
week.cronEvery = '1'
}
}
this.result.day = day
this.result.week = week
},
monthsReverseExp (cron) {
const months = cron.split(' ')[4]
const month = {
cronEvery: '',
incrementStart: 3,
incrementIncrement: 5,
rangeStart: 1,
rangeEnd: 1,
specificSpecific: []
}
switch (true) {
case months.includes('*'):
month.cronEvery = '1'
break
case months.includes('/'):
month.cronEvery = '2'
month.incrementStart = months.split('/')[0]
month.incrementIncrement = months.split('/')[1]
break
case months.includes(','):
month.cronEvery = '3'
month.specificSpecific = months.split(',').map(Number).sort()
break
case months.includes('-'):
month.cronEvery = '4'
month.rangeStart = months.split('-')[0]
month.rangeEnd = months.split('-')[1]
break
default:
month.cronEvery = '1'
}
this.result.month = month
},
yearReverseExp (cron) {
const years = cron.split(' ')[6]
const year = {
cronEvery: '',
incrementStart: 3,
incrementIncrement: 5,
rangeStart: 2019,
rangeEnd: 2019,
specificSpecific: []
}
switch (true) {
case years.includes('*'):
year.cronEvery = '1'
break
case years.includes('/'):
year.cronEvery = '2'
year.incrementStart = years.split('/')[0]
year.incrementIncrement = years.split('/')[1]
break
case years.includes(','):
year.cronEvery = '3'
year.specificSpecific = years.split(',').map(Number).sort()
break
case years.includes('-'):
year.cronEvery = '4'
year.rangeStart = years.split('-')[0]
year.rangeEnd = years.split('-')[1]
break
default:
year.cronEvery = '1'
}
this.result.year = year
}
}
}
</script>
<style lang="less">
.card-container {
background: #fff;
overflow: hidden;
padding: 12px;
position: relative;
width: 100%;
.ant-tabs{
border:1px solid #e6ebf5;
padding: 0;
.ant-tabs-bar {
margin: 0;
outline: none;
border-bottom: none;
.ant-tabs-nav-container{
margin: 0;
.ant-tabs-tab {
padding: 0 24px!important;
background-color: #f5f7fa!important;
margin-right: 0 !important;
border-radius: 0;
line-height: 38px;
border: 1px solid transparent!important;
border-bottom: 1px solid #e6ebf5!important;
}
.ant-tabs-tab-active.ant-tabs-tab{
color: #409eff;
background-color: #fff!important;
border-right:1px solid #e6ebf5!important;
border-left:1px solid #e6ebf5!important;
border-bottom:1px solid #fff!important;
font-weight: normal;
transition:none!important;
}
}
}
.ant-tabs-tabpane{
padding: 15px;
.ant-row{
margin: 10px 0;
}
.ant-select,.ant-input-number{
width: 100px;
}
}
}
}
</style>
<style lang="less" scoped>
.container-widthEn{
width: 755px;
}
.container-widthCn{
width: 608px;
}
.language{
text-align: center;
position: absolute;
right: 13px;
top: 13px;
border: 1px solid transparent;
height: 40px;
line-height: 38px;
font-size: 16px;
color: #409eff;
z-index: 1;
background: #f5f7fa;
outline: none;
width: 47px;
border-bottom: 1px solid #e6ebf5;
border-radius: 0;
}
.card-container{
.bottom{
display: flex;
justify-content: center;
padding: 10px 0 0 0;
.cronButton{
margin: 0 10px;
line-height: 40px;
}
}
}
.tabBody{
.a-row{
margin: 10px 0;
.long{
.a-select{
width:354px;
}
}
.a-input-number{
width: 110px;
}
}
}
</style>
@@ -0,0 +1,48 @@
<template>
<a-modal
title="图片预览"
:width="modalWidth"
:visible="visible"
:confirmLoading="confirmLoading"
:footer="null"
@cancel="close"
cancelText="关闭">
<viewer>
<img :src="imageUrl" style="width: 200px;height: 200px">
</viewer>
</a-modal>
</template>
<script>
import { getFileAccessHttpUrl } from '../../../api/manage'
export default {
name: 'JImagePreviewModal',
data () {
return {
modalWidth: 600,
visible: false,
confirmLoading: false,
imageUrl: null
}
},
methods: {
open (file) {
this.visible = true
this.imageUrl = getFileAccessHttpUrl(file.name)
},
close () {
this.visible = false
}
}
}
</script>
<style scoped>
/deep/.ant-modal-body{
display: flex;
justify-content: center;
align-items: center;
}
</style>
@@ -0,0 +1,440 @@
<template>
<j-modal
:title="title"
:width="modalWidth"
:visible="visible"
:confirmLoading="confirmLoading"
switchFullscreen
wrapClassName="j-popup-modal"
@ok="handleSubmit"
@cancel="handleCancel"
cancelText="关闭">
<div class="table-page-search-wrapper">
<a-form layout="inline" @keyup.enter.native="searchByquery">
<a-row :gutter="24" v-if="showSearchFlag">
<template v-for="(item,index) in queryInfo">
<template v-if=" item.hidden==='1' ">
<a-col :md="8" :sm="24" :key=" 'query'+index " v-show="toggleSearchStatus">
<online-query-form-item :queryParam="queryParam" :item="item" :dictOptions="dictOptions"></online-query-form-item>
</a-col>
</template>
<template v-else>
<a-col :md="8" :sm="24" :key=" 'query'+index ">
<online-query-form-item :queryParam="queryParam" :item="item" :dictOptions="dictOptions"></online-query-form-item>
</a-col>
</template>
</template>
<a-col :md="8" :sm="8">
<span style="float: left;overflow: hidden;" class="table-page-search-submitButtons">
<a-button type="primary" @click="searchByquery" icon="search">查询</a-button>
<a-button type="primary" @click="searchReset" icon="reload" style="margin-left: 8px">重置</a-button>
<a @click="handleToggleSearch" style="margin-left: 8px">
{{ toggleSearchStatus ? '收起' : '展开' }}
<a-icon :type="toggleSearchStatus ? 'up' : 'down'"/>
</a>
</span>
</a-col>
</a-row>
</a-form>
</div>
<div class="ant-alert ant-alert-info" style="margin-bottom: 16px;">
<i class="anticon anticon-info-circle ant-alert-icon"></i>
已选择&nbsp;<a style="font-weight: 600">{{ table.selectedRowKeys.length }}</a>&nbsp;&nbsp;
<a style="margin-left: 24px" @click="onClearSelected">清空</a>
<a v-if="!showSearchFlag" style="margin-left: 24px" @click="onlyReload">刷新</a>
</div>
<a-table
ref="table"
size="middle"
bordered
:rowKey="combineRowKey"
:columns="table.columns"
:dataSource="table.dataSource"
:pagination="table.pagination"
:loading="table.loading"
:rowSelection="{fixed:true,selectedRowKeys: table.selectedRowKeys, onChange: handleChangeInTableSelect}"
@change="handleChangeInTable"
style="min-height: 300px"
:scroll="tableScroll"
:customRow="clickThenCheck">
</a-table>
</j-modal>
</template>
<script>
import { getAction } from '@/api/manage'
import { filterObj } from '@/utils/util'
import { filterMultiDictText } from '@/components/dict/JDictSelectUtil'
import { httpGroupRequest } from '@/api/GroupRequest.js'
const MODAL_WIDTH = 1200
export default {
name: 'JPopupOnlReport',
props: ['multi', 'code', 'sorter', 'groupId', 'param'],
components: {
},
data () {
return {
visible: false,
title: '',
confirmLoading: false,
queryInfo: [],
toggleSearchStatus: false,
queryParam: {
},
dictOptions: {},
url: {
getColumns: '/online/cgreport/api/getRpColumns/',
getData: '/online/cgreport/api/getData/',
getQueryInfo: '/online/cgreport/api/getQueryInfo/'
},
table: {
loading: true,
// 表头
columns: [],
// 数据集
dataSource: [],
// 选择器
selectedRowKeys: [],
selectionRows: [],
// 分页参数
pagination: {
current: 1,
pageSize: 10,
pageSizeOptions: ['10', '20', '30'],
showTotal: (total, range) => {
return range[0] + '-' + range[1] + ' 共' + total + '条'
},
showQuickJumper: true,
showSizeChanger: true,
total: 0
}
},
cgRpConfigId: '',
modalWidth: MODAL_WIDTH,
tableScroll: { x: true },
dynamicParam: {},
// 排序字段,默认无排序
iSorter: null
}
},
mounted () {
// this.loadColumnsInfo()
},
watch: {
code () {
this.loadColumnsInfo()
},
param: {
deep: true,
handler () {
// update--begin--autor:liusq-----date:20210706------forJPopup组件在modal中使用报错#2729------
if (this.visible) {
this.dynamicParamHandler()
this.loadData()
}
// update--begin--autor:liusq-----date:20210706------forJPopup组件在modal中使用报错#2729------
}
},
sorter: {
immediate: true,
handler () {
if (this.sorter) {
const arr = this.sorter.split('=')
if (arr.length === 2 && ['asc', 'desc'].includes(arr[1].toLowerCase())) {
this.iSorter = { column: arr[0], order: arr[1].toLowerCase() }
// 排序字段受控
this.table.columns.forEach(col => {
if (col.dataIndex === this.iSorter.column) {
this.$set(col, 'sortOrder', this.iSorter.order === 'asc' ? 'ascend' : 'descend')
} else {
this.$set(col, 'sortOrder', false)
}
})
} else {
console.warn('【JPopup】sorter参数不合法')
}
}
}
}
},
computed: {
showSearchFlag () {
return this.queryInfo && this.queryInfo.length > 0
}
},
methods: {
loadColumnsInfo () {
const url = `${this.url.getColumns}${this.code}`
// 缓存key
let groupIdKey
if (this.groupId) {
groupIdKey = this.groupId + url
}
httpGroupRequest(() => getAction(url), groupIdKey).then(res => {
if (res.success) {
this.initDictOptionData(res.result.dictOptions)
this.cgRpConfigId = res.result.cgRpConfigId
this.title = res.result.cgRpConfigName
const currColumns = res.result.columns
for (let a = 0; a < currColumns.length; a++) {
if (currColumns[a].customRender) {
const dictCode = currColumns[a].customRender
currColumns[a].customRender = (text) => {
return filterMultiDictText(this.dictOptions[dictCode], text + '')
}
}
// 排序字段受控
if (this.iSorter && currColumns[a].dataIndex === this.iSorter.column) {
currColumns[a].sortOrder = this.iSorter.order === 'asc' ? 'ascend' : 'descend'
}
}
this.table.columns = [...currColumns]
this.initQueryInfo()
}
})
},
initQueryInfo () {
const url = `${this.url.getQueryInfo}${this.cgRpConfigId}`
// 缓存key
let groupIdKey
if (this.groupId) {
groupIdKey = this.groupId + url
}
httpGroupRequest(() => getAction(url), groupIdKey).then((res) => {
// console.log("获取查询条件", res);
if (res.success) {
this.dynamicParamHandler(res.result)
this.queryInfo = res.result
// 查询条件加载后再请求数据
this.loadData(1)
} else {
this.$message.warning(res.message)
}
})
},
// 处理动态参数
dynamicParamHandler (arr) {
if (arr && arr.length > 0) {
// 第一次加载查询条件前 初始化queryParam为空对象
const queryTemp = {}
for (const item of arr) {
if (item.mode === 'single') {
queryTemp[item.field] = ''
}
}
this.queryParam = { ...queryTemp }
}
const dynamicTemp = {}
if (this.param) {
Object.keys(this.param).map(key => {
let str = this.param[key]
if (key in this.queryParam) {
if (str && str.startsWith("'") && str.endsWith("'")) {
str = str.substring(1, str.length - 1)
}
// 如果查询条件包含参数 设置值
this.queryParam[key] = str
}
dynamicTemp[key] = this.param[key]
})
}
this.dynamicParam = { ...dynamicTemp }
},
loadData (arg) {
if (arg === 1) {
this.table.pagination.current = 1
}
const params = this.getQueryParams()// 查询条件
this.table.loading = true
const url = `${this.url.getData}${this.cgRpConfigId}`
// 缓存key
let groupIdKey
if (this.groupId) {
groupIdKey = this.groupId + url + JSON.stringify(params)
}
httpGroupRequest(() => getAction(url, params), groupIdKey).then(res => {
this.table.loading = false
// console.log("daa",res)
const data = res.result
if (data) {
this.table.pagination.total = Number(data.total)
this.table.dataSource = data.records
} else {
this.table.pagination.total = 0
this.table.dataSource = []
}
})
},
getQueryParams () {
const paramTarget = {}
if (this.dynamicParam) {
// 处理自定义参数
Object.keys(this.dynamicParam).map(key => {
paramTarget['self_' + key] = this.dynamicParam[key]
})
}
const param = Object.assign(paramTarget, this.queryParam, this.iSorter)
param.pageNo = this.table.pagination.current
param.pageSize = this.table.pagination.pageSize
return filterObj(param)
},
handleChangeInTableSelect (selectedRowKeys, selectionRows) {
// update-begin-author:taoyan date:2020902 for:【issue】开源online的几个问题 LOWCOD-844
if (!selectedRowKeys || selectedRowKeys.length === 0) {
this.table.selectionRows = []
} else if (selectedRowKeys.length === selectionRows.length) {
this.table.selectionRows = selectionRows
} else {
// 当两者长度不一的时候 需要判断
const keys = this.table.selectedRowKeys
const rows = this.table.selectionRows
// 这个循环 添加新的记录
for (let i = 0; i < selectionRows.length; i++) {
const combineKey = this.combineRowKey(selectionRows[i])
if (keys.indexOf(combineKey) < 0) {
// 如果 原来的key 不包含当前记录 push
rows.push(selectionRows[i])
}
}
// 这个循环 移除取消选中的数据
this.table.selectionRows = rows.filter(item => {
const combineKey = this.combineRowKey(item)
return selectedRowKeys.indexOf(combineKey) >= 0
})
}
// update-end-author:taoyan date:2020902 for:【issue】开源online的几个问题 LOWCOD-844
this.table.selectedRowKeys = selectedRowKeys
},
handleChangeInTable (pagination, filters, sorter) {
// 分页、排序、筛选变化时触发
if (Object.keys(sorter).length > 0) {
this.iSorter = {
column: sorter.field,
order: sorter.order === 'ascend' ? 'asc' : 'desc'
}
// 排序字段受控
this.table.columns.forEach(col => {
if (col.dataIndex === sorter.field) {
this.$set(col, 'sortOrder', sorter.order)
} else {
this.$set(col, 'sortOrder', false)
}
})
}
this.table.pagination = pagination
this.loadData()
},
handleCancel () {
this.close()
},
handleSubmit () {
if (!this.multi) {
if (this.table.selectionRows && this.table.selectionRows.length > 1) {
this.$message.warning('请选择一条记录')
return false
}
}
if (!this.table.selectionRows || this.table.selectionRows.length === 0) {
this.$message.warning('请选择一条记录')
return false
}
this.$emit('ok', this.table.selectionRows)
this.close()
},
close () {
this.$emit('close')
this.visible = false
this.onClearSelected()
},
show () {
this.visible = true
this.loadColumnsInfo()
},
handleToggleSearch () {
this.toggleSearchStatus = !this.toggleSearchStatus
},
searchByquery () {
this.loadData(1)
},
onlyReload () {
this.loadData()
},
searchReset () {
Object.keys(this.queryParam).forEach(key => {
this.queryParam[key] = ''
})
this.loadData(1)
},
onClearSelected () {
this.table.selectedRowKeys = []
this.table.selectionRows = []
},
combineRowKey (record) {
let res = ''
Object.keys(record).forEach(key => {
// update-begin---author:liusq Date:20210203 forpop选择器列主键问题 issues/I29P9Q------------
if (key + '' === 'id') {
res = record[key] + res
} else {
res += record[key]
}
// update-end---author:liusq Date:20210203 forpop选择器列主键问题 issues/I29P9Q------------
})
if (res.length > 50) {
res = res.substring(0, 50)
}
return res
},
clickThenCheck (record) {
return {
on: {
click: () => {
const rowKey = this.combineRowKey(record)
if (!this.table.selectedRowKeys || this.table.selectedRowKeys.length === 0) {
const arr1 = []; const arr2 = []
arr1.push(record)
arr2.push(rowKey)
this.table.selectedRowKeys = arr2
this.table.selectionRows = arr1
} else {
if (this.table.selectedRowKeys.indexOf(rowKey) < 0) {
this.table.selectedRowKeys.push(rowKey)
this.table.selectionRows.push(record)
} else {
const rowKeyIndex = this.table.selectedRowKeys.indexOf(rowKey)
this.table.selectedRowKeys.splice(rowKeyIndex, 1)
this.table.selectionRows.splice(rowKeyIndex, 1)
}
}
}
}
}
},
// 防止字典中有垃圾数据
initDictOptionData (dictOptions) {
const obj = { }
Object.keys(dictOptions).map(k => {
obj[k] = dictOptions[k].filter(item => {
return item != null
})
})
this.dictOptions = obj
}
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,387 @@
<template>
<j-modal
centered
:title="name + '选择'"
:width="width"
:visible="visible"
switchFullscreen
@ok="handleOk"
@cancel="close"
cancelText="关闭">
<a-row :gutter="18">
<a-col :span="16">
<!-- 查询区域 -->
<a-form layout="inline" class="j-inline-form">
<!-- 固定条件 -->
<a-form-item :label="(queryParamText||name)">
<j-input v-model="queryParam[queryParamCode||valueKey]" :placeholder="'请输入' + (queryParamText||name)" @pressEnter="searchQuery"/>
</a-form-item>
<!-- 动态生成的查询条件 -->
<j-select-biz-query-item v-if="queryConfig.length>0" v-show="showMoreQueryItems" :queryParam="queryParam" :queryConfig="queryConfig" @pressEnter="searchQuery"/>
<!-- 按钮 -->
<a-button :style="{marginBottom:'12px'}" type="primary" @click="searchQuery" icon="search">查询</a-button>
<a-button :style="{marginBottom:'12px'}" type="primary" @click="searchReset" icon="reload" style="margin-left: 8px">重置</a-button>
<a v-if="queryConfig.length>0" @click="showMoreQueryItems=!showMoreQueryItems" style="margin-left: 8px">
{{ showMoreQueryItems ? '收起' : '展开' }}
<a-icon :type="showMoreQueryItems ? 'up' : 'down'"/>
</a>
</a-form>
<a-table
size="middle"
bordered
:rowKey="rowKey"
:columns="innerColumns"
:dataSource="dataSource"
:pagination="ipagination"
:loading="loading"
:scroll="{ y: 240 }"
:rowSelection="{selectedRowKeys, onChange: onSelectChange, type: multiple ? 'checkbox':'radio'}"
:customRow="customRowFn"
@change="handleTableChange">
</a-table>
</a-col>
<a-col :span="8">
<a-card :title="'已选' + name" :bordered="false" :head-style="{padding:0}" :body-style="{padding:0}">
<a-table size="middle" :rowKey="rowKey" bordered v-bind="selectedTable">
<span slot="action" slot-scope="text, record, index">
<a @click="handleDeleteSelected(record, index)">删除</a>
</span>
</a-table>
</a-card>
</a-col>
</a-row>
</j-modal>
</template>
<script>
import { getAction } from '@/api/manage'
import Ellipsis from '@/components/Ellipsis'
import { JeroListMixin } from '@/mixins/JeroListMixin'
import { pushIfNotExist } from '@/utils/util'
import JSelectBizQueryItem from './JSelectBizQueryItem'
import { cloneDeep } from 'lodash'
export default {
name: 'JSelectBizComponentModal',
mixins: [JeroListMixin],
components: { Ellipsis, JSelectBizQueryItem },
props: {
value: {
type: Array,
default: () => []
},
visible: {
type: Boolean,
default: false
},
valueKey: {
type: String,
required: true
},
multiple: {
type: Boolean,
default: true
},
width: {
type: Number,
default: 900
},
name: {
type: String,
default: ''
},
listUrl: {
type: String,
required: true,
default: ''
},
// 根据 value 获取显示文本的地址,例如存的是 username,可以通过该地址获取到 realname
valueUrl: {
type: String,
default: ''
},
displayKey: {
type: String,
default: null
},
columns: {
type: Array,
required: true,
default: () => []
},
// 查询条件Code
queryParamCode: {
type: String,
default: null
},
// 查询条件文字
queryParamText: {
type: String,
default: null
},
// 查询配置
queryConfig: {
type: Array,
default: () => []
},
rowKey: {
type: String,
default: 'id'
},
// 过长裁剪长度,设置为 -1 代表不裁剪
ellipsisLength: {
type: Number,
default: 12
}
},
data () {
return {
innerValue: [],
// 已选择列表
selectedTable: {
pagination: false,
scroll: { y: 240 },
columns: [
{
...this.columns[0],
width: this.columns[0].widthRight || this.columns[0].width
},
{ title: '操作', dataIndex: 'action', align: 'center', width: 60, scopedSlots: { customRender: 'action' } }
],
dataSource: []
},
renderEllipsis: (value) => (<ellipsis length={this.ellipsisLength}>{value}</ellipsis>),
url: { list: this.listUrl },
/* 分页参数 */
ipagination: {
current: 1,
pageSize: 5,
pageSizeOptions: ['5', '10', '20', '30'],
showTotal: (total, range) => {
return range[0] + '-' + range[1] + ' 共' + total + '条'
},
showQuickJumper: true,
showSizeChanger: true,
total: 0
},
options: [],
dataSourceMap: {},
showMoreQueryItems: false
}
},
computed: {
// 表头
innerColumns () {
const columns = cloneDeep(this.columns)
columns.forEach(column => {
// 给所有的列加上过长裁剪
if (this.ellipsisLength !== -1) {
const myCustomRender = column.customRender
column.customRender = (text, record, index) => {
let value = text
if (typeof myCustomRender === 'function') {
// noinspection JSVoidFunctionReturnValueUsed
value = myCustomRender(text, record, index)
}
if (typeof value === 'string') {
return this.renderEllipsis(value)
}
return value
}
}
})
return columns
}
},
watch: {
value: {
deep: true,
immediate: true,
handler (val) {
this.innerValue = cloneDeep(val)
this.selectedRowKeys = []
this.valueWatchHandler(val)
this.queryOptionsByValue(val)
}
},
dataSource: {
deep: true,
handler (val) {
this.emitOptions(val)
this.valueWatchHandler(this.innerValue)
}
},
selectedRowKeys: {
immediate: true,
deep: true,
handler (val) {
// update--begin--autor:scott-----date:20200927------for:选取职务名称出现全选 #1753-----
if (this.innerValue) {
this.innerValue.length = 0
}
// update--end--autor:scott-----date:20200927------for:选取职务名称出现全选 #1753-----
this.selectedTable.dataSource = val.map(key => {
for (const data of this.dataSource) {
if (data[this.rowKey] === key) {
pushIfNotExist(this.innerValue, data[this.valueKey])
return data
}
}
for (const data of this.selectedTable.dataSource) {
if (data[this.rowKey] === key) {
pushIfNotExist(this.innerValue, data[this.valueKey])
return data
}
}
console.warn('未找到选择的行信息,key' + key)
return {}
})
}
}
},
methods: {
/** 关闭弹窗 */
close () {
this.$emit('update:visible', false)
},
valueWatchHandler (val) {
val.forEach(item => {
this.dataSource.concat(this.selectedTable.dataSource).forEach(data => {
if (data[this.valueKey] === item) {
pushIfNotExist(this.selectedRowKeys, data[this.rowKey])
}
})
})
},
queryOptionsByValue (value) {
if (!value || value.length === 0) {
return
}
// 判断options是否存在value,如果已存在数据就不再请求后台了
let notExist = false
for (const val of value) {
let find = false
for (const option of this.options) {
if (val === option.value) {
find = true
break
}
}
if (!find) {
notExist = true
break
}
}
if (!notExist) return
getAction(this.valueUrl || this.listUrl, {
// 这里最后加一个 , 的原因是无论如何都要使用 in 查询,防止后台进行了模糊匹配,导致查询结果不准确
[this.valueKey]: value.join(',') + ',',
pageNo: 1,
pageSize: value.length
}).then((res) => {
if (res.success) {
let dataSource = res.result
if (!(dataSource instanceof Array)) {
dataSource = res.result.records
}
this.emitOptions(dataSource, (data) => {
pushIfNotExist(this.innerValue, data[this.valueKey])
pushIfNotExist(this.selectedRowKeys, data[this.rowKey])
pushIfNotExist(this.selectedTable.dataSource, data, this.rowKey)
})
}
})
},
emitOptions (dataSource, callback) {
dataSource.forEach(data => {
const key = data[this.valueKey]
this.dataSourceMap[key] = data
pushIfNotExist(this.options, { label: data[this.displayKey || this.valueKey], value: key }, 'value')
// typeof callback === 'function' ? callback(data) : ''
if (typeof callback === 'function') {
callback(data)
}
})
this.$emit('options', this.options, this.dataSourceMap)
},
/** 完成选择 */
handleOk () {
const value = this.selectedTable.dataSource.map(data => data[this.valueKey])
this.$emit('input', value)
this.close()
},
/** 删除已选择的 */
handleDeleteSelected (record/* , index */) {
this.selectedRowKeys.splice(this.selectedRowKeys.indexOf(record[this.rowKey]), 1)
// update--begin--autor:wangshuai-----date:20200722------forJSelectBizComponent组件切换页数值问题------
this.selectedTable.dataSource.splice(this.selectedTable.dataSource.indexOf(record), 1)
this.innerValue.splice(this.innerValue.indexOf(record[this.valueKey]), 1)
console.log('this.selectedRowKeys:', this.selectedRowKeys)
console.log('this.selectedTable.dataSource:', this.selectedTable.dataSource)
// update--begin--autor:wangshuai-----date:20200722------forJSelectBizComponent组件切换页数值问题------
},
customRowFn (record) {
return {
on: {
click: () => {
const key = record[this.rowKey]
if (!this.multiple) {
this.selectedRowKeys = [key]
this.selectedTable.dataSource = [record]
} else {
const index = this.selectedRowKeys.indexOf(key)
if (index === -1) {
this.selectedRowKeys.push(key)
this.selectedTable.dataSource.push(record)
} else {
this.handleDeleteSelected(record, index)
}
}
}
}
}
}
}
}
</script>
<style lang="less" scoped>
.full-form-item {
display: flex;
margin-right: 0;
/deep/ .ant-form-item-control-wrapper {
flex: 1 1;
display: inline-block;
}
}
.j-inline-form {
/deep/ .ant-form-item {
margin-bottom: 12px;
}
/deep/ .ant-form-item-label {
line-height: 32px;
width: auto;
}
/deep/ .ant-form-item-control {
height: 32px;
line-height: 32px;
}
}
</style>
@@ -0,0 +1,50 @@
export default {
name: 'JSelectBizQueryItem',
props: {
queryParam: Object,
queryConfig: Array
},
data () {
return {}
},
methods: {
renderQueryItem () {
return this.queryConfig.map(queryItem => {
const { key, label, queryModel, placeholder, dictCode, props, customRender } = queryItem
const options = {
props: {},
on: {
pressEnter: () => this.$emit('pressEnter')
}
}
if (props != null) {
Object.assign(options.props, props)
}
if (placeholder === undefined) {
if (dictCode) {
options.props.placeholder = `请选择${label}`
} else {
options.props.placeholder = `请输入${label}`
}
} else {
options.props.placeholder = placeholder
}
let input
if (typeof customRender === 'function') {
input = customRender.call(this, { key, options, queryParam: this.queryParam })
} else if (dictCode) {
input = <j-dict-select-tag {...options} vModel={this.queryParam[key]} dictCode={dictCode} style="width:180px;"/>
} else if (queryModel && queryModel === 'eq') {
input = <a-input {...options} vModel={this.queryParam[key]}/>
} else {
input = <j-input {...options} vModel={this.queryParam[key]}/>
}
return <a-form-item key={key} label={label}>{input}</a-form-item>
})
}
},
render () {
return <span>{this.renderQueryItem()}</span>
}
}
@@ -0,0 +1,36 @@
# JSelectBizComponent
Jero 选择组件的公共可复用组件
## 引用方式
```js
import JSelectBizComponent from '@/src/components/jerobiz/JSelectBizComponent'
export default {
components: { JSelectBizComponent }
}
```
## 参数
### 配置参数
| 参数名 | 类型 | 必填 | 默认值 | 备注 |
|-----------------------|---------|------|--------------|--------------------------------------------------------------------------------------|
| rowKey | String | | "id" | 唯一标识的字段名 |
| value(v-model) | String | | "" | 默认选择的数据,多个用半角逗号分割 |
| name | String | | "" | 显示名字,例如选择用户就填写"用户" |
| listUrl | String | 是 | | 数据请求地址,必须是封装了分页的地址 |
| valueUrl | String | | "" | 获取显示文本的地址,例如存的是 username,可以通过该地址获取到 realname |
| displayKey | String | | null | 显示在标签上的字段 key ,不传则直接显示数据 |
| returnKeys | Array | | ['id', 'id'] | v-model 绑定的 keys,是个数组,默认使用第二项,当配置了 `returnId=true` 就返回第一项 |
| returnId | Boolean | | false | 返回ID,设为true后将返回配置的 `returnKeys` 中的第一项 |
| selectButtonText | String | | "选择" | 选择按钮的文字 |
| queryParamText | String | | null | 查询条件显示文字,不传则使用 `name` |
| columns | Array | 是 | | 列配置项,与antd的table的配置完全一致。列的第一项会被配置成右侧已选择的列表上 |
| columns[0].widthRight | String | | null | 仅列的第一项可以应用此配置,表示右侧已选择列表的宽度,建议 `70%`,不传则应用`width` |
| placeholder | String | | "请选择" | 占位符 |
| disabled | Boolean | | false | 是否禁用 |
| multiple | Boolean | | false | 是否可多选 |
| buttons | Boolean | | true | 是否显示"选择"按钮,如果不显示,可以直接点击文本框打开选择界面 |
@@ -0,0 +1,168 @@
<template>
<a-row class="j-select-biz-component-box" type="flex" :gutter="8">
<a-col class="left" :class="{'full': !buttons}">
<slot name="left">
<a-select
mode="multiple"
:placeholder="placeholder"
v-model="selectValue"
:options="selectOptions"
allowClear
:disabled="disabled"
:open="selectOpen"
style="width: 100%;"
@dropdownVisibleChange="handleDropdownVisibleChange"
@click.native="visible=(buttons || disabled ?visible:true)"
/>
</slot>
</a-col>
<a-col v-if="buttons" class="right">
<a-button type="primary" icon="search" :disabled="disabled" @click="visible=true">{{selectButtonText}}</a-button>
</a-col>
<j-select-biz-component-modal
v-model="selectValue"
:visible.sync="visible"
v-bind="modalProps"
@options="handleOptions"
/>
</a-row>
</template>
<script>
import JSelectBizComponentModal from './JSelectBizComponentModal'
export default {
name: 'JSelectBizComponent',
components: { JSelectBizComponentModal },
props: {
value: {
type: String,
default: ''
},
/** 是否返回 id,默认 false,返回 code */
returnId: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择'
},
disabled: {
type: Boolean,
default: false
},
// 是否支持多选,默认 true
multiple: {
type: Boolean,
default: true
},
// 是否显示按钮,默认 true
buttons: {
type: Boolean,
default: true
},
// 显示的 Key
displayKey: {
type: String,
default: null
},
// 返回的 key
returnKeys: {
type: Array,
default: () => ['id', 'id']
},
// 选择按钮文字
selectButtonText: {
type: String,
default: '选择'
}
},
data () {
return {
selectValue: [],
selectOptions: [],
dataSourceMap: {},
visible: false,
selectOpen: false
}
},
computed: {
valueKey () {
return this.returnId ? this.returnKeys[0] : this.returnKeys[1]
},
modalProps () {
return Object.assign({
valueKey: this.valueKey,
multiple: this.multiple,
returnKeys: this.returnKeys,
displayKey: this.displayKey || this.valueKey
}, this.$attrs)
}
},
watch: {
value: {
immediate: true,
handler (val) {
if (val) {
this.selectValue = val.split(',')
} else {
this.selectValue = []
}
}
},
selectValue: {
deep: true,
handler (val) {
const rows = val.map(key => this.dataSourceMap[key])
const data = val.join(',')
if (data !== this.value) {
this.$emit('select', rows)
this.$emit('input', data)
this.$emit('change', data)
}
}
}
},
methods: {
handleOptions (options, dataSourceMap) {
this.selectOptions = options
this.dataSourceMap = dataSourceMap
},
handleDropdownVisibleChange () {
// 解决antdv自己的bug —— open 设置为 false 了,点击后还是添加了 open 样式,导致点击事件失效
this.selectOpen = true
this.$nextTick(() => {
this.selectOpen = false
})
}
}
}
</script>
<style lang="less" scoped>
.j-select-biz-component-box {
@width: 82px;
.left {
//width: calc(100% - @width - 8px);
width: calc(100% - @width);
}
.right {
width: @width;
}
.full {
width: 100%;
}
/deep/ .ant-select-search__field {
display: none !important;
}
}
</style>

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