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

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));
}
/**
* 批量设定步骤结束时间为同一时间
*/
@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 更新记录数
*/
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 是否成功
*/
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;
}
@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)
</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>

View File

@@ -76,3 +76,12 @@ export function batchDelayStep (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>
</el-dialog>
<!-- ============ 关不掉的浮窗 ============ -->
<!-- ============ 浮窗可最小化但不能关闭 ============ -->
<transition name="float-slide">
<div v-if="floatVisible" class="overdue-float">
<div v-if="floatVisible && !floatMinimized" class="overdue-float">
<div class="float-head">
<i class="el-icon-warning"></i>
<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 class="float-list">
<div v-for="item in pending" :key="item.business_type + '-' + item.business_id"
@@ -100,6 +102,15 @@
<div class="float-footer">点标题打开处理窗 · "完成"直接标记完成</div>
</div>
</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>
</template>
@@ -112,6 +123,7 @@ export default {
return {
visible: false,
floatVisible: false,
floatMinimized: false,
pending: [],
totalCount: 0,
doneCount: 0,
@@ -137,7 +149,12 @@ export default {
try {
const res = await listMyOverdue()
const list = res.data || []
if (!list.length) return
if (!list.length) {
this.pending = []
this.visible = false
this.floatVisible = false
return
}
this.pending = list
this.totalCount = list.length
this.doneCount = 0
@@ -146,6 +163,19 @@ export default {
this.floatVisible = false
} 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() {
this.form = { action: '', newDeadline: '', reason: '' }
},
@@ -163,6 +193,7 @@ export default {
this.resetForm()
this.visible = true
this.floatVisible = false
this.floatMinimized = false
},
async submit() {
if (!this.current) return
@@ -272,19 +303,11 @@ export default {
if (this.strikeMap[key]) return
try {
await postponeComplete(item.business_type, item.business_id)
// 视觉划掉1 秒后从列表移除
// 视觉划掉1 秒后从列表移除 + 跟服务端对齐
this.$set(this.strikeMap, key, true)
setTimeout(() => {
const idx = this.pending.findIndex(x => this.itemKey(x) === key)
if (idx >= 0) {
this.pending.splice(idx, 1)
this.doneCount++
}
setTimeout(async () => {
this.$delete(this.strikeMap, key)
if (!this.pending.length) {
this.visible = false
this.floatVisible = false
}
await this.refresh()
}, 900)
} catch (e) {
this.$modal.msgError('完成失败,请稍后再试')
@@ -330,6 +353,32 @@ export default {
display: flex; align-items: center; gap: 6px; border-bottom: 1px solid #fde2e2;
}
.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-item {
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-time" style="color:#e6a23c"
@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>
</div>
<vxe-table size="mini" :height="tableHeight" ref="tableRef" border show-overflow :edit-config="editConfig" :data="innerData"
@@ -183,6 +185,27 @@
</div>
</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-form :model="dialogAddForm" ref="formRef" label-width="120px">
<el-form-item label="进度类别" prop="tabNode">
@@ -246,7 +269,7 @@
<script>
import { addFileOperationRecord } from '@/api/oa/fileOperationRecord';
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 { listUser } from "@/api/system/user";
@@ -298,6 +321,9 @@ export default {
delayTo: '',
applyReason: '',
},
// 批量设定时间对话框
dialogBatchSetTimeVisible: false,
dialogBatchSetTimeForm: { newEndTime: '' },
// 批量延期对话框控制
dialogBatchDelayVisible: false,
dialogBatchDelayForm: {
@@ -741,6 +767,38 @@ export default {
};
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 () {
const trackIds = this.selectedRows.map(row => row.trackId).filter(id => id);
if (trackIds.length === 0) {