2025-09-23 15:03:29 +08:00

723 lines
28 KiB
Vue
Raw 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="task-add">
<div class="head" :class="{empty:addData.cascader.length == 0,visible:cascaderShow}">
<Cascader
v-model="addData.cascader"
:data="cascaderData"
:clearable="false"
:placeholder="$L('请选择项目')"
:load-data="cascaderLoadData"
@on-change="cascaderChange"
@on-input-change="cascaderInputChange"
@on-visible-change="cascaderShow=!cascaderShow"
filterable/>
</div>
<ul v-if="taskTemplateList.length > 0" class="task-add-template">
<li
v-for="item in taskTemplateList"
:key="item.id"
:class="{active:templateActiveID === item.id}"
@click="setTaskTemplate(item)">
{{ item.name }}
</li>
</ul>
<div class="task-add-form">
<div class="title">
<Input
v-model="addData.name"
ref="input"
type="textarea"
:rows="1"
:autosize="{ minRows: 1, maxRows: 8 }"
:maxlength="255"
:placeholder="$L('任务描述')"
enterkeyhint="done"
@on-keydown="onKeydown"/>
<div class="ai-btn" @click="onAI">
<i class="taskfont">&#xe8a1;</i>
</div>
</div>
<TEditorTask
ref="editorTaskRef"
class="desc"
v-model="addData.content"
:placeholder="$L(windowLandscape ? '详细描述,选填...(点击右键使用工具栏)' : '详细描述,选填...')"
:placeholderFull="$L('详细描述...')"/>
<div class="advanced-option" :class="{'advanced-open': advanced}">
<Button @click="advanced=!advanced">{{$L('高级选项')}}</Button>
<ul class="advanced-priority">
<li v-for="(item, key) in taskPriority" :key="key">
<ETooltip :disabled="$isEEUIApp || windowTouch" :content="taskPriorityContent(item)">
<i
class="taskfont"
:style="{color:item.color}"
v-html="addData.p_name == item.name ? '&#xe61d;' : '&#xe61c;'"
@click="choosePriority(item)"></i>
</ETooltip>
</li>
</ul>
<DatePicker
v-if="taskDays > 0 || taskTimeOpen"
:open="taskTimeOpen"
v-model="addData.times"
:options="timeOptions"
:placeholder="$L('选择计划范围')"
format="yyyy/MM/dd HH:mm"
type="datetimerange"
placement="bottom"
@on-change="taskTimeChange(addData)"
@on-open-change="taskTimeOpenChange">
<div class="advanced-time" @click="taskTimeOpenChange(!taskTimeOpen)">
<Icon type="ios-clock-outline" />
<em type="primary" :style="addData.p_color ? {backgroundColor:addData.p_color} : {}">{{taskDays}}</em>
</div>
</DatePicker>
</div>
</div>
<Form v-if="advanced" class="task-add-advanced" v-bind="formOptions" @submit.native.prevent>
<FormItem :label="$L('计划时间')">
<DatePicker
v-model="addData.times"
:options="timeOptions"
:editable="false"
:placeholder="$L('选择计划范围')"
format="yyyy/MM/dd HH:mm"
type="datetimerange"
@on-change="taskTimeChange(addData)"/>
</FormItem>
<FormItem :label="$L('负责人')" >
<UserSelect
v-model="addData.owner"
:multiple-max="10"
:title="$L('选择任务负责人')"
:project-id="addData.project_id"
:avatar-size="24"
border/>
</FormItem>
<FormItem :label="$L('协助人员')" >
<UserSelect
v-model="addData.assist"
:multiple-max="10"
:title="$L('选择任务协助人员')"
:project-id="addData.project_id"
:disabled-choice="addData.owner"
:avatar-size="24"
border/>
</FormItem>
<FormItem>
<div slot="label" class="visibility-text" @click="showCisibleDropdown">
{{ $L('可见性') }}
<i class="taskfont">&#xe740;</i>
</div>
<div
v-if="addData.visibility_appoint == 1 || addData.visibility_appoint == 2"
ref="visibilityText"
class="ivu-input task-add-visibility"
@click="showCisibleDropdown">
{{ addData.visibility_appoint == 1 ? $L('项目人员可见') : $L('任务人员可见') }}
</div>
<UserSelect
v-else
ref="visibleUserSelectRef"
v-model="addData.visibility_appointor"
:avatar-size="24"
:title="$L('选择指定人员')"
:project-id="addData.project_id"
@on-show-change="visibleUserSelectShowChange"
border/>
</FormItem>
<div class="subtasks">
<div v-if="addData.subtasks.length > 0" class="sublist">
<Row>
<Col span="12">{{$L('任务描述')}}</Col>
<Col span="8">{{$L('计划时间')}}</Col>
<Col span="4">{{$L('负责人')}}</Col>
</Row>
<Row v-for="(item, key) in addData.subtasks" :key="key">
<Col span="12">
<Input
v-model="item.name"
:maxlength="255"
clearable
@on-clear="addData.subtasks.splice(key, 1)"/>
</Col>
<Col span="8" :title="timeTitle(item.times)">
<DatePicker
v-model="item.times"
:options="timeOptions"
:editable="false"
:placeholder="$L('选择时间')"
format="yyyy/MM/dd HH:mm"
type="datetimerange"
@on-change="taskTimeChange(item)"/>
</Col>
<Col span="4">
<UserSelect
v-model="item.owner"
:multiple-max="1"
:title="$L('选择负责人')"
:project-id="addData.project_id"
:avatar-size="24"
border/>
</Col>
</Row>
</div>
<Input
type="text"
v-model="subName"
:class="['enter-input', subName == '' ? 'empty' : '']"
@on-enter="addSubTask"
:placeholder="$L('+ 输入子任务,回车添加子任务')"/>
</div>
</Form>
<div class="ivu-modal-footer">
<div class="adaption">
<Button type="default" @click="close">{{$L('取消')}}</Button>
<ButtonGroup class="page-manage-add-task-button-group">
<Button type="primary" :loading="loadIng > 0" @click="onAdd(false)">{{$L('添加任务')}}</Button>
<Dropdown @on-click="onAdd(true)" transfer>
<Button type="primary">
<Icon type="ios-arrow-down"></Icon>
</Button>
<DropdownMenu slot="list">
<DropdownItem :disabled="loadIng > 0">{{$L('提交继续添加')}}</DropdownItem>
</DropdownMenu>
</Dropdown>
</ButtonGroup>
</div>
</div>
<TaskExistTips ref="taskExistTipsRef" @onContinue="onAdd(addContinue, true)"/>
</div>
</template>
<script>
import {mapState} from "vuex";
import UserSelect from "../../../components/UserSelect.vue";
import TaskExistTips from "./TaskExistTips.vue";
import TEditorTask from "../../../components/TEditorTask.vue";
import nostyle from "../../../components/VMEditor/engine/nostyle";
export default {
name: "TaskAdd",
components: {TEditorTask, UserSelect, TaskExistTips},
props: {
value: {
type: Boolean,
default: false
},
},
data() {
return {
addData: {
// 基本信息
cascader: [],
name: "",
content: "",
owner: [],
assist: [],
project_id: 0,
column_id: 0,
times: [],
subtasks: [],
// 优先级
p_level: 0,
p_name: '',
p_color: '',
// 可见性
visibility_appoint: 1,
visibility_appointor: [],
},
addDefault: {},
cascaderShow: false,
cascaderData: [],
cascaderValue: '',
cascaderLoading: 0,
cascaderAlready: [],
advanced: false,
subName: '',
taskTimeOpen: false,
timeOptions: {shortcuts: $A.timeOptionShortcuts()},
loadIng: 0,
isMounted: false,
beforeClose: [],
addContinue: false,
templateActiveID: 0,
templateCompareData: {name: '', content: ''},
}
},
created() {
this.addDefault = $A.cloneJSON(this.addData);
},
async mounted() {
this.initCascaderData();
await this.initProjectData();
this.$nextTick(() => {
this.$refs.input.focus();
this.templateCompareData = {name: this.addData.name, content: this.addData.content};
})
this.isMounted = true
},
beforeDestroy() {
this.beforeClose.some(func => {
typeof func === "function" && func()
})
this.beforeClose = [];
},
computed: {
...mapState(['cacheProjects', 'projectId', 'cacheColumns', 'taskPriority', 'taskTemplates', 'formOptions']),
taskDays() {
const {times} = this.addData;
const temp = $A.newDateString(times, "YYYY-MM-DD HH:mm");
if (temp[0] && temp[1]) {
const d = Math.ceil($A.dayjs(temp[1]).diff(temp[0], 'day', true));
if (d > 0) {
return d;
}
}
return 0;
},
taskTemplateList() {
return this.taskTemplates.filter(({project_id}) => project_id == this.addData.project_id) || []
}
},
watch: {
'addData.owner'(owner, newOwner) {
if (JSON.stringify(owner) === JSON.stringify(newOwner)) {
return;
}
this.addData.assist = this.addData.assist.filter(item => {
return owner.indexOf(item) === -1;
})
if (this.addData.assist.length === 0 && owner.indexOf(this.userId) === -1) {
this.addData.assist = [this.userId];
}
},
'addData.project_id'(projectId) {
if (projectId > 0) {
$A.IDBSave("cacheAddTaskProjectId", projectId);
this.$store.dispatch("updateTaskTemplates", projectId).then(this.setTaskDefaultTemplate)
}
},
'addData.column_id'(columnId) {
if (columnId > 0) {
$A.IDBSave("cacheAddTaskColumnId", columnId);
}
const {project_id} = this.addData;
if (project_id && columnId) {
this.$set(this.addData, 'cascader', [project_id, columnId]);
} else {
this.$set(this.addData, 'cascader', []);
}
}
},
methods: {
/**
* 初始化级联数据
*/
initCascaderData() {
const data = $A.cloneJSON(this.cacheProjects).sort((a, b) => {
if (a.top_at || b.top_at) {
return $A.sortDay(b.top_at, a.top_at);
}
return b.id - a.id;
});
this.cascaderData = data.map(project => {
const children = this.cacheColumns.filter(({project_id}) => project_id == project.id).map(column => {
return {
value: column.id,
label: column.name
}
});
const data = {
value: project.id,
label: project.name,
children,
};
if (children.length == 0) {
data.loading = false;
}
return data
});
},
/**
* 初始化项目、列表、优先级
*/
async initProjectData() {
// 项目、列表
let cacheAddTaskProjectId = await $A.IDBInt("cacheAddTaskProjectId");
let project = this.cacheProjects.find(({id}) => id == this.projectId)
|| this.cacheProjects.find(({id}) => id == cacheAddTaskProjectId)
|| this.cacheProjects.find(({id}) => id > 0);
if (project) {
let cacheAddTaskColumnId = await $A.IDBInt("cacheAddTaskColumnId");
let column = this.cacheColumns.find(({project_id, id}) => project_id == project.id && id == cacheAddTaskColumnId)
|| this.cacheColumns.find(({project_id}) => project_id == project.id);
if (column) {
this.addData.project_id = column.project_id;
this.addData.column_id = column.id;
} else {
this.$store.dispatch("getColumns", project.id).then(() => {
column = this.cacheColumns.find(({project_id, id}) => project_id == project.id && id == cacheAddTaskColumnId)
|| this.cacheColumns.find(({project_id}) => project_id == project.id);
if (column) {
this.addData.project_id = column.project_id;
this.addData.column_id = column.id;
}
}).catch(() => {});
}
}
// 优先级
if (this.taskPriority.length > 0) {
await this.choosePriority(this.taskPriority[0]);
}
},
async taskTimeChange(data) {
const times = $A.newDateString(data.times, "YYYY-MM-DD HH:mm");
this.$set(data, 'times', await this.$store.dispatch("taskDefaultTime", times))
},
taskTimeOpenChange(val) {
this.taskTimeOpen = val;
},
timeTitle(value) {
return value ? $A.newDateString(value) : null
},
onKeydown(e) {
if (e.keyCode === 13) {
if (e.shiftKey) {
return;
}
e.preventDefault();
this.onAdd();
}
},
addSubTask() {
if (this.subName.trim() !== '') {
this.addData.subtasks.push({
name: this.subName.trim(),
owner: [this.userId],
times: [],
});
this.subName = '';
}
},
taskPriorityContent(item) {
const days = $A.runNum(item.days);
if (days <= 0) {
return item.name + ' (' + this.$L('无时间限制') + ')';
}
return item.name + ' (' + days + this.$L('天') + ')';
},
async choosePriority(item) {
const start = $A.daytz();
const days = $A.runNum(item.days);
if (days > 0) {
const end = start.clone().add(days, 'day');
this.$set(this.addData, 'times', await this.$store.dispatch("taskDefaultTime", $A.newDateString([start, end], 'YYYY-MM-DD 00:00')))
} else {
this.$set(this.addData, 'times', [])
}
this.$set(this.addData, 'p_level', item.priority)
this.$set(this.addData, 'p_name', item.name)
this.$set(this.addData, 'p_color', item.color)
},
cascaderLoadData(item, callback) {
item.loading = true;
this.$store.dispatch("getColumns", item.value).then((data) => {
item.children = data.map(column => {
return {
value: column.id,
label: column.name
}
});
item.loading = false;
callback();
}).catch(() => {
item.loading = false;
callback();
});
},
cascaderChange(value) {
if (value[1]) {
this.$set(this.addData, 'project_id', value[0])
this.$set(this.addData, 'column_id', value[1])
}
},
cascaderInputChange(key) {
this.cascaderValue = key || "";
//
if (this.cascaderAlready[this.cascaderValue] === true) {
return;
}
this.cascaderAlready[this.cascaderValue] = true;
//
setTimeout(() => {
this.cascaderLoading++;
}, 1000)
this.$store.dispatch("getProjects", {
keys: {
name: this.cascaderValue,
},
getcolumn: 'yes'
}).then(() => {
this.cascaderLoading--;
this.initCascaderData();
}).catch(() => {
this.cascaderLoading--;
});
},
setData(data) {
if (!this.isMounted) {
this.__setData && clearTimeout(this.__setData)
this.__setData = setTimeout(_ => this.setData(data) , 10)
return
}
if (typeof data.beforeClose !== "undefined") {
this.beforeClose.push(data.beforeClose)
delete data.beforeClose;
}
this.addData = Object.assign({}, this.addData, data);
},
async onAdd(continued = false, affirm = false) {
if (!this.addData.name) {
$A.messageError("任务描述不能为空");
return;
}
// 存在任务提示
if (!affirm && this.addData.owner.length > 0) {
this.loadIng++;
this.$refs.taskExistTipsRef.isExistTask({
userids: this.addData.owner,
timerange: this.addData.times
}, 600).then(res => {
if (!res) {
this.onAdd(continued, true)
} else {
this.addContinue = continued
}
this.loadIng--;
});
return;
}
this.loadIng++;
this.$store.dispatch("taskAdd", this.addData).then(({msg}) => {
$A.messageSuccess(msg);
if (continued === true) {
this.addData = Object.assign({}, this.addData, this.templateCompareData, {subtasks: []});
this.$refs.input.focus();
} else {
this.addData = $A.cloneJSON(this.addDefault);
this.close()
}
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(() => {
this.loadIng--;
});
},
close() {
this.$emit("input", !this.value)
},
showCisibleDropdown(event){
const list = [
{label: '项目人员', value: 1},
{label: '任务人员', value: 2},
{label: '指定成员', value: 3},
];
this.$store.commit('menu/operation', {
event,
list,
size: 'large',
active: this.addData.visibility_appoint,
onUpdate: (value) => {
this.dropVisible(value)
}
})
},
visibleUserSelectShowChange(isShow){
if(!isShow && (this.addData.visibility_appointor.length == 0 || !this.addData.visibility_appointor[0])){
let old = this.addData.old_visibility_appoint;
this.addData.visibility_appoint = old > 2 ? 1 : (old || 1);
if(this.addData.visibility_appoint < 3 ){
}
}
},
dropVisible(command) {
switch (command) {
case 1:
case 2:
this.addData.visibility_appoint = command
break;
case 3:
this.addData.old_visibility_appoint = this.addData.visibility_appoint
this.addData.visibility_appoint = command
this.$nextTick(() => {
this.$refs.visibleUserSelectRef.onSelection()
});
break;
}
},
setTaskTemplate(item, force = false) {
if (force) {
this.templateActiveID = item.id;
this.addData.name = item.title;
this.addData.content = nostyle(item.content, {sanitize: false});
this.$nextTick(() => {
this.$refs.input.focus();
this.templateCompareData = {name: this.addData.name, content: this.addData.content};
});
return;
}
if ((this.addData.name !== this.templateCompareData.name && this.addData.name !== '') ||
(this.addData.content !== this.templateCompareData.content && this.addData.content !== '')) {
$A.modalConfirm({
content: '当前已有修改的内容,是否要覆盖?',
onOk: () => this.setTaskTemplate(item, true)
});
} else {
this.setTaskTemplate(item, true);
}
},
setTaskDefaultTemplate() {
const defaultTemplate = this.taskTemplateList.find(({is_default}) => is_default);
if (defaultTemplate) {
this.setTaskTemplate(defaultTemplate);
}
},
onAI() {
let canceled = false;
$A.modalInput({
title: 'AI 生成',
placeholder: '请简要描述任务目标、背景或预期交付AI 将生成标题、详细说明和子任务',
inputProps: {
type: 'textarea',
rows: 2,
autosize: { minRows: 2, maxRows: 6 },
maxlength: 500,
},
onCancel: () => {
canceled = true;
},
onOk: (value) => {
if (!value) {
return `请输入任务需求`
}
return new Promise((resolve, reject) => {
if (canceled) {
reject();
return;
}
// 获取当前任务模板信息
const currentTemplate = this.templateActiveID ?
this.taskTemplateList.find(item => item.id === this.templateActiveID) : null;
this.$store.dispatch("call", {
url: 'project/task/ai_generate',
data: {
content: value,
// 当前已有的标题和内容作为参考
current_title: this.addData.name || '',
current_content: this.addData.content || '',
// 当前选中的任务模板信息
template_name: currentTemplate ? currentTemplate.name : '',
template_content: currentTemplate ? currentTemplate.content : '',
// 其他上下文信息
has_owner: this.addData.owner && this.addData.owner.length > 0,
has_time_plan: this.addData.times && this.addData.times.length > 0,
priority_level: this.addData.p_name || ''
},
timeout: 60 * 1000,
}).then(({data}) => {
if (canceled) {
resolve();
return;
}
this.addData.name = data.title;
this.$refs.editorTaskRef.setContent(data.content, {format: 'raw'});
if (Array.isArray(data.subtasks) && data.subtasks.length > 0) {
const normalized = data.subtasks
.map(item => {
if (typeof item === 'string') {
return item.trim();
}
if (item && typeof item === 'object') {
const name = item.title || item.name || '';
return typeof name === 'string' ? name.trim() : '';
}
return '';
})
.filter(item => item !== '');
const unique = Array.from(new Set(normalized)).slice(0, 8);
if (unique.length > 0) {
const mainOwner = Array.isArray(this.addData.owner) && this.addData.owner.length > 0
? [this.addData.owner[0]]
: (this.userId ? [this.userId] : []);
const subtasks = unique.map(name => ({
name,
owner: [...mainOwner],
times: [],
}));
this.$set(this.addData, 'subtasks', subtasks);
this.advanced = true;
}
}
resolve();
}).catch(({msg}) => {
if (canceled) {
resolve();
return;
}
reject(msg);
});
})
}
})
}
}
}
</script>