feat: 添加工作流

This commit is contained in:
kuaifan 2022-01-08 16:51:51 +08:00
parent 8a2571f514
commit bbd394272f
17 changed files with 1330 additions and 3 deletions

View File

@ -89,6 +89,7 @@ git pull
./cmd uninstall
./cmd install
./cmd mysql recovery
./cmd artisan migrate
```
## Uninstall

View File

@ -89,6 +89,7 @@ git pull
./cmd uninstall
./cmd install
./cmd mysql recovery
./cmd artisan migrate
```
## 卸载项目

View File

@ -6,6 +6,8 @@ use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\Project;
use App\Models\ProjectColumn;
use App\Models\ProjectFlow;
use App\Models\ProjectFlowItem;
use App\Models\ProjectInvite;
use App\Models\ProjectLog;
use App\Models\ProjectTask;
@ -1406,6 +1408,163 @@ class ProjectController extends AbstractController
return Base::retSuccess('删除成功', ['id' => $task->id]);
}
/**
* @api {get} api/project/flow/list 29. 工作流列表
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
* @apiGroup project
* @apiName flow__list
*
* @apiParam {Number} project_id 项目ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function flow__list()
{
User::auth();
//
$project_id = intval(Request::input('project_id'));
//
$project = Project::userProject($project_id, true, true);
//
$list = ProjectFlow::with(['ProjectFlowItem'])->whereProjectId($project->id)->get();
return Base::retSuccess('success', $list);
}
/**
* @api {post} api/project/flow/save 29. 保存工作流
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
* @apiGroup project
* @apiName flow__save
*
* @apiParam {Number} project_id 项目ID
* @apiParam {Array} flows 工作流数据
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function flow__save()
{
User::auth();
//
$project_id = intval(Base::getContentValue('project_id'));
$flows = Base::getContentValue('flows');
//
if (!is_array($flows)) {
return Base::retError('参数错误');
}
//
$project = Project::userProject($project_id, true, true);
//
return AbstractModel::transaction(function() use ($project, $flows) {
$projectFlow = ProjectFlow::whereProjectId($project->id)->first();
if (empty($projectFlow)) {
$projectFlow = ProjectFlow::createInstance([
'project_id' => $project->id,
'name' => 'Default'
]);
if (!$projectFlow->save()) {
throw new ApiException('工作流创建失败');
}
}
//
$ids = [];
$idc = [];
$hasStart = false;
$hasEnd = false;
foreach ($flows as $item) {
$id = intval($item['id']);
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
$flow = ProjectFlowItem::updateInsert([
'id' => $id,
'project_id' => $project->id,
'flow_id' => $projectFlow->id,
], [
'name' => trim($item['name']),
'status' => trim($item['status']),
'sort' => intval($item['sort']),
'turns' => $turns,
'userids' => $userids,
]);
if ($flow) {
$ids[] = $flow->id;
if ($flow->id != $id) {
$idc[$id] = $flow->id;
}
if ($flow->status == 'start') {
$hasStart = true;
}
if ($flow->status == 'end') {
$hasEnd = true;
}
}
}
if (!$hasStart) {
throw new ApiException('至少需要1个开始状态');
}
if (!$hasEnd) {
throw new ApiException('至少需要1个结束状态');
}
ProjectFlowItem::whereFlowId($projectFlow->id)->whereNotIn('id', $ids)->delete();
//
$projectFlow = ProjectFlow::with(['projectFlowItem'])->whereProjectId($project->id)->find($projectFlow->id);
$itemIds = $projectFlow->projectFlowItem->pluck('id')->toArray();
foreach ($projectFlow->projectFlowItem as $item) {
$turns = $item->turns;
foreach ($idc as $oid => $nid) {
if (in_array($oid, $turns)) {
$turns = array_diff($turns, [$oid]);
$turns[] = $nid;
}
}
if (!in_array($item->id, $turns)) {
$turns[] = $item->id;
}
$turns = array_values(array_filter(array_unique(array_intersect($turns, $itemIds))));
sort($turns);
$item->turns = $turns;
ProjectFlowItem::whereId($item->id)->update([ 'turns' => Base::array2json($turns) ]);
}
return Base::retSuccess('保存成功', $projectFlow);
});
}
/**
* @api {get} api/project/flow/delete 29. 删除工作流
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
* @apiGroup project
* @apiName flow__delete
*
* @apiParam {Number} project_id 项目ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function flow__delete()
{
User::auth();
//
$project_id = intval(Request::input('project_id'));
//
$project = Project::userProject($project_id, true, true);
//
return AbstractModel::transaction(function() use ($project) {
ProjectFlow::whereProjectId($project->id)->delete();
ProjectFlowItem::whereProjectId($project->id)->delete();
return Base::retSuccess('删除成功');
});
}
/**
* @api {get} api/project/log/lists 30. 获取项目、任务日志
*

View File

@ -24,6 +24,9 @@ class VerifyCsrfToken extends Middleware
// 添加任务
'api/project/task/add/',
// 保存工作流
'api/project/flow/save/',
// 修改任务
'api/project/task/update/',

View File

@ -140,7 +140,11 @@ class AbstractModel extends Model
$row = static::where($where)->first();
if (empty($row)) {
$row = new static;
$row->updateInstance(array_merge($where, $insert ?: $update));
$array = array_merge($where, $insert ?: $update);
if (isset($array[$row->primaryKey])) {
unset($array[$row->primaryKey]);
}
$row->updateInstance($array);
} elseif ($update) {
$row->updateInstance($update);
}

View File

@ -30,6 +30,8 @@ use Request;
* @property-read int|null $project_log_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectUser[] $projectUser
* @property-read int|null $project_user_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectFlow[] $projectFlowItem
* @property-read int|null $project_flow_item_count
* @method static \Illuminate\Database\Eloquent\Builder|Project allData($userid = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project authData($userid = null, $owner = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project newModelQuery()
@ -99,6 +101,14 @@ class Project extends AbstractModel
return $this->hasMany(ProjectUser::class, 'project_id', 'id')->orderBy('id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function projectFlowItem(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(projectFlowItem::class, 'project_id', 'id')->orderBy('sort');
}
/**
* 查询所有项目与正常查询多返回owner字段
* @param self $query

View File

@ -0,0 +1,93 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectFlow
*
* @property int $id
* @property int|null $project_id 项目ID
* @property string|null $name 流程名称
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectFlowItem[] $projectFlowItem
* @property-read int|null $project_flow_item_count
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereUpdatedAt($value)
* @mixin \Eloquent
*/
class ProjectFlow extends AbstractModel
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function projectFlowItem(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ProjectFlowItem::class, 'flow_id', 'id')->orderBy('sort');
}
public static function addFlow()
{
AbstractModel::transaction(function() {
$projectFlow = ProjectFlow::whereProjectId($project->id)->first();
if (empty($projectFlow)) {
$projectFlow = ProjectFlow::createInstance([
'project_id' => $project->id,
'name' => 'Default'
]);
if (!$projectFlow->save()) {
return Base::retError('工作流创建失败');
}
}
//
$ids = [];
$idc = [];
$hasStart = false;
$hasEnd = false;
foreach ($flows as $item) {
$id = intval($item['id']);
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
$flow = ProjectFlowItem::updateInsert([
'id' => $id,
'project_id' => $project->id,
'flow_id' => $projectFlow->id,
], [
'name' => trim($item['name']),
'status' => trim($item['status']),
'sort' => intval($item['sort']),
'turns' => $turns,
'userids' => $userids,
]);
if ($flow) {
$ids[] = $flow->id;
if ($flow->id != $id) {
$idc[$id] = $flow->id;
}
if ($flow->status == 'start') {
$hasStart = true;
}
if ($flow->status == 'end') {
$hasEnd = true;
}
}
}
if (!$hasStart) {
return Base::retError('至少需要1个开始状态');
}
if (!$hasEnd) {
return Base::retError('至少需要1个结束状态');
}
ProjectFlowItem::whereFlowId($projectFlow->id)->whereNotIn('id', $ids)->delete();
});
return Base::retSuccess("success");
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectFlowItem
*
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $flow_id 流程ID
* @property string|null $name 名称
* @property string|null $status 状态
* @property array $turns 可流转
* @property array $userids 自动负责人ID
* @property int|null $sort 排序
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereFlowId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereSort($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereTurns($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereUserids($value)
* @mixin \Eloquent
*/
class ProjectFlowItem extends AbstractModel
{
/**
* @param $value
* @return array
*/
public function getTurnsAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* @param $value
* @return array
*/
public function getUseridsAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
}

View File

@ -53,6 +53,8 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserimg($value)
* @mixin \Eloquent
* @property string|null $disable_at 禁用时间
* @method static \Illuminate\Database\Eloquent\Builder|User whereDisableAt($value)
*/
class User extends AbstractModel
{

View File

@ -830,13 +830,16 @@ class Base
/**
* 数组只保留数字的
* @param $array
* @param bool $int 是否格式化值
* @return array
*/
public static function arrayRetainInt($array)
public static function arrayRetainInt($array, $int = false)
{
foreach ($array as $k => $v) {
if (!is_numeric($v)) {
unset($array[$k]);
} elseif ($int === true) {
$array[$k] = intval($v);
}
}
return array_values($array);

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectFlowItemsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_flow_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
$table->bigInteger('flow_id')->nullable()->default(0)->comment('流程ID');
$table->string('name', 50)->nullable()->default('')->comment('名称');
$table->string('status', 20)->nullable()->default('')->comment('状态');
$table->string('turns')->nullable()->default('')->comment('可流转');
$table->string('userids')->nullable()->default('')->comment('自动负责人ID');
$table->integer('sort')->nullable()->comment('排序');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_flow_items');
}
}

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectFlowsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_flows', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
$table->string('name', 50)->nullable()->default('')->comment('流程名称');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_flows');
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ProjectTasksAddFlowItemId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_tasks', function (Blueprint $table) {
if (!Schema::hasColumn('project_tasks', 'flow_item_id')) {
$table->bigInteger('flow_item_id')->nullable()->default(0)->after('dialog_id')->comment('工作流状态ID');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_tasks', function (Blueprint $table) {
$table->dropColumn("flow_item_id");
});
}
}

