This commit is contained in:
guorui
2025-02-19 10:28:49 +08:00
commit dad6d41cbc
1052 changed files with 102186 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
export default {
data() {
return {
levelList: null
}
},
watch: {
$route(route) {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/index', meta: { title: '首页' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim() === 'Index'
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(path)
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@@ -0,0 +1,161 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model='radioValue' :label="1">
允许的通配符[, - * ? / L W]
</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="30" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="31" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :label="4">
<el-input-number v-model='average01' :min="1" :max="30" /> 号开始
<el-input-number v-model='average02' :min="1" :max="31 - average01 || 1" /> 日执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :label="5">
每月
<el-input-number v-model='workday' :min="1" :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>
export default {
data() {
return {
radioValue: 1,
workday: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
}
},
name: 'crontab-day',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
('day rachange');
if (this.radioValue !== 2 && this.cron.week !== '?') {
this.$emit('update', 'week', '?', 'day')
}
switch (this.radioValue) {
case 1:
this.$emit('update', 'day', '*');
break;
case 2:
this.$emit('update', 'day', '?');
break;
case 3:
this.$emit('update', 'day', this.cycleTotal);
break;
case 4:
this.$emit('update', 'day', this.averageTotal);
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.workdayCheck + 'W');
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '7') {
this.$emit('update', 'day', this.checkboxString);
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'workdayCheck': 'workdayChange',
'checkboxString': 'checkboxChange',
},
computed: {
// 计算两个周期值
cycleTotal: function () {
const cycle01 = this.checkNum(this.cycle01, 1, 30)
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 31, 31)
return cycle01 + '-' + cycle02;
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, 1, 30)
const average02 = this.checkNum(this.average02, 1, 31 - average01 || 0)
return average01 + '/' + average02;
},
// 计算工作日格式
workdayCheck: function () {
const workday = this.checkNum(this.workday, 1, 31)
return workday;
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join();
return str == '' ? '*' : str;
}
}
}
</script>

View File

@@ -0,0 +1,120 @@
<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="22" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :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="22" /> 小时开始
<el-input-number v-model='average02' :min="1" :max="23 - average01 || 0" /> 小时执行一次
</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>
export default {
data() {
return {
radioValue: 1,
cycle01: 0,
cycle02: 1,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
}
},
name: 'crontab-hour',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
if (this.cron.min === '*') {
this.$emit('update', 'min', '0', 'hour');
}
if (this.cron.second === '*') {
this.$emit('update', 'second', '0', 'hour');
}
switch (this.radioValue) {
case 1:
this.$emit('update', 'hour', '*')
break;
case 2:
this.$emit('update', 'hour', this.cycleTotal);
break;
case 3:
this.$emit('update', 'hour', this.averageTotal);
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 () {
const cycle01 = this.checkNum(this.cycle01, 0, 22)
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 23)
return cycle01 + '-' + cycle02;
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, 0, 22)
const average02 = this.checkNum(this.average02, 1, 23 - average01 || 0)
return average01 + '/' + average02;
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join();
return str == '' ? '*' : str;
}
}
}
</script>

View File

@@ -0,0 +1,430 @@
<template>
<div>
<el-tabs type="border-card">
<el-tab-pane label="秒" v-if="shouldHide('second')">
<CrontabSecond
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronsecond"
/>
</el-tab-pane>
<el-tab-pane label="分钟" v-if="shouldHide('min')">
<CrontabMin
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronmin"
/>
</el-tab-pane>
<el-tab-pane label="小时" v-if="shouldHide('hour')">
<CrontabHour
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronhour"
/>
</el-tab-pane>
<el-tab-pane label="日" v-if="shouldHide('day')">
<CrontabDay
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronday"
/>
</el-tab-pane>
<el-tab-pane label="月" v-if="shouldHide('month')">
<CrontabMonth
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronmonth"
/>
</el-tab-pane>
<el-tab-pane label="周" v-if="shouldHide('week')">
<CrontabWeek
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronweek"
/>
</el-tab-pane>
<el-tab-pane label="年" v-if="shouldHide('year')">
<CrontabYear
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronyear"
/>
</el-tab-pane>
</el-tabs>
<div class="popup-main">
<div class="popup-result">
<p class="title">时间表达式</p>
<table>
<thead>
<th v-for="item of tabTitles" width="40" :key="item">{{item}}</th>
<th>Cron 表达式</th>
</thead>
<tbody>
<td>
<span>{{crontabValueObj.second}}</span>
</td>
<td>
<span>{{crontabValueObj.min}}</span>
</td>
<td>
<span>{{crontabValueObj.hour}}</span>
</td>
<td>
<span>{{crontabValueObj.day}}</span>
</td>
<td>
<span>{{crontabValueObj.month}}</span>
</td>
<td>
<span>{{crontabValueObj.week}}</span>
</td>
<td>
<span>{{crontabValueObj.year}}</span>
</td>
<td>
<span>{{crontabValueString}}</span>
</td>
</tbody>
</table>
</div>
<CrontabResult :ex="crontabValueString"></CrontabResult>
<div class="pop_btn">
<el-button size="small" type="primary" @click="submitFill">确定</el-button>
<el-button size="small" type="warning" @click="clearCron">重置</el-button>
<el-button size="small" @click="hidePopup">取消</el-button>
</div>
</div>
</div>
</template>
<script>
import CrontabSecond from "./second.vue";
import CrontabMin from "./min.vue";
import CrontabHour from "./hour.vue";
import CrontabDay from "./day.vue";
import CrontabMonth from "./month.vue";
import CrontabWeek from "./week.vue";
import CrontabYear from "./year.vue";
import CrontabResult from "./result.vue";
export default {
data() {
return {
tabTitles: ["秒", "分钟", "小时", "日", "月", "周", "年"],
tabActive: 0,
myindex: 0,
crontabValueObj: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
},
};
},
name: "vcrontab",
props: ["expression", "hideComponent"],
methods: {
shouldHide(key) {
if (this.hideComponent && this.hideComponent.includes(key)) return false;
return true;
},
resolveExp() {
// 反解析 表达式
if (this.expression) {
let arr = this.expression.split(" ");
if (arr.length >= 6) {
//6 位以上是合法表达式
let obj = {
second: arr[0],
min: arr[1],
hour: arr[2],
day: arr[3],
month: arr[4],
week: arr[5],
year: arr[6] ? arr[6] : "",
};
this.crontabValueObj = {
...obj,
};
for (let i in obj) {
if (obj[i]) this.changeRadio(i, obj[i]);
}
}
} else {
// 没有传入的表达式 则还原
this.clearCron();
}
},
// tab切换值
tabCheck(index) {
this.tabActive = index;
},
// 由子组件触发,更改表达式组成的字段值
updateCrontabValue(name, value, from) {
"updateCrontabValue", name, value, from;
this.crontabValueObj[name] = value;
if (from && from !== name) {
console.log(`来自组件 ${from} 改变了 ${name} ${value}`);
this.changeRadio(name, value);
}
},
// 赋值到组件
changeRadio(name, value) {
let arr = ["second", "min", "hour", "month"],
refName = "cron" + name,
insValue;
if (!this.$refs[refName]) return;
if (arr.includes(name)) {
if (value === "*") {
insValue = 1;
} else if (value.indexOf("-") > -1) {
let indexArr = value.split("-");
isNaN(indexArr[0])
? (this.$refs[refName].cycle01 = 0)
: (this.$refs[refName].cycle01 = indexArr[0]);
this.$refs[refName].cycle02 = indexArr[1];
insValue = 2;
} else if (value.indexOf("/") > -1) {
let indexArr = value.split("/");
isNaN(indexArr[0])
? (this.$refs[refName].average01 = 0)
: (this.$refs[refName].average01 = indexArr[0]);
this.$refs[refName].average02 = indexArr[1];
insValue = 3;
} else {
insValue = 4;
this.$refs[refName].checkboxList = value.split(",");
}
} else if (name == "day") {
if (value === "*") {
insValue = 1;
} else if (value == "?") {
insValue = 2;
} else if (value.indexOf("-") > -1) {
let indexArr = value.split("-");
isNaN(indexArr[0])
? (this.$refs[refName].cycle01 = 0)
: (this.$refs[refName].cycle01 = indexArr[0]);
this.$refs[refName].cycle02 = indexArr[1];
insValue = 3;
} else if (value.indexOf("/") > -1) {
let indexArr = value.split("/");
isNaN(indexArr[0])
? (this.$refs[refName].average01 = 0)
: (this.$refs[refName].average01 = indexArr[0]);
this.$refs[refName].average02 = indexArr[1];
insValue = 4;
} else if (value.indexOf("W") > -1) {
let indexArr = value.split("W");
isNaN(indexArr[0])
? (this.$refs[refName].workday = 0)
: (this.$refs[refName].workday = indexArr[0]);
insValue = 5;
} else if (value === "L") {
insValue = 6;
} else {
this.$refs[refName].checkboxList = value.split(",");
insValue = 7;
}
} else if (name == "week") {
if (value === "*") {
insValue = 1;
} else if (value == "?") {
insValue = 2;
} else if (value.indexOf("-") > -1) {
let indexArr = value.split("-");
isNaN(indexArr[0])
? (this.$refs[refName].cycle01 = 0)
: (this.$refs[refName].cycle01 = indexArr[0]);
this.$refs[refName].cycle02 = indexArr[1];
insValue = 3;
} else if (value.indexOf("#") > -1) {
let indexArr = value.split("#");
isNaN(indexArr[0])
? (this.$refs[refName].average01 = 1)
: (this.$refs[refName].average01 = indexArr[0]);
this.$refs[refName].average02 = indexArr[1];
insValue = 4;
} else if (value.indexOf("L") > -1) {
let indexArr = value.split("L");
isNaN(indexArr[0])
? (this.$refs[refName].weekday = 1)
: (this.$refs[refName].weekday = indexArr[0]);
insValue = 5;
} else {
this.$refs[refName].checkboxList = value.split(",");
insValue = 6;
}
} else if (name == "year") {
if (value == "") {
insValue = 1;
} else if (value == "*") {
insValue = 2;
} else if (value.indexOf("-") > -1) {
insValue = 3;
} else if (value.indexOf("/") > -1) {
insValue = 4;
} else {
this.$refs[refName].checkboxList = value.split(",");
insValue = 5;
}
}
this.$refs[refName].radioValue = insValue;
},
// 表单选项的子组件校验数字格式(通过-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() {
this.$emit("fill", this.crontabValueString);
this.hidePopup();
},
clearCron() {
// 还原选择项
("准备还原");
this.crontabValueObj = {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
};
for (let j in this.crontabValueObj) {
this.changeRadio(j, this.crontabValueObj[j]);
}
},
},
computed: {
crontabValueString: function() {
let obj = this.crontabValueObj;
let str =
obj.second +
" " +
obj.min +
" " +
obj.hour +
" " +
obj.day +
" " +
obj.month +
" " +
obj.week +
(obj.year == "" ? "" : " " + obj.year);
return str;
},
},
components: {
CrontabSecond,
CrontabMin,
CrontabHour,
CrontabDay,
CrontabMonth,
CrontabWeek,
CrontabYear,
CrontabResult,
},
watch: {
expression: "resolveExp",
hideComponent(value) {
// 隐藏部分组件
},
},
mounted: function() {
this.resolveExp();
},
};
</script>
<style scoped>
.pop_btn {
text-align: center;
margin-top: 20px;
}
.popup-main {
position: relative;
margin: 10px auto;
background: #fff;
border-radius: 5px;
font-size: 12px;
overflow: hidden;
}
.popup-title {
overflow: hidden;
line-height: 34px;
padding-top: 6px;
background: #f2f2f2;
}
.popup-result {
box-sizing: border-box;
line-height: 24px;
margin: 25px auto;
padding: 15px 10px 10px;
border: 1px solid #ccc;
position: relative;
}
.popup-result .title {
position: absolute;
top: -28px;
left: 50%;
width: 140px;
font-size: 14px;
margin-left: -70px;
text-align: center;
line-height: 30px;
background: #fff;
}
.popup-result table {
text-align: center;
width: 100%;
margin: 0 auto;
}
.popup-result table span {
display: block;
width: 100%;
font-family: arial;
line-height: 30px;
height: 30px;
white-space: nowrap;
overflow: hidden;
border: 1px solid #e8e8e8;
}
.popup-result-scroll {
font-size: 12px;
line-height: 24px;
height: 10em;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,116 @@
<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="58" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> 分钟
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :label="3">
<el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始
<el-input-number v-model='average02' :min="1" :max="59 - average01 || 0" /> 分钟执行一次
</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>
export default {
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
}
},
name: 'crontab-min',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
switch (this.radioValue) {
case 1:
this.$emit('update', 'min', '*', 'min');
break;
case 2:
this.$emit('update', 'min', this.cycleTotal, 'min');
break;
case 3:
this.$emit('update', 'min', this.averageTotal, '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 () {
const cycle01 = this.checkNum(this.cycle01, 0, 58)
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 59)
return cycle01 + '-' + cycle02;
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, 0, 58)
const average02 = this.checkNum(this.average02, 1, 59 - average01 || 0)
return average01 + '/' + average02;
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join();
return str == '' ? '*' : str;
}
}
}
</script>

View File

@@ -0,0 +1,114 @@
<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="11" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :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="11" /> 月开始
<el-input-number v-model='average02' :min="1" :max="12 - average01 || 0" /> 月月执行一次
</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>
export default {
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
checkNum: this.check
}
},
name: 'crontab-month',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
switch (this.radioValue) {
case 1:
this.$emit('update', 'month', '*');
break;
case 2:
this.$emit('update', 'month', this.cycleTotal);
break;
case 3:
this.$emit('update', 'month', this.averageTotal);
break;
case 4:
this.$emit('update', 'month', this.checkboxString);
break;
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue == '2') {
this.$emit('update', 'month', this.cycleTotal);
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue == '3') {
this.$emit('update', 'month', this.averageTotal);
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue == '4') {
this.$emit('update', 'month', this.checkboxString);
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange'
},
computed: {
// 计算两个周期值
cycleTotal: function () {
const cycle01 = this.checkNum(this.cycle01, 1, 11)
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 12)
return cycle01 + '-' + cycle02;
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, 1, 11)
const average02 = this.checkNum(this.average02, 1, 12 - average01 || 0)
return average01 + '/' + average02;
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join();
return str == '' ? '*' : str;
}
}
}
</script>

View File

@@ -0,0 +1,559 @@
<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>
export default {
data() {
return {
dayRule: '',
dayRuleSup: '',
dateArr: [],
resultList: [],
isShow: false
}
},
name: 'crontab-result',
methods: {
// 表达式值变化时,开始去计算结果
expressionChange() {
// 计算开始-隐藏结果
this.isShow = false;
// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
let ruleArr = this.$options.propsData.ex.split(' ');
// 用于记录进入循环的次数
let nums = 0;
// 用于暂时存符号时间规则结果的数组
let resultArr = [];
// 获取当前时间精确至[年、月、日、时、分、秒]
let nTime = new Date();
let nYear = nTime.getFullYear();
let nMonth = 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.getMonthArr(ruleArr[4]);
this.getWeekArr(ruleArr[5]);
this.getYearArr(ruleArr[6], nYear);
// 将获取到的数组赋值-方便使用
let sDate = this.dateArr[0];
let mDate = this.dateArr[1];
let hDate = this.dateArr[2];
let DDate = this.dateArr[3];
let MDate = this.dateArr[4];
let 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, nMonth);
let 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 resetMonth = function () {
MIdx = 0;
nMonth = MDate[MIdx]
resetDay();
}
// 如果当前年份不为数组中当前值
if (nYear !== YDate[YIdx]) {
resetMonth();
}
// 如果当前月份不为数组中当前值
if (nMonth !== 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++) {
let YY = YDate[Yi];
// 如果到达最大值时
if (nMonth > MDate[MDate.length - 1]) {
resetMonth();
continue;
}
// 循环月份数组
goMonth: 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) {
resetMonth();
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) {
resetMonth();
continue goYear;
}
continue goMonth;
}
continue;
}
// 判断日期的合法性,不合法的话也是跳出当前循环
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && this.dayRule !== 'workDay' && this.dayRule !== 'lastWeek' && this.dayRule !== 'lastDay') {
resetDay();
continue goMonth;
}
// 如果日期规则中有值时
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
let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
// 当星期日时
if (thisWeek == 1) {
// 先找下一个日,并判断是否为月底
DD++;
thisDD = DD < 10 ? '0' + DD : DD;
// 判断下一日已经不是合法日期
if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD -= 3;
}
} else if (thisWeek == 7) {
// 当星期6时只需判断不是1号就可进行操作
if (this.dayRuleSup !== 1) {
DD--;
} else {
DD += 2;
}
}
} else if (this.dayRule == 'weekDay') {
// 如果指定了是星期几
// 获取当前日期是属于星期几
let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
// 校验当前星期是否在星期池dayRuleSup
if (this.dayRuleSup.indexOf(thisWeek) < 0) {
// 如果到达最大值时
if (Di == DDate.length - 1) {
resetDay();
if (Mi == MDate.length - 1) {
resetMonth();
continue goYear;
}
continue goMonth;
}
continue;
}
} else if (this.dayRule == 'assWeek') {
// 如果指定了是第几周的星期几
// 获取每月1号是属于星期几
let 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;
}
}
// 获取月末最后一天是星期几
let 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++) {
let 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) {
resetMonth();
continue goYear;
}
continue goMonth;
}
continue goDay;
}
continue;
}
// 循环"分"数组
goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
let 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) {
resetMonth();
continue goYear;
}
continue goMonth;
}
continue goDay;
}
continue goHour;
}
continue;
}
// 循环"秒"数组
goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
let 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) {
resetMonth();
continue goYear;
}
continue goMonth;
}
continue goDay;
}
continue goHour;
}
continue goMin;
}
} //goSecond
} //goMin
}//goHour
}//goDay
}//goMonth
}
// 判断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)
}
}
},
// 获取"月"数组
getMonthArr(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';
let matchRule = rule.match(/[0-9]{1}/g);
this.dayRuleSup = [Number(matchRule[1]), Number(matchRule[0])];
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)
}
}
},
// 获取"日"数组-少量为日期规则
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) {
let arr = [];
for (let i = min; i <= max; i++) {
arr.push(i);
}
return arr;
},
// 根据规则中指定的零散值返回一个数组
getAssignArr(rule) {
let arr = [];
let 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) {
let arr = [];
let agArr = rule.split('/');
let min = Number(agArr[0]);
let step = Number(agArr[1]);
while (min <= limit) {
arr.push(min);
min += step;
}
return arr;
},
// 根据规则返回一个具有周期性的数组
getCycleArr(rule, limit, status) {
// status--表示是否从0开始则从1开始
let arr = [];
let cycleArr = rule.split('-');
let 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) {
// 计算日期相关值
let time = typeof value == 'number' ? new Date(value) : value;
let Y = time.getFullYear();
let M = time.getMonth() + 1;
let D = time.getDate();
let h = time.getHours();
let m = time.getMinutes();
let s = time.getSeconds();
let 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') {
// 在quartz中 1为星期日
return week + 1;
}
},
// 检查日期是否存在
checkDate(value) {
let time = new Date(value);
let format = this.formatDate(time)
return value === format;
}
},
watch: {
'ex': 'expressionChange'
},
props: ['ex'],
mounted: function () {
// 初始化 获取一次结果
this.expressionChange();
}
}
</script>

View File

@@ -0,0 +1,117 @@
<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="58" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :label="3">
<el-input-number v-model='average01' :min="0" :max="58" /> 秒开始
<el-input-number v-model='average02' :min="1" :max="59 - average01 || 0" /> 秒执行一次
</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>
export default {
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
}
},
name: 'crontab-second',
props: ['check', 'radioParent'],
methods: {
// 单选按钮值变化时
radioChange() {
switch (this.radioValue) {
case 1:
this.$emit('update', 'second', '*', 'second');
break;
case 2:
this.$emit('update', 'second', this.cycleTotal);
break;
case 3:
this.$emit('update', 'second', this.averageTotal);
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);
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange',
radioParent() {
this.radioValue = this.radioParent
}
},
computed: {
// 计算两个周期值
cycleTotal: function () {
const cycle01 = this.checkNum(this.cycle01, 0, 58)
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 59)
return cycle01 + '-' + cycle02;
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, 0, 58)
const average02 = this.checkNum(this.average02, 1, 59 - average01 || 0)
return average01 + '/' + average02;
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join();
return str == '' ? '*' : str;
}
}
}
</script>

View File

@@ -0,0 +1,202 @@
<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-select clearable v-model="cycle01">
<el-option
v-for="(item,index) of weekList"
:key="index"
:label="item.value"
:value="item.key"
:disabled="item.key === 1"
>{{item.value}}</el-option>
</el-select>
-
<el-select clearable v-model="cycle02">
<el-option
v-for="(item,index) of weekList"
:key="index"
:label="item.value"
:value="item.key"
:disabled="item.key < cycle01 && item.key !== 1"
>{{item.value}}</el-option>
</el-select>
</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-select clearable v-model="average02">
<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="item.key">{{item.value}}</el-option>
</el-select>
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :label="5">
本月最后一个星期
<el-select clearable v-model="weekday">
<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="item.key">{{item.value}}</el-option>
</el-select>
</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" :label="item.value" :value="String(item.key)">{{item.value}}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
radioValue: 2,
weekday: 2,
cycle01: 2,
cycle02: 3,
average01: 1,
average02: 2,
checkboxList: [],
weekList: [
{
key: 2,
value: '星期一'
},
{
key: 3,
value: '星期二'
},
{
key: 4,
value: '星期三'
},
{
key: 5,
value: '星期四'
},
{
key: 6,
value: '星期五'
},
{
key: 7,
value: '星期六'
},
{
key: 1,
value: '星期日'
}
],
checkNum: this.$options.propsData.check
}
},
name: 'crontab-week',
props: ['check', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue !== 2 && this.cron.day !== '?') {
this.$emit('update', 'day', '?', 'week');
}
switch (this.radioValue) {
case 1:
this.$emit('update', 'week', '*');
break;
case 2:
this.$emit('update', 'week', '?');
break;
case 3:
this.$emit('update', 'week', this.cycleTotal);
break;
case 4:
this.$emit('update', 'week', this.averageTotal);
break;
case 5:
this.$emit('update', 'week', this.weekdayCheck + 'L');
break;
case 6:
this.$emit('update', 'week', this.checkboxString);
break;
}
},
// 周期两个值变化时
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.average02 + '#' + this.average01;
},
// 最近的工作日(格式)
weekdayCheck: function () {
this.weekday = this.checkNum(this.weekday, 1, 7)
return this.weekday;
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join();
return str == '' ? '*' : str;
}
}
}
</script>

View File

@@ -0,0 +1,131 @@
<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' :max="2098" /> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :label="4" v-model='radioValue'>
<el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始
<el-input-number v-model='average02' :min="1" :max="2099 - average01 || 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>
export default {
data() {
return {
fullYear: 0,
radioValue: 1,
cycle01: 0,
cycle02: 0,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
}
},
name: 'crontab-year',
props: ['check', 'month', 'cron'],
methods: {
// 单选按钮值变化时
radioChange() {
switch (this.radioValue) {
case 1:
this.$emit('update', 'year', '');
break;
case 2:
this.$emit('update', 'year', '*');
break;
case 3:
this.$emit('update', 'year', this.cycleTotal);
break;
case 4:
this.$emit('update', 'year', this.averageTotal);
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 () {
const cycle01 = this.checkNum(this.cycle01, this.fullYear, 2098)
const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : this.fullYear + 1, 2099)
return cycle01 + '-' + cycle02;
},
// 计算平均用到的值
averageTotal: function () {
const average01 = this.checkNum(this.average01, this.fullYear, 2098)
const average02 = this.checkNum(this.average02, 1, 2099 - average01 || this.fullYear)
return average01 + '/' + average02;
},
// 计算勾选的checkbox值合集
checkboxString: function () {
let str = this.checkboxList.join();
return str;
}
},
mounted: function () {
// 仅获取当前年份
this.fullYear = Number(new Date().getFullYear());
this.cycle01 = this.fullYear
this.average01 = this.fullYear
}
}
</script>

View File

@@ -0,0 +1,49 @@
import Vue from 'vue'
import store from '@/store'
import DataDict from '@/utils/dict'
import { getDicts as getDicts } from '@/api/system/dict/data'
function searchDictByKey(dict, key) {
if (key == null && key == "") {
return null
}
try {
for (let i = 0; i < dict.length; i++) {
if (dict[i].key == key) {
return dict[i].value
}
}
} catch (e) {
return null
}
}
function install() {
Vue.use(DataDict, {
metas: {
'*': {
labelField: 'dictLabel',
valueField: 'dictValue',
request(dictMeta) {
const storeDict = searchDictByKey(store.getters.dict, dictMeta.type)
if (storeDict) {
return new Promise(resolve => { resolve(storeDict) })
} else {
return new Promise((resolve, reject) => {
getDicts(dictMeta.type).then(res => {
store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data })
resolve(res.data)
}).catch(error => {
reject(error)
})
})
}
},
},
},
})
}
export default {
install,
}

View File

@@ -0,0 +1,89 @@
<template>
<div>
<template v-for="(item, index) in options">
<template v-if="values.includes(item.value)">
<span
v-if="(item.raw.listClass == 'default' || item.raw.listClass == '') && (item.raw.cssClass == '' || item.raw.cssClass == null)"
:key="item.value"
:index="index"
:class="item.raw.cssClass"
>{{ item.label + ' ' }}</span
>
<el-tag
v-else
:disable-transitions="true"
:key="item.value"
:index="index"
:type="item.raw.listClass == 'primary' ? '' : item.raw.listClass"
:class="item.raw.cssClass"
>
{{ item.label + ' ' }}
</el-tag>
</template>
</template>
<template v-if="unmatch && showValue">
{{ unmatchArray | handleArray }}
</template>
</div>
</template>
<script>
export default {
name: "DictTag",
props: {
options: {
type: Array,
default: null,
},
value: [Number, String, Array],
// 当未找到匹配的数据时显示value
showValue: {
type: Boolean,
default: true,
},
separator: {
type: String,
default: ","
}
},
data() {
return {
unmatchArray: [], // 记录未匹配的项
}
},
computed: {
values() {
if (this.value === null || typeof this.value === 'undefined' || this.value === '') return []
return Array.isArray(this.value) ? this.value.map(item => '' + item) : String(this.value).split(this.separator)
},
unmatch() {
this.unmatchArray = []
// 没有value不显示
if (this.value === null || typeof this.value === 'undefined' || this.value === '' || this.options.length === 0) return false
// 传入值为数组
let unmatch = false // 添加一个标志来判断是否有未匹配项
this.values.forEach(item => {
if (!this.options.some(v => v.value === item)) {
this.unmatchArray.push(item)
unmatch = true // 如果有未匹配项将标志设置为true
}
})
return unmatch // 返回标志的值
},
},
filters: {
handleArray(array) {
if (array.length === 0) return '';
return array.reduce((pre, cur) => {
return pre + ' ' + cur;
})
},
}
};
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,274 @@
<template>
<div>
<el-upload
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
name="file"
:show-file-list="false"
:headers="headers"
style="display: none"
ref="upload"
v-if="this.type == 'url'"
>
</el-upload>
<div class="editor" ref="editor" :style="styles"></div>
</div>
</template>
<script>
import Quill from "quill";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
import { getToken } from "@/utils/auth";
export default {
name: "Editor",
props: {
/* 编辑器的内容 */
value: {
type: String,
default: "",
},
/* 高度 */
height: {
type: Number,
default: null,
},
/* 最小高度 */
minHeight: {
type: Number,
default: null,
},
/* 只读 */
readOnly: {
type: Boolean,
default: false,
},
/* 上传文件大小限制(MB) */
fileSize: {
type: Number,
default: 5,
},
/* 类型base64格式、url格式 */
type: {
type: String,
default: "url",
}
},
data() {
return {
uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken()
},
Quill: null,
currentValue: "",
options: {
theme: "snow",
bounds: document.body,
debug: "warn",
modules: {
// 工具栏配置
toolbar: [
["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
["blockquote", "code-block"], // 引用 代码块
[{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
[{ indent: "-1" }, { indent: "+1" }], // 缩进
[{ size: ["small", false, "large", "huge"] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
[{ align: [] }], // 对齐方式
["clean"], // 清除文本格式
["link", "image", "video"] // 链接、图片、视频
],
},
placeholder: "请输入内容",
readOnly: this.readOnly,
},
};
},
computed: {
styles() {
let style = {};
if (this.minHeight) {
style.minHeight = `${this.minHeight}px`;
}
if (this.height) {
style.height = `${this.height}px`;
}
return style;
},
},
watch: {
value: {
handler(val) {
if (val !== this.currentValue) {
this.currentValue = val === null ? "" : val;
if (this.Quill) {
this.Quill.pasteHTML(this.currentValue);
}
}
},
immediate: true,
},
},
mounted() {
this.init();
},
beforeDestroy() {
this.Quill = null;
},
methods: {
init() {
const editor = this.$refs.editor;
this.Quill = new Quill(editor, this.options);
// 如果设置了上传地址则自定义图片上传事件
if (this.type == 'url') {
let toolbar = this.Quill.getModule("toolbar");
toolbar.addHandler("image", (value) => {
if (value) {
this.$refs.upload.$children[0].$refs.input.click();
} else {
this.quill.format("image", false);
}
});
}
this.Quill.pasteHTML(this.currentValue);
this.Quill.on("text-change", (delta, oldDelta, source) => {
const html = this.$refs.editor.children[0].innerHTML;
const text = this.Quill.getText();
const quill = this.Quill;
this.currentValue = html;
this.$emit("input", html);
this.$emit("on-change", { html, text, quill });
});
this.Quill.on("text-change", (delta, oldDelta, source) => {
this.$emit("on-text-change", delta, oldDelta, source);
});
this.Quill.on("selection-change", (range, oldRange, source) => {
this.$emit("on-selection-change", range, oldRange, source);
});
this.Quill.on("editor-change", (eventName, ...args) => {
this.$emit("on-editor-change", eventName, ...args);
});
},
// 上传前校检格式和大小
handleBeforeUpload(file) {
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
const isJPG = type.includes(file.type);
// 检验文件格式
if (!isJPG) {
this.$message.error(`图片格式错误!`);
return false;
}
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
return true;
},
handleUploadSuccess(res, file) {
// 如果上传成功
if (res.code == 200) {
// 获取富文本组件实例
let quill = this.Quill;
// 获取光标所在位置
let length = quill.getSelection().index;
// 插入图片 res.url为服务器返回的图片地址
quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.fileName);
// 调整光标到最后
quill.setSelection(length + 1);
} else {
this.$message.error("图片插入失败");
}
},
handleUploadError() {
this.$message.error("图片插入失败");
},
},
};
</script>
<style>
.editor, .ql-toolbar {
white-space: pre-wrap !important;
line-height: normal !important;
}
.quill-img {
display: none;
}
.ql-snow .ql-tooltip[data-mode="link"]::before {
content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: "保存";
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode="video"]::before {
content: "请输入视频地址:";
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
content: "32px";
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: "文本";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: "标题1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: "标题2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: "标题3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: "标题4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: "标题5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: "标题6";
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: "标准字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
content: "衬线字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
content: "等宽字体";
}
</style>

View File

@@ -0,0 +1,216 @@
<template>
<div class="upload-file">
<el-upload
multiple
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:show-file-list="false"
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
>
<!-- 上传按钮 -->
<el-button size="mini" type="primary">选取文件</el-button>
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
</el-upload>
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.url" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
</div>
</li>
</transition-group>
</div>
</template>
<script>
import { getToken } from "@/utils/auth";
export default {
name: "FileUpload",
props: {
// 值
value: [String, Object, Array],
// 数量限制
limit: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
}
},
data() {
return {
number: 0,
uploadList: [],
baseUrl: process.env.VUE_APP_BASE_API,
uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传文件服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: [],
};
},
watch: {
value: {
handler(val) {
if (val) {
let temp = 1;
// 首先将值转为数组
const list = Array.isArray(val) ? val : this.value.split(',');
// 然后将数组转为对象数组
this.fileList = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
this.fileList = [];
return [];
}
},
deep: true,
immediate: true
}
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
},
methods: {
// 上传前校检格式和大小
handleBeforeUpload(file) {
// 校检文件类型
if (this.fileType) {
const fileName = file.name.split('.');
const fileExt = fileName[fileName.length - 1];
const isTypeOk = this.fileType.indexOf(fileExt) >= 0;
if (!isTypeOk) {
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}格式文件!`);
return false;
}
}
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$modal.msgError(`上传文件大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
this.$modal.loading("正在上传文件,请稍候...");
this.number++;
return true;
},
// 文件个数超出
handleExceed() {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
},
// 上传失败
handleUploadError(err) {
this.$modal.msgError("上传文件失败,请重试");
this.$modal.closeLoading();
},
// 上传成功回调
handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push({ name: res.fileName, url: res.fileName });
this.uploadedSuccessfully();
} else {
this.number--;
this.$modal.closeLoading();
this.$modal.msgError(res.msg);
this.$refs.fileUpload.handleRemove(file);
this.uploadedSuccessfully();
}
},
// 删除文件
handleDelete(index) {
this.fileList.splice(index, 1);
this.$emit("input", this.listToString(this.fileList));
},
// 上传结束处理
uploadedSuccessfully() {
if (this.number > 0 && this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList);
this.uploadList = [];
this.number = 0;
this.$emit("input", this.listToString(this.fileList));
this.$modal.closeLoading();
}
},
// 获取文件名称
getFileName(name) {
// 如果是url那么取最后的名字 如果不是直接返回
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return name;
}
},
// 对象转成指定字符串分隔
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
strs += list[i].url + separator;
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
}
}
};
</script>
<style scoped lang="scss">
.upload-file-uploader {
margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
border: 1px solid #e4e7ed;
line-height: 2;
margin-bottom: 10px;
position: relative;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
justify-content: space-between;
align-items: center;
color: inherit;
}
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@@ -0,0 +1,198 @@
<template>
<div :class="{'show':show}" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select
ref="headerSearchSelect"
v-model="search"
:remote-method="querySearch"
filterable
default-first-option
remote
placeholder="Search"
class="header-search-select"
@change="change"
>
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
</el-select>
</div>
</template>
<script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js/dist/fuse.min.js'
import path from 'path'
export default {
name: 'HeaderSearch',
data() {
return {
search: '',
options: [],
searchPool: [],
show: false,
fuse: undefined
}
},
computed: {
routes() {
return this.$store.getters.permission_routes
}
},
watch: {
routes() {
this.searchPool = this.generateRoutes(this.routes)
},
searchPool(list) {
this.initFuse(list)
},
show(value) {
if (value) {
document.body.addEventListener('click', this.close)
} else {
document.body.removeEventListener('click', this.close)
}
}
},
mounted() {
this.searchPool = this.generateRoutes(this.routes)
},
methods: {
click() {
this.show = !this.show
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
}
},
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
this.options = []
this.show = false
},
change(val) {
const path = val.path;
const query = val.query;
if(this.ishttp(val.path)) {
// http(s):// 路径新窗口打开
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
} else {
if (query) {
this.$router.push({ path: path, query: JSON.parse(query) });
} else {
this.$router.push(path)
}
}
this.search = ''
this.options = []
this.$nextTick(() => {
this.show = false
})
},
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routes) {
// skip hidden router
if (router.hidden) { continue }
const data = {
path: !this.ishttp(router.path) ? path.resolve(basePath, router.path) : router.path,
title: [...prefixTitle]
}
if (router.meta && router.meta.title) {
data.title = [...data.title, router.meta.title]
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
if (router.query) {
data.query = router.query
}
// recursive child routes
if (router.children) {
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query)
} else {
this.options = []
}
},
ishttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
}
}
</script>
<style lang="scss" scoped>
.header-search {
font-size: 0 !important;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
::v-deep .el-input__inner {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
}
}
</style>

