优化日程提醒模块
This commit is contained in:
@@ -39,6 +39,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待办分类 tabs -->
|
||||
<el-tabs
|
||||
v-model="category"
|
||||
class="category-tabs"
|
||||
@tab-change="handleCategoryChange"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="tab in categoryTabs"
|
||||
:key="tab.value"
|
||||
:name="tab.value"
|
||||
>
|
||||
<template #label>
|
||||
<span class="tab-label">
|
||||
{{ tab.label }}
|
||||
<span class="tab-count">{{ categoryCounts[tab.value] || 0 }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 日历视图 -->
|
||||
<div v-if="viewMode === 'calendar'" class="calendar-layout">
|
||||
<div class="calendar-card">
|
||||
@@ -51,15 +71,6 @@
|
||||
<el-button :icon="ArrowRight" circle @click="changeMonth(1)" />
|
||||
<el-button @click="goToday">今天</el-button>
|
||||
</div>
|
||||
<el-radio-group
|
||||
v-model="calendarStatus"
|
||||
size="small"
|
||||
@change="loadCalendarData"
|
||||
>
|
||||
<el-radio-button value="">全部</el-radio-button>
|
||||
<el-radio-button value="0">待办</el-radio-button>
|
||||
<el-radio-button value="1">已完成</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="calendar-week-header">
|
||||
<div v-for="w in weekNames" :key="w" class="week-name">{{ w }}</div>
|
||||
@@ -118,15 +129,28 @@
|
||||
<div class="day-panel-header">
|
||||
<div>
|
||||
<div class="day-title">{{ selectedDateLabel }}</div>
|
||||
<div class="day-sub">{{ selectedItems.length }} 项日程</div>
|
||||
<div class="day-sub">
|
||||
{{ selectedItems.length }} 项日程<template v-if="pendingCount > 0"
|
||||
>,{{ pendingCount }} 项未完成</template
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<el-button
|
||||
v-if="pendingCount > 0"
|
||||
size="small"
|
||||
:icon="Promotion"
|
||||
@click="handleCarryPending"
|
||||
>延期未完成</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="Plus"
|
||||
@click="openCreate(selectedDate)"
|
||||
>新增</el-button
|
||||
>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="Plus"
|
||||
@click="openCreate(selectedDate)"
|
||||
>新增</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="day-list">
|
||||
<div
|
||||
@@ -150,6 +174,22 @@
|
||||
>
|
||||
{{ priorityLabel(item.priority) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="item.carry_over === 1"
|
||||
size="small"
|
||||
effect="light"
|
||||
type="info"
|
||||
>
|
||||
已延期至 {{ shortDate(item.carry_to_date) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else-if="item.source_id > 0"
|
||||
size="small"
|
||||
effect="light"
|
||||
type="warning"
|
||||
>
|
||||
延期第 {{ item.carry_count + 1 }} 天
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="day-item-time">{{ timeText(item) }}</div>
|
||||
</div>
|
||||
@@ -165,6 +205,11 @@
|
||||
/>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
command="postpone"
|
||||
:disabled="item.status === 1"
|
||||
>延期到下一天</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item command="edit">编辑</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided
|
||||
>删除</el-dropdown-item
|
||||
@@ -192,15 +237,6 @@
|
||||
style="width: 200px"
|
||||
@keyup.enter="loadListData"
|
||||
/>
|
||||
<el-select
|
||||
v-model="listFilters.status"
|
||||
clearable
|
||||
placeholder="全部状态"
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="待办" value="0" />
|
||||
<el-option label="已完成" value="1" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="listFilters.range"
|
||||
type="daterange"
|
||||
@@ -254,11 +290,18 @@
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="toggleFinish(row)">{{
|
||||
row.status === 1 ? "取消完成" : "完成"
|
||||
}}</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="row.status === 1"
|
||||
@click="openPostpone(row)"
|
||||
>延期</el-button
|
||||
>
|
||||
<el-button link type="primary" @click="openEdit(row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
@@ -296,7 +339,16 @@
|
||||
v-model="detailVisible"
|
||||
:schedule="detailItem"
|
||||
@edit="handleDetailEdit"
|
||||
@toggle="toggleFromDetail"
|
||||
@finish="handleDetailFinish"
|
||||
@revert="handleDetailRevert"
|
||||
@postpone="handleDetailPostpone"
|
||||
/>
|
||||
|
||||
<!-- 延期对话框 -->
|
||||
<SchedulePostpone
|
||||
v-model="postponeVisible"
|
||||
:schedule="postponeItem"
|
||||
@saved="reloadAll"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -309,12 +361,15 @@ import {
|
||||
ArrowRight,
|
||||
MoreFilled,
|
||||
Plus,
|
||||
Promotion,
|
||||
Refresh,
|
||||
Search
|
||||
} from "@element-plus/icons-vue";
|
||||
import ScheduleEdit from "./components/edit.vue";
|
||||
import ScheduleDetail from "./components/detail.vue";
|
||||
import SchedulePostpone from "./components/postponeDialog.vue";
|
||||
import {
|
||||
carryPendingSchedules,
|
||||
deleteSchedule,
|
||||
finishSchedule,
|
||||
getScheduleList,
|
||||
@@ -324,6 +379,19 @@ import {
|
||||
const viewMode = ref("calendar");
|
||||
const stats = ref({});
|
||||
|
||||
// ---------- 待办分类 tabs ----------
|
||||
// all-全部 today-今日 pending-未完成 done-已完成
|
||||
// 注:后端仍支持 ongoing(进行中 = 跨天延期中的任务),当前不启用;
|
||||
// 以后要恢复,在下面数组里加一行 { value: "ongoing", label: "进行中" } 即可。
|
||||
const categoryTabs = [
|
||||
{ value: "all", label: "全部" },
|
||||
{ value: "today", label: "今日" },
|
||||
{ value: "pending", label: "未完成" },
|
||||
{ value: "done", label: "已完成" }
|
||||
];
|
||||
const category = ref("all");
|
||||
const categoryCounts = ref({});
|
||||
|
||||
// ---------- 日期工具(避免额外依赖) ----------
|
||||
function pad2(n) {
|
||||
return String(n).padStart(2, "0");
|
||||
@@ -346,12 +414,23 @@ function normDate(value) {
|
||||
: value || "";
|
||||
}
|
||||
|
||||
// 在 YYYY-MM-DD 上加减天数,用于计算"顺延到下一天"的目标日期
|
||||
function addDays(dateStr, days) {
|
||||
const parts = String(dateStr || "")
|
||||
.slice(0, 10)
|
||||
.split("-")
|
||||
.map(Number);
|
||||
if (parts.length !== 3 || parts.some(Number.isNaN)) {
|
||||
return "";
|
||||
}
|
||||
return fmtDate(new Date(parts[0], parts[1] - 1, parts[2] + days));
|
||||
}
|
||||
|
||||
// ---------- 日历视图 ----------
|
||||
const weekNames = ["一", "二", "三", "四", "五", "六", "日"];
|
||||
const now = new Date();
|
||||
const calendarYear = ref(now.getFullYear());
|
||||
const calendarMonth = ref(now.getMonth() + 1);
|
||||
const calendarStatus = ref("");
|
||||
const calendarItems = ref([]);
|
||||
const selectedDate = ref(todayStr());
|
||||
|
||||
@@ -403,6 +482,11 @@ const selectedItems = computed(
|
||||
() => calendarMap.value[selectedDate.value] || []
|
||||
);
|
||||
|
||||
// 当天仍未完成的日程数,用于"顺延未完成"批量操作
|
||||
const pendingCount = computed(
|
||||
() => selectedItems.value.filter(item => item.status !== 1).length
|
||||
);
|
||||
|
||||
const selectedDateLabel = computed(() => {
|
||||
const [y, m, d] = selectedDate.value.split("-").map(Number);
|
||||
const week = "日一二三四五六"[new Date(y, m - 1, d).getDay()];
|
||||
@@ -422,9 +506,12 @@ async function loadCalendarData() {
|
||||
end_date: fmtDate(end),
|
||||
page: 1,
|
||||
pageSize: 500,
|
||||
status: calendarStatus.value
|
||||
category: category.value
|
||||
});
|
||||
const data = res?.data || {};
|
||||
if (data.counts) {
|
||||
categoryCounts.value = data.counts;
|
||||
}
|
||||
calendarItems.value = (data.list || []).map(item => ({
|
||||
...item,
|
||||
schedule_date: normDate(item.schedule_date)
|
||||
@@ -455,7 +542,7 @@ async function loadStats() {
|
||||
}
|
||||
|
||||
// ---------- 列表视图 ----------
|
||||
const listFilters = reactive({ keyword: "", status: "", range: null });
|
||||
const listFilters = reactive({ keyword: "", range: null });
|
||||
const listData = ref([]);
|
||||
const listLoading = ref(false);
|
||||
const listPagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
@@ -467,7 +554,7 @@ async function loadListData() {
|
||||
page: listPagination.page,
|
||||
pageSize: listPagination.pageSize,
|
||||
keyword: listFilters.keyword,
|
||||
status: listFilters.status
|
||||
category: category.value
|
||||
};
|
||||
if (listFilters.range && listFilters.range.length === 2) {
|
||||
params.start_date = listFilters.range[0];
|
||||
@@ -475,6 +562,9 @@ async function loadListData() {
|
||||
}
|
||||
const res = await getScheduleList(params);
|
||||
const data = res?.data || {};
|
||||
if (data.counts) {
|
||||
categoryCounts.value = data.counts;
|
||||
}
|
||||
listData.value = (data.list || []).map(item => ({
|
||||
...item,
|
||||
schedule_date: normDate(item.schedule_date)
|
||||
@@ -487,12 +577,21 @@ async function loadListData() {
|
||||
|
||||
function resetListFilters() {
|
||||
listFilters.keyword = "";
|
||||
listFilters.status = "";
|
||||
listFilters.range = null;
|
||||
listPagination.page = 1;
|
||||
loadListData();
|
||||
}
|
||||
|
||||
// 切换分类:切到"今日"时日历自动回到今天所在月份,避免当月看不到数据
|
||||
function handleCategoryChange(value) {
|
||||
listPagination.page = 1;
|
||||
if (value === "today" && viewMode.value === "calendar") {
|
||||
goToday();
|
||||
return;
|
||||
}
|
||||
reloadAll();
|
||||
}
|
||||
|
||||
// 切到列表视图时按需加载
|
||||
watch(viewMode, mode => {
|
||||
if (mode === "list" && listData.value.length === 0) {
|
||||
@@ -530,23 +629,79 @@ function handleDetailEdit(item) {
|
||||
openEdit(item);
|
||||
}
|
||||
|
||||
// 详情内完成/取消完成后从最新数据重新定位,保持详情内容同步
|
||||
async function toggleFromDetail(item) {
|
||||
// 详情里点"已完成"
|
||||
async function handleDetailFinish(item) {
|
||||
detailVisible.value = false;
|
||||
await toggleFinish(item);
|
||||
detailItem.value = calendarItems.value.find(i => i.id === item.id) || null;
|
||||
if (!detailItem.value) {
|
||||
detailVisible.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 详情里点"反审核":把已完成的日程恢复为待办
|
||||
async function handleDetailRevert(item) {
|
||||
detailVisible.value = false;
|
||||
await toggleFinish(item);
|
||||
}
|
||||
|
||||
// 详情里点"延期":关闭详情后打开延期对话框
|
||||
function handleDetailPostpone(item) {
|
||||
detailVisible.value = false;
|
||||
openPostpone(item);
|
||||
}
|
||||
|
||||
function handleItemCommand(cmd, item) {
|
||||
if (cmd === "edit") {
|
||||
if (cmd === "postpone") {
|
||||
openPostpone(item);
|
||||
} else if (cmd === "edit") {
|
||||
openEdit(item);
|
||||
} else if (cmd === "delete") {
|
||||
handleDelete(item);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 延期到下一天 ----------
|
||||
const postponeVisible = ref(false);
|
||||
const postponeItem = ref(null);
|
||||
|
||||
function openPostpone(item) {
|
||||
if (!item || item.status === 1) {
|
||||
ElMessage.warning("该日程已完成,无需延期");
|
||||
return;
|
||||
}
|
||||
postponeItem.value = item;
|
||||
postponeVisible.value = true;
|
||||
}
|
||||
|
||||
// 批量延期:把选中日期的全部未完成日程一次性延期到下一天
|
||||
async function handleCarryPending() {
|
||||
if (pendingCount.value === 0) {
|
||||
return;
|
||||
}
|
||||
// 目标日期取"选定日期+1天",但不早于今天,避免把过去的任务再延到过去
|
||||
const nextDay = addDays(selectedDate.value, 1);
|
||||
const target = nextDay < todayStr() ? todayStr() : nextDay;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将 ${selectedDate.value} 的 ${pendingCount.value} 条未完成日程延期到 ${target}?当天这些日程会标记为已完成。`,
|
||||
"批量延期",
|
||||
{ type: "warning", confirmButtonText: "延期", cancelButtonText: "取消" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await carryPendingSchedules({
|
||||
date: selectedDate.value,
|
||||
target_date: target
|
||||
});
|
||||
if (res?.code === 200) {
|
||||
const d = res.data || {};
|
||||
ElMessage.success(
|
||||
`已延期 ${d.success || 0} 条${d.skipped ? `,跳过 ${d.skipped} 条` : ""}`
|
||||
);
|
||||
await reloadAll();
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "延期失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFinish(item) {
|
||||
const res = await finishSchedule(item.id);
|
||||
if (res?.code === 200) {
|
||||
@@ -585,6 +740,18 @@ function reloadAll() {
|
||||
}
|
||||
|
||||
// ---------- 展示辅助 ----------
|
||||
// YYYY-MM-DD -> M月D日,用于"已顺延至"标签
|
||||
function shortDate(value) {
|
||||
const parts = String(value || "")
|
||||
.slice(0, 10)
|
||||
.split("-")
|
||||
.map(Number);
|
||||
if (parts.length !== 3 || parts.some(Number.isNaN)) {
|
||||
return value || "";
|
||||
}
|
||||
return `${parts[1]}月${parts[2]}日`;
|
||||
}
|
||||
|
||||
function timeText(item) {
|
||||
if (item.all_day === 1) {
|
||||
return "全天";
|
||||
@@ -672,6 +839,42 @@ onMounted(() => {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
/* 待办分类 tabs */
|
||||
.category-tabs {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.category-tabs :deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 分类内容由下方日历/列表承载,tabs 只作切换栏,隐藏空的面板容器 */
|
||||
.category-tabs :deep(.el-tabs__content) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
min-width: 20px;
|
||||
padding: 0 6px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
border-radius: 9px;
|
||||
background: #f0f2f5;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 日历视图布局 */
|
||||
.calendar-layout {
|
||||
display: flex;
|
||||
@@ -864,6 +1067,13 @@ onMounted(() => {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.day-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
Reference in New Issue
Block a user