View File

@ -51,7 +51,8 @@
<Icon class="menu-icon" type="ios-more" />
<EDropdownMenu v-if="projectData.owner_userid === userId" slot="dropdown">
<EDropdownItem command="setting">{{$L('项目设置')}}</EDropdownItem>
<EDropdownItem command="user">{{$L('成员管理')}}</EDropdownItem>
<EDropdownItem command="workflow">{{$L('工作流设置')}}</EDropdownItem>
<EDropdownItem command="user" divided>{{$L('成员管理')}}</EDropdownItem>
<EDropdownItem command="invite">{{$L('邀请链接')}}</EDropdownItem>
<EDropdownItem command="log" divided>{{$L('项目动态')}}</EDropdownItem>
<EDropdownItem command="archived_task">{{$L('已归档任务')}}</EDropdownItem>
@ -431,6 +432,14 @@
</div>
</Modal>
<!--工作流程设置-->
<DrawerOverlay
v-model="workflowShow"
placement="right"
:size="1200">
<ProjectWorkflow v-if="workflowShow" :project-id="projectId"/>
</DrawerOverlay>
<!--查看项目动态-->
<DrawerOverlay
v-model="logShow"
@ -465,10 +474,12 @@ import TaskRow from "./TaskRow";
import TaskArchived from "./TaskArchived";
import ProjectLog from "./ProjectLog";
import DrawerOverlay from "../../../components/DrawerOverlay";
import ProjectWorkflow from "./ProjectWorkflow";
export default {
name: "ProjectList",
components: {
ProjectWorkflow,
DrawerOverlay,
ProjectLog, TaskArchived, TaskRow, Draggable, TaskAddSimple, UserInput, TaskAdd, TaskPriority},
data() {
@ -510,6 +521,7 @@ export default {
transferData: {},
transferLoad: 0,
workflowShow: false,
logShow: false,
archivedTaskShow: false,
@ -1201,6 +1213,10 @@ export default {
this.inviteGet()
break;
case "workflow":
this.workflowShow = true;
break;
case "log":
this.logShow = true;
break;

View File

@ -0,0 +1,438 @@
<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" :class="item.status">{{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 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 overlay-y">
<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>
<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" 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"
class="taskflow-config-table-list-wrapper"
tag="div"
draggable=".column-border"
@sort="">
<div v-for="item in data.project_flow_item" 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">{{$L(item.name)}}</div>
<EDropdown
trigger="click"
class="more"
@command="onMore($event, item)">
<Icon type="ios-more" />
<EDropdownMenu slot="dropdown" class="taskflow-config-more-dropdown-menu">
<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" :borderWitdh="1" :showName="item.userids.length === 1" tooltipDisabled/>
</div>
</EDropdownItem>
<EDropdownItem command="user">
<div class="item">
<Icon type="md-person" />
{{$L('自动添加负责人')}}
</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>
</EDropdownMenu>
</EDropdown>
</div>
</div>
<div :ref="`overlay_${data.id}`" class="taskflow-config-table-column-body overlay-y">
<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="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" @click="onCreate">{{$L('创建工作流')}}</Button>
</div>
<!--成员管理-->
<Modal
v-model="userShow"
:title="$L('自动负责人')"
:mask-closable="false">
<Form :model="userData" label-width="auto" @submit.native.prevent>
<FormItem prop="userids" :label="userData.name">
<UserInput v-if="userShow" v-model="userData.userids" :project-id="projectId" :multiple-max="5" :placeholder="$L('选择成员')"/>
<div class="form-tip">{{$L('任务流转到此流程时自动添加负责人')}}</div>
</FormItem>
</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 UserInput from "../../../components/UserInput";
export default {
name: "ProjectWorkflow",
components: {UserInput, Draggable},
props: {
projectId: {
type: Number,
default: 0
},
},
data() {
return {
loadIng: 0,
list: [],
openIndex: "",
userShow: false,
userData: {},
}
},
mounted() {
},
computed: {
},
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.loadIng--;
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}) => {
this.loadIng--;
$A.modalError(msg);
});
},
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
},
onCreate() {
let id = -1 * $A.randNum(1000, 10000);
this.list.push({
"id": id,
"name": "Default workflow",
"project_flow_item": [
{
"id": -10,
"name": "待处理",
"status": "start",
"turns": [-10, -11, -12, -13],
"userids": [],
},
{
"id": -11,
"name": "进行中",
"status": "progress",
"turns": [-10, -11, -12, -13],
"userids": [],
},
{
"id": -12,
"name": "已完成",
"status": "end",
"turns": [-10, -11, -12, -13],
"userids": [],
},
{
"id": -13,
"name": "已取消",
"status": "end",
"turns": [-10, -11, -12, -13],
"userids": [],
}
]
})
this.openIndex = "index_" + id;
},
onDelete(data) {
$A.modalConfirm({
title: '删除工作流',
content: '你确定要删除工作流吗?',
loading: true,
onOk: () => {
if (data.id > 0) {
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/flow/delete',
data: {
project_id: this.projectId,
},
}).then(({msg}) => {
this.loadIng--;
$.messageSuccess(msg);
this.$Modal.remove();
//
let index = this.list.findIndex(({id}) => id == data.id)
if (index > -1) {
this.list.splice(index, 1)
}
}).catch(({msg}) => {
this.loadIng--;
$A.modalError(msg, 301);
this.$Modal.remove();
});
} else {
let index = this.list.findIndex(({id}) => id == data.id)
if (index > -1) {
this.list.splice(index, 1)
}
this.$Modal.remove();
}
}
});
},
onMore(name, item) {
switch (name) {
case "user":
this.$set(this.userData, 'id', item.id);
this.$set(this.userData, 'name', item.name);
this.$set(this.userData, 'userids', item.userids);
this.userShow = true;
break;
case "name":
this.onName(item);
break;
case "remove":
this.onRemove(item);
break;
}
},
onUser() {
this.userShow = false;
this.list.some(data => {
let item = data.project_flow_item.find(item => item.id == this.userData.id)
if (item) {
this.$set(item, 'userids', this.userData.userids)
}
})
},
onName(item) {
$A.modalInput({
value: item.name,
title: "修改名称",
placeholder: "输入流程名称",
onOk: (name) => {
if (name) {
this.$set(item, 'name', name);
}
return true;
}
});
},
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) {
let id = $A.randNum(100000, 999999) * -1;
let turns = data.project_flow_item.map(({id}) => id)
data.project_flow_item.push({
id,
name,
status: 'end',
turns,
userids: [],
})
data.project_flow_item.some(item => {
item.turns.push(id)
})
}
return true;
}
});
},
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}) => {
this.loadIng--;
$.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;
}).catch(({msg}) => {
this.loadIng--;
$A.modalError(msg);
});
},
}
}
</script>

