增加日程提醒
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user