推送任务进度操作历史,推送项目总览

This commit is contained in:
2026-05-10 16:38:39 +08:00
parent 9ce5cb8f2e
commit 47baa575df
195 changed files with 2767 additions and 5086 deletions

View 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'
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View 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>

View File

@@ -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

View File

@@ -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;