前后端全面升级

This commit is contained in:
pycook
2023-07-10 17:42:15 +08:00
parent c444fed436
commit db5ff60aff
629 changed files with 97789 additions and 23995 deletions

View File

@@ -1,89 +0,0 @@
<template>
<div class="antd-pro-components-article-list-content-index-listContent">
<div class="description">
<slot>
{{ description }}
</slot>
</div>
<div class="extra">
<a-avatar :src="avatar" size="small" />
<a :href="href">{{ owner }}</a> 发布在 <a :href="href">{{ href }}</a>
<em>{{ updateAt | moment }}</em>
</div>
</div>
</template>
<script>
export default {
name: 'ArticleListContent',
props: {
prefixCls: {
type: String,
default: 'antd-pro-components-article-list-content-index-listContent'
},
description: {
type: String,
default: ''
},
owner: {
type: String,
required: true
},
avatar: {
type: String,
required: true
},
href: {
type: String,
required: true
},
updateAt: {
type: String,
required: true
}
}
}
</script>
<style lang="less" scoped>
@import '../index.less';
.antd-pro-components-article-list-content-index-listContent {
.description {
max-width: 720px;
line-height: 22px;
}
.extra {
margin-top: 16px;
color: @text-color-secondary;
line-height: 22px;
& /deep/ .ant-avatar {
position: relative;
top: 1px;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: top;
}
& > em {
margin-left: 16px;
color: @disabled-color;
font-style: normal;
}
}
}
@media screen and (max-width: @screen-xs) {
.antd-pro-components-article-list-content-index-listContent {
.extra {
& > em {
display: block;
margin-top: 8px;
margin-left: 0;
}
}
}
}
</style>

View File

@@ -1,3 +0,0 @@
import ArticleListContent from './ArticleListContent'
export default ArticleListContent

View File

@@ -1,46 +0,0 @@
<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>

View File

@@ -1,99 +0,0 @@
<!--
<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'
},
/**
* 头像大小 类型: largesmall 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>))
}
const itemList = items.map((item) => (
<li class={ classString }>{ item }</li>
))
return itemList
}
},
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>

View File

@@ -1,4 +0,0 @@
import AvatarList from './List'
import './index.less'
export default AvatarList

View File

@@ -1,60 +0,0 @@
@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;
}
}
}
}
}

View File

@@ -1,64 +0,0 @@
# AvatarList 用户头像列表
一组用户头像常用在项目/团队成员列表可通过设置 `size` 属性来指定头像大小
引用方式
```javascript
import AvatarList from '@/components/AvatarList'
const AvatarListItem = AvatarList.AvatarItem
export default {
components: {
AvatarList,
AvatarListItem
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<avatar-list size="mini">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
```html
<avatar-list :max-length="3">
<avatar-list-item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<avatar-list-item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
<avatar-list-item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</avatar-list>
```
## API
### AvatarList
| 参数 | 说明 | 类型 | 默认值 |
| ---------------- | -------- | ---------------------------------- | --------- |
| size | 头像大小 | `large``small` `mini`, `default` | `default` |
| maxLength | 要显示的最大项目 | number | - |
| excessItemsStyle | 多余的项目风格 | CSSProperties | - |
### AvatarList.Item
| 参数 | 说明 | 类型 | 默认值 |
| ---- | ------ | --------- | --- |
| tips | 头像展示文案 | string | - |
| src | 头像图片连接 | string | - |

View File

@@ -0,0 +1,2 @@
import CMDBExprDrawer from './index.vue'
export default CMDBExprDrawer

View File

@@ -0,0 +1,47 @@
<template>
<CustomDrawer
width="1000px"
:visible="visible"
@close="handleClose"
:hasTitle="false"
:hasFooter="false"
:closable="false"
:bodyStyle="{ padding: '24px 12px' }"
:placement="placement"
>
<ResourceSearch :fromCronJob="true" @copySuccess="copySuccess" />
</CustomDrawer>
</template>
<script>
import ResourceSearch from '@/modules/cmdb/views/resource_search'
export default {
name: 'CMDBExprDrawer',
components: { ResourceSearch },
props: {
placement: {
type: String,
default: 'right',
},
},
data() {
return {
visible: false,
}
},
methods: {
open() {
this.visible = true
},
handleClose() {
this.visible = false
},
copySuccess(text) {
this.$emit('copySuccess', text)
this.handleClose()
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,33 @@
export const ruleTypeList = [
{ value: 'and', label: '' },
{ value: 'or', label: '' },
// { value: 'not', label: '' },
]
export const expList = [
{ value: 'is', label: '等于' },
{ value: '~is', label: '不等于' },
{ value: 'contain', label: '包含' },
{ value: '~contain', label: '不包含' },
{ value: 'start_with', label: '以...开始' },
{ value: '~start_with', label: '不以...开始' },
{ value: 'end_with', label: '以...结束' },
{ value: '~end_with', label: '不以...结束' },
{ value: '~value', label: '为空' }, // 为空的定义有点绕
{ value: 'value', label: '不为空' },
]
export const advancedExpList = [
{ value: 'in', label: 'in查询' },
{ value: '~in', label: '非in查询' },
{ value: 'range', label: '范围' },
{ value: '~range', label: '范围外' },
{ value: 'compare', label: '比较' },
]
export const compareTypeList = [
{ value: '1', label: '>' },
{ value: '2', label: '>=' },
{ value: '3', label: '<' },
{ value: '4', label: '<=' },
]

View File

@@ -0,0 +1,285 @@
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '50px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '50px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
placeholder="请选择"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input class="ops-input" size="small" v-model="item.min" :style="{ width: '78px' }" placeholder="最小值" />
~
<a-input class="ops-input" size="small" v-model="item.max" :style="{ width: '78px' }" placeholder="最大值" />
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? '以 ; 分隔' : ''"
class="ops-input"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<a-tooltip title="复制">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip>
<a-tooltip title="删除">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
</a-space>
<div class="table-filter-add">
<a @click="handleAddRule">+ 新增</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
},
data() {
return {
ruleTypeList,
expList,
advancedExpList,
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: '等于' },
{ value: '~is', label: '不等于' },
{ value: '~value', label: '为空' }, // 为空的定义有点绕
{ value: 'value', label: '不为空' },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,276 @@
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:534px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">确定</a-button>
<a-button size="small" @click="handleClear">清空</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression v-else v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open) {
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<span>
<ops-icon :type="getPropertyIcon(attr)" />
</span>
</template>
<script>
export default {
name: 'ValueTypeIcon',
props: {
attr: {
type: Object,
default: () => {},
},
},
methods: {
getPropertyStyle(attr) {
switch (attr.value_type) {
case '0':
return { color: '#cf1322', backgroundColor: '#fff1f0' }
case '1':
return { color: '#d4b106', backgroundColor: '#feffe6' }
case '2':
return { color: '#d46b08', backgroundColor: '#fff7e6' }
case '3':
return { color: '#531dab', backgroundColor: '#f9f0ff' }
case '4':
return { color: '#389e0d', backgroundColor: '#f6ffed' }
case '5':
return { color: '#08979c', backgroundColor: '#e6fffb' }
case '6':
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
}
},
getPropertyIcon(attr) {
switch (attr.value_type) {
case '0':
return 'icon-xianxing-shishu'
case '1':
return 'icon-xianxing-fudianshu'
case '2':
return 'icon-xianxing-wenben'
case '3':
return 'icon-xianxing-datetime'
case '4':
return 'icon-xianxing-date'
case '5':
return 'icon-xianxing-time'
case '6':
return 'icon-xianxing-json'
}
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,24 @@
<template>
<div class="ops-card-title">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'CardTitle',
}
</script>
<style lang="less" scoped>
.ops-card-title {
border-top-left-radius: 15px;
font-size: 1vw;
height: 2.2vw;
color: #011d93;
background: linear-gradient(270deg, rgba(206, 226, 255, 0) -6.74%, #d2e4ff 96.74%);
padding: 5px 10px;
display: inline-block;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,2 @@
import CardTitle from './CardTitle'
export default CardTitle

View File

@@ -1,62 +0,0 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart
height="254"
:data="data"
:forceFit="true"
:padding="['auto', 'auto', '40', '50']">
<v-tooltip />
<v-axis />
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
export default {
name: 'Bar',
props: {
title: {
type: String,
default: ''
},
data: {
type: Array,
default: () => {
return []
}
},
scale: {
type: Array,
default: () => {
return [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
}
},
tooltip: {
type: Array,
default: () => {
return [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
}
}
},
data () {
return {
}
}
}
</script>

View File

@@ -1,120 +0,0 @@
<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">
<slot name="title">
{{ title }}
</slot>
</span>
<span class="chart-card-action">
<slot name="action"></slot>
</span>
</div>
<div class="total">
<slot name="total">
<span>{{ typeof total === 'function' && total() || total }}</span>
</slot>
</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: [Function, Number, String],
required: false,
default: null
},
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>

View File

@@ -1,67 +0,0 @@
<template>
<div>
<v-chart
:forceFit="true"
:height="height"
:width="width"
:data="data"
:scale="scale"
:padding="0">
<v-tooltip />
<v-interval
:shape="['liquid-fill-gauge']"
position="transfer*value"
color=""
:v-style="{
lineWidth: 10,
opacity: 0.75
}"
:tooltip="[
'transfer*value',
(transfer, value) => {
return {
name: transfer,
value,
};
},
]"
></v-interval>
<v-guide
v-for="(row, index) in data"
:key="index"
type="text"
:top="true"
:position="{
gender: row.transfer,
value: 45
}"
:content="row.value + '%'"
:v-style="{
fontSize: 100,
textAlign: 'center',
opacity: 0.75,
}"
/>
</v-chart>
</div>
</template>
<script>
export default {
name: 'Liquid',
props: {
height: {
type: Number,
default: 0
},
width: {
type: Number,
default: 0
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,56 +0,0 @@
<template>
<div class="antv-chart-mini">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 0, 18, 0]">
<v-tooltip />
<v-smooth-area position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
import moment from 'moment'
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
data.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
export default {
name: 'MiniArea',
data () {
return {
data,
tooltip,
scale,
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "chart";
</style>

View File

@@ -1,57 +0,0 @@
<template>
<div class="antv-chart-mini">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
<v-tooltip />
<v-bar position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
import moment from 'moment'
const data = []
const beginDay = new Date().getTime()
for (let i = 0; i < 10; i++) {
data.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: Math.round(Math.random() * 10)
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 30
}]
export default {
name: 'MiniBar',
data () {
return {
data,
tooltip,
scale,
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "chart";
</style>

View File

@@ -1,75 +0,0 @@
<template>
<div class="chart-mini-progress">
<div class="target" :style="{ left: target + '%'}">
<span :style="{ backgroundColor: color }" />
<span :style="{ backgroundColor: color }"/>
</div>
<div class="progress-wrapper">
<div class="progress" :style="{ backgroundColor: color, width: percentage + '%', height: height }"></div>
</div>
</div>
</template>
<script>
export default {
name: 'MiniProgress',
props: {
target: {
type: Number,
default: 0
},
height: {
type: String,
default: '10px'
},
color: {
type: String,
default: '#13C2C2'
},
percentage: {
type: Number,
default: 0
}
}
}
</script>
<style lang="less" scoped>
.chart-mini-progress {
padding: 5px 0;
position: relative;
width: 100%;
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
&:last-child {
top: auto;
bottom: 0;
}
}
}
.progress-wrapper {
background-color: #f5f5f5;
position: relative;
.progress {
transition: all .4s cubic-bezier(.08,.82,.17,1) 0s;
border-radius: 1px 0 0 1px;
background-color: #1890ff;
width: 0;
height: 100%;
}
}
}
</style>

View File

@@ -1,40 +0,0 @@
<template>
<div :class="prefixCls">
<div class="chart-wrapper" :style="{ height: 46 }">
<v-chart :force-fit="true" :height="100" :data="dataSource" :scale="scale" :padding="[36, 0, 18, 0]">
<v-tooltip />
<v-smooth-line position="x*y" :size="2" />
<v-smooth-area position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
export default {
name: 'MiniSmoothArea',
props: {
prefixCls: {
type: String,
default: 'ant-pro-smooth-area'
},
scale: {
type: [Object, Array],
required: true
},
dataSource: {
type: Array,
required: true
}
},
data () {
return {
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "smooth.area.less";
</style>

View File

@@ -1,68 +0,0 @@
<template>
<v-chart :forceFit="true" height="400" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
<v-tooltip></v-tooltip>
<v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid" />
<v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid" />
<v-legend dataKey="user" marker="circle" :offset="30" />
<v-coord type="polar" radius="0.8" />
<v-line position="item*score" color="user" :size="2" />
<v-point position="item*score" color="user" :size="4" shape="circle" />
</v-chart>
</template>
<script>
const axis1Opts = {
dataKey: 'item',
line: null,
tickLine: null,
grid: {
lineStyle: {
lineDash: null
},
hideFirstLine: false
}
}
const axis2Opts = {
dataKey: 'score',
line: null,
tickLine: null,
grid: {
type: 'polygon',
lineStyle: {
lineDash: null
}
}
}
const scale = [
{
dataKey: 'score',
min: 0,
max: 80
}, {
dataKey: 'user',
alias: '类型'
}
]
export default {
name: 'Radar',
props: {
data: {
type: Array,
default: null
}
},
data () {
return {
axis1Opts,
axis2Opts,
scale
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,77 +0,0 @@
<template>
<div class="rank">
<h4 class="title">{{ title }}</h4>
<ul class="list">
<li :key="index" v-for="(item, index) in list">
<span :class="index < 3 ? 'active' : null">{{ index + 1 }}</span>
<span>{{ item.name }}</span>
<span>{{ item.total }}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'RankList',
// ['title', 'list']
props: {
title: {
type: String,
default: ''
},
list: {
type: Array,
default: null
}
}
}
</script>
<style lang="less" scoped>
.rank {
padding: 0 32px 32px 72px;
.list {
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
margin-top: 16px;
span {
color: rgba(0, 0, 0, .65);
font-size: 14px;
line-height: 22px;
&:first-child {
background-color: #f5f5f5;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 24px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
}
&.active {
background-color: #314659;
color: #fff;
}
&:last-child {
float: right;
}
}
}
}
}
.mobile .rank {
padding: 0 32px 32px 32px;
}
</style>

View File

@@ -1,113 +0,0 @@
<template>
<v-chart :width="width" :height="height" :padding="[0]" :data="data" :scale="scale">
<v-tooltip :show-title="false" />
<v-coord type="rect" direction="TL" />
<v-point position="x*y" color="category" shape="cloud" tooltip="value*category" />
</v-chart>
</template>
<script>
import { registerShape } from 'viser-vue'
const DataSet = require('@antv/data-set')
const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png'
const scale = [
{ dataKey: 'x', nice: false },
{ dataKey: 'y', nice: false }
]
registerShape('point', 'cloud', {
draw (cfg, container) {
return container.addShape('text', {
attrs: {
fillOpacity: cfg.opacity,
fontSize: cfg.origin._origin.size,
rotate: cfg.origin._origin.rotate,
text: cfg.origin._origin.text,
textAlign: 'center',
fontFamily: cfg.origin._origin.font,
fill: cfg.color,
textBaseline: 'Alphabetic',
...cfg.style,
x: cfg.x,
y: cfg.y
}
})
}
})
export default {
name: 'TagCloud',
props: {
tagList: {
type: Array,
required: true
},
height: {
type: Number,
default: 400
},
width: {
type: Number,
default: 640
}
},
data () {
return {
data: [],
scale
}
},
watch: {
tagList: function (val) {
if (val.length > 0) {
this.initTagCloud(val)
}
}
},
mounted () {
if (this.tagList.length > 0) {
this.initTagCloud(this.tagList)
}
},
methods: {
initTagCloud (dataSource) {
const { height, width } = this
const dv = new DataSet.View().source(dataSource)
const range = dv.range('value')
const min = range[0]
const max = range[1]
const imageMask = new Image()
imageMask.crossOrigin = ''
imageMask.src = imgUrl
imageMask.onload = () => {
dv.transform({
type: 'tag-cloud',
fields: ['name', 'value'],
size: [width, height],
imageMask,
font: 'Verdana',
padding: 0,
timeInterval: 5000, // max execute time
rotate () {
let random = ~~(Math.random() * 4) % 4
if (random === 2) {
random = 0
}
return random * 90 // 0, 90, 270
},
fontSize (d) {
if (d.value) {
return ((d.value - min) / (max - min)) * (32 - 8) + 8
}
return 0
}
})
this.data = dv.rows
}
}
}
}
</script>

View File

@@ -1,64 +0,0 @@
<template>
<div :style="{ padding: '0 0 32px 32px' }">
<h4 :style="{ marginBottom: '20px' }">{{ title }}</h4>
<v-chart
height="254"
:data="data"
:scale="scale"
:forceFit="true"
:padding="['auto', 'auto', '40', '50']">
<v-tooltip />
<v-axis />
<v-bar position="x*y"/>
</v-chart>
</div>
</template>
<script>
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
title: '日期(天)',
alias: '日期(天)',
min: 2
}, {
dataKey: 'y',
title: '流量(Gb)',
alias: '流量(Gb)',
min: 1
}]
export default {
name: 'Bar',
props: {
title: {
type: String,
default: ''
}
},
data () {
return {
data: [],
scale,
tooltip
}
},
created () {
this.getMonthBar()
},
methods: {
getMonthBar () {
this.$http.get('/analysis/month-bar')
.then(res => {
this.data = res.result
})
}
}
}
</script>

View File

@@ -1,82 +0,0 @@
<template>
<div class="chart-trend">
{{ term }}
<span>{{ rate }}%</span>
<span :class="['trend-icon', trend]"><a-icon :type="'caret-' + trend"/></span>
</div>
</template>
<script>
export default {
name: 'Trend',
props: {
term: {
type: String,
default: '',
required: true
},
percentage: {
type: Number,
default: null
},
type: {
type: Boolean,
default: null
},
target: {
type: Number,
default: 0
},
value: {
type: Number,
default: 0
},
fixed: {
type: Number,
default: 2
}
},
data () {
return {
trend: this.type && 'up' || 'down',
rate: this.percentage
}
},
created () {
const type = this.type === null ? this.value >= this.target : this.type
this.trend = type ? 'up' : 'down'
this.rate = (this.percentage === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percentage).toFixed(this.fixed)
}
}
</script>
<style lang="less" scoped>
.chart-trend {
display: inline-block;
font-size: 14px;
line-height: 22px;
.trend-icon {
font-size: 12px;
&.up, &.down {
margin-left: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(.83);
}
}
&.up {
color: #f5222d;
}
&.down {
color: #52c41a;
top: -1px;
}
}
}
</style>

View File

@@ -1,13 +0,0 @@
.antv-chart-mini {
position: relative;
width: 100%;
.chart-wrapper {
position: absolute;
bottom: -28px;
width: 100%;
/* margin: 0 -5px;
overflow: hidden;*/
}
}

View File

@@ -1,14 +0,0 @@
@import "../index";
@smoothArea-prefix-cls: ~"@{ant-pro-prefix}-smooth-area";
.@{smoothArea-prefix-cls} {
position: relative;
width: 100%;
.chart-wrapper {
position: absolute;
bottom: -28px;
width: 100%;
}
}

View File

@@ -0,0 +1,124 @@
<template>
<transition
:name="transitionName"
@before-enter="collapseBeforeEnter"
@enter="collapseEnter"
@after-enter="collapseAfterEnter"
@before-leave="collapseBeforeLeave"
@leave="collapseLeave"
@after-leave="collapseAfterLeave"
>
<slot></slot>
</transition>
</template>
<script>
/**
* 元素折叠过度效果
*/
export default {
name: 'CollapseTransition',
props: {
transitionName: {
type: String,
default: 'collapse-transition',
},
},
data() {
return {
oldPaddingTop: '',
oldPaddingBottom: '',
oldOverflow: '',
}
},
methods: {
collapseBeforeEnter(el) {
// console.log('11, collapseBeforeEnter');
this.oldPaddingBottom = el.style.paddingBottom
this.oldPaddingTop = el.style.paddingTop
// 过渡效果开始前设置元素的maxHeight为0让元素maxHeight有一个初始值
el.style.paddingTop = '0'
el.style.paddingBottom = '0'
el.style.maxHeight = '0'
},
collapseEnter(el, done) {
// console.log('22, collapseEnter');
//
this.oldOverflow = el.style.overflow
const elHeight = el.scrollHeight
// 过渡效果进入后将元素的maxHeight设置为元素本身的高度将元素maxHeight设置为auto不会有过渡效果
if (elHeight > 0) {
el.style.maxHeight = elHeight + 'px'
} else {
el.style.maxHeight = '0'
}
el.style.paddingTop = this.oldPaddingTop
el.style.paddingBottom = this.oldPaddingBottom
el.style.overflow = 'hidden'
// done();
const onTransitionDone = function() {
done()
// console.log('enter onTransitionDone');
el.removeEventListener('transitionend', onTransitionDone, false)
el.removeEventListener('transitioncancel', onTransitionDone, false)
}
// 绑定元素的transition完成事件在transition完成后立即完成vue的过度动效
el.addEventListener('transitionend', onTransitionDone, false)
el.addEventListener('transitioncancel', onTransitionDone, false)
},
collapseAfterEnter(el) {
// console.log('33, collapseAfterEnter');
// 过渡效果完成后恢复元素的maxHeight
el.style.maxHeight = ''
el.style.overflow = this.oldOverflow
},
collapseBeforeLeave(el) {
// console.log('44, collapseBeforeLeave', el.scrollHeight);
this.oldPaddingBottom = el.style.paddingBottom
this.oldPaddingTop = el.style.paddingTop
this.oldOverflow = el.style.overflow
el.style.maxHeight = el.scrollHeight + 'px'
el.style.overflow = 'hidden'
},
collapseLeave(el, done) {
// console.log('55, collapseLeave', el.scrollHeight);
if (el.scrollHeight !== 0) {
el.style.maxHeight = '0'
el.style.paddingBottom = '0'
el.style.paddingTop = '0'
}
// done();
const onTransitionDone = function() {
done()
// console.log('leave onTransitionDone');
el.removeEventListener('transitionend', onTransitionDone, false)
el.removeEventListener('transitioncancel', onTransitionDone, false)
}
// 绑定元素的transition完成事件在transition完成后立即完成vue的过度动效
el.addEventListener('transitionend', onTransitionDone, false)
el.addEventListener('transitioncancel', onTransitionDone, false)
},
collapseAfterLeave(el) {
// console.log('66, collapseAfterLeave');
el.style.maxHeight = ''
el.style.overflow = this.oldOverflow
el.style.paddingBottom = this.oldPaddingBottom
el.style.paddingTop = this.oldPaddingTop
this.oldOverflow = this.oldPaddingBottom = this.oldPaddingTop = ''
},
},
}
</script>
<style lang="less">
.collapse-transition-enter-active,
.collapse-transition-leave-active {
transition: height 0.3s ease-in-out, padding 0.3s ease-in-out, max-height 0.3s ease-in-out;
}
</style>

