1507 lines
38 KiB
Vue
1507 lines
38 KiB
Vue
<template>
|
||
<div class="schedule-page">
|
||
<!-- 页头 -->
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>日程管理</h2>
|
||
<p>按日历安排个人日程,跟进待办完成情况</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-radio-group v-model="viewMode">
|
||
<el-radio-button value="calendar">日历</el-radio-button>
|
||
<el-radio-button value="list">列表</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="stats-row">
|
||
<div class="stat-card">
|
||
<div class="stat-label">今日待办</div>
|
||
<div class="stat-value is-green">{{ todayPending }}</div>
|
||
<div class="stat-sub">
|
||
共 {{ stats.today_total || 0 }} 项 · 已完成
|
||
{{ stats.today_done || 0 }}
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">本周日程</div>
|
||
<div class="stat-value is-blue">{{ weekPending }}</div>
|
||
<div class="stat-sub">
|
||
共 {{ stats.week_total || 0 }} 项 · 已完成
|
||
{{ stats.week_done || 0 }}
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">本月日程</div>
|
||
<div class="stat-value is-orange">{{ monthPending }}</div>
|
||
<div class="stat-sub">
|
||
共 {{ stats.month_total || 0 }} 项 · 已完成
|
||
{{ stats.month_done || 0 }}
|
||
</div>
|
||
</div>
|
||
<div class="stat-card" :class="{ danger: (stats.overdue || 0) > 0 }">
|
||
<div class="stat-label">逾期未完成</div>
|
||
<div class="stat-value is-red">{{ stats.overdue || 0 }}</div>
|
||
<div class="stat-sub">项待处理</div>
|
||
</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">
|
||
<div class="calendar-toolbar">
|
||
<div class="toolbar-left">
|
||
<el-button :icon="ArrowLeft" circle @click="changeMonth(-1)" />
|
||
<span class="month-title"
|
||
>{{ calendarYear }} 年 {{ calendarMonth }} 月</span
|
||
>
|
||
<el-button :icon="ArrowRight" circle @click="changeMonth(1)" />
|
||
<el-button @click="goToday">今天</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="calendar-week-header">
|
||
<div v-for="w in weekNames" :key="w" class="week-name">{{ w }}</div>
|
||
</div>
|
||
<div class="calendar-grid">
|
||
<div
|
||
v-for="cell in calendarCells"
|
||
:key="cell.date"
|
||
class="calendar-cell"
|
||
:class="{
|
||
'other-month': !cell.inMonth,
|
||
today: cell.isToday,
|
||
selected: cell.date === selectedDate,
|
||
'has-items': cell.items.length > 0
|
||
}"
|
||
@click="selectedDate = cell.date"
|
||
@dblclick="openCreate(cell.date)"
|
||
>
|
||
<div class="cell-date">
|
||
<span class="day-num">{{ cell.day }}</span>
|
||
<span v-if="cell.isToday" class="today-tag">今</span>
|
||
<span v-else-if="cell.day === 1" class="month-tag"
|
||
>{{ cell.month }}月</span
|
||
>
|
||
</div>
|
||
<div class="cell-items">
|
||
<div
|
||
v-for="item in cell.items.slice(0, 3)"
|
||
:key="item.id"
|
||
class="cell-item"
|
||
:style="{ '--item-color': item.color }"
|
||
:title="item.title"
|
||
@click.stop="selectedDate = cell.date"
|
||
>
|
||
<span v-if="item.all_day === 0" class="item-time">{{
|
||
item.start_time
|
||
}}</span>
|
||
<i v-if="hasImages(item)" class="fa-solid fa-image item-image-icon"></i>
|
||
<i
|
||
v-if="item.reminder_id > 0"
|
||
class="fa-solid fa-bell item-bell"
|
||
title="已开启提醒"
|
||
></i>
|
||
<span class="item-title" :class="{ done: item.status === 1 }">{{
|
||
item.title
|
||
}}</span>
|
||
</div>
|
||
<div
|
||
v-if="cell.items.length > 3"
|
||
class="cell-more"
|
||
@click.stop="selectedDate = cell.date"
|
||
>
|
||
还有 {{ cell.items.length - 3 }} 项
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 选中日期侧栏 -->
|
||
<div class="day-panel">
|
||
<div class="day-panel-header">
|
||
<div>
|
||
<div class="day-title">{{ selectedDateLabel }}</div>
|
||
<div class="day-sub">
|
||
{{ selectedItems.length }} 项日程<template v-if="pendingCount > 0"
|
||
>,{{ pendingCount }} 项未完成</template
|
||
><template v-if="selectedPendingIds.length > 0"
|
||
>,已选 {{ selectedPendingIds.length }} 项</template
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="panel-actions">
|
||
<el-button
|
||
v-if="pendingCount > 0"
|
||
size="small"
|
||
:icon="Promotion"
|
||
@click="handleCarryPending"
|
||
>延期<template v-if="selectedPendingIds.length > 0"
|
||
>({{ selectedPendingIds.length }})</template
|
||
></el-button
|
||
>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
:icon="Plus"
|
||
@click="openCreate(selectedDate)"
|
||
>新增</el-button
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="day-list">
|
||
<div
|
||
v-for="item in selectedItems"
|
||
:key="item.id"
|
||
class="day-item"
|
||
:style="{ '--item-color': item.color }"
|
||
>
|
||
<el-checkbox
|
||
:model-value="selectedIds.includes(item.id)"
|
||
:disabled="item.status === 1"
|
||
:title="item.status === 1 ? '已完成的日程无需延期' : '勾选后批量延期'"
|
||
@click.stop
|
||
@change="checked => toggleSelect(item.id, checked)"
|
||
/>
|
||
<div class="day-item-main" @click="openDetail(item)">
|
||
<div class="day-item-title" :class="{ done: item.status === 1 }">
|
||
<i v-if="hasImages(item)" class="fa-solid fa-image item-image-icon"></i>
|
||
<i
|
||
v-if="item.reminder_id > 0"
|
||
class="fa-solid fa-bell item-bell"
|
||
title="已开启提醒"
|
||
></i>
|
||
{{ item.title }}
|
||
<el-tag
|
||
v-if="item.priority > 0"
|
||
:type="priorityTagType(item.priority)"
|
||
size="small"
|
||
effect="light"
|
||
>
|
||
{{ 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>
|
||
<el-dropdown
|
||
trigger="click"
|
||
@command="cmd => handleItemCommand(cmd, item)"
|
||
>
|
||
<el-button
|
||
text
|
||
:icon="MoreFilled"
|
||
class="day-item-more"
|
||
@click.stop
|
||
/>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="finish">{{
|
||
item.status === 1 ? "取消完成" : "标记完成"
|
||
}}</el-dropdown-item>
|
||
<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
|
||
>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
<el-empty
|
||
v-if="selectedItems.length === 0"
|
||
description="当天暂无日程"
|
||
:image-size="80"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 列表视图 -->
|
||
<div v-else class="table-card">
|
||
<div class="filter-bar">
|
||
<el-input
|
||
v-model="listFilters.keyword"
|
||
clearable
|
||
placeholder="搜索日程标题"
|
||
style="width: 200px"
|
||
@keyup.enter="loadListData"
|
||
/>
|
||
<el-date-picker
|
||
v-model="listFilters.range"
|
||
type="daterange"
|
||
value-format="YYYY-MM-DD"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
style="width: 260px"
|
||
/>
|
||
<el-button type="primary" :icon="Search" @click="loadListData"
|
||
>查询</el-button
|
||
>
|
||
<el-button :icon="Refresh" @click="resetListFilters">重置</el-button>
|
||
</div>
|
||
<el-table v-loading="listLoading" :data="listData" stripe>
|
||
<el-table-column prop="schedule_date" label="日期" width="120" />
|
||
<el-table-column label="时间" width="150">
|
||
<template #default="{ row }">{{ timeText(row) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="title"
|
||
label="标题"
|
||
min-width="220"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">
|
||
<span class="list-title" :class="{ done: row.status === 1 }">
|
||
<span
|
||
class="color-chip"
|
||
:style="{ background: row.color }"
|
||
/><i v-if="hasImages(row)" class="fa-solid fa-image item-image-icon"></i><i
|
||
v-if="row.reminder_id > 0"
|
||
class="fa-solid fa-bell item-image-icon"
|
||
title="已开启提醒"
|
||
></i>{{ row.title }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="优先级" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
v-if="row.priority > 0"
|
||
:type="priorityTagType(row.priority)"
|
||
size="small"
|
||
>{{ priorityLabel(row.priority) }}</el-tag
|
||
>
|
||
<span v-else>普通</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
:type="row.status === 1 ? 'success' : 'warning'"
|
||
size="small"
|
||
>{{ row.status === 1 ? "已完成" : "待办" }}</el-tag
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
<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
|
||
>
|
||
<el-button link type="danger" @click="handleDelete(row)"
|
||
>删除</el-button
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty><el-empty description="暂无日程" /></template>
|
||
</el-table>
|
||
<div class="pagination">
|
||
<el-pagination
|
||
:current-page="listPagination.page"
|
||
:page-size="listPagination.pageSize"
|
||
:total="listPagination.total"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@update:current-page="listPagination.page = $event"
|
||
@update:page-size="listPagination.pageSize = $event"
|
||
@current-change="loadListData"
|
||
@size-change="loadListData"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/编辑抽屉 -->
|
||
<ScheduleEdit
|
||
v-model="editVisible"
|
||
:schedule="editingItem"
|
||
:default-date="selectedDate"
|
||
:draft="pendingDraft"
|
||
@saved="reloadAll"
|
||
@draft-applied="pendingDraft = null"
|
||
/>
|
||
|
||
<!-- 日程详情抽屉 -->
|
||
<ScheduleDetail
|
||
v-model="detailVisible"
|
||
:schedule="detailItem"
|
||
@edit="handleDetailEdit"
|
||
@finish="handleDetailFinish"
|
||
@revert="handleDetailRevert"
|
||
@postpone="handleDetailPostpone"
|
||
@changed="reloadAll"
|
||
/>
|
||
|
||
<!-- 延期对话框 -->
|
||
<SchedulePostpone
|
||
v-model="postponeVisible"
|
||
:schedule="postponeItem"
|
||
@saved="reloadAll"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||
import { ElMessage, ElMessageBox } from "element-plus";
|
||
import {
|
||
ArrowLeft,
|
||
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,
|
||
carrySchedule,
|
||
deleteSchedule,
|
||
finishSchedule,
|
||
getScheduleList,
|
||
getScheduleStats
|
||
} from "@/api/oaSchedule";
|
||
|
||
const viewMode = ref("calendar");
|
||
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 ----------
|
||
// 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");
|
||
}
|
||
|
||
function fmtDate(d) {
|
||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||
}
|
||
|
||
function todayStr() {
|
||
return fmtDate(new Date());
|
||
}
|
||
|
||
// 后端 DSN 开启 parseTime 时 DATE 字段可能返回 Go 默认格式
|
||
// ("2026-08-29 00:00:00 +0800 CST"),统一截取为 YYYY-MM-DD,
|
||
// 保证日历分组与选中日期侧栏能正确匹配。
|
||
function normDate(value) {
|
||
return typeof value === "string" && value.length >= 10
|
||
? value.slice(0, 10)
|
||
: 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 calendarItems = ref([]);
|
||
const selectedDate = ref(todayStr());
|
||
|
||
// 日历 42 格(6 行 x 7 列,周一起始)
|
||
const calendarCells = computed(() => {
|
||
const map = calendarMap.value;
|
||
const today = todayStr();
|
||
const start = calendarRangeStart();
|
||
const cells = [];
|
||
for (let i = 0; i < 42; i++) {
|
||
const d = new Date(
|
||
start.getFullYear(),
|
||
start.getMonth(),
|
||
start.getDate() + i
|
||
);
|
||
const date = fmtDate(d);
|
||
cells.push({
|
||
date,
|
||
day: d.getDate(),
|
||
month: d.getMonth() + 1,
|
||
inMonth: d.getMonth() + 1 === calendarMonth.value,
|
||
isToday: date === today,
|
||
items: map[date] || []
|
||
});
|
||
}
|
||
return cells;
|
||
});
|
||
|
||
// 当前月历网格的起始日期(含上月补齐)
|
||
function calendarRangeStart() {
|
||
const first = new Date(calendarYear.value, calendarMonth.value - 1, 1);
|
||
const offset = (first.getDay() + 6) % 7;
|
||
return new Date(calendarYear.value, calendarMonth.value - 1, 1 - offset);
|
||
}
|
||
|
||
// 按日期分组:{ 'YYYY-MM-DD': [日程...] }
|
||
const calendarMap = computed(() => {
|
||
const map = {};
|
||
for (const item of calendarItems.value) {
|
||
if (!map[item.schedule_date]) {
|
||
map[item.schedule_date] = [];
|
||
}
|
||
map[item.schedule_date].push(item);
|
||
}
|
||
// 同一天内按紧急程度排序:紧急 > 重要 > 普通
|
||
for (const date of Object.keys(map)) {
|
||
map[date].sort(compareByPriority);
|
||
}
|
||
return map;
|
||
});
|
||
|
||
const selectedItems = computed(
|
||
() => calendarMap.value[selectedDate.value] || []
|
||
);
|
||
|
||
// 当天仍未完成的日程数,用于"顺延未完成"批量操作
|
||
const pendingCount = computed(
|
||
() => selectedItems.value.filter(item => item.status !== 1).length
|
||
);
|
||
|
||
// ---------- 多选(勾选后批量延期,区别于"完成"操作) ----------
|
||
// 只记录 id,数据刷新后自动失效
|
||
const selectedIds = ref([]);
|
||
|
||
// 已勾选且仍未完成的日程,只有这些才会被延期
|
||
const selectedPendingIds = computed(() =>
|
||
selectedIds.value.filter(id => {
|
||
const item = selectedItems.value.find(i => i.id === id);
|
||
return item && item.status !== 1;
|
||
})
|
||
);
|
||
|
||
function toggleSelect(id, checked) {
|
||
const set = new Set(selectedIds.value);
|
||
if (checked) {
|
||
set.add(id);
|
||
} else {
|
||
set.delete(id);
|
||
}
|
||
selectedIds.value = [...set];
|
||
}
|
||
|
||
function clearSelection() {
|
||
selectedIds.value = [];
|
||
}
|
||
|
||
const selectedDateLabel = computed(() => {
|
||
const [y, m, d] = selectedDate.value.split("-").map(Number);
|
||
const week = "日一二三四五六"[new Date(y, m - 1, d).getDay()];
|
||
return `${m}月${d}日 星期${week}`;
|
||
});
|
||
|
||
async function loadCalendarData() {
|
||
// 拉取整个 42 格区间的数据,一次覆盖前后补齐的日期
|
||
const start = calendarRangeStart();
|
||
const end = new Date(
|
||
start.getFullYear(),
|
||
start.getMonth(),
|
||
start.getDate() + 41
|
||
);
|
||
const res = await getScheduleList({
|
||
start_date: fmtDate(start),
|
||
end_date: fmtDate(end),
|
||
page: 1,
|
||
pageSize: 500,
|
||
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)
|
||
}));
|
||
// 数据刷新后旧的勾选已失效(id 可能已因延期重新生成),统一清空
|
||
clearSelection();
|
||
}
|
||
|
||
function changeMonth(delta) {
|
||
const d = new Date(calendarYear.value, calendarMonth.value - 1 + delta, 1);
|
||
calendarYear.value = d.getFullYear();
|
||
calendarMonth.value = d.getMonth() + 1;
|
||
loadCalendarData();
|
||
}
|
||
|
||
function goToday() {
|
||
const d = new Date();
|
||
calendarYear.value = d.getFullYear();
|
||
calendarMonth.value = d.getMonth() + 1;
|
||
selectedDate.value = todayStr();
|
||
loadCalendarData();
|
||
}
|
||
|
||
// ---------- 统计 ----------
|
||
async function loadStats() {
|
||
const res = await getScheduleStats();
|
||
if (res?.code === 200) {
|
||
stats.value = res.data || {};
|
||
}
|
||
}
|
||
|
||
// ---------- 列表视图 ----------
|
||
const listFilters = reactive({ keyword: "", range: null });
|
||
const listData = ref([]);
|
||
const listLoading = ref(false);
|
||
const listPagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||
|
||
async function loadListData() {
|
||
listLoading.value = true;
|
||
try {
|
||
const params = {
|
||
page: listPagination.page,
|
||
pageSize: listPagination.pageSize,
|
||
keyword: listFilters.keyword,
|
||
category: category.value
|
||
};
|
||
if (listFilters.range && listFilters.range.length === 2) {
|
||
params.start_date = listFilters.range[0];
|
||
params.end_date = listFilters.range[1];
|
||
}
|
||
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)
|
||
}));
|
||
listPagination.total = data.total || 0;
|
||
} finally {
|
||
listLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function resetListFilters() {
|
||
listFilters.keyword = "";
|
||
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) {
|
||
loadListData();
|
||
}
|
||
});
|
||
|
||
// ---------- 详情/新增/编辑/操作 ----------
|
||
const editVisible = ref(false);
|
||
const editingItem = ref(null);
|
||
const detailVisible = ref(false);
|
||
const detailItem = ref(null);
|
||
// 从通知设置页返回时恢复的草稿(含提醒设置)
|
||
const pendingDraft = ref(null);
|
||
|
||
function openCreate(date) {
|
||
editingItem.value = null;
|
||
if (date) {
|
||
selectedDate.value = date;
|
||
}
|
||
editVisible.value = true;
|
||
}
|
||
|
||
function openEdit(item) {
|
||
editingItem.value = item;
|
||
editVisible.value = true;
|
||
}
|
||
|
||
function openDetail(item) {
|
||
detailItem.value = item;
|
||
detailVisible.value = true;
|
||
}
|
||
|
||
// 详情里点编辑:关闭详情后打开编辑抽屉
|
||
function handleDetailEdit(item) {
|
||
detailVisible.value = false;
|
||
openEdit(item);
|
||
}
|
||
|
||
// 详情里点"已完成"
|
||
async function handleDetailFinish(item) {
|
||
detailVisible.value = false;
|
||
await toggleFinish(item);
|
||
}
|
||
|
||
// 详情里点"反审核":把已完成的日程恢复为待办
|
||
async function handleDetailRevert(item) {
|
||
detailVisible.value = false;
|
||
await toggleFinish(item);
|
||
}
|
||
|
||
// 详情里点"延期":关闭详情后打开延期对话框
|
||
function handleDetailPostpone(item) {
|
||
detailVisible.value = false;
|
||
openPostpone(item);
|
||
}
|
||
|
||
function handleItemCommand(cmd, item) {
|
||
if (cmd === "postpone") {
|
||
openPostpone(item);
|
||
} else if (cmd === "finish") {
|
||
toggleFinish(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() {
|
||
const ids = selectedPendingIds.value;
|
||
const count = ids.length || pendingCount.value;
|
||
if (count === 0) {
|
||
return;
|
||
}
|
||
// 目标日期取"选定日期+1天",但不早于今天,避免把过去的任务再延到过去
|
||
const nextDay = addDays(selectedDate.value, 1);
|
||
const target = nextDay < todayStr() ? todayStr() : nextDay;
|
||
const tip = ids.length
|
||
? `将选中的 ${count} 条日程延期到 ${target}?当天这些日程会标记为已完成。`
|
||
: `将 ${selectedDate.value} 的 ${count} 条未完成日程延期到 ${target}?当天这些日程会标记为已完成。`;
|
||
try {
|
||
await ElMessageBox.confirm(tip, "批量延期", {
|
||
type: "warning",
|
||
confirmButtonText: "延期",
|
||
cancelButtonText: "取消"
|
||
});
|
||
} catch {
|
||
return;
|
||
}
|
||
|
||
// 有勾选:逐条调用单条延期接口(后端批量接口只支持按日期整批延期)
|
||
if (ids.length) {
|
||
let success = 0;
|
||
for (const id of ids) {
|
||
const res = await carrySchedule(id, { target_date: target });
|
||
if (res?.code === 200) {
|
||
success++;
|
||
}
|
||
}
|
||
const skipped = ids.length - success;
|
||
if (success) {
|
||
ElMessage.success(
|
||
`已延期 ${success} 条${skipped ? `,跳过 ${skipped} 条` : ""}`
|
||
);
|
||
} else {
|
||
ElMessage.warning("所选日程均无法延期");
|
||
}
|
||
await reloadAll();
|
||
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) {
|
||
ElMessage.success(res.data?.status === 1 ? "已完成" : "已恢复待办");
|
||
return reloadAll();
|
||
} else {
|
||
ElMessage.error(res?.msg || "操作失败");
|
||
}
|
||
}
|
||
|
||
async function handleDelete(item) {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定删除日程「${item.title}」吗?`,
|
||
"删除确认",
|
||
{ type: "warning", confirmButtonText: "删除", cancelButtonText: "取消" }
|
||
);
|
||
} catch {
|
||
return;
|
||
}
|
||
const res = await deleteSchedule(item.id);
|
||
if (res?.code === 200) {
|
||
ElMessage.success("删除成功");
|
||
reloadAll();
|
||
} else {
|
||
ElMessage.error(res?.msg || "删除失败");
|
||
}
|
||
}
|
||
|
||
function reloadAll() {
|
||
const tasks = [loadStats(), loadCalendarData()];
|
||
if (viewMode.value === "list") {
|
||
tasks.push(loadListData());
|
||
}
|
||
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日,用于"已顺延至"标签
|
||
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 "全天";
|
||
}
|
||
return item.end_time
|
||
? `${item.start_time} - ${item.end_time}`
|
||
: item.start_time;
|
||
}
|
||
|
||
function priorityLabel(priority) {
|
||
return { 1: "重要", 2: "紧急" }[priority] || "普通";
|
||
}
|
||
|
||
function priorityTagType(priority) {
|
||
return { 1: "warning", 2: "danger" }[priority] || "info";
|
||
}
|
||
|
||
// 判断日程是否关联了图片(与详情页 parseImages 语义一致),用于列表标题前展示图片图标
|
||
function hasImages(item) {
|
||
const imgs = item?.images;
|
||
if (!imgs) return false;
|
||
if (Array.isArray(imgs)) return imgs.some(img => img && img.url);
|
||
if (typeof imgs === "string" && imgs.trim()) {
|
||
try {
|
||
const parsed = JSON.parse(imgs);
|
||
return Array.isArray(parsed) && parsed.some(img => img && img.url);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 排序权重:紧急(2) > 重要(1) > 普通(0)
|
||
function priorityWeight(priority) {
|
||
return { 2: 3, 1: 2 }[priority] || 1;
|
||
}
|
||
|
||
// 同天内排序:先按紧急程度降序,再按开始时间升序(全天排在最前),最后按 id
|
||
function compareByPriority(a, b) {
|
||
const diff = priorityWeight(b.priority) - priorityWeight(a.priority);
|
||
if (diff !== 0) {
|
||
return diff;
|
||
}
|
||
const ta = a.all_day === 1 ? "00:00" : a.start_time || "";
|
||
const tb = b.all_day === 1 ? "00:00" : b.start_time || "";
|
||
if (ta !== tb) {
|
||
return ta < tb ? -1 : 1;
|
||
}
|
||
return (a.id || 0) - (b.id || 0);
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadStats();
|
||
loadCalendarData();
|
||
// 从通知设置页返回:恢复上次暂存的新建日程草稿
|
||
const raw = sessionStorage.getItem("oa_schedule_draft");
|
||
if (raw) {
|
||
try {
|
||
pendingDraft.value = JSON.parse(raw);
|
||
sessionStorage.removeItem("oa_schedule_draft");
|
||
editingItem.value = null;
|
||
editVisible.value = true;
|
||
ElMessage.info("已从暂存恢复未提交的新建日程");
|
||
} catch {
|
||
sessionStorage.removeItem("oa_schedule_draft");
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.schedule-page {
|
||
padding: 16px 20px 24px;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.page-header h2 {
|
||
margin: 0 0 4px;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.page-header p {
|
||
margin: 0;
|
||
color: #909399;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 统计卡片 */
|
||
.stats-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: #fff;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
padding: 14px 18px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 26px;
|
||
font-weight: 600;
|
||
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 {
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.stat-sub {
|
||
font-size: 12px;
|
||
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;
|
||
gap: 16px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.calendar-card {
|
||
flex: 1;
|
||
min-width: 0;
|
||
background: #fff;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.calendar-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.month-title {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
min-width: 120px;
|
||
text-align: center;
|
||
}
|
||
|
||
.calendar-week-header {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.week-name {
|
||
padding: 8px 0;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
.calendar-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
grid-auto-rows: minmax(96px, auto);
|
||
}
|
||
|
||
.calendar-cell {
|
||
border-right: 1px solid #f2f4f8;
|
||
border-bottom: 1px solid #f2f4f8;
|
||
padding: 6px;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.calendar-cell:nth-child(7n) {
|
||
border-right: none;
|
||
}
|
||
|
||
.calendar-cell:hover {
|
||
background: #f5f8ff;
|
||
}
|
||
|
||
.calendar-cell.other-month {
|
||
color: #c0c4cc;
|
||
background: #fafbfc;
|
||
}
|
||
|
||
.calendar-cell.selected {
|
||
background: #ecf5ff;
|
||
box-shadow: inset 0 0 0 2px #409eff;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.cell-date {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.day-num {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.calendar-cell.today .day-num {
|
||
background: #409eff;
|
||
color: #fff;
|
||
border-radius: 50%;
|
||
width: 22px;
|
||
height: 22px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.today-tag,
|
||
.month-tag {
|
||
font-size: 11px;
|
||
color: #409eff;
|
||
}
|
||
|
||
.cell-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.cell-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 1px 4px 1px 6px;
|
||
border-left: 3px solid var(--item-color);
|
||
background: #f5f7fa;
|
||
border-radius: 3px;
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.cell-item:hover {
|
||
background: #ebeef5;
|
||
}
|
||
|
||
.item-time {
|
||
color: #909399;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.item-title {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.item-image-icon {
|
||
flex-shrink: 0;
|
||
margin-right: 4px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.item-bell {
|
||
flex-shrink: 0;
|
||
margin-right: 4px;
|
||
color: #e6a23c;
|
||
}
|
||
|
||
.item-title.done {
|
||
text-decoration: line-through;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.cell-more {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
padding-left: 6px;
|
||
}
|
||
|
||
.cell-more:hover {
|
||
color: #409eff;
|
||
}
|
||
|
||
/* 选中日期侧栏 */
|
||
.day-panel {
|
||
width: 400px;
|
||
flex-shrink: 0;
|
||
background: #fff;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.day-panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #f2f4f8;
|
||
}
|
||
|
||
.day-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.day-sub {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.panel-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.day-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.day-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 10px 8px;
|
||
border-left: 3px solid var(--item-color);
|
||
border-radius: 4px;
|
||
background: #fafbfc;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.day-item-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.day-item-title {
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.day-item-title.done {
|
||
text-decoration: line-through;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.day-item-time {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.day-item-more {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 列表视图 */
|
||
.table-card {
|
||
background: #fff;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.filter-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.list-title {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.list-title.done {
|
||
text-decoration: line-through;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.color-chip {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 3px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
/* 响应式 */
|
||
@media (max-width: 1200px) {
|
||
.calendar-layout {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.day-panel {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.stats-row {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.calendar-grid {
|
||
grid-auto-rows: minmax(64px, auto);
|
||
}
|
||
|
||
.cell-items {
|
||
display: none;
|
||
}
|
||
|
||
.calendar-cell {
|
||
position: relative;
|
||
}
|
||
|
||
.calendar-cell.has-items::after {
|
||
content: "";
|
||
position: absolute;
|
||
bottom: 6px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: #409eff;
|
||
}
|
||
}
|
||
// ===== 暗色主题适配 =====
|
||
html.dark & {
|
||
.page-header p {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.stat-card {
|
||
background: var(--el-bg-color);
|
||
border-color: var(--el-border-color-lighter);
|
||
|
||
.stat-label {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.stat-sub {
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
}
|
||
|
||
.category-tabs {
|
||
background: var(--el-bg-color);
|
||
border-color: var(--el-border-color-lighter);
|
||
}
|
||
.tab-count {
|
||
background: var(--el-fill-color);
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.calendar-card {
|
||
background: var(--el-bg-color);
|
||
border-color: var(--el-border-color-lighter);
|
||
}
|
||
.calendar-week-header {
|
||
border-bottom-color: var(--el-border-color-lighter);
|
||
}
|
||
.week-name {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.calendar-cell {
|
||
border-right-color: var(--el-border-color-lighter);
|
||
border-bottom-color: var(--el-border-color-lighter);
|
||
|
||
&:hover {
|
||
background: var(--el-fill-color-light);
|
||
}
|
||
&.other-month {
|
||
color: var(--el-text-color-placeholder);
|
||
background: var(--el-fill-color-light);
|
||
}
|
||
&.selected {
|
||
background: var(--el-color-primary-light-9);
|
||
box-shadow: inset 0 0 0 2px var(--el-color-primary);
|
||
}
|
||
}
|
||
.today-tag,
|
||
.month-tag {
|
||
color: var(--el-color-primary);
|
||
}
|
||
.cell-item {
|
||
background: var(--el-fill-color-light);
|
||
|
||
&:hover {
|
||
background: var(--el-fill-color);
|
||
}
|
||
}
|
||
.item-time {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.item-title.done {
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
.cell-more {
|
||
color: var(--el-text-color-secondary);
|
||
|
||
&:hover {
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
|
||
.day-panel {
|
||
background: var(--el-bg-color);
|
||
border-color: var(--el-border-color-lighter);
|
||
}
|
||
.day-panel-header {
|
||
border-bottom-color: var(--el-border-color-lighter);
|
||
}
|
||
.day-sub {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.day-item {
|
||
background: var(--el-fill-color-light);
|
||
}
|
||
.day-item-title.done {
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
.day-item-time {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.table-card {
|
||
background: var(--el-bg-color);
|
||
border-color: var(--el-border-color-lighter);
|
||
}
|
||
.list-title.done {
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
}
|
||
</style>
|