Files
kuaifanandClaude Opus 4.5 478876ddc1 feat(workflow): 在工作流配置中添加规则摘要展示
在工作流展开后的配置表格上方添加规则摘要区块,根据实际配置动态展示:
- 状态负责人规则:区分添加模式、流转模式、剔除模式的不同描述
- 限制负责人规则:显示仅限任务负责人和项目管理员修改状态
- 关联列表规则:显示流转时自动移动至指定列表

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 01:55:00 +00:00

656 lines
32 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="project-workflow">
<div class="workflow-title">
{{$L('工作流设置')}}
<div class="title-icon">
<Loading v-if="loadIng > 0"/>
<Icon v-else type="ios-refresh" @click="getData"/>
</div>
</div>
<div v-if="list.length > 0" class="workflow-content">
<Collapse v-model="openIndex" accordion>
<Panel v-for="data in list" :key="data.id" :name="'index_' + data.id">
<div class="workflow-item">
<div class="workflow-name">{{data.name}}</div>
<div class="workflow-status">
<div
v-for="item in data.project_flow_item"
:key="item.id"
:class="item.status"
:style="$A.generateColorVarStyle(item.color, [10], 'flow-item-custom-color')">
{{item.name}}
</div>
</div>
<div class="workflow-save" @click.stop="">
<template v-if="contrast(data.project_flow_item, data.project_flow_bak)">
<Button :loading="loadIng > 0" type="primary" @click="onSave(data)">{{$L('保存')}}</Button>
<Button v-if="data.id > 0" :disabled="loadIng > 0" type="primary" ghost @click="onReduction(data, $event)">{{$L('还原')}}</Button>
</template>
<Button :disabled="loadIng > 0" type="error" ghost @click="onDelete(data)">{{$L('删除')}}</Button>
</div>
</div>
<div slot="content" class="taskflow-config">
<div v-if="flowRulesMap[data.id] && flowRulesMap[data.id].length > 0" class="taskflow-config-rules">
<div class="rules-title">
<Icon type="md-list-box" />
<span>{{$L('工作流规则')}}</span>
</div>
<div class="rules-list">
<div v-for="(rule, ruleIndex) in flowRulesMap[data.id]" :key="ruleIndex" class="rules-item">
<template v-if="rule.type === 'owner'">
<span>{{$L('流转到')}}</span>
<span class="rule-status" :class="rule.status">{{rule.name}}</span>
<span v-if="rule.usertype === 'add'">{{$L('时添加')}}</span>
<span v-else>{{$L('时改变任务负责人为')}}</span>
<UserAvatar v-for="(uid, uidx) in rule.userids" :key="`${ruleIndex}_${uidx}`" :userid="uid" :size="20" :borderWidth="1" showName/>
<span v-if="rule.usertype === 'add'">{{$L('至任务负责人')}}</span>
<span v-else-if="rule.usertype === 'merge'">{{$L('(并保留操作人),原负责人移至协助人员')}}</span>
<span v-else>{{$L(',原负责人移至协助人员')}}</span>
</template>
<template v-else-if="rule.type === 'limit'">
<span class="rule-status" :class="rule.status">{{rule.name}}</span>
<span>{{$L('仅限任务负责人和项目管理员修改状态')}}</span>
</template>
<template v-else-if="rule.type === 'column'">
<span>{{$L('流转到')}}</span>
<span class="rule-status" :class="rule.status">{{rule.name}}</span>
<span>{{$L('时自动将任务移动至列表')}}</span>
<span class="rule-column">{{rule.columnName}}</span>
</template>
</div>
</div>
</div>
<div class="taskflow-config-table">
<div class="taskflow-config-table-left-container">
<div class="taskflow-config-table-column-header left-header">{{$L('配置项')}}</div>
<div :ref="`overlay_${data.id}`" class="taskflow-config-table-column-body">
<div class="taskflow-config-table-block">
<div class="taskflow-config-table-block-title">{{$L('设置状态为')}}</div>
<div class="taskflow-config-table-block-item">
<div>
<div class="title">{{$L('开始状态')}}</div>
<div class="subtitle">{{$L('新建任务默认状态')}}</div>
</div>
</div>
<div class="taskflow-config-table-block-item">
<div>
<div class="title">{{$L('进行中')}}</div>
<div class="subtitle">{{$L('可设置多个状态为进行中')}}</div>
</div>
</div>
<div class="taskflow-config-table-block-item">
<div>
<div class="title">{{$L('验收/测试')}}</div>
<div class="subtitle">{{$L('只能设置单个状态为验收/测试')}}</div>
</div>
</div>
<div class="taskflow-config-table-block-item">
<div>
<div class="title">{{$L('结束状态')}}</div>
<div class="subtitle">{{$L('该状态下任务自动标记完成')}}</div>
</div>
</div>
</div>
<div class="taskflow-config-table-block hr">
<div class="taskflow-config-table-block-title">{{$L('可流转到')}}</div>
<div v-for="item in data.project_flow_item" :key="item.id" class="taskflow-config-table-block-item">
<span class="transform-status-name">{{item.name}}</span>
</div>
</div>
</div>
</div>
<div class="taskflow-config-table-right-container">
<Draggable
:list="data.project_flow_item"
:animation="150"
:disabled="$isEEUIApp || windowTouch"
class="taskflow-config-table-list-wrapper"
tag="div"
draggable=".column-border"
@sort="() => {}">
<div
v-for="(item, index) in data.project_flow_item"
:key="index"
:style="$A.generateColorVarStyle(item.color, [10, 20, 70], 'flow-item-custom-color')"
class="taskflow-config-table-status-column column-border"
:class="item.status">
<div
class="taskflow-config-table-status-item taskflow-config-table-column-header">
<div class="status-label-with-menu" :class="item.status">
<div class="name">{{item.name}}</div>
<EDropdown
trigger="click"
class="more"
:class="{opacity: item.userids.length > 0 || item.columnid > 0}"
@command="onMore($event, item)">
<div class="more-icon">
<Badge :dot="item.userids.length > 0 || item.columnid > 0">
<Icon type="ios-more" />
</Badge>
</div>
<EDropdownMenu slot="dropdown" class="taskflow-config-more-dropdown-menu">
<li class="taskflow-config-more-dropdown-warp">
<ul>
<EDropdownItem v-if="item.userids.length > 0" command="user">
<div class="users">
<UserAvatar v-for="(uid, ukey) in item.userids" :key="ukey" :userid="uid" :size="28" :borderWidth="1" :showName="item.userids.length === 1"/>
</div>
</EDropdownItem>
<EDropdownItem command="user">
<div class="item">
<Icon type="md-settings" />
<Badge :dot="item.userids.length > 0 || item.columnid > 0">
{{$L('状态设置')}}
</Badge>
</div>
</EDropdownItem>
<EDropdownItem command="name">
<div class="item">
<Icon type="md-create" />{{$L('修改名称')}}
</div>
</EDropdownItem>
<EDropdownItem command="remove">
<div class="item delete">
<Icon type="md-trash" />{{$L('删除')}}
</div>
</EDropdownItem>
<EDropdownItem v-for="(c, k) in $store.state.columnColorList" :key="k" :divided="k==0" :command="c">
<div class="item">
<i class="taskfont" :style="{color:c.color||'#ddd'}" v-html="c.color == item.color ? '&#xe61d;' : '&#xe61c;'"></i>{{$L(c.name)}}
</div>
</EDropdownItem>
</ul>
</li>
</EDropdownMenu>
</EDropdown>
</div>
</div>
<div :ref="`overlay_${data.id}`" class="taskflow-config-table-column-body">
<div class="taskflow-config-table-block">
<div class="taskflow-config-table-block-title"></div>
<RadioGroup v-model="item.status">
<Radio label="start"><span></span></Radio>
<Radio label="progress"><span></span></Radio>
<Radio label="test"><span></span></Radio>
<Radio label="end"><span></span></Radio>
</RadioGroup>
</div>
<div class="taskflow-config-table-block">
<div class="taskflow-config-table-block-title"></div>
<CheckboxGroup v-model="item.turns" @on-change="onTurns(item)">
<Checkbox v-for="v in data.project_flow_item" :key="v.id" :label="v.id" :disabled="v.id==item.id"><span></span></Checkbox>
</CheckboxGroup>
</div>
</div>
</div>
<div class="taskflow-config-table-status-column addnew" @click="onAdd(data)">{{$L('添加状态')}}</div>
</Draggable>
</div>
</div>
</div>
</Panel>
</Collapse>
</div>
<div v-else-if="loadIng == 0" class="workflow-no">
{{$L('当前项目还没有创建工作流')}}
<Button type="primary" icon="md-add" @click="onCreate">{{$L('创建工作流')}}</Button>
</div>
<!--状态设置-->
<Modal
v-model="userShow"
:styles="{
width: '90%',
maxWidth: '640px'
}"
:title="`${$L('状态设置')} (${settingData.name})`"
:mask-closable="false">
<Form :model="settingData" v-bind="formOptions" @submit.native.prevent>
<div class="workflow-setting-box">
<h3>{{ $L('状态负责人') }}</h3>
<div class="form-box">
<FormItem prop="userids" :label="$L('状态负责人')">
<UserSelect v-model="settingData.userids" :project-id="projectId" :multiple-max="5" :title="$L('选择状态负责人')"/>
</FormItem>
<FormItem prop="usertype" :label="$L('流转模式')">
<RadioGroup v-model="settingData.usertype">
<Radio label="add">{{$L('添加模式')}}</Radio>
<Radio label="replace">{{$L('流转模式')}}</Radio>
<Radio label="merge">{{$L('剔除模式')}}</Radio>
</RadioGroup>
<div v-if="settingData.usertype=='replace'" class="form-tip">{{$L(`流转到【${settingData.name}】时改变任务负责人为状态负责人,原本的任务负责人移至协助人员。`)}}</div>
<div v-else-if="settingData.usertype=='merge'" class="form-tip">{{$L(`流转到【${settingData.name}】时改变任务负责人为状态负责人(并保留操作状态的人员),原本的任务负责人移至协助人员。`)}}</div>
<div v-else class="form-tip">{{$L(`流转到【${settingData.name}】时添加状态负责人至任务负责人。`)}}</div>
</FormItem>
<FormItem prop="userlimit" :label="$L('限制负责人')">
<iSwitch v-model="settingData.userlimit" :true-value="1" :false-value="0"/>
<div v-if="settingData.userlimit===1" class="form-tip">{{$L(`流转到【${settingData.name}】时,[任务负责人] 和 [项目管理员] 可以修改状态。`)}}</div>
<div v-else class="form-tip">{{$L(`流转到【${settingData.name}】时,[任务负责人] 和 [项目管理员] 可以修改状态。`)}}</div>
</FormItem>
</div>
</div>
<div class="workflow-setting-box">
<h3>{{ $L('关联列表') }}</h3>
<div class="form-box">
<FormItem prop="usertype" :label="$L('关联列表')">
<Select v-model="settingData.columnid" :placeholder="$L('选择关联列表')" transfer>
<Option v-for="(item, index) in columnList" :value="item.id" :key="index">{{ item.name }}</Option>
</Select>
<div class="form-tip">
{{$L(`流转到【${settingData.name}】时自动将任务移动至关联列表。`)}}
<a v-if="settingData.columnid" href="javascript:void(0)" @click="settingData.columnid=0">{{$L('取消关联')}}</a>
</div>
</FormItem>
</div>
</div>
</Form>
<div slot="footer" class="adaption">
<Button type="default" @click="userShow=false">{{$L('取消')}}</Button>
<Button type="primary" @click="onUser">{{$L('保存')}}</Button>
</div>
</Modal>
</div>
</template>
<script>
import Draggable from "vuedraggable";
import UserSelect from "../../../components/UserSelect.vue";
import {mapState} from "vuex";
export default {
name: "ProjectWorkflow",
components: {UserSelect, Draggable},
props: {
projectId: {
type: Number,
default: 0
},
},
data() {
return {
loadIng: 0,
list: [],
openIndex: "",
userShow: false,
settingData: {},
}
},
mounted() {
},
computed: {
...mapState(['cacheColumns', 'formOptions']),
columnList({projectId, cacheColumns}) {
return cacheColumns.filter(({project_id}) => {
return project_id == projectId
}).sort((a, b) => {
if (a.sort != b.sort) {
return $A.sortFloat(a.sort, b.sort);
}
return a.id - b.id;
}).map(item => {
return {
id: item.id,
name: item.name,
}
});
},
flowRulesMap() {
const map = {};
const columnMap = {};
this.columnList.forEach(col => {
columnMap[col.id] = col.name;
});
this.list.forEach(data => {
const rules = [];
data.project_flow_item.forEach(item => {
// 状态负责人规则
if (item.userids && item.userids.length > 0) {
rules.push({
type: 'owner',
name: item.name,
status: item.status,
userids: item.userids,
usertype: item.usertype
});
}
// 限制负责人规则(不依赖状态负责人)
if (item.userlimit === 1) {
rules.push({
type: 'limit',
name: item.name,
status: item.status
});
}
// 关联列表规则
if (item.columnid && columnMap[item.columnid]) {
rules.push({
type: 'column',
name: item.name,
status: item.status,
columnName: columnMap[item.columnid]
});
}
});
map[data.id] = rules;
});
return map;
}
},
watch: {
projectId: {
handler(val) {
if (val) {
this.getData()
}
},
immediate: true
},
},
methods: {
getData() {
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/flow/list',
data: {
project_id: this.projectId,
},
}).then(({data}) => {
this.list = data.map(item => {
item.project_flow_bak = JSON.stringify(item.project_flow_item)
return item;
});
this.openIndex = this.list.length === 1 ? ("index_" + this.list[0].id) : ""
this.$nextTick(this.syncScroller);
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(_ => {
this.loadIng--;
});
},
syncScroller() {
this.list.some(data => {
this.$refs[`overlay_${data.id}`] && this.$refs[`overlay_${data.id}`].some(el => {
if (!Object.keys(el.attributes).includes("sync-scroller")) {
el.setAttribute("sync-scroller", true);
el.addEventListener('scroll', ({target}) => {
let top = target.scrollTop;
let left = target.scrollLeft;
this.$nextTick(() => {
this.$refs[`overlay_${data.id}`].some(node => {
if (node != el) {
node.scrollTo(left, top)
}
})
})
});
}
});
})
},
contrast(project_flow_item, project_flow_bak) {
return JSON.stringify(project_flow_item) != project_flow_bak
},
existDiff() {
return !!this.list.find(data => {
return this.contrast(data.project_flow_item, data.project_flow_bak)
});
},
onCreate() {
let id = -1 * $A.randNum(1000, 10000);
this.list.push({
"id": id,
"name": "Default",
"project_flow_item": [
{
"id": -10,
"name": this.$L("待处理"),
"status": "start",
"turns": [-10, -11, -12, -13, -14],
"userids": [],
"usertype": 'add',
"userlimit": 0,
"columnid": 0,
},
{
"id": -11,
"name": this.$L("进行中"),
"status": "progress",
"turns": [-10, -11, -12, -13, -14],
"userids": [],
"usertype": 'add',
"userlimit": 0,
"columnid": 0,
},
{
"id": -12,
"name": this.$L("待测试"),
"status": "test",
"turns": [-10, -11, -12, -13, -14],
"userids": [],
"usertype": 'add',
"userlimit": 0,
"columnid": 0,
},
{
"id": -13,
"name": this.$L("已完成"),
"status": "end",
"turns": [-10, -11, -12, -13, -14],
"userids": [],
"usertype": 'add',
"userlimit": 0,
"columnid": 0,
},
{
"id": -14,
"name": this.$L("已取消"),
"status": "end",
"color": "#999999",
"turns": [-10, -11, -12, -13, -14],
"userids": [],
"usertype": 'add',
"userlimit": 0,
"columnid": 0,
}
]
})
this.openIndex = "index_" + id;
this.$nextTick(this.syncScroller);
},
onDelete(data) {
$A.modalConfirm({
title: '删除工作流',
content: '你确定要删除工作流吗?',
loading: true,
onOk: () => {
if (data.id > 0) {
return new Promise((resolve, reject) => {
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/flow/delete',
data: {
project_id: this.projectId,
},
}).then(({msg}) => {
resolve(msg);
//
let index = this.list.findIndex(({id}) => id == data.id)
if (index > -1) {
this.list.splice(index, 1)
}
}).catch(({msg}) => {
reject(msg);
}).finally(_ => {
this.loadIng--;
});
})
}
const index = this.list.findIndex(({id}) => id == data.id)
if (index > -1) {
this.list.splice(index, 1)
}
}
});
},
onMore(command, item) {
switch (command) {
case "user":
this.$set(this.settingData, 'id', item.id);
this.$set(this.settingData, 'name', item.name);
this.$set(this.settingData, 'userids', item.userids);
this.$set(this.settingData, 'usertype', item.usertype);
this.$set(this.settingData, 'userlimit', item.userlimit);
this.$set(this.settingData, 'columnid', item.columnid);
this.userShow = true;
break;
case "name":
this.onName(item);
break;
case "remove":
this.onRemove(item);
break;
default:
if (command.name) {
this.$set(item, 'color', command.color);
}
}
},
onUser() {
this.userShow = false;
this.list.some(data => {
let item = data.project_flow_item.find(item => item.id == this.settingData.id)
if (item) {
this.$set(item, 'userids', this.settingData.userids)
this.$set(item, 'usertype', this.settingData.usertype)
this.$set(item, 'userlimit', this.settingData.userlimit)
this.$set(item, 'columnid', this.settingData.columnid)
}
})
},
onName(item) {
$A.modalInput({
value: item.name,
title: "修改名称",
placeholder: "请输入流程名称",
onOk: (name) => {
if (!name) {
return '请输入流程名称';
}
this.$set(item, 'name', name);
}
});
},
onRemove(item) {
this.list.some(data => {
let index = data.project_flow_item.findIndex(({id}) => id == item.id)
if (index > -1) {
data.project_flow_item.splice(index, 1)
}
})
},
onTurns(item) {
this.$set(item, 'turns', item.turns.sort())
},
onAdd(data) {
$A.modalInput({
title: "添加状态",
placeholder: "请输入状态名称",
onOk: (name) => {
if (!name) {
return '请输入状态名称'
}
const id = $A.randNum(100000, 999999) * -1;
const turns = data.project_flow_item.map(({id}) => id)
data.project_flow_item.push({
id,
name,
status: 'end',
turns,
userids: [],
usertype: 'add',
userlimit: 0,
columnid: 0,
})
data.project_flow_item.some(item => {
item.turns.push(id)
})
}
});
},
onReduction(data) {
this.$set(data, 'project_flow_item', JSON.parse(data.project_flow_bak))
},
onSave(formData) {
let sort = 0;
formData.project_flow_item.some(item => {
item.sort = sort++
})
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/flow/save',
data: {
project_id: this.projectId,
flows: formData.project_flow_item,
},
method: 'post',
}).then(({data, msg}) => {
$.messageSuccess(msg)
//
data.project_flow_bak = JSON.stringify(data.project_flow_item)
let index = this.list.findIndex(({id}) => id == formData.id)
if (index > -1) {
this.list.splice(index, 1, data)
} else {
this.list.push(data)
}
this.openIndex = "index_" + data.id;
this.$nextTick(this.syncScroller);
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(_ => {
this.loadIng--;
});
},
saveAll() {
this.list.some(data => {
if (this.contrast(data.project_flow_item, data.project_flow_bak)) {
this.onSave(data)
}
});
},
}
}
</script>