View File

@@ -1,102 +0,0 @@
<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>

View File

@@ -1,3 +0,0 @@
import CountDown from './CountDown'
export default CountDown

View File

@@ -1,34 +0,0 @@
# CountDown 倒计时
倒计时组件
引用方式
```javascript
import CountDown from '@/components/CountDown/CountDown'
export default {
components: {
CountDown
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<count-down :target="new Date().getTime() + 3000000" :on-end="onEndHandle" />
```
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| target | 目标时间 | Date | - |
| onEnd | 倒计时结束回调 | funtion | -|

View File

@@ -0,0 +1,170 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
允许的通配符[, - * / L M]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
不指定
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
周期从
<el-input-number v-model="cycle01" :min="0" :max="31" /> -
<el-input-number v-model="cycle02" :min="0" :max="31" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
<el-input-number v-model="average01" :min="0" :max="31" /> 号开始
<el-input-number v-model="average02" :min="0" :max="31" /> 日执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="5">
每月
<el-input-number v-model="workday" :min="0" :max="31" /> 号最近的那个工作日
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="6">
本月最后一天
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="7">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
<el-option v-for="item in 31" :key="item" :value="item">{{ item }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
radioValue: 1,
workday: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check,
}
},
name: 'CrontabDay',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
;('day rachange')
switch (this.radioValue) {
case 1:
this.$emit('update', 'day', '*', 'day')
this.$emit('update', 'week', '?', 'day')
break
case 2:
this.$emit('update', 'day', '?')
this.$emit('update', 'week', '*')
break
case 3:
this.$emit('update', 'day', this.cycle01 + '-' + this.cycle02)
break
case 4:
this.$emit('update', 'day', this.average01 + '/' + this.average02)
break
case 5:
this.$emit('update', 'day', this.workday + 'W')
break
case 6:
this.$emit('update', 'day', 'L')
break
case 7:
this.$emit('update', 'day', this.checkboxString)
break
}
;('day rachange end')
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '3') {
this.$emit('update', 'day', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '4') {
this.$emit('update', 'day', this.averageTotal)
}
},
// 最近工作日值变化时
workdayChange() {
if (this.radioValue == '5') {
this.$emit('update', 'day', this.workday + 'W')
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '7') {
this.$emit('update', 'day', this.checkboxString)
}
},
// 父组件传递的week发生变化触发
weekChange() {
// 判断week值与day不能同时为?
if (this.cron.week == '?' && this.radioValue == '2') {
this.radioValue = '1'
} else if (this.cron.week !== '?' && this.radioValue != '2') {
this.radioValue = '2'
}
},
},
watch: {
radioValue: 'radioChange',
cycleTotal: 'cycleChange',
averageTotal: 'averageChange',
workdayCheck: 'workdayChange',
checkboxString: 'checkboxChange',
},
computed: {
// 计算两个周期值
cycleTotal: function() {
this.cycle01 = this.checkNum(this.cycle01, 1, 31)
this.cycle02 = this.checkNum(this.cycle02, 1, 31)
return this.cycle01 + '-' + this.cycle02
},
// 计算平均用到的值
averageTotal: function() {
this.average01 = this.checkNum(this.average01, 1, 31)
this.average02 = this.checkNum(this.average02, 1, 31)
return this.average01 + '/' + this.average02
},
// 计算工作日格式
workdayCheck: function() {
this.workday = this.checkNum(this.workday, 1, 31)
return this.workday
},
// 计算勾选的checkbox值合集
checkboxString: function() {
const str = this.checkboxList.join()
return str == '' ? '*' : str
},
},
}
</script>

View File

@@ -0,0 +1,123 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
小时允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
周期从
<el-input-number v-model="cycle01" :min="0" :max="23" /> -
<el-input-number v-model="cycle02" :min="0" :max="23" /> 小时
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
<el-input-number v-model="average01" :min="0" :max="23" /> 小时开始
<el-input-number v-model="average02" :min="0" :max="23" /> 小时执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
<el-option v-for="item in 24" :key="item" :value="item - 1">{{ item - 1 }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
radioValue: 1,
cycle01: 0,
cycle02: 1,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check,
}
},
name: 'CrontabHour',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue === 1) {
this.$emit('update', 'hour', '*', 'hour')
this.$emit('update', 'day', '*', 'hour')
} else {
if (this.cron.min === '*') {
this.$emit('update', 'min', '0', 'hour')
}
if (this.cron.second === '*') {
this.$emit('update', 'second', '0', 'hour')
}
}
switch (this.radioValue) {
case 2:
this.$emit('update', 'hour', this.cycle01 + '-' + this.cycle02)
break
case 3:
this.$emit('update', 'hour', this.average01 + '/' + this.average02)
break
case 4:
this.$emit('update', 'hour', this.checkboxString)
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '2') {
this.$emit('update', 'hour', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '3') {
this.$emit('update', 'hour', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '4') {
this.$emit('update', 'hour', this.checkboxString)
}
},
},
watch: {
radioValue: 'radioChange',
cycleTotal: 'cycleChange',
averageTotal: 'averageChange',
checkboxString: 'checkboxChange',
},
computed: {
// 计算两个周期值
cycleTotal: function() {
this.cycle01 = this.checkNum(this.cycle01, 0, 23)
this.cycle02 = this.checkNum(this.cycle02, 0, 23)
return this.cycle01 + '-' + this.cycle02
},
// 计算平均用到的值
averageTotal: function() {
this.average01 = this.checkNum(this.average01, 0, 23)
this.average02 = this.checkNum(this.average02, 1, 23)
return this.average01 + '/' + this.average02
},
// 计算勾选的checkbox值合集
checkboxString: function() {
const str = this.checkboxList.join()
return str == '' ? '*' : str
},
},
}
</script>

View File

@@ -0,0 +1,119 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
分钟允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
周期从
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
<el-input-number v-model="cycle02" :min="0" :max="60" /> 分钟
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
<el-input-number v-model="average01" :min="0" :max="60" /> 分钟开始
<el-input-number v-model="average02" :min="0" :max="60" /> 分钟执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
<el-option v-for="item in 60" :key="item" :value="item - 1">{{ item - 1 }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check,
}
},
name: 'CrontabMin',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue !== 1 && this.cron.second === '*') {
this.$emit('update', 'second', '0', 'min')
}
switch (this.radioValue) {
case 1:
this.$emit('update', 'min', '*', 'min')
this.$emit('update', 'hour', '*', 'min')
break
case 2:
this.$emit('update', 'min', this.cycle01 + '-' + this.cycle02, 'min')
break
case 3:
this.$emit('update', 'min', this.average01 + '/' + this.average02, 'min')
break
case 4:
this.$emit('update', 'min', this.checkboxString, 'min')
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '2') {
this.$emit('update', 'min', this.cycleTotal, 'min')
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '3') {
this.$emit('update', 'min', this.averageTotal, 'min')
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '4') {
this.$emit('update', 'min', this.checkboxString, 'min')
}
},
},
watch: {
radioValue: 'radioChange',
cycleTotal: 'cycleChange',
averageTotal: 'averageChange',
checkboxString: 'checkboxChange',
},
computed: {
// 计算两个周期值
cycleTotal: function() {
this.cycle01 = this.checkNum(this.cycle01, 0, 59)
this.cycle02 = this.checkNum(this.cycle02, 0, 59)
return this.cycle01 + '-' + this.cycle02
},
// 计算平均用到的值
averageTotal: function() {
this.average01 = this.checkNum(this.average01, 0, 59)
this.average02 = this.checkNum(this.average02, 1, 59)
return this.average01 + '/' + this.average02
},
// 计算勾选的checkbox值合集
checkboxString: function() {
const str = this.checkboxList.join()
return str == '' ? '*' : str
},
},
}
</script>

View File

@@ -0,0 +1,128 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
周期从
<el-input-number v-model="cycle01" :min="1" :max="12" /> -
<el-input-number v-model="cycle02" :min="1" :max="12" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
<el-input-number v-model="average01" :min="1" :max="12" /> 月开始
<el-input-number v-model="average02" :min="1" :max="12" /> 月月执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
<el-option v-for="item in 12" :key="item" :value="item">{{ item }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
checkNum: this.check,
}
},
name: 'CrontabMouth',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue === 1) {
this.$emit('update', 'mouth', '*')
} else {
if (this.cron.day === '*') {
this.$emit('update', 'day', '0', 'mouth')
}
if (this.cron.hour === '*') {
this.$emit('update', 'hour', '0', 'mouth')
}
if (this.cron.min === '*') {
this.$emit('update', 'min', '0', 'mouth')
}
if (this.cron.second === '*') {
this.$emit('update', 'second', '0', 'mouth')
}
}
switch (this.radioValue) {
case 2:
this.$emit('update', 'mouth', this.cycle01 + '-' + this.cycle02)
break
case 3:
this.$emit('update', 'mouth', this.average01 + '/' + this.average02)
break
case 4:
this.$emit('update', 'mouth', this.checkboxString)
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '2') {
this.$emit('update', 'mouth', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '3') {
this.$emit('update', 'mouth', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '4') {
this.$emit('update', 'mouth', this.checkboxString)
}
},
},
watch: {
radioValue: 'radioChange',
cycleTotal: 'cycleChange',
averageTotal: 'averageChange',
checkboxString: 'checkboxChange',
},
computed: {
// 计算两个周期值
cycleTotal: function() {
this.cycle01 = this.checkNum(this.cycle01, 1, 12)
this.cycle02 = this.checkNum(this.cycle02, 1, 12)
return this.cycle01 + '-' + this.cycle02
},
// 计算平均用到的值
averageTotal: function() {
this.average01 = this.checkNum(this.average01, 1, 12)
this.average02 = this.checkNum(this.average02, 1, 12)
return this.average01 + '/' + this.average02
},
// 计算勾选的checkbox值合集
checkboxString: function() {
const str = this.checkboxList.join()
return str == '' ? '*' : str
},
},
}
</script>

View File

@@ -0,0 +1,580 @@
<template>
<div class="popup-result">
<p class="title">最近5次运行时间</p>
<ul class="popup-result-scroll">
<template v-if="isShow">
<li v-for="item in resultList" :key="item">{{ item }}</li>
</template>
<li v-else>计算结果中...</li>
</ul>
</div>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
dayRule: '',
dayRuleSup: '',
dateArr: [],
resultList: [],
isShow: false,
}
},
name: 'CrontabResult',
methods: {
// 表达式值变化时开始去计算结果
expressionChange() {
// 计算开始-隐藏结果
this.isShow = false
// 获取规则数组[012345星期6]
const ruleArr = this.$options.propsData.ex.split(' ')
// 用于记录进入循环的次数
let nums = 0
// 用于暂时存符号时间规则结果的数组
const resultArr = []
// 获取当前时间精确至[]
const nTime = new Date()
const nYear = nTime.getFullYear()
let nMouth = nTime.getMonth() + 1
let nDay = nTime.getDate()
let nHour = nTime.getHours()
let nMin = nTime.getMinutes()
let nSecond = nTime.getSeconds()
// 根据规则获取到近100年可能年数组月数组等等
this.getSecondArr(ruleArr[0])
this.getMinArr(ruleArr[1])
this.getHourArr(ruleArr[2])
this.getDayArr(ruleArr[3])
this.getMouthArr(ruleArr[4])
this.getWeekArr(ruleArr[5])
this.getYearArr(ruleArr[6], nYear)
// 将获取到的数组赋值-方便使用
const sDate = this.dateArr[0]
const mDate = this.dateArr[1]
const hDate = this.dateArr[2]
const DDate = this.dateArr[3]
const MDate = this.dateArr[4]
const YDate = this.dateArr[5]
// 获取当前时间在数组中的索引
let sIdx = this.getIndex(sDate, nSecond)
let mIdx = this.getIndex(mDate, nMin)
let hIdx = this.getIndex(hDate, nHour)
let DIdx = this.getIndex(DDate, nDay)
let MIdx = this.getIndex(MDate, nMouth)
const YIdx = this.getIndex(YDate, nYear)
// 重置月日时分秒的函数(后面用的比较多)
const resetSecond = function() {
sIdx = 0
nSecond = sDate[sIdx]
}
const resetMin = function() {
mIdx = 0
nMin = mDate[mIdx]
resetSecond()
}
const resetHour = function() {
hIdx = 0
nHour = hDate[hIdx]
resetMin()
}
const resetDay = function() {
DIdx = 0
nDay = DDate[DIdx]
resetHour()
}
const resetMouth = function() {
MIdx = 0
nMouth = MDate[MIdx]
resetDay()
}
// 如果当前年份不为数组中当前值
if (nYear !== YDate[YIdx]) {
resetMouth()
}
// 如果当前月份不为数组中当前值
if (nMouth !== MDate[MIdx]) {
resetDay()
}
// 如果当前不为数组中当前值
if (nDay !== DDate[DIdx]) {
resetHour()
}
// 如果当前不为数组中当前值
if (nHour !== hDate[hIdx]) {
resetMin()
}
// 如果当前不为数组中当前值
if (nMin !== mDate[mIdx]) {
resetSecond()
}
// 循环年份数组
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
const YY = YDate[Yi]
// 如果到达最大值时
if (nMouth > MDate[MDate.length - 1]) {
resetMouth()
continue
}
// 循环月份数组
goMouth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
// 赋值方便后面运算
let MM = MDate[Mi]
MM = MM < 10 ? '0' + MM : MM
// 如果到达最大值时
if (nDay > DDate[DDate.length - 1]) {
resetDay()
if (Mi == MDate.length - 1) {
resetMouth()
continue goYear
}
continue
}
// 循环日期数组
goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
// 赋值方便后面运算
let DD = DDate[Di]
let thisDD = DD < 10 ? '0' + DD : DD
// 如果到达最大值时
if (nHour > hDate[hDate.length - 1]) {
resetHour()
if (Di == DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
resetMouth()
continue goYear
}
continue goMouth
}
continue
}
// 判断日期的合法性不合法的话也是跳出当前循环
if (
this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true &&
this.dayRule !== 'workDay' &&
this.dayRule !== 'lastWeek' &&
this.dayRule !== 'lastDay'
) {
resetDay()
continue goMouth
}
// 如果日期规则中有值时
if (this.dayRule == 'lastDay') {
// 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
} else if (this.dayRule == 'workDay') {
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取达到条件的日期是星期X
const thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 当星期日时
if (thisWeek == 0) {
// 先找下一个日并判断是否为月底
DD++
thisDD = DD < 10 ? '0' + DD : DD
// 判断下一日已经不是合法日期
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD -= 3
}
} else if (thisWeek == 6) {
// 当星期6时只需判断不是1号就可进行操作
if (this.dayRuleSup !== 1) {
DD--
} else {
DD += 2
}
}
} else if (this.dayRule == 'weekDay') {
// 如果指定了是星期几
// 获取当前日期是属于星期几
const thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
// 校验当前星期是否在星期池dayRuleSup
if (!this.dayRuleSup.includes(thisWeek)) {
// 如果到达最大值时
if (Di == DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
resetMouth()
continue goYear
}
continue goMouth
}
continue
}
} else if (this.dayRule == 'assWeek') {
// 如果指定了是第几周的星期几
// 获取每月1号是属于星期几
const thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
if (this.dayRuleSup[1] >= thisWeek) {
DD = (this.dayRuleSup[0] - 1) * 7 + this.dayRuleSup[1] - thisWeek + 1
} else {
DD = this.dayRuleSup[0] * 7 + this.dayRuleSup[1] - thisWeek + 1
}
} else if (this.dayRule == 'lastWeek') {
// 如果指定了每月最后一个星期几
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取月末最后一天是星期几
const thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 找到要求中最近的那个星期几
if (this.dayRuleSup < thisWeek) {
DD -= thisWeek - this.dayRuleSup
} else if (this.dayRuleSup > thisWeek) {
DD -= 7 - (this.dayRuleSup - thisWeek)
}
}
// 判断时间值是否小于10置换成05这种格式
DD = DD < 10 ? '0' + DD : DD
// 循环数组
goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
const hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
// 如果到达最大值时
if (nMin > mDate[mDate.length - 1]) {
resetMin()
if (hi == hDate.length - 1) {
resetHour()
if (Di == DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
resetMouth()
continue goYear
}
continue goMouth
}
continue goDay
}
continue
}
// 循环""数组
goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
const mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
// 如果到达最大值时
if (nSecond > sDate[sDate.length - 1]) {
resetSecond()
if (mi == mDate.length - 1) {
resetMin()
if (hi == hDate.length - 1) {
resetHour()
if (Di == DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
resetMouth()
continue goYear
}
continue goMouth
}
continue goDay
}
continue goHour
}
continue
}
// 循环""数组
goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
const ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]
// 添加当前时间时间合法性在日期循环时已经判断
if (MM !== '00' && DD !== '00') {
resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
nums++
}
// 如果条数满了就退出循环
if (nums == 5) break goYear
// 如果到达最大值时
if (si == sDate.length - 1) {
resetSecond()
if (mi == mDate.length - 1) {
resetMin()
if (hi == hDate.length - 1) {
resetHour()
if (Di == DDate.length - 1) {
resetDay()
if (Mi == MDate.length - 1) {
resetMouth()
continue goYear
}
continue goMouth
}
continue goDay
}
continue goHour
}
continue goMin
}
} // goSecond
} // goMin
} // goHour
} // goDay
} // goMouth
}
// 判断100年内的结果条数
if (resultArr.length == 0) {
this.resultList = ['没有达到条件的结果!']
} else {
this.resultList = resultArr
if (resultArr.length !== 5) {
this.resultList.push('最近100年内只有上面' + resultArr.length + '条结果!')
}
}
// 计算完成-显示结果
this.isShow = true
},
// 用于计算某位数字在数组中的索引
getIndex(arr, value) {
if (value <= arr[0] || value > arr[arr.length - 1]) {
return 0
} else {
for (let i = 0; i < arr.length - 1; i++) {
if (value > arr[i] && value <= arr[i + 1]) {
return i + 1
}
}
}
},
// 获取""数组
getYearArr(rule, year) {
this.dateArr[5] = this.getOrderArr(year, year + 100)
if (rule !== undefined) {
if (rule.indexOf('-') >= 0) {
this.dateArr[5] = this.getCycleArr(rule, year + 100, false)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[5] = this.getAverageArr(rule, year + 100)
} else if (rule !== '*') {
this.dateArr[5] = this.getAssignArr(rule)
}
}
},
// 获取""数组
getMouthArr(rule) {
this.dateArr[4] = this.getOrderArr(1, 12)
if (rule.indexOf('-') >= 0) {
this.dateArr[4] = this.getCycleArr(rule, 12, false)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[4] = this.getAverageArr(rule, 12)
} else if (rule !== '*') {
this.dateArr[4] = this.getAssignArr(rule)
}
},
// 获取""数组-主要为日期规则
getWeekArr(rule) {
// 只有当日期规则的两个值均为时则表达日期是有选项的
if (this.dayRule == '' && this.dayRuleSup == '') {
if (rule.indexOf('-') >= 0) {
this.dayRule = 'weekDay'
this.dayRuleSup = this.getCycleArr(rule, 7, false)
} else if (rule.indexOf('#') >= 0) {
this.dayRule = 'assWeek'
const matchRule = rule.match(/[0-9]{1}/g)
this.dayRuleSup = [Number(matchRule[0]), Number(matchRule[1])]
this.dateArr[3] = [1]
if (this.dayRuleSup[1] == 7) {
this.dayRuleSup[1] = 0
}
} else if (rule.indexOf('L') >= 0) {
this.dayRule = 'lastWeek'
this.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0])
this.dateArr[3] = [31]
if (this.dayRuleSup == 7) {
this.dayRuleSup = 0
}
} else if (rule !== '*' && rule !== '?') {
this.dayRule = 'weekDay'
this.dayRuleSup = this.getAssignArr(rule)
}
// 如果weekDay时将7调整为0week值0即是星期日
if (this.dayRule == 'weekDay') {
for (let i = 0; i < this.dayRuleSup.length; i++) {
if (this.dayRuleSup[i] == 7) {
this.dayRuleSup[i] = 0
}
}
}
}
},
// 获取""数组-少量为日期规则
getDayArr(rule) {
this.dateArr[3] = this.getOrderArr(1, 31)
this.dayRule = ''
this.dayRuleSup = ''
if (rule.indexOf('-') >= 0) {
this.dateArr[3] = this.getCycleArr(rule, 31, false)
this.dayRuleSup = 'null'
} else if (rule.indexOf('/') >= 0) {
this.dateArr[3] = this.getAverageArr(rule, 31)
this.dayRuleSup = 'null'
} else if (rule.indexOf('W') >= 0) {
this.dayRule = 'workDay'
this.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0])
this.dateArr[3] = [this.dayRuleSup]
} else if (rule.indexOf('L') >= 0) {
this.dayRule = 'lastDay'
this.dayRuleSup = 'null'
this.dateArr[3] = [31]
} else if (rule !== '*' && rule !== '?') {
this.dateArr[3] = this.getAssignArr(rule)
this.dayRuleSup = 'null'
} else if (rule == '*') {
this.dayRuleSup = 'null'
}
},
// 获取""数组
getHourArr(rule) {
this.dateArr[2] = this.getOrderArr(0, 23)
if (rule.indexOf('-') >= 0) {
this.dateArr[2] = this.getCycleArr(rule, 24, true)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[2] = this.getAverageArr(rule, 23)
} else if (rule !== '*') {
this.dateArr[2] = this.getAssignArr(rule)
}
},
// 获取""数组
getMinArr(rule) {
this.dateArr[1] = this.getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
this.dateArr[1] = this.getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[1] = this.getAverageArr(rule, 59)
} else if (rule !== '*') {
this.dateArr[1] = this.getAssignArr(rule)
}
},
// 获取""数组
getSecondArr(rule) {
this.dateArr[0] = this.getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
this.dateArr[0] = this.getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
this.dateArr[0] = this.getAverageArr(rule, 59)
} else if (rule !== '*') {
this.dateArr[0] = this.getAssignArr(rule)
}
},
// 根据传进来的min-max返回一个顺序的数组
getOrderArr(min, max) {
const arr = []
for (let i = min; i <= max; i++) {
arr.push(i)
}
return arr
},
// 根据规则中指定的零散值返回一个数组
getAssignArr(rule) {
const arr = []
const assiginArr = rule.split(',')
for (let i = 0; i < assiginArr.length; i++) {
arr[i] = Number(assiginArr[i])
}
arr.sort(this.compare)
return arr
},
// 根据一定算术规则计算返回一个数组
getAverageArr(rule, limit) {
const arr = []
const agArr = rule.split('/')
let min = Number(agArr[0])
const step = Number(agArr[1])
while (min <= limit) {
arr.push(min)
min += step
}
return arr
},
// 根据规则返回一个具有周期性的数组
getCycleArr(rule, limit, status) {
// status--表示是否从0开始则从1开始
const arr = []
const cycleArr = rule.split('-')
const min = Number(cycleArr[0])
let max = Number(cycleArr[1])
if (min > max) {
max += limit
}
for (let i = min; i <= max; i++) {
let add = 0
if (status == false && i % limit == 0) {
add = limit
}
arr.push(Math.round((i % limit) + add))
}
arr.sort(this.compare)
return arr
},
// 比较数字大小用于Array.sort
compare(value1, value2) {
if (value2 - value1 > 0) {
return -1
} else {
return 1
}
},
// 格式化日期格式如2017-9-19 18:04:33
formatDate(value, type) {
// 计算日期相关值
const time = typeof value === 'number' ? new Date(value) : value
const Y = time.getFullYear()
const M = time.getMonth() + 1
const D = time.getDate()
const h = time.getHours()
const m = time.getMinutes()
const s = time.getSeconds()
const week = time.getDay()
// 如果传递了type的话
if (type == undefined) {
return (
Y +
'-' +
(M < 10 ? '0' + M : M) +
'-' +
(D < 10 ? '0' + D : D) +
' ' +
(h < 10 ? '0' + h : h) +
':' +
(m < 10 ? '0' + m : m) +
':' +
(s < 10 ? '0' + s : s)
)
} else if (type == 'week') {
return week
}
},
// 检查日期是否存在
checkDate(value) {
const time = new Date(value)
const format = this.formatDate(time)
return value == format
},
},
watch: {
ex: 'expressionChange',
},
props: ['ex'],
mounted: function() {
// 初始化 获取一次结果
this.expressionChange()
},
}
</script>