View File

@@ -0,0 +1,104 @@
<!-- @author zhengjie -->
<template>
<div class="icon-body">
<el-input v-model="name" class="icon-search" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons">
<i slot="suffix" class="el-icon-search el-input__icon" />
</el-input>
<div class="icon-list">
<div class="list-container">
<div v-for="(item, index) in iconList" class="icon-item-wrapper" :key="index" @click="selectedIcon(item)">
<div :class="['icon-item', { active: activeIcon === item }]">
<svg-icon :icon-class="item" class-name="icon" style="height: 25px;width: 16px;"/>
<span>{{ item }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import icons from './requireIcons'
export default {
name: 'IconSelect',
props: {
activeIcon: {
type: String
}
},
data() {
return {
name: '',
iconList: icons
}
},
methods: {
filterIcons() {
this.iconList = icons
if (this.name) {
this.iconList = this.iconList.filter(item => item.includes(this.name))
}
},
selectedIcon(name) {
this.$emit('selected', name)
document.body.click()
},
reset() {
this.name = ''
this.iconList = icons
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.icon-body {
width: 100%;
padding: 10px;
.icon-search {
position: relative;
margin-bottom: 5px;
}
.icon-list {
height: 200px;
overflow: auto;
.list-container {
display: flex;
flex-wrap: wrap;
.icon-item-wrapper {
width: calc(100% / 3);
height: 25px;
line-height: 25px;
cursor: pointer;
display: flex;
.icon-item {
display: flex;
max-width: 100%;
height: 100%;
padding: 0 5px;
&:hover {
background: #ececec;
border-radius: 5px;
}
.icon {
flex-shrink: 0;
}
span {
display: inline-block;
vertical-align: -0.15em;
fill: currentColor;
padding-left: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.icon-item.active {
background: #ececec;
border-radius: 5px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,11 @@
const req = require.context('../../assets/icons/svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys()
const re = /\.\/(.*)\.svg/
const icons = requireAll(req).map(i => {
return i.match(re)[1]
})
export default icons

View File

@@ -0,0 +1,90 @@
<template>
<el-image
:src="`${realSrc}`"
fit="cover"
:style="`width:${realWidth};height:${realHeight};`"
:preview-src-list="realSrcList"
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</template>
<script>
import { isExternal } from "@/utils/validate";
export default {
name: "ImagePreview",
props: {
src: {
type: String,
default: ""
},
width: {
type: [Number, String],
default: ""
},
height: {
type: [Number, String],
default: ""
}
},
computed: {
realSrc() {
if (!this.src) {
return;
}
let real_src = this.src.split(",")[0];
if (isExternal(real_src)) {
return real_src;
}
return process.env.VUE_APP_BASE_API + real_src;
},
realSrcList() {
if (!this.src) {
return;
}
let real_src_list = this.src.split(",");
let srcList = [];
real_src_list.forEach(item => {
if (isExternal(item)) {
return srcList.push(item);
}
return srcList.push(process.env.VUE_APP_BASE_API + item);
});
return srcList;
},
realWidth() {
return typeof this.width == "string" ? this.width : `${this.width}px`;
},
realHeight() {
return typeof this.height == "string" ? this.height : `${this.height}px`;
}
},
};
</script>
<style lang="scss" scoped>
.el-image {
border-radius: 5px;
background-color: #ebeef5;
box-shadow: 0 0 5px 1px #ccc;
::v-deep .el-image__inner {
transition: all 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
}
::v-deep .image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: #909399;
font-size: 30px;
}
}
</style>

View File

@@ -0,0 +1,226 @@
<template>
<div class="component-upload-image">
<el-upload
multiple
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
ref="imageUpload"
:on-remove="handleDelete"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:class="{hide: this.fileList.length >= this.limit}"
>
<i class="el-icon-plus"></i>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
<el-dialog
:visible.sync="dialogVisible"
title="预览"
width="800"
append-to-body
>
<img
:src="dialogImageUrl"
style="display: block; max-width: 100%; margin: 0 auto"
/>
</el-dialog>
</div>
</template>
<script>
import { getToken } from "@/utils/auth";
export default {
props: {
value: [String, Object, Array],
// 图片数量限制
limit: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
}
},
data() {
return {
number: 0,
uploadList: [],
dialogImageUrl: "",
dialogVisible: false,
hideUpload: false,
baseUrl: process.env.VUE_APP_BASE_API,
uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: []
};
},
watch: {
value: {
handler(val) {
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : this.value.split(',');
// 然后将数组转为对象数组
this.fileList = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(this.baseUrl) === -1) {
item = { name: this.baseUrl + item, url: this.baseUrl + item };
} else {
item = { name: item, url: item };
}
}
return item;
});
} else {
this.fileList = [];
return [];
}
},
deep: true,
immediate: true
}
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
},
methods: {
// 上传前loading加载
handleBeforeUpload(file) {
let isImg = false;
if (this.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
}
isImg = this.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
} else {
isImg = file.type.indexOf("image") > -1;
}
if (!isImg) {
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`);
return false;
}
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
this.$modal.loading("正在上传图片,请稍候...");
this.number++;
},
// 文件个数超出
handleExceed() {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
},
// 上传成功回调
handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push({ name: res.fileName, url: res.fileName });
this.uploadedSuccessfully();
} else {
this.number--;
this.$modal.closeLoading();
this.$modal.msgError(res.msg);
this.$refs.imageUpload.handleRemove(file);
this.uploadedSuccessfully();
}
},
// 删除图片
handleDelete(file) {
const findex = this.fileList.map(f => f.name).indexOf(file.name);
if (findex > -1) {
this.fileList.splice(findex, 1);
this.$emit("input", this.listToString(this.fileList));
}
},
// 上传失败
handleUploadError() {
this.$modal.msgError("上传图片失败,请重试");
this.$modal.closeLoading();
},
// 上传结束处理
uploadedSuccessfully() {
if (this.number > 0 && this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList);
this.uploadList = [];
this.number = 0;
this.$emit("input", this.listToString(this.fileList));
this.$modal.closeLoading();
}
},
// 预览
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
},
// 对象转成指定字符串分隔
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
if (list[i].url) {
strs += list[i].url.replace(this.baseUrl, "") + separator;
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
}
}
};
</script>
<style scoped lang="scss">
// .el-upload--picture-card 控制加号部分
::v-deep.hide .el-upload--picture-card {
display: none;
}
// 去掉动画效果
::v-deep .el-list-enter-active,
::v-deep .el-list-leave-active {
transition: all 0s;
}
::v-deep .el-list-enter, .el-list-leave-active {
opacity: 0;
transform: translateY(0);
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<div :class="{'hidden':hidden}" class="pagination-container">
<el-pagination
:background="background"
:current-page.sync="currentPage"
:page-size.sync="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
:total="total"
v-bind="$attrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
import { scrollTo } from '@/utils/scroll-to'
export default {
name: 'Pagination',
props: {
total: {
required: true,
type: Number
},
page: {
type: Number,
default: 1
},
limit: {
type: Number,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
// 移动端页码按钮的数量端默认值5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
},
data() {
return {
};
},
computed: {
currentPage: {
get() {
return this.page
},
set(val) {
this.$emit('update:page', val)
}
},
pageSize: {
get() {
return this.limit
},
set(val) {
this.$emit('update:limit', val)
}
}
},
methods: {
handleSizeChange(val) {
if (this.currentPage * val > this.total) {
this.currentPage = 1
}
this.$emit('pagination', { page: this.currentPage, limit: val })
if (this.autoScroll) {
scrollTo(0, 800)
}
},
handleCurrentChange(val) {
this.$emit('pagination', { page: val, limit: this.pageSize })
if (this.autoScroll) {
scrollTo(0, 800)
}
}
}
}
</script>
<style scoped>
.pagination-container {
background: #fff;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
<div class="pan-info">
<div class="pan-info-roles-container">
<slot />
</div>
</div>
<!-- eslint-disable-next-line -->
<div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
</div>
</template>
<script>
export default {
name: 'PanThumb',
props: {
image: {
type: String,
required: true
},
zIndex: {
type: Number,
default: 1
},
width: {
type: String,
default: '150px'
},
height: {
type: String,
default: '150px'
}
}
}
</script>
<style scoped>
.pan-item {
width: 200px;
height: 200px;
border-radius: 50%;
display: inline-block;
position: relative;
cursor: default;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.pan-info-roles-container {
padding: 20px;
text-align: center;
}
.pan-thumb {
width: 100%;
height: 100%;
background-position: center center;
background-size: cover;
border-radius: 50%;
overflow: hidden;
position: absolute;
transform-origin: 95% 40%;
transition: all 0.3s ease-in-out;
}
/* .pan-thumb:after {
content: '';
width: 8px;
height: 8px;
position: absolute;
border-radius: 50%;
top: 40%;
left: 95%;
margin: -4px 0 0 -4px;
background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
} */
.pan-info {
position: absolute;
width: inherit;
height: inherit;
border-radius: 50%;
overflow: hidden;
box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
}
.pan-info h3 {
color: #fff;
text-transform: uppercase;
position: relative;
letter-spacing: 2px;
font-size: 18px;
margin: 0 60px;
padding: 22px 0 0 0;
height: 85px;
font-family: 'Open Sans', Arial, sans-serif;
text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
}
.pan-info p {
color: #fff;
padding: 10px 5px;
font-style: italic;
margin: 0 30px;
font-size: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.5);
}
.pan-info p a {
display: block;
color: #333;
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
color: #fff;
font-style: normal;
font-weight: 700;
text-transform: uppercase;
font-size: 9px;
letter-spacing: 1px;
padding-top: 24px;
margin: 7px auto 0;
font-family: 'Open Sans', Arial, sans-serif;
opacity: 0;
transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
transform: translateX(60px) rotate(90deg);
}
.pan-info p a:hover {
background: rgba(255, 255, 255, 0.5);
}
.pan-item:hover .pan-thumb {
transform: rotate(-110deg);
}
.pan-item:hover .pan-info p a {
opacity: 1;
transform: translateX(0px) rotate(0deg);
}
</style>

View File

@@ -0,0 +1,3 @@
<template >
<router-view />
</template>

View File

@@ -0,0 +1,181 @@
## 界面布局结构
- palette(工具栏) :提供拖拽工具、框选工具、连线工具、基本图元等
- contextPad(上下文面板):可以理解为快捷面板
- propertiesPanel(属性面板):定义流程图中图形元素属性
- shape(图形) 是所有图形的基类比如ConnectionRoot
## 导入与导出
````
## 导入
// 异步方式(推荐)
let result = await bpmnModeler.importXML(xml)
// 回调方式
bpmnModeler.importXML(xml, (result) => {} )
### 导出xml
// 异步方式
let { xml } = await bpmnModeler.saveXML()
// 回调方式
bpmnModeler.saveXML({ format: false },({ xml }) => {})
// 格式化导出的xml
let { xml } = await bpmnModeler.saveXML({ format: true })
### 导出svg
// 异步方式
let { svg } = await bpmnModeler.saveSVG()
// 回调方式
bpmnModeler.saveXML(( { svg } )=>{ })
## 导入的生命周期事件如下:
import.parse.start (即将从xml读取模型)
import.parse.complete (模型读取完成)
import.render.start (图形导入开始)
import.render.complete (图形导入完成)
import.done (一切都完成)
````
## 内部模块/供应商/服务
- eventBus - 事件总线管理bpmn实例中所有事件
- canvas - 画布管理svg元素、连线/图形的添加/删除、缩放等
- commandStack - 命令堆栈管理bpmn内部所有命令操作提供撤销、重做功能等
- elementRegistry - 元素注册表管理bpmn内部所有元素
- moddle - 模型管理用于管理bpmn的xml结构
- modeling - 建模器,绘图时用到,提供用于更新画布上元素的 API移动、删除
````
## 获取一个模块
// 第一个参数为模块名称,第二参数表示是否严格模式
bpmnModeler.get("模块名称",false)
````
## 事件总线 - eventBus
````
## 获取事件总线模块
let eventBus = bpmnModeler.get("eventBus")
## 监听事件
// 监听事件
eventBus.on('element.changed', (ev) => {})
// 监听多个事件
eventBus.on(
['shape.added', 'connection.added', 'shape.removed', 'connection.removed'],
(ev) => {
}
)
// 设置优先级
eventBus.on('element.changed', 100, (ev) => {})
// 传入上下文
eventBus.on('element.changed', (ev) => {}, that)
// 使用所有参数
eventBus.on('事件名称', 优先级(可选), 回调函数, 上下文(可选))
## 只监听一次事件
// 用法同on
eventBus.once('事件名称', 优先级(可选), 回调函数, 上下文(可选))
## 取消监听事件
// 取消监听
eventBus.off('element.changed', callback)
// 取消监听多个事件
eventBus.off(['shape.added', 'connection.added', 'shape.removed', 'connection.removed'], callback)
## 触发事件
eventBus.fire('element.changed', data)
````
## 画布 - canvas
````
## 获取画布模块
let canvas = bpmnModeler.get("canvas")
## 缩放
/**
*
* @param {'fit-viewport' | 'fit-content' | number} lvl
* @param {'auto'|{ x: number, y: number }} center
*/
function zoom(lvl, center) {
let canvas = bpmnModeler.get('canvas')
canvas.zoom(lvl, center)
}
// 适应容器缩放
zoom('fit-canvas','auto')
// 完全显示内容
zoom('fit-content','auto')
## 对齐选择多个元素使用shift+鼠标左键)
/**
* 获取当前选集并对齐
* @param {'left'|'right'|'top'|'bottom'|'middle'|'center'} mode
*/
function align(mode) {
const align = bpmnModeler.get('alignElements')
const selection = bpmnModeler.get('selection')
const elements = selection.get()
if (!elements || elements.length === 0) {
return
}
align.trigger(elements, mode)
}
````
## 元素注册表 - elementRegistry
````
## 获取元素注册表模块
let elementRegistry = bpmnModeler.get('elementRegistry')
## 遍历所有元素
elementRegistry.forEach((shape, svgElement) => { })
## 获取指定元素
let shape = elementRegistry.get(元素id或者SVGElement)
## 获取过滤后的元素
let shapes = elementRegistry.filter((shape) => shape.type === 'bpmn:Task')
## 更新元素ID
elementRegistry.updateId(shape, "123xxxxsssd")
## 删除一个元素
elementRegistry .remove(传入SVGElement)
## 模型 - moddle
基本上没有用到,具体类型定义见此
## 建模器 - modeling
获取建模器模块
let modeling= bpmnModeler.get('modeling')
## 修改元素显示文本(常用)
modeling.updateLabel(shape, '审核')
## 修改元素属性(常用)
modeling.updateProperties(shape, { 属性名称: 属性值 })
## 对齐元素集合
const selection = bpmnModeler.get('selection')
const elements = selection.get()
modeling.updateProperties(selection, 'left')
````

View File

@@ -0,0 +1,120 @@
import { NodeName } from '../lang/zh'
// 创建监听器实例
export function createListenerObject(moddle, options, isTask, prefix) {
const listenerObj = Object.create(null);
listenerObj.event = options.event;
isTask && (listenerObj.id = options.id); // 任务监听器特有的 id 字段
switch (options.listenerType) {
case "scriptListener":
listenerObj.script = createScriptObject(moddle, options, prefix);
break;
case "expressionListener":
listenerObj.expression = options.expression;
break;
case "delegateExpressionListener":
listenerObj.delegateExpression = options.delegateExpression;
break;
default:
listenerObj.class = options.class;
}
// 注入字段
if (options.fields) {
listenerObj.fields = options.fields.map(field => {
return createFieldObject(moddle, field, prefix);
});
}
// 任务监听器的 定时器 设置
if (isTask && options.event === "timeout" && !!options.eventDefinitionType) {
const timeDefinition = moddle.create("bpmn:FormalExpression", {
body: options.eventTimeDefinitions
});
const TimerEventDefinition = moddle.create("bpmn:TimerEventDefinition", {
id: `TimerEventDefinition_${uuid(8)}`,
[`time${options.eventDefinitionType.replace(/^\S/, s => s.toUpperCase())}`]: timeDefinition
});
listenerObj.eventDefinitions = [TimerEventDefinition];
}
return moddle.create(`${prefix}:${isTask ? "TaskListener" : "ExecutionListener"}`, listenerObj);
}
// 处理内置流程监听器
export function createSystemListenerObject(moddle, options, isTask, prefix) {
const listenerObj = Object.create(null);
listenerObj.event = options.eventType;
listenerObj.listenerType = options.valueType;
switch (options.valueType) {
case "scriptListener":
listenerObj.script = createScriptObject(moddle, options, prefix);
break;
case "expressionListener":
listenerObj.expression = options.expression;
break;
case "delegateExpressionListener":
listenerObj.delegateExpression = options.delegateExpression;
break;
default:
listenerObj.class = options.value;
}
return moddle.create(`${prefix}:${isTask ? "TaskListener" : "ExecutionListener"}`, listenerObj);
}
// 转换成字段
export function changeListenerObject(options) {
const listenerObj = Object.create(null);
listenerObj.event = options.eventType;
listenerObj.listenerType = options.valueType;
switch (options.valueType) {
case "scriptListener":
// listenerObj.script = createScriptObject(moddle, options, prefix);
break;
case "expressionListener":
listenerObj.expression = options.expression;
break;
case "delegateExpressionListener":
listenerObj.delegateExpression = options.delegateExpression;
break;
default:
listenerObj.class = options.value;
}
return listenerObj;
}
// 创建 监听器的注入字段 实例
export function createFieldObject(moddle, option, prefix) {
const { name, fieldType, string, expression } = option;
const fieldConfig = fieldType === "string" ? { name, string } : { name, expression };
return moddle.create(`${prefix}:Field`, fieldConfig);
}
// 创建脚本实例
export function createScriptObject(moddle, options, prefix) {
const { scriptType, scriptFormat, value, resource } = options;
const scriptConfig = scriptType === "inlineScript" ? { scriptFormat, value } : { scriptFormat, resource };
return moddle.create(`${prefix}:Script`, scriptConfig);
}
// 更新元素扩展属性
export function updateElementExtensions(moddle, modeling, element, extensionList) {
const extensions = moddle.create("bpmn:ExtensionElements", {
values: extensionList
});
modeling.updateProperties(element, {
extensionElements: extensions
});
}
// 创建一个id
export function uuid(length = 8, chars) {
let result = "";
let charsString = chars || "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (let i = length; i > 0; --i) {
result += charsString[Math.floor(Math.random() * charsString.length)];
}
return result;
}
// 转换流程节点名称
export function translateNodeName(node){
return NodeName[node];
}

View File

@@ -0,0 +1,18 @@
// 全局流程相关变量
const modelerStore = {
'userList': [],
'roleList': [],
'expList': [],
'modeler': null,
'modeling': null,
'moddle': null,
'canvas': null,
'bpmnFactory': null,
'elRegistry': null,
'element': null,
}
export default
{
modelerStore,
}

View File

@@ -0,0 +1,12 @@
import inherits from "inherits";
import Viewer from "bpmn-js/lib/Viewer";
import ZoomScrollModule from "diagram-js/lib/navigation/zoomscroll";
import MoveCanvasModule from "diagram-js/lib/navigation/movecanvas";
function CustomViewer(options) {
Viewer.call(this, options);
}
inherits(CustomViewer, Viewer);
CustomViewer.prototype._modules = [].concat(Viewer.prototype._modules, [ZoomScrollModule, MoveCanvasModule]);
export {
CustomViewer
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,527 @@
/**
* 自定义任务弹窗节点
*/
import {
assign,
forEach,
isArray,
every
} from 'min-dash';
import {
is
} from 'bpmn-js/lib/util/ModelUtil';
import {
isExpanded,
isEventSubProcess
} from 'bpmn-js/lib/util/DiUtil';
import {
isAny
} from 'bpmn-js/lib/features/modeling/util/ModelingUtil';
import {
getChildLanes
} from 'bpmn-js/lib/features/modeling/util/LaneUtil';
import {
hasPrimaryModifier
} from 'diagram-js/lib/util/Mouse';
/**
* A provider for BPMN 2.0 elements context pad
*/
export default function ContextPadProvider(
config, injector, eventBus,
contextPad, modeling, elementFactory,
connect, create, popupMenu,
canvas, rules, translate) {
config = config || {};
contextPad.registerProvider(this);
this._contextPad = contextPad;
this._modeling = modeling;
this._elementFactory = elementFactory;
this._connect = connect;
this._create = create;
this._popupMenu = popupMenu;
this._canvas = canvas;
this._rules = rules;
this._translate = translate;
if (config.autoPlace !== false) {
this._autoPlace = injector.get('autoPlace', false);
}
eventBus.on('create.end', 250, function(event) {
var context = event.context,
shape = context.shape;
if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) {
return;
}
var entries = contextPad.getEntries(shape);
if (entries.replace) {
entries.replace.action.click(event, shape);
}
});
}
ContextPadProvider.$inject = [
'config.contextPad',
'injector',
'eventBus',
'contextPad',
'modeling',
'elementFactory',
'connect',
'create',
'popupMenu',
'canvas',
'rules',
'translate'
];
ContextPadProvider.prototype.getMultiElementContextPadEntries = function(elements) {
var modeling = this._modeling;
var actions = {};
if (this._isDeleteAllowed(elements)) {
assign(actions, {
'delete': {
group: 'edit',
className: 'bpmn-icon-trash',
title: this._translate('Remove'),
action: {
click: function(event, elements) {
modeling.removeElements(elements.slice());
}
}
}
});
}
return actions;
};
/**
* @param {djs.model.Base[]} elements
* @return {boolean}
*/
ContextPadProvider.prototype._isDeleteAllowed = function(elements) {
var baseAllowed = this._rules.allowed('elements.delete', {
elements: elements
});
if (isArray(baseAllowed)) {
return every(baseAllowed, function(element) {
return includes(baseAllowed, element);
});
}
return baseAllowed;
};
ContextPadProvider.prototype.getContextPadEntries = function(element) {
var contextPad = this._contextPad,
modeling = this._modeling,
elementFactory = this._elementFactory,
connect = this._connect,
create = this._create,
popupMenu = this._popupMenu,
rules = this._rules,
autoPlace = this._autoPlace,
translate = this._translate;
var actions = {};
if (element.type === 'label') {
return actions;
}
var businessObject = element.businessObject;
function startConnect(event, element) {
connect.start(event, element);
}
function removeElement(e, element) {
modeling.removeElements([ element ]);
}
function getReplaceMenuPosition(element) {
var Y_OFFSET = 5;
var pad = contextPad.getPad(element).html;
var padRect = pad.getBoundingClientRect();
var pos = {
x: padRect.left,
y: padRect.bottom + Y_OFFSET
};
return pos;
}
/**
* Create an append action
*
* @param {string} type
* @param {string} className
* @param {string} [title]
* @param {Object} [options]
*
* @return {Object} descriptor
*/
function appendAction(type, className, title, options) {
if (typeof title !== 'string') {
options = title;
title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') });
}
function appendStart(event, element) {
var shape = elementFactory.createShape(assign({ type: type }, options));
create.start(event, shape, {
source: element
});
}
var append = autoPlace ? function(event, element) {
var shape = elementFactory.createShape(assign({ type: type }, options));
autoPlace.append(element, shape);
} : appendStart;
return {
group: 'model',
className: className,
title: title,
action: {
dragstart: appendStart,
click: append
}
};
}
function splitLaneHandler(count) {
return function(event, element) {
// actual split
modeling.splitLane(element, count);
// refresh context pad after split to
// get rid of split icons
contextPad.open(element, true);
};
}
if (isAny(businessObject, [ 'bpmn:Lane', 'bpmn:Participant' ]) && isExpanded(element)) {
var childLanes = getChildLanes(element);
assign(actions, {
'lane-insert-above': {
group: 'lane-insert-above',
className: 'bpmn-icon-lane-insert-above',
title: translate('Add Lane above'),
action: {
click: function(event, element) {
modeling.addLane(element, 'top');
}
}
}
});
if (childLanes.length < 2) {
if (element.height >= 120) {
assign(actions, {
'lane-divide-two': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-two',
title: translate('Divide into two Lanes'),
action: {
click: splitLaneHandler(2)
}
}
});
}
if (element.height >= 180) {
assign(actions, {
'lane-divide-three': {
group: 'lane-divide',
className: 'bpmn-icon-lane-divide-three',
title: translate('Divide into three Lanes'),
action: {
click: splitLaneHandler(3)
}
}
});
}
}
assign(actions, {
'lane-insert-below': {
group: 'lane-insert-below',
className: 'bpmn-icon-lane-insert-below',
title: translate('Add Lane below'),
action: {
click: function(event, element) {
modeling.addLane(element, 'bottom');
}
}
}
});
}
if (is(businessObject, 'bpmn:FlowNode')) {
if (is(businessObject, 'bpmn:EventBasedGateway')) {
assign(actions, {
'append.receive-task': appendAction(
'bpmn:ReceiveTask',
'bpmn-icon-receive-task',
translate('Append ReceiveTask')
),
'append.message-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-message',
translate('Append MessageIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:MessageEventDefinition' }
),
'append.timer-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-timer',
translate('Append TimerIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:TimerEventDefinition' }
),
'append.condition-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-condition',
translate('Append ConditionIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:ConditionalEventDefinition' }
),
'append.signal-intermediate-event': appendAction(
'bpmn:IntermediateCatchEvent',
'bpmn-icon-intermediate-event-catch-signal',
translate('Append SignalIntermediateCatchEvent'),
{ eventDefinitionType: 'bpmn:SignalEventDefinition' }
)
});
} else
if (isEventType(businessObject, 'bpmn:BoundaryEvent', 'bpmn:CompensateEventDefinition')) {
assign(actions, {
'append.compensation-activity':
appendAction(
'bpmn:Task',
'bpmn-icon-task',
translate('Append compensation activity'),
{
isForCompensation: true
}
)
});
} else
if (!is(businessObject, 'bpmn:EndEvent') &&
!businessObject.isForCompensation &&
!isEventType(businessObject, 'bpmn:IntermediateThrowEvent', 'bpmn:LinkEventDefinition') &&
!isEventSubProcess(businessObject)) {
assign(actions, {
'append.end-event': appendAction(
'bpmn:EndEvent',
'bpmn-icon-end-event-none',
translate('Append EndEvent')
),
'append.gateway': appendAction(
'bpmn:ExclusiveGateway',
'bpmn-icon-gateway-none',
translate('Append Gateway')
),
'append.append-user-task': appendAction(
'bpmn:UserTask',
'bpmn-icon-user-task',
'添加用户任务'
),
'append.intermediate-event': appendAction(
'bpmn:IntermediateThrowEvent',
'bpmn-icon-intermediate-event-none',
translate('Append Intermediate/Boundary Event')
)
});
}
}
if (!popupMenu.isEmpty(element, 'bpmn-replace')) {
// Replace menu entry
assign(actions, {
'replace': {
group: 'edit',
className: 'bpmn-icon-screw-wrench',
title: translate('Change type'),
action: {
click: function(event, element) {
var position = assign(getReplaceMenuPosition(element), {
cursor: { x: event.x, y: event.y }
});
popupMenu.open(element, 'bpmn-replace', position, {
title: translate('Change element'),
width: 300,
search: true
});
}
}
}
});
}
if (is(businessObject, 'bpmn:SequenceFlow')) {
assign(actions, {
'append.text-annotation': appendAction(
'bpmn:TextAnnotation',
'bpmn-icon-text-annotation'
)
});
}
if (
isAny(businessObject, [
'bpmn:FlowNode',
'bpmn:InteractionNode',
'bpmn:DataObjectReference',
'bpmn:DataStoreReference',
])
) {
assign(actions, {
'append.text-annotation': appendAction(
'bpmn:TextAnnotation',
'bpmn-icon-text-annotation'
),
'connect': {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate(
'Connect using ' +
(businessObject.isForCompensation
? ''
: 'Sequence/MessageFlow or ') +
'Association'
),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (is(businessObject, 'bpmn:TextAnnotation')) {
assign(actions, {
'connect': {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate('Connect using Association'),
action: {
click: startConnect,
dragstart: startConnect,
},
},
});
}
if (isAny(businessObject, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ])) {
assign(actions, {
'connect': {
group: 'connect',
className: 'bpmn-icon-connection-multi',
title: translate('Connect using DataInputAssociation'),
action: {
click: startConnect,
dragstart: startConnect
}
}
});
}
if (is(businessObject, 'bpmn:Group')) {
assign(actions, {
'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation')
});
}
// delete element entry, only show if allowed by rules
var deleteAllowed = rules.allowed('elements.delete', { elements: [ element ] });
if (isArray(deleteAllowed)) {
// was the element returned as a deletion candidate?
deleteAllowed = deleteAllowed[0] === element;
}
if (deleteAllowed) {
assign(actions, {
'delete': {
group: 'edit',
className: 'bpmn-icon-trash',
title: translate('Remove'),
action: {
click: removeElement
}
}
});
}
return actions;
};
// helpers /////////
function isEventType(eventBo, type, definition) {
var isType = eventBo.$instanceOf(type);
var isDefinition = false;
var definitions = eventBo.eventDefinitions || [];
forEach(definitions, function(def) {
if (def.$type === definition) {
isDefinition = true;
}
});
return isType && isDefinition;
}
function includes(array, item) {
return array.indexOf(item) !== -1;
}

View File

@@ -0,0 +1,145 @@
/**
* 自定义左侧工具栏
*/
import { assign } from "min-dash";
export default function CustomPalette(
palette,
create,
elementFactory,
handTool,
lassoTool,
spaceTool,
globalConnect,
translate
) {
this.create = create;
this.elementFactory = elementFactory;
this.handTool = handTool;
this.lassoTool = lassoTool;
this.spaceTool = spaceTool;
this.globalConnect = globalConnect;
this.translate = translate;
palette.registerProvider(this);
}
CustomPalette.$inject = [
"palette",
"create",
"elementFactory",
"handTool",
"lassoTool",
"spaceTool",
"globalConnect",
"translate"
];
CustomPalette.prototype.getPaletteEntries = function (element) {
const {
create,
elementFactory,
handTool,
lassoTool,
spaceTool,
globalConnect,
translate
} = this;
function createAction(type, group, className, title, options) {
function createListener(event) {
var shape = elementFactory.createShape(assign({ type: type }, options));
if (options) {
shape.businessObject.di.isExpanded = options.isExpanded;
}
create.start(event, shape);
}
var shortType = type.replace(/^bpmn:/, "");
return {
group: group,
className: className,
title: title || translate("Create {type}", { type: shortType }),
action: {
dragstart: createListener,
click: createListener
}
};
}
return {
'hand-tool': {
group: 'tools',
className: 'bpmn-icon-hand-tool',
title: '激活抓手工具',
action: {
click: function(event) {
handTool.activateHand(event);
}
}
},
"lasso-tool": {
group: "tools",
className: "bpmn-icon-lasso-tool",
title: "激活套索工具",
action: {
click: function (event) {
lassoTool.activateSelection(event);
}
}
},
'space-tool': {
group: 'tools',
className: 'bpmn-icon-space-tool',
title: translate('Activate the create/remove space tool'),
action: {
click: function(event) {
spaceTool.activateSelection(event);
}
}
},
'global-connect-tool': {
group: 'tools',
className: 'bpmn-icon-connection-multi',
title: translate('Activate the global connect tool'),
action: {
click: function(event) {
globalConnect.start(event);
}
}
},
"tool-separator": {
group: "tools",
separator: true
},
"create.start-event": createAction(
"bpmn:StartEvent",
"event",
"bpmn-icon-start-event-none",
"创建开始节点"
),
"create.end-event": createAction(
"bpmn:EndEvent",
"event",
"bpmn-icon-end-event-none",
"创建结束节点"
),
"create.user-task": createAction(
"bpmn:UserTask",
"activity",
"bpmn-icon-user-task",
"创建用户任务"
),
"create.exclusive-gateway": createAction(
"bpmn:ExclusiveGateway",
"gateway",
"bpmn-icon-gateway-xor",
"创建排他网关"
)
};
};

View File

@@ -0,0 +1,32 @@
import translations from '../lang/zh'
// export default function customTranslate(template, replacements) {
// replacements = replacements || {}
//
// // Translate
// template = translations[template] || template
//
// // Replace
// return template.replace(/{([^}]+)}/g, function(_, key) {
// var str = replacements[key]
// if (
// translations[replacements[key]] !== null &&
// translations[replacements[key]] !== 'undefined'
// ) {
// str = translations[replacements[key]]
// }
// return str || '{' + key + '}'
// })
// }
export default function customTranslate(template, replacements) {
replacements = replacements || {};
// Translate
template = translations[template] || template;
// Replace
return template.replace(/{([^}]+)}/g, function(_, key) {
return replacements[key] || '{' + key + '}';
});
}

View File

@@ -0,0 +1,8 @@
import CustomContextPad from './CustomContextPad';
import CustomPalette from "./CustomPalette";
export default {
__init__: [ 'paletteProvider','contextPadProvider'],
paletteProvider: [ 'type', CustomPalette ],
contextPadProvider: [ 'type', CustomContextPad ],
};

View File

@@ -0,0 +1,187 @@
<template>
<div>
<template slot="header">
<div class="card-header">
<span>{{ translateNodeName(elementType) }}</span>
</div>
</template>
<el-collapse v-model="activeName" >
<!-- 常规信息 -->
<el-collapse-item name="common">
<template slot="title"><i class="el-icon-info"></i> 常规信息</template>
<common-panel :id="elementId"/>
</el-collapse-item>
<!-- 任务信息 -->
<el-collapse-item name="Task" v-if="elementType.indexOf('Task') !== -1">
<template slot="title"><i class="el-icon-s-claim"></i> 任务配置</template>
<user-task-panel :id="elementId"/>
</el-collapse-item>
<!-- 表单 -->
<el-collapse-item name="form" v-if="formVisible">
<template slot="title"><i class="el-icon-s-order"></i> 表单配置</template>
<form-panel :id="elementId"/>
</el-collapse-item>
<!-- 执行监听器 -->
<el-collapse-item name="executionListener">
<template slot="title"><i class="el-icon-s-promotion"></i> 执行监听器
<el-badge :value="executionListenerCount" class="item" type="primary"/>
</template>
<execution-listener :id="elementId" @getExecutionListenerCount="getExecutionListenerCount"/>
</el-collapse-item>
<!-- 任务监听器 -->
<el-collapse-item name="taskListener" v-if="elementType === 'UserTask'" >
<template slot="title"><i class="el-icon-s-flag"></i> 任务监听器
<el-badge :value="taskListenerCount" class="item" type="primary"/>
</template>
<task-listener :id="elementId" @getTaskListenerCount="getTaskListenerCount"/>
</el-collapse-item>
<!-- 多实例 -->
<el-collapse-item name="multiInstance" v-if="elementType.indexOf('Task') !== -1" >
<template slot="title"><i class="el-icon-s-grid"></i> 多实例</template>
<multi-instance :id="elementId"/>
</el-collapse-item>
<!-- 流转条件 -->
<el-collapse-item name="condition" v-if="conditionVisible" >
<template slot="title"><i class="el-icon-share"></i> 流转条件</template>
<condition-panel :id="elementId"/>
</el-collapse-item>
<!-- 扩展属性 -->
<el-collapse-item name="properties" >
<template slot="title"><i class="el-icon-circle-plus"></i> 扩展属性</template>
<properties-panel :id="elementId"/>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script>
import ExecutionListener from './panel/executionListener'
import TaskListener from './panel/taskListener'
import MultiInstance from './panel/multiInstance'
import CommonPanel from './panel/commonPanel'
import UserTaskPanel from './panel/taskPanel'
import ConditionPanel from './panel/conditionPanel'
import FormPanel from './panel/formPanel'
import OtherPanel from './panel/otherPanel'
import PropertiesPanel from './panel/PropertiesPanel'
import { translateNodeName } from "./common/bpmnUtils";
import FlowUser from "@/components/flow/User/index.vue";
import FlowRole from "@/components/flow/Role/index.vue";
import FlowExp from "@/components/flow/Expression/index.vue";
export default {
name: "Designer",
components: {
ExecutionListener,
TaskListener,
MultiInstance,
CommonPanel,
UserTaskPanel,
ConditionPanel,
FormPanel,
OtherPanel,
PropertiesPanel,
FlowUser,
FlowRole,
FlowExp,
},
data() {
return {
activeName : 'common',
executionListenerCount: 0,
taskListenerCount:0,
elementId:"",
elementType:"",
conditionVisible:false,// 流转条件设置
formVisible:false, // 表单配置
rules:{
id: [
{ required: true, message: '节点Id 不能为空', trigger: 'blur' },
],
name: [
{ required: true, message: '节点名称不能为空', trigger: 'blur' },
],
},
}
},
/** 传值监听 */
watch: {
elementId: {
handler() {
this.activeName = "common";
}
},
},
created() {
this.initModels();
},
methods: {
// 初始化流程设计器
initModels() {
this.getActiveElement();
},
// 注册节点事件
getActiveElement() {
// 初始第一个选中元素 bpmn:Process
this.initFormOnChanged(null);
this.modelerStore.modeler.on("import.done", e => {
this.initFormOnChanged(null);
});
// 监听选择事件,修改当前激活的元素以及表单
this.modelerStore.modeler.on("selection.changed", ({newSelection}) => {
this.initFormOnChanged(newSelection[0] || null);
});
this.modelerStore.modeler.on("element.changed", ({element}) => {
// 保证 修改 "默认流转路径" 类似需要修改多个元素的事件发生的时候,更新表单的元素与原选中元素不一致。
if (element && element.id === this.elementId) {
this.initFormOnChanged(element);
}
});
},
// 初始化数据
initFormOnChanged(element) {
let activatedElement = element;
if (!activatedElement) {
activatedElement =
this.modelerStore.elRegistry.find(el => el.type === "bpmn:Process") ??
this.modelerStore.elRegistry.find(el => el.type === "bpmn:Collaboration");
}
if (!activatedElement) return;
this.modelerStore.element = activatedElement;
this.elementId = activatedElement.id;
this.elementType = activatedElement.type.split(":")[1] || "";
this.conditionVisible = !!(
this.elementType === "SequenceFlow" &&
activatedElement.source &&
activatedElement.source.type.indexOf("StartEvent") === -1
);
this.formVisible = this.elementType === "UserTask" || this.elementType === "StartEvent";
},
/** 获取执行监听器数量 */
getExecutionListenerCount(value) {
this.executionListenerCount = value;
},
/** 获取任务监听器数量 */
getTaskListenerCount(value) {
this.taskListenerCount = value;
},
translateNodeName(val){
return translateNodeName(val);
}
}
}
</script>
<style lang="scss">
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
function randomStr() {
return Math.random().toString(36).slice(-8)
}
export default function() {
return `<?xml version="1.0" encoding="UTF-8"?>
<definitions
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:bioc="http://bpmn.io/schema/bpmn/biocolor/1.0"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="http://www.flowable.org/processdef"
>
<process id="flow_${randomStr()}" name="flow_${randomStr()}">
<startEvent id="start_event" name="开始" />
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_flow">
<bpmndi:BPMNPlane id="BPMNPlane_flow" bpmnElement="T-2d89e7a3-ba79-4abd-9f64-ea59621c258c">
<bpmndi:BPMNShape id="BPMNShape_start_event" bpmnElement="start_event" bioc:stroke="">
<omgdc:Bounds x="240" y="200" width="30" height="30" />
<bpmndi:BPMNLabel>
<omgdc:Bounds x="242" y="237" width="23" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
`
}

View File

@@ -0,0 +1,352 @@
<template>
<div v-loading="isView" class="flow-containers" :class="{ 'view-mode': isView }">
<el-container style="height: 100%">
<el-header style="border-bottom: 1px solid rgb(218 218 218);height: auto;padding-left:0">
<div style="display: flex; padding: 10px 0; justify-content: space-between;">
<el-button-group>
<el-upload action="" :before-upload="openBpmn" style="margin-right: 10px; display:inline-block;">
<el-tooltip effect="dark" content="加载xml" placement="bottom">
<el-button size="mini" icon="el-icon-folder-opened" />
</el-tooltip>
</el-upload>
<el-tooltip effect="dark" content="新建" placement="bottom">
<el-button size="mini" icon="el-icon-circle-plus" @click="newDiagram" />
</el-tooltip>
<el-tooltip effect="dark" content="自适应屏幕" placement="bottom">
<el-button size="mini" icon="el-icon-rank" @click="fitViewport" />
</el-tooltip>
<el-tooltip effect="dark" content="放大" placement="bottom">
<el-button size="mini" icon="el-icon-zoom-in" @click="zoomViewport(true)" />
</el-tooltip>
<el-tooltip effect="dark" content="缩小" placement="bottom">
<el-button size="mini" icon="el-icon-zoom-out" @click="zoomViewport(false)" />
</el-tooltip>
<el-tooltip effect="dark" content="后退" placement="bottom">
<el-button size="mini" icon="el-icon-back" @click="modeler.get('commandStack').undo()" />
</el-tooltip>
<el-tooltip effect="dark" content="前进" placement="bottom">
<el-button size="mini" icon="el-icon-right" @click="modeler.get('commandStack').redo()" />
</el-tooltip>
<!-- <el-button size="mini" icon="el-icon-share" @click="processSimulation">-->
<!-- {{ this.simulationStatus ? '退出模拟' : '开启模拟' }}-->
<!-- </el-button>-->
<!-- <el-button size="mini" icon="el-icon-first-aid-kit" @click="handlerIntegrityCheck">-->
<!-- {{ this.bpmnlintStatus ? '关闭检查' : '开启检查' }}-->
<!-- </el-button>-->
</el-button-group>
<el-button-group>
<el-button size="mini" icon="el-icon-view" @click="showXML">查看xml</el-button>
<el-button size="mini" icon="el-icon-download" @click="saveXML(true)">下载xml</el-button>
<el-button size="mini" icon="el-icon-picture" @click="saveImg('svg', true)">下载svg</el-button>
<el-button size="mini" type="primary" @click="save">保存模型</el-button>
<el-button size="mini" type="danger" @click="goBack">关闭</el-button>
</el-button-group>
</div>
</el-header>
<!-- 流程设计页面 -->
<el-container style="align-items: stretch">
<el-main>
<div ref="canvas" class="canvas" />
</el-main>
<!--右侧属性栏-->
<el-card shadow="never" class="normalPanel">
<designer v-if="loadCanvas"></designer>
</el-card>
</el-container>
</el-container>
</div>
</template>
<script>
// 汉化
import customTranslate from './customPanel/customTranslate'
import Modeler from 'bpmn-js/lib/Modeler'
import Designer from './designer'
import getInitStr from './flowable/init'
import {StrUtil} from '@/utils/StrUtil'
// 引入flowable的节点文件
import FlowableModule from './flowable/flowable.json'
import customControlsModule from './customPanel'
export default {
name: "BpmnModel",
components: {Designer},
/** 组件传值 */
props : {
xml: {
type: String,
default: ''
},
isView: {
type: Boolean,
default: false
},
},
data() {
return {
modeler: null,
zoom: 1,
loadCanvas: false, // 当前组件渲染然后再加载canvas
simulationStatus: false,
bpmnlintStatus: false,
simulation: true,
designer: true,
}
},
/** 传值监听 */
watch: {
xml: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.createNewDiagram(newVal)
} else {
this.newDiagram()
}
},
immediate: true, // 立即生效
},
},
computed: {
additionalModules() {
const Modules = [];
Modules.push(customControlsModule);
Modules.push({ //汉化
translate: ['value', customTranslate]
});
return Modules;
},
},
mounted() {
/** 创建bpmn 实例 */
const modeler = new Modeler({
container: this.$refs.canvas,
additionalModules: this.additionalModules,
moddleExtensions: {
flowable: FlowableModule
},
keyboard: { bindTo: document },
})
this.modeler = modeler;
// 注册 modeler 相关信息
this.modelerStore.modeler = modeler;
this.modelerStore.modeling = modeler.get("modeling");
this.modelerStore.moddle = modeler.get("moddle");
this.modelerStore.canvas = modeler.get("canvas");
this.modelerStore.bpmnFactory = modeler.get("bpmnFactory");
this.modelerStore.elRegistry = modeler.get("elementRegistry");
// 直接点击新建按钮时,进行新增流程图
if (StrUtil.isBlank(this.xml)) {
this.newDiagram()
} else {
this.createNewDiagram(this.xml)
}
},
methods: {
// 根据默认文件初始化流程图
newDiagram() {
this.createNewDiagram(getInitStr())
},
// 根据提供的xml创建流程图
async createNewDiagram(data) {
// 将字符串转换成图显示出来
// data = data.replace(/<!\[CDATA\[(.+?)]]>/g, '&lt;![CDATA[$1]]&gt;')
if (StrUtil.isNotBlank(this.modelerStore.modeler)) {
data = data.replace(/<!\[CDATA\[(.+?)]]>/g, function (match, str) {
return str.replace(/</g, '&lt;')
}
)
try {
await this.modelerStore.modeler.importXML(data)
this.fitViewport()
} catch (err) {
console.error(err.message, err.warnings)
}
}
},
// 让图能自适应屏幕
fitViewport() {
this.zoom = this.modelerStore.canvas.zoom('fit-viewport')
const bbox = document.querySelector('.flow-containers .viewport').getBBox()
const currentViewBox = this.modelerStore.canvas.viewbox()
const elementMid = {
x: bbox.x + bbox.width / 2 - 65,
y: bbox.y + bbox.height / 2
}
this.modelerStore.canvas.viewbox({
x: elementMid.x - currentViewBox.width / 2,
y: elementMid.y - currentViewBox.height / 2,
width: currentViewBox.width,
height: currentViewBox.height
})
this.zoom = bbox.width / currentViewBox.width * 1.8
this.loadCanvas = true;
},
// 放大缩小
zoomViewport(zoomIn = true) {
this.zoom = this.modelerStore.canvas.zoom()
this.zoom += (zoomIn ? 0.1 : -0.1)
this.modelerStore.canvas.zoom(this.zoom)
},
// 获取流程基础信息
getProcess() {
const element = this.getProcessElement()
return {
id: element.id,
name: element.name,
category: element.processCategory
}
},
// 获取流程主面板节点
getProcessElement() {
const rootElements = this.modelerStore.modeler.getDefinitions().rootElements
for (let i = 0; i < rootElements.length; i++) {
if (rootElements[i].$type === 'bpmn:Process') return rootElements[i]
}
},
// 保存xml
async saveXML(download = false) {
try {
const {xml} = await this.modelerStore.modeler.saveXML({format: true})
if (download) {
this.downloadFile(`${this.getProcessElement().name}.bpmn20.xml`, xml, 'application/xml')
}
return xml
} catch (err) {
console.log(err)
}
},
// 在线查看xml
async showXML() {
try {
const xmlStr = await this.saveXML()
this.$emit('showXML', xmlStr)
} catch (err) {
console.log(err)
}
},
// 保存流程图为svg
async saveImg(type = 'svg', download = false) {
try {
const {svg} = await this.modelerStore.modeler.saveSVG({format: true})
if (download) {
this.downloadFile(this.getProcessElement().name, svg, 'image/svg+xml')
}
return svg
} catch (err) {
console.log(err)
}
},
// 保存流程图
async save() {
const process = this.getProcess()
const xml = await this.saveXML()
const svg = await this.saveImg()
const result = {process, xml, svg}
this.$emit('save', result)
window.parent.postMessage(result, '*')
this.goBack();
},
// 打开流程文件
openBpmn(file) {
const reader = new FileReader()
reader.readAsText(file, 'utf-8')
reader.onload = () => {
this.createNewDiagram(reader.result)
}
return false
},
// 下载流程文件
downloadFile(filename, data, type) {
const a = document.createElement('a');
const url = window.URL.createObjectURL(new Blob([data], {type: type}));
a.href = url
a.download = filename
a.click()
window.URL.revokeObjectURL(url)
},
/** 关闭当前标签页并返回上个页面 */
goBack() {
const obj = {path: "/flowable/definition", query: {t: Date.now()}};
this.$tab.closeOpenPage(obj);
this.toggleSideBar();
},
}
}
</script>
<style lang="scss">
/*左边工具栏以及编辑节点的样式*/
@import "~bpmn-js/dist/assets/diagram-js.css";
@import "~bpmn-js/dist/assets/bpmn-font/css/bpmn.css";
@import "~bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css";
@import "~bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";
//@import "~bpmn-js-bpmnlint/dist/assets/css/bpmn-js-bpmnlint.css";
.view-mode {
.el-header, .el-aside, .djs-palette, .bjs-powered-by {
display: none;
}
.el-loading-mask {
background-color: initial;
}
.el-loading-spinner {
display: none;
}
}
.flow-containers {
width: 100%;
height: 100%;
.canvas {
min-height: 850px;
width: 100%;
height: 100%;
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+")
}
.panel {
position: absolute;
right: 0;
top: 50px;
width: 300px;
}
.load {
margin-right: 10px;
}
.normalPanel {
width: 460px;
height: 100%;
padding: 20px 20px;
}
.el-main {
position: relative;
padding: 0;
}
.el-main .button-group {
display: flex;
flex-direction: column;
position: absolute;
width: auto;
height: auto;
top: 10px;
right: 10px;
}
.button-group .el-button {
width: 100%;
margin: 0 0 5px;
}
}
</style>

View File

@@ -0,0 +1,277 @@
// https://github.com/bpmn-io/bpmn-js-i18n/blob/master/translations/zn.js
export default {
// Labels
'Activate the global connect tool': '激活全局连接工具',
'Append {type}': '添加 {type}',
'Append Task': '添加任务',
'Append Gateway': '添加网关',
'Append EndEvent': '添加结束事件',
'Append StartEvent': '添加开始事件',
"Append Intermediate/Boundary Event": '添加边界事件',
'Add Lane above': '在上面添加道',
'Divide into two Lanes': '分割成两个道',
'Divide into three Lanes': '分割成三个道',
'Add Lane below': '在下面添加道',
'Append compensation activity': '追加补偿活动',
'Change type': '修改类型',
'Connect using Association': '使用关联连接',
'Connect using Sequence/MessageFlow or Association': '使用顺序/消息流或者关联连接',
'Connect using DataInputAssociation': '使用数据输入关联连接',
'Remove': '移除',
'Activate the hand tool': '激活抓手工具',
'Activate the lasso tool': '激活套索工具',
'Activate the create/remove space tool': '激活创建/删除空间工具',
'Create expanded SubProcess': '创建扩展子过程',
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
'Create Pool/Participant': '创建池/参与者',
'Parallel Multi Instance': '并行多重事件',
'Sequential Multi Instance': '时序多重事件',
'DataObjectReference': '数据对象参考',
'DataStoreReference': '数据存储参考',
'Loop': '循环',
'Ad-hoc': '即席',
'Create {type}': '创建 {type}',
'Task': '任务',
'Send Task': '发送任务',
'Receive Task': '接收任务',
'User Task': '用户任务',
'Manual Task': '手工任务',
'Business Rule Task': '业务规则任务',
'Service Task': '服务任务',
'Script Task': '脚本任务',
'Call Activity': '调用活动',
'Sub Process (collapsed)': '子流程(折叠的)',
'Sub Process (expanded)': '子流程(展开的)',
'Start Event': '开始事件',
'StartEvent': '开始事件',
'Intermediate Throw Event': '中间事件',
'End Event': '结束事件',
'EndEvent': '结束事件',
'Create Gateway': '创建网关',
'Create Intermediate/Boundary Event': '创建中间/边界事件',
'Message Start Event': '消息开始事件',
'Timer Start Event': '定时开始事件',
'Conditional Start Event': '条件开始事件',
'Signal Start Event': '信号开始事件',
'Error Start Event': '错误开始事件',
'Escalation Start Event': '升级开始事件',
'Compensation Start Event': '补偿开始事件',
'Message Start Event (non-interrupting)': '消息开始事件(非中断)',
'Timer Start Event (non-interrupting)': '定时开始事件(非中断)',
'Conditional Start Event (non-interrupting)': '条件开始事件(非中断)',
'Signal Start Event (non-interrupting)': '信号开始事件(非中断)',
'Escalation Start Event (non-interrupting)': '升级开始事件(非中断)',
'Message Intermediate Catch Event': '消息中间捕获事件',
'Message Intermediate Throw Event': '消息中间抛出事件',
'Timer Intermediate Catch Event': '定时中间捕获事件',
'Escalation Intermediate Throw Event': '升级中间抛出事件',
'Conditional Intermediate Catch Event': '条件中间捕获事件',
'Link Intermediate Catch Event': '链接中间捕获事件',
'Link Intermediate Throw Event': '链接中间抛出事件',
'Compensation Intermediate Throw Event': '补偿中间抛出事件',
'Signal Intermediate Catch Event': '信号中间捕获事件',
'Signal Intermediate Throw Event': '信号中间抛出事件',
'Message End Event': '消息结束事件',
'Escalation End Event': '定时结束事件',
'Error End Event': '错误结束事件',
'Cancel End Event': '取消结束事件',
'Compensation End Event': '补偿结束事件',
'Signal End Event': '信号结束事件',
'Terminate End Event': '终止结束事件',
'Message Boundary Event': '消息边界事件',
'Message Boundary Event (non-interrupting)': '消息边界事件(非中断)',
'Timer Boundary Event': '定时边界事件',
'Timer Boundary Event (non-interrupting)': '定时边界事件(非中断)',
'Escalation Boundary Event': '升级边界事件',
'Escalation Boundary Event (non-interrupting)': '升级边界事件(非中断)',
'Conditional Boundary Event': '条件边界事件',
'Conditional Boundary Event (non-interrupting)': '条件边界事件(非中断)',
'Error Boundary Event': '错误边界事件',
'Cancel Boundary Event': '取消边界事件',
'Signal Boundary Event': '信号边界事件',
'Signal Boundary Event (non-interrupting)': '信号边界事件(非中断)',
'Compensation Boundary Event': '补偿边界事件',
'Exclusive Gateway': '互斥网关',
'Parallel Gateway': '并行网关',
'Inclusive Gateway': '相容网关',
'Complex Gateway': '复杂网关',
'Event based Gateway': '事件网关',
'Transaction': '转运',
'Sub Process': '子流程',
'Event Sub Process': '事件子流程',
'Collapsed Pool': '折叠池',
'Expanded Pool': '展开池',
// Errors
'no parent for {element} in {parent}': '在{parent}里,{element}没有父类',
'no shape type specified': '没有指定的形状类型',
'flow elements must be children of pools/participants': '流元素必须是池/参与者的子类',
'out of bounds release': 'out of bounds release',
'more than {count} child lanes': '子道大于{count} ',
'element required': '元素不能为空',
'diagram not part of Definitions': '流程图不符合bpmn规范',
'no diagram to display': '没有可展示的流程图',
'no process or collaboration to display': '没有可展示的流程/协作',
'element {element} referenced by {referenced}#{property} not yet drawn': '由{referenced}#{property}引用的{element}元素仍未绘制',
'already rendered {element}': '{element} 已被渲染',
'failed to import {element}': '导入{element}失败',
// 属性面板的参数
'Id': '标识',
'Name': '名称',
'General': '常规',
'Details': '详情',
'Message Name': '消息名称',
'Message': '消息',
'Initiator': '创建者',
'Asynchronous Continuations': '持续异步',
'Asynchronous Before': '异步前',
'Asynchronous After': '异步后',
'Job Configuration': '工作配置',
'Exclusive': '排除',
'Job Priority': '工作优先级',
'Retry Time Cycle': '重试时间周期',
'Documentation': '文档',
'Element Documentation': '元素文档',
'History Configuration': '历史配置',
'History Time To Live': '历史的生存时间',
'Forms': '表单',
'Form Key': '表单key',
'Form Fields': '表单字段',
'Business Key': '业务key',
'Form Field': '表单字段',
'ID': '编号',
'Type': '类型',
'Label': '名称',
'Default Value': '默认值',
'Validation': '校验',
'Add Constraint': '添加约束',
'Config': '配置',
'Properties': '属性',
'Add Property': '添加属性',
'Value': '值',
'Listeners': '监听器',
'Execution Listener': '执行监听',
'Event Type': '事件类型',
'Listener Type': '监听器类型',
'Java Class': 'Java类',
'Expression': '表达式',
'Must provide a value': '必须提供一个值',
'Delegate Expression': '代理表达式',
'Script': '脚本',
'Script Format': '脚本格式',
'Script Type': '脚本类型',
'Inline Script': '内联脚本',
'External Script': '外部脚本',
'Resource': '资源',
'Field Injection': '字段注入',
'Extensions': '扩展',
'Input/Output': '输入/输出',
'Input Parameters': '输入参数',
'Output Parameters': '输出参数',
'Parameters': '参数',
'Output Parameter': '输出参数',
'Timer Definition Type': '定时器定义类型',
'Timer Definition': '定时器定义',
'Date': '日期',
'Duration': '持续',
'Cycle': '循环',
'Signal': '信号',
'Signal Name': '信号名称',
'Escalation': '升级',
'Error': '错误',
'Link Name': '链接名称',
'Condition': '条件名称',
'Variable Name': '变量名称',
'Variable Event': '变量事件',
'Specify more than one variable change event as a comma separated list.': '多个变量事件以逗号隔开',
'Wait for Completion': '等待完成',
'Activity Ref': '活动参考',
'Version Tag': '版本标签',
'Executable': '可执行文件',
'External Task Configuration': '扩展任务配置',
'Task Priority': '任务优先级',
'External': '外部',
'Connector': '连接器',
'Must configure Connector': '必须配置连接器',
'Connector Id': '连接器编号',
'Implementation': '实现方式',
'Field Injections': '字段注入',
'Fields': '字段',
'Result Variable': '结果变量',
'Topic': '主题',
'Configure Connector': '配置连接器',
'Input Parameter': '输入参数',
'Assignee': '代理人',
'Candidate Users': '候选用户',
'Candidate Groups': '候选组',
'Due Date': '到期时间',
'Follow Up Date': '跟踪日期',
'Priority': '优先级',
'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)': '跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)': '跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
'Variables': '变量',
// 流程校验器翻译字段
'{errors} Errors, {warnings} Warnings': '{errors} 错误, {warnings} 警告',
'Element is missing label/name': '元素未设置 label 标签',
'Sequence flow is missing condition': '检查从条件分叉网关或任务节点传出的序列流是否为默认流或具有附加条件',
'is missing end event': '检查每个流程范围内是否存在结束事件',
'Start event is missing event definition': '检查流程中是否具有开始事件',
'Incoming flows do not join': '检查事件或者任务节点是否具有隐式的流转规则,默认事件和任务只能有一个流入条件',
'Element has disallowed type': '元素具有不允许的类型',
'Element is missing bpmndi': '检查可见的元素是否都具有对应的 DI 标签',
'Element is not connected': '检查节点是否正常连接',
'SequenceFlow is a duplicate': 'SequenceFlow重复',
'Duplicate outgoing sequence flows': '重复的传出序列流',
'Duplicate incoming sequence flows': '重复的传入序列流',
'Gateway forks and joins': '检查网关是否同时有多个流入和流出规则',
'Flow splits implicitly': '检查网关或者任务节点是否有多个流出路径且没有配置条件',
'has multiple blank start events': '检查流程内是否有多个开始事件',
'Event has multiple event definitions': '验证事件是否包含有超过一个事件定义的规则',
'is missing start event': '检查流程内是否存在开始事件',
'Process is missing end event': '检查流程内是否存在结束事件',
'Start event must be blank': '检查子流程的开始事件是否具有启动条件',
'Gateway is superfluous. It only has one source and target.': '检查网关是否同时只有一个流入和流出路径,否则需要移除',
}
//
// export const NodeName = {
// 'bpmn:Process': '流程',
// 'bpmn:StartEvent': '开始事件',
// 'bpmn:IntermediateThrowEvent': '中间事件',
// 'bpmn:Task': '任务',
// 'bpmn:SendTask': '发送任务',
// 'bpmn:ReceiveTask': '接收任务',
// 'bpmn:UserTask': '用户任务',
// 'bpmn:ManualTask': '手工任务',
// 'bpmn:BusinessRuleTask': '业务规则任务',
// 'bpmn:ServiceTask': '服务任务',
// 'bpmn:ScriptTask': '脚本任务',
// 'bpmn:EndEvent': '结束事件',
// 'bpmn:SequenceFlow': '流程线',
// 'bpmn:ExclusiveGateway': '互斥网关',
// 'bpmn:ParallelGateway': '并行网关',
// 'bpmn:InclusiveGateway': '相容网关',
// 'bpmn:ComplexGateway': '复杂网关',
// 'bpmn:EventBasedGateway': '事件网关'
// }
export const NodeName = {
'Process': '流程',
'StartEvent': '开始事件',
'IntermediateThrowEvent': '中间事件',
'Task': '任务',
'SendTask': '发送任务',
'ReceiveTask': '接收任务',
'UserTask': '用户任务',
'ManualTask': '手工任务',
'BusinessRuleTask': '业务规则任务',
'ServiceTask': '服务任务',
'ScriptTask': '脚本任务',
'EndEvent': '结束事件',
'SequenceFlow': '流程线',
'ExclusiveGateway': '互斥网关',
'ParallelGateway': '并行网关',
'InclusiveGateway': '相容网关',
'ComplexGateway': '复杂网关',
'EventBasedGateway': '事件网关'
}

View File

@@ -0,0 +1,140 @@
<template>
<div class="panel-tab__content">
<el-divider content-position="center">按钮设置</el-divider>
<el-table :data="elementButtonList" size="mini" max-height="240" border fit>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="属性名" prop="name" min-width="100px" show-overflow-tooltip />
<el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />
<el-table-column label="操作" width="90px">
<template slot-scope="{ row, $index }">
<el-button size="mini" type="text" @click="openAttributesForm(row, $index)">编辑</el-button>
<el-divider direction="vertical" />
<el-button size="mini" type="text" style="color: #ff4d4f" @click="removeAttributes(row, $index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button_save">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openAttributesForm(null, -1)">添加按钮</el-button>
</div>
<el-dialog :visible.sync="buttonFormModelVisible" title="按钮配置" width="600px" append-to-body destroy-on-close>
<el-form :model="buttonForm" label-width="80px" size="mini" ref="attributeFormRef" @submit.native.prevent>
<el-form-item label="属性名:" prop="label">
<el-input v-model="buttonForm.label" clearable />
</el-form-item>
<el-form-item label="属性值:" prop="value">
<el-input v-model="buttonForm.value" clearable />
</el-form-item>
</el-form>
<template slot="footer">
<el-button size="mini" @click="buttonFormModelVisible = false"> </el-button>
<el-button size="mini" type="primary" @click="saveAttribute"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import {StrUtil} from "@/utils/StrUtil";
export default {
name: "ButtonsPanel",
props: {
id: {
type: String,
required: true
},
},
data() {
return {
elementButtonList: [],
otherExtensionList: [],
buttonForm: {},
editingPropertyIndex: -1,
buttonFormModelVisible: false
};
},
watch: {
id: {
immediate: true,
handler(val) {
if (StrUtil.isNotBlank(val)) {
this.resetAttributesList();
}
}
}
},
methods: {
resetAttributesList() {
this.bpmnElement = this.modelerStore.element;
this.otherExtensionList = []; // 其他扩展配置
this.bpmnElementProperties =
this.bpmnElement.businessObject?.extensionElements?.values?.filter(ex => {
if (ex.$type !== `flowable:Buttons`) {
this.otherExtensionList.push(ex);
}
return ex.$type === `flowable:Buttons`;
}) ?? [];
// 保存所有的 扩展属性字段
this.bpmnElementButtonList = this.bpmnElementProperties.reduce((pre, current) => pre.concat(current.values), []);
// 复制 显示
this.elementButtonList = JSON.parse(JSON.stringify(this.bpmnElementButtonList ?? []));
},
openAttributesForm(attr, index) {
this.editingPropertyIndex = index;
this.buttonForm = index === -1 ? {} : JSON.parse(JSON.stringify(attr));
this.buttonFormModelVisible = true;
this.$nextTick(() => {
if (this.$refs["attributeFormRef"]) this.$refs["attributeFormRef"].clearValidate();
});
},
removeAttributes(attr, index) {
this.$confirm("确认移除该属性吗?", "提示", {
confirmButtonText: "确 认",
cancelButtonText: "取 消"
})
.then(() => {
this.elementButtonList.splice(index, 1);
this.bpmnElementButtonList.splice(index, 1);
// 新建一个属性字段的保存列表
const propertiesObject = this.modelerStore.moddle.create(`flowable:Properties`, {
values: this.bpmnElementButtonList
});
this.updateElementExtensions(propertiesObject);
this.resetAttributesList();
})
.catch(() => console.info("操作取消"));
},
saveAttribute() {
const { name, value } = this.buttonForm;
console.log(this.bpmnElementButtonList);
if (this.editingPropertyIndex !== -1) {
this.modelerStore.modeling.updateModdleProperties(this.bpmnElement, this.bpmnElementButtonList[this.editingPropertyIndex], {
name,
value
});
} else {
// 新建属性字段
const newPropertyObject = this.modelerStore.moddle.create(`flowable:Button`, { name, value });
// 新建一个属性字段的保存列表
const propertiesObject = this.modelerStore.moddle.create(`flowable:Buttons`, {
values: this.bpmnElementButtonList.concat([newPropertyObject])
});
this.updateElementExtensions(propertiesObject);
}
this.buttonFormModelVisible = false;
this.resetAttributesList();
},
updateElementExtensions(properties) {
const extensions = this.modelerStore.moddle.create("bpmn:ExtensionElements", {
values: this.otherExtensionList.concat([properties])
});
this.modelerStore.modeling.updateProperties(this.bpmnElement, {
extensionElements: extensions
});
}
}
};
</script>

View File

@@ -0,0 +1,139 @@
<template>
<div class="panel-tab__content">
<el-table :data="elementPropertyList" size="mini" max-height="240" border fit>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="属性名" prop="name" min-width="100px" show-overflow-tooltip />
<el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />
<el-table-column label="操作" width="90px">
<template slot-scope="{ row, $index }">
<el-button size="mini" type="text" @click="openAttributesForm(row, $index)">编辑</el-button>
<el-divider direction="vertical" />
<el-button size="mini" type="text" style="color: #ff4d4f" @click="removeAttributes(row, $index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openAttributesForm(null, -1)">添加属性</el-button>
</div>
<el-dialog :visible.sync="propertyFormModelVisible" title="属性配置" width="600px" append-to-body destroy-on-close>
<el-form :model="propertyForm" label-width="80px" size="mini" ref="attributeFormRef" @submit.native.prevent>
<el-form-item label="属性名:" prop="name">
<el-input v-model="propertyForm.name" clearable />
</el-form-item>
<el-form-item label="属性值:" prop="value">
<el-input v-model="propertyForm.value" clearable />
</el-form-item>
</el-form>
<template slot="footer">
<el-button size="mini" @click="propertyFormModelVisible = false"> </el-button>
<el-button size="mini" type="primary" @click="saveAttribute"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import {StrUtil} from "@/utils/StrUtil";
export default {
name: "PropertiesPanel",
props: {
id: {
type: String,
required: true
},
},
data() {
return {
elementPropertyList: [],
otherExtensionList: [],
propertyForm: {},
editingPropertyIndex: -1,
propertyFormModelVisible: false
};
},
watch: {
id: {
immediate: true,
handler(val) {
if (StrUtil.isNotBlank(val)) {
this.resetAttributesList();
}
}
}
},
methods: {
resetAttributesList() {
this.bpmnElement = this.modelerStore.element;
this.otherExtensionList = []; // 其他扩展配置
this.bpmnElementProperties =
this.bpmnElement.businessObject?.extensionElements?.values?.filter(ex => {
if (ex.$type !== `flowable:Properties`) {
this.otherExtensionList.push(ex);
}
return ex.$type === `flowable:Properties`;
}) ?? [];
// 保存所有的 扩展属性字段
this.bpmnElementPropertyList = this.bpmnElementProperties.reduce((pre, current) => pre.concat(current.values), []);
// 复制 显示
this.elementPropertyList = JSON.parse(JSON.stringify(this.bpmnElementPropertyList ?? []));
},
openAttributesForm(attr, index) {
this.editingPropertyIndex = index;
this.propertyForm = index === -1 ? {} : JSON.parse(JSON.stringify(attr));
this.propertyFormModelVisible = true;
this.$nextTick(() => {
if (this.$refs["attributeFormRef"]) this.$refs["attributeFormRef"].clearValidate();
});
},
removeAttributes(attr, index) {
this.$confirm("确认移除该属性吗?", "提示", {
confirmButtonText: "确 认",
cancelButtonText: "取 消"
})
.then(() => {
this.elementPropertyList.splice(index, 1);
this.bpmnElementPropertyList.splice(index, 1);
// 新建一个属性字段的保存列表
const propertiesObject = this.modelerStore.moddle.create(`flowable:Properties`, {
values: this.bpmnElementPropertyList
});
this.updateElementExtensions(propertiesObject);
this.resetAttributesList();
})
.catch(() => console.info("操作取消"));
},
saveAttribute() {
const { name, value } = this.propertyForm;
console.log(this.bpmnElementPropertyList);
if (this.editingPropertyIndex !== -1) {
this.modelerStore.modeling.updateModdleProperties(this.bpmnElement, this.bpmnElementPropertyList[this.editingPropertyIndex], {
name,
value
});
} else {
// 新建属性字段
const newPropertyObject = this.modelerStore.moddle.create(`flowable:Property`, { name, value });
// 新建一个属性字段的保存列表
const propertiesObject = this.modelerStore.moddle.create(`flowable:Properties`, {
values: this.bpmnElementPropertyList.concat([newPropertyObject])
});
this.updateElementExtensions(propertiesObject);
}
this.propertyFormModelVisible = false;
this.resetAttributesList();
},
updateElementExtensions(properties) {
const extensions = this.modelerStore.moddle.create("bpmn:ExtensionElements", {
values: this.otherExtensionList.concat([properties])
});
this.modelerStore.modeling.updateProperties(this.bpmnElement, {
extensionElements: extensions
});
}
}
};
</script>

View File

@@ -0,0 +1,87 @@
<template>
<div>
<el-form :model="bpmnFormData" label-width="80px" :rules="rules" size="small">
<el-form-item :label="bpmnFormData.$type === 'bpmn:Process'? '流程标识': '节点ID'" prop="id" @change="updateElementTask('id')">
<el-input v-model="bpmnFormData.id"/>
</el-form-item>
<el-form-item :label="bpmnFormData.$type === 'bpmn:Process'? '流程名称': '节点名称'" prop="name">
<el-input v-model="bpmnFormData.name" @change="updateElementTask('name')"/>
</el-form-item>
<!--流程的基础属性-->
<template v-if="bpmnFormData.$type === 'bpmn:Process'">
<el-form-item label="流程分类" prop="processCategory">
<el-select v-model="bpmnFormData.processCategory" placeholder="请选择流程分类" @change="updateElementTask('processCategory')">
<el-option
v-for="dict in dict.type.sys_process_category"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
</template>
<el-form-item v-if="bpmnFormData.$type === 'bpmn:SubProcess'" label="状态">
<el-switch v-model="bpmnFormData.isExpanded" active-text="展开" inactive-text="折叠" @change="updateElementTask('isExpanded')" />
</el-form-item>
<!-- <el-form-item label="节点描述">-->
<!-- <el-input :rows="2" type="textarea" v-model="bpmnFormData.documentation" @change="updateElementTask('documentation')"/>-->
<!-- </el-form-item>-->
</el-form>
</div>
</template>
<script>
import {StrUtil} from '@/utils/StrUtil'
export default {
name: "CommonPanel",
dicts: ['sys_process_category'],
/** 组件传值 */
props : {
id: {
type: String,
required: true
},
},
data() {
return {
rules:{
id: [
{ required: true, message: '节点Id 不能为空', trigger: 'blur' },
],
name: [
{ required: true, message: '节点名称不能为空', trigger: 'blur' },
],
},
bpmnFormData: {}
}
},
/** 传值监听 */
watch: {
id: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.resetTaskForm();
}
},
immediate: true, // 立即生效
},
},
created() {
},
methods: {
resetTaskForm() {
this.bpmnFormData = JSON.parse(JSON.stringify(this.modelerStore.element.businessObject));
},
updateElementTask(key) {
const taskAttr = Object.create(null);
taskAttr[key] = this.bpmnFormData[key] || null;
this.modelerStore.modeling.updateProperties(this.modelerStore.element, taskAttr);
}
}
}
</script>

View File

@@ -0,0 +1,175 @@
<template>
<div>
<el-form label-width="100px" size="small" @submit.native.prevent>
<el-form-item>
<template slot="label">
<span>
流转类型
<el-tooltip placement="top">
<template slot="content">
<div>
普通流转路径流程执行过程中一个元素被访问后会沿着其所有出口顺序流继续执行
<br />默认流转路径只有当没有其他顺序流可以选择时才会选择默认顺序流作为活动的出口顺序流流程会忽略默认顺序流上的条件
<br />条件流转路径是计算其每个出口顺序流上的条件当条件计算为true时选择该出口顺序流如果该方法选择了多条顺序流则会生成多个执行流程会以并行方式继续
</div>
</template>
<i class="el-icon-question" />
</el-tooltip>
</span>
</template>
<el-select v-model="bpmnFormData.type" @change="updateFlowType">
<el-option label="普通流转路径" value="normal" />
<el-option label="默认流转路径" value="default" />
<el-option label="条件流转路径" value="condition" />
</el-select>
</el-form-item>
<el-form-item label="条件格式" v-if="bpmnFormData.type === 'condition'" key="condition">
<el-select v-model="bpmnFormData.conditionType">
<el-option label="表达式" value="expression" />
<el-option label="脚本" value="script" />
</el-select>
</el-form-item>
<el-form-item label="表达式" v-if="bpmnFormData.conditionType && bpmnFormData.conditionType === 'expression'" key="express">
<el-input v-model="bpmnFormData.body" clearable @change="updateFlowCondition" />
</el-form-item>
<template v-if="bpmnFormData.conditionType && bpmnFormData.conditionType === 'script'">
<el-form-item label="脚本语言" key="language">
<el-input v-model="bpmnFormData.language" clearable @change="updateFlowCondition" />
</el-form-item>
<el-form-item label="脚本类型" key="scriptType">
<el-select v-model="bpmnFormData.scriptType">
<el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" />
</el-select>
</el-form-item>
<el-form-item label="脚本" v-if="bpmnFormData.scriptType === 'inlineScript'" key="body">
<el-input v-model="bpmnFormData.body" type="textarea" clearable @change="updateFlowCondition" />
</el-form-item>
<el-form-item label="资源地址" v-if="bpmnFormData.scriptType === 'externalScript'" key="resource">
<el-input v-model="bpmnFormData.resource" clearable @change="updateFlowCondition" />
</el-form-item>
</template>
</el-form>
</div>
</template>
<script>
import {StrUtil} from "@/utils/StrUtil";
export default {
name: "BpmnModel",
/** 组件传值 */
props : {
id: {
type: String,
required: true
},
},
data() {
return {
bpmnElementSource: {},
bpmnElementSourceRef: {},
bpmnFormData: {}
}
},
/** 传值监听 */
watch: {
id: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.resetFlowCondition();
}
},
immediate: true, // 立即生效
},
},
created() {
},
methods: {
// 方法区
resetFlowCondition() {
this.bpmnFormData = {
body: null
};
this.bpmnElementSource = this.modelerStore.element.source;
this.bpmnElementSourceRef = this.modelerStore.element.businessObject.sourceRef;
if (this.bpmnElementSourceRef && this.bpmnElementSourceRef.default && this.bpmnElementSourceRef.default.id === this.modelerStore.element.id) {
// 默认
this.$set(this.bpmnFormData, "type", "default");
} else if (!this.modelerStore.element.businessObject.conditionExpression) {
// 普通
this.$set(this.bpmnFormData, "type", "normal");
} else {
// 带条件
const conditionExpression = this.modelerStore.element.businessObject.conditionExpression;
this.bpmnFormData = {...conditionExpression, type: "condition"};
// resource 可直接标识 是否是外部资源脚本
if (this.bpmnFormData.resource) {
this.$set(this.bpmnFormData, "conditionType", "script");
this.$set(this.bpmnFormData, "scriptType", "externalScript");
return;
}
if (conditionExpression.language) {
this.$set(this.bpmnFormData, "conditionType", "script");
this.$set(this.bpmnFormData, "scriptType", "inlineScript");
return;
}
this.$set(this.bpmnFormData, "conditionType", "expression");
}
},
updateFlowType(flowType) {
// 正常条件类
if (flowType === "condition") {
const flowConditionRef = this.modelerStore.moddle.create("bpmn:FormalExpression");
this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
conditionExpression: flowConditionRef
});
return;
}
// 默认路径
if (flowType === "default") {
this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
conditionExpression: null
});
this.modelerStore.modeling.updateProperties(this.bpmnElementSource, {
default: this.modelerStore.element
});
// 清空条件格式
this.bpmnFormData.conditionType = null;
return;
}
// 清空条件格式
this.bpmnFormData.conditionType = null;
// 正常路径,如果来源节点的默认路径是当前连线时,清除父元素的默认路径配置
if (this.bpmnElementSourceRef.default && this.bpmnElementSourceRef.default.id === this.modelerStore.element.id) {
this.modelerStore.modeling.updateProperties(this.bpmnElementSource, {
default: null
});
}
this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
conditionExpression: null
});
},
updateFlowCondition() {
let {conditionType, scriptType, body, resource, language} = this.bpmnFormData;
let condition;
if (conditionType === "expression") {
condition = this.modelerStore.moddle.create("bpmn:FormalExpression", {body});
} else {
if (scriptType === "inlineScript") {
condition = this.modelerStore.moddle.create("bpmn:FormalExpression", {body, language});
this.$set(this.bpmnFormData, "resource", "");
} else {
this.$set(this.bpmnFormData, "body", "");
condition = this.modelerStore.moddle.create("bpmn:FormalExpression", {resource, language});
}
}
this.modelerStore.modeling.updateProperties(this.modelerStore.element, {conditionExpression: condition});
}
}
}
</script>

View File

@@ -0,0 +1,472 @@
<template>
<div class="panel-tab__content">
<el-table :data="elementListenersList" size="mini" border>
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="类型" width="60px" prop="event" />
<el-table-column label="监听类型" width="80px" show-overflow-tooltip :formatter="row => listenerTypeObject[row.listenerType]" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="openListenerForm(scope.row, scope.$index)">编辑</el-button>
<el-divider direction="vertical" />
<el-button size="mini" type="danger" @click="removeListener(scope.row, scope.$index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button_save">
<el-button type="primary" icon="el-icon-plus" size="small" @click="listenerSystemVisible = true">内置监听器</el-button>
<el-button type="primary" icon="el-icon-plus" size="small" @click="openListenerForm(null)" >自定义监听器</el-button>
</div>
<!-- 监听器 编辑/创建 部分 -->
<el-drawer :visible.sync="listenerFormModelVisible" title="执行监听器" size="480px" append-to-body destroy-on-close>
<el-form :model="listenerForm" size="small" label-width="96px" ref="listenerFormRef" @submit.native.prevent>
<el-form-item label="事件类型" prop="event" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-select v-model="listenerForm.event">
<el-option label="start" value="start" />
<el-option label="end" value="end" />
</el-select>
</el-form-item>
<el-form-item label="监听器类型" prop="listenerType" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-select v-model="listenerForm.listenerType">
<el-option v-for="i in Object.keys(listenerTypeObject)" :key="i" :label="listenerTypeObject[i]" :value="i" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'classListener'"
label="Java类"
prop="class"
key="listener-class"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.class" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'expressionListener'"
label="表达式"
prop="expression"
key="listener-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.expression" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label="代理表达式"
prop="delegateExpression"
key="listener-delegate"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.delegateExpression" clearable />
</el-form-item>
<template v-if="listenerForm.listenerType === 'scriptListener'">
<el-form-item
label="脚本格式"
prop="scriptFormat"
key="listener-script-format"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本格式' }"
>
<el-input v-model="listenerForm.scriptFormat" clearable />
</el-form-item>
<el-form-item
label="脚本类型"
prop="scriptType"
key="listener-script-type"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
>
<el-select v-model="listenerForm.scriptType">
<el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'inlineScript'"
label="脚本内容"
prop="value"
key="listener-script"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本内容' }"
>
<el-input v-model="this.listenerForm" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'externalScript'"
label="资源地址"
prop="resource"
key="listener-resource"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写资源地址' }"
>
<el-input v-model="listenerForm.resource" clearable />
</el-form-item>
</template>
</el-form>
<el-divider />
<p class="listener-filed__title">
<span><el-icon><BellFilled /></el-icon>注入字段</span>
<el-button type="primary" size="mini" @click="openListenerFieldForm(null)">添加字段</el-button>
</p>
<el-table :data="fieldsListOfListener" size="mini" max-height="240" border fit style="flex: none">
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="字段名称" width="80px" prop="name" />
<el-table-column label="字段类型" width="80px" show-overflow-tooltip :formatter="row => fieldTypeObject[row.fieldType]" />
<el-table-column label="值内容" width="80px" show-overflow-tooltip :formatter="row => row.string || row.expression" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="openListenerFieldForm(scope.row, scope.$index)">编辑</el-button>
<el-divider direction="vertical" />
<el-button size="mini" type="danger" @click="removeListenerField(scope.row, scope.$index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<el-button size="small" @click="listenerFormModelVisible = false"> </el-button>
<el-button size="small" type="primary" @click="saveListenerConfig"> </el-button>
</div>
</el-drawer>
<!-- 注入西段 编辑/创建 部分 -->
<el-dialog title="字段配置" :visible.sync="listenerFieldFormModelVisible" width="600px" append-to-body destroy-on-close>
<el-form :model="listenerFieldForm" label-width="96px" ref="listenerFieldFormRef" style="height: 136px" @submit.native.prevent>
<el-form-item label="字段名称:" prop="name" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-input v-model="listenerFieldForm.name" clearable />
</el-form-item>
<el-form-item label="字段类型:" prop="fieldType" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-select v-model="listenerFieldForm.fieldType">
<el-option v-for="i in Object.keys(fieldTypeObject)" :key="i" :label="fieldTypeObject[i]" :value="i" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'string'"
label="字段值:"
prop="string"
key="field-string"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.string" clearable />
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'expression'"
label="表达式:"
prop="expression"
key="field-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.expression" clearable />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="listenerFieldFormModelVisible= false"> </el-button>
<el-button size="small" type="primary" @click="saveListenerFiled"> </el-button>
</div>
</el-dialog>
<!-- 内置监听器 -->
<el-drawer :visible.sync="listenerSystemVisible" title="执行监听器" size="580px" append-to-body destroy-on-close>
<el-table v-loading="loading" :data="listenerList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="类型" align="center" prop="eventType"/>
<el-table-column label="监听类型" align="center" prop="valueType">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_listener_value_type" :value="scope.row.valueType"/>
</template>
</el-table-column>
<el-table-column label="执行内容" align="center" prop="value" :show-overflow-tooltip="true"/>
</el-table>
<pagination
v-show="total>0"
:total="total"
layout="prev, pager, next"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<div class="element-drawer__button">
<el-button size="small" @click="listenerSystemVisible = false"> </el-button>
<el-button size="small" type="primary" :disabled="listenerSystemChecked" @click="saveSystemListener"> </el-button>
</div>
</el-drawer>
</div>
</template>
<script>
import { listListener } from "@/api/flowable/listener";
import {
changeListenerObject,
createListenerObject,
createSystemListenerObject,
updateElementExtensions
} from "../common/bpmnUtils";
import {StrUtil} from "@/utils/StrUtil";
export default {
name: "ExecutionListener",
// 内置监听器相关信息
dicts: ['sys_listener_value_type', 'sys_listener_event_type'],
/** 组件传值 */
props : {
id: {
type: String,
required: true
},
},
data() {
return {
elementListenersList: [], // 监听器列表
listenerForm: {},// 监听器详情表单
listenerFormModelVisible: false, // 监听器 编辑 侧边栏显示状态
fieldsListOfListener: [],
bpmnElementListeners: [],
otherExtensionList: [],
listenerFieldForm: {}, // 监听器 注入字段 详情表单
listenerFieldFormModelVisible: false, // 监听器 注入字段表单弹窗 显示状态
editingListenerIndex: -1, // 监听器所在下标,-1 为新增
editingListenerFieldIndex: -1, // 字段所在下标,-1 为新增
listenerList: [],
checkedListenerData: [],
listenerSystemVisible: false,
listenerSystemChecked: true,
loading: true,
total: 0,
listenerTypeObject: {
classListener: "Java 类",
expressionListener: "表达式",
delegateExpressionListener: "代理表达式",
scriptListener: "脚本"
},
eventType: {
create: "创建",
assignment: "指派",
complete: "完成",
delete: "删除",
update: "更新",
timeout: "超时"
},
fieldTypeObject: {
string: "字符串",
expression: "表达式"
},
queryParams: {
pageNum: 1,
pageSize: 10,
type: 2,
},
}
},
/** 传值监听 */
watch: {
id: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.resetListenersList();
}
},
immediate: true, // 立即生效
},
},
created() {
this.getList();
},
methods: {
resetListenersList() {
this.bpmnElementListeners =
this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type === `flowable:ExecutionListener`) ?? [];
this.elementListenersList = this.bpmnElementListeners.map(listener => this.initListenerType(listener));
this.$emit('getExecutionListenerCount', this.elementListenersList.length)
},
// 打开 监听器详情 侧边栏
openListenerForm(listener, index) {
if (listener) {
this.listenerForm = this.initListenerForm(listener);
this.editingListenerIndex = index;
} else {
this.listenerForm = {};
this.editingListenerIndex = -1; // 标记为新增
}
if (listener && listener.fields) {
this.fieldsListOfListener = listener.fields.map(field => ({
...field,
fieldType: field.string ? "string" : "expression"
}));
} else {
this.fieldsListOfListener = [];
this.$set(this.listenerForm, "fields", []);
}
// 打开侧边栏并清楚验证状态
this.listenerFormModelVisible = true;
this.$nextTick(() => {
if (this.$refs["listenerFormRef"]) this.$refs["listenerFormRef"].clearValidate();
});
},
// 打开监听器字段编辑弹窗
openListenerFieldForm(field, index) {
this.listenerFieldForm = field ? JSON.parse(JSON.stringify(field)) : {};
this.editingListenerFieldIndex = field ? index : -1;
this.listenerFieldFormModelVisible = true;
this.$nextTick(() => {
if (this.$refs["listenerFieldFormRef"]) this.$refs["listenerFieldFormRef"].clearValidate();
});
},
// 保存监听器注入字段
async saveListenerFiled() {
let validateStatus = await this.$refs["listenerFieldFormRef"].validate();
if (!validateStatus) return; // 验证不通过直接返回
if (this.editingListenerFieldIndex === -1) {
this.fieldsListOfListener.push(this.listenerFieldForm);
this.listenerForm.fields.push(this.listenerFieldForm);
} else {
this.fieldsListOfListener.splice(this.editingListenerFieldIndex, 1, this.listenerFieldForm);
this.listenerForm.fields.splice(this.editingListenerFieldIndex, 1, this.listenerFieldForm);
}
this.listenerFieldFormModelVisible = false;
this.$nextTick(() => (this.listenerFieldForm = {}));
},
// 移除监听器字段
removeListenerField(field, index) {
this.$confirm("确认移除该字段吗?", "提示", {
confirmButtonText: "确 认",
cancelButtonText: "取 消"
}).then(() => {
this.fieldsListOfListener.splice(index, 1);
this.listenerForm.fields.splice(index, 1);
}).catch(() => console.info("操作取消"));
},
// 移除监听器
removeListener(listener, index) {
this.$confirm("确认移除该监听器吗?", "提示", {
confirmButtonText: "确 认",
cancelButtonText: "取 消"
}).then(() => {
this.bpmnElementListeners.splice(index, 1);
this.elementListenersList.splice(index, 1);
updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
this.$emit('getExecutionListenerCount', this.elementListenersList.length)
}).catch((r) => console.info(r, "操作取消"));
},
// 保存监听器配置
async saveListenerConfig() {
let validateStatus = await this.$refs["listenerFormRef"].validate();
if (!validateStatus) return; // 验证不通过直接返回
const listenerObject = createListenerObject(this.modelerStore.moddle, this.listenerForm, false, "flowable");
if (this.editingListenerIndex === -1) {
this.bpmnElementListeners.push(listenerObject);
this.elementListenersList.push(this.listenerForm);
} else {
this.bpmnElementListeners.splice(this.editingListenerIndex, 1, listenerObject);
this.elementListenersList.splice(this.editingListenerIndex, 1, this.listenerForm);
}
// 保存其他配置
this.otherExtensionList = this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type !== `flowable:ExecutionListener`) ?? [];
updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
this.$emit('getExecutionListenerCount', this.elementListenersList.length)
// 4. 隐藏侧边栏
this.listenerFormModelVisible = false;
this.listenerForm = {};
},
initListenerType(listener) {
let listenerType;
if (listener.class) listenerType = "classListener";
if (listener.expression) listenerType = "expressionListener";
if (listener.delegateExpression) listenerType = "delegateExpressionListener";
if (listener.script) listenerType = "scriptListener";
return {
...JSON.parse(JSON.stringify(listener)),
...(listener.script ?? {}),
listenerType: listenerType
};
},
// 初始化表单数据
initListenerForm(listener) {
let self = {
...listener
};
if (listener.script) {
self = {
...listener,
...listener.script,
scriptType: listener.script.resource ? "externalScript" : "inlineScript"
};
}
if (listener.event === "timeout" && listener.eventDefinitions) {
if (listener.eventDefinitions.length) {
let k = "";
for (let key in listener.eventDefinitions[0]) {
console.log(listener.eventDefinitions, key);
if (key.indexOf("time") !== -1) {
k = key;
self.eventDefinitionType = key.replace("time", "").toLowerCase();
}
}
console.log(k);
self.eventTimeDefinitions = listener.eventDefinitions[0][k].body;
}
}
return self;
},
/** 查询流程达式列表 */
getList() {
this.loading = true;
listListener(this.queryParams).then(response => {
this.listenerList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 多选框选中数据
handleSelectionChange(selection) {
// ids.value = selection.map(item => item.id);
// TODO 应该使用 push?
this.checkedListenerData = selection;
this.listenerSystemChecked = selection.length !== 1;
},
// 保存内置监听器
saveSystemListener() {
if (this.checkedListenerData.length > 0) {
this.checkedListenerData.forEach(value => {
// 保存其他配置
const listenerObject = createSystemListenerObject(this.modelerStore.moddle, value, false, "flowable");
this.bpmnElementListeners.push(listenerObject);
this.elementListenersList.push(changeListenerObject(value));
this.otherExtensionList = this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type !== `flowable:TaskListener`) ?? [];
updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
})
// 回传显示数量
this.$emit('getExecutionListenerCount', this.elementListenersList.length)
}
this.checkedListenerData = [];
this.listenerSystemChecked = true;
// 隐藏侧边栏
this.listenerSystemVisible = false;
}
}
}
</script>
<style lang="scss">
@import '../style/process-panel';
.flow-containers .el-badge__content.is-fixed {
top: 18px;
}
.dialog-footer button:first-child {
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<div>
<el-form label-width="80px" size="small" @submit.native.prevent>
<el-form-item label="流程表单">
<el-select v-model="bpmnFormData.formKey" clearable class="m-2" placeholder="挂载节点表单" @change="updateElementFormKey">
<el-option
v-for="item in formList"
:key="item.value"
:label="item.formName"
:value="item.formId"
/>
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { listAllForm } from '@/api/flowable/form'
import {StrUtil} from "@/utils/StrUtil";
export default {
name: "FormPanel",
/** 组件传值 */
props : {
id: {
type: String,
required: true
},
},
data() {
return {
formList: [], // 表单数据
bpmnFormData: {}
}
},
/** 传值监听 */
watch: {
id: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
// 加载表单列表
this.getListForm();
this.resetFlowForm();
}
},
immediate: true, // 立即生效
},
},
created() {
},
methods: {
// 方法区
resetFlowForm() {
this.bpmnFormData.formKey = this.modelerStore.element.businessObject.formKey;
},
updateElementFormKey(val) {
if (StrUtil.isBlank(val)) {
delete this.modelerStore.element.businessObject[`formKey`]
} else {
this.modelerStore.modeling.updateProperties(this.modelerStore.element, {'formKey': val});
}
},
// 获取表单信息
getListForm() {
listAllForm().then(res => {
res.data.forEach(item => {
item.formId = item.formId.toString();
})
this.formList = res.data;
})
}
}
}
</script>

View File

@@ -0,0 +1,236 @@
<template>
<div class="panel-tab__content">
<el-form label-width="70px" @submit.native.prevent size="small">
<el-form-item label="参数说明">
<el-button size="small" type="primary" @click="dialogVisible = true">查看</el-button>
</el-form-item>
<el-form-item label="回路特性">
<el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType">
<!--bpmn:MultiInstanceLoopCharacteristics-->
<el-option label="并行多重事件" value="ParallelMultiInstance" />
<el-option label="时序多重事件" value="SequentialMultiInstance" />
<!--bpmn:StandardLoopCharacteristics-->
<el-option label="循环事件" value="StandardLoop" />
<el-option label="无" value="Null" />
</el-select>
</el-form-item>
<template v-if="loopCharacteristics === 'ParallelMultiInstance' || loopCharacteristics === 'SequentialMultiInstance'">
<el-form-item label="循环基数" key="loopCardinality">
<el-input v-model="loopInstanceForm.loopCardinality" clearable @change="updateLoopCardinality" />
</el-form-item>
<el-form-item label="集合" key="collection">
<el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
</el-form-item>
<el-form-item label="元素变量" key="elementVariable">
<el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
</el-form-item>
<el-form-item label="完成条件" key="completionCondition">
<el-input v-model="loopInstanceForm.completionCondition" clearable @change="updateLoopCondition" />
</el-form-item>
<el-form-item label="异步状态" key="async">
<el-checkbox v-model="loopInstanceForm.asyncBefore" label="异步前" @change="updateLoopAsync('asyncBefore')" />
<el-checkbox v-model="loopInstanceForm.asyncAfter" label="异步后" @change="updateLoopAsync('asyncAfter')" />
<el-checkbox
v-model="loopInstanceForm.exclusive"
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
label="排除"
@change="updateLoopAsync('exclusive')"
/>
</el-form-item>
<el-form-item label="重试周期" prop="timeCycle" v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore" key="timeCycle">
<el-input v-model="loopInstanceForm.timeCycle" clearable @change="updateLoopTimeCycle" />
</el-form-item>
</template>
</el-form>
<!-- 参数说明 -->
<el-dialog title="多实例参数" :visible.sync="dialogVisible" width="680px" @closed="$emit('close')">
<el-descriptions :column="1" border>
<el-descriptions-item label="使用说明">按照BPMN2.0规范的要求用于为每个实例创建执行的父执行会提供下列变量:</el-descriptions-item>
<el-descriptions-item label="collection(集合变量)">传入List参数, 一般为用户ID集合</el-descriptions-item>
<el-descriptions-item label="elementVariable(元素变量)">List中单个参数的名称</el-descriptions-item>
<el-descriptions-item label="loopCardinality(基数)">List循环次数</el-descriptions-item>
<el-descriptions-item label="isSequential(串并行)">Parallel: 并行多实例Sequential: 串行多实例</el-descriptions-item>
<el-descriptions-item label="completionCondition(完成条件)">任务出口条件</el-descriptions-item>
<el-descriptions-item label="nrOfInstances(实例总数)">实例总数</el-descriptions-item>
<el-descriptions-item label="nrOfActiveInstances(未完成数)">当前活动的即未完成的实例数量对于顺序多实例这个值总为1</el-descriptions-item>
<el-descriptions-item label="nrOfCompletedInstances(已完成数)">已完成的实例数量</el-descriptions-item>
<el-descriptions-item label="loopCounter">给定实例在for-each循环中的index</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script>
import {StrUtil} from '@/utils/StrUtil'
export default {
name: "MultiInstance",
/** 组件传值 */
props: {
id: {
type: String,
required: true
},
},
data() {
return {
dialogVisible: false,
loopCharacteristics: "",
loopInstanceForm: {},
multiLoopInstance: {},
defaultLoopInstanceForm: {
completionCondition: "",
loopCardinality: "",
extensionElements: [],
asyncAfter: false,
asyncBefore: false,
exclusive: false
},
}
},
/** 传值监听 */
watch: {
id: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.getElementLoop(this.modelerStore.element.businessObject); }
},
immediate: true, // 立即生效
},
},
created() {
},
methods: {
// 方法区
getElementLoop(businessObject) {
if (!businessObject.loopCharacteristics) {
this.loopCharacteristics = "Null";
this.loopInstanceForm = {};
return;
}
if (businessObject.loopCharacteristics.$type === "bpmn:StandardLoopCharacteristics") {
this.loopCharacteristics = "StandardLoop";
this.loopInstanceForm = {};
return;
}
if (businessObject.loopCharacteristics.isSequential) {
this.loopCharacteristics = "SequentialMultiInstance";
} else {
this.loopCharacteristics = "ParallelMultiInstance";
}
// 合并配置
this.loopInstanceForm = {
...this.defaultLoopInstanceForm,
...businessObject.loopCharacteristics,
completionCondition: businessObject.loopCharacteristics?.completionCondition?.body ?? "",
loopCardinality: businessObject.loopCharacteristics?.loopCardinality?.body ?? ""
};
// 保留当前元素 businessObject 上的 loopCharacteristics 实例
this.multiLoopInstance = this.modelerStore.element.businessObject.loopCharacteristics;
// 更新表单
if (
businessObject.loopCharacteristics.extensionElements &&
businessObject.loopCharacteristics.extensionElements.values &&
businessObject.loopCharacteristics.extensionElements.values.length
) {
this.$set(this.loopInstanceForm, "timeCycle", businessObject.loopCharacteristics.extensionElements.values[0].body);
}
},
changeLoopCharacteristicsType(type) {
// 切换类型取消原表单配置
this.loopInstanceForm = {...this.defaultLoopInstanceForm};
// 取消多实例配置
if (type === "Null") {
this.modelerStore.modeling.updateProperties(this.modelerStore.element, {loopCharacteristics: null});
return;
}
// 配置循环
if (type === "StandardLoop") {
const loopCharacteristicsObject = this.modelerStore.moddle.create("bpmn:StandardLoopCharacteristics");
this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
loopCharacteristics: loopCharacteristicsObject
});
this.multiLoopInstance = null;
return;
}
// 时序
if (type === "SequentialMultiInstance") {
this.multiLoopInstance = this.modelerStore.moddle.create("bpmn:MultiInstanceLoopCharacteristics", {
isSequential: true
});
} else {
this.multiLoopInstance = this.modelerStore.moddle.create("bpmn:MultiInstanceLoopCharacteristics");
}
this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
loopCharacteristics: this.multiLoopInstance
});
},
// 循环基数
updateLoopCardinality(cardinality) {
let loopCardinality = null;
if (cardinality && cardinality.length) {
loopCardinality = this.modelerStore.moddle.create("bpmn:FormalExpression", {body: cardinality});
}
this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, {
loopCardinality
});
},
// 完成条件
updateLoopCondition(condition) {
let completionCondition = null;
if (condition && condition.length) {
completionCondition = this.modelerStore.moddle.create("bpmn:FormalExpression", {body: condition});
}
this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, {
completionCondition
});
},
// 重试周期
updateLoopTimeCycle(timeCycle) {
const extensionElements = this.modelerStore.moddle.create("bpmn:ExtensionElements", {
values: [
this.modelerStore.moddle.create(`flowable:FailedJobRetryTimeCycle`, {
body: timeCycle
})
]
});
this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, {
extensionElements
});
},
// 直接更新的基础信息
updateLoopBase() {
this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, {
collection: this.loopInstanceForm.collection || null,
elementVariable: this.loopInstanceForm.elementVariable || null
});
},
// 各异步状态
updateLoopAsync(key) {
const {asyncBefore, asyncAfter} = this.loopInstanceForm;
let asyncAttr = Object.create(null);
if (!asyncBefore && !asyncAfter) {
this.$set(this.loopInstanceForm, "exclusive", false);
asyncAttr = {asyncBefore: false, asyncAfter: false, exclusive: false, extensionElements: null};
} else {
asyncAttr[key] = this.loopInstanceForm[key];
}
this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, asyncAttr);
}
}
}
</script>
<style lang="scss">
@import '../style/process-panel';
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div>
<el-form label-width="80px" size="small" @submit.native.prevent>
<el-form-item label="跳过表达式">
<el-input v-model="bpmnFormData.skipExpression" @change="updateElementTask('skipExpression')"/>
</el-form-item>
<el-form-item label="是否为补偿">
<el-input v-model="bpmnFormData.isForCompensation" @change="updateElementTask('isForCompensation')"/>
</el-form-item>
<el-form-item label="服务任务可触发">
<el-input v-model="bpmnFormData.triggerable" @change="updateElementTask('triggerable')"/>
</el-form-item>
</el-form>
</div>
</template>
<script>
import {StrUtil} from "@/utils/StrUtil";
export default {
name: "OtherPanel",
/** 组件传值 */
props : {
id: {
type: String,
required: true
},
},
data() {
return {
bpmnFormData: {}
}
},
/** 传值监听 */
watch: {
id: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.resetTaskForm();
}
},
immediate: true, // 立即生效
},
},
created() {
},
methods: {
// 方法区
resetFlowForm() {
this.bpmnFormData = JSON.parse(JSON.stringify(this.modelerStore.element.businessObject));
},
updateElementTask(key) {
const taskAttr = Object.create(null);
taskAttr[key] = this.bpmnFormData[key] || null;
this.modelerStore.modeling.updateProperties(this.modelerStore.element, taskAttr);
}
}
}
</script>

View File

@@ -0,0 +1,529 @@
<template>
<div class="panel-tab__content">
<el-table :data="elementListenersList" border size="mini">
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="类型" width="60px" show-overflow-tooltip :formatter="row => listenerEventTypeObject[row.event]" />
<!-- <el-table-column label="事件id" min-width="70px" prop="id" show-overflow-tooltip />-->
<el-table-column label="监听类型" width="85px" show-overflow-tooltip :formatter="row => listenerTypeObject[row.listenerType]" />
<el-table-column label="操作" >
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="openListenerForm(scope.row, scope.$index)">编辑</el-button>
<el-divider direction="vertical" />
<el-button size="mini" type="danger" @click="removeListener(scope.row, scope.$index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button_save">
<el-button type="primary" icon="el-icon-plus" size="small" @click="listenerSystemVisible = true">内置监听器</el-button>
<el-button type="primary" icon="el-icon-plus" size="small" @click="openListenerForm(null)">自定义监听器</el-button>
</div>
<!-- 监听器 编辑/创建 部分 -->
<el-drawer :visible.sync="listenerFormModelVisible" title="任务监听器" size="480px" append-to-body destroy-on-close>
<el-form :model="listenerForm" size="small" label-width="110px" ref="listenerFormRef" @submit.native.prevent>
<el-form-item prop="event" :rules="{ required: true, trigger: ['blur', 'change'] }">
<template slot="label">
<span>
事件类型
<el-tooltip placement="top">
<template slot="content">
<div>
create创建当任务已经创建并且所有任务参数都已经设置时触发
<br />assignment指派当任务已经指派给某人时触发请注意当流程执行到达用户任务时
<br />在触发create事件之前会首先触发assignment事件这顺序看起来不太自然
<br />但是有实际原因的当收到create事件时我们通常希望能看到任务的所有参数包括办理人
<br />complete完成当任务已经完成从运行时数据中删除前触发
<br />delete删除在任务即将被删除前触发请注意任务由completeTask正常完成时也会触发
</div>
</template>
<i class="el-icon-question" />
</el-tooltip>
</span>
</template>
<el-select v-model="listenerForm.event">
<el-option v-for="i in Object.keys(listenerEventTypeObject)" :key="i" :label="listenerEventTypeObject[i]" :value="i" />
</el-select>
</el-form-item>
<!-- <el-form-item label="监听器ID" prop="id" :rules="{ required: true, trigger: ['blur', 'change'] }">-->
<!-- <el-input v-model="listenerForm.id" clearable />-->
<!-- </el-form-item>-->
<el-form-item label="监听器类型" prop="listenerType" :rules="{ required: true, trigger: ['blur', 'change'] }">
<template slot="label">
<span>
监听类型
<el-tooltip placement="top">
<template slot="content">
<div>
class需要调用的委托类这个类必须实现org.flowable.engine.delegate.TaskListener接口
<br />assignment指派当任务已经指派给某人时触发请注意当流程执行到达用户任务时
<br /> 在触发create事件之前会首先触发assignment事件这顺序看起来不太自然
<br /> 但是有实际原因的当收到create事件时我们通常希望能看到任务的所有参数包括办理人
<br />complete完成当任务已经完成从运行时数据中删除前触发
<br />delete删除在任务即将被删除前触发请注意任务由completeTask正常完成时也会触发
</div>
</template>
<i class="el-icon-question" />
</el-tooltip>
</span>
</template>
<el-select v-model="listenerForm.listenerType">
<el-option v-for="i in Object.keys(listenerTypeObject)" :key="i" :label="listenerTypeObject[i]" :value="i" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'classListener'"
label="Java类"
prop="class"
key="listener-class"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.class" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'expressionListener'"
label="表达式"
prop="expression"
key="listener-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.expression" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
label="代理表达式"
prop="delegateExpression"
key="listener-delegate"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerForm.delegateExpression" clearable />
</el-form-item>
<template v-if="listenerForm.listenerType === 'scriptListener'">
<el-form-item
label="脚本格式"
prop="scriptFormat"
key="listener-script-format"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本格式' }"
>
<el-input v-model="listenerForm.scriptFormat" clearable />
</el-form-item>
<el-form-item
label="脚本类型"
prop="scriptType"
key="listener-script-type"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
>
<el-select v-model="listenerForm.scriptType">
<el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'inlineScript'"
label="脚本内容"
prop="value"
key="listener-script"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本内容' }"
>
<el-input v-model="listenerForm.value" clearable />
</el-form-item>
<el-form-item
v-if="listenerForm.scriptType === 'externalScript'"
label="资源地址"
prop="resource"
key="listener-resource"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写资源地址' }"
>
<el-input v-model="listenerForm.resource" clearable />
</el-form-item>
</template>
<template v-if="listenerForm.event === 'timeout'">
<el-form-item label="定时器类型" prop="eventDefinitionType" key="eventDefinitionType">
<el-select v-model="listenerForm.eventDefinitionType">
<el-option label="日期" value="date" />
<el-option label="持续时长" value="duration" />
<el-option label="循环" value="cycle" />
<el-option label="无" value="null" />
</el-select>
</el-form-item>
<el-form-item
v-if="!!listenerForm.eventDefinitionType && listenerForm.eventDefinitionType !== 'null'"
label="定时器"
prop="eventTimeDefinitions"
key="eventTimeDefinitions"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请填写定时器配置' }"
>
<el-input v-model="listenerForm.eventTimeDefinitions" clearable />
</el-form-item>
</template>
</el-form>
<el-divider />
<p class="listener-filed__title">
<span><i class="el-icon-menu"></i>注入字段</span>
<el-button size="small" type="primary" @click="openListenerFieldForm(null)">添加字段</el-button>
</p>
<el-table :data="fieldsListOfListener" size="mini" max-height="240" border fit style="flex: none">
<el-table-column label="序号" width="50px" type="index" />
<el-table-column label="字段名称" width="80px" prop="name" />
<el-table-column label="字段类型" width="80px" show-overflow-tooltip :formatter="row => fieldTypeObject[row.fieldType]" />
<el-table-column label="值内容" width="80px" show-overflow-tooltip :formatter="row => row.string || row.expression" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="openListenerFieldForm(scope.row, scope.$index)">编辑</el-button>
<el-divider direction="vertical" />
<el-button size="mini" type="danger" @click="removeListenerField(scope.row, scope.$index)">移除</el-button>
</template>
</el-table-column>
</el-table>
<div class="element-drawer__button">
<el-button size="small" @click="listenerFormModelVisible = false"> </el-button>
<el-button size="small" type="primary" @click="saveListenerConfig"> </el-button>
</div>
</el-drawer>
<!-- 注入西段 编辑/创建 部分 -->
<el-dialog title="字段配置" :visible.sync="listenerFieldFormModelVisible" width="600px" append-to-body destroy-on-close>
<el-form :model="listenerFieldForm" label-width="96px" ref="listenerFieldFormRef" style="height: 136px" @submit.native.prevent>
<el-form-item label="字段名称:" prop="name" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-input v-model="listenerFieldForm.name" clearable />
</el-form-item>
<el-form-item label="字段类型:" prop="fieldType" :rules="{ required: true, trigger: ['blur', 'change'] }">
<el-select v-model="listenerFieldForm.fieldType">
<el-option v-for="i in Object.keys(fieldTypeObject)" :key="i" :label="fieldTypeObject[i]" :value="i" />
</el-select>
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'string'"
label="字段值:"
prop="string"
key="field-string"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.string" clearable />
</el-form-item>
<el-form-item
v-if="listenerFieldForm.fieldType === 'expression'"
label="表达式:"
prop="expression"
key="field-expression"
:rules="{ required: true, trigger: ['blur', 'change'] }"
>
<el-input v-model="listenerFieldForm.expression" clearable />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="listenerFieldFormModelVisible= false"> </el-button>
<el-button size="small" type="primary" @click="saveListenerFiled"> </el-button>
</div>
</el-dialog>
<!-- 内置监听器 -->
<el-drawer :visible.sync="listenerSystemVisible" title="任务监听器" size="580px" append-to-body destroy-on-close>
<el-table v-loading="loading" :data="listenerList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="类型" align="center" prop="eventType"/>
<el-table-column label="监听类型" align="center" prop="valueType">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_listener_value_type" :value="scope.row.valueType"/>
</template>
</el-table-column>
<el-table-column label="执行内容" align="center" prop="value" :show-overflow-tooltip="true"/>
</el-table>
<pagination
v-show="total>0"
:total="total"
layout="prev, pager, next"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<div class="element-drawer__button">
<el-button size="small" @click="listenerSystemVisible = false"> </el-button>
<el-button size="small" type="primary" :disabled="listenerSystemChecked" @click="saveSystemListener"> </el-button>
</div>
</el-drawer>
</div>
</template>
<script>
import { listListener } from "@/api/flowable/listener";
import {
changeListenerObject,
createListenerObject,
createSystemListenerObject,
updateElementExtensions
} from "../common/bpmnUtils";
import {StrUtil} from "@/utils/StrUtil";
export default {
name: "TaskListener",
// 内置监听器相关信息
dicts: ['sys_listener_value_type', 'sys_listener_event_type'],
/** 组件传值 */
props : {
id: {
type: String,
required: true
},
},
data() {
return {
elementListenersList: [], // 监听器列表
listenerForm: {},// 监听器详情表单
listenerFormModelVisible: false, // 监听器 编辑 侧边栏显示状态
fieldsListOfListener: [],
bpmnElementListeners: [],
otherExtensionList: [],
listenerFieldForm: {}, // 监听器 注入字段 详情表单
listenerFieldFormModelVisible: false, // 监听器 注入字段表单弹窗 显示状态
editingListenerIndex: -1, // 监听器所在下标,-1 为新增
editingListenerFieldIndex: -1, // 字段所在下标,-1 为新增
listenerList: [],
checkedListenerData: [],
listenerSystemVisible: false,
listenerSystemChecked: true,
loading: true,
total: 0,
listenerTypeObject: {
classListener: "Java 类",
expressionListener: "表达式",
delegateExpressionListener: "代理表达式",
scriptListener: "脚本"
},
listenerEventTypeObject:{
create: "创建",
assignment: "指派",
complete: "完成",
delete: "删除",
// update: "更新",
// timeout: "超时"
},
fieldTypeObject:{
string: "字符串",
expression: "表达式"
},
queryParams: {
pageNum: 1,
pageSize: 10,
type: 1,
},
}
},
/** 传值监听 */
watch: {
id: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.resetListenersList();
}
},
immediate: true, // 立即生效
},
},
created() {
this.getList();
},
methods: {
resetListenersList() {
this.bpmnElementListeners =
this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type === `flowable:TaskListener`) ?? [];
this.elementListenersList = this.bpmnElementListeners.map(listener => this.initListenerType(listener));
this.$emit('getTaskListenerCount', this.elementListenersList.length)
},
// 打开 监听器详情 侧边栏
openListenerForm(listener, index) {
if (listener) {
this.listenerForm = this.initListenerForm(listener);
this.editingListenerIndex = index;
} else {
this.listenerForm = {};
this.editingListenerIndex = -1; // 标记为新增
}
if (listener && listener.fields) {
this.fieldsListOfListener = listener.fields.map(field => ({
...field,
fieldType: field.string ? "string" : "expression"
}));
} else {
this.fieldsListOfListener = [];
this.$set(this.listenerForm, "fields", []);
}
// 打开侧边栏并清楚验证状态
this.listenerFormModelVisible = true;
this.$nextTick(() => {
if (this.$refs["listenerFormRef"]) this.$refs["listenerFormRef"].clearValidate();
});
},
// 打开监听器字段编辑弹窗
openListenerFieldForm(field, index) {
this.listenerFieldForm = field ? JSON.parse(JSON.stringify(field)) : {};
this.editingListenerFieldIndex = field ? index : -1;
this.listenerFieldFormModelVisible = true;
this.$nextTick(() => {
if (this.$refs["listenerFieldFormRef"]) this.$refs["listenerFieldFormRef"].clearValidate();
});
},
// 保存监听器注入字段
async saveListenerFiled() {
let validateStatus = await this.$refs["listenerFieldFormRef"].validate();
if (!validateStatus) return; // 验证不通过直接返回
if (this.editingListenerFieldIndex === -1) {
this.fieldsListOfListener.push(this.listenerFieldForm);
this.listenerForm.fields.push(this.listenerFieldForm);
} else {
this.fieldsListOfListener.splice(this.editingListenerFieldIndex, 1, this.listenerFieldForm);
this.listenerForm.fields.splice(this.editingListenerFieldIndex, 1, this.listenerFieldForm);
}
this.listenerFieldFormModelVisible = false;
this.$nextTick(() => (this.listenerFieldForm = {}));
},
// 移除监听器字段
removeListenerField(field, index) {
this.$confirm("确认移除该字段吗?", "提示", {
confirmButtonText: "确 认",
cancelButtonText: "取 消"
}).then(() => {
this.fieldsListOfListener.splice(index, 1);
this.listenerForm.fields.splice(index, 1);
}).catch(() => console.info("操作取消"));
},
// 移除监听器
removeListener(listener, index) {
this.$confirm("确认移除该监听器吗?", "提示", {
confirmButtonText: "确 认",
cancelButtonText: "取 消"
}).then(() => {
this.bpmnElementListeners.splice(index, 1);
this.elementListenersList.splice(index, 1);
updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
this.$emit('getTaskListenerCount', this.elementListenersList.length)
}).catch((r) => console.info(r, "操作取消"));
},
// 保存监听器配置
async saveListenerConfig() {
let validateStatus = await this.$refs["listenerFormRef"].validate();
if (!validateStatus) return; // 验证不通过直接返回
const listenerObject = createListenerObject(this.modelerStore.moddle, this.listenerForm, false, "flowable");
if (this.editingListenerIndex === -1) {
this.bpmnElementListeners.push(listenerObject);
this.elementListenersList.push(this.listenerForm);
} else {
this.bpmnElementListeners.splice(this.editingListenerIndex, 1, listenerObject);
this.elementListenersList.splice(this.editingListenerIndex, 1, this.listenerForm);
}
// 保存其他配置
this.otherExtensionList = this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type !== `flowable:TaskListener`) ?? [];
updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
this.$emit('getTaskListenerCount', this.elementListenersList.length)
// 4. 隐藏侧边栏
this.listenerFormModelVisible = false;
this.listenerForm = {};
},
initListenerType(listener) {
let listenerType;
if (listener.class) listenerType = "classListener";
if (listener.expression) listenerType = "expressionListener";
if (listener.delegateExpression) listenerType = "delegateExpressionListener";
if (listener.script) listenerType = "scriptListener";
return {
...JSON.parse(JSON.stringify(listener)),
...(listener.script ?? {}),
listenerType: listenerType
};
},
// 初始化表单数据
initListenerForm(listener) {
let self = {
...listener
};
if (listener.script) {
self = {
...listener,
...listener.script,
scriptType: listener.script.resource ? "externalScript" : "inlineScript"
};
}
if (listener.event === "timeout" && listener.eventDefinitions) {
if (listener.eventDefinitions.length) {
let k = "";
for (let key in listener.eventDefinitions[0]) {
console.log(listener.eventDefinitions, key);
if (key.indexOf("time") !== -1) {
k = key;
self.eventDefinitionType = key.replace("time", "").toLowerCase();
}
}
console.log(k);
self.eventTimeDefinitions = listener.eventDefinitions[0][k].body;
}
}
return self;
},
/** 查询流程达式列表 */
getList() {
this.loading = true;
listListener(this.queryParams).then(response => {
this.listenerList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 多选框选中数据
handleSelectionChange(selection) {
// ids.value = selection.map(item => item.id);
// TODO 应该使用 push?
this.checkedListenerData = selection;
this.listenerSystemChecked = selection.length !== 1;
},
// 保存内置监听器
saveSystemListener() {
if (this.checkedListenerData.length > 0) {
this.checkedListenerData.forEach(value => {
// 保存其他配置
const listenerObject = createSystemListenerObject(this.modelerStore.moddle, value, true, "flowable");
this.bpmnElementListeners.push(listenerObject);
this.elementListenersList.push(changeListenerObject(value));
this.otherExtensionList = this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type !== `flowable:TaskListener`) ?? [];
updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
})
// 回传显示数量
this.$emit('getTaskListenerCount', this.elementListenersList.length)
}
this.checkedListenerData = [];
this.listenerSystemChecked = true;
// 隐藏侧边栏
this.listenerSystemVisible = false;
}
}
}
</script>
<style lang="scss">
@import '../style/process-panel';
.flow-containers .el-badge__content.is-fixed {
top: 18px;
}
.dialog-footer button:first-child {
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,424 @@
<template>
<div>
<el-form label-width="80px" size="small">
<el-form-item label="异步">
<el-switch v-model="bpmnFormData.async" active-text="是" inactive-text="否" @change="updateElementTask('async')"/>
</el-form-item>
<el-form-item label="用户类型">
<el-select v-model="bpmnFormData.userType" placeholder="选择人员" @change="updateUserType">
<el-option
v-for="item in userTypeOption"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="指定人员" v-if="bpmnFormData.userType === 'assignee'">
<el-input-tag v-model="bpmnFormData.assignee" :value="bpmnFormData.assignee"/>
<el-button-group class="ml-4" style="margin-top: 4px">
<!--指定人员-->
<el-tooltip class="box-item" effect="dark" content="指定人员" placement="bottom">
<el-button size="mini" type="primary" icon="el-icon-user" @click="singleUserCheck"/>
</el-tooltip>
<!--选择表达式-->
<el-tooltip class="box-item" effect="dark" content="选择表达式" placement="bottom">
<el-button size="mini" type="warning" icon="el-icon-postcard" @click="singleExpCheck"/>
</el-tooltip>
</el-button-group>
</el-form-item>
<el-form-item label="候选人员" v-else-if="bpmnFormData.userType === 'candidateUsers'">
<el-input-tag v-model="bpmnFormData.candidateUsers" :value="bpmnFormData.candidateUsers"/>
<el-button-group class="ml-4" style="margin-top: 4px">
<!--候选人员-->
<el-tooltip class="box-item" effect="dark" content="候选人员" placement="bottom">
<el-button size="mini" type="primary" icon="el-icon-user" @click="multipleUserCheck"/>
</el-tooltip>
<!--选择表达式-->
<el-tooltip class="box-item" effect="dark" content="选择表达式" placement="bottom">
<el-button size="mini" type="warning" icon="el-icon-postcard" @click="singleExpCheck"/>
</el-tooltip>
</el-button-group>
</el-form-item>
<el-form-item label="候选角色" v-else>
<el-input-tag v-model="bpmnFormData.candidateGroups" :value="bpmnFormData.candidateGroups"/>
<el-button-group class="ml-4" style="margin-top: 4px">
<!--候选角色-->
<el-tooltip class="box-item" effect="dark" content="候选角色" placement="bottom">
<el-button size="mini" type="primary" icon="el-icon-user" @click="multipleRoleCheck"/>
</el-tooltip>
<!--选择表达式-->
<el-tooltip class="box-item" effect="dark" content="选择表达式" placement="bottom">
<el-button size="mini" type="warning" icon="el-icon-postcard" @click="singleExpCheck"/>
</el-tooltip>
</el-button-group>
</el-form-item>
<el-form-item label="优先级">
<el-input v-model="bpmnFormData.priority" @change="updateElementTask('priority')"/>
</el-form-item>
<el-form-item label="到期时间">
<el-input v-model="bpmnFormData.dueDate" @change="updateElementTask('dueDate')"/>
</el-form-item>
</el-form>
<!--选择人员-->
<el-dialog
title="选择人员"
:visible.sync="userVisible"
width="60%"
:close-on-press-escape="false"
:show-close="false"
>
<flow-user v-if="userVisible" :checkType="checkType" :selectValues="selectData.assignee || selectData.candidateUsers" @handleUserSelect="userSelect"></flow-user>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="userVisible = false"> </el-button>
<el-button size="small" type="primary" @click="checkUserComplete"> </el-button>
</div>
</el-dialog>
<!--选择角色-->
<el-dialog
title="选择候选角色"
:visible.sync="roleVisible"
width="60%"
:close-on-press-escape="false"
:show-close="false"
>
<flow-role v-if="roleVisible" :selectValues="selectData.candidateGroups" @handleRoleSelect="roleSelect"></flow-role>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="roleVisible = false"> </el-button>
<el-button size="small" type="primary" @click="checkRoleComplete"> </el-button>
</div>
</el-dialog>
<!--选择表达式-->
<el-dialog
title="选择表达式"
:visible.sync="expVisible"
width="60%"
:close-on-press-escape="false"
:show-close="false"
>
<flow-exp v-if="expVisible" :selectValues="selectData.exp" @handleSingleExpSelect="expSelect"></flow-exp>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="expVisible = false"> </el-button>
<el-button size="small" type="primary" @click="checkExpComplete"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import FlowUser from '@/components/flow/User'
import FlowRole from '@/components/flow/Role'
import FlowExp from '@/components/flow/Expression'
import ElInputTag from '@/components/flow/ElInputTag'
import {StrUtil} from '@/utils/StrUtil'
export default {
name: "TaskPanel",
components: {
FlowUser,
FlowRole,
FlowExp,
ElInputTag
},
/** 组件传值 */
props : {
id: {
type: String,
required: true
},
},
data() {
return {
userVisible: false,
roleVisible: false,
expVisible: false,
isIndeterminate: true,
checkType: 'single', // 选类
userType: '',
userTypeOption: [
{label: '指定人员', value: 'assignee'},
{label: '候选人员', value: 'candidateUsers'},
{label: '候选角色', value: 'candidateGroups'}
],
checkAll: false,
bpmnFormData: {
userType: "",
assignee: "",
candidateUsers: "",
candidateGroups: "",
dueDate: "",
priority: "",
dataType: "",
expId: "",
},
// 数据回显
selectData: {
assignee: null,
candidateUsers: null,
candidateGroups: null,
exp: null,
},
otherExtensionList:[],
}
},
/** 传值监听 */
watch: {
id: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.resetTaskForm();
}
},
immediate: true, // 立即生效
},
},
created() {
},
methods: {
// 初始化表单
resetTaskForm() {
// 初始化设为空值
this.bpmnFormData = {
userType: "",
assignee: "",
candidateUsers: "",
candidateGroups: "",
dueDate: "",
priority: "",
dataType: "",
expId: "",
}
this.selectData = {
assignee: null,
candidateUsers: null,
candidateGroups: null,
exp: null,
}
// 流程节点信息上取值
for (let key in this.bpmnFormData) {
const value = this.modelerStore.element?.businessObject[key] || this.bpmnFormData[key];
this.$set(this.bpmnFormData, key, value);
}
// 人员信息回显
this.checkValuesEcho(this.bpmnFormData);
},
// 更新节点信息
updateElementTask(key) {
const taskAttr = Object.create(null);
taskAttr[key] = this.bpmnFormData[key] || "";
this.modelerStore.modeling.updateProperties(this.modelerStore.element, taskAttr);
},
// 更新自定义流程节点/参数信息
updateCustomElement(key, value) {
const taskAttr = Object.create(null);
taskAttr[key] = value;
this.modelerStore.modeling.updateProperties(this.modelerStore.element, taskAttr);
},
// 更新人员类型
updateUserType(val) {
// 删除xml中已选择数据类型节点
this.deleteFlowAttar();
delete this.modelerStore.element.businessObject[`userType`]
// 清除已选人员数据
this.bpmnFormData[val] = null;
this.selectData = {
assignee: null,
candidateUsers: null,
candidateGroups: null,
exp: null,
}
// 写入userType节点信息到xml
this.updateCustomElement('userType', val);
},
// 设计器右侧表单数据回显
checkValuesEcho(formData) {
if (StrUtil.isNotBlank(formData.expId)) {
this.getExpList(formData.expId, formData.userType);
} else {
if ("candidateGroups" === formData.userType) {
this.getRoleList(formData[formData.userType], formData.userType);
} else {
this.getUserList(formData[formData.userType], formData.userType);
}
}
},
// 获取表达式信息
getExpList(val, key) {
if (StrUtil.isNotBlank(val)) {
this.bpmnFormData[key] = this.modelerStore.expList?.find(item => item.id.toString() === val).name;
this.selectData.exp = this.modelerStore.expList?.find(item => item.id.toString() === val).id;
}
},
// 获取人员信息
getUserList(val, key) {
if (StrUtil.isNotBlank(val)) {
const newArr = this.modelerStore.userList?.filter(i => val.split(',').includes(i.userId.toString()))
this.bpmnFormData[key] = newArr.map(item => item.nickName).join(',');
if ('assignee' === key) {
this.selectData[key] = newArr.find(item => item.userId.toString() === val).userId;
} else {
this.selectData[key] = newArr.map(item => item.userId);
}
}
},
// 获取角色信息
getRoleList(val, key) {
if (StrUtil.isNotBlank(val)) {
const newArr = this.modelerStore.roleList?.filter(i => val.split(',').includes(i.roleId.toString()))
this.bpmnFormData[key] = newArr.map(item => item.roleName).join(',');
if ('assignee' === key) {
this.selectData[key] = newArr.find(item => item.roleId.toString() === val).roleId;
} else {
this.selectData[key] = newArr.map(item => item.roleId);
}
}
},
// ------ 流程审批人员信息弹出框 start---------
/*单选人员*/
singleUserCheck() {
this.userVisible = true;
this.checkType = "single";
},
/*多选人员*/
multipleUserCheck() {
this.userVisible = true;
this.checkType = "multiple";
},
/*单选角色*/
singleRoleCheck() {
this.roleVisible = true;
this.checkType = "single";
},
/*多选角色*/
multipleRoleCheck() {
this.roleVisible = true;
},
/*单选表达式*/
singleExpCheck() {
this.expVisible = true;
},
// 表达式选中数据
expSelect(selection) {
if (selection) {
this.deleteFlowAttar();
this.bpmnFormData[this.bpmnFormData.userType] = selection.name;
this.updateCustomElement('dataType', selection.dataType);
this.updateCustomElement('expId', selection.id.toString());
this.updateCustomElement(this.bpmnFormData.userType, selection.expression);
this.handleSelectData("exp", selection.id);
}
},
// 用户选中数据 TODO: 后面更改为 点击确认按钮再赋值人员信息
userSelect(selection) {
if (selection) {
this.deleteFlowAttar();
this.updateCustomElement('dataType', 'fixed');
if (selection instanceof Array) {
const userIds = selection.map(item => item.userId);
const nickName = selection.map(item => item.nickName);
// userType = candidateUsers
this.bpmnFormData[this.bpmnFormData.userType] = nickName.join(',');
this.updateCustomElement(this.bpmnFormData.userType, userIds.join(','));
this.handleSelectData(this.bpmnFormData.userType, userIds);
} else {
// userType = assignee
this.bpmnFormData[this.bpmnFormData.userType] = selection.nickName;
this.updateCustomElement(this.bpmnFormData.userType, selection.userId);
this.handleSelectData(this.bpmnFormData.userType, selection.userId);
}
}
},
// 角色选中数据
roleSelect(selection, name) {
if (selection && name) {
this.deleteFlowAttar();
this.bpmnFormData[this.bpmnFormData.userType] = name;
this.updateCustomElement('dataType', 'fixed');
// userType = candidateGroups
this.updateCustomElement(this.bpmnFormData.userType, selection);
this.handleSelectData(this.bpmnFormData.userType, selection);
}
},
// 处理人员回显
handleSelectData(key, value) {
for (let oldKey in this.selectData) {
if (key !== oldKey) {
this.$set(this.selectData, oldKey, null);
} else {
this.$set(this.selectData, oldKey, value);
}
}
},
/*用户选中赋值*/
checkUserComplete() {
this.userVisible = false;
this.checkType = "";
},
/*候选角色选中赋值*/
checkRoleComplete() {
this.roleVisible = false;
},
/*表达式选中赋值*/
checkExpComplete() {
this.expVisible = false;
},
// 删除节点
deleteFlowAttar() {
delete this.modelerStore.element.businessObject[`dataType`]
delete this.modelerStore.element.businessObject[`expId`]
delete this.modelerStore.element.businessObject[`assignee`]
delete this.modelerStore.element.businessObject[`candidateUsers`]
delete this.modelerStore.element.businessObject[`candidateGroups`]
},
// 去重数据
unique(arr, code) {
const res = new Map();
return arr.filter((item) => !res.has(item[code]) && res.set(item[code], 1));
},
// 更新扩展属性信息
updateElementExtensions(properties) {
const extensions = this.modelerStore.moddle.create("bpmn:ExtensionElements", {
values: this.otherExtensionList.concat([properties])
});
this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
extensionElements: extensions
});
}
}
}
</script>

View File

@@ -0,0 +1,183 @@
.my-header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.flowMsgPopover {
display: none;
}
.tipBox {
width: 180px;
background: #fff;
border-radius: 4px;
border: 1px solid #ebeef5;
padding: 12px;
p{
line-height: 28px;
margin:0;
padding:0;
}
}
.cell-item {
display: flex;
align-items: center;
}
// bpmn 画布 logo
//.bjs-powered-by {
// display: none;
//}
.view-mode {
.el-header, .el-aside, .djs-palette, .bjs-powered-by {
display: none;
}
.el-loading-mask {
background-color: initial;
}
.el-loading-spinner {
display: none;
}
}
.containers {
width: 100%;
height: 100%;
.canvas {
width: 100%;
height: 100%;
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+")
}
.panel {
position: absolute;
right: 0;
top: 50px;
width: 300px;
}
.load {
margin-right: 10px;
}
.djs-palette {
left: 0px !important;
top: 0px;
border-top: none;
}
.djs-container svg {
min-height: 650px;
}
.overlays-div {
font-size: 10px;
color: red;
width: 100px;
top: -20px !important;
}
.flow-viewer {
position: relative;
padding: 0;
}
.flow-viewer .button-group {
display: flex;
position: absolute;
width: auto;
height: auto;
top: 10px;
right: 10px;
}
// 流程线
.highlight.djs-shape .djs-visual > :nth-child(1) {
fill: #56bb56 !important;
stroke: #56bb56 !important;
fill-opacity: 0.2 !important;
}
.highlight.djs-shape .djs-visual > :nth-child(2) {
fill: #56bb56 !important;
}
.highlight.djs-shape .djs-visual > path {
fill: #56bb56 !important;
fill-opacity: 0.2 !important;
stroke: #56bb56 !important;
}
.highlight.djs-connection > .djs-visual > path {
stroke: #56bb56 !important;
}
.highlight-todo.djs-connection > .djs-visual > path {
stroke: #eab24a !important;
stroke-dasharray: 4px !important;
fill-opacity: 0.2 !important;
}
.highlight-todo.djs-shape .djs-visual > :nth-child(1) {
stroke-dasharray: 5, 5;
stroke-dashoffset: 500;
stroke: #eab24a !important;
fill: rgba(252, 211, 127, 0.2) !important;
}
@keyframes draw {
100% {
stroke-dashoffset: 0;
}
}
.process-status {
position: absolute;
width: auto;
height: auto;
display: flex;
float: right;
top: 10px;
left: 10px;
font-size: 12px;
.intro {
color: #303133;
margin-top: 5px;
}
.finish {
background-color: #E8FFEA;
padding: 4px;
border: 1px solid rgba(0, 180, 42, 0.1);
border-radius: 3px;
color: #56bb56;
margin-right: 8px;
}
.processing {
background-color: #fcf5ea;
padding: 4px;
border: 1px solid #fce9c7;
border-radius: 3px;
color: #eab24a;
margin-right: 8px;
}
.todo {
padding: 4px;
background: #ECEDEE;
border: 1px solid rgba(204, 204, 204, 0.1);
border-radius: 3px;
color: #666666;
margin-right: 5px;
}
}
}

View File

@@ -0,0 +1,123 @@
//.process-panel__container {
// box-sizing: border-box;
// padding: 0 8px;
// border-left: 1px solid #eeeeee;
// box-shadow: 0 0 8px #cccccc;
// max-height: 100%;
// overflow-y: scroll;
//}
.panel-tab__title {
font-weight: 600;
padding: 0 8px;
font-size: 1.1em;
line-height: 1.2em;
i {
margin-right: 8px;
font-size: 1.2em;
}
}
.panel-tab__content {
width: 100%;
box-sizing: border-box;
//border-top: 1px solid #eeeeee;
padding: 8px 16px;
.panel-tab__content--title {
display: flex;
justify-content: space-between;
padding-bottom: 8px;
span {
flex: 1;
text-align: left;
}
}
}
.element-property {
width: 100%;
display: flex;
align-items: flex-start;
margin: 8px 0;
.element-property__label {
display: block;
width: 90px;
text-align: right;
overflow: hidden;
padding-right: 12px;
line-height: 32px;
font-size: 14px;
box-sizing: border-box;
}
.element-property__value {
flex: 1;
line-height: 32px;
}
.el-form-item {
width: 100%;
margin-bottom: 0;
padding-bottom: 18px;
}
}
.list-property {
flex-direction: column;
.element-listener-item {
width: 100%;
display: inline-grid;
grid-template-columns: 16px auto 32px 32px;
grid-column-gap: 8px;
}
.element-listener-item + .element-listener-item {
margin-top: 8px;
}
}
.listener-filed__title {
width: 100%;
justify-content: space-between;
align-items: center;
margin-top: 0;
span {
font-size: 14px;
}
i {
margin-right: 8px;
}
}
.element-drawer__button {
margin-top: 8px;
display: inline-flex;
justify-content: space-around;
}
.element-drawer__button > .el-button {
width: 100%;
}
.element-drawer__button_save {
margin-top: 8px;
width: 100%;
display: inline-flex;
justify-content: space-around;
}
.element-drawer__button_save > .el-button {
width: 100%;
}
.el-collapse-item__content {
padding-bottom: 0;
}
.el-input.is-disabled .el-input__inner {
color: #999999;
}
.el-form-item.el-form-item--mini {
margin-bottom: 0;
& + .el-form-item {
margin-top: 16px;
}
}
.el-drawer__header{
margin-bottom: 0;
border-bottom: 1px solid #e8e8e8;
padding: 16px 16px 8px 16px;
font-size: 18px;
color: #303133;
}
.el-drawer__body{
padding: 10px;
}

View File

@@ -0,0 +1,176 @@
<template>
<div class="containers">
<el-container style="align-items: stretch">
<el-main class="flow-viewer">
<div class="process-status">
<span class="intro">状态</span>
<div class="finish">已办理</div>
<div class="processing">处理中</div>
<div class="todo">未进行</div>
</div>
<!-- 流程图显示 -->
<div v-loading="loading" class="canvas" ref="flowCanvas"></div>
<!-- 按钮区域 -->
<el-button-group class="button-group">
<el-tooltip effect="dark" content="适中" placement="bottom">
<el-button size="small" icon="el-icon-rank" @click="fitViewport" />
</el-tooltip>
<el-tooltip effect="dark" content="放大" placement="bottom">
<el-button size="small" icon="el-icon-zoom-in" @click="zoomViewport(true)" />
</el-tooltip>
<el-tooltip effect="dark" content="缩小" placement="bottom">
<el-button size="small" icon="el-icon-zoom-out" @click="zoomViewport(false)" />
</el-tooltip>
</el-button-group>
</el-main>
</el-container>
</div>
</template>
<script>
import { CustomViewer as BpmnViewer } from "@/components/Process/common";
export default {
name: "BpmnViewer",
/** 组件传值 */
props: {
// 回显数据传值
flowData: {
type: Object,
default: () => {
},
required: false
},
procInsId: {
type: String,
default: ''
},
},
data() {
return {
bpmnViewer: null,
flowDetail: {},
loading: true,
}
},
/** 传值监听 */
watch: {
flowData: {
handler(newValue) {
if (Object.keys(newValue).length > 0) {
// 生成实例
this.bpmnViewer && this.bpmnViewer.destroy();
this.bpmnViewer = new BpmnViewer({
container: this.$refs.flowCanvas,
height: 'calc(100vh - 200px)',
});
this.loadFlowCanvas(newValue);
}
}
},
},
created() {
},
methods: {
// 加载流程图片
async loadFlowCanvas(flowData) {
try {
await this.bpmnViewer.importXML(flowData.xmlData);
await this.fitViewport();
// 流程线高亮设置
if (flowData.nodeData !== undefined && flowData.nodeData.length > 0 && this.procInsId) {
await this.fillColor(flowData.nodeData);
}
} catch (err) {
console.error(err.message, err.warnings)
}
},
// 让图能自适应屏幕
fitViewport() {
this.zoom = this.bpmnViewer.get('canvas').zoom("fit-viewport", "auto");
this.loading = false;
},
// 放大缩小
zoomViewport(zoomIn = true) {
this.zoom = this.bpmnViewer.get('canvas').zoom()
this.zoom += (zoomIn ? 0.1 : -0.1)
if (this.zoom >= 0.2) this.bpmnViewer.get('canvas').zoom(this.zoom);
},
// 设置高亮颜色的
fillColor(nodeData) {
const canvas = this.bpmnViewer.get('canvas')
this.bpmnViewer.getDefinitions().rootElements[0].flowElements.forEach(n => {
const completeTask = nodeData.find(m => m.key === n.id)
const todoTask = nodeData.find(m => !m.completed)
const endTask = nodeData[nodeData.length - 1]
if (n.$type === 'bpmn:UserTask') {
if (completeTask) {
canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo')
n.outgoing?.forEach(nn => {
const targetTask = nodeData.find(m => m.key === nn.targetRef.id)
if (targetTask) {
if (todoTask && completeTask.key === todoTask.key && !todoTask.completed) {
canvas.addMarker(nn.id, todoTask.completed ? 'highlight' : 'highlight-todo')
canvas.addMarker(nn.targetRef.id, todoTask.completed ? 'highlight' : 'highlight-todo')
} else {
canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo')
canvas.addMarker(nn.targetRef.id, targetTask.completed ? 'highlight' : 'highlight-todo')
}
}
})
}
}
// 排他网关
else if (n.$type === 'bpmn:ExclusiveGateway') {
if (completeTask) {
canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo')
n.outgoing?.forEach(nn => {
const targetTask = nodeData.find(m => m.key === nn.targetRef.id)
if (targetTask) {
canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo')
canvas.addMarker(nn.targetRef.id, targetTask.completed ? 'highlight' : 'highlight-todo')
}
})
}
}
// 并行网关
else if (n.$type === 'bpmn:ParallelGateway') {
if (completeTask) {
canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo')
n.outgoing?.forEach(nn => {
const targetTask = nodeData.find(m => m.key === nn.targetRef.id)
if (targetTask) {
canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo')
canvas.addMarker(nn.targetRef.id, targetTask.completed ? 'highlight' : 'highlight-todo')
}
})
}
} else if (n.$type === 'bpmn:StartEvent') {
n.outgoing.forEach(nn => {
const completeTask = nodeData.find(m => m.key === nn.targetRef.id)
if (completeTask) {
canvas.addMarker(nn.id, 'highlight')
canvas.addMarker(n.id, 'highlight')
return;
}
})
} else if (n.$type === 'bpmn:EndEvent') {
if (endTask.key === n.id && endTask.completed) {
canvas.addMarker(n.id, 'highlight')
return;
}
}
})
}
}
}
</script>
<style lang="scss">
@import "../style/flow-viewer.scss";
</style>

View File

@@ -0,0 +1,106 @@
<template>
<div ref="rightPanel" class="rightPanel-container">
<div class="rightPanel-background" />
<div class="rightPanel">
<div class="rightPanel-items">
<slot />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'RightPanel',
props: {
clickNotClose: {
default: false,
type: Boolean
}
},
computed: {
show: {
get() {
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val
})
}
}
},
watch: {
show(value) {
if (value && !this.clickNotClose) {
this.addEventClick()
}
}
},
mounted() {
this.addEventClick()
},
beforeDestroy() {
const elx = this.$refs.rightPanel
elx.remove()
},
methods: {
addEventClick() {
window.addEventListener('click', this.closeSidebar)
},
closeSidebar(evt) {
const parent = evt.target.closest('.el-drawer__body')
if (!parent) {
this.show = false
window.removeEventListener('click', this.closeSidebar)
}
}
}
}
</script>
<style lang="scss" scoped>
.rightPanel-background {
position: fixed;
top: 0;
left: 0;
opacity: 0;
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
background: rgba(0, 0, 0, .2);
z-index: -1;
}
.rightPanel {
width: 100%;
max-width: 260px;
height: 100vh;
position: fixed;
top: 0;
right: 0;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
transition: all .25s cubic-bezier(.7, .3, .1, 1);
transform: translate(100%);
background: #fff;
z-index: 40000;
}
.handle-button {
width: 48px;
height: 48px;
position: absolute;
left: -48px;
text-align: center;
font-size: 24px;
border-radius: 6px 0 0 6px !important;
z-index: 0;
pointer-events: auto;
cursor: pointer;
color: #fff;
line-height: 48px;
i {
font-size: 24px;
line-height: 48px;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div class="top-right-btn" :style="style">
<el-row>
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
<el-button size="mini" circle icon="el-icon-search" @click="toggleSearch()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button size="mini" circle icon="el-icon-refresh" @click="refresh()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
<el-button size="mini" circle icon="el-icon-menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
<el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
<el-button size="mini" circle icon="el-icon-menu" />
<el-dropdown-menu slot="dropdown">
<template v-for="item in columns">
<el-dropdown-item :key="item.key">
<el-checkbox :checked="item.visible" @change="checkboxChange($event, item.label)" :label="item.label" />
</el-dropdown-item>
</template>
</el-dropdown-menu>
</el-dropdown>
</el-tooltip>
</el-row>
<el-dialog :title="title" :visible.sync="open" append-to-body>
<el-transfer
:titles="['显示', '隐藏']"
v-model="value"
:data="columns"
@change="dataChange"
></el-transfer>
</el-dialog>
</div>
</template>
<script>
export default {
name: "RightToolbar",
data() {
return {
// 显隐数据
value: [],
// 弹出层标题
title: "显示/隐藏",
// 是否显示弹出层
open: false,
};
},
props: {
/* 是否显示检索条件 */
showSearch: {
type: Boolean,
default: true,
},
/* 显隐列信息 */
columns: {
type: Array,
},
/* 是否显示检索图标 */
search: {
type: Boolean,
default: true,
},
/* 显隐列类型transfer穿梭框、checkbox复选框 */
showColumnsType: {
type: String,
default: "checkbox",
},
/* 右外边距 */
gutter: {
type: Number,
default: 10,
},
},
computed: {
style() {
const ret = {};
if (this.gutter) {
ret.marginRight = `${this.gutter / 2}px`;
}
return ret;
}
},
created() {
if (this.showColumnsType == 'transfer') {
// 显隐列初始默认隐藏列
for (let item in this.columns) {
if (this.columns[item].visible === false) {
this.value.push(parseInt(item));
}
}
}
},
methods: {
// 搜索
toggleSearch() {
this.$emit("update:showSearch", !this.showSearch);
},
// 刷新
refresh() {
this.$emit("queryTable");
},
// 右侧列表元素变化
dataChange(data) {
for (let item in this.columns) {
const key = this.columns[item].key;
this.columns[item].visible = !data.includes(key);
}
},
// 打开显隐列dialog
showColumn() {
this.open = true;
},
// 勾选
checkboxChange(event, label) {
this.columns.filter(item => item.label == label)[0].visible = event;
}
},
};
</script>
<style lang="scss" scoped>
::v-deep .el-transfer__button {
border-radius: 50%;
padding: 12px;
display: block;
margin-left: 0px;
}
::v-deep .el-transfer__button:first-child {
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div>
<svg-icon icon-class="question" @click="goto" />
</div>
</template>
<script>
export default {
name: 'RuoYiDoc',
data() {
return {
url: 'https://www.yuque.com/u1024153/icipor/vh4wfl'
}
},
methods: {
goto() {
window.open(this.url)
}
}
}
</script>

View File

@@ -0,0 +1,21 @@
<template>
<div>
<svg-icon icon-class="github" @click="goto" />
</div>
</template>
<script>
export default {
name: 'RuoYiGit',
data() {
return {
url: 'https://gitee.com/tony2y/RuoYi-flowable'
}
},
methods: {
goto() {
window.open(this.url)
}
}
}
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
</div>
</template>
<script>
import screenfull from 'screenfull'
export default {
name: 'Screenfull',
data() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.destroy()
},
methods: {
click() {
if (!screenfull.isEnabled) {
this.$message({ message: '你的浏览器不支持全屏', type: 'warning' })
return false
}
screenfull.toggle()
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.isEnabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.isEnabled) {
screenfull.off('change', this.change)
}
}
}
}
</script>
<style scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;;
width: 20px;
height: 20px;
vertical-align: 10px;
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<el-dropdown trigger="click" @command="handleSetSize">
<div>
<svg-icon class-name="size-icon" icon-class="size" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
data() {
return {
sizeOptions: [
{ label: 'Default', value: 'default' },
{ label: 'Medium', value: 'medium' },
{ label: 'Small', value: 'small' },
{ label: 'Mini', value: 'mini' }
]
}
},
computed: {
size() {
return this.$store.getters.size
}
},
methods: {
handleSetSize(size) {
this.$ELEMENT.size = size
this.$store.dispatch('app/setSize', size)
this.refreshView()
this.$message({
message: 'Switch Size Success',
type: 'success'
})
},
refreshView() {
// In order to make the cached page re-rendered
this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
const { fullPath } = this.$route
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
}
}
}
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<el-color-picker
v-model="theme"
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ''
}
},
computed: {
defaultTheme() {
return this.$store.state.settings.theme
}
},
watch: {
defaultTheme: {
handler: function(val, oldVal) {
this.theme = val
},
immediate: true
},
async theme(val) {
await this.setTheme(val)
}
},
created() {
if(this.defaultTheme !== ORIGINAL_THEME) {
this.setTheme(this.defaultTheme)
}
},
methods: {
async setTheme(val) {
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
}
if (!this.chalk) {
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
await this.getCSSString(url, 'chalk')
}
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$emit('change', val)
},
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, variable) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
resolve()
}
}
xhr.open('GET', url)
xhr.send()
})
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<el-menu
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
>
<template v-for="(item, index) in topMenus">
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-submenu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template slot="title">更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item
:index="item.path"
:key="index"
v-if="index >= visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
</el-submenu>
</el-menu>
</template>
<script>
import { constantRoutes } from "@/router";
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile'];
export default {
data() {
return {
// 顶部栏初始数
visibleNumber: 5,
// 当前激活菜单的 index
currentIndex: undefined
};
},
computed: {
theme() {
return this.$store.state.settings.theme;
},
// 顶部显示菜单
topMenus() {
let topMenus = [];
this.routers.map((menu) => {
if (menu.hidden !== true) {
// 兼容顶部栏一级菜单内部跳转
if (menu.path === "/") {
topMenus.push(menu.children[0]);
} else {
topMenus.push(menu);
}
}
});
return topMenus;
},
// 所有的路由信息
routers() {
return this.$store.state.permission.topbarRouters;
},
// 设置子路由
childrenMenus() {
var childrenMenus = [];
this.routers.map((router) => {
for (var item in router.children) {
if (router.children[item].parentPath === undefined) {
if(router.path === "/") {
router.children[item].path = "/" + router.children[item].path;
} else {
if(!this.ishttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path;
}
}
router.children[item].parentPath = router.path;
}
childrenMenus.push(router.children[item]);
}
});
return constantRoutes.concat(childrenMenus);
},
// 默认激活的菜单
activeMenu() {
const path = this.$route.path;
let activePath = path;
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
if (!this.$route.meta.link) {
this.$store.dispatch('app/toggleSideBarHide', false);
}
} else if(!this.$route.children) {
activePath = path;
this.$store.dispatch('app/toggleSideBarHide', true);
}
this.activeRoutes(activePath);
return activePath;
},
},
beforeMount() {
window.addEventListener('resize', this.setVisibleNumber)
},
beforeDestroy() {
window.removeEventListener('resize', this.setVisibleNumber)
},
mounted() {
this.setVisibleNumber();
},
methods: {
// 根据宽度计算设置显示栏数
setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3;
this.visibleNumber = parseInt(width / 85);
},
// 菜单选择事件
handleSelect(key, keyPath) {
this.currentIndex = key;
const route = this.routers.find(item => item.path === key);
if (this.ishttp(key)) {
// http(s):// 路径新窗口打开
window.open(key, "_blank");
} else if (!route || !route.children) {
// 没有子路由路径内部打开
const routeMenu = this.childrenMenus.find(item => item.path === key);
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query);
this.$router.push({ path: key, query: query });
} else {
this.$router.push({ path: key });
}
this.$store.dispatch('app/toggleSideBarHide', true);
} else {
// 显示左侧联动菜单
this.activeRoutes(key);
this.$store.dispatch('app/toggleSideBarHide', false);
}
},
// 当前激活的路由
activeRoutes(key) {
var routes = [];
if (this.childrenMenus && this.childrenMenus.length > 0) {
this.childrenMenus.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item);
}
});
}
if(routes.length > 0) {
this.$store.commit("SET_SIDEBAR_ROUTERS", routes);
} else {
this.$store.dispatch('app/toggleSideBarHide', true);
}
},
ishttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
},
};
</script>
<style lang="scss">
.topmenu-container.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-submenu.is-active .el-submenu__title {
border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133;
}
/* submenu item */
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
</style>

View File

@@ -0,0 +1,12 @@
import inherits from "inherits";
import Viewer from "bpmn-js/lib/Viewer";
import ZoomScrollModule from "diagram-js/lib/navigation/zoomscroll";
import MoveCanvasModule from "diagram-js/lib/navigation/movecanvas";
function CustomViewer(options) {
Viewer.call(this, options);
}
inherits(CustomViewer, Viewer);
CustomViewer.prototype._modules = [].concat(Viewer.prototype._modules, [ZoomScrollModule, MoveCanvasModule]);
export {
CustomViewer
};

View File

@@ -0,0 +1,191 @@
<template>
<div
class="el-input-tag input-tag-wrapper"
:class="[size ? 'el-input-tag--' + size : '']"
@click="focusTagInput">
<el-tag
v-for="(tag, idx) in innerTags"
v-bind="$attrs"
:key="tag"
:size="size"
effect="dark"
closable
:disable-transitions="false"
@close="remove(idx)">
{{tag}}
</el-tag>
<input
v-if="!readOnly"
class="tag-input"
:placeholder="placeholder"
@input="inputTag"
:value="newTag"
@keydown.delete.stop = "removeLastTag"
/>
<!-- @keydown = "addNew"
@blur = "addNew"-->
</div>
</template>
<script>
import {StrUtil} from '@/utils/StrUtil'
export default {
name: "ElInputTag",
/** 组件传值 */
props : {
value: {
type: String,
default: ""
},
addTagOnKeys: {
type: Array,
default: () => []
},
size: {
type: String,
default: 'small'
},
placeholder: String,
},
data() {
return {
newTag :"",
innerTags :[],
readOnly :true,
}
},
/** 传值监听 */
watch: {
value: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.innerTags = newVal.split(',');
}else {
this.innerTags = [];
}
},
immediate: true, // 立即生效
},
},
methods: {
focusTagInput() {
if (this.readOnly || !this.$el.querySelector('.tag-input')) {
return
} else {
this.$el.querySelector('.tag-input').focus()
}
},
inputTag(ev) {
this.newTag = ev.target.value
},
addNew(e) {
if (e && (!this.addTagOnKeys.includes(e.keyCode)) && (e.type !== 'blur')) {
return
}
if (e) {
e.stopPropagation()
e.preventDefault()
}
let addSuccess = false
if (this.newTag.includes(',')) {
this.newTag.split(',').forEach(item => {
if (this.addTag(item.trim())) {
addSuccess = true
}
})
} else {
if (this.addTag(this.newTag.trim())) {
addSuccess = true
}
}
if (addSuccess) {
this.tagChange()
this.newTag = ''
}
},
addTag(tag) {
tag = tag.trim()
if (tag && !this.innerTags.includes(tag)) {
this.innerTags.push(tag)
return true
}
return false
},
remove(index) {
this.innerTags.splice(index, 1)
this.tagChange();
},
removeLastTag() {
if (this.newTag) {
return
}
this.innerTags.pop()
this.tagChange()
},
tagChange() {
this.$emit('input', this.innerTags)
}
}
}
</script>
<style scoped>
.el-form-item.is-error .el-input-tag {
border-color: #f56c6c;
}
.input-tag-wrapper {
position: relative;
font-size: 14px;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
outline: none;
padding: 0 10px 0 5px;
transition: border-color .2s cubic-bezier(.645,.045,.355,1);
width: 100%;
}
.el-tag {
margin-right: 4px;
}
.tag-input {
background: transparent;
border: 0;
font-size: inherit;
outline: none;
padding-left: 0;
width: 100px;
}
.el-input-tag {
min-height: 42px;
}
.el-input-tag--small {
min-height: 32px;
line-height: 32px;
font-size: 12px;
}
.el-input-tag--default {
min-height: 34px;
line-height: 34px;
}
.el-input-tag--large {
min-height: 36px;
line-height: 36px;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入表达式名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="expressionList" @current-change="handleSingleExpSelect">
<el-table-column width="55" align="center" >
<template slot-scope="scope">
<!-- 可以手动的修改label的值从而控制选择哪一项 -->
<el-radio v-model="radioSelected" :label="scope.row.id">{{''}}</el-radio>
</template>
</el-table-column>
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="表达式内容" align="center" prop="expression" />
<el-table-column label="表达式类型" align="center" prop="dataType" >
<template slot-scope="scope">
<dict-tag :options="dict.type.exp_data_type" :value="scope.row.dataType"/>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page-sizes="[5,10]"
layout="prev, pager, next"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script>
import { listExpression } from "@/api/flowable/expression";
import {StrUtil} from "@/utils/StrUtil";
export default {
name: "Expression",
dicts: ['sys_common_status','exp_data_type'],
// 接受父组件的值
props: {
// 回显数据传值
selectValues: {
type: Number | String,
default: null,
required: false
}
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 流程达式表格数据
expressionList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
name: null,
expression: null,
status: null,
},
radioSelected: null // 单选框传值
};
},
watch: {
selectValues: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
this.radioSelected = newVal
}
},
immediate: true,
}
},
created() {
this.getList();
},
methods: {
/** 查询流程达式列表 */
getList() {
this.loading = true;
listExpression(this.queryParams).then(response => {
this.expressionList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 单选框选中数据
handleSingleExpSelect(selection) {
this.radioSelected = selection.id;//点击当前行时,radio同样有选中效果
this.$emit('handleSingleExpSelect',selection);
},
}
};
</script>

View File

@@ -0,0 +1,187 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
<el-form-item label="角色名称" prop="roleName">
<el-input
v-model="queryParams.roleName"
placeholder="请输入角色名称"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-show="checkType === 'multiple'" ref="dataTable" v-loading="loading" :data="roleList" @selection-change="handleMultipleRoleSelect">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="角色编号" prop="roleId" width="120" />
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
<el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="150" />
<el-table-column label="显示顺序" prop="roleSort" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
<el-table v-show="checkType === 'single'" v-loading="loading" :data="roleList" @current-change="handleSingleRoleSelect">
<el-table-column width="55" align="center" >
<template slot-scope="scope">
<!-- 可以手动的修改label的值从而控制选择哪一项 -->
<el-radio v-model="radioSelected" :label="scope.row.roleId">{{''}}</el-radio>
</template>
</el-table-column>
<el-table-column label="角色编号" prop="roleId" width="120" />
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
<el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="150" />
<el-table-column label="显示顺序" prop="roleSort" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page-sizes="[5,10]"
layout="prev, pager, next"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script>
import { listRole} from "@/api/system/role";
import {StrUtil} from "@/utils/StrUtil";
export default {
name: "FlowRole",
dicts: ['sys_normal_disable'],
// 接受父组件的值
props: {
// 回显数据传值
selectValues: {
type: Number | String | Array,
default: null,
required: false
},
checkType: {
type: String,
default: 'multiple',
required: false
},
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 角色表格数据
roleList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 5,
roleName: undefined,
roleKey: undefined,
status: undefined
},
// 表单参数
form: {},
radioSelected: 0, // 单选框传值
selectRoleList: [] // 回显数据传值
};
},
watch: {
selectValues: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
if (newVal instanceof Number || newVal instanceof String) {
this.radioSelected = newVal
} else {
this.selectRoleList = newVal;
}
}
},
immediate: true
},
roleList: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal) && this.selectRoleList.length > 0) {
this.$nextTick(() => {
this.$refs.dataTable.clearSelection();
this.selectRoleList?.split(',').forEach(key => {
this.$refs.dataTable.toggleRowSelection(newVal.find(
item => key == item.roleId
), true)
});
});
}
}
}
},
created() {
this.getList();
},
methods: {
/** 查询角色列表 */
getList() {
this.loading = true;
listRole(this.queryParams).then(response => {
this.roleList = response.rows;
this.total = response.total;
this.loading = false;
}
);
},
// 多选框选中数据
handleMultipleRoleSelect(selection) {
const idList = selection.map(item => item.roleId);
const nameList = selection.map(item => item.roleName);
this.$emit('handleRoleSelect', idList.join(','), nameList.join(','));
},
// 单选框选中数据
handleSingleRoleSelect(selection) {
this.radioSelected = selection.roleId;
const roleName = selection.roleName;
this.$emit('handleRoleSelect', this.radioSelected.toString(), roleName);
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.handleQuery();
},
}
};
</script>
<style>
/*隐藏radio展示的label及本身自带的样式*/
/*.el-radio__label{*/
/* display:none;*/
/*}*/
</style>

View File

@@ -0,0 +1,257 @@
<template>
<div>
<el-row :gutter="20">
<!--部门数据-->
<el-col :span="6" :xs="24">
<div class="head-container">
<el-input
v-model="deptName"
placeholder="请输入部门名称"
clearable
size="small"
prefix-icon="el-icon-search"
style="margin-bottom: 20px"
/>
</div>
<div class="head-container">
<el-tree
:data="deptOptions"
:props="defaultProps"
:expand-on-click-node="false"
:filter-node-method="filterNode"
ref="tree"
node-key="id"
default-expand-all
highlight-current
@node-click="handleNodeClick"
/>
</div>
</el-col>
<!--用户数据-->
<el-col :span="18" :xs="24">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="用户名称" prop="userName">
<el-input
v-model="queryParams.userName"
placeholder="请输入用户名称"
clearable
style="width: 150px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-show="checkType === 'multiple'" ref="dataTable" v-loading="loading" :row-key="getRowKey" :data="userList" @selection-change="handleMultipleUserSelect">
<el-table-column type="selection" :reserve-selection="true" width="50" align="center" />
<el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
<el-table-column label="登录账号" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
<el-table-column label="用户姓名" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
<el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns[3].visible" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
</el-table>
<el-table v-show="checkType === 'single'" v-loading="loading" :data="userList" @current-change="handleSingleUserSelect">
<el-table-column width="55" align="center" >
<template slot-scope="scope">
<el-radio v-model="radioSelected" :label="scope.row.userId">{{''}}</el-radio>
</template>
</el-table-column>
<el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
<el-table-column label="登录账号" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
<el-table-column label="用户姓名" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
<el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns[3].visible" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
</el-table>
<pagination
v-show="total>0"
:total="total"
:page-sizes="[5,10]"
layout="prev, pager, next"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-col>
</el-row>
</div>
</template>
<script>
import { listUser, deptTreeSelect } from "@/api/system/user";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import {StrUtil} from '@/utils/StrUtil'
export default {
name: "FlowUser",
dicts: ['sys_normal_disable', 'sys_user_sex'],
components: { Treeselect },
// 接受父组件的值
props: {
// 回显数据传值
selectValues: {
type: Number | String | Array,
default: null,
required: false
},
// 表格类型
checkType: {
type: String,
default: 'multiple',
required: true
},
},
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 用户表格数据
userList: [],
// 弹出层标题
title: "",
// 部门树选项
deptOptions: undefined,
// 是否显示弹出层
open: false,
// 部门名称
deptName: undefined,
// 表单参数
form: {},
defaultProps: {
children: "children",
label: "label"
},
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 5,
userName: undefined,
phonenumber: undefined,
status: undefined,
deptId: undefined
},
// 列信息
columns: [
{ key: 0, label: `用户编号`, visible: true },
{ key: 1, label: `用户名称`, visible: true },
{ key: 2, label: `用户昵称`, visible: true },
{ key: 3, label: `部门`, visible: true },
{ key: 4, label: `手机号码`, visible: true },
{ key: 5, label: `状态`, visible: true },
{ key: 6, label: `创建时间`, visible: true }
],
radioSelected: 0, // 单选框传值
selectUserList: [] // 回显数据传值
};
},
watch: {
// 根据名称筛选部门树
deptName(val) {
this.$refs.tree.filter(val);
},
selectValues: {
handler(newVal) {
if (StrUtil.isNotBlank(newVal)) {
if (newVal instanceof Number) {
this.radioSelected = newVal
} else {
this.selectUserList = newVal;
}
}
},
immediate: true
},
userList: {
handler(newVal) {
debugger
if (StrUtil.isNotBlank(newVal) && this.selectUserList.length > 0) {
this.$nextTick(() => {
this.$refs.dataTable.clearSelection();
this.selectUserList?.split(',').forEach(key => {
this.$refs.dataTable.toggleRowSelection(newVal.find(
item => key == item.userId
), true)
});
});
}
}
}
},
created() {
this.getList();
this.getDeptTree();
},
methods: {
/** 查询用户列表 */
getList() {
this.loading = true;
listUser(this.queryParams).then(response => {
this.userList = response.rows;
this.total = response.total;
this.loading = false;
}
);
},
/** 查询部门下拉树结构 */
getDeptTree() {
deptTreeSelect().then(response => {
this.deptOptions = response.data;
});
},
// 保存选中的数据id,row-key就是要指定一个key标识这一行的数据
getRowKey (row) {
return row.id
},
// 筛选节点
filterNode(value, data) {
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
// 节点单击事件
handleNodeClick(data) {
this.queryParams.deptId = data.id;
this.handleQuery();
},
// 多选框选中数据
handleMultipleUserSelect(selection) {
this.$emit('handleUserSelect', selection);
},
// 单选框选中数据
handleSingleUserSelect(selection) {
this.radioSelected = selection.userId;//点击当前行时,radio同样有选中效果
this.$emit('handleUserSelect', selection);
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = [];
this.resetForm("queryForm");
this.queryParams.deptId = undefined;
this.$refs.tree.setCurrentKey(null);
this.handleQuery();
},
}
};
</script>
<style>
/*隐藏radio展示的label及本身自带的样式*/
/*.el-radio__label{*/
/* display:none;*/
/*}*/
</style>

View File

@@ -0,0 +1,36 @@
<template>
<div v-loading="loading" :style="'height:' + height">
<iframe
:src="src"
frameborder="no"
style="width: 100%; height: 100%"
scrolling="auto"
/>
</div>
</template>
<script>
export default {
props: {
src: {
type: String,
required: true
},
},
data() {
return {
height: document.documentElement.clientHeight - 94.5 + "px;",
loading: true,
url: this.src
};
},
mounted: function () {
setTimeout(() => {
this.loading = false;
}, 300);
const that = this;
window.onresize = function temp() {
that.height = document.documentElement.clientHeight - 94.5 + "px;";
};
}
};
</script>

View File

@@ -0,0 +1,257 @@
<script>
import { deepClone } from '../../utils/index'
import render from '../render/render.js'
const ruleTrigger = {
'el-input': 'blur',
'el-input-number': 'blur',
'el-select': 'change',
'el-radio-group': 'change',
'el-checkbox-group': 'change',
'el-cascader': 'change',
'el-time-picker': 'change',
'el-date-picker': 'change',
'el-rate': 'change'
}
const layouts = {
colFormItem(h, scheme) {
const config = scheme.__config__
const listeners = buildListeners.call(this, scheme)
let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null
if (config.showLabel === false) labelWidth = '0'
return (
<el-col span={config.span}>
<el-form-item label-width={labelWidth} prop={scheme.__vModel__}
label={config.showLabel ? config.label : ''}>
<render conf={scheme} on={listeners} />
</el-form-item>
</el-col>
)
},
rowFormItem(h, scheme) {
let child = renderChildren.apply(this, arguments)
if (scheme.type === 'flex') {
child = <el-row type={scheme.type} justify={scheme.justify} align={scheme.align}>
{child}
</el-row>
}
return (
<el-col span={scheme.span}>
<el-row gutter={scheme.gutter}>
{child}
</el-row>
</el-col>
)
}
}
function renderFrom(h) {
const { formConfCopy } = this
return (
<el-row gutter={formConfCopy.gutter}>
<el-form
size={formConfCopy.size}
label-position={formConfCopy.labelPosition}
disabled={formConfCopy.disabled}
label-width={`${formConfCopy.labelWidth}px`}
ref={formConfCopy.formRef}
// model不能直接赋值 https://github.com/vuejs/jsx/issues/49#issuecomment-472013664
props={{ model: this[formConfCopy.formModel] }}
rules={this[formConfCopy.formRules]}
>
{renderFormItem.call(this, h, formConfCopy.fields)}
{formConfCopy.formBtns && formBtns.call(this, h)}
</el-form>
</el-row>
)
}
function formBtns(h) {
return <el-col>
<el-form-item size="large">
<el-button type="primary" onClick={this.submitForm}>提交</el-button>
<el-button onClick={this.resetForm}>重置</el-button>
</el-form-item>
</el-col>
}
function renderFormItem(h, elementList) {
if (elementList) {
return elementList.map(scheme => {
const config = scheme.__config__
const layout = layouts[config.layout]
if (layout) {
return layout.call(this, h, scheme)
}
throw new Error(`没有与${config.layout}匹配的layout`)
})
}
}
function renderChildren(h, scheme) {
const config = scheme.__config__
if (!Array.isArray(config.children)) return null
return renderFormItem.call(this, h, config.children)
}
function setValue(event, config, scheme) {
this.$set(config, 'defaultValue', event)
this.$set(this[this.formConf.formModel], scheme.__vModel__, event)
}
function buildListeners(scheme) {
const config = scheme.__config__;
const methods = this.formConf.__methods__ || {};
const listeners = {};
// 给__methods__中的方法绑定this和event
Object.keys(methods).forEach((key) => {
listeners[key] = (event) => methods[key].call(this, event);
});
// 响应 render.js 中的 vModel $emit('input', val)
listeners.input = (event) => setValue.call(this, event, config, scheme);
// 上传表单元素组件的成功、移除和预览事件
if (config.tag === "el-upload") {
listeners.upload = (response, file, fileList) =>
setUpload.call(this, config, scheme, response, file, fileList);
listeners.deleteUpload = (file, fileList) =>
deleteUpload.call(this, config, scheme, file, fileList);
listeners.previewUpload = (file, fileList) =>
window.open(file.url, "_blank");
}
return listeners;
}
//获取上传表单元素组件 上传的文件
function setUpload(config, scheme, response, file, fileList) {
//response: 上传接口返回的数据
let fileObj = {
name: response.fileName,
url: response.url,
fileType: response.contentType,
};
let oldValue = "";
try {
oldValue = JSON.parse(this[this.formConf.formModel][scheme.__vModel__]);
} catch (err) {
console.warn(err);
}
if (oldValue) {
oldValue.push(fileObj);
} else {
oldValue = [fileObj];
}
this.$set(config, "defaultValue", JSON.stringify(oldValue));
this.$set(
this[this.formConf.formModel],
scheme.__vModel__,
JSON.stringify(oldValue)
);
}
//获取上传表单元素组件 删除文件后的文件列表
function deleteUpload(config, scheme, file, fileList) {
let oldValue = JSON.parse(this[this.formConf.formModel][scheme.__vModel__]);
//file 删除的文件
//过滤掉删除的文件
let newValue = oldValue.filter((item) => item.name !== file.name);
this.$set(config, "defaultValue", JSON.stringify(newValue));
this.$set(
this[this.formConf.formModel],
scheme.__vModel__,
JSON.stringify(newValue)
);
}
export default {
components: {
render
},
props: {
formConf: {
type: Object,
required: true
}
},
data() {
const data = {
formConfCopy: deepClone(this.formConf),
[this.formConf.formModel]: {},
[this.formConf.formRules]: {}
}
if (data.formConfCopy.fields) {
this.initFormData(data.formConfCopy.fields, data[this.formConf.formModel])
this.buildRules(data.formConfCopy.fields, data[this.formConf.formRules])
}
return data
},
methods: {
initFormData(componentList, formData) {
componentList.forEach(cur => {
const config = cur.__config__
if (cur.__vModel__) formData[cur.__vModel__] = config.defaultValue
if (config.children) this.initFormData(config.children, formData)
})
},
buildRules(componentList, rules) {
componentList.forEach(cur => {
const config = cur.__config__
if (Array.isArray(config.regList)) {
if (config.required) {
const required = { required: config.required, message: cur.placeholder }
if (Array.isArray(config.defaultValue)) {
required.type = 'array'
required.message = `请至少选择一个${config.label}`
}
required.message === undefined && (required.message = `${config.label}不能为空`)
config.regList.push(required)
}
rules[cur.__vModel__] = config.regList.map(item => {
item.pattern && (item.pattern = eval(item.pattern))
item.trigger = ruleTrigger && ruleTrigger[config.tag]
return item
})
}
if (config.children) this.buildRules(config.children, rules)
})
},
resetForm() {
this.formConfCopy = deepClone(this.formConf)
this.$refs[this.formConf.formRef].resetFields()
},
submitForm() {
this.$refs[this.formConf.formRef].validate(valid => {
if (!valid) return false
// 触发sumit事件
// this.$emit('submit', this[this.formConf.formModel])
const params = {
formData: this.formConfCopy,
valData: this[this.formConf.formModel]
}
this.$emit('submit', params)
return true
})
},
// 传值给父组件
getData(){
this.$emit('getData', this[this.formConf.formModel])
// this.$emit('getData',this.formConfCopy)
}
},
render(h) {
return renderFrom.call(this, h)
}
}
</script>

View File

@@ -0,0 +1,17 @@
## form-generator JSON 解析器
>用于将form-generator导出的JSON解析成一个表单。
### 安装组件
```
npm i form-gen-parser
```
或者
```
yarn add form-gen-parser
```
### 使用示例
> [查看在线示例](https://mrhj.gitee.io/form-generator/#/parser)
示例代码:
> [src\components\parser\example\Index.vue](https://github.com/JakHuang/form-generator/blob/dev/src/components/parser/example/Index.vue)

View File

@@ -0,0 +1,324 @@
<template>
<div class="test-form">
<parser :form-conf="formConf" @submit="sumbitForm1" />
<parser :key="key2" :form-conf="formConf" @submit="sumbitForm2" />
<el-button @click="change">
change
</el-button>
</div>
</template>
<script>
import Parser from '../Parser'
// 若parser是通过安装npm方式集成到项目中的使用此行引入
// import Parser from 'form-gen-parser'
export default {
components: {
Parser
},
props: {},
data() {
return {
key2: +new Date(),
formConf: {
fields: [
{
__config__: {
label: '单行文本',
labelWidth: null,
showLabel: true,
changeTag: true,
tag: 'el-input',
tagIcon: 'input',
required: true,
layout: 'colFormItem',
span: 24,
document: 'https://element.eleme.cn/#/zh-CN/component/input',
regList: [
{
pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
message: '手机号格式错误'
}
]
},
__slot__: {
prepend: '',
append: ''
},
__vModel__: 'mobile',
placeholder: '请输入手机号',
style: {
width: '100%'
},
clearable: true,
'prefix-icon': 'el-icon-mobile',
'suffix-icon': '',
maxlength: 11,
'show-word-limit': true,
readonly: false,
disabled: false
},
{
__config__: {
label: '日期范围',
tag: 'el-date-picker',
tagIcon: 'date-range',
defaultValue: null,
span: 24,
showLabel: true,
labelWidth: null,
required: true,
layout: 'colFormItem',
regList: [],
changeTag: true,
document:
'https://element.eleme.cn/#/zh-CN/component/date-picker',
formId: 101,
renderKey: 1585980082729
},
style: {
width: '100%'
},
type: 'daterange',
'range-separator': '至',
'start-placeholder': '开始日期',
'end-placeholder': '结束日期',
disabled: false,
clearable: true,
format: 'yyyy-MM-dd',
'value-format': 'yyyy-MM-dd',
readonly: false,
__vModel__: 'field101'
},
{
__config__: {
layout: 'rowFormItem',
tagIcon: 'row',
label: '行容器',
layoutTree: true,
children: [
{
__config__: {
label: '评分',
tag: 'el-rate',
tagIcon: 'rate',
defaultValue: 0,
span: 24,
showLabel: true,
labelWidth: null,
layout: 'colFormItem',
required: true,
regList: [],
changeTag: true,
document: 'https://element.eleme.cn/#/zh-CN/component/rate',
formId: 102,
renderKey: 1586839671259
},
style: {},
max: 5,
'allow-half': false,
'show-text': false,
'show-score': false,
disabled: false,
__vModel__: 'field102'
}
],
document: 'https://element.eleme.cn/#/zh-CN/component/layout',
formId: 101,
span: 24,
renderKey: 1586839668999,
componentName: 'row101',
gutter: 15
},
type: 'default',
justify: 'start',
align: 'top'
},
{
__config__: {
label: '按钮',
showLabel: true,
changeTag: true,
labelWidth: null,
tag: 'el-button',
tagIcon: 'button',
span: 24,
layout: 'colFormItem',
document: 'https://element.eleme.cn/#/zh-CN/component/button',
renderKey: 1594288459289
},
__slot__: {
default: '测试按钮1'
},
type: 'primary',
icon: 'el-icon-search',
round: false,
size: 'medium',
plain: false,
circle: false,
disabled: false,
on: {
click: 'clickTestButton1'
}
}
],
__methods__: {
clickTestButton1() {
console.log(
`%c【测试按钮1】点击事件里可以访问当前表单
1) formModel='formData', 所以this.formData可以拿到当前表单的model
2) formRef='elForm', 所以this.$refs.elForm可以拿到当前表单的ref(vue组件)
`,
'color:#409EFF;font-size: 15px'
)
console.log('表单的Model', this.formData)
console.log('表单的ref', this.$refs.elForm)
}
},
formRef: 'elForm',
formModel: 'formData',
size: 'small',
labelPosition: 'right',
labelWidth: 100,
formRules: 'rules',
gutter: 15,
disabled: false,
span: 24,
formBtns: true,
unFocusedComponentBorder: false
},
formConf2: {
fields: [
{
__config__: {
label: '单行文本',
labelWidth: null,
showLabel: true,
changeTag: true,
tag: 'el-input',
tagIcon: 'input',
required: true,
layout: 'colFormItem',
span: 24,
document: 'https://element.eleme.cn/#/zh-CN/component/input',
regList: [
{
pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
message: '手机号格式错误'
}
]
},
__slot__: {
prepend: '',
append: ''
},
__vModel__: 'mobile',
placeholder: '请输入手机号',
style: {
width: '100%'
},
clearable: true,
'prefix-icon': 'el-icon-mobile',
'suffix-icon': '',
maxlength: 11,
'show-word-limit': true,
readonly: false,
disabled: false
},
{
__config__: {
label: '日期范围',
tag: 'el-date-picker',
tagIcon: 'date-range',
defaultValue: null,
span: 24,
showLabel: true,
labelWidth: null,
required: true,
layout: 'colFormItem',
regList: [],
changeTag: true,
document:
'https://element.eleme.cn/#/zh-CN/component/date-picker',
formId: 101,
renderKey: 1585980082729
},
style: {
width: '100%'
},
type: 'daterange',
'range-separator': '至',
'start-placeholder': '开始日期',
'end-placeholder': '结束日期',
disabled: false,
clearable: true,
format: 'yyyy-MM-dd',
'value-format': 'yyyy-MM-dd',
readonly: false,
__vModel__: 'field101'
}
],
formRef: 'elForm',
formModel: 'formData',
size: 'small',
labelPosition: 'right',
labelWidth: 100,
formRules: 'rules',
gutter: 15,
disabled: false,
span: 24,
formBtns: true,
unFocusedComponentBorder: false
}
}
},
computed: {},
watch: {},
created() {},
mounted() {
// 表单数据回填,模拟异步请求场景
setTimeout(() => {
// 请求回来的表单数据
const data = {
mobile: '18836662555'
}
// 回填数据
this.fillFormData(this.formConf, data)
// 更新表单
this.key2 = +new Date()
}, 2000)
},
methods: {
fillFormData(form, data) {
form.fields.forEach(item => {
const val = data[item.__vModel__]
if (val) {
item.__config__.defaultValue = val
}
})
},
change() {
this.key2 = +new Date()
const t = this.formConf
this.formConf = this.formConf2
this.formConf2 = t
},
sumbitForm1(data) {
console.log('sumbitForm1提交数据', data)
},
sumbitForm2(data) {
console.log('sumbitForm2提交数据', data)
}
}
}
</script>
<style lang="scss" scoped>
.test-form {
margin: 15px auto;
width: 800px;
padding: 15px;
}
</style>

View File

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

View File

@@ -0,0 +1,25 @@
{
"name": "form-gen-parser",
"version": "1.0.3",
"description": "表单json解析器",
"main": "lib/form-gen-parser.umd.js",
"directories": {
"example": "example"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/JakHuang/form-generator.git"
},
"dependencies": {
"form-gen-render": "^1.0.0"
},
"author": "jakHuang",
"license": "MIT",
"bugs": {
"url": "https://github.com/JakHuang/form-generator/issues"
},
"homepage": "https://github.com/JakHuang/form-generator/blob/dev/src/components/parser"
}

View File

@@ -0,0 +1,19 @@
{
"name": "form-gen-render",
"version": "1.0.4",
"description": "表单核心render",
"main": "lib/form-gen-render.umd.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/JakHuang/form-generator.git"
},
"author": "jakhuang",
"license": "MIT",
"bugs": {
"url": "https://github.com/JakHuang/form-generator/issues"
},
"homepage": "https://github.com/JakHuang/form-generator#readme"
}

View File

@@ -0,0 +1,147 @@
import { deepClone } from '@/utils/index'
const componentChild = {}
/**
* 将./slots中的文件挂载到对象componentChild上
* 文件名为key对应JSON配置中的__config__.tag
* 文件内容为value解析JSON配置中的__slot__
*/
const slotsFiles = require.context('./slots', false, /\.js$/)
const keys = slotsFiles.keys() || []
keys.forEach(key => {
const tag = key.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = slotsFiles(key).default
componentChild[tag] = value
})
function vModel(dataObject, defaultValue, config) {
// 获取上传表单元素组件上传的文件
if (config.tag === "el-upload") {
// 上传表单元素组件的成功和移除事件
dataObject.attrs["on-success"] = (response, file, fileList) => {
this.$emit("upload", response, file, fileList);
};
dataObject.attrs["on-remove"] = (file, fileList) => {
this.$emit("deleteUpload", file, fileList);
};
dataObject.attrs["on-preview"] = (file, fileList) => {
this.$emit("previewUpload", file, fileList);
};
// 获取上传表单元素的默认值
try {
dataObject.props["file-list"] = JSON.parse(defaultValue);
} catch (err) {
console.warn(err);
}
return;
}
// 获取普通表单元素的值
dataObject.props.value = defaultValue;
dataObject.on.input = (val) => {
this.$emit("input", val);
};
}
function mountSlotFiles(h, confClone, children) {
const childObjs = componentChild[confClone.__config__.tag]
if (childObjs) {
Object.keys(childObjs).forEach(key => {
const childFunc = childObjs[key]
if (confClone.__slot__ && confClone.__slot__[key]) {
children.push(childFunc(h, confClone, key))
}
})
}
}
function emitEvents(confClone) {
['on', 'nativeOn'].forEach(attr => {
const eventKeyList = Object.keys(confClone[attr] || {})
eventKeyList.forEach(key => {
const val = confClone[attr][key]
if (typeof val === 'string') {
confClone[attr][key] = event => this.$emit(val, event)
}
})
})
}
function buildDataObject(confClone, dataObject) {
Object.keys(confClone).forEach(key => {
const val = confClone[key]
if (key === '__vModel__') {
vModel.call(this, dataObject, confClone.__config__.defaultValue, confClone.__config__)
} else if (dataObject[key] !== undefined) {
if (dataObject[key] === null
|| dataObject[key] instanceof RegExp
|| ['boolean', 'string', 'number', 'function'].includes(typeof dataObject[key])) {
dataObject[key] = val
} else if (Array.isArray(dataObject[key])) {
dataObject[key] = [...dataObject[key], ...val]
} else {
dataObject[key] = { ...dataObject[key], ...val }
}
} else {
dataObject.attrs[key] = val
}
})
// 清理属性
clearAttrs(dataObject)
}
function clearAttrs(dataObject) {
delete dataObject.attrs.__config__
delete dataObject.attrs.__slot__
delete dataObject.attrs.__methods__
}
function makeDataObject() {
// 深入数据对象:
// https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1
return {
class: {},
attrs: {},
props: {},
domProps: {},
nativeOn: {},
on: {},
style: {},
directives: [],
scopedSlots: {},
slot: null,
key: null,
ref: null,
refInFor: true
}
}
export default {
props: {
conf: {
type: Object,
required: true
}
},
render(h) {
const dataObject = makeDataObject()
const confClone = deepClone(this.conf)
const children = this.$slots.default || []
// 如果slots文件夹存在与当前tag同名的文件则执行文件中的代码
mountSlotFiles.call(this, h, confClone, children)
// 将字符串类型的事件,发送为消息
emitEvents.call(this, confClone)
// 将json表单配置转化为vue render可以识别的 “数据对象dataObject
buildDataObject.call(this, confClone, dataObject)
return h(this.conf.__config__.tag, dataObject, children)
}
}

View File

@@ -0,0 +1,5 @@
export default {
default(h, conf, key) {
return conf.__slot__[key]
}
}

View File

@@ -0,0 +1,13 @@
export default {
options(h, conf, key) {
const list = []
conf.__slot__.options.forEach(item => {
if (conf.__config__.optionType === 'button') {
list.push(<el-checkbox-button label={item.value}>{item.label}</el-checkbox-button>)
} else {
list.push(<el-checkbox label={item.value} border={conf.border}>{item.label}</el-checkbox>)
}
})
return list
}
}

View File

@@ -0,0 +1,8 @@
export default {
prepend(h, conf, key) {
return <template slot="prepend">{conf.__slot__[key]}</template>
},
append(h, conf, key) {
return <template slot="append">{conf.__slot__[key]}</template>
}
}

View File

@@ -0,0 +1,13 @@
export default {
options(h, conf, key) {
const list = []
conf.__slot__.options.forEach(item => {
if (conf.__config__.optionType === 'button') {
list.push(<el-radio-button label={item.value}>{item.label}</el-radio-button>)
} else {
list.push(<el-radio label={item.value} border={conf.border}>{item.label}</el-radio>)
}
})
return list
}
}

View File

@@ -0,0 +1,9 @@
export default {
options(h, conf, key) {
const list = []
conf.__slot__.options.forEach(item => {
list.push(<el-option label={item.label} value={item.value} disabled={item.disabled}></el-option>)
})
return list
}
}

View File

@@ -0,0 +1,17 @@
export default {
'list-type': (h, conf, key) => {
const list = []
const config = conf.__config__
if (conf['list-type'] === 'picture-card') {
list.push(<i class="el-icon-plus"></i>)
} else {
list.push(<el-button size="small" type="primary" icon="el-icon-upload">{config.buttonText}</el-button>)
}
if (config.showTip) {
list.push(
<div slot="tip" class="el-upload__tip">只能上传不超过 {config.fileSize}{config.sizeUnit} {conf.accept}文件</div>
)
}
return list
}
}

View File

@@ -0,0 +1,3 @@
## 简介
富文本编辑器tinymce的一个vue版本封装。使用cdn动态脚本引入的方式加载。

View File

@@ -0,0 +1,8 @@
/* eslint-disable max-len */
export const plugins = [
'advlist anchor autolink autosave code codesample directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textpattern visualblocks visualchars wordcount'
]
export const toolbar = [
'code searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote removeformat subscript superscript codesample hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'
]

View File

@@ -0,0 +1,3 @@
import Index from './index.vue'
export default Index

View File

@@ -0,0 +1,88 @@
<template>
<textarea :id="tinymceId" style="visibility: hidden" />
</template>
<script>
import loadTinymce from '@/utils/loadTinymce'
import { plugins, toolbar } from './config'
import { debounce } from 'throttle-debounce'
let num = 1
export default {
props: {
id: {
type: String,
default: () => {
num === 10000 && (num = 1)
return `tinymce${+new Date()}${num++}`
}
},
value: {
default: ''
}
},
data() {
return {
tinymceId: this.id
}
},
mounted() {
loadTinymce(tinymce => {
// eslint-disable-next-line global-require
require('./zh_CN')
let conf = {
selector: `#${this.tinymceId}`,
language: 'zh_CN',
menubar: 'file edit insert view format table',
plugins,
toolbar,
height: 300,
branding: false,
object_resizing: false,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
advlist_bullet_styles: 'square',
advlist_number_styles: 'default',
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true
}
conf = Object.assign(conf, this.$attrs)
conf.init_instance_callback = editor => {
if (this.value) editor.setContent(this.value)
this.vModel(editor)
}
tinymce.init(conf)
})
},
destroyed() {
this.destroyTinymce()
},
methods: {
vModel(editor) {
// 控制连续写入时setContent的触发频率
const debounceSetContent = debounce(250, editor.setContent)
this.$watch('value', (val, prevVal) => {
if (editor && val !== prevVal && val !== editor.getContent()) {
if (typeof val !== 'string') val = val.toString()
debounceSetContent.call(editor, val)
}
})
editor.on('change keyup undo redo', () => {
this.$emit('input', editor.getContent())
})
},
destroyTinymce() {
if (!window.tinymce) return
const tinymce = window.tinymce.get(this.tinymceId)
if (tinymce) {
tinymce.destroy()
}
}
}
}
</script>

View File

@@ -0,0 +1,28 @@
{
"name": "form-gen-tinymce",
"version": "1.0.0",
"description": "富文本编辑器tinymce的一个vue版本封装。使用cdn动态脚本引入的方式加载。",
"main": "lib/form-gen-tinymce.umd.js",
"directories": {
"example": "example"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/JakHuang/form-generator.git"
},
"keywords": [
"tinymce-vue"
],
"dependencies": {
"throttle-debounce": "^2.1.0"
},
"author": "jakHuang",
"license": "MIT",
"bugs": {
"url": "https://github.com/JakHuang/form-generator/issues"
},
"homepage": "https://github.com/JakHuang/form-generator/blob/dev/src/components/tinymce"
}

View File

@@ -0,0 +1,420 @@
/* eslint-disable */
tinymce.addI18n('zh_CN',{
"Redo": "\u91cd\u505a",
"Undo": "\u64a4\u9500",
"Cut": "\u526a\u5207",
"Copy": "\u590d\u5236",
"Paste": "\u7c98\u8d34",
"Select all": "\u5168\u9009",
"New document": "\u65b0\u6587\u4ef6",
"Ok": "\u786e\u5b9a",
"Cancel": "\u53d6\u6d88",
"Visual aids": "\u7f51\u683c\u7ebf",
"Bold": "\u7c97\u4f53",
"Italic": "\u659c\u4f53",
"Underline": "\u4e0b\u5212\u7ebf",
"Strikethrough": "\u5220\u9664\u7ebf",
"Superscript": "\u4e0a\u6807",
"Subscript": "\u4e0b\u6807",
"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
"Align left": "\u5de6\u8fb9\u5bf9\u9f50",
"Align center": "\u4e2d\u95f4\u5bf9\u9f50",
"Align right": "\u53f3\u8fb9\u5bf9\u9f50",
"Justify": "\u4e24\u7aef\u5bf9\u9f50",
"Bullet list": "\u9879\u76ee\u7b26\u53f7",
"Numbered list": "\u7f16\u53f7\u5217\u8868",
"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
"Increase indent": "\u589e\u52a0\u7f29\u8fdb",
"Close": "\u5173\u95ed",
"Formats": "\u683c\u5f0f",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u6253\u5f00\u526a\u8d34\u677f\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u7b49\u5feb\u6377\u952e\u3002",
"Headers": "\u6807\u9898",
"Header 1": "\u6807\u98981",
"Header 2": "\u6807\u98982",
"Header 3": "\u6807\u98983",
"Header 4": "\u6807\u98984",
"Header 5": "\u6807\u98985",
"Header 6": "\u6807\u98986",
"Headings": "\u6807\u9898",
"Heading 1": "\u6807\u98981",
"Heading 2": "\u6807\u98982",
"Heading 3": "\u6807\u98983",
"Heading 4": "\u6807\u98984",
"Heading 5": "\u6807\u98985",
"Heading 6": "\u6807\u98986",
"Preformatted": "\u9884\u5148\u683c\u5f0f\u5316\u7684",
"Div": "Div",
"Pre": "Pre",
"Code": "\u4ee3\u7801",
"Paragraph": "\u6bb5\u843d",
"Blockquote": "\u5f15\u6587\u533a\u5757",
"Inline": "\u6587\u672c",
"Blocks": "\u57fa\u5757",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
"Fonts": "\u5b57\u4f53",
"Font Sizes": "\u5b57\u53f7",
"Class": "\u7c7b\u578b",
"Browse for an image": "\u6d4f\u89c8\u56fe\u50cf",
"OR": "\u6216",
"Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u50cf\u81f3\u6b64",
"Upload": "\u4e0a\u4f20",
"Block": "\u5757",
"Align": "\u5bf9\u9f50",
"Default": "\u9ed8\u8ba4",
"Circle": "\u7a7a\u5fc3\u5706",
"Disc": "\u5b9e\u5fc3\u5706",
"Square": "\u65b9\u5757",
"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Anchor...": "\u951a\u70b9...",
"Name": "\u540d\u79f0",
"Id": "\u6807\u8bc6\u7b26",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
"Special character...": "\u7279\u6b8a\u5b57\u7b26...",
"Source code": "\u6e90\u4ee3\u7801",
"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
"Language": "\u8bed\u8a00",
"Code sample...": "\u793a\u4f8b\u4ee3\u7801...",
"Color Picker": "\u9009\u8272\u5668",
"R": "R",
"G": "G",
"B": "B",
"Left to right": "\u4ece\u5de6\u5230\u53f3",
"Right to left": "\u4ece\u53f3\u5230\u5de6",
"Emoticons...": "\u8868\u60c5\u7b26\u53f7...",
"Metadata and Document Properties": "\u5143\u6570\u636e\u548c\u6587\u6863\u5c5e\u6027",
"Title": "\u6807\u9898",
"Keywords": "\u5173\u952e\u8bcd",
"Description": "\u63cf\u8ff0",
"Robots": "\u673a\u5668\u4eba",
"Author": "\u4f5c\u8005",
"Encoding": "\u7f16\u7801",
"Fullscreen": "\u5168\u5c4f",
"Action": "\u64cd\u4f5c",
"Shortcut": "\u5feb\u6377\u952e",
"Help": "\u5e2e\u52a9",
"Address": "\u5730\u5740",
"Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f",
"Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\u680f",
"Focus to element path": "\u79fb\u52a8\u7126\u70b9\u5230\u5143\u7d20\u8def\u5f84",
"Focus to contextual toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u4e0a\u4e0b\u6587\u83dc\u5355",
"Insert link (if link plugin activated)": "\u63d2\u5165\u94fe\u63a5 (\u5982\u679c\u94fe\u63a5\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Save (if save plugin activated)": "\u4fdd\u5b58(\u5982\u679c\u4fdd\u5b58\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Find (if searchreplace plugin activated)": "\u67e5\u627e(\u5982\u679c\u67e5\u627e\u66ff\u6362\u63d2\u4ef6\u5df2\u6fc0\u6d3b)",
"Plugins installed ({0}):": "\u5df2\u5b89\u88c5\u63d2\u4ef6 ({0}):",
"Premium plugins:": "\u4f18\u79c0\u63d2\u4ef6\uff1a",
"Learn more...": "\u4e86\u89e3\u66f4\u591a...",
"You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}",
"Plugins": "\u63d2\u4ef6",
"Handy Shortcuts": "\u5feb\u6377\u952e",
"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247",
"Image description": "\u56fe\u7247\u63cf\u8ff0",
"Source": "\u5730\u5740",
"Dimensions": "\u5927\u5c0f",
"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
"General": "\u666e\u901a",
"Advanced": "\u9ad8\u7ea7",
"Style": "\u6837\u5f0f",
"Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
"Border": "\u8fb9\u6846",
"Insert image": "\u63d2\u5165\u56fe\u7247",
"Image...": "\u56fe\u7247...",
"Image list": "\u56fe\u7247\u5217\u8868",
"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c",
"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c",
"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c",
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c",
"Edit image": "\u7f16\u8f91\u56fe\u7247",
"Image options": "\u56fe\u7247\u9009\u9879",
"Zoom in": "\u653e\u5927",
"Zoom out": "\u7f29\u5c0f",
"Crop": "\u88c1\u526a",
"Resize": "\u8c03\u6574\u5927\u5c0f",
"Orientation": "\u65b9\u5411",
"Brightness": "\u4eae\u5ea6",
"Sharpen": "\u9510\u5316",
"Contrast": "\u5bf9\u6bd4\u5ea6",
"Color levels": "\u989c\u8272\u5c42\u6b21",
"Gamma": "\u4f3d\u9a6c\u503c",
"Invert": "\u53cd\u8f6c",
"Apply": "\u5e94\u7528",
"Back": "\u540e\u9000",
"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4",
"Date\/time": "\u65e5\u671f\/\u65f6\u95f4",
"Insert\/Edit Link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Text to display": "\u663e\u793a\u6587\u5b57",
"Url": "\u5730\u5740",
"Open link in...": "\u94fe\u63a5\u6253\u5f00\u4f4d\u7f6e...",
"Current window": "\u5f53\u524d\u7a97\u53e3",
"None": "\u65e0",
"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00",
"Remove link": "\u5220\u9664\u94fe\u63a5",
"Anchors": "\u951a\u70b9",
"Link...": "\u94fe\u63a5...",
"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f",
"Link list": "\u94fe\u63a5\u5217\u8868",
"Insert video": "\u63d2\u5165\u89c6\u9891",
"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891",
"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53",
"Alternative source": "\u955c\u50cf",
"Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740",
"Media poster (Image URL)": "\u5c01\u9762(\u56fe\u7247\u5730\u5740)",
"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
"Embed": "\u5185\u5d4c",
"Media...": "\u591a\u5a92\u4f53...",
"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
"Page break": "\u5206\u9875\u7b26",
"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
"Preview": "\u9884\u89c8",
"Print...": "\u6253\u5370...",
"Save": "\u4fdd\u5b58",
"Find": "\u67e5\u627e",
"Replace with": "\u66ff\u6362\u4e3a",
"Replace": "\u66ff\u6362",
"Replace all": "\u5168\u90e8\u66ff\u6362",
"Previous": "\u4e0a\u4e00\u4e2a",
"Next": "\u4e0b\u4e00\u4e2a",
"Find and replace...": "\u67e5\u627e\u5e76\u66ff\u6362...",
"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
"Match case": "\u533a\u5206\u5927\u5c0f\u5199",
"Find whole words only": "\u5168\u5b57\u5339\u914d",
"Spell check": "\u62fc\u5199\u68c0\u67e5",
"Ignore": "\u5ffd\u7565",
"Ignore all": "\u5168\u90e8\u5ffd\u7565",
"Finish": "\u5b8c\u6210",
"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178",
"Insert table": "\u63d2\u5165\u8868\u683c",
"Table properties": "\u8868\u683c\u5c5e\u6027",
"Delete table": "\u5220\u9664\u8868\u683c",
"Cell": "\u5355\u5143\u683c",
"Row": "\u884c",
"Column": "\u5217",
"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
"Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
"Split cell": "\u62c6\u5206\u5355\u5143\u683c",
"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
"Delete row": "\u5220\u9664\u884c",
"Row properties": "\u884c\u5c5e\u6027",
"Cut row": "\u526a\u5207\u884c",
"Copy row": "\u590d\u5236\u884c",
"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
"Delete column": "\u5220\u9664\u5217",
"Cols": "\u5217",
"Rows": "\u884c",
"Width": "\u5bbd",
"Height": "\u9ad8",
"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
"Show caption": "\u663e\u793a\u6807\u9898",
"Left": "\u5de6\u5bf9\u9f50",
"Center": "\u5c45\u4e2d",
"Right": "\u53f3\u5bf9\u9f50",
"Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
"Scope": "\u8303\u56f4",
"Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
"H Align": "\u6c34\u5e73\u5bf9\u9f50",
"V Align": "\u5782\u76f4\u5bf9\u9f50",
"Top": "\u9876\u90e8\u5bf9\u9f50",
"Middle": "\u5782\u76f4\u5c45\u4e2d",
"Bottom": "\u5e95\u90e8\u5bf9\u9f50",
"Header cell": "\u8868\u5934\u5355\u5143\u683c",
"Row group": "\u884c\u7ec4",
"Column group": "\u5217\u7ec4",
"Row type": "\u884c\u7c7b\u578b",
"Header": "\u8868\u5934",
"Body": "\u8868\u4f53",
"Footer": "\u8868\u5c3e",
"Border color": "\u8fb9\u6846\u989c\u8272",
"Insert template...": "\u63d2\u5165\u6a21\u677f...",
"Templates": "\u6a21\u677f",
"Template": "\u6a21\u677f",
"Text color": "\u6587\u5b57\u989c\u8272",
"Background color": "\u80cc\u666f\u8272",
"Custom...": "\u81ea\u5b9a\u4e49...",
"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
"No color": "\u65e0",
"Remove color": "\u79fb\u9664\u989c\u8272",
"Table of Contents": "\u5185\u5bb9\u5217\u8868",
"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
"Word count": "\u5b57\u6570",
"Count": "\u8ba1\u6570",
"Document": "\u6587\u6863",
"Selection": "\u9009\u62e9",
"Words": "\u5355\u8bcd",
"Words: {0}": "\u5b57\u6570\uff1a{0}",
"{0} words": "{0} \u5b57",
"File": "\u6587\u4ef6",
"Edit": "\u7f16\u8f91",
"Insert": "\u63d2\u5165",
"View": "\u89c6\u56fe",
"Format": "\u683c\u5f0f",
"Table": "\u8868\u683c",
"Tools": "\u5de5\u5177",
"Powered by {0}": "\u7531{0}\u9a71\u52a8",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
"Image title": "\u56fe\u7247\u6807\u9898",
"Border width": "\u8fb9\u6846\u5bbd\u5ea6",
"Border style": "\u8fb9\u6846\u6837\u5f0f",
"Error": "\u9519\u8bef",
"Warn": "\u8b66\u544a",
"Valid": "\u6709\u6548",
"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952e\u6253\u5f00\u5bf9\u8bdd\u6846",
"Rich Text Area. Press ALT-0 for help.": "\u7f16\u8f91\u533a\u3002\u6309Alt+0\u952e\u6253\u5f00\u5e2e\u52a9\u3002",
"System Font": "\u7cfb\u7edf\u5b57\u4f53",
"Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}",
"Failed to load plugin: {0} from url {1}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0} \u6765\u81ea\u94fe\u63a5 {1}",
"Failed to load plugin url: {0}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25 \u94fe\u63a5: {0}",
"Failed to initialize plugin: {0}": "\u63d2\u4ef6\u521d\u59cb\u5316\u5931\u8d25: {0}",
"example": "\u793a\u4f8b",
"Search": "\u641c\u7d22",
"All": "\u5168\u90e8",
"Currency": "\u8d27\u5e01",
"Text": "\u6587\u5b57",
"Quotations": "\u5f15\u7528",
"Mathematical": "\u6570\u5b66",
"Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5145",
"Symbols": "\u7b26\u53f7",
"Arrows": "\u7bad\u5934",
"User Defined": "\u81ea\u5b9a\u4e49",
"dollar sign": "\u7f8e\u5143\u7b26\u53f7",
"currency sign": "\u8d27\u5e01\u7b26\u53f7",
"euro-currency sign": "\u6b27\u5143\u7b26\u53f7",
"colon sign": "\u5192\u53f7",
"cruzeiro sign": "\u514b\u9c81\u8d5b\u7f57\u5e01\u7b26\u53f7",
"french franc sign": "\u6cd5\u90ce\u7b26\u53f7",
"lira sign": "\u91cc\u62c9\u7b26\u53f7",
"mill sign": "\u5bc6\u5c14\u7b26\u53f7",
"naira sign": "\u5948\u62c9\u7b26\u53f7",
"peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7",
"rupee sign": "\u5362\u6bd4\u7b26\u53f7",
"won sign": "\u97e9\u5143\u7b26\u53f7",
"new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7",
"dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7",
"kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7",
"tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7",
"drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7",
"german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7",
"peso sign": "\u6bd4\u7d22\u7b26\u53f7",
"guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7",
"austral sign": "\u6fb3\u5143\u7b26\u53f7",
"hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7",
"cedi sign": "\u585e\u5730\u7b26\u53f7",
"livre tournois sign": "\u91cc\u5f17\u5f17\u5c14\u7b26\u53f7",
"spesmilo sign": "spesmilo\u7b26\u53f7",
"tenge sign": "\u575a\u6208\u7b26\u53f7",
"indian rupee sign": "\u5370\u5ea6\u5362\u6bd4",
"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9",
"nordic mark sign": "\u5317\u6b27\u9a6c\u514b",
"manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7",
"ruble sign": "\u5362\u5e03\u7b26\u53f7",
"yen character": "\u65e5\u5143\u5b57\u6837",
"yuan character": "\u4eba\u6c11\u5e01\u5143\u5b57\u6837",
"yuan character, in hong kong and taiwan": "\u5143\u5b57\u6837\uff08\u6e2f\u53f0\u5730\u533a\uff09",
"yen\/yuan character variant one": "\u5143\u5b57\u6837\uff08\u5927\u5199\uff09",
"Loading emoticons...": "\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7...",
"Could not load emoticons": "\u4e0d\u80fd\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7",
"People": "\u4eba\u7c7b",
"Animals and Nature": "\u52a8\u7269\u548c\u81ea\u7136",
"Food and Drink": "\u98df\u7269\u548c\u996e\u54c1",
"Activity": "\u6d3b\u52a8",
"Travel and Places": "\u65c5\u6e38\u548c\u5730\u70b9",
"Objects": "\u7269\u4ef6",
"Flags": "\u65d7\u5e1c",
"Characters": "\u5b57\u7b26",
"Characters (no spaces)": "\u5b57\u7b26(\u65e0\u7a7a\u683c)",
"{0} characters": "{0} \u4e2a\u5b57\u7b26",
"Error: Form submit field collision.": "\u9519\u8bef: \u8868\u5355\u63d0\u4ea4\u5b57\u6bb5\u51b2\u7a81\u3002",
"Error: No form element found.": "\u9519\u8bef: \u6ca1\u6709\u8868\u5355\u63a7\u4ef6\u3002",
"Update": "\u66f4\u65b0",
"Color swatch": "\u989c\u8272\u6837\u672c",
"Turquoise": "\u9752\u7eff\u8272",
"Green": "\u7eff\u8272",
"Blue": "\u84dd\u8272",
"Purple": "\u7d2b\u8272",
"Navy Blue": "\u6d77\u519b\u84dd",
"Dark Turquoise": "\u6df1\u84dd\u7eff\u8272",
"Dark Green": "\u6df1\u7eff\u8272",
"Medium Blue": "\u4e2d\u84dd\u8272",
"Medium Purple": "\u4e2d\u7d2b\u8272",
"Midnight Blue": "\u6df1\u84dd\u8272",
"Yellow": "\u9ec4\u8272",
"Orange": "\u6a59\u8272",
"Red": "\u7ea2\u8272",
"Light Gray": "\u6d45\u7070\u8272",
"Gray": "\u7070\u8272",
"Dark Yellow": "\u6697\u9ec4\u8272",
"Dark Orange": "\u6df1\u6a59\u8272",
"Dark Red": "\u6df1\u7ea2\u8272",
"Medium Gray": "\u4e2d\u7070\u8272",
"Dark Gray": "\u6df1\u7070\u8272",
"Light Green": "\u6d45\u7eff\u8272",
"Light Yellow": "\u6d45\u9ec4\u8272",
"Light Red": "\u6d45\u7ea2\u8272",
"Light Purple": "\u6d45\u7d2b\u8272",
"Light Blue": "\u6d45\u84dd\u8272",
"Dark Purple": "\u6df1\u7d2b\u8272",
"Dark Blue": "\u6df1\u84dd\u8272",
"Black": "\u9ed1\u8272",
"White": "\u767d\u8272",
"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5c4f\u6a21\u5f0f",
"Open help dialog": "\u6253\u5f00\u5e2e\u52a9\u5bf9\u8bdd\u6846",
"history": "\u5386\u53f2",
"styles": "\u6837\u5f0f",
"formatting": "\u683c\u5f0f\u5316",
"alignment": "\u5bf9\u9f50",
"indentation": "\u7f29\u8fdb",
"permanent pen": "\u8bb0\u53f7\u7b14",
"comments": "\u5907\u6ce8",
"Format Painter": "\u683c\u5f0f\u5237",
"Insert\/edit iframe": "\u63d2\u5165\/\u7f16\u8f91\u6846\u67b6",
"Capitalization": "\u5927\u5199",
"lowercase": "\u5c0f\u5199",
"UPPERCASE": "\u5927\u5199",
"Title Case": "\u9996\u5b57\u6bcd\u5927\u5199",
"Permanent Pen Properties": "\u6c38\u4e45\u7b14\u5c5e\u6027",
"Permanent pen properties...": "\u6c38\u4e45\u7b14\u5c5e\u6027...",
"Font": "\u5b57\u4f53",
"Size": "\u5b57\u53f7",
"More...": "\u66f4\u591a...",
"Spellcheck Language": "\u62fc\u5199\u68c0\u67e5\u8bed\u8a00",
"Select...": "\u9009\u62e9...",
"Preferences": "\u9996\u9009\u9879",
"Yes": "\u662f",
"No": "\u5426",
"Keyboard Navigation": "\u952e\u76d8\u6307\u5f15",
"Version": "\u7248\u672c",
"Anchor": "\u951a\u70b9",
"Special character": "\u7279\u6b8a\u7b26\u53f7",
"Code sample": "\u4ee3\u7801\u793a\u4f8b",
"Color": "\u989c\u8272",
"Emoticons": "\u8868\u60c5",
"Document properties": "\u6587\u6863\u5c5e\u6027",
"Image": "\u56fe\u7247",
"Insert link": "\u63d2\u5165\u94fe\u63a5",
"Target": "\u6253\u5f00\u65b9\u5f0f",
"Link": "\u94fe\u63a5",
"Poster": "\u5c01\u9762",
"Media": "\u5a92\u4f53",
"Print": "\u6253\u5370",
"Prev": "\u4e0a\u4e00\u4e2a",
"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362",
"Whole words": "\u5168\u5b57\u5339\u914d",
"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
"Caption": "\u6807\u9898",
"Insert template": "\u63d2\u5165\u6a21\u677f"
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long