工作日程提醒
@@ -42,19 +102,36 @@
>查看全部
+
+
+
+
+ {{ tab.label }}
+ {{
+ scheduleCounts[tab.value] || 0
+ }}
+
+
+
+
-
-
+
{{ item.priority === 2 ? "紧急" : "重要" }}
+
+ 延期第 {{ item.carry_count + 1 }} 天
+
-
-
-
-
-
-
状态分布
-
-
-
{{
- item.name
- }}
-
-
{{ item.count }} 单 / ¥ {{ money(item.amount) }}
-
-
-
-
-
报销类型分析
-
-
-
{{ item.name }}
-
-
{{ item.count }} 笔
-
¥ {{ money(item.amount) }}
-
-
-
-
+
+
+
+
+
@@ -154,6 +206,8 @@ import * as echarts from "echarts";
import { Refresh } from "@element-plus/icons-vue";
import { getReimbursementDashboard } from "@/api/reimburse";
import { finishSchedule, getScheduleList } from "@/api/oaSchedule";
+import ScheduleDetail from "../schedule/components/detail.vue";
+import SchedulePostpone from "../schedule/components/postponeDialog.vue";
const emptyStat = () => ({ total: 0, paid: 0, unpaid: 0, count: 0 });
const dashboard = reactive({
@@ -249,6 +303,51 @@ const router = useRouter();
const scheduleReminders = ref([]);
const scheduleLoading = ref(false);
+// 待办分类:all-全部 today-今日 pending-未完成 done-已完成
+// 注:matchScheduleCategory 仍保留 ongoing 分支(进行中 = 跨天延期中的任务),
+// 当前不启用;以后要恢复,在下面数组里加一行 { value: "ongoing", label: "进行中" } 即可。
+const scheduleTabs = [
+ { value: "all", label: "全部" },
+ { value: "today", label: "今日" },
+ { value: "pending", label: "未完成" },
+ { value: "done", label: "已完成" },
+];
+const scheduleTab = ref("all");
+
+// 判断一条日程是否属于某个分类(与后端 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 fmtDate = (d) =>
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
@@ -258,44 +357,29 @@ 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}`,
- );
+ const res = await getScheduleList({
+ end_date: fmtDate(weekLater),
+ page: 1,
+ pageSize: 500,
});
+ scheduleReminders.value = (res?.data?.list || [])
+ .map((item) => ({
+ ...item,
+ // 统一日期格式,避免后端 parseTime 输出 Go 默认格式导致标签解析 NaN
+ schedule_date: String(item.schedule_date || "").slice(0, 10),
+ }))
+ .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 {
@@ -335,16 +419,36 @@ const scheduleTimeText = (item) =>
const isOverdueSchedule = (item) =>
item.status === 0 && item.schedule_date < fmtDate(new Date());
+// 点击任务只打开详情抽屉,由用户明确选择延期 / 已完成 / 反审核,避免误点即完成
+const scheduleDetailVisible = ref(false);
+const scheduleDetailItem = ref(null);
+const schedulePostponeVisible = ref(false);
+
+const openScheduleDetail = (item) => {
+ scheduleDetailItem.value = item;
+ scheduleDetailVisible.value = true;
+};
+
const toggleScheduleFinish = async (item) => {
const res = await finishSchedule(item.id);
if (res?.code === 200) {
- ElMessage.success(res.data?.status === 1 ? "已完成" : "已恢复待办");
+ ElMessage.success(res.data?.status === 1 ? "已完成" : "已反审核");
+ scheduleDetailVisible.value = false;
loadScheduleReminders();
} else {
ElMessage.error(res?.msg || "操作失败");
}
};
+const handleScheduleFinish = (item) => toggleScheduleFinish(item);
+const handleScheduleRevert = (item) => toggleScheduleFinish(item);
+
+const handleSchedulePostpone = (item) => {
+ scheduleDetailItem.value = item;
+ scheduleDetailVisible.value = false;
+ schedulePostponeVisible.value = true;
+};
+
const goSchedule = () => router.push("/apps/oa/schedule");
const loadDashboard = async () => {
@@ -455,11 +559,28 @@ onBeforeUnmount(() => {
}
}
-.chart-row {
+/* 左右分栏:左 70% 报销,右 30% 日程提醒 */
+.dashboard-columns {
+ display: flex;
+ align-items: flex-start;
+ gap: 16px;
+}
+
+.col-left {
+ flex: 0 0 calc(70% - 8px);
+ min-width: 0;
+}
+
+.col-right {
+ flex: 0 0 calc(30% - 8px);
+ min-width: 0;
+}
+
+.col-left > .chart-card {
margin-bottom: 16px;
- .el-col {
- margin-bottom: 8px;
+ &:last-child {
+ margin-bottom: 0;
}
}
@@ -489,9 +610,10 @@ onBeforeUnmount(() => {
}
.schedule-card {
- height: 100%;
display: flex;
flex-direction: column;
+ /*min-height: 340px; */
+ height: 600px;
.schedule-title {
display: flex;
@@ -504,6 +626,40 @@ onBeforeUnmount(() => {
}
}
+.schedule-tabs {
+ :deep(.el-tabs__header) {
+ margin: 0 0 10px;
+ }
+
+ /* 列表由下方 .schedule-list 承载,tabs 只作切换栏,隐藏空的面板容器 */
+ :deep(.el-tabs__content) {
+ display: none;
+ }
+
+ :deep(.el-tabs__item) {
+ padding: 0 10px;
+ font-size: 13px;
+ }
+}
+
+.tab-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.tab-count {
+ min-width: 18px;
+ padding: 0 5px;
+ height: 16px;
+ line-height: 16px;
+ border-radius: 8px;
+ background: #f0f2f5;
+ color: #909399;
+ font-size: 11px;
+ text-align: center;
+}
+
.status-card {
margin-bottom: 16px;
}
@@ -512,7 +668,9 @@ onBeforeUnmount(() => {
display: flex;
flex-direction: column;
gap: 8px;
- min-height: 80px;
+ min-height: 120px;
+ max-height: 520px;
+ overflow-y: auto;
}
.schedule-item {
@@ -523,12 +681,32 @@ onBeforeUnmount(() => {
background: #fafbfc;
border-left: 3px solid var(--item-color);
border-radius: 4px;
+ cursor: pointer;
+ transition: background 0.15s;
+
+ &:hover {
+ background: #eef2f7;
+ }
&.done {
background: #f5f7fa;
border-left-color: #dcdfe6;
}
+ /* 状态指示点:仅标识状态,点击整行弹出详情由用户选择操作 */
+ .item-status-dot {
+ width: 10px;
+ height: 10px;
+ margin-top: 5px;
+ flex-shrink: 0;
+ border-radius: 50%;
+ background: var(--item-color);
+
+ &.done {
+ background: #c0c4cc;
+ }
+ }
+
.schedule-item-main {
flex: 1;
min-width: 0;
@@ -633,6 +811,23 @@ onBeforeUnmount(() => {
white-space: nowrap;
}
+/* 窄屏下左右分栏改为上下堆叠 */
+@media (max-width: 1100px) {
+ .dashboard-columns {
+ flex-direction: column;
+ }
+
+ .col-left,
+ .col-right {
+ flex: 1 1 auto;
+ width: 100%;
+ }
+
+ .col-right {
+ margin-top: 16px;
+ }
+}
+
@media (max-width: 768px) {
.oa-dashboard {
padding: 12px;
diff --git a/backend/src/views/apps/oa/employeefile/components/fileEditDialog.vue b/backend/src/views/apps/oa/employeefile/components/fileEditDialog.vue
index e490bf4..aa4ac00 100644
--- a/backend/src/views/apps/oa/employeefile/components/fileEditDialog.vue
+++ b/backend/src/views/apps/oa/employeefile/components/fileEditDialog.vue
@@ -53,6 +53,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -149,7 +181,7 @@
+
+
diff --git a/backend/src/views/apps/oa/schedule/index.vue b/backend/src/views/apps/oa/schedule/index.vue
index a90b4a6..dda1550 100644
--- a/backend/src/views/apps/oa/schedule/index.vue
+++ b/backend/src/views/apps/oa/schedule/index.vue
@@ -39,6 +39,26 @@
+
+
+
+
+
+ {{ tab.label }}
+ {{ categoryCounts[tab.value] || 0 }}
+
+
+
+
+
@@ -51,15 +71,6 @@
今天
-
- 全部
- 待办
- 已完成
-
@@ -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;
diff --git a/backend/src/views/apps/organization/components/EmployeeEditDialog.vue b/backend/src/views/apps/organization/components/EmployeeEditDialog.vue
index f6cf435..9a17656 100644
--- a/backend/src/views/apps/organization/components/EmployeeEditDialog.vue
+++ b/backend/src/views/apps/organization/components/EmployeeEditDialog.vue
@@ -46,66 +46,6 @@
-
-
组织信息
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
联系与其它
-
-
-
-
-
-
-
-
-
-
-
@@ -129,6 +69,56 @@
+
+
+
组织信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -139,7 +129,7 @@