校历修改
This commit is contained in:
1 parent
3450d882cf
commit
f106973d62
2 files changed
+873
-31
No files matched your search
@@ -65,7 +65,6 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
|||||||
{
|
{
|
||||||
key: 'overview',
|
key: 'overview',
|
||||||
label: '教务总览',
|
label: '教务总览',
|
||||||
direct: true,
|
|
||||||
items: [
|
items: [
|
||||||
{ path: '/dashboard', label: '教务总览' },
|
{ path: '/dashboard', label: '教务总览' },
|
||||||
{ path: '/academic-calendar', label: '学期校历与日程' },
|
{ path: '/academic-calendar', label: '学期校历与日程' },
|
||||||
|
|||||||
@@ -1,34 +1,877 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { ArrowLeft, ArrowRight, Plus } from '@element-plus/icons-vue'
|
import { ArrowLeft, ArrowRight, Plus } from "@element-plus/icons-vue";
|
||||||
import http from '../api/http'
|
import http from "../api/http";
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from "../stores/auth";
|
||||||
import { academicTermLabel, defaultAcademicTermId } from '../utils/academicTerms'
|
import {
|
||||||
interface CalendarEvent { id: string; academicTermId: string; title: string; kind: number | string; startsOn: string; endsOn?: string; reminderDays: number; audienceRoles: string[]; description?: string; isEnabled: boolean }
|
academicTermLabel,
|
||||||
const auth = useAuthStore(), terms = ref<any[]>([]), events = ref<CalendarEvent[]>([]), selectedTermId = ref(''), loading = ref(false), dialogVisible = ref(false), editingId = ref<string>(), cursor = ref(new Date(new Date().getFullYear(), new Date().getMonth(), 1))
|
defaultAcademicTermId,
|
||||||
const canManage = computed(() => auth.user?.roles.some(role => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false)
|
} from "../utils/academicTerms";
|
||||||
const isStudent = computed(() => auth.user?.roles.includes('Student') ?? false)
|
interface CalendarEvent {
|
||||||
const roles = [['SuperAdmin','超级管理员'],['AcademicAdmin','校级教务管理员'],['CollegeAdmin','学院教务管理员'],['Leader','领导'],['Counselor','辅导员'],['Teacher','教师'],['Student','学生'],['MaintenanceAdmin','维修管理员'],['MaintenanceStaff','维修人员']]
|
id: string;
|
||||||
const kinds = [[1,'选课'],[2,'成绩'],[3,'评教'],[4,'考试'],[5,'毕业审核'],[6,'法定节假日'],[99,'其他']], weekdays = ['一','二','三','四','五','六','日']
|
academicTermId: string;
|
||||||
const form = reactive<any>({ academicTermId:'', title:'', kind:99, startsOn:'', endsOn:'', reminderDays:7, audienceRoles:[], description:'', isEnabled:true })
|
title: string;
|
||||||
function key(date: Date) { return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}` }
|
kind: number | string;
|
||||||
function parse(value: string) { const [y,m,d] = value.slice(0,10).split('-').map(Number); return new Date(y,m-1,d) }
|
startsOn: string;
|
||||||
function kindKey(kind: number | string) { return ({'1':'course',courseselection:'course','2':'grade',grade:'grade','3':'evaluation',evaluation:'evaluation','4':'exam',exam:'exam','5':'graduation',graduationaudit:'graduation','6':'holiday',publicholiday:'holiday','99':'other',other:'other'} as Record<string,string>)[String(kind).toLowerCase()] ?? 'other' }
|
endsOn?: string;
|
||||||
function label(kind: number | string) { return ({course:'选课',grade:'成绩',evaluation:'评教',exam:'考试',graduation:'毕业审核',holiday:'法定节假日',other:'其他'} as Record<string,string>)[kindKey(kind)] }
|
reminderDays: number;
|
||||||
function spans(item: CalendarEvent, day: string) { return item.startsOn <= day && (item.endsOn ?? item.startsOn) >= day }
|
audienceRoles: string[];
|
||||||
const today = key(new Date()), monthTitle = computed(() => `${cursor.value.getFullYear()} 年 ${cursor.value.getMonth()+1} 月`)
|
description?: string;
|
||||||
const days = computed(() => { const first = new Date(cursor.value.getFullYear(),cursor.value.getMonth(),1), start = new Date(first); start.setDate(first.getDate()-((first.getDay()+6)%7)); return Array.from({length:42},(_,i) => { const date = new Date(start); date.setDate(start.getDate()+i); const day = key(date); return { date, day, inMonth:date.getMonth()===cursor.value.getMonth(), events:events.value.filter(item=>spans(item,day)) } }) })
|
isEnabled: boolean;
|
||||||
const monthEvents = computed(() => events.value.filter(item => { const date=parse(item.startsOn); return date.getFullYear()===cursor.value.getFullYear() && date.getMonth()===cursor.value.getMonth() }))
|
}
|
||||||
function shift(amount:number) { cursor.value = new Date(cursor.value.getFullYear(),cursor.value.getMonth()+amount,1) } function goToday() { cursor.value = new Date(new Date().getFullYear(),new Date().getMonth(),1) }
|
interface HolidayPreview {
|
||||||
function due(item:CalendarEvent) { const gap=Math.ceil((parse(item.startsOn).getTime()-parse(today).getTime())/86400000); return gap>=0 && gap<=item.reminderDays } function audience(item:CalendarEvent) { return item.audienceRoles.length ? item.audienceRoles.map(role=>roles.find(x=>x[0]===role)?.[1]??role).join('、') : '全部角色' }
|
name: string;
|
||||||
async function load() { loading.value=true; try { const calendarRequest=http.get(canManage.value?'/academic-calendar/manage':'/academic-calendar',{params:selectedTermId.value?{academicTermId:selectedTermId.value}:undefined}); const examRequest=isStudent.value?http.get('/exams/my-schedule'):Promise.resolve({data:[]}); const [calendar,exams]=await Promise.all([calendarRequest,examRequest]); const examEvents=(exams.data as any[]).map(exam=>({id:`exam-${exam.id}`,academicTermId:selectedTermId.value,title:`考试:${exam.courseName}`,kind:'Exam',startsOn:exam.examDate,reminderDays:7,audienceRoles:['Student'],description:`${exam.startsAt?.slice(11,16)??''}—${exam.endsAt?.slice(11,16)??''} · ${[exam.buildingName,exam.classroomName].filter(Boolean).join(' ')}`,isEnabled:true,source:'ExamSchedule'})); events.value=[...calendar.data,...examEvents] } finally { loading.value=false } }
|
date: string;
|
||||||
function open(item?:CalendarEvent) { editingId.value=item?.id; Object.assign(form,item?{...item,endsOn:item.endsOn??''}:{academicTermId:selectedTermId.value||defaultAcademicTermId(terms.value)||'',title:'',kind:99,startsOn:today,endsOn:'',reminderDays:7,audienceRoles:[],description:'',isEnabled:true}); dialogVisible.value=true }
|
isOffDay: boolean;
|
||||||
async function save() { if(!form.academicTermId||!form.title.trim()||!form.startsOn)return ElMessage.warning('请填写学期、日程名称和开始日期。'); const body={...form,title:form.title.trim(),endsOn:form.endsOn||null,description:form.description?.trim()||null}; if(editingId.value)await http.put(`/academic-calendar/${editingId.value}`,body);else await http.post('/academic-calendar',body); ElMessage.success('校历日程已保存;它只用于展示和提醒。'); dialogVisible.value=false; await load() }
|
}
|
||||||
async function remove(item:CalendarEvent) { await ElMessageBox.confirm(`删除“${item.title}”吗?`,'删除日程',{type:'warning'});await http.delete(`/academic-calendar/${item.id}`);ElMessage.success('日程已删除');await load() }
|
const auth = useAuthStore(),
|
||||||
onMounted(async()=>{terms.value=(await http.get('/base-data/terms')).data;selectedTermId.value=defaultAcademicTermId(terms.value)??'';await load()})
|
terms = ref<any[]>([]),
|
||||||
|
events = ref<CalendarEvent[]>([]),
|
||||||
|
selectedTermId = ref(""),
|
||||||
|
loading = ref(false),
|
||||||
|
dialogVisible = ref(false),
|
||||||
|
holidayDialogVisible = ref(false),
|
||||||
|
holidayLoading = ref(false),
|
||||||
|
holidayDays = ref<HolidayPreview[]>([]),
|
||||||
|
holidayYear = ref(new Date().getFullYear()),
|
||||||
|
editingId = ref<string>(),
|
||||||
|
cursor = ref(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
|
||||||
|
const canManage = computed(
|
||||||
|
() =>
|
||||||
|
auth.user?.roles.some((role) =>
|
||||||
|
["SuperAdmin", "AcademicAdmin"].includes(role),
|
||||||
|
) ?? false,
|
||||||
|
);
|
||||||
|
const isStudent = computed(() => auth.user?.roles.includes("Student") ?? false);
|
||||||
|
const roles = [
|
||||||
|
["SuperAdmin", "超级管理员"],
|
||||||
|
["AcademicAdmin", "校级教务管理员"],
|
||||||
|
["CollegeAdmin", "学院教务管理员"],
|
||||||
|
["Leader", "领导"],
|
||||||
|
["Counselor", "辅导员"],
|
||||||
|
["Teacher", "教师"],
|
||||||
|
["Student", "学生"],
|
||||||
|
["MaintenanceAdmin", "维修管理员"],
|
||||||
|
["MaintenanceStaff", "维修人员"],
|
||||||
|
];
|
||||||
|
const kinds = [
|
||||||
|
[1, "选课"],
|
||||||
|
[2, "成绩"],
|
||||||
|
[3, "评教"],
|
||||||
|
[4, "考试"],
|
||||||
|
[5, "毕业审核"],
|
||||||
|
[6, "法定节假日"],
|
||||||
|
[99, "其他"],
|
||||||
|
],
|
||||||
|
weekdays = ["一", "二", "三", "四", "五", "六", "日"];
|
||||||
|
const form = reactive<any>({
|
||||||
|
academicTermId: "",
|
||||||
|
title: "",
|
||||||
|
kind: 99,
|
||||||
|
startsOn: "",
|
||||||
|
endsOn: "",
|
||||||
|
reminderDays: 7,
|
||||||
|
audienceRoles: [],
|
||||||
|
description: "",
|
||||||
|
isEnabled: true,
|
||||||
|
});
|
||||||
|
function key(date: Date) {
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
function parse(value: string) {
|
||||||
|
const [y, m, d] = value.slice(0, 10).split("-").map(Number);
|
||||||
|
return new Date(y, m - 1, d);
|
||||||
|
}
|
||||||
|
function kindKey(kind: number | string) {
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"1": "course",
|
||||||
|
courseselection: "course",
|
||||||
|
"2": "grade",
|
||||||
|
grade: "grade",
|
||||||
|
"3": "evaluation",
|
||||||
|
evaluation: "evaluation",
|
||||||
|
"4": "exam",
|
||||||
|
exam: "exam",
|
||||||
|
"5": "graduation",
|
||||||
|
graduationaudit: "graduation",
|
||||||
|
"6": "holiday",
|
||||||
|
publicholiday: "holiday",
|
||||||
|
"99": "other",
|
||||||
|
other: "other",
|
||||||
|
} as Record<string, string>
|
||||||
|
)[String(kind).toLowerCase()] ?? "other"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function label(kind: number | string) {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
course: "选课",
|
||||||
|
grade: "成绩",
|
||||||
|
evaluation: "评教",
|
||||||
|
exam: "考试",
|
||||||
|
graduation: "毕业审核",
|
||||||
|
holiday: "法定节假日",
|
||||||
|
other: "其他",
|
||||||
|
} as Record<string, string>
|
||||||
|
)[kindKey(kind)];
|
||||||
|
}
|
||||||
|
function spans(item: CalendarEvent, day: string) {
|
||||||
|
return item.startsOn <= day && (item.endsOn ?? item.startsOn) >= day;
|
||||||
|
}
|
||||||
|
const today = key(new Date()),
|
||||||
|
monthTitle = computed(
|
||||||
|
() => `${cursor.value.getFullYear()} 年 ${cursor.value.getMonth() + 1} 月`,
|
||||||
|
);
|
||||||
|
const days = computed(() => {
|
||||||
|
const first = new Date(
|
||||||
|
cursor.value.getFullYear(),
|
||||||
|
cursor.value.getMonth(),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
start = new Date(first);
|
||||||
|
start.setDate(first.getDate() - ((first.getDay() + 6) % 7));
|
||||||
|
return Array.from({ length: 42 }, (_, i) => {
|
||||||
|
const date = new Date(start);
|
||||||
|
date.setDate(start.getDate() + i);
|
||||||
|
const day = key(date);
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
day,
|
||||||
|
inMonth: date.getMonth() === cursor.value.getMonth(),
|
||||||
|
events: events.value.filter((item) => spans(item, day)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const monthEvents = computed(() =>
|
||||||
|
events.value.filter((item) => {
|
||||||
|
const date = parse(item.startsOn);
|
||||||
|
return (
|
||||||
|
date.getFullYear() === cursor.value.getFullYear() &&
|
||||||
|
date.getMonth() === cursor.value.getMonth()
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
function shift(amount: number) {
|
||||||
|
cursor.value = new Date(
|
||||||
|
cursor.value.getFullYear(),
|
||||||
|
cursor.value.getMonth() + amount,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function goToday() {
|
||||||
|
cursor.value = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
|
||||||
|
}
|
||||||
|
function due(item: CalendarEvent) {
|
||||||
|
const gap = Math.ceil(
|
||||||
|
(parse(item.startsOn).getTime() - parse(today).getTime()) / 86400000,
|
||||||
|
);
|
||||||
|
return gap >= 0 && gap <= item.reminderDays;
|
||||||
|
}
|
||||||
|
function audience(item: CalendarEvent) {
|
||||||
|
return item.audienceRoles.length
|
||||||
|
? item.audienceRoles
|
||||||
|
.map((role) => roles.find((x) => x[0] === role)?.[1] ?? role)
|
||||||
|
.join("、")
|
||||||
|
: "全部角色";
|
||||||
|
}
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const calendarRequest = http.get(
|
||||||
|
canManage.value ? "/academic-calendar/manage" : "/academic-calendar",
|
||||||
|
{
|
||||||
|
params: selectedTermId.value
|
||||||
|
? { academicTermId: selectedTermId.value }
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const examRequest = isStudent.value
|
||||||
|
? http.get("/exams/my-schedule")
|
||||||
|
: Promise.resolve({ data: [] });
|
||||||
|
const [calendar, exams] = await Promise.all([calendarRequest, examRequest]);
|
||||||
|
const examEvents = (exams.data as any[]).map((exam) => ({
|
||||||
|
id: `exam-${exam.id}`,
|
||||||
|
academicTermId: selectedTermId.value,
|
||||||
|
title: `考试:${exam.courseName}`,
|
||||||
|
kind: "Exam",
|
||||||
|
startsOn: exam.examDate,
|
||||||
|
reminderDays: 7,
|
||||||
|
audienceRoles: ["Student"],
|
||||||
|
description: `${exam.startsAt?.slice(11, 16) ?? ""}—${exam.endsAt?.slice(11, 16) ?? ""} · ${[exam.buildingName, exam.classroomName].filter(Boolean).join(" ")}`,
|
||||||
|
isEnabled: true,
|
||||||
|
source: "ExamSchedule",
|
||||||
|
}));
|
||||||
|
events.value = [...calendar.data, ...examEvents];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function previewHolidays() {
|
||||||
|
holidayLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await http.get(
|
||||||
|
`/academic-calendar/holidays/${holidayYear.value}`,
|
||||||
|
);
|
||||||
|
holidayDays.value = data.days;
|
||||||
|
} finally {
|
||||||
|
holidayLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function openHolidaySync() {
|
||||||
|
holidayDialogVisible.value = true;
|
||||||
|
await previewHolidays();
|
||||||
|
}
|
||||||
|
async function syncHolidays() {
|
||||||
|
if (!selectedTermId.value)
|
||||||
|
return ElMessage.warning("请先选择要写入节假日的学期。");
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`将同步 ${holidayYear.value} 年节假日,并替换该学期已同步的同年度数据。`,
|
||||||
|
"同步法定节假日",
|
||||||
|
{ type: "warning", confirmButtonText: "确认同步" },
|
||||||
|
);
|
||||||
|
holidayLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await http.put("/academic-calendar/holidays/sync", {
|
||||||
|
academicTermId: selectedTermId.value,
|
||||||
|
year: holidayYear.value,
|
||||||
|
});
|
||||||
|
ElMessage.success(`已同步 ${data.syncedCount} 条节假日与调休数据。`);
|
||||||
|
holidayDialogVisible.value = false;
|
||||||
|
await load();
|
||||||
|
} finally {
|
||||||
|
holidayLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function open(item?: CalendarEvent) {
|
||||||
|
editingId.value = item?.id;
|
||||||
|
Object.assign(
|
||||||
|
form,
|
||||||
|
item
|
||||||
|
? { ...item, endsOn: item.endsOn ?? "" }
|
||||||
|
: {
|
||||||
|
academicTermId:
|
||||||
|
selectedTermId.value || defaultAcademicTermId(terms.value) || "",
|
||||||
|
title: "",
|
||||||
|
kind: 99,
|
||||||
|
startsOn: today,
|
||||||
|
endsOn: "",
|
||||||
|
reminderDays: 7,
|
||||||
|
audienceRoles: [],
|
||||||
|
description: "",
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
async function save() {
|
||||||
|
if (!form.academicTermId || !form.title.trim() || !form.startsOn)
|
||||||
|
return ElMessage.warning("请填写学期、日程名称和开始日期。");
|
||||||
|
const body = {
|
||||||
|
...form,
|
||||||
|
title: form.title.trim(),
|
||||||
|
endsOn: form.endsOn || null,
|
||||||
|
description: form.description?.trim() || null,
|
||||||
|
};
|
||||||
|
if (editingId.value)
|
||||||
|
await http.put(`/academic-calendar/${editingId.value}`, body);
|
||||||
|
else await http.post("/academic-calendar", body);
|
||||||
|
ElMessage.success("校历日程已保存;它只用于展示和提醒。");
|
||||||
|
dialogVisible.value = false;
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
async function remove(item: CalendarEvent) {
|
||||||
|
await ElMessageBox.confirm(`删除“${item.title}”吗?`, "删除日程", {
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
await http.delete(`/academic-calendar/${item.id}`);
|
||||||
|
ElMessage.success("日程已删除");
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
onMounted(async () => {
|
||||||
|
terms.value = (await http.get("/base-data/terms")).data;
|
||||||
|
selectedTermId.value = defaultAcademicTermId(terms.value) ?? "";
|
||||||
|
await load();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template><section class="calendar-page"><header class="calendar-masthead"><div><p>ACADEMIC ALMANAC</p><h1>学期校历</h1><span>教学日程、法定节假日与临期提醒都落在同一张日历上;仅作提醒,不改变任何业务办理规则。</span></div><el-button v-if="canManage" type="primary" :icon="Plus" @click="open()">新增日程</el-button></header><div class="calendar-controls"><el-select v-model="selectedTermId" clearable placeholder="当前学期" @change="load"><el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id"/></el-select><div class="month-switcher"><el-button :icon="ArrowLeft" circle text @click="shift(-1)"/><strong>{{ monthTitle }}</strong><el-button text @click="goToday">今天</el-button><el-button :icon="ArrowRight" circle text @click="shift(1)"/></div><div class="calendar-legend"><span class="holiday">法定节假日</span><span>教学日程</span><span class="due">临期</span></div></div><el-alert v-if="!canManage" title="这里是校历提醒,不表示您有待办,也不会限制任何业务操作。" type="info" :closable="false" show-icon/><div v-loading="loading" class="calendar-layout"><div class="calendar-board"><div class="weekdays"><span v-for="weekday in weekdays" :key="weekday">周{{ weekday }}</span></div><div class="calendar-grid"><article v-for="day in days" :key="day.day" :class="['calendar-day',{muted:!day.inMonth,today:day.day===today,weekend:day.date.getDay()===0||day.date.getDay()===6}]"><div class="day-number">{{ day.date.getDate() }}</div><button v-for="item in day.events.slice(0,3)" :key="item.id" type="button" :class="['calendar-event',kindKey(item.kind),{due:due(item)}]" :title="item.title" @click="canManage&&open(item)"><span>{{ item.title.replace('(法定节假日)','') }}</span></button><span v-if="day.events.length>3" class="more-events">+{{ day.events.length-3 }} 项</span></article></div></div><aside class="month-agenda"><header><span>本月安排</span><strong>{{ monthEvents.length }} 项</strong></header><div v-if="monthEvents.length" class="agenda-items"><article v-for="item in monthEvents" :key="item.id" :class="['agenda-item',kindKey(item.kind)]"><time>{{ item.startsOn.slice(5).replace('-',' / ') }}<template v-if="item.endsOn&&item.endsOn!==item.startsOn">—{{ item.endsOn.slice(5).replace('-',' / ') }}</template></time><div><b>{{ item.title }}</b><p>{{ item.description||label(item.kind) }}</p><small v-if="canManage">面向:{{ audience(item) }}<template v-if="due(item)"> · 临期提醒</template></small></div><div v-if="canManage" class="agenda-actions"><el-button text @click="open(item)">编辑</el-button><el-button text type="danger" @click="remove(item)">删除</el-button></div></article></div><el-empty v-else :image-size="60" description="本月暂无日程"/></aside></div><el-dialog v-model="dialogVisible" :title="editingId?'编辑校历日程':'新增校历日程'" width="620px" destroy-on-close><el-form label-width="98px"><el-form-item label="所属学期" required><el-select v-model="form.academicTermId"><el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id"/></el-select></el-form-item><el-form-item label="日程名称" required><el-input v-model="form.title" maxlength="120"/></el-form-item><el-form-item label="日程类型"><el-select v-model="form.kind"><el-option v-for="kind in kinds" :key="kind[0]" :label="kind[1]" :value="kind[0]"/></el-select></el-form-item><el-form-item label="开始 / 结束" required><el-date-picker v-model="form.startsOn" type="date" value-format="YYYY-MM-DD"/><span class="range-separator">至</span><el-date-picker v-model="form.endsOn" type="date" value-format="YYYY-MM-DD" clearable/></el-form-item><el-form-item label="提前提醒"><el-input-number v-model="form.reminderDays" :min="0" :max="90"/> <span class="form-hint">天(只控制页面临期提示)</span></el-form-item><el-form-item label="面向角色"><el-checkbox-group v-model="form.audienceRoles"><el-checkbox v-for="role in roles" :key="role[0]" :label="role[0]">{{ role[1] }}</el-checkbox></el-checkbox-group><div class="form-hint">不选则向所有角色显示。</div></el-form-item><el-form-item label="说明"><el-input v-model="form.description" type="textarea" :rows="3" maxlength="1000" show-word-limit/></el-form-item><el-form-item label="启用"><el-switch v-model="form.isEnabled"/></el-form-item></el-form><template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" @click="save">保存</el-button></template></el-dialog></section></template>
|
<template>
|
||||||
|
<section class="calendar-page">
|
||||||
|
<header class="calendar-masthead">
|
||||||
|
<div>
|
||||||
|
<p>ACADEMIC ALMANAC</p>
|
||||||
|
<h1>学期校历</h1>
|
||||||
|
<span
|
||||||
|
>教学日程、法定节假日与临期提醒都落在同一张日历上;仅作提醒,不改变任何业务办理规则。</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-if="canManage" class="calendar-masthead-actions">
|
||||||
|
<el-button @click="openHolidaySync">同步法定节假日</el-button>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="open()"
|
||||||
|
>新增日程</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="calendar-controls">
|
||||||
|
<el-select
|
||||||
|
v-model="selectedTermId"
|
||||||
|
clearable
|
||||||
|
placeholder="当前学期"
|
||||||
|
@change="load"
|
||||||
|
><el-option
|
||||||
|
v-for="term in terms"
|
||||||
|
:key="term.id"
|
||||||
|
:label="academicTermLabel(term)"
|
||||||
|
:value="term.id"
|
||||||
|
/></el-select>
|
||||||
|
<div class="month-switcher">
|
||||||
|
<el-button :icon="ArrowLeft" circle text @click="shift(-1)" /><strong>{{
|
||||||
|
monthTitle
|
||||||
|
}}</strong
|
||||||
|
><el-button text @click="goToday">今天</el-button
|
||||||
|
><el-button :icon="ArrowRight" circle text @click="shift(1)" />
|
||||||
|
</div>
|
||||||
|
<div class="calendar-legend">
|
||||||
|
<span class="holiday">法定节假日</span><span>教学日程</span
|
||||||
|
><span class="due">临期</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-alert
|
||||||
|
v-if="!canManage"
|
||||||
|
title="这里是校历提醒,不表示您有待办,也不会限制任何业务操作。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<div v-loading="loading" class="calendar-layout">
|
||||||
|
<div class="calendar-board">
|
||||||
|
<div class="weekdays">
|
||||||
|
<span v-for="weekday in weekdays" :key="weekday"
|
||||||
|
>周{{ weekday }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-grid">
|
||||||
|
<article
|
||||||
|
v-for="day in days"
|
||||||
|
:key="day.day"
|
||||||
|
:class="[
|
||||||
|
'calendar-day',
|
||||||
|
{
|
||||||
|
muted: !day.inMonth,
|
||||||
|
today: day.day === today,
|
||||||
|
weekend: day.date.getDay() === 0 || day.date.getDay() === 6,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="day-number">{{ day.date.getDate() }}</div>
|
||||||
|
<button
|
||||||
|
v-for="item in day.events.slice(0, 3)"
|
||||||
|
:key="item.id"
|
||||||
|
type="button"
|
||||||
|
:class="[
|
||||||
|
'calendar-event',
|
||||||
|
kindKey(item.kind),
|
||||||
|
{ due: due(item) },
|
||||||
|
]"
|
||||||
|
:title="item.title"
|
||||||
|
@click="canManage && open(item)"
|
||||||
|
>
|
||||||
|
<span>{{
|
||||||
|
item.title.replace("(法定节假日)", "")
|
||||||
|
}}</span></button
|
||||||
|
><span v-if="day.events.length > 3" class="more-events"
|
||||||
|
>+{{ day.events.length - 3 }} 项</span
|
||||||
|
>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<aside class="month-agenda">
|
||||||
|
<header>
|
||||||
|
<span>本月安排</span><strong>{{ monthEvents.length }} 项</strong>
|
||||||
|
</header>
|
||||||
|
<div v-if="monthEvents.length" class="agenda-items">
|
||||||
|
<article
|
||||||
|
v-for="item in monthEvents"
|
||||||
|
:key="item.id"
|
||||||
|
:class="['agenda-item', kindKey(item.kind)]"
|
||||||
|
>
|
||||||
|
<time
|
||||||
|
>{{ item.startsOn.slice(5).replace("-", " / ")
|
||||||
|
}}<template v-if="item.endsOn && item.endsOn !== item.startsOn"
|
||||||
|
>—{{ item.endsOn.slice(5).replace("-", " / ") }}</template
|
||||||
|
></time
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<b>{{ item.title }}</b>
|
||||||
|
<p>{{ item.description || label(item.kind) }}</p>
|
||||||
|
<small v-if="canManage"
|
||||||
|
>面向:{{ audience(item)
|
||||||
|
}}<template v-if="due(item)"> · 临期提醒</template></small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-if="canManage" class="agenda-actions">
|
||||||
|
<el-button text @click="open(item)">编辑</el-button
|
||||||
|
><el-button text type="danger" @click="remove(item)"
|
||||||
|
>删除</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else :image-size="60" description="本月暂无日程" />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="editingId ? '编辑校历日程' : '新增校历日程'"
|
||||||
|
width="620px"
|
||||||
|
destroy-on-close
|
||||||
|
><el-form label-width="98px"
|
||||||
|
><el-form-item label="所属学期" required
|
||||||
|
><el-select v-model="form.academicTermId"
|
||||||
|
><el-option
|
||||||
|
v-for="term in terms"
|
||||||
|
:key="term.id"
|
||||||
|
:label="academicTermLabel(term)"
|
||||||
|
:value="term.id" /></el-select></el-form-item
|
||||||
|
><el-form-item label="日程名称" required
|
||||||
|
><el-input v-model="form.title" maxlength="120" /></el-form-item
|
||||||
|
><el-form-item label="日程类型"
|
||||||
|
><el-select v-model="form.kind"
|
||||||
|
><el-option
|
||||||
|
v-for="kind in kinds"
|
||||||
|
:key="kind[0]"
|
||||||
|
:label="kind[1]"
|
||||||
|
:value="kind[0]" /></el-select></el-form-item
|
||||||
|
><el-form-item label="开始 / 结束" required
|
||||||
|
><el-date-picker
|
||||||
|
v-model="form.startsOn"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD" /><span class="range-separator">至</span
|
||||||
|
><el-date-picker
|
||||||
|
v-model="form.endsOn"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
clearable /></el-form-item
|
||||||
|
><el-form-item label="提前提醒"
|
||||||
|
><el-input-number v-model="form.reminderDays" :min="0" :max="90" />
|
||||||
|
<span class="form-hint">天(只控制页面临期提示)</span></el-form-item
|
||||||
|
><el-form-item label="面向角色"
|
||||||
|
><el-checkbox-group v-model="form.audienceRoles"
|
||||||
|
><el-checkbox
|
||||||
|
v-for="role in roles"
|
||||||
|
:key="role[0]"
|
||||||
|
:label="role[0]"
|
||||||
|
>{{ role[1] }}</el-checkbox
|
||||||
|
></el-checkbox-group
|
||||||
|
>
|
||||||
|
<div class="form-hint">不选则向所有角色显示。</div></el-form-item
|
||||||
|
><el-form-item label="说明"
|
||||||
|
><el-input
|
||||||
|
v-model="form.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="1000"
|
||||||
|
show-word-limit /></el-form-item
|
||||||
|
><el-form-item label="启用"
|
||||||
|
><el-switch v-model="form.isEnabled" /></el-form-item></el-form
|
||||||
|
><template #footer
|
||||||
|
><el-button @click="dialogVisible = false">取消</el-button
|
||||||
|
><el-button type="primary" @click="save">保存</el-button></template
|
||||||
|
></el-dialog
|
||||||
|
><el-dialog
|
||||||
|
v-model="holidayDialogVisible"
|
||||||
|
title="同步法定节假日"
|
||||||
|
width="620px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<p class="holiday-sync-intro">
|
||||||
|
从节假日数据源读取指定年份,写入当前选择的学期。再次同步同一年会替换此前同步的数据。
|
||||||
|
</p>
|
||||||
|
<div class="holiday-sync-controls">
|
||||||
|
<el-input-number
|
||||||
|
v-model="holidayYear"
|
||||||
|
:min="2000"
|
||||||
|
:max="2100"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
<el-button :loading="holidayLoading" @click="previewHolidays"
|
||||||
|
>预览数据</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-loading="holidayLoading" class="holiday-preview">
|
||||||
|
<el-empty
|
||||||
|
v-if="!holidayDays.length"
|
||||||
|
:image-size="50"
|
||||||
|
description="点击预览以加载节假日数据"
|
||||||
|
/>
|
||||||
|
<div v-else class="holiday-preview-list">
|
||||||
|
<span
|
||||||
|
v-for="item in holidayDays"
|
||||||
|
:key="`${item.date}-${item.name}`"
|
||||||
|
:class="{ workday: !item.isOffDay }"
|
||||||
|
>
|
||||||
|
<b>{{ item.date }}</b
|
||||||
|
>{{ item.name }} · {{ item.isOffDay ? "休息" : "调休上班" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="holidayDialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="holidayLoading"
|
||||||
|
:disabled="!holidayDays.length"
|
||||||
|
@click="syncHolidays"
|
||||||
|
>同步到当前学期</el-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.calendar-page{max-width:1280px;margin:auto;padding:10px 0 38px}.calendar-masthead{display:flex;justify-content:space-between;align-items:end;gap:24px;margin:10px 0 22px}.calendar-masthead p{margin:0 0 5px;color:#2a7890;font:700 11px/1 ui-monospace,monospace;letter-spacing:.14em}.calendar-masthead h1{margin:0 0 9px;color:#183e57;font:700 32px/1.1 "STZhongsong","Songti SC",serif}.calendar-masthead span{color:#657b8d;font-size:14px}.calendar-controls{display:flex;align-items:center;gap:18px;margin:14px 0 16px;padding:11px 14px;border-top:2px solid #2b6f83;border-bottom:1px solid #d9e4e8;background:#f8fbfb}.calendar-controls>.el-select{width:238px}.month-switcher{display:flex;align-items:center;gap:8px}.month-switcher strong{min-width:112px;color:#214d63;text-align:center;font:700 18px "STZhongsong","Songti SC",serif}.calendar-legend{display:flex;gap:12px;margin-left:auto;color:#60798a;font-size:12px}.calendar-legend span{display:flex;align-items:center;gap:5px}.calendar-legend span:before{width:8px;height:8px;border-radius:50%;background:#4f91a0;content:""}.calendar-legend .holiday:before{background:#be4b4f}.calendar-legend .due:before{background:#df9c35}.calendar-layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;border:1px solid #dce6e8;background:#fff}.weekdays,.calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}.weekdays{border-bottom:1px solid #dce6e8;background:#edf4f5}.weekdays span{padding:9px;text-align:center;color:#456a78;font-size:12px;font-weight:700}.weekdays span:nth-last-child(-n+2){color:#a45a55}.calendar-day{min-height:120px;padding:8px;border-right:1px solid #e4ecee;border-bottom:1px solid #e4ecee;background:#fff}.calendar-day:nth-child(7n){border-right:0}.calendar-day.muted{background:#fafcfc}.calendar-day.weekend:not(.muted){background:#fffafa}.calendar-day.today .day-number{display:grid;place-items:center;width:23px;height:23px;border-radius:50%;background:#22687e;color:#fff}.day-number{margin:0 0 5px;color:#4a6270;font:700 13px/1 ui-monospace,monospace}.muted .day-number{color:#b1bdc3}.calendar-event{display:block;overflow:hidden;width:100%;margin:3px 0;padding:3px 5px;color:#315966;text-overflow:ellipsis;white-space:nowrap;border:0;border-left:2px solid #5d9aac;background:#edf7f8;cursor:pointer;text-align:left;font-size:11px}.calendar-event.holiday{color:#9c3439;border-left-color:#c24348;background:#fff0f0;font-weight:700}.calendar-event.grade{border-left-color:#9170b0;background:#f5f0fb}.calendar-event.exam{border-left-color:#bc783a;background:#fff5e8}.calendar-event.due{box-shadow:inset -3px 0 #df9c35}.more-events{color:#708996;font-size:11px}.month-agenda{border-left:1px solid #dce6e8;background:#f9fbfb}.month-agenda header{display:flex;justify-content:space-between;padding:16px;border-bottom:1px solid #dde8ea;color:#284e61}.month-agenda header span{font-size:13px}.month-agenda header strong{font:700 15px ui-monospace,monospace}.agenda-items{padding:5px 12px}.agenda-item{display:grid;grid-template-columns:68px 1fr;gap:9px;padding:12px 0;border-bottom:1px solid #e2eaec}.agenda-item time{color:#4f8796;font:700 11px/1.45 ui-monospace,monospace}.agenda-item b{display:block;color:#304f5f;font-size:13px}.agenda-item p{margin:4px 0;color:#718691;font-size:12px;line-height:1.5}.agenda-item small{color:#9a7740;font-size:11px}.agenda-item.holiday time{color:#b04a4d}.agenda-actions{grid-column:2;display:flex;gap:3px}.range-separator{padding:0 8px;color:#7b8794}.form-hint{color:#718096;font-size:13px}@media(max-width:980px){.calendar-layout{grid-template-columns:1fr}.month-agenda{border-top:1px solid #dce6e8;border-left:0}.agenda-items{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 15px}}@media(max-width:700px){.calendar-masthead,.calendar-controls{align-items:flex-start;flex-direction:column}.calendar-legend{margin-left:0}.calendar-day{min-height:88px;padding:5px}.calendar-event{padding:2px 3px;font-size:10px}.calendar-event span{display:none}.calendar-event:after{content:"●"}.calendar-event.holiday:after{content:"休"}.weekdays span{padding:7px 2px;font-size:11px}.agenda-items{grid-template-columns:1fr}.calendar-controls>.el-select{width:100%}}
|
.calendar-page {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px 0 38px;
|
||||||
|
}
|
||||||
|
.calendar-masthead {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: end;
|
||||||
|
gap: 24px;
|
||||||
|
margin: 10px 0 22px;
|
||||||
|
}
|
||||||
|
.calendar-masthead-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.calendar-masthead p {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
color: #2a7890;
|
||||||
|
font:
|
||||||
|
700 11px/1 ui-monospace,
|
||||||
|
monospace;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
.calendar-masthead h1 {
|
||||||
|
margin: 0 0 9px;
|
||||||
|
color: #183e57;
|
||||||
|
font:
|
||||||
|
700 32px/1.1 "STZhongsong",
|
||||||
|
"Songti SC",
|
||||||
|
serif;
|
||||||
|
}
|
||||||
|
.calendar-masthead span {
|
||||||
|
color: #657b8d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.calendar-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
margin: 14px 0 16px;
|
||||||
|
padding: 11px 14px;
|
||||||
|
border-top: 2px solid #2b6f83;
|
||||||
|
border-bottom: 1px solid #d9e4e8;
|
||||||
|
background: #f8fbfb;
|
||||||
|
}
|
||||||
|
.calendar-controls > .el-select {
|
||||||
|
width: 238px;
|
||||||
|
}
|
||||||
|
.month-switcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.month-switcher strong {
|
||||||
|
min-width: 112px;
|
||||||
|
color: #214d63;
|
||||||
|
text-align: center;
|
||||||
|
font:
|
||||||
|
700 18px "STZhongsong",
|
||||||
|
"Songti SC",
|
||||||
|
serif;
|
||||||
|
}
|
||||||
|
.calendar-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
color: #60798a;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.calendar-legend span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.calendar-legend span:before {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4f91a0;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
.calendar-legend .holiday:before {
|
||||||
|
background: #be4b4f;
|
||||||
|
}
|
||||||
|
.calendar-legend .due:before {
|
||||||
|
background: #df9c35;
|
||||||
|
}
|
||||||
|
.calendar-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 300px;
|
||||||
|
border: 1px solid #dce6e8;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.weekdays,
|
||||||
|
.calendar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
.weekdays {
|
||||||
|
border-bottom: 1px solid #dce6e8;
|
||||||
|
background: #edf4f5;
|
||||||
|
}
|
||||||
|
.weekdays span {
|
||||||
|
padding: 9px;
|
||||||
|
text-align: center;
|
||||||
|
color: #456a78;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.weekdays span:nth-last-child(-n + 2) {
|
||||||
|
color: #a45a55;
|
||||||
|
}
|
||||||
|
.calendar-day {
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 8px;
|
||||||
|
border-right: 1px solid #e4ecee;
|
||||||
|
border-bottom: 1px solid #e4ecee;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.calendar-day:nth-child(7n) {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
.calendar-day.muted {
|
||||||
|
background: #fafcfc;
|
||||||
|
}
|
||||||
|
.calendar-day.weekend:not(.muted) {
|
||||||
|
background: #fffafa;
|
||||||
|
}
|
||||||
|
.calendar-day.today .day-number {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #22687e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.day-number {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
color: #4a6270;
|
||||||
|
font:
|
||||||
|
700 13px/1 ui-monospace,
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
.muted .day-number {
|
||||||
|
color: #b1bdc3;
|
||||||
|
}
|
||||||
|
.calendar-event {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
margin: 3px 0;
|
||||||
|
padding: 3px 5px;
|
||||||
|
color: #315966;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
border-left: 2px solid #5d9aac;
|
||||||
|
background: #edf7f8;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.calendar-event.holiday {
|
||||||
|
color: #9c3439;
|
||||||
|
border-left-color: #c24348;
|
||||||
|
background: #fff0f0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.calendar-event.grade {
|
||||||
|
border-left-color: #9170b0;
|
||||||
|
background: #f5f0fb;
|
||||||
|
}
|
||||||
|
.calendar-event.exam {
|
||||||
|
border-left-color: #bc783a;
|
||||||
|
background: #fff5e8;
|
||||||
|
}
|
||||||
|
.calendar-event.due {
|
||||||
|
box-shadow: inset -3px 0 #df9c35;
|
||||||
|
}
|
||||||
|
.more-events {
|
||||||
|
color: #708996;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.month-agenda {
|
||||||
|
border-left: 1px solid #dce6e8;
|
||||||
|
background: #f9fbfb;
|
||||||
|
}
|
||||||
|
.month-agenda header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #dde8ea;
|
||||||
|
color: #284e61;
|
||||||
|
}
|
||||||
|
.month-agenda header span {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.month-agenda header strong {
|
||||||
|
font:
|
||||||
|
700 15px ui-monospace,
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
.agenda-items {
|
||||||
|
padding: 5px 12px;
|
||||||
|
}
|
||||||
|
.agenda-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 68px 1fr;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #e2eaec;
|
||||||
|
}
|
||||||
|
.agenda-item time {
|
||||||
|
color: #4f8796;
|
||||||
|
font:
|
||||||
|
700 11px/1.45 ui-monospace,
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
.agenda-item b {
|
||||||
|
display: block;
|
||||||
|
color: #304f5f;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.agenda-item p {
|
||||||
|
margin: 4px 0;
|
||||||
|
color: #718691;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.agenda-item small {
|
||||||
|
color: #9a7740;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.agenda-item.holiday time {
|
||||||
|
color: #b04a4d;
|
||||||
|
}
|
||||||
|
.agenda-actions {
|
||||||
|
grid-column: 2;
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
.range-separator {
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #7b8794;
|
||||||
|
}
|
||||||
|
.form-hint {
|
||||||
|
color: #718096;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.holiday-sync-intro {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
color: #60798a;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
.holiday-sync-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.holiday-preview {
|
||||||
|
min-height: 130px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #e1eaed;
|
||||||
|
background: #fafcfc;
|
||||||
|
}
|
||||||
|
.holiday-preview-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.holiday-preview-list span {
|
||||||
|
padding: 5px 7px;
|
||||||
|
color: #9c3439;
|
||||||
|
border-left: 2px solid #c24348;
|
||||||
|
background: #fff1f1;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.holiday-preview-list span.workday {
|
||||||
|
color: #835d20;
|
||||||
|
border-left-color: #c08831;
|
||||||
|
background: #fff7e9;
|
||||||
|
}
|
||||||
|
.holiday-preview-list b {
|
||||||
|
margin-right: 5px;
|
||||||
|
font:
|
||||||
|
700 11px ui-monospace,
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.calendar-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.month-agenda {
|
||||||
|
border-top: 1px solid #dce6e8;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
.agenda-items {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.calendar-masthead,
|
||||||
|
.calendar-controls {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.calendar-legend {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.calendar-day {
|
||||||
|
min-height: 88px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.calendar-event {
|
||||||
|
padding: 2px 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.calendar-event span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.calendar-event:after {
|
||||||
|
content: "●";
|
||||||
|
}
|
||||||
|
.calendar-event.holiday:after {
|
||||||
|
content: "休";
|
||||||
|
}
|
||||||
|
.weekdays span {
|
||||||
|
padding: 7px 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.agenda-items {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.calendar-controls > .el-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user