perf: 支持查看任务描述修改历史

This commit is contained in:
Pang 2024-04-22 09:13:55 +08:00
parent bd61b8c948
commit dbdb805269
11 changed files with 442 additions and 11 deletions

View File

@ -32,6 +32,7 @@ use App\Models\ProjectTaskUser;
use App\Models\WebSocketDialog;
use App\Exceptions\ApiException;
use App\Models\ProjectPermission;
use App\Models\ProjectTaskContent;
use App\Models\WebSocketDialogMsg;
use App\Module\BillMultipleExport;
use Illuminate\Support\Facades\DB;
@ -1576,7 +1577,8 @@ class ProjectController extends AbstractController
* @apiGroup project
* @apiName task__content
*
* @apiParam {Number} task_id 任务ID
* @apiParam {Number} task_id 任务ID
* @apiParam {Number} [history_id] 历史ID获取历史版本
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -1587,15 +1589,57 @@ class ProjectController extends AbstractController
User::auth();
//
$task_id = intval(Request::input('task_id'));
$history_id = intval(Request::input('history_id'));
//
$task = ProjectTask::userTask($task_id, null);
//
if ($history_id > 0) {
$taskContent = ProjectTaskContent::whereTaskId($task->id)->whereId($history_id)->first();
if (empty($taskContent)) {
return Base::retError('历史版本不存在');
}
return Base::retSuccess('success', array_merge($taskContent->getContentInfo(), [
'name' => $task->name,
]));
}
if (empty($task->content)) {
return Base::retSuccess('success', json_decode('{}'));
}
return Base::retSuccess('success', $task->content->getContentInfo());
}
/**
* @api {get} api/project/task/content_history 25. 获取任务详细历史描述
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__content_history
*
* @apiParam {Number} task_id 任务ID
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:100
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__content_history()
{
User::auth();
//
$task_id = intval(Request::input('task_id'));
//
$task = ProjectTask::userTask($task_id, null);
//
$data = ProjectTaskContent::select(['id', 'task_id', 'desc', 'userid', 'created_at'])
->whereTaskId($task->id)
->orderByDesc('id')
->paginate(Base::getPaginate(100, 20));
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/project/task/files 26. 获取任务文件列表
*

View File

@ -393,7 +393,7 @@ class ProjectTask extends AbstractModel
$p_color = $data['p_color'];
$top = intval($data['top']);
$userid = User::userid();
$visibility = isset($data['visibility_appoint']) ? $data['visibility_appoint'] : $data['visibility'];
$visibility = $data['visibility_appoint'] ?? $data['visibility'];
$visibility_userids = $data['visibility_appointor'] ?: [];
//
if (ProjectTask::whereProjectId($project_id)
@ -527,6 +527,8 @@ class ProjectTask extends AbstractModel
ProjectTaskContent::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'userid' => $task->userid,
'desc' => $task->desc,
'content' => [
'url' => ProjectTaskContent::saveContent($task->id, $content)
],
@ -913,15 +915,25 @@ class ProjectTask extends AbstractModel
}
// 内容
if (Arr::exists($data, 'content')) {
$logRecord = [];
$logContent = ProjectTaskContent::whereTaskId($this->id)->orderByDesc('id')->first();
if ($logContent) {
$logRecord['link'] = [
'title' => '查看历史',
'url' => 'single/task/content/' . $this->id . '?history_id=' . $logContent->id,
];
}
$this->desc = self::generateDesc($data['content']);
ProjectTaskContent::createInstance([
'project_id' => $this->project_id,
'task_id' => $this->id,
'userid' => User::userid(),
'desc' => $this->desc,
'content' => [
'url' => ProjectTaskContent::saveContent($this->id, $data['content'])
],
])->save();
$this->desc = self::generateDesc($data['content']);
$this->addLog("修改{任务}详细描述");
$this->addLog("修改{任务}详细描述", $logRecord);
$updateMarking['is_update_content'] = true;
}
// 优先级

View File

@ -34,7 +34,6 @@ use App\Exceptions\ApiException;
class ProjectTaskContent extends AbstractModel
{
protected $hidden = [
'created_at',
'updated_at',
];

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddProjectTaskContentsUserid extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
//
Schema::table('project_task_contents', function (Blueprint $table) {
if (!Schema::hasColumn('project_task_contents', 'userid')) {
$table->string('desc', 500)->nullable()->default('')->after('task_id')->comment('内容描述');
$table->bigInteger('userid')->nullable()->default(0)->after('task_id')->comment('用户ID');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
Schema::table('project_task_contents', function (Blueprint $table) {
$table->dropColumn("desc");
$table->dropColumn("userid");
});
}
}

View File

@ -354,6 +354,7 @@ export default {
}
});
editor.ui.registry.addMenuItem('imagePreview', {
icon: 'preview',
text: this.$L('预览图片'),
onAction: () => {
this.operateImg = null
@ -394,6 +395,7 @@ export default {
}
});
editor.ui.registry.addMenuItem('screenload', {
icon: 'fullscreen',
text: this.$L('退出全屏'),
onAction: () => {
this.closeFull();
@ -418,6 +420,7 @@ export default {
}
});
editor.ui.registry.addMenuItem('screenload', {
icon: 'fullscreen',
text: this.$L('全屏'),
onAction: () => {
this.onFull();

View File

@ -23,6 +23,7 @@
<div :style="{userSelect:operateVisible ? 'none' : 'auto', height: operateStyles.height}"></div>
<DropdownMenu slot="list">
<DropdownItem @click.native="onEditing">{{ $L('编辑描述') }}</DropdownItem>
<DropdownItem @click.native="onHistory">{{ $L('历史记录') }}</DropdownItem>
<DropdownItem v-if="operateLink" @click.native="onLinkPreview">{{ $L('打开链接') }}</DropdownItem>
<DropdownItem v-if="operateImg" @click.native="onImagePreview">{{ $L('查看图片') }}</DropdownItem>
</DropdownMenu>
@ -81,7 +82,7 @@ export default {
autoresize_bottom_margin: 2,
min_height: 200,
max_height: 380,
contextmenu: 'checklist | bold italic underline forecolor backcolor | link | uploadImages imagePreview | screenload',
contextmenu: 'checklist | bold italic underline forecolor backcolor | link | uploadImages imagePreview | history screenload',
valid_elements: 'a[href|title|target=_blank],em,strong/b,div[align],span[style],a,br,p,img[src|alt|witdh|height],pre[class],code,ol[class],ul[class],li[class]',
extended_valid_elements: 'a[href|title|target=_blank]',
toolbar: false
@ -89,9 +90,13 @@ export default {
optionFull: {
menubar: 'file edit view',
removed_menuitems: 'preview,print',
contextmenu: 'checklist | bold italic underline forecolor backcolor | link | uploadImages imagePreview | screenload',
valid_elements: 'a[href|title|target=_blank],em,strong/b,div[align],span[style],a,br,p,img[src|alt|witdh|height],pre[class],code,ol[class],ul[class],li[class]',
extended_valid_elements: 'a[href|title|target=_blank]',
toolbar: 'uploadImages | checklist | bold italic underline | forecolor backcolor'
toolbar: 'uploadImages | checklist | bold italic underline | forecolor backcolor',
mobile: {
menubar: 'file edit view',
},
},
operateStyles: {},
@ -147,12 +152,17 @@ export default {
this.$refs.desc.onFull()
},
onHistory() {
this.$emit('on-history');
},
onBlur() {
this.$emit('on-blur');
},
onEditorInit(editor) {
this.updateTouchContent();
this.updateHistoryContent(editor);
this.$emit('on-editor-init', editor);
},
@ -237,6 +247,16 @@ export default {
}, timeout)
},
updateHistoryContent(editor) {
editor.ui.registry.addMenuItem('history', {
icon: 'insert-time',
text: this.$L('历史记录'),
onAction: () => {
this.onHistory();
}
});
},
onLinkPreview() {
if (this.operateLink) {
window.open(this.operateLink);

View File

@ -180,6 +180,16 @@ export default {
vNode.push(h('span', {class:'change-value'}, now || '-'))
}
}
if ($A.isJson(record.link)) {
let {title, url} = record.link
vNode.push(h('span', ': '))
vNode.push(h('a', {
attrs: {
href: $A.baseUrl(url),
target: '_blank'
}
}, this.$L(title)))
}
if (record.userid) {
let userids = $A.isArray(record.userid) ? record.userid : [record.userid]
let userNode = [];

View File

@ -0,0 +1,190 @@
<template>
<div class="task-content-history">
<Table
:max-height="windowHeight - 180"
:columns="columns"
:data="list"
:loading="loadIng > 0"
:no-data-text="$L(noText)"
highlight-row
stripe/>
<Page
v-if="total > pageSize"
:total="total"
:current="page"
:page-size="pageSize"
:disabled="loadIng > 0"
:simple="true"
@on-change="setPage"
@on-page-size-change="setPageSize"/>
</div>
</template>
<style lang="scss" scoped>
.task-content-history {
.ivu-page {
margin-top: 12px;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>
<script>
export default {
name: "TaskContentHistory",
props: {
taskId: {
type: Number,
default: 0
},
taskName: {
type: String,
default: ''
},
},
data() {
return {
loadIng: 0,
columns: [
{
title: this.$L('日期'),
key: 'created_at',
width: 168,
}, {
title: this.$L('描述'),
key: 'desc',
ellipsis: true,
minWidth: 150,
render: (h, {row}) => {
return h('span', row.desc || '-');
}
}, {
title: this.$L('创建人'),
width: 120,
render: (h, {row}) => {
if (!row.userid) {
return h('div', '-');
}
return h('UserAvatar', {
props: {
showName: true,
size: 22,
userid: row.userid,
}
})
}
}, {
title: this.$L('操作'),
align: 'center',
width: 100,
render: (h, {index, row, column}) => {
if (index === 0 && this.page === 1) {
return h('div', '-');
}
return h('TableAction', {
props: {
column: column,
menu: [
{
label: this.$L('查看'),
action: "preview",
}
]
},
on: {
action: (name) => {
this.onAction(name, row)
}
}
});
}
}
],
list: [],
page: 1,
pageSize: 10,
total: 0,
noText: ''
}
},
mounted() {
},
watch: {
taskId: {
handler(val) {
if (val) {
this.setPage(1);
}
},
immediate: true,
},
},
methods: {
getLists() {
if (this.taskId === 0) {
return;
}
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/task/content_history',
data: {
task_id: this.taskId,
page: Math.max(this.page, 1),
pagesize: Math.max($A.runNum(this.pageSize), 10),
},
}).then(({data}) => {
this.page = data.current_page;
this.total = data.total;
this.list = data.data;
this.noText = '没有相关的数据';
}).catch(() => {
this.noText = '数据加载失败';
}).finally(_ => {
this.loadIng--;
})
},
setPage(page) {
this.page = page;
this.getLists();
},
setPageSize(pageSize) {
this.page = 1;
this.pageSize = pageSize;
this.getLists();
},
onAction(name, row) {
switch (name) {
case 'preview':
const title = (this.taskName || `ID: ${this.taskId}`) + ` [${row.created_at}]`;
const path = `/single/task/content/${this.taskId}?history_id=${row.id}&history_title=${title}`;
if (this.$isEEUiApp) {
this.$store.dispatch('openAppChildPage', {
pageType: 'app',
pageTitle: title,
url: 'web.js',
params: {
titleFixed: true,
allowAccess: true,
url: $A.rightDelete(window.location.href, window.location.hash) + `#${path}`
},
})
} else {
window.open($A.apiUrl(`..${path}`))
}
break;
}
},
}
}
</script>

View File

@ -140,6 +140,7 @@
class="desc"
:value="taskContent"
:placeholder="$L('详细描述...')"
@on-history="onHistory"
@on-blur="updateBlur('content')"/>
<Form class="items" label-position="left" label-width="auto" @submit.native.prevent>
<FormItem v-if="taskDetail.p_name">
@ -496,6 +497,20 @@
<Button type="primary" @click="onDelay" :loading="delayTaskLoading">{{$L('确定')}}</Button>
</div>
</Modal>
<!--任务描述历史记录-->
<Modal
v-model="historyShow"
:title="$L('任务描述历史记录')"
:mask-closable="false"
:styles="{
width: '90%',
maxWidth: '700px'
}">
<TaskContentHistory v-if="historyShow" :task-id="taskDetail.id" :task-name="taskDetail.name"/>
<div slot="footer">
<Button @click="historyShow=false">{{$L('关闭')}}</Button>
</div>
</Modal>
</div>
</template>
@ -511,10 +526,12 @@ import ChatInput from "./ChatInput";
import UserSelect from "../../../components/UserSelect.vue";
import TaskExistTips from "./TaskExistTips.vue";
import TEditorTask from "../../../components/TEditorTask.vue";
import TaskContentHistory from "./TaskContentHistory.vue";
export default {
name: "TaskDetail",
components: {
TaskContentHistory,
TEditorTask,
UserSelect,
TaskExistTips,
@ -621,7 +638,9 @@ export default {
remark: [
{ required: true, message: this.$L('请输入备注'), trigger: 'blur' },
],
}
},
historyShow: false,
}
},
@ -993,6 +1012,10 @@ export default {
return isModify;
},
onHistory() {
this.historyShow = true;
},
updateBlur(action, params) {
if (this.canUpdateBlur) {
this.updateData(action, params)

View File

@ -0,0 +1,87 @@
<template>
<div class="file-preview">
<PageTitle :title="pageName"/>
<Loading v-if="loadIng > 0"/>
<div v-else-if="info" class="file-preview">
<div class="edit-header">
<div class="header-title">
<div class="title-name">{{pageName}}</div>
<Tag color="default">{{$L('只读')}}</Tag>
<div class="refresh">
<Icon type="ios-refresh" @click="getInfo" />
</div>
</div>
</div>
<div class="content-body">
<TEditor :value="info.content" height="100%" readOnly/>
</div>
</div>
</div>
</template>
<script>
import TEditor from "../../components/TEditor.vue";
export default {
components: {TEditor},
data() {
return {
loadIng: 0,
info: null,
}
},
mounted() {
},
computed: {
taskId() {
return this.$route.params ? $A.runNum(this.$route.params.taskId) : 0;
},
historyId() {
return this.$route.query ? $A.runNum(this.$route.query.history_id) : 0;
},
pageName() {
if (this.$route.query && this.$route.query.history_title) {
return this.$route.query.history_title
}
if (this.info) {
return `${this.info.name} [${this.info.created_at}]`;
}
return '';
}
},
watch: {
'$route': {
handler() {
this.getInfo();
},
immediate: true
},
},
methods: {
getInfo() {
setTimeout(_ => {
this.loadIng++;
}, 600)
this.$store.dispatch("call", {
url: 'project/task/content',
data: {
task_id: this.taskId,
history_id: this.historyId,
},
}).then(({data}) => {
this.info = data;
}).catch(({msg}) => {
$A.modalError({
content: msg,
onOk: () => {
window.close();
}
});
}).finally(_ => {
this.loadIng--;
});
}
}
}
</script>

View File

@ -143,6 +143,11 @@ export default [
path: '/single/file/:codeOrFileId',
component: () => import('./pages/single/file.vue'),
},
{
name: 'single-task-content',
path: '/single/task/content/:taskId',
component: () => import('./pages/single/taskContent.vue'),
},
{
name: 'single-task',
path: '/single/task/:taskId',
@ -154,17 +159,17 @@ export default [
component: () => import('./pages/single/apps.vue')
},
{
name: 'valid-email',
name: 'single-valid-email',
path: '/single/valid/email',
component: () => import('./pages/single/validEmail.vue')
},
{
name: 'report-edit',
name: 'single-report-edit',
path: '/single/report/edit/:reportEditId',
component: () => import('./pages/single/reportEdit.vue')
},
{
name: 'report-detail',
name: 'single-report-detail',
path: '/single/report/detail/:reportDetailId',
component: () => import('./pages/single/reportDetail.vue')
},