推送任务进度操作历史,推送项目总览
This commit is contained in:
28
ruoyi-ui/src/api/oa/projectOperationLog.js
Normal file
28
ruoyi-ui/src/api/oa/projectOperationLog.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询操作历史列表(分页)
|
||||
export function listOperationLog(query) {
|
||||
return request({
|
||||
url: '/oa/projectOperationLog/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询操作历史详情
|
||||
export function getOperationLog(logId) {
|
||||
return request({
|
||||
url: '/oa/projectOperationLog/' + logId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 导出操作历史
|
||||
export function exportOperationLog(query) {
|
||||
return request({
|
||||
url: '/oa/projectOperationLog/export',
|
||||
method: 'post',
|
||||
params: query,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
1079
ruoyi-ui/src/views/oa/project/dispatch/index.vue
Normal file
1079
ruoyi-ui/src/views/oa/project/dispatch/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:title="title || '操作历史'"
|
||||
:visible.sync="localVisible"
|
||||
direction="rtl"
|
||||
size="900px"
|
||||
append-to-body
|
||||
@open="loadData"
|
||||
@close="$emit('update:visible', false)"
|
||||
>
|
||||
<div class="log-drawer-body">
|
||||
|
||||
<!-- 顶部摘要 -->
|
||||
<div class="log-header">
|
||||
<span class="log-total">共 <b>{{ total }}</b> 条操作记录</span>
|
||||
<div class="log-filters">
|
||||
<el-select v-model="filterOpType" placeholder="操作类型" clearable size="mini" style="width:110px;margin-right:8px" @change="loadData">
|
||||
<el-option v-for="o in OPERATION_TYPES" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filterOperator" placeholder="操作人" clearable size="mini" style="width:100px" @change="loadData">
|
||||
<el-option v-for="op in operatorOptions" :key="op" :label="op" :value="op" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表 + 时间轴并排 -->
|
||||
<div v-loading="loading" class="log-split">
|
||||
|
||||
<!-- 左:列表 -->
|
||||
<div class="pane pane-list">
|
||||
<div class="pane-title"><i class="el-icon-s-grid" /> 列表</div>
|
||||
<el-table :data="logList" size="mini" border stripe height="calc(100vh - 240px)">
|
||||
<el-table-column label="时间" prop="operateTime" width="130">
|
||||
<template slot-scope="{ row }">{{ parseTime(row.operateTime, '{y}-{m}-{d}\n{h}:{i}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作类型" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<span class="badge-tag" :style="{ color: opColor(row.operationType), borderColor: opColor(row.operationType) }">
|
||||
{{ opLabel(row.operationType) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="描述" prop="operationDesc" show-overflow-tooltip min-width="140" />
|
||||
<el-table-column label="操作人" prop="operator" width="72" align="center" />
|
||||
<el-table-column label="" width="44" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button v-if="row.beforeValue || row.afterValue" type="text" size="mini" @click="openDiff(row)" title="变更详情">
|
||||
<i class="el-icon-view" />
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 右:时间轴 -->
|
||||
<div class="pane pane-timeline">
|
||||
<div class="pane-title"><i class="el-icon-s-order" /> 时间轴</div>
|
||||
<div class="tl-scroll">
|
||||
<div v-if="logList.length === 0 && !loading" class="tl-empty">暂无记录</div>
|
||||
<template v-for="(group, date) in groupedLogs">
|
||||
<div :key="'d-' + date" class="tl-date">{{ date }}</div>
|
||||
<div v-for="row in group" :key="row.logId" class="tl-row">
|
||||
<div class="tl-dot" :style="{ background: opColor(row.operationType) }" />
|
||||
<div class="tl-content">
|
||||
<div class="tl-top">
|
||||
<span class="tl-op-label badge-tag" :style="{ color: opColor(row.operationType), borderColor: opColor(row.operationType) }">{{ opLabel(row.operationType) }}</span>
|
||||
<span class="tl-time">{{ parseTime(row.operateTime, '{h}:{i}:{s}') }}</span>
|
||||
<el-button v-if="row.beforeValue || row.afterValue" type="text" size="mini" class="tl-diff-btn" @click="openDiff(row)">
|
||||
<i class="el-icon-view" />
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="tl-desc">{{ row.operationDesc }}</div>
|
||||
<div class="tl-operator">{{ row.operator }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="log-pagination">
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="pageNum"
|
||||
:limit.sync="pageSize"
|
||||
:page-sizes="[15, 30, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@pagination="loadData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 变更详情弹窗 -->
|
||||
<el-dialog title="变更详情" :visible.sync="diffVisible" width="680px" append-to-body>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<div class="diff-panel">
|
||||
<div class="diff-title diff-before">变更前</div>
|
||||
<pre class="diff-content">{{ diffRow.beforeValue | prettyJson }}</pre>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="diff-panel">
|
||||
<div class="diff-title diff-after">变更后</div>
|
||||
<pre class="diff-content">{{ diffRow.afterValue | prettyJson }}</pre>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div slot="footer"><el-button @click="diffVisible = false">关闭</el-button></div>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listOperationLog } from '@/api/oa/projectOperationLog'
|
||||
|
||||
const OPERATION_TYPES = [
|
||||
{ value: 1, label: '新增', color: '#67C23A' },
|
||||
{ value: 2, label: '修改', color: '#409EFF' },
|
||||
{ value: 3, label: '删除', color: '#F56C6C' },
|
||||
{ value: 4, label: '状态变更', color: '#E6A23C' },
|
||||
{ value: 5, label: '完成', color: '#67C23A' },
|
||||
{ value: 6, label: '申请延期', color: '#E6A23C' },
|
||||
{ value: 7, label: '审批通过', color: '#67C23A' },
|
||||
{ value: 8, label: '审批驳回', color: '#F56C6C' }
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'OperationLogDrawer',
|
||||
filters: {
|
||||
prettyJson (val) {
|
||||
if (!val) return '(无)'
|
||||
try { return JSON.stringify(JSON.parse(val), null, 2) } catch { return val }
|
||||
}
|
||||
},
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
targetType: { type: Number, default: null },
|
||||
targetId: { type: [Number, String], default: null },
|
||||
title: { type: String, default: '' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
logList: [],
|
||||
total: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 15,
|
||||
filterOpType: null,
|
||||
filterOperator: null,
|
||||
operatorOptions: [],
|
||||
OPERATION_TYPES,
|
||||
diffVisible: false,
|
||||
diffRow: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localVisible: {
|
||||
get () { return this.visible },
|
||||
set (v) { this.$emit('update:visible', v) }
|
||||
},
|
||||
groupedLogs () {
|
||||
const groups = {}
|
||||
this.logList.forEach(row => {
|
||||
const date = this.parseTime(row.operateTime, '{y}-{m}-{d}')
|
||||
if (!groups[date]) groups[date] = []
|
||||
groups[date].push(row)
|
||||
})
|
||||
return groups
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
targetId (v) { if (v && this.visible) this.loadData() }
|
||||
},
|
||||
methods: {
|
||||
loadData () {
|
||||
if (!this.targetType || !this.targetId) return
|
||||
this.loading = true
|
||||
listOperationLog({
|
||||
targetType: this.targetType,
|
||||
targetId: this.targetId,
|
||||
operationType: this.filterOpType,
|
||||
operator: this.filterOperator,
|
||||
pageNum: this.pageNum,
|
||||
pageSize: this.pageSize
|
||||
}).then(res => {
|
||||
this.logList = res.rows
|
||||
this.total = res.total
|
||||
const ops = [...new Set(res.rows.map(r => r.operator).filter(Boolean))]
|
||||
if (ops.length > 0) this.operatorOptions = [...new Set([...this.operatorOptions, ...ops])]
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
opLabel (val) { return (OPERATION_TYPES.find(t => t.value === val) || {}).label || val },
|
||||
opColor (val) { return (OPERATION_TYPES.find(t => t.value === val) || {}).color || '#909399' },
|
||||
openDiff (row) { this.diffRow = row; this.diffVisible = true }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-drawer-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0 16px 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部摘要行 */
|
||||
.log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.log-total { font-size: 13px; color: #606266; }
|
||||
.log-total b { color: #303133; font-size: 15px; }
|
||||
.log-filters { display: flex; align-items: center; }
|
||||
|
||||
/* 并排布局 */
|
||||
.log-split {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
.pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pane-list { flex: 1.1; }
|
||||
.pane-timeline { flex: 0.9; border-left: 1px solid #ebeef5; padding-left: 12px; }
|
||||
|
||||
.pane-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #909399;
|
||||
letter-spacing: .5px;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 时间轴滚动容器 */
|
||||
.tl-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 240px);
|
||||
}
|
||||
.tl-empty { text-align: center; color: #c0c4cc; padding: 40px 0; font-size: 13px; }
|
||||
|
||||
.tl-date {
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
font-weight: 600;
|
||||
margin: 14px 0 8px 18px;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
|
||||
.tl-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.tl-row:not(:last-child)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 18px;
|
||||
bottom: -10px;
|
||||
width: 1px;
|
||||
background: #ebeef5;
|
||||
}
|
||||
.tl-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin-top: 5px;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px #dcdfe6;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.tl-content {
|
||||
flex: 1;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 7px 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.tl-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.tl-time { font-size: 11px; color: #c0c4cc; margin-left: auto; }
|
||||
.tl-diff-btn { padding: 0; margin-left: 0; }
|
||||
.tl-desc { font-size: 12px; color: #606266; line-height: 1.4; margin-bottom: 3px; }
|
||||
.tl-operator { font-size: 11px; color: #909399; }
|
||||
|
||||
/* 通用标签 */
|
||||
.badge-tag {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
height: 18px;
|
||||
line-height: 16px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.log-pagination {
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid #ebeef5;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* diff弹窗 */
|
||||
.diff-panel { border: 1px solid #dcdfe6; border-radius: 4px; overflow: hidden; }
|
||||
.diff-title { padding: 8px 12px; font-size: 13px; font-weight: 600; border-bottom: 1px solid #dcdfe6; background: #f5f7fa; color: #606266; }
|
||||
.diff-before { border-left: 3px solid #F56C6C; }
|
||||
.diff-after { border-left: 3px solid #67C23A; }
|
||||
.diff-content { margin: 0; padding: 12px; font-size: 12px; line-height: 1.6; max-height: 380px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; background: #fff; color: #303133; }
|
||||
</style>
|
||||
480
ruoyi-ui/src/views/oa/project/operationLog/index.vue
Normal file
480
ruoyi-ui/src/views/oa/project/operationLog/index.vue
Normal file
@@ -0,0 +1,480 @@
|
||||
<template>
|
||||
<div class="op-log-page">
|
||||
|
||||
<!-- 统计区 -->
|
||||
<el-row :gutter="12" class="stat-row">
|
||||
<el-col :span="4">
|
||||
<div class="stat-card">
|
||||
<div class="stat-num">{{ total }}</div>
|
||||
<div class="stat-label">操作总数</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col v-for="t in TARGET_TYPES" :key="t.value" :span="5">
|
||||
<div class="stat-card clickable" :class="queryParams.targetType === t.value ? 'stat-active' : ''" @click="toggleTargetType(t.value)">
|
||||
<div class="stat-num">{{ typeCounts[t.value] !== undefined ? typeCounts[t.value] : '—' }}</div>
|
||||
<div class="stat-label">{{ t.label }}</div>
|
||||
<div class="stat-bar" :style="{ background: t.color }" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 搜索区 -->
|
||||
<div class="search-block">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
|
||||
<el-form-item label="所属项目" prop="projectId">
|
||||
<project-select v-model="queryParams.projectId" placeholder="全部项目" clearable style="width:190px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作对象" prop="targetType">
|
||||
<el-select v-model="queryParams.targetType" placeholder="全部" clearable style="width:110px">
|
||||
<el-option v-for="t in TARGET_TYPES" :key="t.value" :label="t.label" :value="t.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型" prop="operationType">
|
||||
<el-select v-model="queryParams.operationType" placeholder="全部" clearable style="width:110px">
|
||||
<el-option v-for="o in OPERATION_TYPES" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人" prop="operator">
|
||||
<el-input v-model="queryParams.operator" placeholder="请输入操作人" clearable style="width:120px" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作时间">
|
||||
<el-date-picker
|
||||
v-model="timeRange"
|
||||
type="daterange"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
:default-time="['00:00:00', '23:59:59']"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width:260px"
|
||||
/>
|
||||
</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>
|
||||
<div class="toolbar-right">
|
||||
<el-button-group size="mini">
|
||||
<el-button :type="viewMode === 'list' ? 'primary' : ''" icon="el-icon-s-grid" @click="viewMode = 'list'" title="列表视图" />
|
||||
<el-button :type="viewMode === 'timeline' ? 'primary' : ''" icon="el-icon-s-order" @click="viewMode = 'timeline'" title="时间轴视图" />
|
||||
</el-button-group>
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" style="margin-left:8px">导出</el-button>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" style="display:inline-flex;margin-left:4px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表视图 -->
|
||||
<template v-if="viewMode === 'list'">
|
||||
<el-table v-loading="loading" :data="logList" size="small" border stripe style="width:100%">
|
||||
<el-table-column label="操作时间" prop="operateTime" width="150" align="center">
|
||||
<template slot-scope="{ row }">{{ parseTime(row.operateTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属项目" prop="projectName" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="项目编号" prop="projectNum" width="120" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="操作对象" width="90" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<span class="tag-badge" :style="{ borderColor: targetTypeColor(row.targetType), color: targetTypeColor(row.targetType) }">
|
||||
{{ targetTypeLabel(row.targetType) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="对象名称" prop="targetName" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作类型" width="90" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<span class="tag-badge" :style="{ borderColor: opTypeColor(row.operationType), color: opTypeColor(row.operationType) }">
|
||||
{{ operationTypeLabel(row.operationType) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作描述" prop="operationDesc" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作人" prop="operator" width="90" align="center" />
|
||||
<el-table-column label="详情" width="70" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button v-if="row.beforeValue || row.afterValue" type="text" size="mini" @click="handleViewDetail(row)">查看</el-button>
|
||||
<span v-else style="color:#c0c4cc;font-size:12px">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<!-- 时间轴视图 -->
|
||||
<template v-else>
|
||||
<div v-loading="loading" class="timeline-wrap">
|
||||
<div v-if="!loading && logList.length === 0" class="empty-tip">暂无操作记录</div>
|
||||
|
||||
<template v-for="(group, date) in groupedLogs">
|
||||
<div :key="'hd-' + date" class="tl-date-header">{{ date }}</div>
|
||||
<div v-for="row in group" :key="row.logId" class="tl-item">
|
||||
<div class="tl-dot" :style="{ background: targetTypeColor(row.targetType) }" />
|
||||
<div class="tl-line" />
|
||||
<div class="tl-card">
|
||||
<div class="tl-card-top">
|
||||
<span class="tl-operator">{{ row.operator }}</span>
|
||||
<span class="tag-badge sm" :style="{ borderColor: targetTypeColor(row.targetType), color: targetTypeColor(row.targetType) }">{{ targetTypeLabel(row.targetType) }}</span>
|
||||
<span class="tag-badge sm" :style="{ borderColor: opTypeColor(row.operationType), color: opTypeColor(row.operationType) }">{{ operationTypeLabel(row.operationType) }}</span>
|
||||
<span class="tl-time">{{ parseTime(row.operateTime, '{h}:{i}:{s}') }}</span>
|
||||
<el-button v-if="row.beforeValue || row.afterValue" type="text" size="mini" class="tl-detail-btn" @click="handleViewDetail(row)">变更详情</el-button>
|
||||
</div>
|
||||
<div class="tl-desc">
|
||||
<span v-if="row.targetName" class="tl-target">{{ row.targetName }}</span>
|
||||
{{ row.operationDesc }}
|
||||
</div>
|
||||
<div v-if="row.projectName" class="tl-project">{{ row.projectName }}<span v-if="row.projectNum">({{ row.projectNum }})</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 变更详情弹窗 -->
|
||||
<el-dialog title="变更详情" :visible.sync="detailVisible" width="720px" append-to-body>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<div class="diff-panel">
|
||||
<div class="diff-title diff-before">变更前</div>
|
||||
<pre class="diff-content">{{ currentDetail.beforeValue | prettyJson }}</pre>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="diff-panel">
|
||||
<div class="diff-title diff-after">变更后</div>
|
||||
<pre class="diff-content">{{ currentDetail.afterValue | prettyJson }}</pre>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div slot="footer"><el-button @click="detailVisible = false">关闭</el-button></div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listOperationLog, exportOperationLog } from '@/api/oa/projectOperationLog'
|
||||
import ProjectSelect from '@/components/fad-service/ProjectSelect/index.vue'
|
||||
|
||||
const TARGET_TYPES = [
|
||||
{ value: 1, label: '项目进度', color: '#409EFF' },
|
||||
{ value: 2, label: '进度步骤', color: '#67C23A' },
|
||||
{ value: 3, label: '任务', color: '#E6A23C' },
|
||||
{ value: 4, label: '延期申请', color: '#F56C6C' }
|
||||
]
|
||||
|
||||
const OPERATION_TYPES = [
|
||||
{ value: 1, label: '新增', color: '#67C23A' },
|
||||
{ value: 2, label: '修改', color: '#409EFF' },
|
||||
{ value: 3, label: '删除', color: '#F56C6C' },
|
||||
{ value: 4, label: '状态变更', color: '#E6A23C' },
|
||||
{ value: 5, label: '完成', color: '#67C23A' },
|
||||
{ value: 6, label: '申请延期', color: '#E6A23C' },
|
||||
{ value: 7, label: '审批通过', color: '#67C23A' },
|
||||
{ value: 8, label: '审批驳回', color: '#F56C6C' }
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'ProjectOperationLog',
|
||||
components: { ProjectSelect },
|
||||
filters: {
|
||||
prettyJson (val) {
|
||||
if (!val) return '(无)'
|
||||
try { return JSON.stringify(JSON.parse(val), null, 2) } catch { return val }
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
logList: [],
|
||||
typeCounts: {},
|
||||
timeRange: [],
|
||||
viewMode: 'list',
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
projectId: null,
|
||||
targetType: null,
|
||||
operationType: null,
|
||||
operator: null,
|
||||
operateTimeStart: null,
|
||||
operateTimeEnd: null
|
||||
},
|
||||
TARGET_TYPES,
|
||||
OPERATION_TYPES,
|
||||
detailVisible: false,
|
||||
currentDetail: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
groupedLogs () {
|
||||
const groups = {}
|
||||
this.logList.forEach(row => {
|
||||
const date = this.parseTime(row.operateTime, '{y}-{m}-{d}')
|
||||
if (!groups[date]) groups[date] = []
|
||||
groups[date].push(row)
|
||||
})
|
||||
return groups
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.$route && this.$route.query && this.$route.query.projectId) {
|
||||
this.queryParams.projectId = Number(this.$route.query.projectId)
|
||||
}
|
||||
this.getList()
|
||||
this.loadTypeCounts()
|
||||
},
|
||||
methods: {
|
||||
buildParams () {
|
||||
const p = { ...this.queryParams }
|
||||
if (this.timeRange && this.timeRange.length === 2) {
|
||||
p.operateTimeStart = this.timeRange[0]
|
||||
p.operateTimeEnd = this.timeRange[1]
|
||||
} else {
|
||||
p.operateTimeStart = null
|
||||
p.operateTimeEnd = null
|
||||
}
|
||||
return p
|
||||
},
|
||||
getList () {
|
||||
this.loading = true
|
||||
listOperationLog(this.buildParams()).then(res => {
|
||||
this.logList = res.rows
|
||||
this.total = res.total
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
loadTypeCounts () {
|
||||
TARGET_TYPES.forEach(t => {
|
||||
const p = this.buildParams()
|
||||
p.targetType = t.value
|
||||
p.pageSize = 1
|
||||
p.pageNum = 1
|
||||
listOperationLog(p).then(res => {
|
||||
this.$set(this.typeCounts, t.value, res.total)
|
||||
})
|
||||
})
|
||||
},
|
||||
toggleTargetType (val) {
|
||||
this.queryParams.targetType = this.queryParams.targetType === val ? null : val
|
||||
this.handleQuery()
|
||||
},
|
||||
handleQuery () {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
this.loadTypeCounts()
|
||||
},
|
||||
resetQuery () {
|
||||
this.$refs.queryForm.resetFields()
|
||||
this.timeRange = []
|
||||
this.queryParams.operationType = null
|
||||
this.handleQuery()
|
||||
},
|
||||
handleExport () { exportOperationLog(this.buildParams()) },
|
||||
handleViewDetail (row) { this.currentDetail = row; this.detailVisible = true },
|
||||
targetTypeLabel (val) { return (TARGET_TYPES.find(t => t.value === val) || {}).label || val },
|
||||
targetTypeColor (val) { return (TARGET_TYPES.find(t => t.value === val) || {}).color || '#909399' },
|
||||
operationTypeLabel (val) { return (OPERATION_TYPES.find(t => t.value === val) || {}).label || val },
|
||||
opTypeColor (val) { return (OPERATION_TYPES.find(t => t.value === val) || {}).color || '#909399' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.op-log-page {
|
||||
padding: 16px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* ── 统计卡片 ── */
|
||||
.stat-row { margin-bottom: 12px; }
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 14px 16px 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.stat-card.clickable { cursor: pointer; transition: border-color .2s; }
|
||||
.stat-card.clickable:hover { border-color: #409eff; }
|
||||
.stat-card.stat-active { border-color: #409eff; background: #ecf5ff; }
|
||||
.stat-bar {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
width: 3px;
|
||||
}
|
||||
.stat-num {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* ── 搜索区 ── */
|
||||
.search-block {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 14px 16px 6px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
/* ── 通用标签徽章 ── */
|
||||
.tag-badge {
|
||||
display: inline-block;
|
||||
padding: 0 7px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tag-badge.sm {
|
||||
padding: 0 5px;
|
||||
height: 18px;
|
||||
line-height: 16px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* ── 列表视图 ── */
|
||||
.el-table { background: #fff; border-radius: 4px; }
|
||||
|
||||
/* ── 时间轴视图 ── */
|
||||
.timeline-wrap {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 16px 20px;
|
||||
min-height: 200px;
|
||||
}
|
||||
.empty-tip { text-align: center; padding: 60px 0; color: #c0c4cc; font-size: 14px; }
|
||||
|
||||
.tl-date-header {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: 600;
|
||||
margin: 18px 0 10px 30px;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
.tl-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
/* 竖线 */
|
||||
.tl-line {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 20px;
|
||||
bottom: -14px;
|
||||
width: 1px;
|
||||
background: #e4e7ed;
|
||||
}
|
||||
.tl-item:last-child .tl-line { display: none; }
|
||||
|
||||
.tl-dot {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
margin-top: 13px;
|
||||
margin-right: 16px;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px #dcdfe6;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tl-card {
|
||||
flex: 1;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.tl-card:hover { border-color: #c6e2ff; background: #fff; }
|
||||
|
||||
.tl-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.tl-operator {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.tl-time {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
margin-left: auto;
|
||||
}
|
||||
.tl-detail-btn { padding: 0; margin-left: 0; font-size: 12px; }
|
||||
|
||||
.tl-desc {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.tl-target {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.tl-project { font-size: 12px; color: #909399; }
|
||||
|
||||
/* ── diff弹窗 ── */
|
||||
.diff-panel {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.diff-title {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
background: #f5f7fa;
|
||||
color: #606266;
|
||||
}
|
||||
.diff-before { border-left: 3px solid #F56C6C; }
|
||||
.diff-after { border-left: 3px solid #67C23A; }
|
||||
.diff-content {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
background: #fff;
|
||||
color: #303133;
|
||||
}
|
||||
</style>
|
||||
@@ -116,12 +116,14 @@
|
||||
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleDetail(scope.row)">进度详情
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-time" @click="handlePostpone(scope.row)">延期记录
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-s-order" @click="handleOpLog(scope.row)">操作历史
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-check"
|
||||
v-if="scope.row.schedulePercentage === 100 && scope.row.status !== 2"
|
||||
@click="handleComplete(scope.row)">完成任务
|
||||
@@ -150,6 +152,14 @@
|
||||
<el-drawer title="延期记录" :visible.sync="postponeDrawer" direction="btt" size="90%">
|
||||
<postpone :scheduleId="scheduleDetail.scheduleId" />
|
||||
</el-drawer>
|
||||
|
||||
<!-- 操作历史抽屉 -->
|
||||
<operation-log-drawer
|
||||
:visible.sync="opLogDrawer"
|
||||
:target-type="1"
|
||||
:target-id="opLogTargetId"
|
||||
:title="opLogTitle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -161,6 +171,7 @@ import UserSelect from "@/components/UserSelect/index.vue";
|
||||
import FormDialog from "./components/FormDialog.vue";
|
||||
import Postpone from "./components/postpone.vue";
|
||||
import ProjectScheduleStep from "./components/step.vue";
|
||||
import OperationLogDrawer from "@/views/oa/project/operationLog/OperationLogDrawer.vue";
|
||||
|
||||
export default {
|
||||
name: "Schedule",
|
||||
@@ -170,7 +181,8 @@ export default {
|
||||
ProjectScheduleStep,
|
||||
FormDialog,
|
||||
ProjectSelect,
|
||||
Postpone
|
||||
Postpone,
|
||||
OperationLogDrawer
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -197,7 +209,11 @@ export default {
|
||||
userList: [],
|
||||
postponeDrawer: false,
|
||||
/** 综合看板等深链:打开抽屉后传给 step,用于选中进度类别/一级节点 */
|
||||
scheduleStepFocusHint: null
|
||||
scheduleStepFocusHint: null,
|
||||
// 操作历史抽屉
|
||||
opLogDrawer: false,
|
||||
opLogTargetId: null,
|
||||
opLogTitle: ''
|
||||
};
|
||||
|
||||
},
|
||||
@@ -381,6 +397,11 @@ export default {
|
||||
this.postponeDrawer = true
|
||||
this.scheduleDetail = row
|
||||
},
|
||||
handleOpLog (row) {
|
||||
this.opLogTargetId = row.scheduleId
|
||||
this.opLogTitle = '操作历史 — ' + (row.projectName || '项目进度')
|
||||
this.opLogDrawer = true
|
||||
},
|
||||
getScheduleDetail (row) {
|
||||
this.scheduleDetail = row
|
||||
this.detailDrawer = true
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
<el-button size="mini" type="text" icon="el-icon-copy-document"
|
||||
@click.stop="handleUpdate(item)">修改</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDelete(item)">删除</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-s-order" @click.stop="handleOpLog(item)">操作历史</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -211,6 +212,14 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 操作历史抽屉 -->
|
||||
<operation-log-drawer
|
||||
:visible.sync="opLogDrawer"
|
||||
:target-type="3"
|
||||
:target-id="opLogTargetId"
|
||||
:title="opLogTitle"
|
||||
/>
|
||||
|
||||
<!--查看弹出层-->
|
||||
<el-dialog title="任务详情" :visible.sync="openLook" width="70%" center>
|
||||
<div class="task-detail-container">
|
||||
@@ -365,13 +374,15 @@ import { addTask, delTask, getTask, listTask, updateTask } from "@/api/oa/task";
|
||||
import { deptTreeSelect, selectUser } from "@/api/system/user";
|
||||
import UserSelect from "@/components/UserSelect";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
import OperationLogDrawer from "@/views/oa/project/operationLog/OperationLogDrawer.vue";
|
||||
|
||||
|
||||
export default {
|
||||
name: "Task",
|
||||
components: {
|
||||
UserSelect,
|
||||
ProjectSelect
|
||||
ProjectSelect,
|
||||
OperationLogDrawer
|
||||
},
|
||||
dicts: ['sys_project_type', 'sys_project_status', 'sys_work_type', 'sys_sort_grade'],
|
||||
data () {
|
||||
@@ -447,6 +458,10 @@ export default {
|
||||
}
|
||||
},
|
||||
trackLoading: false,
|
||||
// 操作历史抽屉
|
||||
opLogDrawer: false,
|
||||
opLogTargetId: null,
|
||||
opLogTitle: ''
|
||||
};
|
||||
|
||||
},
|
||||
@@ -722,6 +737,12 @@ export default {
|
||||
this.resetForm("form");
|
||||
this.fileList = [];
|
||||
},
|
||||
handleOpLog (item) {
|
||||
this.opLogTargetId = item.taskId
|
||||
this.opLogTitle = '操作历史 — ' + (item.taskTitle || '任务')
|
||||
this.opLogDrawer = true
|
||||
},
|
||||
|
||||
/** 删除按钮操作 */
|
||||
handleDelete (row) {
|
||||
const taskIds = row.taskId || this.ids;
|
||||
|
||||
Reference in New Issue
Block a user