优化日程管理功能
This commit is contained in:
@@ -136,19 +136,14 @@
|
|||||||
>查看全部</el-button
|
>查看全部</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<el-tabs v-model="scheduleTab" class="schedule-tabs">
|
<el-tabs v-model="scheduleRange" class="schedule-tabs">
|
||||||
<el-tab-pane
|
<el-tab-pane
|
||||||
v-for="tab in scheduleTabs"
|
v-for="tab in scheduleRanges"
|
||||||
:key="tab.value"
|
:key="tab.value"
|
||||||
:name="tab.value"
|
:name="tab.value"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="tab-label">
|
<span class="tab-label">{{ tab.label }}</span>
|
||||||
{{ tab.label }}
|
|
||||||
<span class="tab-count">{{
|
|
||||||
scheduleCounts[tab.value] || 0
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
@@ -203,9 +198,9 @@
|
|||||||
<el-empty
|
<el-empty
|
||||||
v-if="!filteredScheduleReminders.length && !scheduleLoading"
|
v-if="!filteredScheduleReminders.length && !scheduleLoading"
|
||||||
:description="
|
:description="
|
||||||
scheduleTab === 'all'
|
scheduleRange === 'today'
|
||||||
? '近期暂无日程安排'
|
? '今日暂无日程安排'
|
||||||
: '该分类下暂无日程'
|
: '该时间范围内暂无日程'
|
||||||
"
|
"
|
||||||
:image-size="60"
|
:image-size="60"
|
||||||
/>
|
/>
|
||||||
@@ -263,7 +258,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import * as echarts from "echarts";
|
import * as echarts from "echarts";
|
||||||
@@ -376,67 +371,74 @@ const router = useRouter();
|
|||||||
const scheduleReminders = ref([]);
|
const scheduleReminders = ref([]);
|
||||||
const scheduleLoading = ref(false);
|
const scheduleLoading = ref(false);
|
||||||
|
|
||||||
// 待办分类:all-全部 today-今日 pending-未完成 done-已完成
|
// 时间范围筛选:今日(默认)/ 本周 / 本月 / 本季度 / 本年
|
||||||
// 注:matchScheduleCategory 仍保留 ongoing 分支(进行中 = 跨天延期中的任务),
|
const scheduleRanges = [
|
||||||
// 当前不启用;以后要恢复,在下面数组里加一行 { value: "ongoing", label: "进行中" } 即可。
|
|
||||||
const scheduleTabs = [
|
|
||||||
{ value: "all", label: "全部" },
|
|
||||||
{ value: "today", label: "今日" },
|
{ value: "today", label: "今日" },
|
||||||
{ value: "pending", label: "未完成" },
|
{ value: "week", label: "本周" },
|
||||||
{ value: "done", label: "已完成" },
|
{ value: "month", label: "本月" },
|
||||||
|
{ value: "quarter", label: "本季度" },
|
||||||
|
{ value: "year", label: "本年" },
|
||||||
];
|
];
|
||||||
const scheduleTab = ref("all");
|
const scheduleRange = ref("today");
|
||||||
|
|
||||||
// 判断一条日程是否属于某个分类(与后端 category 语义保持一致)
|
|
||||||
const matchScheduleCategory = (item, category) => {
|
|
||||||
switch (category) {
|
|
||||||
case "today":
|
|
||||||
return item.schedule_date === fmtDate(new Date());
|
|
||||||
case "pending":
|
|
||||||
return item.status === 0;
|
|
||||||
// 进行中 = 跨天顺延中的任务:被顺延出去的源日程,或顺延出来的续做日程
|
|
||||||
case "ongoing":
|
|
||||||
return item.source_id > 0 || item.carry_over === 1;
|
|
||||||
case "done":
|
|
||||||
return item.status === 1;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredScheduleReminders = computed(() =>
|
|
||||||
scheduleReminders.value.filter((item) =>
|
|
||||||
matchScheduleCategory(item, scheduleTab.value),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 各分类数量按当前面板范围(逾期 + 未来7天)统计
|
|
||||||
const scheduleCounts = computed(() => {
|
|
||||||
const result = {};
|
|
||||||
for (const tab of scheduleTabs) {
|
|
||||||
result[tab.value] = scheduleReminders.value.filter((item) =>
|
|
||||||
matchScheduleCategory(item, tab.value),
|
|
||||||
).length;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pad2 = (n) => String(n).padStart(2, "0");
|
const pad2 = (n) => String(n).padStart(2, "0");
|
||||||
const fmtDate = (d) =>
|
const fmtDate = (d) =>
|
||||||
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||||
|
|
||||||
// 拉取逾期 + 未来7天日程(均含已完成,完成后灰显沉底而非从列表消失)
|
// 计算指定时间范围的起止日期(含本周一 ~ 周日、季度首末月等)
|
||||||
const loadScheduleReminders = async () => {
|
const rangeDates = (range) => {
|
||||||
|
const now = new Date();
|
||||||
|
const today = fmtDate(now);
|
||||||
|
if (range === "today") {
|
||||||
|
return { start: today, end: today };
|
||||||
|
}
|
||||||
|
if (range === "week") {
|
||||||
|
// 周一为一周起点
|
||||||
|
const weekday = now.getDay() || 7;
|
||||||
|
const start = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate() - (weekday - 1),
|
||||||
|
);
|
||||||
|
const end = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate() + (7 - weekday),
|
||||||
|
);
|
||||||
|
return { start: fmtDate(start), end: fmtDate(end) };
|
||||||
|
}
|
||||||
|
if (range === "month") {
|
||||||
|
const start = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||||
|
return { start: fmtDate(start), end: fmtDate(end) };
|
||||||
|
}
|
||||||
|
if (range === "quarter") {
|
||||||
|
const q = Math.floor(now.getMonth() / 3);
|
||||||
|
const start = new Date(now.getFullYear(), q * 3, 1);
|
||||||
|
const end = new Date(now.getFullYear(), q * 3 + 3, 0);
|
||||||
|
return { start: fmtDate(start), end: fmtDate(end) };
|
||||||
|
}
|
||||||
|
// year
|
||||||
|
const start = new Date(now.getFullYear(), 0, 1);
|
||||||
|
const end = new Date(now.getFullYear(), 11, 31);
|
||||||
|
return { start: fmtDate(start), end: fmtDate(end) };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 列表直接展示按所选时间范围拉取的数据(已完成灰显沉底)
|
||||||
|
const filteredScheduleReminders = computed(() => scheduleReminders.value);
|
||||||
|
|
||||||
|
// 切换时间范围时重新拉取对应区间的日程
|
||||||
|
watch(scheduleRange, () => loadScheduleReminders());
|
||||||
|
|
||||||
|
// 按所选时间范围拉取日程(含已完成,完成后灰显沉底而非从列表消失)
|
||||||
|
const loadScheduleReminders = async (range) => {
|
||||||
|
const r = range || scheduleRange.value;
|
||||||
scheduleLoading.value = true;
|
scheduleLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const today = new Date();
|
const { start, end } = rangeDates(r);
|
||||||
const weekLater = new Date(
|
|
||||||
today.getFullYear(),
|
|
||||||
today.getMonth(),
|
|
||||||
today.getDate() + 7,
|
|
||||||
);
|
|
||||||
const res = await getScheduleList({
|
const res = await getScheduleList({
|
||||||
end_date: fmtDate(weekLater),
|
start_date: start,
|
||||||
|
end_date: end,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 500,
|
pageSize: 500,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -336,9 +336,10 @@ const remarkDirty = computed(
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.schedule,
|
() => props.schedule,
|
||||||
schedule => {
|
(schedule, prev) => {
|
||||||
// 未编辑备注时跟随日程内容回显,避免覆盖用户正在输入的未保存内容
|
// 切换到不同日程时强制以新日程内容回显,避免上一个日程的备注串到当前日程;
|
||||||
if (!remarkDirty.value) {
|
// 同一日程刷新时仅在不脏(无未保存编辑)时回显,避免覆盖正在输入的备注
|
||||||
|
if (!prev || prev.id !== schedule?.id || !remarkDirty.value) {
|
||||||
remarkText.value = schedule?.content || "";
|
remarkText.value = schedule?.content || "";
|
||||||
}
|
}
|
||||||
imageList.value = parseImages(schedule?.images);
|
imageList.value = parseImages(schedule?.images);
|
||||||
|
|||||||
@@ -18,21 +18,32 @@
|
|||||||
<div class="stats-row">
|
<div class="stats-row">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">今日待办</div>
|
<div class="stat-label">今日待办</div>
|
||||||
<div class="stat-value">{{ stats.today_total || 0 }}</div>
|
<div class="stat-value is-green">{{ todayPending }}</div>
|
||||||
<div class="stat-sub">已完成 {{ stats.today_done || 0 }}</div>
|
<div class="stat-sub">
|
||||||
|
共 {{ stats.today_total || 0 }} 项 · 已完成
|
||||||
|
{{ stats.today_done || 0 }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">本周日程</div>
|
<div class="stat-label">本周日程</div>
|
||||||
<div class="stat-value">{{ stats.week_total || 0 }}</div>
|
<div class="stat-value is-blue">{{ weekPending }}</div>
|
||||||
|
<div class="stat-sub">
|
||||||
|
共 {{ stats.week_total || 0 }} 项 · 已完成
|
||||||
|
{{ stats.week_done || 0 }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">本月日程</div>
|
<div class="stat-label">本月日程</div>
|
||||||
<div class="stat-value">{{ stats.month_total || 0 }}</div>
|
<div class="stat-value is-orange">{{ monthPending }}</div>
|
||||||
<div class="stat-sub">已完成 {{ stats.month_done || 0 }}</div>
|
<div class="stat-sub">
|
||||||
|
共 {{ stats.month_total || 0 }} 项 · 已完成
|
||||||
|
{{ stats.month_done || 0 }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" :class="{ danger: (stats.overdue || 0) > 0 }">
|
<div class="stat-card" :class="{ danger: (stats.overdue || 0) > 0 }">
|
||||||
<div class="stat-label">逾期未完成</div>
|
<div class="stat-label">逾期未完成</div>
|
||||||
<div class="stat-value">{{ stats.overdue || 0 }}</div>
|
<div class="stat-value is-red">{{ stats.overdue || 0 }}</div>
|
||||||
|
<div class="stat-sub">项待处理</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -406,6 +417,18 @@ import {
|
|||||||
const viewMode = ref("calendar");
|
const viewMode = ref("calendar");
|
||||||
const stats = ref({});
|
const stats = ref({});
|
||||||
|
|
||||||
|
// 今日剩余待办(未完成)= 今日总任务 - 今日已完成
|
||||||
|
const todayPending = computed(
|
||||||
|
() => (stats.value.today_total || 0) - (stats.value.today_done || 0)
|
||||||
|
);
|
||||||
|
// 本周 / 本月剩余未完成数量
|
||||||
|
const weekPending = computed(
|
||||||
|
() => (stats.value.week_total || 0) - (stats.value.week_done || 0)
|
||||||
|
);
|
||||||
|
const monthPending = computed(
|
||||||
|
() => (stats.value.month_total || 0) - (stats.value.month_done || 0)
|
||||||
|
);
|
||||||
|
|
||||||
// ---------- 待办分类 tabs ----------
|
// ---------- 待办分类 tabs ----------
|
||||||
// all-全部 today-今日 pending-未完成 done-已完成
|
// all-全部 today-今日 pending-未完成 done-已完成
|
||||||
// 注:后端仍支持 ongoing(进行中 = 跨天延期中的任务),当前不启用;
|
// 注:后端仍支持 ongoing(进行中 = 跨天延期中的任务),当前不启用;
|
||||||
@@ -829,6 +852,21 @@ function reloadAll() {
|
|||||||
return Promise.all(tasks);
|
return Promise.all(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数据刷新后用最新对象回同步 detailItem,避免备注保存 / 图片变更后
|
||||||
|
// detailItem 仍指向旧对象(其 content 未更新),导致编辑时回显旧内容
|
||||||
|
watch([calendarItems, listData], () => {
|
||||||
|
if (!detailVisible.value || !detailItem.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = detailItem.value.id;
|
||||||
|
const fresh =
|
||||||
|
calendarItems.value.find(i => i.id === id) ||
|
||||||
|
listData.value.find(i => i.id === id);
|
||||||
|
if (fresh) {
|
||||||
|
detailItem.value = fresh;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ---------- 展示辅助 ----------
|
// ---------- 展示辅助 ----------
|
||||||
// YYYY-MM-DD -> M月D日,用于"已顺延至"标签
|
// YYYY-MM-DD -> M月D日,用于"已顺延至"标签
|
||||||
function shortDate(value) {
|
function shortDate(value) {
|
||||||
@@ -968,6 +1006,17 @@ onMounted(() => {
|
|||||||
margin: 4px 0 2px;
|
margin: 4px 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 统计卡片大数字按分类配色:今日绿 / 本周蓝 / 本月橙 / 逾期红 */
|
||||||
|
.stat-value.is-green {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
.stat-value.is-blue {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
.stat-value.is-orange {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
.stat-value.is-red,
|
||||||
.stat-card.danger .stat-value {
|
.stat-card.danger .stat-value {
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ func (c *BackendOaScheduleController) Stats() {
|
|||||||
todayTotal, _ := base().Filter("schedule_date", today).Count()
|
todayTotal, _ := base().Filter("schedule_date", today).Count()
|
||||||
todayDone, _ := base().Filter("schedule_date", today).Filter("status", 1).Count()
|
todayDone, _ := base().Filter("schedule_date", today).Filter("status", 1).Count()
|
||||||
weekTotal, _ := base().Filter("schedule_date__gte", weekStart).Filter("schedule_date__lte", weekEnd).Count()
|
weekTotal, _ := base().Filter("schedule_date__gte", weekStart).Filter("schedule_date__lte", weekEnd).Count()
|
||||||
|
weekDone, _ := base().Filter("schedule_date__gte", weekStart).Filter("schedule_date__lte", weekEnd).Filter("status", 1).Count()
|
||||||
monthTotal, _ := base().Filter("schedule_date__gte", monthStart).Count()
|
monthTotal, _ := base().Filter("schedule_date__gte", monthStart).Count()
|
||||||
monthDone, _ := base().Filter("schedule_date__gte", monthStart).Filter("status", 1).Count()
|
monthDone, _ := base().Filter("schedule_date__gte", monthStart).Filter("status", 1).Count()
|
||||||
overdue, _ := base().Filter("schedule_date__lt", today).Filter("status", 0).Count()
|
overdue, _ := base().Filter("schedule_date__lt", today).Filter("status", 0).Count()
|
||||||
@@ -283,6 +284,7 @@ func (c *BackendOaScheduleController) Stats() {
|
|||||||
"today_total": todayTotal,
|
"today_total": todayTotal,
|
||||||
"today_done": todayDone,
|
"today_done": todayDone,
|
||||||
"week_total": weekTotal,
|
"week_total": weekTotal,
|
||||||
|
"week_done": weekDone,
|
||||||
"month_total": monthTotal,
|
"month_total": monthTotal,
|
||||||
"month_done": monthDone,
|
"month_done": monthDone,
|
||||||
"overdue": overdue,
|
"overdue": overdue,
|
||||||
|
|||||||
Reference in New Issue
Block a user