添加了浮窗最小话以及一键写入日期

This commit is contained in:
2026-06-18 12:41:45 +08:00
parent 88c374952a
commit acaf13ff95
8 changed files with 177 additions and 15 deletions

View File

@@ -127,6 +127,15 @@ public class OaProjectScheduleStepController extends BaseController {
return toAjax(iOaProjectScheduleStepService.batchDelay(batchDelayBo)); return toAjax(iOaProjectScheduleStepService.batchDelay(batchDelayBo));
} }
/**
* 批量设定步骤结束时间为同一时间
*/
@RepeatSubmit()
@PutMapping("/batch-set-end")
public R<Void> batchSetEndTime(@RequestBody com.ruoyi.oa.domain.bo.BatchSetEndTimeBo bo) {
return toAjax(iOaProjectScheduleStepService.batchSetEndTime(bo));
}
/** /**
* 删除项目进度步骤跟踪 * 删除项目进度步骤跟踪
* *

View File

@@ -63,4 +63,7 @@ public interface OaProjectScheduleStepMapper extends BaseMapperPlus<OaProjectSch
* @return 更新记录数 * @return 更新记录数
*/ */
int batchDelayPlanEnd(@Param("trackIds") List<Long> trackIds, @Param("delayMinutes") Long delayMinutes); int batchDelayPlanEnd(@Param("trackIds") List<Long> trackIds, @Param("delayMinutes") Long delayMinutes);
/** 批量设定 plan_end 为同一个具体时间 */
int batchSetPlanEnd(@Param("trackIds") List<Long> trackIds, @Param("newEndTime") java.util.Date newEndTime);
} }

View File

@@ -89,4 +89,7 @@ public interface IOaProjectScheduleStepService{
* @return 是否成功 * @return 是否成功
*/ */
Boolean batchDelay(com.ruoyi.oa.domain.bo.BatchDelayBo bo); Boolean batchDelay(com.ruoyi.oa.domain.bo.BatchDelayBo bo);
/** 批量设定步骤结束时间为同一时间 */
Boolean batchSetEndTime(com.ruoyi.oa.domain.bo.BatchSetEndTimeBo bo);
} }

View File

@@ -1071,5 +1071,24 @@ public class OaProjectScheduleStepServiceImpl implements IOaProjectScheduleStepS
return updated > 0; return updated > 0;
} }
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean batchSetEndTime(com.ruoyi.oa.domain.bo.BatchSetEndTimeBo bo) {
if (bo.getTrackIds() == null || bo.getTrackIds().isEmpty()) return false;
if (bo.getNewEndTime() == null) return false;
List<OaProjectScheduleStep> steps = baseMapper.selectList(
new LambdaQueryWrapper<OaProjectScheduleStep>()
.in(OaProjectScheduleStep::getTrackId, bo.getTrackIds())
);
String completedTrack = steps.stream()
.filter(s -> s.getStatus() != null && s.getStatus() == 2)
.map(OaProjectScheduleStep::getSecondLevelNode)
.collect(Collectors.joining(","));
if (!completedTrack.isEmpty()) {
throw new RuntimeException("以下步骤已完成,不允许修改:" + completedTrack);
}
int updated = baseMapper.batchSetPlanEnd(bo.getTrackIds(), bo.getNewEndTime());
return updated > 0;
}
} }

View File

@@ -321,4 +321,16 @@
AND status IN (0, 1) AND status IN (0, 1)
</update> </update>
<update id="batchSetPlanEnd">
UPDATE oa_project_schedule_step
SET plan_end = #{newEndTime}
WHERE track_id IN
<foreach collection="trackIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND del_flag = '0'
AND use_flag = '1'
AND status IN (0, 1)
</update>
</mapper> </mapper>

View File

@@ -76,3 +76,12 @@ export function batchDelayStep (data) {
data: data data: data
}) })
} }
// 批量设定步骤结束时间
export function batchSetEndTimeStep (data) {
return request({
url: '/oa/projectScheduleStep/batch-set-end',
method: 'put',
data: data
})
}

