| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793 |
- <template>
- <view class="page">
- <!-- 导航栏 -->
- <view class="nav_bar">
- <view class="nav_back" @click="handleBack">
- <uni-icons type="back" size="26" color="#333333"></uni-icons>
- </view>
- <text class="nav_title">校历安排</text>
- <view class="nav_right"></view>
- </view>
- <!-- 日历 -->
- <view class="calendar_container">
- <view class="calendar_header">
- <view class="prev_month" @click="handlePrevMonth" style="margin-right: 40rpx;">
- <uni-icons type="left" size="16" color="#2E64FA"></uni-icons>
- </view>
- <picker mode="date" :value="selectedDate" @change="handleDateChange">
- <view class="month_select">
- <text class="current_month">{{ currentYear }}-{{ String(currentMonth).padStart(2, '0') }}</text>
- </view>
- </picker>
- <view class="next_month" @click="handleNextMonth" style="margin-left: 40rpx;">
- <uni-icons type="right" size="16" color="#2E64FA"></uni-icons>
- </view>
- </view>
- <view class="calendar_weekdays">
- <text v-for="day in weekdays" :key="day" class="weekday">{{ day }}</text>
- </view>
- <view class="calendar_days">
- <view v-for="day in calendarDays" :key="day.dateStr" class="day_item" :class="{
- 'other-month': !day.isCurrentMonth,
- 'active': day.dateStr === selectedDate,
- 'today': day.dateStr === todayStr
- }" @click="handleDayClick(day)">
- <text class="day_num">{{ day.date }}</text>
- <view class="day_dot" :class="{ visible: day.hasSchedule }"></view>
- </view>
- </view>
- </view>
- <!-- 日程 -->
- <view class="schedule_container">
- <view class="schedule_header">
- <text class="schedule_date">{{ selectedDate }}</text>
- <view class="add_btn" @click="handleAddSchedule">
- <image src="/static/image/workspace/add.png" class="add_icon" mode="aspectFit"></image>
- <text>新增日程</text>
- </view>
- </view>
- <view v-if="currentScheduleList.length === 0" class="schedule_empty">
- <image src="@/static/image/bg/no_data.png" class="empty_image" mode="aspectFit"></image>
- <text class="empty_text">暂无日程安排</text>
- </view>
- <view v-else class="schedule_list">
- <view v-for="item in currentScheduleList" :key="item.id" class="schedule_item">
- <view class="schedule_dot"></view>
- <view class="schedule_text_container">
- <text class="schedule_text" :class="{ expanded: expandedItems[item.id] }">{{ item.title }}</text>
- <text
- v-if="item.title.length > 30"
- class="toggle_text"
- @click.stop="toggleExpand(item.id)"
- >{{ expandedItems[item.id] ? '收起' : '更多' }}</text>
- </view>
- <view class="schedule_actions">
- <text class="action_btn delete" @click="handleDelete(item.id)">删除</text>
- <text class="action_btn edit" @click="handleEdit(item)">编辑</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 新增日程弹窗 -->
- <view v-if="showAddModal" class="modal-overlay" @click="handleCloseModal">
- <view class="modal-content" @click.stop>
- <view class="modal-header">
- <text class="modal-title">{{ editItem ? '编辑日程' : '新增日程' }}</text>
- <view class="modal-close" @click="handleCloseModal">
- <image src="/static/image/workspace/close.png" class="close-icon" mode="aspectFit"></image>
- </view>
- </view>
- <view class="modal-body">
- <text class="modal-date">{{ selectedDate }}日程</text>
- <textarea v-model="scheduleContent" class="schedule-textarea" placeholder="请输入日程内容"></textarea>
- </view>
- <view class="modal-footer">
- <view class="modal-btn cancel-btn" @click="handleCloseModal">
- <text>取消</text>
- </view>
- <view class="modal-btn confirm-btn" @click="handleConfirmSchedule">
- <text>确定</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { ref, computed, onMounted } from 'vue';
- import workspaceApi from '@/reqApi/workspace';
- interface CalendarDay {
- date: number;
- month: number;
- year: number;
- dateStr: string;
- isCurrentMonth: boolean;
- hasSchedule: boolean;
- }
- const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
- const today = new Date();
- const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
- const currentYear = ref(today.getFullYear());
- const currentMonth = ref(today.getMonth() + 1);
- const selectedDate = ref(todayStr);
- const calendarDays = ref<CalendarDay[]>([]);
- const scheduleEvents = ref<{ [key: string]: Array<{ id: string; title: string }> }>({});
- const isLoading = ref(false);
- const showAddModal = ref(false);
- const scheduleContent = ref('');
- const editItem = ref<{ id: string; title: string } | null>(null);
- const expandedItems = ref<{ [key: string]: boolean }>({});
- const toggleExpand = (id: string) => {
- expandedItems.value[id] = !expandedItems.value[id];
- };
- const currentScheduleList = computed(() => {
- return scheduleEvents.value[selectedDate.value] || [];
- });
- const generateCalendarDays = () => {
- const days = [];
- const firstDay = new Date(currentYear.value, currentMonth.value - 1, 1);
- const lastDay = new Date(currentYear.value, currentMonth.value, 0);
- const startDayOfWeek = firstDay.getDay();
- const totalDays = lastDay.getDate();
- const lastDayOfWeek = lastDay.getDay();
- const prevMonthLastDay = new Date(currentYear.value, currentMonth.value - 1, 0).getDate();
- for (let i = startDayOfWeek - 1; i >= 0; i--) {
- const date = prevMonthLastDay - i;
- const month = currentMonth.value - 1 || 12;
- const year = currentMonth.value === 1 ? currentYear.value - 1 : currentYear.value;
- const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(date).padStart(2, '0')}`;
- days.push({
- date,
- month,
- year,
- dateStr,
- isCurrentMonth: false,
- hasSchedule: !!scheduleEvents.value[dateStr] && scheduleEvents.value[dateStr].length > 0
- });
- }
- for (let i = 1; i <= totalDays; i++) {
- const dateStr = `${currentYear.value}-${String(currentMonth.value).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
- days.push({
- date: i,
- month: currentMonth.value,
- year: currentYear.value,
- dateStr,
- isCurrentMonth: true,
- hasSchedule: !!scheduleEvents.value[dateStr] && scheduleEvents.value[dateStr].length > 0
- });
- }
- if (lastDayOfWeek !== 6) {
- const remainingDays = 6 - lastDayOfWeek;
- for (let i = 1; i <= remainingDays; i++) {
- const month = currentMonth.value + 1 > 12 ? 1 : currentMonth.value + 1;
- const year = currentMonth.value === 12 ? currentYear.value + 1 : currentYear.value;
- const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
- days.push({
- date: i,
- month,
- year,
- dateStr,
- isCurrentMonth: false,
- hasSchedule: !!scheduleEvents.value[dateStr] && scheduleEvents.value[dateStr].length > 0
- });
- }
- }
- calendarDays.value = days;
- };
- const fetchScheduleList = async () => {
- const firstDay = new Date(currentYear.value, currentMonth.value - 1, 1);
- const lastDay = new Date(currentYear.value, currentMonth.value, 0);
- const startDate = `${firstDay.getFullYear()}-${String(firstDay.getMonth() + 1).padStart(2, '0')}-${String(firstDay.getDate()).padStart(2, '0')}`;
- const endDate = `${lastDay.getFullYear()}-${String(lastDay.getMonth() + 1).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`;
- isLoading.value = true;
- try {
- const res = await workspaceApi.getSchedule({ startDate, endDate });
- updateScheduleEvents(res.data);
- } catch (error) {
- console.error('获取日程列表失败:', error);
- uni.showToast({ title: '获取日程失败,请稍后重试', icon: 'none', duration: 3000 });
- } finally {
- isLoading.value = false;
- }
- };
- const updateScheduleEvents = (data: { [key: string]: any[] }) => {
- Object.keys(scheduleEvents.value).forEach(key => {
- delete scheduleEvents.value[key];
- });
- Object.keys(data).forEach(dateKey => {
- const list = data[dateKey] || [];
- scheduleEvents.value[dateKey] = list.map((item: any) => ({
- id: String(item.id),
- title: item.content || ''
- }));
- });
- generateCalendarDays();
- };
- const handlePrevMonth = () => {
- if (currentMonth.value === 1) {
- currentMonth.value = 12;
- currentYear.value--;
- } else {
- currentMonth.value--;
- }
- fetchScheduleList();
- };
- const handleNextMonth = () => {
- if (currentMonth.value === 12) {
- currentMonth.value = 1;
- currentYear.value++;
- } else {
- currentMonth.value++;
- }
- fetchScheduleList();
- };
- const handleDayClick = (day: CalendarDay) => {
- selectedDate.value = day.dateStr;
- currentYear.value = day.year;
- currentMonth.value = day.month;
- };
- const handleDateChange = (e: { detail: { value: string } }) => {
- const date = e.detail.value;
- const [year, month, day] = date.split('-');
- currentYear.value = parseInt(year);
- currentMonth.value = parseInt(month);
- selectedDate.value = date;
- fetchScheduleList();
- };
- const handleBack = () => {
- uni.switchTab({
- url: '/pages/workspace/index'
- });
- };
- const handleAddSchedule = () => {
- editItem.value = null;
- scheduleContent.value = '';
- showAddModal.value = true;
- };
- const handleEdit = (item: { id: string; title: string }) => {
- editItem.value = item;
- scheduleContent.value = item.title;
- showAddModal.value = true;
- };
- const handleDelete = async (id: string) => {
- uni.showModal({
- title: '提示',
- content: '确定要删除这条日程吗?',
- success: async (res) => {
- if (res.confirm) {
- try {
- await workspaceApi.deleteSchedule(id);
- uni.showToast({ title: '删除成功', icon: 'success' });
- fetchScheduleList();
- } catch (error) {
- console.error('删除日程失败:', error);
- uni.showToast({ title: '删除失败,请稍后重试', icon: 'none', duration: 3000 });
- }
- }
- }
- });
- };
- const handleCloseModal = () => {
- showAddModal.value = false;
- scheduleContent.value = '';
- editItem.value = null;
- };
- const handleConfirmSchedule = async () => {
- if (!scheduleContent.value.trim()) {
- uni.showToast({ title: '请输入日程内容', icon: 'none' });
- return;
- }
- try {
- if (editItem.value) {
- await workspaceApi.saveSchedule({
- id: editItem.value.id,
- content: scheduleContent.value.trim(),
- scheduleDate: selectedDate.value
- });
- uni.showToast({ title: '修改成功', icon: 'success' });
- } else {
- await workspaceApi.saveSchedule({
- content: scheduleContent.value.trim(),
- scheduleDate: selectedDate.value
- });
- uni.showToast({ title: '添加成功', icon: 'success' });
- }
- handleCloseModal();
- fetchScheduleList();
- } catch (error) {
- console.error('保存日程失败:', error);
- uni.showToast({ title: '保存失败,请稍后重试', icon: 'none', duration: 3000 });
- }
- };
- onMounted(() => {
- fetchScheduleList();
- });
- </script>
- <style lang="scss" scoped>
- .page {
- height: 100vh;
- background-color: #F5F7FA;
- padding-bottom: env(safe-area-inset-bottom);
- box-sizing: border-box;
- }
- .nav_bar {
- background-color: #FFFFFF;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding-top: calc(24rpx + env(safe-area-inset-top));
- padding-bottom: 24rpx;
- padding-left: 24rpx;
- padding-right: 24rpx;
- .nav_back {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 48rpx;
- height: 48rpx;
- }
- .nav_title {
- font-size: 32rpx;
- font-weight: 500;
- color: #333333;
- }
- .nav_right {
- width: 48rpx;
- height: 48rpx;
- }
- }
- .calendar_container {
- background-color: #FFFFFF;
- margin: 16rpx;
- border-radius: 20rpx;
- padding: 16rpx;
- .calendar_header {
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 32rpx;
- .prev_month,
- .next_month {
- width: 40rpx;
- height: 40rpx;
- background: rgba(46, 100, 250, 0.1);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .month_select {
- display: flex;
- align-items: center;
- gap: 8rpx;
- padding: 0 20rpx;
- }
- .current_month {
- font-weight: 500;
- font-size: 36rpx;
- color: #333333;
- }
- }
- .calendar_weekdays {
- display: flex;
- margin-bottom: 16rpx;
- .weekday {
- flex: 1;
- text-align: center;
- font-size: 28rpx;
- color: #666666;
- height: 40rpx;
- line-height: 40rpx;
- }
- }
- .calendar_days {
- display: flex;
- flex-wrap: wrap;
- gap: 24rpx 0rpx;
- margin-bottom: 48rpx;
- .day_item {
- width: calc(100% / 7);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 80rpx;
- position: relative;
- .day_num {
- font-size: 28rpx;
- color: #333333;
- width: 80rpx;
- height: 80rpx;
- line-height: 80rpx;
- text-align: center;
- border-radius: 50%;
- border: 2rpx solid transparent;
- box-sizing: border-box;
- }
- .day_dot {
- width: 8rpx;
- height: 8rpx;
- border-radius: 50%;
- background-color: #2E64FA;
- position: absolute;
- bottom: 8rpx;
- opacity: 0;
- transition: opacity 0.3s;
- &.visible {
- opacity: 1;
- }
- }
- &.other-month .day_num {
- color: #C0C4CC;
- }
- &.active .day_num {
- background: rgba(46,100,250,0.1);
- border: 2rpx solid #2E64FA;
- color: #2E64FA;
- }
- &.today:not(.active) .day_num {
- border-color: #2E64FA;
- }
- }
- }
- }
- .schedule_container {
- background-color: #FFFFFF;
- margin: 0 16rpx 16rpx;
- border-radius: 16rpx;
- padding: 24rpx;
- .schedule_header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 24rpx;
- padding-bottom: 32rpx;
- border-bottom: 2rpx solid #EBEEF5;
- .schedule_date {
- font-size: 36rpx;
- font-weight: 500;
- color: #333333;
- }
- .add_btn {
- display: flex;
- align-items: center;
- gap: 8rpx;
- padding: 20rpx 24rpx;
- background-color: #2E64FA;
- border-radius: 8rpx;
- font-size: 28rpx;
- color: #FFFFFF;
- .add_icon {
- width: 32rpx;
- height: 32rpx;
- }
- }
- }
- .schedule_empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- font-size: 28rpx;
- color: #999999;
- position: relative;
- .empty_image {
- width: 400rpx;
- height: 400rpx;
- }
- .empty_text {
- position: absolute;
- bottom: 15%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
- }
- .schedule_list {
- .schedule_item {
- display: flex;
- align-items: center;
- padding: 20rpx 0;
- border-bottom: 2rpx solid #EBEEF5;
- &:last-child {
- border-bottom: none;
- }
- .schedule_dot {
- width: 12rpx;
- height: 12rpx;
- background-color: #2E64FA;
- border-radius: 50%;
- margin-right: 16rpx;
- flex-shrink: 0;
- }
- .schedule_text_container {
- flex: 1;
- position: relative;
- }
- .schedule_text {
- font-size: 28rpx;
- color: #666666;
- line-height: 1.6;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- overflow: hidden;
- text-overflow: ellipsis;
- &.expanded {
- -webkit-line-clamp: unset;
- overflow: visible;
- text-overflow: unset;
- }
- }
- .toggle_text {
- font-size: 24rpx;
- color: #2E64FA;
- position: absolute;
- right: 0;
- bottom: 0;
- line-height: 1.6;
- background-color: #FFFFFF;
- padding-left: 10rpx;
- }
- .schedule_actions {
- display: flex;
- gap: 24rpx;
- margin-left: 36rpx;
- .action_btn {
- font-size: 26rpx;
- &.delete {
- color: #F56C6C;
- }
- &.edit {
- color: #2E64FA;
- }
- }
- }
- }
- }
- }
- .modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, 0.5);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 1000;
- }
- .modal-content {
- width: 702rpx;
- background-color: #FFFFFF;
- border-radius: 16rpx;
- overflow: hidden;
- .modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- height: 96rpx;
- padding: 0 40rpx;
- background-color: #F5F7FA;
- .modal-title {
- font-size: 32rpx;
- font-weight: 500;
- color: #303133;
- }
- .modal-close {
- width: 60rpx;
- height: 60rpx;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .close-icon {
- width: 40rpx;
- height: 40rpx;
- }
- }
- .modal-body {
- padding: 24rpx;
- .modal-date {
- font-size: 32rpx;
- color: #333333;
- margin-bottom: 24rpx;
- display: block;
- }
- .schedule-textarea {
- width: 100%;
- height: 364rpx;
- padding: 20rpx;
- border: 2rpx solid #E8E8E8;
- border-radius: 12rpx;
- font-size: 28rpx;
- color: #333333;
- box-sizing: border-box;
- }
- }
- .modal-footer {
- display: flex;
- justify-content: flex-end;
- padding: 32rpx;
- gap: 24rpx;
- border-top: 2rpx solid #EBEEF5;
- .modal-btn {
- height: 80rpx;
- padding: 0 48rpx;
- border-radius: 8rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 28rpx;
- &.cancel-btn {
- background-color: #FFFFFF;
- border: 2rpx solid #DCDFE6;
- color: #666666;
- }
- &.confirm-btn {
- background-color: #2E64FA;
- border: none;
- color: #FFFFFF;
- }
- }
- }
- }
- .month-picker-content {
- width: 600rpx;
- background-color: #FFFFFF;
- border-radius: 16rpx;
- overflow: hidden;
- .picker-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- height: 96rpx;
- padding: 0 40rpx;
- background-color: #F5F7FA;
- .picker-title {
- font-size: 32rpx;
- font-weight: 500;
- color: #303133;
- }
- .picker-close {
- width: 60rpx;
- height: 60rpx;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .close-icon {
- width: 40rpx;
- height: 40rpx;
- }
- }
- .picker-body {
- padding: 32rpx;
- .picker-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- height: 80rpx;
- padding: 0 24rpx;
- background-color: #F9FAFF;
- border-radius: 8rpx;
- font-size: 28rpx;
- color: #333333;
- & + & {
- margin-top: 24rpx;
- }
- }
- }
- .picker-footer {
- display: flex;
- justify-content: flex-end;
- padding: 32rpx;
- border-top: 2rpx solid #EBEEF5;
- .picker-btn {
- height: 80rpx;
- padding: 0 48rpx;
- border-radius: 8rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 28rpx;
- &.confirm-btn {
- background-color: #2E64FA;
- border: none;
- color: #FFFFFF;
- }
- }
- }
- }
- </style>
|