View File

@ -5,6 +5,7 @@
@import "project-list";
@import "project-log";
@import "project-management";
@import "project-workflow";
@import "task-add";
@import "task-add-simple";
@import "task-archived";

View File

@ -0,0 +1,431 @@
.project-workflow {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
.workflow-title {
color: #333333;
font-size: 20px;
font-weight: 500;
line-height: 1;
padding: 20px 20px 24px;
display: flex;
align-items: center;
.title-icon {
display: flex;
align-items: center;
width: 14px;
height: 14px;
margin-left: 4px;
margin-top: 2px;
> i {
cursor: pointer;
}
}
}
.workflow-content {
flex: 1;
padding: 0 20px;
overflow: auto;
.ivu-collapse-header {
display: flex;
align-items: center;
}
.workflow-item {
flex: 1;
display: flex;
align-items: center;
.workflow-name {
flex-shrink: 0;
margin-right: 24px;
}
.workflow-status {
flex: 1;
display: flex;
align-items: center;
> div {
height: 22px;
line-height: 22px;
margin-right: 8px;
padding: 0 8px;
border: 1px solid #e8eaec;
border-radius: 3px;
background: #f7f7f7;
font-size: 12px;
vertical-align: middle;
overflow: hidden;
&.start {
background-color: rgba(38, 38, 38, 0.05);
border-color: rgba(38, 38, 38, 0.05);
color: #595959;
}
&.progress {
background-color: rgba(27, 154, 238, 0.1);
border-color: rgba(27, 154, 238, 0.1);
color: #0171c2;
}
&.end {
background-color: rgba(21, 173, 49, 0.1);
border-color: rgba(21, 173, 49, 0.1);
color: #038a24;
}
}
}
.workflow-save {
margin: 0 8px;
flex-shrink: 0;
> button {
height: 26px;
line-height: 24px;
padding: 0 13px;
font-size: 13px;
margin-right: 4px;
}
}
}
}
.workflow-no {
flex: 1;
padding: 0 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
> button {
margin-top: 18px;
}
}
.taskflow-config {
display: flex;
max-height: 580px;
.taskflow-config-table {
display: flex;
width: 100%;
border-radius: 4px;
box-shadow: 1px 0 3px rgba(0, 0, 0, 0.1);
.taskflow-config-table-left-container {
position: relative;
flex-shrink: 0;
width: 246px;
padding-top: 16px;
padding-bottom: 16px;
overflow-x: scroll;
-ms-overflow-style: none;
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 1;
border-right: 1px solid #f4f4f5;
}
}
.taskflow-config-table-column-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 58px;
padding: 0 20px;
font-size: 16px;
font-weight: 700;
line-height: 58px;
&.left-header {
top: 16px
}
}
.taskflow-config-table-column-body {
margin-top: 58px;
height: calc(100% - 58px);
}
.taskflow-config-table-block {
width: 100%;
padding: 12px 0;
&.hr {
position: relative;
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
border-top: 1px solid #f4f4f5;
}
}
.taskflow-config-table-block-title {
padding: 0 20px;
height: 40px;
color: #8c8c8c;
line-height: 40px
}
.ivu-radio-group {
display: block;
text-align: center;
.ivu-radio-group-item {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
padding: 0 20px;
height: 58px;
}
}
.ivu-checkbox-group {
display: block;
text-align: center;
.ivu-checkbox-group-item {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
padding: 0 20px;
height: 58px;
}
}
.taskflow-config-table-block-item {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
padding: 0 20px;
height: 58px;
&.with-indicator:before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
height: 20px;
width: 4px;
border-radius: 4px
}
.title {
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.subtitle {
margin-top: 2px;
font-size: 12px;
color: #8c8c8c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
&.center {
align-items: center
}
&.radio-item>span {
display: none
}
.transform-status-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
}
}
.taskflow-config-table-right-container {
flex-grow: 1;
flex-direction: row;
padding-top: 16px;
padding-bottom: 16px;
overflow-x: auto;
overflow-y: hidden;
}
.taskflow-config-table-list-wrapper,
.taskflow-config-table-right-container {
display: flex
}
.taskflow-config-table-status-column {
position: relative;
flex-shrink: 0;
width: 210px;
height: 100%;
margin-bottom: 16px;
margin-right: 16px;
border-radius: 4px;
&:first-child {
margin-left: 20px
}
&:hover {
.status-label-with-menu {
.more {
opacity: 1;
}
}
}
&.addnew {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px dashed #d9d9d9;
cursor: pointer;
color: #8c8c8c;
&:hover {
color: #777777;
border-color: #bfbfbf
}
}
&.column-border {
border-width: 1px;
border-style: solid
}
&.start {
border-color: #e5e5e5;
&:hover {
border-color: #bfbfbf
}
}
&.progress {
border-color: #ccecff;
&:hover {
border-color: #87d2ff
}
}
&.end {
border-color: #cafac8;
&:hover {
border-color: #64d16d
}
}
}
.taskflow-config-table-status-item {
display: flex;
align-items: center;
padding: 8px;
height: 58px;
line-height: 58px;
cursor: move
}
.status-label-with-menu {
max-width: 100%;
display: inline-flex;
align-items: center;
border-radius: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
padding: 0 32px;
width: 200px;
height: 36px;
line-height: 32px;
font-size: 14px;
font-weight: 400;
justify-content: center;
&:hover {
font-weight: 700;
}
&.start {
background: rgba(38,38,38,0.05);
color: #595959
}
&.progress {
background: rgba(27,154,238,0.1);
color: #0171c2
}
&.end {
background: rgba(21,173,49,0.1);
color: #038a24
}
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.more {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
font-size: 18px;
opacity: 0;
transition: opacity 0.2s;
&.opacity {
opacity: 1;
}
}
}
}
}
}
.taskflow-config-more-dropdown-menu {
.users {
display: flex;
padding: 6px 0;
margin: 0 -8px;
overflow: auto;
.common-avatar {
max-width: 100%;
margin-right: -5px;
}
}
.item {
display: flex;
align-items: center;
}
.delete {
color: #f00;
}
}