View File

@@ -73,12 +73,14 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- ============ 关不掉的浮窗 ============ --> <!-- ============ 浮窗可最小化但不能关闭 ============ -->
<transition name="float-slide"> <transition name="float-slide">
<div v-if="floatVisible" class="overdue-float"> <div v-if="floatVisible && !floatMinimized" class="overdue-float">
<div class="float-head"> <div class="float-head">
<i class="el-icon-warning"></i> <i class="el-icon-warning"></i>
<span>我的超期 ({{ pending.length }})</span> <span>我的超期 ({{ pending.length }})</span>
<i class="el-icon-refresh-right float-action" title="刷新" @click.stop="refresh"></i>
<i class="el-icon-minus float-action" title="最小化" @click.stop="floatMinimized = true"></i>
</div> </div>
<div class="float-list"> <div class="float-list">
<div v-for="item in pending" :key="item.business_type + '-' + item.business_id" <div v-for="item in pending" :key="item.business_type + '-' + item.business_id"
@@ -100,6 +102,15 @@
<div class="float-footer">点标题打开处理窗 · "完成"直接标记完成</div> <div class="float-footer">点标题打开处理窗 · "完成"直接标记完成</div>
</div> </div>
</transition> </transition>
<!-- 最小化后的气泡 -->
<transition name="float-slide">
<div v-if="floatVisible && floatMinimized" class="overdue-pill" @click="floatMinimized = false">
<i class="el-icon-warning"></i>
<span class="num">{{ pending.length }}</span>
<span class="lbl">超期</span>
</div>
</transition>
</div> </div>
</template> </template>
@@ -112,6 +123,7 @@ export default {
return { return {
visible: false, visible: false,
floatVisible: false, floatVisible: false,
floatMinimized: false,
pending: [], pending: [],
totalCount: 0, totalCount: 0,
doneCount: 0, doneCount: 0,
@@ -137,7 +149,12 @@ export default {
try { try {
const res = await listMyOverdue() const res = await listMyOverdue()
const list = res.data || [] const list = res.data || []
if (!list.length) return if (!list.length) {
this.pending = []
this.visible = false
this.floatVisible = false
return
}
this.pending = list this.pending = list
this.totalCount = list.length this.totalCount = list.length
this.doneCount = 0 this.doneCount = 0
@@ -146,6 +163,19 @@ export default {
this.floatVisible = false this.floatVisible = false
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}, },
async refresh() {
// 手动 / 自动刷新:与服务端对齐,剔除已被别处操作完成的事项
try {
const res = await listMyOverdue()
const list = res.data || []
this.pending = list
if (!list.length) {
this.visible = false
this.floatVisible = false
this.floatMinimized = false
}
} catch (e) { /* ignore */ }
},
resetForm() { resetForm() {
this.form = { action: '', newDeadline: '', reason: '' } this.form = { action: '', newDeadline: '', reason: '' }
}, },
@@ -163,6 +193,7 @@ export default {
this.resetForm() this.resetForm()
this.visible = true this.visible = true
this.floatVisible = false this.floatVisible = false
this.floatMinimized = false
}, },
async submit() { async submit() {
if (!this.current) return if (!this.current) return
@@ -272,19 +303,11 @@ export default {
if (this.strikeMap[key]) return if (this.strikeMap[key]) return
try { try {
await postponeComplete(item.business_type, item.business_id) await postponeComplete(item.business_type, item.business_id)
// 视觉划掉1 秒后从列表移除 // 视觉划掉1 秒后从列表移除 + 跟服务端对齐
this.$set(this.strikeMap, key, true) this.$set(this.strikeMap, key, true)
setTimeout(() => { setTimeout(async () => {
const idx = this.pending.findIndex(x => this.itemKey(x) === key)
if (idx >= 0) {
this.pending.splice(idx, 1)
this.doneCount++
}
this.$delete(this.strikeMap, key) this.$delete(this.strikeMap, key)
if (!this.pending.length) { await this.refresh()
this.visible = false
this.floatVisible = false
}
}, 900) }, 900)
} catch (e) { } catch (e) {
this.$modal.msgError('完成失败,请稍后再试') this.$modal.msgError('完成失败,请稍后再试')
@@ -330,6 +353,32 @@ export default {
display: flex; align-items: center; gap: 6px; border-bottom: 1px solid #fde2e2; display: flex; align-items: center; gap: 6px; border-bottom: 1px solid #fde2e2;
} }
.overdue-float .float-head i { font-size: 18px; } .overdue-float .float-head i { font-size: 18px; }
.overdue-float .float-head .float-action {
font-size: 16px; cursor: pointer; color: #f56c6c;
padding: 4px; border-radius: 4px; transition: background .15s;
}
.overdue-float .float-head .float-action:first-of-type { margin-left: auto; }
.overdue-float .float-head .float-action:hover { background: rgba(245,108,108,0.15); }
/* 最小化后的气泡 */
.overdue-pill {
position: fixed; right: 16px; bottom: 16px; z-index: 2000;
background: #f56c6c; color: #fff;
border-radius: 20px; padding: 8px 14px;
display: flex; align-items: center; gap: 6px;
cursor: pointer; font-size: 13px; font-weight: 600;
box-shadow: 0 4px 16px rgba(245,108,108,.4);
animation: overduePillPulse 2.4s ease-in-out infinite;
transition: transform .15s;
}
.overdue-pill:hover { transform: scale(1.05); }
.overdue-pill i { font-size: 16px; }
.overdue-pill .num { font-size: 16px; font-weight: 800; }
.overdue-pill .lbl { opacity: 0.9; }
@keyframes overduePillPulse {
0%, 100% { box-shadow: 0 4px 16px rgba(245,108,108,.4); }
50% { box-shadow: 0 6px 24px rgba(245,108,108,.7); }
}
.overdue-float .float-list { padding: 4px 0; overflow-y: auto; flex: 1; } .overdue-float .float-list { padding: 4px 0; overflow-y: auto; flex: 1; }
.overdue-float .float-item { .overdue-float .float-item {
padding: 8px 14px; cursor: pointer; border-left: 3px solid transparent; padding: 8px 14px; cursor: pointer; border-left: 3px solid transparent;

View File

@@ -4,6 +4,8 @@
<el-button type="text" icon="el-icon-plus" @click="addInnerData">新增</el-button> <el-button type="text" icon="el-icon-plus" @click="addInnerData">新增</el-button>
<el-button type="text" icon="el-icon-time" style="color:#e6a23c" <el-button type="text" icon="el-icon-time" style="color:#e6a23c"
@click="handleBatchDelay" :disabled="selectedRows.length === 0">批量延期</el-button> @click="handleBatchDelay" :disabled="selectedRows.length === 0">批量延期</el-button>
<el-button type="text" icon="el-icon-date" style="color:#409EFF"
@click="handleBatchSetTime" :disabled="selectedRows.length === 0">批量设定时间</el-button>
<slot name="extra-buttons"></slot> <slot name="extra-buttons"></slot>
</div> </div>
<vxe-table size="mini" :height="tableHeight" ref="tableRef" border show-overflow :edit-config="editConfig" :data="innerData" <vxe-table size="mini" :height="tableHeight" ref="tableRef" border show-overflow :edit-config="editConfig" :data="innerData"
@@ -183,6 +185,27 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 批量设定时间对话框 -->
<el-dialog :visible.sync="dialogBatchSetTimeVisible" title="批量设定时间" append-to-body width="420px">
<el-alert type="info" :closable="false" show-icon
title="把所选步骤的结束时间统一设为下面这个时间。已完成的步骤会被跳过。"
style="margin-bottom: 14px;" />
<el-form :model="dialogBatchSetTimeForm" label-width="100px">
<el-form-item label="新结束时间" prop="newEndTime">
<el-date-picker v-model="dialogBatchSetTimeForm.newEndTime"
type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择新的结束时间" style="width: 100%;" />
</el-form-item>
<el-form-item label="影响行数">
<span style="color:#606266">{{ selectedRows.length }} </span>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogBatchSetTimeVisible = false">取消</el-button>
<el-button type="primary" :loading="buttonLoading" @click="submitBatchSetTime">确认设定</el-button>
</div>
</el-dialog>
<el-dialog :visible.sync="addDialogVisible" title="新增进度" append-to-body> <el-dialog :visible.sync="addDialogVisible" title="新增进度" append-to-body>
<el-form :model="dialogAddForm" ref="formRef" label-width="120px"> <el-form :model="dialogAddForm" ref="formRef" label-width="120px">
<el-form-item label="进度类别" prop="tabNode"> <el-form-item label="进度类别" prop="tabNode">
@@ -246,7 +269,7 @@
<script> <script>
import { addFileOperationRecord } from '@/api/oa/fileOperationRecord'; import { addFileOperationRecord } from '@/api/oa/fileOperationRecord';
import { applyProjectScheduleDelay } from "@/api/oa/projectScheduleDelay"; import { applyProjectScheduleDelay } from "@/api/oa/projectScheduleDelay";
import { updateProjectScheduleStep, batchDelayStep } from "@/api/oa/projectScheduleStep"; import { updateProjectScheduleStep, batchDelayStep, batchSetEndTimeStep } from "@/api/oa/projectScheduleStep";
import { listSupplier } from "@/api/oa/supplier"; import { listSupplier } from "@/api/oa/supplier";
import { listUser } from "@/api/system/user"; import { listUser } from "@/api/system/user";
@@ -298,6 +321,9 @@ export default {
delayTo: '', delayTo: '',
applyReason: '', applyReason: '',
}, },
// 批量设定时间对话框
dialogBatchSetTimeVisible: false,
dialogBatchSetTimeForm: { newEndTime: '' },
// 批量延期对话框控制 // 批量延期对话框控制
dialogBatchDelayVisible: false, dialogBatchDelayVisible: false,
dialogBatchDelayForm: { dialogBatchDelayForm: {
@@ -741,6 +767,38 @@ export default {
}; };
this.dialogBatchDelayVisible = true; this.dialogBatchDelayVisible = true;
}, },
handleBatchSetTime () {
if (this.selectedRows.length === 0) {
this.$modal.msgWarning("请先选择要设定时间的步骤");
return;
}
this.dialogBatchSetTimeForm = { newEndTime: '' };
this.dialogBatchSetTimeVisible = true;
},
submitBatchSetTime () {
if (!this.dialogBatchSetTimeForm.newEndTime) {
this.$modal.msgWarning("请选择新的结束时间");
return;
}
const trackIds = this.selectedRows.map(row => row.trackId).filter(id => id);
if (trackIds.length === 0) {
this.$modal.msgWarning("请选择有效的步骤");
return;
}
this.buttonLoading = true;
batchSetEndTimeStep({
trackIds,
newEndTime: this.dialogBatchSetTimeForm.newEndTime
}).then(() => {
this.$modal.msgSuccess("批量设定时间成功");
this.dialogBatchSetTimeVisible = false;
this.$emit("refresh", this.innerData);
}).catch(() => {
this.$modal.msgError("批量设定时间失败");
}).finally(() => {
this.buttonLoading = false;
});
},
submitBatchDelay () { submitBatchDelay () {
const trackIds = this.selectedRows.map(row => row.trackId).filter(id => id); const trackIds = this.selectedRows.map(row => row.trackId).filter(id => id);
if (trackIds.length === 0) { if (trackIds.length === 0) {