View File

@@ -0,0 +1,133 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
周期从
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
<el-input-number v-model="cycle02" :min="0" :max="60" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
<el-input-number v-model="average01" :min="0" :max="60" /> 秒开始
<el-input-number v-model="average02" :min="0" :max="60" /> 秒执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
<el-option v-for="item in 60" :key="item" :value="item - 1">{{ item - 1 }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check,
}
},
name: 'CrontabSecond',
props: ['check', 'radioParent'],
methods: {
// 单选按钮值变化时
radioChange() {
switch (this.radioValue) {
case 1:
this.$emit('update', 'second', '*', 'second')
this.$emit('update', 'min', '*', 'second')
break
case 2:
this.$emit('update', 'second', this.cycle01 + '-' + this.cycle02)
break
case 3:
this.$emit('update', 'second', this.average01 + '/' + this.average02)
break
case 4:
this.$emit('update', 'second', this.checkboxString)
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '2') {
this.$emit('update', 'second', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '3') {
this.$emit('update', 'second', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '4') {
this.$emit('update', 'second', this.checkboxString)
}
},
othChange() {
// 反解析
const ins = this.cron.second('反解析 second', ins)
if (ins === '*') {
this.radioValue = 1
} else if (ins.indexOf('-') > -1) {
this.radioValue = 2
} else if (ins.indexOf('/') > -1) {
this.radioValue = 3
} else {
this.radioValue = 4
this.checkboxList = ins.split(',')
}
},
},
watch: {
radioValue: 'radioChange',
cycleTotal: 'cycleChange',
averageTotal: 'averageChange',
checkboxString: 'checkboxChange',
radioParent() {
this.radioValue = this.radioParent
},
},
computed: {
// 计算两个周期值
cycleTotal: function() {
this.cycle01 = this.checkNum(this.cycle01, 0, 59)
this.cycle02 = this.checkNum(this.cycle02, 0, 59)
return this.cycle01 + '-' + this.cycle02
},
// 计算平均用到的值
averageTotal: function() {
this.average01 = this.checkNum(this.average01, 0, 59)
this.average02 = this.checkNum(this.average02, 1, 59)
return this.average01 + '/' + this.average02
},
// 计算勾选的checkbox值合集
checkboxString: function() {
const str = this.checkboxList.join()
return str == '' ? '*' : str
},
},
}
</script>

View File

@@ -0,0 +1,154 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
允许的通配符[, - * / L #]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
不指定
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
周期从星期
<el-input-number v-model="cycle01" :min="1" :max="7" /> -
<el-input-number v-model="cycle02" :min="1" :max="7" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
<el-input-number v-model="average01" :min="1" :max="4" /> 周的星期
<el-input-number v-model="average02" :min="1" :max="7" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="5">
本月最后一个星期
<el-input-number v-model="weekday" :min="1" :max="7" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="6">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
<el-option v-for="(item, index) of weekList" :key="index" :value="index + 1">{{ item }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
radioValue: 2,
weekday: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
weekList: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
checkNum: this.$options.propsData.check,
}
},
name: 'CrontabWeek',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue !== 2) {
this.$emit('update', 'day', '?')
}
switch (this.radioValue) {
case 1:
this.$emit('update', 'week', '*')
break
case 2:
this.$emit('update', 'week', '?')
this.$emit('update', 'day', '*')
break
case 3:
this.$emit('update', 'week', this.cycle01 + '-' + this.cycle02)
break
case 4:
this.$emit('update', 'week', this.average01 + '#' + this.average02)
break
case 5:
this.$emit('update', 'week', this.weekday + 'L')
break
case 6:
this.$emit('update', 'week', this.checkboxString)
break
}
},
// 根据互斥事件更改radio的值
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '3') {
this.$emit('update', 'week', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '4') {
this.$emit('update', 'week', this.averageTotal)
}
},
// 最近工作日值变化时
weekdayChange() {
if (this.radioValue == '5') {
this.$emit('update', 'week', this.weekday + 'L')
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '6') {
this.$emit('update', 'week', this.checkboxString)
}
},
},
watch: {
radioValue: 'radioChange',
cycleTotal: 'cycleChange',
averageTotal: 'averageChange',
weekdayCheck: 'weekdayChange',
checkboxString: 'checkboxChange',
},
computed: {
// 计算两个周期值
cycleTotal: function() {
this.cycle01 = this.checkNum(this.cycle01, 1, 7)
this.cycle02 = this.checkNum(this.cycle02, 1, 7)
return this.cycle01 + '-' + this.cycle02
},
// 计算平均用到的值
averageTotal: function() {
this.average01 = this.checkNum(this.average01, 1, 4)
this.average02 = this.checkNum(this.average02, 1, 7)
return this.average01 + '#' + this.average02
},
// 最近的工作日格式
weekdayCheck: function() {
this.weekday = this.checkNum(this.weekday, 1, 7)
return this.weekday
},
// 计算勾选的checkbox值合集
checkboxString: function() {
const str = this.checkboxList.join()
return str == '' ? '*' : str
},
},
}
</script>

View File

