增加日程提醒

This commit is contained in:
2026-08-28 16:58:17 +08:00
parent 9588ddbf31
commit d3bc1833f8
13 changed files with 2267 additions and 20 deletions
+256 -19
View File
@@ -35,30 +35,54 @@
</div>
</el-col>
<el-col :span="10" :xs="24">
<div class="chart-card">
<div class="chart-title">状态分布</div>
<div class="status-dist">
<div
v-for="(item, key) in dashboard.status_dist"
:key="key"
class="dist-row"
<div class="chart-card schedule-card">
<div class="chart-title schedule-title">
<span>工作日程提醒</span>
<el-button link type="primary" @click="goSchedule"
>查看全部</el-button
>
<el-tag :type="statusType(Number(key))" size="small">{{
item.name
}}</el-tag>
<div class="dist-bar-wrap">
</div>
<div v-loading="scheduleLoading" class="schedule-list">
<div
v-for="item in scheduleReminders"
:key="item.id"
class="schedule-item"
:class="{ done: item.status === 1 }"
:style="{ '--item-color': item.color }"
>
<el-checkbox
:model-value="item.status === 1"
@change="toggleScheduleFinish(item)"
/>
<div class="schedule-item-main" @click="toggleScheduleFinish(item)">
<div
class="dist-bar"
:style="{ width: distPercent(item.count) }"
/>
class="schedule-item-title"
:class="{ done: item.status === 1 }"
>
{{ item.title }}
<el-tag
v-if="item.priority > 0"
:type="item.priority === 2 ? 'danger' : 'warning'"
size="small"
effect="light"
>
{{ item.priority === 2 ? "紧急" : "重要" }}
</el-tag>
</div>
<div
class="schedule-item-time"
:class="{ done: item.status === 1 }"
>
<span :class="{ overdue: isOverdueSchedule(item) }">{{
scheduleDateTag(item)
}}</span>
<span>{{ scheduleTimeText(item) }}</span>
</div>
</div>
<span class="dist-count"
>{{ item.count }} 单 / ¥ {{ money(item.amount) }}</span
>
</div>
<el-empty
v-if="!Object.keys(dashboard.status_dist).length"
description="暂无数据"
v-if="!scheduleReminders.length && !scheduleLoading"
description="近期暂无日程安排"
:image-size="60"
/>
</div>
@@ -66,6 +90,36 @@
</el-col>
</el-row>
<!-- 状态分布 -->
<div class="chart-card status-card">
<div class="chart-title">状态分布</div>
<div class="status-dist">
<div
v-for="(item, key) in dashboard.status_dist"
:key="key"
class="dist-row"
>
<el-tag :type="statusType(Number(key))" size="small">{{
item.name
}}</el-tag>
<div class="dist-bar-wrap">
<div
class="dist-bar"
:style="{ width: distPercent(item.count) }"
/>
</div>
<span class="dist-count"
>{{ item.count }} 单 / ¥ {{ money(item.amount) }}</span
>
</div>
<el-empty
v-if="!Object.keys(dashboard.status_dist).length"
description="暂无数据"
:image-size="60"
/>
</div>
</div>
<!-- 报销类型分析 -->
<div class="chart-card type-card">
<div class="chart-title">报销类型分析</div>
@@ -94,9 +148,12 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import * as echarts from "echarts";
import { Refresh } from "@element-plus/icons-vue";
import { getReimbursementDashboard } from "@/api/reimburse";
import { finishSchedule, getScheduleList } from "@/api/oaSchedule";
const emptyStat = () => ({ total: 0, paid: 0, unpaid: 0, count: 0 });
const dashboard = reactive({
@@ -187,6 +244,109 @@ const typePercent = (amount) =>
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
// ---------- 工作日程提醒 ----------
const router = useRouter();
const scheduleReminders = ref([]);
const scheduleLoading = ref(false);
const pad2 = (n) => String(n).padStart(2, "0");
const fmtDate = (d) =>
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
// 拉取逾期 + 未来7天日程(均含已完成,完成后灰显沉底而非从列表消失)
const loadScheduleReminders = async () => {
scheduleLoading.value = true;
try {
const today = new Date();
const todayStr = fmtDate(today);
const weekLater = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() + 7,
);
const yesterday = fmtDate(
new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1),
);
const [overdueRes, upcomingRes] = await Promise.all([
getScheduleList({
end_date: yesterday,
page: 1,
pageSize: 50,
}),
getScheduleList({
start_date: todayStr,
end_date: fmtDate(weekLater),
page: 1,
pageSize: 50,
}),
]);
const overdue = (overdueRes?.data?.list || []).map(item => ({
...item,
// 统一日期格式,避免后端 parseTime 输出 Go 默认格式导致标签解析 NaN
schedule_date: String(item.schedule_date || "").slice(0, 10),
}));
const upcoming = (upcomingRes?.data?.list || []).map(item => ({
...item,
schedule_date: String(item.schedule_date || "").slice(0, 10),
}));
scheduleReminders.value = [...overdue, ...upcoming].sort((a, b) => {
// 未完成在前,已完成沉底,保证点击完成后条目保留可见(灰显)
if (a.status !== b.status) return a.status - b.status;
return `${a.schedule_date}${a.start_time}`.localeCompare(
`${b.schedule_date}${b.start_time}`,
);
});
} catch (error) {
console.warn("加载日程提醒失败:", error?.message);
} finally {
scheduleLoading.value = false;
}
};
// 列表项日期标签:已逾期N天 / 今天 / 明天 / M/D 周X
const scheduleDateTag = (item) => {
const today = new Date();
const todayStr = fmtDate(today);
if (item.schedule_date === todayStr) return "今天";
if (item.schedule_date < todayStr) {
const diff = Math.round(
(new Date(todayStr) - new Date(item.schedule_date)) / 86400000,
);
return `已逾期${diff}天`;
}
const tomorrow = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() + 1,
);
if (fmtDate(tomorrow) === item.schedule_date) return "明天";
const [y, m, d] = item.schedule_date.split("-").map(Number);
const week = "日一二三四五六"[new Date(y, m - 1, d).getDay()];
return `${m}/${d} 周${week}`;
};
const scheduleTimeText = (item) =>
item.all_day === 1
? "全天"
: item.end_time
? `${item.start_time} - ${item.end_time}`
: item.start_time;
const isOverdueSchedule = (item) =>
item.status === 0 && item.schedule_date < fmtDate(new Date());
const toggleScheduleFinish = async (item) => {
const res = await finishSchedule(item.id);
if (res?.code === 200) {
ElMessage.success(res.data?.status === 1 ? "已完成" : "已恢复待办");
loadScheduleReminders();
} else {
ElMessage.error(res?.msg || "操作失败");
}
};
const goSchedule = () => router.push("/apps/oa/schedule");
const loadDashboard = async () => {
try {
const data = responseData(await getReimbursementDashboard());
@@ -200,6 +360,7 @@ const loadDashboard = async () => {
console.warn("加载报销统计失败:", error?.message);
}
renderTrendChart();
loadScheduleReminders();
};
const handleResize = () => trendChart?.resize();
@@ -327,6 +488,82 @@ onBeforeUnmount(() => {
margin-bottom: 0;
}
.schedule-card {
height: 100%;
display: flex;
flex-direction: column;
.schedule-title {
display: flex;
align-items: center;
justify-content: space-between;
}
.schedule-list {
flex: 1;
}
}
.status-card {
margin-bottom: 16px;
}
.schedule-list {
display: flex;
flex-direction: column;
gap: 8px;
min-height: 80px;
}
.schedule-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px 12px;
background: #fafbfc;
border-left: 3px solid var(--item-color);
border-radius: 4px;
&.done {
background: #f5f7fa;
border-left-color: #dcdfe6;
}
.schedule-item-main {
flex: 1;
min-width: 0;
cursor: pointer;
}
.schedule-item-title {
font-size: 14px;
color: #303133;
word-break: break-all;
&.done {
text-decoration: line-through;
color: #c0c4cc;
}
}
.schedule-item-time {
display: flex;
gap: 10px;
margin-top: 2px;
font-size: 12px;
color: #909399;
&.done {
color: #c0c4cc;
}
}
.overdue {
color: #f56c6c;
font-weight: 500;
}
}
.status-dist,
.type-dist {
display: flex;