@@ -0,0 +1,144 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio :label="1" v-model="radioValue">
不填允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="2" v-model="radioValue">
每年
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="3" v-model="radioValue">
周期从
<el-input-number v-model="cycle01" :min="fullYear" /> -
<el-input-number v-model="cycle02" :min="fullYear" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="4" v-model="radioValue">
<el-input-number v-model="average01" :min="fullYear" /> 年开始
<el-input-number v-model="average02" :min="fullYear" /> 年执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="5" v-model="radioValue">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple>
<el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item - 1 + fullYear" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
/* eslint-disable */
export default {
data() {
return {
fullYear: 0,
radioValue: 1,
cycle01: 0,
cycle02: 0,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check,
}
},
name: 'CrontabYear',
props: ['check', 'mouth', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
if (this.cron.mouth === '*') {
this.$emit('update', 'mouth', '0', 'year')
}
if (this.cron.day === '*') {
this.$emit('update', 'day', '0', 'year')
}
if (this.cron.hour === '*') {
this.$emit('update', 'hour', '0', 'year')
}
if (this.cron.min === '*') {
this.$emit('update', 'min', '0', 'year')
}
if (this.cron.second === '*') {
this.$emit('update', 'second', '0', 'year')
}
switch (this.radioValue) {
case 1:
this.$emit('update', 'year', '')
break
case 2:
this.$emit('update', 'year', '*')
break
case 3:
this.$emit('update', 'year', this.cycle01 + '-' + this.cycle02)
break
case 4:
this.$emit('update', 'year', this.average01 + '/' + this.average02)
break
case 5:
this.$emit('update', 'year', this.checkboxString)
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '3') {
this.$emit('update', 'year', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '4') {
this.$emit('update', 'year', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '5') {
this.$emit('update', 'year', this.checkboxString)
}
},
},
watch: {
radioValue: 'radioChange',
cycleTotal: 'cycleChange',
averageTotal: 'averageChange',
checkboxString: 'checkboxChange',
},
computed: {
// 计算两个周期值
cycleTotal: function() {
this.cycle01 = this.checkNum(this.cycle01, this.fullYear, this.fullYear + 100)
this.cycle02 = this.checkNum(this.cycle02, this.fullYear + 1, this.fullYear + 101)
return this.cycle01 + '-' + this.cycle02
},
// 计算平均用到的值
averageTotal: function() {
this.average01 = this.checkNum(this.average01, this.fullYear, this.fullYear + 100)
this.average02 = this.checkNum(this.average02, 1, 10)
return this.average01 + '/' + this.average02
},
// 计算勾选的checkbox值合集
checkboxString: function() {
const str = this.checkboxList.join()
return str
},
},
mounted: function() {
// 仅获取当前年份
this.fullYear = Number(new Date().getFullYear())
},
}
</script>

View File

@@ -0,0 +1,408 @@
<template>
<div :style="{ width: '490px' }">
<el-tabs type="card" class="ops-crontab">
<el-tab-pane label="" v-if="shouldHide('second')">
<CrontabSecond @update="updateContabValue" :check="checkNumber" ref="cronsecond" />
</el-tab-pane>
<el-tab-pane label="分钟" v-if="shouldHide('min')">
<CrontabMin @update="updateContabValue" :check="checkNumber" :cron="contabValueObj" ref="cronmin" />
</el-tab-pane>
<el-tab-pane label="小时" v-if="shouldHide('hour')">
<CrontabHour @update="updateContabValue" :check="checkNumber" :cron="contabValueObj" ref="cronhour" />
</el-tab-pane>
<el-tab-pane label="" v-if="shouldHide('day')">
<CrontabDay @update="updateContabValue" :check="checkNumber" :cron="contabValueObj" ref="cronday" />
</el-tab-pane>
<el-tab-pane label="" v-if="shouldHide('mouth')">
<CrontabMouth @update="updateContabValue" :check="checkNumber" :cron="contabValueObj" ref="cronmouth" />
</el-tab-pane>
<el-tab-pane label="" v-if="shouldHide('week')">
<CrontabWeek @update="updateContabValue" :check="checkNumber" :cron="contabValueObj" ref="cronweek" />
</el-tab-pane>
<el-tab-pane label="" v-if="shouldHide('year')">
<CrontabYear @update="updateContabValue" :check="checkNumber" :cron="contabValueObj" ref="cronyear" />
</el-tab-pane>
</el-tabs>
<div class="popup-main">
<div class="popup-result">
<p class="title">时间表达式</p>
<div style="padding: 12px;">
<div></div>
<table>
<thead>
<th v-for="item of displayTabTitles" width="40" :key="item.value">{{ item.label }}</th>
<th>crontab完整表达式</th>
</thead>
<tbody>
<td v-if="shouldHide('second')">
<span class="square">{{ contabValueObj.second }}</span>
</td>
<td v-if="shouldHide('min')">
<span class="square">{{ contabValueObj.min }}</span>
</td>
<td v-if="shouldHide('hour')">
<span class="square">{{ contabValueObj.hour }}</span>
</td>
<td v-if="shouldHide('day')">
<span class="square">{{ contabValueObj.day === '?' ? '*' : contabValueObj.day }}</span>
</td>
<td v-if="shouldHide('mouth')">
<span class="square">{{ contabValueObj.mouth }}</span>
</td>
<td v-if="shouldHide('week')">
<span class="square">{{ contabValueObj.week === '?' ? '*' : contabValueObj.week }}</span>
</td>
<td v-if="shouldHide('year')">
<span class="square">{{ contabValueObj.year }}</span>
</td>
<td>
<span class="rectangle">{{ displayContabValueString }}</span>
</td>
</tbody>
</table>
</div>
</div>
<!-- <CrontabResult :ex="contabValueString"></CrontabResult> -->
</div>
<div class="pop_btn" v-if="hasFooter">
<a-space>
<a-button size="small" type="primary" @click="submitFill">确定</a-button>
<a-button size="small" type="warning" @click="clearCron">重置</a-button>
<a-button size="small" @click="hidePopup">取消</a-button>
</a-space>
</div>
</div>
</template>
<script>
/* eslint-disable */
import CrontabSecond from './Crontab-Second.vue'
import CrontabMin from './Crontab-Min.vue'
import CrontabHour from './Crontab-Hour.vue'
import CrontabDay from './Crontab-Day.vue'
import CrontabMouth from './Crontab-Mouth.vue'
import CrontabWeek from './Crontab-Week.vue'
import CrontabYear from './Crontab-Year.vue'
import CrontabResult from './Crontab-Result.vue'
import { cronValidate } from './utils/index'
// 对表达式进行特异化处理 不展示 但是计算的时候还是有
export default {
data() {
return {
tabTitles: [
{ value: 'second', label: '' },
{ value: 'min', label: '分钟' },
{ value: 'hour', label: '小时' },
{ value: 'day', label: '' },
{ value: 'month', label: '' },
{ value: 'week', label: '' },
{ value: 'year', label: '' },
],
tabActive: 0,
myindex: 0,
contabValueObj: {
second: '*',
min: '*',
hour: '*',
day: '*',
mouth: '*',
week: '?',
year: '',
},
}
},
name: 'Vcrontab',
props: ['expression', 'hideComponent', 'defaultExpression', 'hasFooter'],
methods: {
shouldHide(key) {
if (this.hideComponent && this.hideComponent.includes(key)) return false
return true
},
resolveExp(expression) {
// 反解析 表达式
if (expression) {
const arr = expression.split(' ')
if (arr.length >= 6) {
// 6 位以上是合法表达式
const obj = {
second: arr[0],
min: arr[1],
hour: arr[2],
day: arr[3],
mouth: arr[4],
week: arr[5],
year: arr[6] ? arr[6] : '',
}
this.contabValueObj = {
...obj,
}
for (const i in obj) {
if (obj[i]) this.changeRadio(i, obj[i])
}
}
}
},
// tab切换值
tabCheck(index) {
this.tabActive = index
},
// 由子组件触发更改表达式组成的字段值
updateContabValue(name, value, from) {
'updateContabValue', name, value, from
this.$set(this.contabValueObj, name, value)
if (from && from !== name) {
// console.log(`来自组件 ${from} 改变了 ${name} ${value}`);
this.changeRadio(name, value)
}
},
// 赋值到组件
changeRadio(name, value) {
const arr = ['second', 'min', 'hour', 'mouth']
const refName = 'cron' + name
let insVlaue
if (!this.$refs[refName]) return
if (arr.includes(name)) {
if (value === '*') {
insVlaue = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
isNaN(indexArr[0]) ? (this.$refs[refName].cycle01 = 0) : (this.$refs[refName].cycle01 = indexArr[0])
this.$refs[refName].cycle02 = indexArr[1]
insVlaue = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
isNaN(indexArr[0]) ? (this.$refs[refName].average01 = 0) : (this.$refs[refName].average01 = indexArr[0])
this.$refs[refName].average02 = indexArr[1]
insVlaue = 3
} else {
insVlaue = 4
this.$refs[refName].checkboxList = value.split(',').map((v) => Number(v))
}
} else if (name == 'day') {
if (value === '*') {
insVlaue = 1
} else if (value == '?') {
insVlaue = 2
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
isNaN(indexArr[0]) ? (this.$refs[refName].cycle01 = 0) : (this.$refs[refName].cycle01 = indexArr[0])
this.$refs[refName].cycle02 = indexArr[1]
insVlaue = 3
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
isNaN(indexArr[0]) ? (this.$refs[refName].average01 = 0) : (this.$refs[refName].average01 = indexArr[0])
this.$refs[refName].average02 = indexArr[1]
insVlaue = 4
} else if (value.indexOf('W') > -1) {
const indexArr = value.split('W')
isNaN(indexArr[0]) ? (this.$refs[refName].workday = 0) : (this.$refs[refName].workday = indexArr[0])
insVlaue = 5
} else if (value === 'L') {
insVlaue = 6
} else {
this.$refs[refName].checkboxList = value.split(',')
insVlaue = 7
}
} else if (name == 'week') {
if (value === '*') {
insVlaue = 1
} else if (value == '?') {
insVlaue = 2
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
isNaN(indexArr[0]) ? (this.$refs[refName].cycle01 = 0) : (this.$refs[refName].cycle01 = indexArr[0])
this.$refs[refName].cycle02 = indexArr[1]
insVlaue = 3
} else if (value.indexOf('#') > -1) {
const indexArr = value.split('#')
isNaN(indexArr[0]) ? (this.$refs[refName].average01 = 1) : (this.$refs[refName].average01 = indexArr[0])
this.$refs[refName].average02 = indexArr[1]
insVlaue = 4
} else if (value.indexOf('L') > -1) {
const indexArr = value.split('L')
isNaN(indexArr[0]) ? (this.$refs[refName].weekday = 1) : (this.$refs[refName].weekday = indexArr[0])
insVlaue = 5
} else {
this.$refs[refName].checkboxList = value.split(',')
insVlaue = 6
}
} else if (name == 'year') {
if (value == '') {
insVlaue = 1
} else if (value == '*') {
insVlaue = 2
} else if (value.indexOf('-') > -1) {
insVlaue = 3
} else if (value.indexOf('/') > -1) {
insVlaue = 4
} else {
this.$refs[refName].checkboxList = value.split(',')
insVlaue = 5
}
}
this.$refs[refName].radioValue = insVlaue
},
// 表单选项的子组件校验数字格式通过-props传递
checkNumber(value, minLimit, maxLimit) {
// 检查必须为整数
value = Math.floor(value)
if (value < minLimit) {
value = minLimit
} else if (value > maxLimit) {
value = maxLimit
}
return value
},
// 隐藏弹窗
hidePopup() {
this.$emit('hide')
},
// 填充表达式
submitFill() {
const result = cronValidate(this.contabValueString)
console.log(result)
if (typeof result !== 'boolean') {
this.$message.warning(result)
return this.$emit('error', result)
}
this.$emit('fill', this.displayContabValueString)
this.hidePopup()
},
clearCron() {
// 还原选择项
this.resolveExp(this.defaultExpression || '* * * * * ?')
},
},
computed: {
contabValueString: function() {
const obj = this.contabValueObj
const str =
obj.second +
' ' +
obj.min +
' ' +
obj.hour +
' ' +
obj.day +
' ' +
obj.mouth +
' ' +
obj.week +
(obj.year == '' ? '' : ' ' + obj.year)
return str
},
displayContabValueString() {
//去掉第一位秒改成 * 仅作展示用
const _temp = this.contabValueString.substring(2)
const reg = /\?/g
return _temp.replace(reg, '*')
},
displayTabTitles() {
return this.tabTitles.filter((item) => !this.hideComponent.includes(item.value))
},
},
components: {
CrontabSecond,
CrontabMin,
CrontabHour,
CrontabDay,
CrontabMouth,
CrontabWeek,
CrontabYear,
CrontabResult,
},
watch: {
expression: {
handler(val) {
if (!val) {
this.clearCron()
return
}
this.resolveExp(val)
},
immediate: true,
},
},
mounted() {
// 初始化
if (this.expression) {
this.resolveExp(this.expression)
} else {
this.clearCron()
}
},
}
</script>
<style scoped>
.pop_btn {
text-align: right;
margin-top: 24px;
}
.popup-main {
position: relative;
margin: 16px auto;
background: #fff;
border-radius: 8px;
font-size: 12px;
overflow: hidden;
box-shadow: 0px 8px 16px rgba(160, 181, 235, 0.25);
}
.popup-title {
overflow: hidden;
line-height: 34px;
padding-top: 6px;
background: #f2f2f2;
}
.popup-result {
border-radius: 8px;
}
.popup-result .title {
background: #fff;
font-weight: 400;
font-size: 14px;
color: #2f54eb;
background-color: #f0f5ff;
margin: 0px;
box-sizing: border-box;
padding-left: 12px;
}
.popup-result table {
text-align: center;
width: 100%;
margin: 0 auto;
}
.popup-result table span {
display: block;
width: 100%;
font-family: arial;
line-height: 26px;
height: 26px;
white-space: nowrap;
overflow: hidden;
border: 1px solid #e8e8e8;
border-radius: 4px;
}
.popup-result table span.square {
width: 40px;
box-sizing: border-box;
}
.popup-result table span.rectangle {
width: 247px;
}
.popup-result-scroll {
font-size: 12px;
line-height: 24px;
height: 10em;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,2 @@
import Vcrontab from './Crontab.vue'
export default Vcrontab

View File

@@ -0,0 +1,441 @@
/* eslint-disable */
/*
!!!!!!!
以下为凶残的cron表达式验证胆小肾虚及心脏病者慎入!!!
不听劝告者后果自负T T
!!!!!!!
cron表达式为秒
判断正误方法错误的话返回错误信息正确的话返回true
*/
export function cronValidate(cronExpression ){
//返回错误信息用
var message = '';
//先将cron表达式进行分割
var cronParams = cronExpression.split(" ");
//判断cron表达式是否具有该具有的属性长度没有年份的长度为6带年份的长度为7其他情况都是错误的
if (cronParams.length < 6 || cronParams.length > 7) {
return "cron表达式需要输入6-7位参数请重新输入";
}else{
//日和周必须有一个为?或者全为*
if((cronParams[3] == "?" && cronParams[5] != "?") || (cronParams[5] == "?" && cronParams[3] != "?") || (cronParams[3] == "*" && cronParams[5] == "*")){
//检查第一位的秒是否正确
message = checkSecondsField(cronParams[0]);
if (message != true) {
return message;
}
//检查第二位的分是否正确
message = checkMinutesField(cronParams[1]);
if (message != true) {
return message;
}
//检查第三位的时是否正确
message = checkHoursField(cronParams[2]);
if (message != true) {
return message;
}
//检查第四位的日是否正确
message = checkDayOfMonthField(cronParams[3]);
if (message != true) {
return message;
}
//检查第五位的月是否正确
message = checkMonthsField(cronParams[4]);
if (message != true) {
return message;
}
//检查第6位的周是否正确
message = checkDayOfWeekField(cronParams[5]);
if (message != true) {
return message;
}
//检查第七位的年是否正确
if(cronParams.length>6){
message = checkYearField(cronParams[6]);
if (message != true) {
return message;
}
}
return true;
}else{
return "指定日时周必须设为不指定(?),指定周时日必须设为不指定(?)"
}
}
}
let message = ''
//检查秒的函数方法
function checkSecondsField(secondsField) {
return checkField(secondsField, 0, 59, "");
}
//检查分的函数方法
function checkMinutesField(minutesField) {
return checkField(minutesField, 0, 59, "");
}
//检查小时的函数方法
function checkHoursField(hoursField) {
return checkField(hoursField, 0, 23, "");
}
//检查日期的函数方法
function checkDayOfMonthField(dayOfMonthField) {
if (dayOfMonthField == "?") {
return true;
}
if (dayOfMonthField.indexOf("L") >= 0) {
return checkFieldWithLetter(dayOfMonthField, "L", 1, 7, "");
} else if ( dayOfMonthField.indexOf("W") >= 0) {
return checkFieldWithLetter(dayOfMonthField, "W", 1, 31, "");
} else if (dayOfMonthField.indexOf("C") >= 0) {
return checkFieldWithLetter(dayOfMonthField, "C", 1, 31, "");
}
return checkField( dayOfMonthField, 1, 31, "");
}
//检查月份的函数方法
function checkMonthsField(monthsField) {
//月份简写处理
if(monthsField != "*"){
monthsField=monthsField.replace("JAN", "1");
monthsField=monthsField.replace("FEB", "2");
monthsField=monthsField.replace("MAR", "3");
monthsField=monthsField.replace("APR", "4");
monthsField=monthsField.replace("MAY", "5");
monthsField=monthsField.replace("JUN", "6");
monthsField=monthsField.replace("JUL", "7");
monthsField=monthsField.replace("AUG", "8");
monthsField=monthsField.replace("SEP", "9");
monthsField=monthsField.replace("OCT", "10");
monthsField=monthsField.replace("NOV", "11");
monthsField=monthsField.replace("DEC", "12");
return checkField(monthsField, 1, 12, "月份");
}else{
return true;
}
}
//星期验证
function checkDayOfWeekField(dayOfWeekField) {
dayOfWeekField=dayOfWeekField.replace("SUN", "1" );
dayOfWeekField=dayOfWeekField.replace("MON", "2" );
dayOfWeekField=dayOfWeekField.replace("TUE", "3" );
dayOfWeekField=dayOfWeekField.replace("WED", "4" );
dayOfWeekField=dayOfWeekField.replace("THU", "5" );
dayOfWeekField=dayOfWeekField.replace("FRI", "6" );
dayOfWeekField=dayOfWeekField.replace("SAT", "7" );
if (dayOfWeekField == "?") {
return true;
}
if (dayOfWeekField.indexOf("L") >= 0) {
return checkFieldWithLetterWeek(dayOfWeekField, "L", 1, 7, "星期");
} else if (dayOfWeekField.indexOf("C") >= 0) {
return checkFieldWithLetterWeek(dayOfWeekField, "C", 1, 7, "星期");
} else if (dayOfWeekField.indexOf("#") >= 0) {
return checkFieldWithLetterWeek(dayOfWeekField, "#", 1, 7, "星期");
} else {
return checkField(dayOfWeekField, 1, 7, "星期");
}
}
//检查年份的函数方法
function checkYearField(yearField) {
return checkField(yearField, 1970, 2099, "年的");
}
//通用的检查值的大小范围的方法( - , / *)
function checkField(value, minimal, maximal, attribute) {
//校验值中是否有-如果有-的话下标会>0
if (value.indexOf("-") > -1 ) {
return checkRangeAndCycle(value, minimal, maximal,attribute);
}
//校验值中是否有如果有,的话下标会>0
else if (value.indexOf(",") > -1) {
return checkListField(value, minimal, maximal,attribute);
}
//校验值中是否有/如果有/的话下标会>0
else if (value.indexOf( "/" ) > -1) {
return checkIncrementField( value, minimal, maximal ,attribute);
}
//校验值是否为*
else if (value=="*") {
return true;
}
//校验单独的数字英文字母以及各种神奇的符号等...
else {
return checkIntValue(value, minimal, maximal,true, attribute);
}
}
//检测是否是整数以及是否在范围内,参数检测的值下限上限是否检查端点检查的属性
function checkIntValue(value, minimal, maximal, checkExtremity,attribute) {
try {
//用10进制犯法来进行整数转换
var val = parseInt(value, 10);
if (value == val) {
if (checkExtremity) {
if (val < minimal || val > maximal) {
return (attribute+"的参数取值范围必须在"+ minimal + "-" + maximal +"之间");
}
return true;
}
return true;
}
return (attribute+"的参数存在非法字符,必须为整数或允许的大写英文");
} catch (e) {
return (attribute+"的参数有非法字符,必须是整数~")
}
}
//检验枚举类型的参数是否正确
function checkListField(value, minimal, maximal,attribute) {
var st = value.split(",");
var values = new Array(st.length);
//计算枚举的数字在数组中中出现的次数出现一次为没有重复的
var count=0;
for(var j = 0; j < st.length; j++) {
values[j] = st[j];
}
//判断枚举类型的值是否重复
for(var i=0;i<values.length;i++){
//判断枚举的值是否在范围内
message = checkIntValue(values[i], minimal, maximal, true, attribute);
if (message!=true) {
return message;
}
count=0;
for(var j=0;j<values.length;j++){
if(values[i]==values[j])
{
count++;
}
if(count>1){
return (attribute+"中的参数重复");
}
}
}
var previousValue = -1;
//判断枚举的值是否排序正确
for (var i= 0; i < values.length; i++) {
var currentValue = values[i];
try {
var val = parseInt(currentValue, 10);
if (val < previousValue) {
return (attribute+"的参数应该从小到大");
} else {
previousValue = val;
}
} catch (e) {
//前面验证过了这边的代码不可能跑到
return ("这段提示用不到")
}
}
return true;
}
//检验循环
function checkIncrementField(value, minimal, maximal, attribute) {
if(value.split("/").length>2){
return (attribute + "中的参数只能有一个'/'");
}
var start = value.substring(0, value.indexOf("/"));
var increment = value.substring(value.indexOf("/") + 1);
if (start != "*") {
//检验前值是否正确
message = checkIntValue(start, minimal, maximal, true, attribute);
if(message != true){
return message;
}
//检验后值是否正确
message = checkIntValue(increment, minimal, maximal, true, attribute);
if(message != true){
return message;
}
return true;
} else {
//检验后值是否正确
return checkIntValue(increment, minimal, maximal, false, attribute);
}
}
//检验范围
function checkRangeAndCycle(params, minimal, maximal, attribute){
//校验-符号是否只有一个
if(params.split("-").length>2){
return (attribute + "中的参数只能有一个'-'");
}
var value = null;
var cycle = null;
//检验范围内是否有嵌套周期
if(params.indexOf("/") > -1){
//校验/符号是否只有一个
if(params.split("/").length>2){
return (attribute + "中的参数只能有一个'/'");
}
value = params.split("/")[0];
cycle = params.split("/")[1];
//判断循环的参数是否正确
message =checkIntValue(cycle, minimal, maximal, true, attribute);
if (message!=true) {
return message;
}
}else{
value = params;
}
var startValue = value.substring(0, value.indexOf( "-" ));
var endValue = value.substring(value.indexOf( "-" ) + 1);
//判断参数范围的第一个值是否正确
message =checkIntValue(startValue, minimal, maximal, true, attribute);
if (message!=true) {
return message;
}
//判断参数范围的第二个值是否正确
message =checkIntValue(endValue, minimal, maximal, true, attribute);
if(message!=true){
return message;
}
//判断参数的范围前值是否小于后值
try {
var startVal = parseInt(startValue, 10);
var endVal = parseInt(endValue, 10);
if(endVal < startVal){
return (attribute+"的取值范围错误,前值必须小于后值");
}
if((endVal-startVal)<parseInt(cycle,10)){
return (attribute+"的取值范围内的循环无意义");
}
return true;
} catch (e) {
//用不到这行代码的
return (attribute+"的参数有非法字符,必须是整数");
}
}
//检查日中的特殊字符
function checkFieldWithLetter(value, letter, minimalBefore, maximalBefore,attribute) {
//判断是否只有一个字母
for(var i=0;i<value.length;i++){
var count = 0;
if(value.charAt(i)==letter){
count++;
}
if(count>1){
return (attribute+"的值的"+letter+"字母只能有一个")
}
}
//校验L
if(letter == "L"){
if(value == "LW"){
return true;
}
if(value=="L"){
return true;
}
if(value.endsWith("LW")&&value.length>2)
{
return (attribute + "中的参数最后的LW前面不能有任何字母参数")
}
if(!value.endsWith("L"))
{
return (attribute + "中的参数L字母后面不能有W以外的字符、数字等")
}else{
var num = value.substring(0,value.indexOf(letter));
return checkIntValue(num, minimalBefore, maximalBefore, true, attribute);
}
}
//校验W
if(letter == "W"){
if(!value.endsWith("W")){
return (attribute + "中的参数的W必须作为结尾")
}else{
if(value=="W"){
return (attribute + "中的参数的W前面必须有数字")
}
var num = value.substring(0,value.indexOf(letter));
return checkIntValue(num, minimalBefore, maximalBefore, true, attribute);
}
}
if(letter == "C"){
if(!value.endsWith("C")){
return (attribute + "中的参数的C必须作为结尾")
}else{
if(value=="C"){
return (attribute + "中的参数的C前面必须有数字")
}
var num = value.substring(0,value.indexOf(letter));
return checkIntValue(num, minimalBefore, maximalBefore, true, attribute);
}
}
}
//检查星期中的特殊字符
function checkFieldWithLetterWeek(value, letter, minimalBefore, maximalBefore,attribute) {
//判断是否只有一个字母
for(var i=0;i<value.length;i++){
var count = 0;
if(value.charAt(i)==letter){
count++;
}
if(count>1){
return (attribute+"的值的"+letter+"字母只能有一个")
}
}
//校验L
if(letter == "L"){
if(value=="L"){
return true;
}
if(!value.endsWith("L"))
{
return (attribute + "中的参数L字母必须是最后一位")
}else{
var num = value.substring(0,value.indexOf(letter));
return checkIntValue(num, minimalBefore, maximalBefore, true, attribute);
}
}
if(letter == "C"){
if(!value.endsWith("C")){
return (attribute + "中的参数的C必须作为结尾")
}else{
if(value=="C"){
return (attribute + "中的参数的C前面必须有数字")
}
var num = value.substring(0,value.indexOf(letter));
return checkIntValue(num, minimalBefore, maximalBefore, true, attribute);
}
}
if(letter == "#"){
if(value=="#"){
return (attribute + "中的#前后必须有整数");
}
if(value.charAt(0)==letter){
return (attribute + "中的#前面必须有整数")
}
if(value.endsWith("#")){
return (attribute + "中的#后面必须有整数")
}
var num1 = value.substring(0,value.indexOf(letter));
var num2 = value.substring(value.indexOf(letter)+1,value.length)
message = checkIntValue(num1, 1, 4, true, (attribute+"的#前面"));
if(message!=true){
return message;
}
message = checkIntValue(num2, minimalBefore, maximalBefore, true, (attribute+"的#后面"));
if(message!=true){
return message;
}
return true;
}
}

View File

@@ -0,0 +1,2 @@
import CusotomCodeMirror from './index.vue'
export default CusotomCodeMirror

View File

@@ -0,0 +1,275 @@
<template>
<div :style="{ marginTop: '20px' }">
<div class="codemirror-toolbar">
<div class="codemirror-toolbar-item">
<a class="icon" @click="changeFontSize(-1)"><a-icon type="minus-circle"/></a>
<span> A </span>
<a class="icon" @click="changeFontSize(1)"><a-icon type="plus-circle"/></a>
</div>
<div class="codemirror-toolbar-item" :style="{ position: 'absolute', right: '0' }">
<a class="icon" @click="changeFullscreen"><a-icon type="fullscreen"/></a>
</div>
<div class="codemirror-toolbar-item">
<a-select :value="keyMap" :style="{ width: '100px' }" @change="changeKeyMap">
<a-select-option v-for="item in keyMapList" :key="item.value" :value="item.value">{{
item.label
}}</a-select-option>
</a-select>
</div>
</div>
<textarea :id="codeMirrorId" :style="{ width: '100%' }" />
<div class="codemirror-toolbar-fullscreen-exit" v-if="fullscreenExitVisible" @click="changeFullscreen">
<a-icon type="fullscreen-exit" />
</div>
</div>
</template>
<script>
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/show-hint.css'
// import 'codemirror/addon/fold/foldcode.js'
// import 'codemirror/addon/fold/foldgutter.js'
// import 'codemirror/addon/fold/brace-fold.js'
// import 'codemirror/addon/fold/indent-fold.js'
// import 'codemirror/addon/fold/comment-fold.js'
// import 'codemirror/addon/edit/closebrackets.js'
// import 'codemirror/addon/edit/matchbrackets.js'
// import 'codemirror/addon/selection/active-line.js'
import 'codemirror/addon/display/fullscreen.js'
import 'codemirror/addon/display/fullscreen.css'
import 'codemirror/addon/dialog/dialog.js'
import 'codemirror/addon/dialog/dialog.css'
import 'codemirror/addon/search/searchcursor.js'
import 'codemirror/addon/search/search.js'
import 'codemirror/addon/search/matchesonscrollbar.css'
import 'codemirror/addon/scroll/annotatescrollbar.js'
import 'codemirror/addon/search/matchesonscrollbar.js'
import 'codemirror/addon/search/jump-to-line.js'
import 'codemirror/addon/search/match-highlighter.js'
import 'codemirror/keymap/vim.js'
import 'codemirror/keymap/emacs.js'
import 'codemirror/keymap/sublime.js'
import 'codemirror/addon/edit/matchbrackets.js'
import 'codemirror/addon/edit/closebrackets.js'
import 'codemirror/addon/comment/comment.js'
import 'codemirror/addon/wrap/hardwrap.js'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/brace-fold.js'
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/mode/clike/clike.js'
require('codemirror/mode/python/python.js')
require('codemirror/mode/shell/shell.js')
require('codemirror/mode/powershell/powershell.js')
export default {
name: 'CustomCodeMirror',
// props: {
// codeContent: {
// type: String,
// default: '',
// },
// },
// model: {
// prop: 'codeContent',
// event: 'change',
// },
props: {
codeMirrorId: {
type: String,
default: 'codemirror',
},
},
data() {
const keyMapList = [
{ value: 'default', label: '默认' },
{ value: 'vim', label: 'vim' },
{ value: 'emacs', label: 'emacs' },
{ value: 'sublime', label: 'sublime' },
]
return {
keyMapList,
coder: null,
fontSize: 14,
keyMap: 'default',
fullscreenExitVisible: false,
}
},
mounted() {},
methods: {
initCodeMirror(codeContent) {
const that = this
if (this.coder) {
this.coder.setValue(codeContent)
return
}
this.coder = CodeMirror.fromTextArea(document.getElementById(this.codeMirrorId), {
lineNumbers: true,
mode: 'python',
theme: 'monokai',
smartIndent: true,
tabSize: 4,
lineWrapping: false,
indentUnit: 4,
extraKeys: {
F11: function(cm) {
that.fullscreenExitVisible = !cm.getOption('fullScreen')
cm.setOption('fullScreen', !cm.getOption('fullScreen'))
},
Tab: (cm) => {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
cm.replaceSelection(Array(cm.getOption('indentUnit') + 1).join(' '), 'end', '+input')
}
},
'Shift-Tab': (cm) => {
if (cm.somethingSelected()) {
cm.indentSelection('subtract')
} else {
const cursor = cm.getCursor()
cm.setCursor({ line: cursor.line, ch: cursor.ch - 4 })
}
},
// Esc: function(cm) {
// if (cm.getOption('fullScreen')) cm.setOption('fullScreen', false)
// },
},
hintOptions: {
// 自定义提示选项
completeSingle: false, // 当匹配只有一项的时候是否自动补全
},
})
const keyMap = localStorage.getItem('dagCodeMirrorKeyMap')
if (keyMap) {
this.changeKeyMap(keyMap)
}
this.coder.setValue(codeContent)
this.coder.on('keypress', () => {
// 显示智能提示
this.coder.showHint()
})
this.coder.on('change', () => {
this.$emit('changeCodeContent', this.coder.getValue())
})
this.coder.on('focus', () => {
this.$emit('focus')
})
},
changeStyle(value) {
switch (value) {
case '0':
this.coder.setOption('mode', 'shell')
break
case '1':
this.coder.setOption('mode', 'shell')
break
case '3':
this.coder.setOption('mode', 'powershell')
break
case '4':
this.coder.setOption('mode', 'ruby')
break
default:
this.coder.setOption('mode', 'python')
}
},
changeFontSize(num) {
const element = document.getElementsByClassName('CodeMirror')[0]
if (element) {
if (num === -1 && this.fontSize <= 12) {
return
}
if (num === 1 && this.fontSize >= 25) {
return
}
this.fontSize = this.fontSize + num
element.style.fontSize = `${this.fontSize}px`
}
},
changeFullscreen() {
this.fullscreenExitVisible = !this.coder.getOption('fullScreen')
this.coder.setOption('fullScreen', !this.coder.getOption('fullScreen'))
},
changeKeyMap(value) {
this.keyMap = value
localStorage.setItem('dagCodeMirrorKeyMap', value)
this.coder.setOption('keyMap', value)
this.coder.setOption('matchBrackets', true)
if (value === 'vim') {
// var commandDisplay = document.getElementById('command-display')
var keys = ''
CodeMirror.on(this.coder, 'vim-keypress', function(key) {
keys = keys + key
// commandDisplay.innerText = keys
})
CodeMirror.on(this.coder, 'vim-command-done', function(e) {
keys = ''
// commandDisplay.innerHTML = keys
})
// var vimMode = document.getElementById('vim-mode')
// CodeMirror.on(this.coder, 'vim-mode-change', function(e) {
// vimMode.innerText = JSON.stringify(e)
// })
}
},
},
}
</script>
<style lang="less">
.CodeMirror {
border: 1px solid silver;
border-width: 1px 2px;
line-height: 150%;
font-size: 14px;
}
.CodeMirror-hints {
z-index: 9999;
}
.codemirror-toolbar {
width: 100%;
height: 34px;
background-color: #fafafa;
border: 1px solid #d9d9d9;
position: relative;
.codemirror-toolbar-item {
display: inline-block;
height: 30px;
line-height: 30px;
padding: 0 10px;
vertical-align: text-bottom;
.icon {
color: #000000a6;
&:hover {
color: black;
}
}
}
}
.codemirror-toolbar-fullscreen-exit {
position: fixed;
z-index: 9999;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
border-radius: 15px;
background-color: #f3f3f3;
text-align: center;
line-height: 30px;
cursor: pointer;
&:hover {
color: black;
}
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<a-drawer
ref="customDrawer"
v-bind="$attrs"
v-on="$listeners"
:closable="false"
:placement="placement"
:bodyStyle="{ maxHeight: bodyMaxHeight, overflow: 'auto', ...$attrs.bodyStyle }"
:keyboard="false"
>
<div v-if="closable" :class="`custom-drawer-close custom-drawer-${placement}`" @click="clickCustomClose">
<a-icon :type="closeIconType" />
</div>
<template v-if="hasTitle" slot="title">
<slot name="title">{{ title }}</slot>
</template>
<slot></slot>
</a-drawer>
</template>
<script>
export default {
name: 'CustomDrawer',
components: {},
props: {
closable: {
type: Boolean,
default: true,
},
placement: {
type: String,
default: 'right',
},
hasTitle: {
type: Boolean,
default: true,
},
hasFooter: {
type: Boolean,
default: true,
},
title: {
type: String,
default: '',
},
},
computed: {
closeIconType() {
if (this.placement === 'top') return 'up'
if (this.placement === 'bottom') return 'down'
return this.placement || 'right'
},
customClass() {
if (!this.placement) return 'right'
return this.placement
},
bodyMaxHeight() {
const titleHeight = this.hasTitle ? 55 : 0
const footerHeight = this.hasFooter ? 53 : 0
return `calc(100vh - ${titleHeight + footerHeight}px)`
},
},
methods: {
clickCustomClose() {
this.$refs.customDrawer.close()
},
},
}
</script>
<style lang="less">
@import '~@/style/static.less';
.custom-drawer-close {
position: absolute;
cursor: pointer;
background: #custom_colors[color_1];
color: white;
text-align: center;
transition: all 0.3s;
z-index: 1;
&:hover {
background: #597ef7;
}
}
.custom-drawer-right,
.custom-drawer-left {
width: 14px;
height: 50px;
top: 50%;
transform: translateY(-50%);
line-height: 50px;
}
.custom-drawer-left {
right: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.custom-drawer-right {
left: 0;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.custom-drawer-top,
.custom-drawer-bottom {
width: 50px;
height: 14px;
left: 50%;
transform: translateX(-50%);
line-height: 14px;
}
.custom-drawer-top {
bottom: 0;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.custom-drawer-bottom {
top: 0;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
</style>

View File

@@ -0,0 +1,2 @@
import CustomDrawer from './CustomDrawer'
export default CustomDrawer

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
import CustomIconSelect from './index.vue'
export default CustomIconSelect

View File

@@ -0,0 +1,248 @@
<template>
<a-popover
:visible="visible"
overlayClassName="custom-icon-select-popover"
:destroyTooltipOnHide="true"
placement="bottom"
>
<div id="custom-icon-select-popover" slot="content">
<div class="custom-icon-select-popover-icon-type">
<div
:class="`${currentIconType === item.value ? 'selected' : ''}`"
v-for="item in iconTypeList"
:key="item.value"
@click="handleChangeIconType(item.value)"
>
{{ item.label }}
</div>
</div>
<div class="custom-icon-select-popover-content">
<div v-for="category in iconList" :key="category.value">
<h4 class="category">{{ category.label }}</h4>
<div class="custom-icon-select-popover-content-wrapper">
<div
v-for="name in category.list"
:key="name.value"
:class="`custom-icon-select-popover-item ${value.name === name.value ? 'selected' : ''}`"
@click="clickIcon(name.value)"
>
<ops-icon :type="name.value" />
<span class="custom-icon-select-popover-item-label">{{ name.label }}</span>
</div>
</div>
</div>
</div>
<template v-if="currentIconType !== '0' && currentIconType !== '3'">
<a-divider :style="{ margin: '5px 0' }" />
<el-color-picker size="mini" v-model="value.color"> </el-color-picker>
</template>
</div>
<div class="custom-icon-select-block" id="custom-icon-select-block" @click="showSelect">
<ops-icon
:type="value.name"
:style="{ color: value.name && value.name.startsWith('icon-') ? value.color || '' : '' }"
/>
</div>
</a-popover>
</template>
<script>
import { ColorPicker } from 'element-ui'
import {
iconTypeList,
commonIconList,
linearIconList,
fillIconList,
multicolorIconList,
} from './constants'
export default {
name: 'CustomIconSelect',
components: { ElColorPicker: ColorPicker },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Object,
default: () => {
return { name: '', color: '' }
},
},
iconType: {
type: String,
default: 'cmdb',
},
},
data() {
return {
iconTypeList,
commonIconList,
linearIconList,
fillIconList,
multicolorIconList,
visible: false,
currentIconType: '1',
}
},
computed: {
iconList() {
switch (this.currentIconType) {
case '0': // 常用
return this.commonIconList
case '1': // 线性
return this.linearIconList
case '2': // 实底
return this.fillIconList
case '3': // 多色
return this.multicolorIconList
default:
return this.linearIconList
}
},
},
mounted() {
document.addEventListener('click', this.eventListener)
},
beforeDestroy() {
document.removeEventListener('click', this.eventListener)
},
methods: {
eventListener(e) {
if (this.visible) {
const dom = document.getElementById(`custom-icon-select-popover`)
const dom_icon = document.getElementById(`custom-icon-select-block`)
e.stopPropagation()
e.preventDefault()
if (dom) {
const isSelf = dom.contains(e.target) || dom_icon.contains(e.target)
if (!isSelf) {
this.visible = false
}
}
}
},
clickIcon(name) {
// 当name一样时取消选择
if (name === this.value.name) {
this.$emit('change', {
name: '',
color: '',
})
} else {
this.$emit('change', {
name,
color: this.value.name && this.value.name.startsWith('icon-') ? this.value.color || '' : '',
})
}
},
showSelect() {
this.visible = true
if (!this.value.name) {
this.currentIconType = '1'
return
}
if (this.value.name.startsWith('changyong-')) {
this.currentIconType = '0'
} else if (this.value.name.startsWith('icon-xianxing')) {
this.currentIconType = '1'
} else if (this.value.name.startsWith('icon-shidi')) {
this.currentIconType = '2'
} else {
this.currentIconType = '3'
}
},
handleChangeIconType(value) {
this.currentIconType = value
},
},
}
</script>
<style lang="less">
.custom-icon-select-popover.ant-popover-placement-top .ant-popover-content {
margin-bottom: -10px;
}
.custom-icon-select-popover {
width: 650px;
overflow: auto;
padding-top: 0;
box-shadow: 0px 2px 12px rgba(0, 0, 0, 0.1);
.ant-popover-arrow {
display: none;
}
.ant-popover-inner-content {
padding: 4px 6px;
}
.custom-icon-select-popover-content {
max-height: 400px;
overflow: auto;
.category {
font-size: 14px;
}
.custom-icon-select-popover-content-wrapper {
font-size: 24px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-left: 10px;
.custom-icon-select-popover-item {
width: 60px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
padding: 5px 5px 2px 5px;
margin: 0 2px 6px;
color: #666;
.custom-icon-select-popover-item-label {
margin-top: 6px;
font-size: 11px;
}
&:hover {
background-color: #eeeeee;
}
}
.selected {
background-color: #eeeeee;
}
}
}
.custom-icon-select-popover-icon-type {
display: inline-block;
> div {
cursor: pointer;
display: inline-block;
padding: 2px 8px;
border: 1px solid #eeeeee;
&:hover {
color: #2f54eb;
}
}
.selected {
border-color: #2f54eb;
}
}
}
</style>
<style lang="less" scoped>
.custom-icon-select-block {
position: relative;
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid #eeeeee;
display: inline-block;
cursor: pointer;
> i {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 18px;
}
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="custom-radio">
<div
:class="`custom-radio-inner custom-radio-inner-${layout || 'inline'}`"
v-for="{ value: radioValue, label, layout } in radioList"
:key="radioValue"
>
<a-radio @click="clickRadio(radioValue)" :checked="value === radioValue" :key="`raido_${radioValue}`">{{
label
}}</a-radio>
<slot :name="`extra_${radioValue}`" v-bind="{ radioValue, label }"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'CustomRadio',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Number],
default: '',
},
radioList: {
type: Array,
default: () => [],
},
},
methods: {
clickRadio(radioValue) {
this.$emit('change', radioValue)
},
},
}
</script>
<style lang="less" scoped>
.custom-radio {
.custom-radio-inner {
min-height: 40px;
}
.custom-radio-inner-inline {
display: flex;
align-items: center;
}
.custom-radio-inner-vertical label {
line-height: 40px;
}
}
</style>

View File

@@ -0,0 +1,2 @@
import CustomRadio from './CustomRadio'
export default CustomRadio

View File

@@ -0,0 +1,65 @@
<template>
<a-transfer v-bind="$attrs" v-on="$listeners" :data-source="dataSource" :target-keys="targetKeys"> </a-transfer>
</template>
<script>
export default {
name: 'CustomTransfer',
props: {
dataSource: {
type: Array,
default: () => [],
},
targetKeys: {
type: Array,
default: () => [],
},
},
methods: {
// 穿梭框双击实现
leftToRight(leftList, dataSource, targetKeys, sourceImportantKey, targetImportantKey) {
for (let i = 0; i < leftList.length; i++) {
leftList[i].ondblclick = e => {
dataSource.forEach(item => {
if (item[`${sourceImportantKey}`] === e.toElement.innerText) {
targetKeys.push(item[`${targetImportantKey}`])
}
})
}
}
},
rightToLeft(rightList, dataSource, targetKeys, sourceImportantKey, targetImportantKey) {
for (let i = 0; i < rightList.length; i++) {
rightList[i].ondblclick = e => {
dataSource.forEach(item => {
if (item[`${sourceImportantKey}`] === e.toElement.innerText) {
const idx = targetKeys.findIndex(item1 => {
return item1 === item[`${targetImportantKey}`]
})
targetKeys.splice(idx, 1)
}
})
}
}
},
// 必须传入importantKey用来做键名比对传错或不传会造成错误
dbClick(sourceSelectedKeys, targetSelectedKeys, sourceImportantKey, targetImportantKey) {
window.setTimeout(() => {
const element = document.getElementsByClassName('ant-transfer-list-content')
if (this.dataSource.length !== this.targetKeys.length) {
const leftList = element[0].children
const rightList = element[1] ? element[1].children : []
this.leftToRight(leftList, this.dataSource, this.targetKeys, sourceImportantKey, targetImportantKey)
this.rightToLeft(rightList, this.dataSource, this.targetKeys, sourceImportantKey, targetImportantKey)
}
if (this.targetKeys.length && this.targetKeys.length === this.dataSource.length) {
const rightList = element[0].children
this.rightToLeft(rightList, this.dataSource, this.targetKeys, sourceImportantKey, targetImportantKey)
}
}, 100)
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,2 @@
import CustomTransfer from './CustomTransfer'
export default CustomTransfer

View File

@@ -1,153 +0,0 @@
<template>
<div :class="['description-list', size, layout === 'vertical' ? 'vertical': 'horizontal']">
<div v-if="title" class="title">{{ title }}</div>
<a-row>
<slot></slot>
</a-row>
</div>
</template>
<script>
import { Col } from 'ant-design-vue/es/grid/'
const Item = {
name: 'DetailListItem',
props: {
term: {
type: String,
default: '',
required: false
}
},
inject: {
col: {
type: Number
}
},
render () {
return (
<Col {...{ props: responsive[this.col] }}>
<div class="term">{this.$props.term}</div>
<div class="content">{this.$slots.default}</div>
</Col>
)
}
}
const responsive = {
1: { xs: 24 },
2: { xs: 24, sm: 12 },
3: { xs: 24, sm: 12, md: 8 },
4: { xs: 24, sm: 12, md: 6 }
}
export default {
name: 'DetailList',
Item: Item,
components: {
Col
},
props: {
title: {
type: String,
default: '',
required: false
},
col: {
type: Number,
required: false,
default: 3
},
size: {
type: String,
required: false,
default: 'large'
},
layout: {
type: String,
required: false,
default: 'horizontal'
}
},
provide () {
return {
col: this.col > 4 ? 4 : this.col
}
}
}
</script>
<style lang="less" scoped>
.description-list {
.title {
color: rgba(0,0,0,.85);
font-size: 14px;
font-weight: 500;
margin-bottom: 16px;
}
/deep/ .term {
color: rgba(0,0,0,.85);
// display: table-cell;
line-height: 20px;
margin-right: 8px;
padding-bottom: 16px;
white-space: nowrap;
&:not(:empty):after {
content: "";
margin: 0 8px 0 2px;
position: relative;
top: -.5px;
}
}
/deep/ .content {
color: rgba(0,0,0,.65);
// display: table-cell;
min-height: 22px;
line-height: 22px;
padding-bottom: 16px;
width: 100%;
&:empty {
content: ' ';
height: 38px;
padding-bottom: 16px;
}
}
&.small {
.title {
font-size: 14px;
color: rgba(0, 0, 0, .65);
font-weight: normal;
margin-bottom: 12px;
}
/deep/ .term, .content {
padding-bottom: 8px;
}
}
&.large {
/deep/ .term, .content {
padding-bottom: 16px;
}
.title {
font-size: 16px;
}
}
&.vertical {
.term {
padding-bottom: 8px;
}
/deep/ .term, .content {
display: block;
}
}
}
</style>

View File

@@ -1,2 +0,0 @@
import DescriptionList from './DescriptionList'
export default DescriptionList

View File

@@ -1,82 +0,0 @@
<template>
<div :class="prefixCls">
<quill-editor
v-model="content"
ref="myQuillEditor"
:options="editorOption"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@ready="onEditorReady($event)"
@change="onEditorChange($event)">
</quill-editor>
</div>
</template>
<script>
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import { quillEditor } from 'vue-quill-editor'
export default {
name: 'QuillEditor',
components: {
quillEditor
},
props: {
prefixCls: {
type: String,
default: 'ant-editor-quill'
},
// 表单校验用字段
// eslint-disable-next-line
value: {
type: String
}
},
data () {
return {
content: null,
editorOption: {
// some quill options
}
}
},
methods: {
onEditorBlur (quill) {
console.log('editor blur!', quill)
},
onEditorFocus (quill) {
console.log('editor focus!', quill)
},
onEditorReady (quill) {
console.log('editor ready!', quill)
},
onEditorChange ({ quill, html, text }) {
console.log('editor change!', quill, html, text)
this.$emit('change', html)
}
},
watch: {
value (val) {
this.content = val
}
}
}
</script>
<style lang="less" scoped>
@import url('../index.less');
/* 覆盖 quill 默认边框圆角为 ant 默认圆角用于统一 ant 组件风格 */
.ant-editor-quill {
/deep/ .ql-toolbar.ql-snow {
border-radius: @border-radius-base @border-radius-base 0 0;
}
/deep/ .ql-container.ql-snow {
border-radius: 0 0 @border-radius-base @border-radius-base;
}
}
</style>

View File

@@ -1,57 +0,0 @@
<template>
<div :class="prefixCls">
<div ref="editor" class="editor-wrapper"></div>
</div>
</template>
<script>
import WEditor from 'wangeditor'
export default {
name: 'WangEditor',
props: {
prefixCls: {
type: String,
default: 'ant-editor-wang'
},
// eslint-disable-next-line
value: {
type: String
}
},
data () {
return {
editor: null,
editorContent: null
}
},
watch: {
value (val) {
this.editorContent = val
this.editor.txt.html(val)
}
},
mounted () {
this.initEditor()
},
methods: {
initEditor () {
this.editor = new WEditor(this.$refs.editor)
// this.editor.onchangeTimeout = 200
this.editor.customConfig.onchange = (html) => {
this.editorContent = html
this.$emit('change', this.editorContent)
}
this.editor.create()
}
}
}
</script>
<style lang="less" scoped>
.ant-editor-wang {
.editor-wrapper {
text-align: left;
}
}
</style>

View File

@@ -1,64 +0,0 @@
<script>
import Tooltip from 'ant-design-vue/es/tooltip'
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
/*
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
const TooltipOverlayStyle = {
overflowWrap: 'break-word',
wordWrap: 'break-word',
};
*/
export default {
name: 'Ellipsis',
components: {
Tooltip
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-ellipsis'
},
tooltip: {
type: Boolean
},
length: {
type: Number,
required: true
},
lines: {
type: Number,
default: 1
},
fullWidthRecognition: {
type: Boolean,
default: false
}
},
methods: {
getStrDom (str, fullLength) {
return (
<span>{ cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '') }</span>
)
},
getTooltip (fullStr, fullLength) {
return (
<Tooltip>
<template slot="title">{ fullStr }</template>
{ this.getStrDom(fullStr, fullLength) }
</Tooltip>
)
}
},
render () {
const { tooltip, length } = this.$props
const str = this.$slots.default.map(vNode => vNode.text).join('')
const fullLength = getStrFullLength(str)
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
return (
strDom
)
}
}
</script>

View File

@@ -1,3 +0,0 @@
import Ellipsis from './Ellipsis'
export default Ellipsis

View File

@@ -1,38 +0,0 @@
# Ellipsis 文本自动省略号
文本过长自动处理省略号支持按照文本长度和最大行数两种方式截取
引用方式
```javascript
import Ellipsis from '@/components/Ellipsis'
export default {
components: {
Ellipsis
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<ellipsis :length="100" tooltip>
There were injuries alleged in three cases in 2015, and a
fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
</ellipsis>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
tooltip | 移动到文本展示完整内容的提示 | boolean | -
length | 在按照长度截取下的文本最大字符数超过则截取省略 | number | -

View File

@@ -0,0 +1,2 @@
import EmployeeTransfer from './index.vue'
export default EmployeeTransfer

View File

@@ -0,0 +1,293 @@
<template>
<div class="employee-transfer" :style="{ '--custom-height': `${height}px` }">
<div class="employee-transfer-left" v-if="!readOnly">
<treeselect
:disable-branch-nodes="disableBranchNodes"
:flat="true"
:multiple="true"
:options="employeeTreeSelectOption"
placeholder="请输入搜索内容"
v-model="treeValue"
:max-height="height - 50"
noChildrenText=""
noOptionsText=""
:clearable="false"
:always-open="true"
:default-expand-level="1"
:class="{ 'employee-transfer': true, 'employee-transfer-has-input': !!inputValue }"
@search-change="changeInputValue"
noResultsText="暂无数据"
openDirection="below"
>
</treeselect>
</div>
<div class="employee-transfer-operation" v-if="!readOnly">
<div @click="handleRight" class="operation-right"><a-icon type="right" /></div>
<br />
<div @click="handleLeft" class="operation-left"><a-icon type="left" /></div>
</div>
<div class="employee-transfer-right">
<div
:class="{
'employee-transfer-right-item': true,
'employee-transfer-right-selected': !readOnly && selectedRight.includes(right),
}"
v-for="right in rightData"
:key="right"
@click="handleSelectedRight(right)"
>
{{ getLabel(right) }}
</div>
</div>
</div>
</template>
<script>
import { getAllDepAndEmployee, getAllDepartmentList } from '@/api/company'
import { getEmployeeList } from '@/api/employee'
import { formatOption } from '@/utils/util'
export default {
name: 'EmployeeTransfer',
inject: {
getDataBySelf: {
from: 'getDataBySelf',
default: true,
},
provide_allTreeDepAndEmp: {
default: () => null,
},
provide_allFlatDepartments: {
default: () => null,
},
provide_allFlatEmployees: {
default: () => null,
},
},
props: {
height: {
type: Number,
default: 260,
},
disableBranchNodes: {
type: Boolean,
default: false,
},
uniqueKey: {
type: String,
default: '',
},
readOnly: {
type: Boolean,
default: false,
},
isDisabledAllCompany: {
type: Boolean,
default: false,
},
},
data() {
return {
default_allTreeDepAndEmp: [],
treeValue: [],
inputValue: '',
rightData: [],
selectedRight: [],
default_allFlatDepartments: [],
default_allFlatEmployees: [],
}
},
computed: {
employeeTreeSelectOption() {
return formatOption(
this.allTreeDepAndEmp,
2,
this.isDisabledAllCompany,
this.uniqueKey || 'department_id',
this.uniqueKey || 'employee_id'
)
},
allTreeDepAndEmp() {
if (this.getDataBySelf) {
return this.default_allTreeDepAndEmp
}
return this.provide_allTreeDepAndEmp()
},
allFlatDepartments() {
if (this.getDataBySelf) {
return this.default_allFlatDepartments
}
return this.provide_allFlatDepartments()
},
allFlatEmployees() {
if (this.getDataBySelf) {
return this.default_allFlatEmployees
}
return this.provide_allFlatEmployees()
},
},
mounted() {
if (this.getDataBySelf) {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.default_allTreeDepAndEmp = res
})
// 获取全部员工和部门的平铺列表
getEmployeeList({ block_status: 0, page_size: 99999 }).then((res) => {
this.default_allFlatEmployees = res.data_list
})
getAllDepartmentList({ is_tree: 0 }).then((res) => {
this.default_allFlatDepartments = res
})
}
},
methods: {
setValues({ rightData }) {
this.rightData = rightData
},
getValues() {
const department = []
const user = []
this.rightData.forEach((item) => {
const _split = item.split('-')
if (_split[0] === 'department') {
department.push(Number(_split[1]))
} else {
user.push(Number(_split[1]))
}
})
const _idx = department.findIndex((item) => item === 0)
if (_idx > -1) {
department.splice(_idx, 1)
department.unshift(-1)
}
return {
department,
user,
}
},
changeInputValue(value) {
this.inputValue = value
},
handleRight() {
this.rightData = [...new Set([...this.treeValue, ...this.rightData])]
this.treeValue = []
this.selectedRight = []
},
handleLeft() {
this.selectedRight.forEach((id) => {
const _idx = this.rightData.findIndex((item) => item === id)
if (_idx > -1) {
this.rightData.splice(_idx, 1)
}
})
this.selectedRight = []
},
handleSelectedRight(id) {
const _idx = this.selectedRight.findIndex((item) => item === id)
if (_idx > -1) {
this.selectedRight.splice(_idx, 1)
} else {
this.selectedRight.push(id)
}
},
getLabel(id) {
const _split = id.split('-')
const type = _split[0]
const _id = Number(_split[1])
if (type === 'department') {
const _find = this.allFlatDepartments.find((item) => item[this.uniqueKey || 'department_id'] === _id)
return _find?.department_name ?? ''
} else {
const _find = this.allFlatEmployees.find((item) => item[this.uniqueKey || 'employee_id'] === _id)
return _find?.nickname ?? ''
}
},
},
}
</script>
<style lang="less">
@import '~@/style/static.less';
.employee-transfer {
width: 100%;
.vue-treeselect__multi-value-item-container {
display: none;
}
.vue-treeselect__menu {
border: none;
box-shadow: none;
margin-top: 10px;
background-color: #f9fbff;
margin-top: 0 !important;
}
}
.employee-transfer.vue-treeselect--open.vue-treeselect--open-below .vue-treeselect__control {
border-radius: 2px;
width: 90%;
margin-left: 5%;
border-color: rgba(0, 0, 0, 0.1);
}
.employee-transfer.vue-treeselect--has-value {
.vue-treeselect-helper-hide {
display: block;
}
}
.employee-transfer.employee-transfer-has-input {
.vue-treeselect-helper-hide {
display: none;
}
}
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.employee-transfer {
display: flex;
justify-content: space-between;
& &-left,
& &-right {
width: 40%;
background-color: #f9fbff;
padding-top: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
height: var(--custom-height);
}
& &-right {
padding-top: 12px;
overflow: auto;
.employee-transfer-right-item {
cursor: pointer;
padding: 2px 12px;
margin: 2px 0;
}
.employee-transfer-right-selected {
background-color: #f0f5ff;
}
}
& &-operation {
width: 10%;
height: var(--custom-height);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.operation-left,
.operation-right {
width: 20px;
height: 20px;
border-radius: 2px;
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
background-color: #custom_colors[color_1];
color: #fff;
}
}
}
}
</style>

View File

@@ -32,7 +32,7 @@ export default {
},
methods: {
handleToHome () {
this.$router.push({ name: 'cmdb_preference' })
this.$router.push('/')
}
}
}

View File

@@ -1,30 +0,0 @@
<template>
<div :class="prefixCls">
<div style="float: left">
<slot name="extra">{{ extra }}</slot>
</div>
<div style="float: right">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'FooterToolBar',
props: {
prefixCls: {
type: String,
default: 'ant-pro-footer-toolbar'
},
extra: {
type: [String, Object],
default: ''
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -1,4 +0,0 @@
import FooterToolBar from './FooterToolBar'
import './index.less'
export default FooterToolBar

View File

@@ -1,23 +0,0 @@
@import "../index";
@footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar";
.@{footer-toolbar-prefix-cls} {
position: fixed;
width: 100%;
bottom: 0;
right: 0;
height: 56px;
line-height: 56px;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
background: #fff;
border-top: 1px solid #e8e8e8;
padding: 0 24px;
z-index: 9;
&:after {
content: "";
display: block;
clear: both;
}
}

View File

@@ -1,48 +0,0 @@
# FooterToolbar 底部工具栏
固定在底部的工具栏
## 何时使用
固定在内容区域的底部不随滚动条移动常用于长页面的数据搜集和提交工作
引用方式
```javascript
import FooterToolBar from '@/components/FooterToolbar'
export default {
components: {
FooterToolBar
}
}
```
## 代码演示
```html
<footer-tool-bar>
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
```html
<footer-tool-bar extra="扩展信息提示">
<a-button type="primary" @click="validate" :loading="loading">提交</a-button>
</footer-tool-bar>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
children (slot) | 工具栏内容向右对齐 | - | -
extra | 额外信息向左对齐 | String, Object | -

View File

@@ -1,6 +1,22 @@
<template>
<div class="footer">
<div class="links">
<a
href="https://veops.cn/"
target="_blank"
>维易科技</a>
<a
href="https://github.com/sendya/ant-design-pro-vue"
target="_blank"
>
<a-icon type="github" />
</a>
</div>
<div class="copyright">
Copyright
<a-icon type="copyright" /> 2021-2023 <span>@维易科技</span>
</div>
</div>
</template>

View File

@@ -1,21 +1,29 @@
<template>
<transition name="showHeader">
<div v-if="visible" class="header-animat">
<div class="header-animat">
<a-layout-header
v-if="visible"
:class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', ]"
:style="{ padding: '0' }">
:class="[
fixedHeader && 'ant-header-fixedHeader',
sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed',
]"
:style="{ padding: '0' }"
>
<div v-if="mode === 'sidemenu'" class="header">
<a-icon v-if="device==='mobile'" class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle"/>
<a-icon v-else class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggle"/>
<a-icon
v-if="device === 'mobile'"
class="trigger"
:type="collapsed ? 'menu-fold' : 'menu-unfold'"
@click="toggle"
/>
<a-icon v-else class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggle" />
<top-menu></top-menu>
<user-menu></user-menu>
</div>
<div v-else :class="['top-nav-header-index', theme]">
<div class="header-index-wide">
<div class="header-index-left">
<logo class="top-nav-header" :show-title="device !== 'mobile'"/>
<s-menu v-if="device !== 'mobile'" mode="horizontal" :menu="menus" :theme="theme" :i18n-render="i18nRender" />
<logo class="top-nav-header" :show-title="device !== 'mobile'" :collapsed="collapsed" />
<s-menu v-if="device !== 'mobile'" mode="horizontal" :menu="menus" :theme="theme" />
<a-icon v-else class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle" />
</div>
<top-menu></top-menu>
@@ -33,7 +41,6 @@ import TopMenu from '../tools/TopMenu'
import SMenu from '../Menu/'
import Logo from '../tools/Logo'
import { mixin } from '@/utils/mixin'
import { i18nRender } from '@/locales'
export default {
name: 'GlobalHeader',
@@ -41,47 +48,46 @@ export default {
UserMenu,
TopMenu,
SMenu,
Logo
Logo,
},
mixins: [mixin],
props: {
mode: {
type: String,
// sidemenu, topmenu
default: 'sidemenu'
default: 'sidemenu',
},
menus: {
type: Array,
required: true
required: true,
},
theme: {
type: String,
required: false,
default: 'dark'
default: 'dark',
},
collapsed: {
type: Boolean,
required: false,
default: false
default: false,
},
device: {
type: String,
required: false,
default: 'desktop'
}
default: 'desktop',
},
},
data () {
data() {
return {
visible: true,
oldScrollTop: 0
oldScrollTop: 0,
}
},
mounted () {
computed: {},
mounted() {
document.addEventListener('scroll', this.handleScroll, { passive: true })
},
methods: {
i18nRender,
handleScroll () {
handleScroll() {
if (!this.autoHideHeader) {
return
}
@@ -90,32 +96,32 @@ export default {
if (!this.ticking) {
this.ticking = true
requestAnimationFrame(() => {
if (this.oldScrollTop > scrollTop) {
this.visible = true
} else if (scrollTop > 300 && this.visible) {
this.visible = false
} else if (scrollTop < 300 && !this.visible) {
this.visible = true
}
// if (this.oldScrollTop > scrollTop) {
// // this.visible = true
// } else if (scrollTop > 300 && this.visible) {
// // this.visible = false
// } else if (scrollTop < 300 && !this.visible) {
// // this.visible = true
// }
this.oldScrollTop = scrollTop
this.ticking = false
})
}
},
toggle () {
toggle() {
this.$emit('toggle')
}
},
},
beforeDestroy () {
beforeDestroy() {
document.body.removeEventListener('scroll', this.handleScroll, true)
}
},
}
</script>
<style lang="less">
@import '../index.less';
.header-animat{
.header-animat {
position: relative;
z-index: @ant-global-header-zindex;
}
@@ -125,7 +131,8 @@ export default {
.showHeader-leave-active {
transition: all 0.5s ease;
}
.showHeader-enter, .showHeader-leave-to {
.showHeader-enter,
.showHeader-leave-to {
opacity: 0;
}
</style>

View File

@@ -1,86 +0,0 @@
<template>
<div :class="prefixCls">
<a-tabs v-model="currentTab" @change="handleTabChange">
<a-tab-pane v-for="v in icons" :tab="v.title" :key="v.key">
<ul>
<li v-for="(icon, key) in v.icons" :key="`${v.key}-${key}`" :class="{ 'active': selectedIcon==icon }" @click="handleSelectedIcon(icon)" >
<a-icon :type="icon" :style="{ fontSize: '36px' }" />
</li>
</ul>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import icons from './icons'
export default {
name: 'IconSelect',
props: {
prefixCls: {
type: String,
default: 'ant-pro-icon-selector'
},
// eslint-disable-next-line
value: {
type: String
}
},
data () {
return {
selectedIcon: this.value || '',
currentTab: 'directional',
icons
}
},
watch: {
value (val) {
this.selectedIcon = val
this.autoSwitchTab()
}
},
created () {
if (this.value) {
this.autoSwitchTab()
}
},
methods: {
handleSelectedIcon (icon) {
this.selectedIcon = icon
this.$emit('change', icon)
},
handleTabChange (activeKey) {
this.currentTab = activeKey
},
autoSwitchTab () {
icons.some(item => item.icons.some(icon => icon === this.value) && (this.currentTab = item.key))
}
}
}
</script>
<style lang="less" scoped>
@import "../index.less";
ul{
list-style: none;
padding: 0;
overflow-y: scroll;
height: 250px;
li{
display: inline-block;
padding: @padding-sm;
margin: 3px 0;
border-radius: @border-radius-base;
&:hover, &.active{
// box-shadow: 0px 0px 5px 2px @primary-color;
cursor: pointer;
color: @white;
background-color: @primary-color;
}
}
}
</style>

View File

@@ -1,48 +0,0 @@
IconSelector
====
> 图标选择组件常用于为某一个数据设定一个图标时使用
> eg: 设定菜单列表时为每个菜单设定一个图标
该组件由 [@Saraka](https://github.com/saraka-tsukai) 封装
### 使用方式
```vue
<template>
<div>
<icon-selector @change="handleIconChange"/>
</div>
</template>
<script>
import IconSelector from '@/components/IconSelector'
export default {
name: 'YourView',
components: {
IconSelector
},
data () {
return {
}
},
methods: {
handleIconChange (icon) {
console.log('change Icon', icon)
}
}
}
</script>
```
### 事件
| 名称 | 说明 | 类型 | 默认值 |
| ------ | -------------------------- | ------ | ------ |
| change | 当改变了 `icon` 选中项触发 | String | - |

View File

@@ -1,36 +0,0 @@
/**
* 增加新的图标时请遵循以下数据结构
* Adding new icon please follow the data structure below
*/
export default [
{
key: 'directional',
title: '方向性图标',
icons: ['step-backward', 'step-forward', 'fast-backward', 'fast-forward', 'shrink', 'arrows-alt', 'down', 'up', 'left', 'right', 'caret-up', 'caret-down', 'caret-left', 'caret-right', 'up-circle', 'down-circle', 'left-circle', 'right-circle', 'double-right', 'double-left', 'vertical-left', 'vertical-right', 'forward', 'backward', 'rollback', 'enter', 'retweet', 'swap', 'swap-left', 'swap-right', 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'play-circle', 'up-square', 'down-square', 'left-square', 'right-square', 'login', 'logout', 'menu-fold', 'menu-unfold', 'border-bottom', 'border-horizontal', 'border-inner', 'border-left', 'border-right', 'border-top', 'border-verticle', 'pic-center', 'pic-left', 'pic-right', 'radius-bottomleft', 'radius-bottomright', 'radius-upleft', 'fullscreen', 'fullscreen-exit']
},
{
key: 'suggested',
title: '提示建议性图标',
icons: ['question', 'question-circle', 'plus', 'plus-circle', 'pause', 'pause-circle', 'minus', 'minus-circle', 'plus-square', 'minus-square', 'info', 'info-circle', 'exclamation', 'exclamation-circle', 'close', 'close-circle', 'close-square', 'check', 'check-circle', 'check-square', 'clock-circle', 'warning', 'issues-close', 'stop']
},
{
key: 'editor',
title: '编辑类图标',
icons: ['edit', 'form', 'copy', 'scissor', 'delete', 'snippets', 'diff', 'highlight', 'align-center', 'align-left', 'align-right', 'bg-colors', 'bold', 'italic', 'underline', 'strikethrough', 'redo', 'undo', 'zoom-in', 'zoom-out', 'font-colors', 'font-size', 'line-height', 'colum-height', 'dash', 'small-dash', 'sort-ascending', 'sort-descending', 'drag', 'ordered-list', 'radius-setting']
},
{
key: 'data',
title: '数据类图标',
icons: ['area-chart', 'pie-chart', 'bar-chart', 'dot-chart', 'line-chart', 'radar-chart', 'heat-map', 'fall', 'rise', 'stock', 'box-plot', 'fund', 'sliders']
},
{
key: 'brand_logo',
title: '网站通用图标',
icons: ['lock', 'unlock', 'bars', 'book', 'calendar', 'cloud', 'cloud-download', 'code', 'copy', 'credit-card', 'delete', 'desktop', 'download', 'ellipsis', 'file', 'file-text', 'file-unknown', 'file-pdf', 'file-word', 'file-excel', 'file-jpg', 'file-ppt', 'file-markdown', 'file-add', 'folder', 'folder-open', 'folder-add', 'hdd', 'frown', 'meh', 'smile', 'inbox', 'laptop', 'appstore', 'link', 'mail', 'mobile', 'notification', 'paper-clip', 'picture', 'poweroff', 'reload', 'search', 'setting', 'share-alt', 'shopping-cart', 'tablet', 'tag', 'tags', 'to-top', 'upload', 'user', 'video-camera', 'home', 'loading', 'loading-3-quarters', 'cloud-upload', 'star', 'heart', 'environment', 'eye', 'camera', 'save', 'team', 'solution', 'phone', 'filter', 'exception', 'export', 'customer-service', 'qrcode', 'scan', 'like', 'dislike', 'message', 'pay-circle', 'calculator', 'pushpin', 'bulb', 'select', 'switcher', 'rocket', 'bell', 'disconnect', 'database', 'compass', 'barcode', 'hourglass', 'key', 'flag', 'layout', 'printer', 'sound', 'usb', 'skin', 'tool', 'sync', 'wifi', 'car', 'schedule', 'user-add', 'user-delete', 'usergroup-add', 'usergroup-delete', 'man', 'woman', 'shop', 'gift', 'idcard', 'medicine-box', 'red-envelope', 'coffee', 'copyright', 'trademark', 'safety', 'wallet', 'bank', 'trophy', 'contacts', 'global', 'shake', 'api', 'fork', 'dashboard', 'table', 'profile', 'alert', 'audit', 'branches', 'build', 'border', 'crown', 'experiment', 'fire', 'money-collect', 'property-safety', 'read', 'reconciliation', 'rest', 'security-scan', 'insurance', 'interation', 'safety-certificate', 'project', 'thunderbolt', 'block', 'cluster', 'deployment-unit', 'dollar', 'euro', 'pound', 'file-done', 'file-exclamation', 'file-protect', 'file-search', 'file-sync', 'gateway', 'gold', 'robot', 'shopping']
},
{
key: 'application',
title: '品牌和标识',
icons: ['android', 'apple', 'windows', 'ie', 'chrome', 'github', 'aliwangwang', 'dingding', 'weibo-square', 'weibo-circle', 'taobao-circle', 'html5', 'weibo', 'twitter', 'wechat', 'youtube', 'alipay-circle', 'taobao', 'skype', 'qq', 'medium-workmark', 'gitlab', 'medium', 'linkedin', 'google-plus', 'dropbox', 'facebook', 'codepen', 'code-sandbox', 'amazon', 'google', 'codepen-circle', 'alipay', 'ant-design', 'aliyun', 'zhihu', 'slack', 'slack-square', 'behance', 'behance-square', 'dribbble', 'dribbble-square', 'instagram', 'yuque', 'alibaba', 'yahoo']
}
]

View File

@@ -1,2 +0,0 @@
import IconSelector from './IconSelector'
export default IconSelector

View File

@@ -1,27 +1,26 @@
<template>
<a-layout-sider
:class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null ]"
width="256px"
:class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null]"
width="200px"
:collapsible="collapsible"
v-model="collapsed"
:trigger="null">
<logo />
:trigger="null"
>
<logo :collapsed="collapsed" />
<s-menu
:collapsed="collapsed"
:menu="menus"
:theme="theme"
:mode="mode"
:i18n-render="i18nRender"
@select="onSelect"
style="padding: 16px 0px;"></s-menu>
style="padding: 16px 0px;"
></s-menu>
</a-layout-sider>
</template>
<script>
import Logo from '@/components/tools/Logo'
import SMenu from './index'
import { i18nRender } from '@/locales'
import { mixin, mixinDevice } from '@/utils/mixin'
export default {
@@ -32,35 +31,33 @@ export default {
mode: {
type: String,
required: false,
default: 'inline'
default: 'inline',
},
theme: {
type: String,
required: false,
default: 'dark'
default: 'dark',
},
collapsible: {
type: Boolean,
required: false,
default: false
default: false,
},
collapsed: {
type: Boolean,
required: false,
default: false
default: false,
},
menus: {
type: Array,
required: true
}
required: true,
},
},
methods: {
i18nRender,
onSelect (obj) {
onSelect(obj) {
this.$emit('menuSelect', obj)
}
},
},
watch: {
}
watch: {},
}
</script>

View File

@@ -1,104 +1,46 @@
import { Menu, Icon } from 'ant-design-vue'
import router, { resetRouter } from '@/router'
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
import store from '@/store'
import {
subscribeCIType,
subscribeTreeView,
} from '@/modules/cmdb/api/preference'
import { searchResourceType } from '@/modules/acl/api/resource'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
const menuProps = {
menu: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
mode: {
type: String,
required: false,
default: 'inline'
},
collapsed: {
type: Boolean,
required: false,
default: false
},
i18nRender: {
type: Function,
required: false
}
}
const { Item, SubMenu } = Menu
const defaultI18nRender = (key) => `${key}`
// render
const renderItem = (h, menu, i18nRender) => {
if (!menu.hidden) {
// const localeKey = `menu.${menu.name}`
// const localeKey = menu.meta && menu.meta.title
// i18nRender(localeKey)
return menu.children && !menu.hideChildrenInMenu ? renderSubMenu(h, menu, i18nRender) : renderMenuItem(h, menu, i18nRender)
}
return null
}
const renderMenuItem = (h, menu, i18nRender) => {
const target = menu.meta.target || null
const CustomTag = target && 'a' || 'router-link'
const props = { to: { name: menu.name } }
const attrs = { href: menu.path, target: menu.meta.target }
if (menu.children && menu.hideChildrenInMenu) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时 selectedKeys 做控制用
menu.children.forEach(item => {
item.meta = Object.assign(item.meta, { hidden: true })
})
}
return (
<Menu.Item {...{ key: menu.path }}>
<CustomTag {...{ props, attrs }}>
{renderIcon(h, menu.meta.icon)}
<span>{i18nRender(menu.meta.title)}</span>
</CustomTag>
</Menu.Item>
)
}
const renderSubMenu = (h, menu, i18nRender) => {
const itemArr = []
if (!menu.hideChildrenInMenu) {
menu.children.forEach(item => itemArr.push(renderItem(h, item, i18nRender)))
}
return (
<Menu.SubMenu {...{ key: menu.path }}>
<span slot="title">
{renderIcon(h, menu.meta.icon)}
<span>{i18nRender(menu.meta.title)}</span>
</span>
{itemArr}
</Menu.SubMenu>
)
}
const renderIcon = (h, icon) => {
if (icon === 'none' || icon === undefined) {
return null
}
const props = {}
typeof (icon) === 'object' ? props.component = icon : props.type = icon
return (
<Icon {...{ props }}/>
)
}
const SMenu = {
export default {
name: 'SMenu',
props: menuProps,
data () {
props: {
menu: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
mode: {
type: String,
required: false,
default: 'inline'
},
collapsed: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: []
cachedOpenKeys: [],
resource_type: {}
}
},
computed: {
@@ -106,27 +48,73 @@ const SMenu = {
const keys = []
vm.menu.forEach(item => keys.push(item.path))
return keys
},
},
provide() {
return {
resource_type: () => {
return this.resource_type
},
}
},
created () {
this.$watch('collapsed', collapsed => {
if (collapsed) {
created() {
},
mounted() {
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then(res => {
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
})
this.updateMenu()
},
watch: {
collapsed(val) {
if (val) {
this.cachedOpenKeys = this.openKeys.concat()
this.openKeys = []
} else {
this.openKeys = this.cachedOpenKeys
}
})
this.$watch('$route', () => {
},
$route: function () {
this.updateMenu()
})
},
mounted () {
this.updateMenu()
},
},
inject: ['reload'],
methods: {
// 取消订阅
cancelAttributes(e, menu) {
const that = this
e.preventDefault()
e.stopPropagation()
this.$confirm({
title: '警告',
content: `确认取消订阅 ${menu.meta.title}?`,
onOk() {
const citypeId = menu.meta.typeId
const unsubCIType = subscribeCIType(citypeId, '')
const unsubTree = subscribeTreeView(citypeId, '')
Promise.all([unsubCIType, unsubTree]).then(() => {
that.$message.success('取消订阅成功')
// 删除路由
const href = window.location.href
const hrefSplit = href.split('/')
if (Number(hrefSplit[hrefSplit.length - 1]) === Number(citypeId)) {
that.$router.push('/cmdb/preference')
}
const roles = store.getters.roles
resetRouter()
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
router.addRoutes(store.getters.appRoutes)
})
if (hrefSplit[hrefSplit.length - 1] === 'preference') {
that.reload()
}
})
},
})
},
// select menu item
onOpenChange (openKeys) {
onOpenChange(openKeys) {
// 在水平模式下时执行并且不再执行后续
if (this.mode === 'horizontal') {
this.openKeys = openKeys
@@ -140,8 +128,9 @@ const SMenu = {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
},
updateMenu () {
updateMenu() {
const routes = this.$route.matched.concat()
const { hidden } = this.$route.meta
if (routes.length >= 3 && hidden) {
routes.pop()
@@ -157,10 +146,145 @@ const SMenu = {
}
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
},
// render
renderItem(menu) {
if (this.collapsed && menu.meta.disabled) {
return null
}
if (!menu.hidden) {
return menu.children && !menu.hideChildrenInMenu ? this.renderSubMenu(menu) : this.renderMenuItem(menu)
}
return null
},
renderMenuItem(menu) {
const isShowDot = menu.path.substr(0, 22) === '/cmdb/instances/types/'
const isShowGrant = menu.path.substr(0, 20) === '/cmdb/relationviews/'
const target = menu.meta.target || null
const tag = target && 'a' || 'router-link'
const props = { to: { name: menu.name } }
const attrs = { href: menu.meta.targetHref || menu.path, target: menu.meta.target }
if (menu.children && menu.hideChildrenInMenu) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时 selectedKeys 做控制用
menu.children.forEach(item => {
item.meta = Object.assign(item.meta, { hidden: true })
})
}
return (
<Item {...{ key: menu.path }} disabled={menu.meta.disabled || false}>
<tag {...{ props, attrs }}>
{this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })}
<span>
<span class={menu.meta.title.length > 10 ? 'scroll' : ''}>{menu.meta.title}</span>
{isShowDot &&
<a-popover
overlayClassName="custom-menu-extra-submenu"
placement="rightTop"
arrowPointAtCenter
autoAdjustOverflow={false}
getPopupContainer={(trigger) => trigger}
content={() =>
<div>
<div onClick={e => this.handlePerm(e, menu, 'CIType')} class="custom-menu-extra-submenu-item"><a-icon type="user-add" />授权</div>
<div onClick={e => this.cancelAttributes(e, menu)} class="custom-menu-extra-submenu-item"><a-icon type="star" />取消订阅</div>
</div>}
>
<a-icon type="menu" ref="extraEllipsis" class="custom-menu-extra-ellipsis"></a-icon>
</a-popover>
}
{isShowGrant && <a-icon class="custom-menu-extra-ellipsis" onClick={e => this.handlePerm(e, menu, 'RelationView')} type="user-add" />}
</span>
</tag>
{isShowDot && <CMDBGrant ref="cmdbGrantCIType" resourceType="CIType" app_id="cmdb" />}
{isShowGrant && <CMDBGrant ref="cmdbGrantRelationView" resourceType="RelationView" app_id="cmdb" />}
</Item>
)
},
renderSubMenu(menu) {
const itemArr = []
if (!menu.hideChildrenInMenu) {
menu.children.forEach(item => itemArr.push(this.renderItem(item)))
}
return (
<SubMenu {...{ key: menu.path }}>
<span slot="title">
{this.renderIcon({ icon: menu.meta.icon, selectedIcon: menu.meta.selectedIcon, routeName: menu.name })}
<span>{menu.meta.title}</span>
</span>
{itemArr}
</SubMenu>
)
},
renderIcon({ icon, selectedIcon, customIcon = undefined, name = undefined, typeId = undefined, routeName }) {
if (typeId) {
if (customIcon) {
return <ops-icon
style={{
color: customIcon.split('$$')[1],
}}
type={customIcon.split('$$')[0]}
/>
}
return <span
style={{
display: 'inline-block',
width: '14px',
height: '14px',
borderRadius: '50%',
backgroundColor: '#d3d3d3',
color: '#fff',
textAlign: 'center',
lineHeight: '14px',
fontSize: '10px',
marginRight: '10px'
}}
>{name[0].toUpperCase()}</span>
}
if (icon === 'none' || icon === undefined) {
return null
}
const props = {}
if (this.$route.name === routeName && selectedIcon) {
return <ops-icon type={selectedIcon}></ops-icon>
} else if (icon.startsWith('ops-') || icon.startsWith('icon-xianxing') || icon.startsWith('icon-shidi')) {
return <ops-icon type={icon}></ops-icon>
} else {
typeof (icon) === 'object' ? props.component = icon : props.type = icon
return (
<Icon {... { props }} />
)
}
},
handlePerm(e, menu, resource_type_name) {
e.stopPropagation()
e.preventDefault()
roleHasPermissionToGrant({
app_id: 'cmdb',
resource_type_name,
perm: 'grant',
resource_name: menu.meta.name,
}).then(res => {
if (res.result) {
console.log(menu)
if (resource_type_name === 'CIType') {
this.$refs.cmdbGrantCIType.open({ name: menu.meta.name, cmdbGrantType: 'ci', CITypeId: menu.meta?.typeId })
} else {
this.$refs.cmdbGrantRelationView.open({ name: menu.meta.name, cmdbGrantType: 'relation_view' })
}
} else {
this.$message.error('权限不足!')
}
})
}
},
render (h) {
const { mode, theme, menu, i18nRender = defaultI18nRender } = this
render() {
const { mode, theme, menu } = this
const props = {
mode: mode,
theme: theme,
@@ -168,7 +292,7 @@ const SMenu = {
}
const on = {
select: obj => {
this.selectedKeys = obj.selectedKeys
// this.selectedKeys = obj.selectedKeys
this.$emit('select', obj)
},
openChange: this.onOpenChange
@@ -178,15 +302,13 @@ const SMenu = {
if (item.hidden) {
return null
}
return renderItem(h, item, i18nRender)
return this.renderItem(item)
})
// {...{ props, on: on }}
return (
<Menu vModel={this.selectedKeys} {...{ props, on: on }}>
<Menu class="ops-side-bar" selectedKeys={this.selectedKeys} {...{ props, on: on }}>
{menuTree}
</Menu>
)
}
}
export default SMenu

View File

@@ -1,156 +0,0 @@
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
const { Item, SubMenu } = Menu
export default {
name: 'SMenu',
props: {
menu: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
mode: {
type: String,
required: false,
default: 'inline'
},
collapsed: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: []
}
},
computed: {
rootSubmenuKeys: vm => {
const keys = []
vm.menu.forEach(item => keys.push(item.path))
return keys
}
},
created () {
this.updateMenu()
},
watch: {
collapsed (val) {
if (val) {
this.cachedOpenKeys = this.openKeys.concat()
this.openKeys = []
} else {
this.openKeys = this.cachedOpenKeys
}
},
$route: function () {
this.updateMenu()
}
},
methods: {
renderIcon: function (h, icon) {
if (icon === 'none' || icon === undefined) {
return null
}
const props = {}
typeof (icon) === 'object' ? props.component = icon : props.type = icon
return h(Icon, { props: { ...props } })
},
renderMenuItem: function (h, menu, pIndex, index) {
const target = menu.meta.target || null
return h(Item, { key: menu.path ? menu.path : 'item_' + pIndex + '_' + index }, [
h('router-link', { attrs: { to: { name: menu.name }, target: target } }, [
this.renderIcon(h, menu.meta.icon),
h('span', [menu.meta.title])
])
])
},
renderSubMenu: function (h, menu, pIndex, index) {
const this2_ = this
const subItem = [h('span', { slot: 'title' }, [this.renderIcon(h, menu.meta.icon), h('span', [menu.meta.title])])]
const itemArr = []
const pIndex_ = pIndex + '_' + index
console.log('menu', menu)
if (!menu.hideChildrenInMenu) {
menu.children.forEach(function (item, i) {
itemArr.push(this2_.renderItem(h, item, pIndex_, i))
})
}
return h(SubMenu, { key: menu.path ? menu.path : 'submenu_' + pIndex + '_' + index }, subItem.concat(itemArr))
},
renderItem: function (h, menu, pIndex, index) {
if (!menu.hidden) {
return menu.children && !menu.hideChildrenInMenu
? this.renderSubMenu(h, menu, pIndex, index)
: this.renderMenuItem(h, menu, pIndex, index)
}
},
renderMenu: function (h, menuTree) {
const this2_ = this
const menuArr = []
menuTree.forEach(function (menu, i) {
if (!menu.hidden) {
menuArr.push(this2_.renderItem(h, menu, '0', i))
}
})
return menuArr
},
onOpenChange (openKeys) {
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
this.openKeys = openKeys
} else {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
},
updateMenu () {
const routes = this.$route.matched.concat()
if (routes.length >= 4 && this.$route.meta.hidden) {
routes.pop()
this.selectedKeys = [routes[2].path]
} else {
this.selectedKeys = [routes.pop().path]
}
const openKeys = []
if (this.mode === 'inline') {
routes.forEach(item => {
openKeys.push(item.path)
})
}
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
}
},
render (h) {
return h(
Menu,
{
props: {
theme: this.$props.theme,
mode: this.$props.mode,
openKeys: this.openKeys,
selectedKeys: this.selectedKeys
},
on: {
openChange: this.onOpenChange,
select: obj => {
this.selectedKeys = obj.selectedKeys
this.$emit('select', obj)
}
}
},
this.renderMenu(h, this.menu)
)
}
}

View File

@@ -0,0 +1,2 @@
import MonitorNodeSetting from './index.vue'
export default MonitorNodeSetting

View File

@@ -0,0 +1,168 @@
<template>
<div>
<a-form-item v-for="(node, index) in nodes" :key="node.id">
<a-row :gutter="24">
<a-col :span="6" :offset="1">
<a-form-item :label="index ? '' : 'ip地址'">
<a-input
allowClear
size="small"
v-decorator="[
`node_ip_${node.id}`,
{
rules: [
{ required: false, message: '请输入ip地址' },
{
pattern:
'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$',
message: 'ip地址格式错误',
trigger: 'blur',
},
],
},
]"
placeholder="请输入ip地址"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :label="index ? '' : 'community'" colon>
<a-input
allowClear
size="small"
v-decorator="[
`node_community_${node.id}`,
{
rules: [{ required: false, message: '请输入community' }],
},
]"
placeholder="请输入community"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :label="index ? '' : '版本'" colon>
<a-select
size="small"
v-decorator="[
`node_version_${node.id}`,
{
rules: [{ required: false, message: '请输入版本' }],
},
]"
placeholder="请选择版本"
>
<a-select-option value="1">
v1
</a-select-option>
<a-select-option value="2c">
v2c
</a-select-option>
<a-select-option value="3">
v3
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="3">
<a-form-item :label="index ? '' : ' '" :colon="false">
<a @click="() => removeNode(node.id, 1)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-form-item>
</a-col>
</a-row>
</a-form-item>
<a-form-item style="text-align: center">
<a-button type="dashed" style="width: 30%;" @click="addNode">
<a-icon type="plus" />
添加节点
</a-button>
</a-form-item>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'MonitorNodeSetting',
props: {
initNodes: {
type: Array,
default: () => [],
},
form: {
type: Object,
default: null,
},
},
data() {
return {
nodes: [],
}
},
methods: {
initNodesFunc() {
this.nodes = _.cloneDeep(this.initNodes)
},
addNode() {
const newNode = {
id: uuidv4(),
ip: '',
community: '',
version: '',
}
this.nodes.push(newNode)
this.$nextTick(() => {
this.form.setFieldsValue({
[`node_ip_${newNode.id}`]: newNode.ip,
[`node_community_${newNode.id}`]: newNode.community,
[`node_version_${newNode.id}`]: newNode.version,
})
})
},
removeNode(removeId, minLength) {
if (this.nodes.length <= minLength) {
this.$message.error('不可再删除!')
return
}
const _idx = this.nodes.findIndex((item) => item.id === removeId)
if (_idx > -1) {
this.nodes.splice(_idx, 1)
}
},
getInfoValuesFromForm(values) {
return this.nodes.map((item) => {
return {
id: item.id,
ip: values[`node_ip_${item.id}`],
community: values[`node_community_${item.id}`],
version: values[`node_version_${item.id}`],
}
})
},
setNodeField() {
if (this.nodes && this.nodes.length) {
this.nodes.forEach((item) => {
this.form.setFieldsValue({
[`node_ip_${item.id}`]: item.ip,
[`node_community_${item.id}`]: item.community,
[`node_version_${item.id}`]: item.version,
})
})
}
},
getNodeValue() {
const values = this.form.getFieldsValue()
return this.getInfoValuesFromForm(values)
},
},
}
</script>
<style></style>

View File

@@ -1,90 +0,0 @@
<template>
<a-popover
v-model="visible"
trigger="click"
placement="bottomRight"
overlayClassName="header-notice-wrapper"
:getPopupContainer="() => $refs.noticeRef.parentElement"
:autoAdjustOverflow="true"
:arrowPointAtCenter="true"
:overlayStyle="{ width: '300px', top: '50px' }"
>
<template slot="content">
<a-spin :spinning="loadding">
<a-tabs>
<a-tab-pane tab="通知" key="1">
<a-list>
<a-list-item>
<a-list-item-meta title="你收到了 14 份新周报" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane tab="消息" key="2">
123
</a-tab-pane>
<a-tab-pane tab="待办" key="3">
123
</a-tab-pane>
</a-tabs>
</a-spin>
</template>
<span @click="fetchNotice" class="header-notice" ref="noticeRef">
<a-badge count="12">
<a-icon style="font-size: 16px; padding: 4px" type="bell" />
</a-badge>
</span>
</a-popover>
</template>
<script>
export default {
name: 'HeaderNotice',
data () {
return {
loadding: false,
visible: false
}
},
methods: {
fetchNotice () {
if (!this.visible) {
this.loadding = true
setTimeout(() => {
this.loadding = false
}, 2000)
} else {
this.loadding = false
}
this.visible = !this.visible
}
}
}
</script>
<style lang="css">
.header-notice-wrapper {
top: 50px !important;
}
</style>
<style lang="less" scoped>
.header-notice{
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
}
</style>

View File

@@ -1,2 +0,0 @@
import NoticeIcon from './NoticeIcon'
export default NoticeIcon

View File

@@ -1,54 +0,0 @@
<template>
<div :class="[prefixCls]">
<slot name="subtitle">
<div :class="[`${prefixCls}-subtitle`]">{{ typeof subTitle === 'string' ? subTitle : subTitle() }}</div>
</slot>
<div class="number-info-value">
<span>{{ total }}</span>
<span class="sub-total">
{{ subTotal }}
<icon :type="`caret-${status}`" />
</span>
</div>
</div>
</template>
<script>
import Icon from 'ant-design-vue/es/icon'
export default {
name: 'NumberInfo',
props: {
prefixCls: {
type: String,
default: 'ant-pro-number-info'
},
total: {
type: Number,
required: true
},
subTotal: {
type: Number,
required: true
},
subTitle: {
type: [String, Function],
default: ''
},
status: {
type: String,
default: 'up'
}
},
components: {
Icon
},
data () {
return {}
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>

View File

@@ -1,3 +0,0 @@
import NumberInfo from './NumberInfo'
export default NumberInfo

View File

@@ -1,55 +0,0 @@
@import "../index";
@numberInfo-prefix-cls: ~"@{ant-pro-prefix}-number-info";
.@{numberInfo-prefix-cls} {
.ant-pro-number-info-subtitle {
color: @text-color-secondary;
font-size: @font-size-base;
height: 22px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.number-info-value {
margin-top: 4px;
font-size: 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
& > span {
color: @heading-color;
display: inline-block;
line-height: 32px;
height: 32px;
font-size: 24px;
margin-right: 32px;
}
.sub-total {
color: @text-color-secondary;
font-size: @font-size-lg;
vertical-align: top;
margin-right: 0;
i {
font-size: 12px;
transform: scale(0.82);
margin-left: 4px;
}
:global {
.anticon-caret-up {
color: @red-6;
}
.anticon-caret-down {
color: @green-6;
}
}
}
}
}

View File

@@ -1,43 +0,0 @@
# NumberInfo 数据文本
常用在数据卡片中用于突出展示某个业务数据
引用方式
```javascript
import NumberInfo from '@/components/NumberInfo'
export default {
components: {
NumberInfo
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<number-info
:sub-title="() => { return 'Visits this week' }"
:total="12321"
status="up"
:sub-total="17.1"></number-info>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
title | 标题 | ReactNode\|string | -
subTitle | 子标题 | ReactNode\|string | -
total | 总量 | ReactNode\|string | -
subTotal | 子总量 | ReactNode\|string | -
status | 增加状态 | 'up \| down' | -
theme | 状态样式 | string | 'light'
gap | 设置数字和描述之间的间距像素| number | 8

View File

@@ -0,0 +1,2 @@
import OpsTable from './index.vue'
export default OpsTable

View File

@@ -0,0 +1,121 @@
<template>
<vxe-table v-bind="$attrs" v-on="new$listeners" ref="xTable">
<slot></slot>
<template #empty>
<slot name="empty">
<div>
<img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" />
<div>暂无数据</div>
</div>
</slot>
</template>
<template #loading>
<slot name="loading"></slot>
</template>
</vxe-table>
</template>
<script>
import _ from 'lodash'
// 该组件使用方法与vxe-table一致但调用它的方法时需先调用getVxetableRef()获取到vxe-table实体
export default {
name: 'OpsTable',
data() {
return {
// isShifting: false,
// lastIndex: -1,
lastSelected: [],
currentSelected: [],
}
},
computed: {
new$listeners() {
if (!Object.keys(this.$listeners).length) {
return this.$listeners
}
return Object.assign(this.$listeners, {
// 在这里覆盖原有的change事件
// 'checkbox-change': this.selectChangeEvent,
'checkbox-range-change': this.checkboxRangeChange,
'checkbox-range-start': this.checkboxRangeStart,
'checkbox-range-end': this.checkboxRangeEnd,
})
},
},
mounted() {
// window.onkeydown = (e) => {
// if (e.key === 'Shift') {
// this.isShifting = true
// }
// }
// window.onkeyup = (e) => {
// if (e.key === 'Shift') {
// this.isShifting = false
// this.lastIndex = -1
// }
// }
},
beforeDestroy() {
// window.onkeydown = ''
// window.onkeyup = ''
},
methods: {
getVxetableRef() {
return this.$refs.xTable
},
// selectChangeEvent(e) {
// const xTable = this.$refs.xTable
// const { lastIndex } = this
// const currentIndex = e.rowIndex
// const { tableData } = xTable.getTableData()
// if (lastIndex > -1 && this.isShifting) {
// let start = lastIndex
// let end = currentIndex
// if (lastIndex > currentIndex) {
// start = currentIndex
// end = lastIndex
// }
// const rangeData = tableData.slice(start, end + 1)
// xTable.setCheckboxRow(rangeData, true)
// }
// this.lastIndex = currentIndex
// this.$emit('checkbox-change', { ...e, records: xTable.getCheckboxRecords() })
// },
checkboxRangeStart(e) {
const xTable = this.$refs.xTable
const lastSelected = xTable.getCheckboxRecords()
const selectedReserve = xTable.getCheckboxReserveRecords()
this.lastSelected = [...lastSelected, ...selectedReserve]
this.$emit('checkbox-range-start', e)
},
checkboxRangeChange(e) {
const xTable = this.$refs.xTable
xTable.setCheckboxRow(this.lastSelected, true)
this.currentSelected = e.records
// this.lastSelected = [...new Set([...this.lastSelected, ...e.records])]
this.$emit('checkbox-range-change', {
...e,
records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()],
})
},
checkboxRangeEnd(e) {
const xTable = this.$refs.xTable
const isAllSelected = this.currentSelected.every((item) => {
const _idx = this.lastSelected.findIndex((ele) => _.isEqual(ele, item))
return _idx > -1
})
if (isAllSelected) {
xTable.setCheckboxRow(this.currentSelected, false)
}
this.currentSelected = []
this.lastSelected = []
this.$emit('checkbox-range-end', {
...e,
records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()],
})
},
},
}
</script>
<style lang="less"></style>

View File

@@ -1,11 +1,11 @@
<template>
<div class="page-header">
<div class="page-header-index-wide">
<s-breadcrumb :i18n-render="i18nRender" />
<s-breadcrumb v-if="isShowBreadcrumb"/>
<div class="detail">
<div class="main" v-if="!$route.meta.hiddenHeaderContent">
<div class="row">
<img v-if="logo" :src="logo" class="logo"/>
<img v-if="logo" :src="logo" class="logo" />
<h1 v-if="title" class="title">{{ title }}</h1>
<div class="action">
<slot name="action"></slot>
@@ -32,38 +32,37 @@
</template>
<script>
/* eslint-disable */
import Breadcrumb from '@/components/tools/Breadcrumb'
import { i18nRender } from '@/locales'
export default {
name: 'PageHeader',
components: {
's-breadcrumb': Breadcrumb
's-breadcrumb': Breadcrumb,
},
props: {
title: {
type: [String, Boolean],
default: true,
required: false
required: false,
},
logo: {
type: String,
default: '',
required: false
required: false,
},
avatar: {
type: String,
default: '',
required: false
}
required: false,
},
isShowBreadcrumb: {
type: Boolean,
default: true,
},
},
data () {
data() {
return {}
},
methods () {
i18nRender
}
}
</script>

View File

@@ -155,7 +155,7 @@
<a-alert type="warning" :style="{ marginTop: '24px' }">
<span slot="message">
配置栏只在开发环境用于预览生产环境不会展现请手动修改配置文件
<a href="https://github.com/sendya/ant-design-pro-vue/blob/master/src/config/defaultSettings.js" target="_blank">src/config/defaultSettings.js</a>
<a href="https://github.com/sendya/ant-design-pro-vue/blob/master/src/config/setting.js" target="_blank">src/config/setting.js</a>
</span>
</a-alert>
</div>
@@ -169,15 +169,13 @@
</template>
<script>
import { DetailList } from '@/components'
import SettingItem from './SettingItem'
import config from '@/config/defaultSettings'
import config from '@/config/setting'
import { updateTheme, updateColorWeak, colorList } from './settingConfig'
import { mixin, mixinDevice } from '@/utils/mixin'
export default {
components: {
DetailList,
SettingItem
},
mixins: [mixin, mixinDevice],

View File

@@ -1,5 +1,5 @@
import { message } from 'ant-design-vue/es'
// import defaultSettings from '../defaultSettings';
// import setting from '../setting';
import themeColor from './themeColor.js'
// let lessNodesAppended

View File

@@ -0,0 +1,125 @@
<template>
<div>
<div
:class="{
'sidebar-list-item': true,
'sidebar-list-item-dotline': dotLine,
'sidebar-list-item-selected': selected[`${value}`] === item[`${value}`],
}"
v-for="item in list"
:key="item[`${value}`]"
@click="
() => {
selected = item
$emit('clickItem', item)
}
"
>
<div class="sidebar-list-label" :title="item[`${label}`]">
<slot name="icon" :item="item"></slot>
<slot name="label" :item="item">{{ item[`${label}`] }}</slot>
</div>
<a-space class="sidebar-list-action"><slot name="action"> </slot></a-space>
</div>
</div>
</template>
<script>
export default {
name: 'SidebarList',
props: {
list: {
type: Array,
default: () => {},
},
value: {
type: String,
default: 'id',
},
label: {
type: String,
default: 'name',
},
dotLine: {
type: Boolean,
default: true,
},
},
data() {
return {
selected: {},
}
},
mounted() {},
methods: {
setSelected(item) {
this.selected = item
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.sidebar-list-item {
.ops_popover_item();
margin: 2px 0;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
.sidebar-list-label {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-list-action {
margin-left: auto;
display: none;
}
&:hover {
.sidebar-list-action {
display: inline-flex;
}
.sidebar-list-label {
width: calc(100% - 36px);
}
}
}
.sidebar-list-item-selected {
.ops_popover_item_selected();
background-color: transparent;
}
.sidebar-list-item.sidebar-list-item-selected::before {
background-color: #custom_colors[color_1];
}
.sidebar-list-item-dotline {
padding-left: 20px;
&::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 10px;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #cacaca;
z-index: 2;
}
}
.sidebar-list-item-dotline:not(:last-child)::after {
content: '';
width: 1px;
height: 31px;
position: absolute;
left: 12px;
background-color: #cacaca;
top: 15px;
z-index: 1;
}
</style>

View File

@@ -0,0 +1,2 @@
import SidebarList from './SidebarList'
export default SidebarList

View File

@@ -0,0 +1,179 @@
<template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot>
</div>
<div class="spliter-wrap">
<a-button
v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'"
class="collapse-btn"
@click="handleExpand"
></a-button>
<div
class="pane-trigger"
@mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div>
</div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'SplitPane',
props: {
direction: {
type: String,
default: 'row',
},
min: {
type: Number,
default: 10,
},
max: {
type: Number,
default: 90,
},
paneLengthPixel: {
type: Number,
default: 220,
},
triggerLength: {
type: Number,
default: 8,
},
appName: {
type: String,
default: 'viewer',
},
collapsable: {
type: Boolean,
default: false,
},
triggerColor: {
type: String,
default: '#f0f2f5',
},
},
data() {
return {
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
: false,
parentContainer: null,
}
},
computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},
minLengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight'
},
paneLengthValue1() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthValue2() {
const rest = 100 - this.paneLengthPercent
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthPercent() {
const clientRectWidth = this.parentContainer
? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100
},
},
watch: {
isExpanded(newValue) {
if (newValue) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
},
mounted() {
this.parentContainer = document.querySelector(`.${this.appName}`)
if (this.isExpanded) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
methods: {
// 按下滑动器
handleMouseDown(e) {
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp)
if (this.direction === 'row') {
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
}
},
// 按下滑动器后移动鼠标
handleMouseMove(e) {
this.isExpanded = false
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPixel = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
}
if (paneLengthPixel < this.min) {
paneLengthPixel = this.min
}
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
}
this.$emit('update:paneLengthPixel', paneLengthPixel)
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
},
// 松开滑动器
handleMouseUp() {
document.removeEventListener('mousemove', this.handleMouseMove)
},
handleExpand() {
this.isExpanded = !this.isExpanded
this.$emit('expand', this.isExpanded)
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
},
}
</script>
<style scoped lang="less">
@import './index.less';
</style>

View File

@@ -0,0 +1,2 @@
import SplitPane from './SplitPane'
export default SplitPane

View File

@@ -0,0 +1,48 @@
.split-pane {
height: 100%;
display: flex;
}
.split-pane .pane-two {
flex: 1;
}
.split-pane .pane-trigger {
user-select: none;
}
.split-pane.row .pane-one {
width: 20%;
height: 100%;
// overflow-y: auto;
}
.split-pane.column .pane {
width: 100%;
}
.split-pane.row .pane-trigger {
width: 8px;
height: 100%;
cursor: e-resize;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAPCAYAAADDNm69AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAeSURBVBhXY/4PBMzMzA379u1rANFMDGhgGAswMAAAn6EH6K9ktYAAAAAASUVORK5CYII=')
1px 50% no-repeat #f0f2f5;
}
.split-pane .collapse-btn {
width: 25px;
height: 70px;
position: absolute;
right: 8px;
top: calc(50% - 35px);
background-color: #f0f2f5;
border-color: transparent;
border-radius: 8px 0px 0px 8px;
.anticon {
color: #7cb0fe;
}
}
.split-pane .spliter-wrap {
position: relative;
}

View File

@@ -1,122 +0,0 @@
<template>
<div :class="[prefixCls, lastCls, blockCls, gridCls]">
<div v-if="title" class="antd-pro-components-standard-form-row-index-label">
<span>{{ title }}</span>
</div>
<div class="antd-pro-components-standard-form-row-index-content">
<slot></slot>
</div>
</div>
</template>
<script>
const classes = [
'antd-pro-components-standard-form-row-index-standardFormRowBlock',
'antd-pro-components-standard-form-row-index-standardFormRowGrid',
'antd-pro-components-standard-form-row-index-standardFormRowLast'
]
export default {
name: 'StandardFormRow',
props: {
prefixCls: {
type: String,
default: 'antd-pro-components-standard-form-row-index-standardFormRow'
},
title: {
type: String,
default: undefined
},
last: {
type: Boolean
},
block: {
type: Boolean
},
grid: {
type: Boolean
}
},
computed: {
lastCls () {
return this.last ? classes[2] : null
},
blockCls () {
return this.block ? classes[0] : null
},
gridCls () {
return this.grid ? classes[1] : null
}
}
}
</script>
<style lang="less" scoped>
@import '../index.less';
.antd-pro-components-standard-form-row-index-standardFormRow {
display: flex;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px dashed @border-color-split;
/deep/ .ant-form-item {
margin-right: 24px;
}
/deep/ .ant-form-item-label label {
margin-right: 0;
color: @text-color;
}
/deep/ .ant-form-item-label,
.ant-form-item-control {
padding: 0;
line-height: 32px;
}
.antd-pro-components-standard-form-row-index-label {
flex: 0 0 auto;
margin-right: 24px;
color: @heading-color;
font-size: @font-size-base;
text-align: right;
& > span {
display: inline-block;
height: 32px;
line-height: 32px;
&::after {
content: '';
}
}
}
.antd-pro-components-standard-form-row-index-content {
flex: 1 1 0;
/deep/ .ant-form-item:last-child {
margin-right: 0;
}
}
&.antd-pro-components-standard-form-row-index-standardFormRowLast {
margin-bottom: 0;
padding-bottom: 0;
border: none;
}
&.antd-pro-components-standard-form-row-index-standardFormRowBlock {
/deep/ .ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
}
&.antd-pro-components-standard-form-row-index-standardFormRowGrid {
/deep/ .ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
/deep/ .ant-form-item-label {
float: left;
}
}
}
</style>

View File

@@ -1,3 +0,0 @@
import StandardFormRow from './StandardFormRow'
export default StandardFormRow

View File

@@ -1,328 +0,0 @@
import T from 'ant-design-vue/es/table/Table'
import get from 'lodash.get'
import i18n from '@/locales'
export default {
data () {
return {
needTotalList: [],
selectedRows: [],
selectedRowKeys: [],
localLoading: false,
localDataSource: [],
localPagination: Object.assign({}, this.pagination)
}
},
props: Object.assign({}, T.props, {
rowKey: {
type: [String, Function],
default: 'key'
},
loaded: {
type: Boolean,
default: true
},
data: {
type: Function,
required: true
},
pageNum: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
showSizeChanger: {
type: Boolean,
default: true
},
size: {
type: String,
default: 'default'
},
alert: {
type: [Object, Boolean],
default: null
},
rowSelection: {
type: Object,
default: null
},
/** @Deprecated */
showAlertInfo: {
type: Boolean,
default: false
},
showPagination: {
type: String | Boolean,
default: 'auto'
},
/**
* enable page URI mode
*
* e.g:
* /users/1
* /users/2
* /users/3?queryParam=test
* ...
*/
pageURI: {
type: Boolean,
default: false
}
}),
watch: {
'localPagination.current' (val) {
this.pageURI && this.$router.push({
...this.$route,
name: this.$route.name,
params: Object.assign({}, this.$route.params, {
pageNo: val
})
})
},
pageNum (val) {
Object.assign(this.localPagination, {
current: val
})
},
pageSize (val) {
Object.assign(this.localPagination, {
pageSize: val
})
},
showSizeChanger (val) {
Object.assign(this.localPagination, {
showSizeChanger: val
})
},
'$route.path': function (newPath, oldPath) {
if (oldPath.indexOf(newPath) === -1) {
this.refresh(true)
}
}
},
created () {
const { pageNo } = this.$route.params
const localPageNum = this.pageURI && (pageNo && parseInt(pageNo)) || this.pageNum
this.localPagination = ['auto', true].includes(this.showPagination) && Object.assign({}, this.localPagination, {
current: localPageNum,
pageSize: this.pageSize,
showSizeChanger: this.showSizeChanger
}) || false
console.log('this.localPagination', this.localPagination)
this.needTotalList = this.initTotalList(this.columns)
this.loadData()
},
methods: {
/**
* 表格重新加载方法
* 如果参数为 true, 则强制刷新到第一页
* @param Boolean bool
*/
refresh (bool = false) {
bool && (this.localPagination = Object.assign({}, {
current: 1, pageSize: this.pageSize
}))
this.loadData()
},
/**
* 加载数据方法
* @param {Object} pagination 分页选项器
* @param {Object} filters 过滤条件
* @param {Object} sorter 排序条件
*/
loadData (pagination, filters, sorter) {
this.localLoading = true
const parameter = Object.assign({
pageNo: (pagination && pagination.current) ||
this.showPagination && this.localPagination.current || this.pageNum,
pageSize: (pagination && pagination.pageSize) ||
this.showPagination && this.localPagination.pageSize || this.pageSize
},
(sorter && sorter.field && {
sortField: sorter.field
}) || {},
(sorter && sorter.order && {
sortOrder: sorter.order
}) || {}, {
...filters
}
)
const result = this.data(parameter)
// 对接自己的通用数据接口需要修改下方代码中的 r.pageNo, r.totalCount, r.data
// eslint-disable-next-line
if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
result.then(r => {
this.localPagination = this.showPagination && Object.assign({}, this.localPagination, {
current: r.pageNo, // 返回结果中的当前分页数
total: r.totalCount, // 返回结果中的总记录数
showSizeChanger: this.showSizeChanger,
pageSize: (pagination && pagination.pageSize) ||
this.localPagination.pageSize
}) || false
// 为防止删除数据后导致页面当前页面数据长度为 0 ,自动翻页到上一页
if (r.data.length === 0 && this.showPagination && this.localPagination.current > 1) {
this.localPagination.current--
this.loadData()
return
}
// 这里用于判断接口是否有返回 r.totalCount this.showPagination = true pageNo pageSize 存在 totalCount 小于等于 pageNo * pageSize 的大小
// 当情况满足时表示数据不满足分页大小关闭 table 分页功能
try {
if ((['auto', true].includes(this.showPagination) && r.totalCount <= (r.pageNo * this.localPagination.pageSize))) {
this.localPagination.hideOnSinglePage = false
}
} catch (e) {
this.localPagination = false
}
this.localDataSource = r.data // 返回结果中的数组数据
this.localLoading = false
})
}
},
initTotalList (columns) {
const totalList = []
columns && columns instanceof Array && columns.forEach(column => {
if (column.needTotal) {
totalList.push({
...column,
total: 0
})
}
})
return totalList
},
/**
* 用于更新已选中的列表数据 total 统计
* @param selectedRowKeys
* @param selectedRows
*/
updateSelect (selectedRowKeys, selectedRows) {
this.selectedRows = selectedRows
this.selectedRowKeys = selectedRowKeys
const list = this.needTotalList
this.needTotalList = list.map(item => {
return {
...item,
total: selectedRows.reduce((sum, val) => {
const total = sum + parseInt(get(val, item.dataIndex))
return isNaN(total) ? 0 : total
}, 0)
}
})
},
/**
* 清空 table 已选中项
*/
clearSelected () {
if (this.rowSelection) {
this.rowSelection.onChange([], [])
this.updateSelect([], [])
}
},
/**
* 处理交给 table 使用者去处理 clear 事件时内部选中统计同时调用
* @param callback
* @returns {*}
*/
renderClear (callback) {
if (this.selectedRowKeys.length <= 0) return null
return (
<a style="margin-left: 24px" onClick={() => {
callback()
this.clearSelected()
}}>{ i18n.t('table.clear') }</a>
)
},
renderAlert () {
// 绘制统计列数据
const needTotalItems = this.needTotalList.map((item) => {
return (<span style="margin-right: 12px">
{item.title}总计 <a style="font-weight: 600">{!item.customRender ? item.total : item.customRender(item.total)}</a>
</span>)
})
// 绘制 清空 按钮
const clearItem = (typeof this.alert.clear === 'boolean' && this.alert.clear) ? (
this.renderClear(this.clearSelected)
) : (this.alert !== null && typeof this.alert.clear === 'function') ? (
this.renderClear(this.alert.clear)
) : null
// 绘制 alert 组件
return (
<a-alert showIcon={true} style="margin-bottom: 16px">
<template slot="message">
<span style="margin-right: 12px">{ i18n.t('table.selected') }: <a style="font-weight: 600">{this.selectedRows.length}</a></span>
{needTotalItems}
{clearItem}
</template>
</a-alert>
)
}
},
render () {
if (!this.loaded) {
return (
<div style="width: 100%; height:160px; text-align: center; line-height:160px">
<a-spin tip="Loading...">
</a-spin>
</div>
)
}
const props = {}
const localKeys = Object.keys(this.$data)
const showAlert = (typeof this.alert === 'object' && this.alert !== null && this.alert.show) && typeof this.rowSelection.selectedRowKeys !== 'undefined' || this.alert
Object.keys(T.props).forEach(k => {
const localKey = `local${k.substring(0, 1).toUpperCase()}${k.substring(1)}`
if (localKeys.includes(localKey)) {
props[k] = this[localKey]
return props[k]
}
if (k === 'rowSelection') {
if (showAlert && this.rowSelection) {
// 如果需要使用alert则重新绑定 rowSelection 事件
props[k] = {
...this.rowSelection,
selectedRows: this.selectedRows,
selectedRowKeys: this.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.updateSelect(selectedRowKeys, selectedRows)
typeof this[k].onChange !== 'undefined' && this[k].onChange(selectedRowKeys, selectedRows)
}
}
return props[k]
} else if (!this.rowSelection) {
// 如果没打算开启 rowSelection 则清空默认的选择项
props[k] = null
return props[k]
}
}
this[k] && (props[k] = this[k])
return props[k]
})
console.log('re-render table', new Date())
const table = (
<a-table {...{ props, scopedSlots: { ...this.$scopedSlots } }} onChange={this.loadData}>
{ Object.keys(this.$slots).map(name => (<template slot={name}>{this.$slots[name]}</template>)) }
</a-table>
)
return (
<div class="table-wrapper">
{ showAlert ? this.renderAlert() : null }
{ table }
</div>
)
}
}

View File

@@ -1,124 +0,0 @@
import { Menu, Icon, Input } from 'ant-design-vue'
const { Item, ItemGroup, SubMenu } = Menu
const { Search } = Input
export default {
name: 'Tree',
props: {
dataSource: {
type: Array,
required: true
},
openKeys: {
type: Array,
default: () => []
},
search: {
type: Boolean,
default: false
}
},
created () {
this.localOpenKeys = this.openKeys.slice(0)
},
data () {
return {
localOpenKeys: []
}
},
methods: {
handlePlus (item) {
this.$emit('add', item)
},
handleTitleClick (...args) {
this.$emit('titleClick', { args })
},
renderSearch () {
return (
<Search
placeholder="input search text"
style="width: 100%; margin-bottom: 1rem"
/>
)
},
renderIcon (icon) {
return icon && (<Icon type={icon} />) || null
},
renderMenuItem (item) {
return (
<Item key={item.key}>
{ this.renderIcon(item.icon) }
{ item.title }
<a class="btn" style="width: 20px;z-index:1300" {...{ on: { click: () => this.handlePlus(item) } }}><a-icon type="plus"/></a>
</Item>
)
},
renderItem (item) {
return item.children ? this.renderSubItem(item, item.key) : this.renderMenuItem(item, item.key)
},
renderItemGroup (item) {
const childrenItems = item.children.map(o => {
return this.renderItem(o, o.key)
})
return (
<ItemGroup key={item.key}>
<template slot="title">
<span>{ item.title }</span>
<a-dropdown>
<a class="btn"><a-icon type="ellipsis" /></a>
<a-menu slot="overlay">
<a-menu-item key="1">新增</a-menu-item>
<a-menu-item key="2">合并</a-menu-item>
<a-menu-item key="3">移除</a-menu-item>
</a-menu>
</a-dropdown>
</template>
{ childrenItems }
</ItemGroup>
)
},
renderSubItem (item, key) {
const childrenItems = item.children && item.children.map(o => {
return this.renderItem(o, o.key)
})
const title = (
<span slot="title">
{ this.renderIcon(item.icon) }
<span>{ item.title }</span>
</span>
)
if (item.group) {
return this.renderItemGroup(item)
}
// titleClick={this.handleTitleClick(item)}
return (
<SubMenu key={key}>
{ title }
{ childrenItems }
</SubMenu>
)
}
},
render () {
const { dataSource, search } = this.$props
// this.localOpenKeys = openKeys.slice(0)
const list = dataSource.map(item => {
return this.renderItem(item)
})
return (
<div class="tree-wrapper">
{ search ? this.renderSearch() : null }
<Menu mode="inline" class="custom-tree" {...{ on: { click: item => this.$emit('click', item), 'update:openKeys': val => { this.localOpenKeys = val } } }} openKeys={this.localOpenKeys}>
{ list }
</Menu>
</div>
)
}
}

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