feat(calendar): 添加日历组件并替换待办事项列表
重构待办事项列表为日历视图,添加@fullcalendar/core依赖 支持营期设置、日期选择和事件展示功能
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@fullcalendar/core": "^6.1.19",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
|
|||||||
15
my-vue-app/pnpm-lock.yaml
generated
15
my-vue-app/pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@element-plus/icons-vue':
|
'@element-plus/icons-vue':
|
||||||
specifier: ^2.3.1
|
specifier: ^2.3.1
|
||||||
version: 2.3.1(vue@3.5.17(typescript@5.8.3))
|
version: 2.3.1(vue@3.5.17(typescript@5.8.3))
|
||||||
|
'@fullcalendar/core':
|
||||||
|
specifier: ^6.1.19
|
||||||
|
version: 6.1.19
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.10.0
|
specifier: ^1.10.0
|
||||||
version: 1.10.0
|
version: 1.10.0
|
||||||
@@ -456,6 +459,9 @@ packages:
|
|||||||
'@floating-ui/utils@0.2.10':
|
'@floating-ui/utils@0.2.10':
|
||||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||||
|
|
||||||
|
'@fullcalendar/core@6.1.19':
|
||||||
|
resolution: {integrity: sha512-z0aVlO5e4Wah6p6mouM0UEqtRf1MZZPt4mwzEyU6kusaNL+dlWQgAasF2cK23hwT4cmxkEmr4inULXgpyeExdQ==}
|
||||||
|
|
||||||
'@humanfs/core@0.19.1':
|
'@humanfs/core@0.19.1':
|
||||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@@ -1676,6 +1682,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
preact@10.12.1:
|
||||||
|
resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==}
|
||||||
|
|
||||||
prelude-ls@1.2.1:
|
prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -2356,6 +2365,10 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/utils@0.2.10': {}
|
'@floating-ui/utils@0.2.10': {}
|
||||||
|
|
||||||
|
'@fullcalendar/core@6.1.19':
|
||||||
|
dependencies:
|
||||||
|
preact: 10.12.1
|
||||||
|
|
||||||
'@humanfs/core@0.19.1': {}
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
'@humanfs/node@0.16.6':
|
'@humanfs/node@0.16.6':
|
||||||
@@ -3575,6 +3588,8 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
preact@10.12.1: {}
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prettier-linter-helpers@1.0.0:
|
prettier-linter-helpers@1.0.0:
|
||||||
|
|||||||
@@ -1,686 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="action-items">
|
<div class="action-items-container">
|
||||||
<div class="actions-header">
|
<Calendar />
|
||||||
<h2>待处理事项</h2>
|
|
||||||
<div class="header-controls">
|
|
||||||
<select v-model="filterPriority" class="priority-filter">
|
|
||||||
<option value="all">全部状态</option>
|
|
||||||
<option value="待处理">待处理</option>
|
|
||||||
<option value="正在处理">正在处理</option>
|
|
||||||
<option value="已完成">已完成</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions-list">
|
|
||||||
<div v-if="filteredActions.length === 0" class="no-tasks">
|
|
||||||
<p>暂无任务</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="task-list">
|
|
||||||
<div
|
|
||||||
v-for="task in filteredActions"
|
|
||||||
:key="task.task_id"
|
|
||||||
class="task-row"
|
|
||||||
>
|
|
||||||
<div class="task-info">
|
|
||||||
<div class="task-row-1">
|
|
||||||
<span class="task-title">{{ task.task_title }}</span>
|
|
||||||
<span class="task-content">{{ task.task_content }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="task-row-2">
|
|
||||||
<span class="task-date">到期时间: {{ formatDueDate(task.expiration_date) }}</span>
|
|
||||||
<span class="task-created">创建时间: {{ task.created_at }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="task-actions">
|
|
||||||
<span class="status-tag" :class="getTaskStatusClass(task.state)">
|
|
||||||
{{ task.state }}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="status-btn"
|
|
||||||
@click="changeTaskStatus(task)"
|
|
||||||
>
|
|
||||||
处理任务
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import Calendar from './Calendar.vue';
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
selectedGroup: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 筛选优先级
|
|
||||||
const filterPriority = ref('all')
|
|
||||||
|
|
||||||
// 显示新增表单
|
|
||||||
const showAddForm = ref(false)
|
|
||||||
|
|
||||||
// 新增事项表单数据
|
|
||||||
const newAction = ref({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
priority: 'medium',
|
|
||||||
dueDate: '',
|
|
||||||
relatedGroup: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 待处理事项数据
|
|
||||||
const actions = ref([])
|
|
||||||
|
|
||||||
// 筛选后的事项
|
|
||||||
const filteredActions = computed(() => {
|
|
||||||
console.log('计算filteredActions - actions.value:', actions.value)
|
|
||||||
console.log('计算filteredActions - filterPriority.value:', filterPriority.value)
|
|
||||||
|
|
||||||
let filtered = actions.value
|
|
||||||
|
|
||||||
if (filterPriority.value !== 'all') {
|
|
||||||
filtered = filtered.filter(task => task.state === filterPriority.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('计算filteredActions - filtered结果:', filtered)
|
|
||||||
return filtered
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取任务状态样式类
|
|
||||||
const getTaskStatusClass = (state) => {
|
|
||||||
switch (state) {
|
|
||||||
case '待处理':
|
|
||||||
return 'status-pending'
|
|
||||||
case '正在处理':
|
|
||||||
return 'status-processing'
|
|
||||||
case '已完成':
|
|
||||||
return 'status-completed'
|
|
||||||
default:
|
|
||||||
return 'status-default'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化到期日期
|
|
||||||
const formatDueDate = (dateStr) => {
|
|
||||||
if (!dateStr) return '无'
|
|
||||||
// 如果是YYYYMMDD格式,转换为YYYY-MM-DD
|
|
||||||
if (dateStr.length === 8) {
|
|
||||||
const year = dateStr.substring(0, 4)
|
|
||||||
const month = dateStr.substring(4, 6)
|
|
||||||
const day = dateStr.substring(6, 8)
|
|
||||||
return `${year}-${month}-${day}`
|
|
||||||
}
|
|
||||||
return dateStr
|
|
||||||
}
|
|
||||||
// 获取任务列表
|
|
||||||
const getTaskList = async () => {
|
|
||||||
try {
|
|
||||||
const res = await axios.post('http://192.168.15.60:8890/api/v1/level_five/overview/view_tasks', {}, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log(888888,res)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
actions.value = res.data.data.tasks || res.data.data
|
|
||||||
console.log(777777,actions.value)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取任务列表失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 修改任务状态按钮点击事件
|
|
||||||
const changeTaskStatus = (task) => {
|
|
||||||
const statusOptions = ['待处理', '正在处理', '已完成']
|
|
||||||
const currentIndex = statusOptions.indexOf(task.state)
|
|
||||||
const nextIndex = (currentIndex + 1) % statusOptions.length
|
|
||||||
const newState = statusOptions[nextIndex]
|
|
||||||
|
|
||||||
updateTaskState(task.task_id, newState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改任务状态 http://192.168.15.56:8890/api/v1/level_four/overview/update_task_state
|
|
||||||
const updateTaskState = async (taskId, state) => {
|
|
||||||
try {
|
|
||||||
const res = await axios.put('http://192.168.15.60:8890/api/v1/level_four/overview/update_task_state', {
|
|
||||||
task_ids: [taskId],
|
|
||||||
new_state: state
|
|
||||||
}, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
console.log('任务状态更新成功')
|
|
||||||
// 刷新任务列表
|
|
||||||
await getTaskList()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新任务状态失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑事项
|
|
||||||
const editAction = (action) => {
|
|
||||||
// 这里可以实现编辑功能
|
|
||||||
console.log('编辑事项:', action)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除事项
|
|
||||||
const deleteAction = (id) => {
|
|
||||||
if (confirm('确定要删除这个事项吗?')) {
|
|
||||||
const index = actions.value.findIndex(a => a.id === id)
|
|
||||||
if (index > -1) {
|
|
||||||
actions.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加新事项
|
|
||||||
const addAction = () => {
|
|
||||||
const newId = Math.max(...actions.value.map(a => a.id)) + 1
|
|
||||||
actions.value.push({
|
|
||||||
id: newId,
|
|
||||||
...newAction.value,
|
|
||||||
progress: 0,
|
|
||||||
tags: [],
|
|
||||||
completed: false,
|
|
||||||
createdAt: new Date().toISOString().split('T')[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
newAction.value = {
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
priority: 'medium',
|
|
||||||
dueDate: '',
|
|
||||||
relatedGroup: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
showAddForm.value = false
|
|
||||||
}
|
|
||||||
onMounted(async () => {
|
|
||||||
await getTaskList()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style scoped>
|
||||||
.action-items {
|
.action-items-container {
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
max-height: 600px;
|
||||||
flex-direction: column;
|
overflow: hidden;
|
||||||
|
padding: 5px;
|
||||||
.actions-header {
|
background: #ffffff;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.priority-filter {
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: #3b82f6;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #2563eb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 事项列表
|
|
||||||
.actions-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.no-tasks {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
color: #999;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-item {
|
|
||||||
display: flex;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
border-color: #d0d0d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-pending {
|
|
||||||
border-left: 4px solid #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-processing {
|
|
||||||
border-left: 4px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-completed {
|
|
||||||
border-left: 4px solid #10b981;
|
|
||||||
opacity: 0.8;
|
|
||||||
background: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-content {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.action-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
|
||||||
.action-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1f2937;
|
|
||||||
margin: 0;
|
|
||||||
flex: 1;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 0.75rem;
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&.status-pending {
|
|
||||||
background: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
border: 1px solid #ffeaa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-processing {
|
|
||||||
background: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
border: 1px solid #bee5eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-completed {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-default {
|
|
||||||
background: #f8f9fa;
|
|
||||||
color: #6c757d;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.due-date {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #666;
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-description {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #6b7280;
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-details {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
|
|
||||||
.detail-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #888;
|
|
||||||
|
|
||||||
.detail-label {
|
|
||||||
font-weight: 500;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-value {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 空状态
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
h3 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #374151;
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #6b7280;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模态框
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 80vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #6b7280;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-form {
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #374151;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input, .form-textarea, .form-select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-textarea {
|
|
||||||
height: 80px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
|
|
||||||
.btn-cancel, .btn-submit {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
background: #f3f4f6;
|
|
||||||
color: #374151;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #e5e7eb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
background: #3b82f6;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #2563eb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 任务列表样式
|
.calendar-title {
|
||||||
.task-list {
|
margin: 0 0 20px 0;
|
||||||
.task-row {
|
color: #303133;
|
||||||
display: flex;
|
font-size: 20px;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 0;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
flex: 1;
|
|
||||||
margin-right: 16px;
|
|
||||||
|
|
||||||
.task-row-1, .task-row-2 {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-row-1 {
|
|
||||||
.task-title {
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #111827;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-content {
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 13px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-row-2 {
|
|
||||||
.task-date, .task-created {
|
|
||||||
color: #9ca3af;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.status-tag {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&.pending {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
color: #d97706;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.in-progress {
|
|
||||||
background-color: #dbeafe;
|
|
||||||
color: #2563eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.completed {
|
|
||||||
background-color: #d1fae5;
|
|
||||||
color: #059669;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-btn {
|
|
||||||
padding: 6px 12px;
|
|
||||||
background-color: #3b82f6;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #2563eb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-tasks {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px 20px;
|
padding-bottom: 15px;
|
||||||
color: #6b7280;
|
border-bottom: 2px solid #f0f2f5;
|
||||||
}
|
|
||||||
|
|
||||||
// 移动端适配
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.task-list {
|
|
||||||
.task-row {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.task-actions {
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
967
my-vue-app/src/views/secondTop/components/Calendar.vue
Normal file
967
my-vue-app/src/views/secondTop/components/Calendar.vue
Normal file
@@ -0,0 +1,967 @@
|
|||||||
|
<template>
|
||||||
|
<div class="calendar-container">
|
||||||
|
<!-- 头部导航 -->
|
||||||
|
<div class="calendar-header">
|
||||||
|
<button @click="previousMonth" class="nav-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor" d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<h2 class="month-year">{{ currentYear }}年{{ currentMonth + 1 }}月</h2>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button v-if="!shouldShowFinishCamp()" @click="showCampModal = true" class="camp-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||||
|
</svg>
|
||||||
|
设置营期
|
||||||
|
</button>
|
||||||
|
<button v-if="shouldShowFinishCamp()" @click="finishCamp" class="camp-btn finish-camp">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor" d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
|
||||||
|
</svg>
|
||||||
|
结束营期
|
||||||
|
</button>
|
||||||
|
<button @click="nextMonth" class="nav-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor" d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 星期标题 -->
|
||||||
|
<div class="weekdays">
|
||||||
|
<div v-for="day in weekdays" :key="day" class="weekday">
|
||||||
|
{{ day }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日期网格 -->
|
||||||
|
<div class="calendar-grid">
|
||||||
|
<div
|
||||||
|
v-for="date in calendarDates"
|
||||||
|
:key="date.key"
|
||||||
|
:class="[
|
||||||
|
'date-cell',
|
||||||
|
{
|
||||||
|
'other-month': !date.isCurrentMonth,
|
||||||
|
'today': date.isToday,
|
||||||
|
'selected': date.isSelected,
|
||||||
|
'has-event': date.hasEvent
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
@click="selectDate(date, $event)"
|
||||||
|
>
|
||||||
|
<span class="date-number">{{ date.day }}</span>
|
||||||
|
<div v-if="date.hasEvent && !isRestDay(date.dateStr)" class="event-dot" :class="getEventTypeClass(date.dateStr)"></div>
|
||||||
|
<span v-if="isRestDay(date.dateStr)" class="rest-text">休</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 悬浮日期信息 -->
|
||||||
|
<div
|
||||||
|
v-if="showTooltip && selectedDate"
|
||||||
|
class="date-tooltip"
|
||||||
|
:style="{ left: tooltipPosition.x + 'px', top: tooltipPosition.y + 'px' }"
|
||||||
|
>
|
||||||
|
<h3>{{ formatSelectedDate }}</h3>
|
||||||
|
<div v-if="selectedDateEvents.length > 0" class="events-list">
|
||||||
|
<div v-for="event in selectedDateEvents" :key="event.id" class="event-item">
|
||||||
|
<span class="event-title">{{ event.title }}</span>
|
||||||
|
<span class="event-desc">{{ event.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-events">
|
||||||
|
暂无日程安排
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置营期弹窗 -->
|
||||||
|
<div v-if="showCampModal" class="modal-overlay" @click="showCampModal = false">
|
||||||
|
<div class="modal-content" @click.stop>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>设置营期</h3>
|
||||||
|
<button @click="showCampModal = false" class="close-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20">
|
||||||
|
<path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="campStartDate">营期开始时间:</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="campStartDate"
|
||||||
|
v-model="campStartDate"
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="campDays">接数据天数:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="campDays"
|
||||||
|
v-model="campDays"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="请输入天数"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button @click="showCampModal = false" class="cancel-btn">取消</button>
|
||||||
|
<button @click="saveCampSettings" class="save-btn">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
import {getCampPeriodAdmin} from '@/api/secondTop.js'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const currentYear = ref(new Date().getFullYear());
|
||||||
|
const currentMonth = ref(new Date().getMonth());
|
||||||
|
const selectedDate = ref(null);
|
||||||
|
const today = new Date();
|
||||||
|
const showTooltip = ref(false);
|
||||||
|
const tooltipPosition = ref({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// 营期设置相关
|
||||||
|
const showCampModal = ref(false);
|
||||||
|
const campStartDate = ref('');
|
||||||
|
const campDays = ref();
|
||||||
|
const isCampFinished = ref(false);
|
||||||
|
|
||||||
|
// 获取本地时区的今日日期字符串
|
||||||
|
const getTodayString = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const todayString = getTodayString();
|
||||||
|
|
||||||
|
// 星期标题
|
||||||
|
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||||
|
|
||||||
|
// 示例事件数据
|
||||||
|
const events = ref([]);
|
||||||
|
|
||||||
|
// 计算属性:当前月份的日期数组
|
||||||
|
const calendarDates = computed(() => {
|
||||||
|
const dates = [];
|
||||||
|
const firstDay = new Date(currentYear.value, currentMonth.value, 1);
|
||||||
|
const lastDay = new Date(currentYear.value, currentMonth.value + 1, 0);
|
||||||
|
const startDate = new Date(firstDay);
|
||||||
|
|
||||||
|
// 调整到周日开始
|
||||||
|
startDate.setDate(startDate.getDate() - startDate.getDay());
|
||||||
|
|
||||||
|
// 生成6周的日期
|
||||||
|
for (let i = 0; i < 42; i++) {
|
||||||
|
const date = new Date(startDate);
|
||||||
|
date.setDate(startDate.getDate() + i);
|
||||||
|
|
||||||
|
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
const isCurrentMonth = date.getMonth() === currentMonth.value;
|
||||||
|
const isToday = dateStr === todayString;
|
||||||
|
const hasEvent = events.value.some(event => event.date === dateStr);
|
||||||
|
|
||||||
|
dates.push({
|
||||||
|
key: `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`,
|
||||||
|
date: date,
|
||||||
|
day: date.getDate(),
|
||||||
|
dateStr: dateStr,
|
||||||
|
isCurrentMonth: isCurrentMonth,
|
||||||
|
isToday: isToday,
|
||||||
|
isSelected: selectedDate.value && selectedDate.value.dateStr === dateStr,
|
||||||
|
hasEvent: hasEvent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return dates;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:格式化选中日期
|
||||||
|
const formatSelectedDate = computed(() => {
|
||||||
|
if (!selectedDate.value) return '';
|
||||||
|
const date = selectedDate.value.date;
|
||||||
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:选中日期的事件
|
||||||
|
const selectedDateEvents = computed(() => {
|
||||||
|
if (!selectedDate.value) return [];
|
||||||
|
return events.value.filter(event => event.date === selectedDate.value.dateStr);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 方法:上一月
|
||||||
|
const previousMonth = () => {
|
||||||
|
if (currentMonth.value === 0) {
|
||||||
|
currentMonth.value = 11;
|
||||||
|
currentYear.value--;
|
||||||
|
} else {
|
||||||
|
currentMonth.value--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方法:下一月
|
||||||
|
const nextMonth = () => {
|
||||||
|
if (currentMonth.value === 11) {
|
||||||
|
currentMonth.value = 0;
|
||||||
|
currentYear.value++;
|
||||||
|
} else {
|
||||||
|
currentMonth.value++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方法:选择日期
|
||||||
|
const selectDate = (date, event) => {
|
||||||
|
selectedDate.value = date;
|
||||||
|
if (event) {
|
||||||
|
tooltipPosition.value = {
|
||||||
|
x: event.clientX + 10,
|
||||||
|
y: event.clientY - 10
|
||||||
|
};
|
||||||
|
showTooltip.value = true;
|
||||||
|
|
||||||
|
// 3秒后自动隐藏
|
||||||
|
setTimeout(() => {
|
||||||
|
showTooltip.value = false;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方法:隐藏悬浮框
|
||||||
|
const hideTooltip = () => {
|
||||||
|
showTooltip.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方法:保存营期设置
|
||||||
|
const saveCampSettings = async () => {
|
||||||
|
if (!campStartDate.value) {
|
||||||
|
alert('请选择营期开始时间');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!campDays.value || campDays.value < 1) {
|
||||||
|
alert('请输入有效的天数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用API设置营期参数并获取营期安排
|
||||||
|
const result = await CenterCampPeriodAdmin({
|
||||||
|
receipt_data_start_time: campStartDate.value,
|
||||||
|
receipt_data_time: campDays.value.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result && result.data) {
|
||||||
|
showCampModal.value = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存营期设置失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方法:生成营期日程安排
|
||||||
|
const generateCampSchedule = () => {
|
||||||
|
const startDate = new Date(campStartDate.value);
|
||||||
|
const dataDays = campDays.value || 2; // 接数据天数,默认2天
|
||||||
|
let currentDate = new Date(startDate);
|
||||||
|
let eventId = events.value.length + 1;
|
||||||
|
|
||||||
|
// 清除之前的营期相关事件
|
||||||
|
events.value = events.value.filter(event => !event.isCampEvent);
|
||||||
|
|
||||||
|
// 添加接数据日程
|
||||||
|
for (let i = 0; i < dataDays; i++) {
|
||||||
|
const dateStr = formatDateToString(currentDate);
|
||||||
|
events.value.push({
|
||||||
|
id: eventId++,
|
||||||
|
date: dateStr,
|
||||||
|
title: `接数据 第${i + 1}天`,
|
||||||
|
description: '营期数据接收阶段',
|
||||||
|
isCampEvent: true,
|
||||||
|
type: 'data'
|
||||||
|
});
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加课程日程
|
||||||
|
const courses = ['课1', '课2', '课3', '课4'];
|
||||||
|
courses.forEach((course, index) => {
|
||||||
|
const dateStr = formatDateToString(currentDate);
|
||||||
|
events.value.push({
|
||||||
|
id: eventId++,
|
||||||
|
date: dateStr,
|
||||||
|
title: course,
|
||||||
|
description: `${course}`,
|
||||||
|
isCampEvent: true,
|
||||||
|
type: 'course'
|
||||||
|
});
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加休息日程
|
||||||
|
const restDateStr = formatDateToString(currentDate);
|
||||||
|
events.value.push({
|
||||||
|
id: eventId++,
|
||||||
|
date: restDateStr,
|
||||||
|
title: '休息',
|
||||||
|
description: '营期休息日',
|
||||||
|
isCampEvent: true,
|
||||||
|
type: 'rest'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助方法:格式化日期为字符串
|
||||||
|
const formatDateToString = (date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方法:获取事件类型样式类
|
||||||
|
const getEventTypeClass = (dateStr) => {
|
||||||
|
const dayEvents = events.value.filter(event => event.date === dateStr && event.isCampEvent);
|
||||||
|
if (dayEvents.length === 0) return '';
|
||||||
|
|
||||||
|
const eventType = dayEvents[0].type;
|
||||||
|
switch (eventType) {
|
||||||
|
case 'data':
|
||||||
|
return 'event-data';
|
||||||
|
case 'course':
|
||||||
|
return 'event-course';
|
||||||
|
case 'rest':
|
||||||
|
return 'event-rest';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方法:判断是否为休息日
|
||||||
|
const isRestDay = (dateStr) => {
|
||||||
|
const dayEvents = events.value.filter(event => event.date === dateStr && event.isCampEvent && event.type === 'rest');
|
||||||
|
return dayEvents.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方法:检查营期是否应该结束(休息日已过)
|
||||||
|
const shouldShowFinishCamp = () => {
|
||||||
|
if (isCampFinished.value) return false;
|
||||||
|
|
||||||
|
const today = formatDateToString(new Date());
|
||||||
|
const restDayEvents = events.value.filter(event => event.isCampEvent && event.type === 'rest');
|
||||||
|
|
||||||
|
if (restDayEvents.length === 0) return false;
|
||||||
|
|
||||||
|
// 检查是否有休息日已经过去
|
||||||
|
const todayDate = new Date(today);
|
||||||
|
for (const restEvent of restDayEvents) {
|
||||||
|
const restDate = new Date(restEvent.date);
|
||||||
|
if (todayDate > restDate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方法:结束营期
|
||||||
|
const finishCamp = async () => {
|
||||||
|
try {
|
||||||
|
isCampFinished.value = true;
|
||||||
|
await CenterCampPeriodAdmin({
|
||||||
|
is_camp_finish: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('结束营期失败:', error);
|
||||||
|
isCampFinished.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 路由实例
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 用户store实例
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 获取通用请求参数的函数
|
||||||
|
const getRequestParams = () => {
|
||||||
|
const params = {}
|
||||||
|
// 只从路由参数获取
|
||||||
|
const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level
|
||||||
|
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
|
||||||
|
// 如果路由有参数,使用路由参数
|
||||||
|
if (routeUserLevel) {
|
||||||
|
params.user_level = routeUserLevel.toString()
|
||||||
|
}
|
||||||
|
if (routeUserName) {
|
||||||
|
params.user_name = routeUserName
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为路由导航(有路由参数)
|
||||||
|
const isRouteNavigation = computed(() => {
|
||||||
|
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
|
||||||
|
return !!routeUserName
|
||||||
|
})
|
||||||
|
// 获取和设置当前营期阶段 getCampPeriodAdmin
|
||||||
|
async function CenterCampPeriodAdmin(data = {}) {
|
||||||
|
const params = getRequestParams()
|
||||||
|
const hasParams = params.user_name
|
||||||
|
|
||||||
|
// 根据传入的参数决定传递哪些数据
|
||||||
|
let Finsh = {}
|
||||||
|
if (data.is_camp_finish !== undefined) {
|
||||||
|
// 结束营期时,只传递is_camp_finish参数
|
||||||
|
Finsh.is_camp_finish = data.is_camp_finish
|
||||||
|
} else if (data.receipt_data_start_time && data.receipt_data_time) {
|
||||||
|
// 设置营期时,传递开始时间和天数参数
|
||||||
|
Finsh.receipt_data_start_time = data.receipt_data_start_time
|
||||||
|
Finsh.receipt_data_time = data.receipt_data_time
|
||||||
|
} else {
|
||||||
|
// 兼容原有逻辑,使用全局变量
|
||||||
|
if (isCampFinished.value) {
|
||||||
|
Finsh.is_camp_finish = isCampFinished.value
|
||||||
|
} else if (campStartDate.value && campDays.value) {
|
||||||
|
// 只有在有营期设置数据时才传递参数
|
||||||
|
Finsh.receipt_data_start_time = campStartDate.value
|
||||||
|
Finsh.receipt_data_time = campDays.value.toString()
|
||||||
|
}
|
||||||
|
// 如果没有营期设置数据,Finsh 保持为空对象,用于获取现有数据
|
||||||
|
}
|
||||||
|
console.log('Finsh', Finsh)
|
||||||
|
console.log('params', params)
|
||||||
|
const res = await getCampPeriodAdmin(hasParams ? {...params, ...Finsh} : Finsh)
|
||||||
|
|
||||||
|
// 如果获取到营期数据,映射到日历中
|
||||||
|
if (res && res.data && res.data.camp_period) {
|
||||||
|
mapCampPeriodToCalendar(res.data.camp_period);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法:将营期时间安排映射到日历
|
||||||
|
const mapCampPeriodToCalendar = (campPeriod) => {
|
||||||
|
// 清除之前的营期相关事件
|
||||||
|
events.value = events.value.filter(event => !event.isCampEvent);
|
||||||
|
|
||||||
|
let eventId = events.value.length + 1;
|
||||||
|
|
||||||
|
// 解析接数据时间
|
||||||
|
if (campPeriod.receipt_data_time) {
|
||||||
|
const dataPeriod = parseDateRange(campPeriod.receipt_data_time);
|
||||||
|
for (let date = new Date(dataPeriod.start); date <= dataPeriod.end; date.setDate(date.getDate() + 1)) {
|
||||||
|
const dateStr = formatDateToString(new Date(date));
|
||||||
|
events.value.push({
|
||||||
|
id: eventId++,
|
||||||
|
date: dateStr,
|
||||||
|
title: '接数据',
|
||||||
|
description: '营期数据接收阶段',
|
||||||
|
isCampEvent: true,
|
||||||
|
type: 'data'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析课程时间
|
||||||
|
const courses = [
|
||||||
|
{ key: 'class_one', title: '课1' },
|
||||||
|
{ key: 'class_two', title: '课2' },
|
||||||
|
{ key: 'class_three', title: '课3' },
|
||||||
|
{ key: 'class_four', title: '课4' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastCourseEndDate = null;
|
||||||
|
|
||||||
|
courses.forEach(course => {
|
||||||
|
if (campPeriod[course.key]) {
|
||||||
|
const coursePeriod = parseDateRange(campPeriod[course.key]);
|
||||||
|
const dateStr = formatDateToString(coursePeriod.start);
|
||||||
|
events.value.push({
|
||||||
|
id: eventId++,
|
||||||
|
date: dateStr,
|
||||||
|
title: course.title,
|
||||||
|
description: `营期${course.title}阶段`,
|
||||||
|
isCampEvent: true,
|
||||||
|
type: 'course'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 记录最后一个课程的结束日期
|
||||||
|
if (course.key === 'class_four') {
|
||||||
|
lastCourseEndDate = coursePeriod.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在课4之后添加休息日
|
||||||
|
if (lastCourseEndDate) {
|
||||||
|
const restDate = new Date(lastCourseEndDate);
|
||||||
|
restDate.setDate(restDate.getDate() + 1);
|
||||||
|
const restDateStr = formatDateToString(restDate);
|
||||||
|
|
||||||
|
events.value.push({
|
||||||
|
id: eventId++,
|
||||||
|
date: restDateStr,
|
||||||
|
title: '休息',
|
||||||
|
description: '营期休息日',
|
||||||
|
isCampEvent: true,
|
||||||
|
type: 'rest'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助方法:解析日期范围字符串
|
||||||
|
const parseDateRange = (dateRangeStr) => {
|
||||||
|
// 解析格式: "2025-08-15 00:00:00 to 2025-08-16 23:59:59"
|
||||||
|
const [startStr, endStr] = dateRangeStr.split(' to ');
|
||||||
|
return {
|
||||||
|
start: new Date(startStr.split(' ')[0]),
|
||||||
|
end: new Date(endStr.split(' ')[0])
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 组件挂载时选中今天并获取营期数据
|
||||||
|
onMounted(async () => {
|
||||||
|
const todayDate = calendarDates.value.find(date => date.isToday);
|
||||||
|
if (todayDate) {
|
||||||
|
selectedDate.value = todayDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取现有的营期数据
|
||||||
|
try {
|
||||||
|
await CenterCampPeriodAdmin();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取营期数据失败:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-container {
|
||||||
|
max-width: 100%;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 24px;
|
||||||
|
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-year {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekdays {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday {
|
||||||
|
padding: 12px 8px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell {
|
||||||
|
position: relative;
|
||||||
|
min-height: 44px;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell.other-month {
|
||||||
|
color: #adb5bd;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell.today {
|
||||||
|
background: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell.today .date-number {
|
||||||
|
background: #2196f3;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell.selected {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-number {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff6b6b;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell.has-event .event-dot {
|
||||||
|
background: #4ecdc4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 16px;
|
||||||
|
max-width: 280px;
|
||||||
|
min-width: 200px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-tooltip h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list {
|
||||||
|
space-y: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-events {
|
||||||
|
color: #adb5bd;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.calendar-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-year {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell {
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday {
|
||||||
|
padding: 10px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-tooltip {
|
||||||
|
max-width: 250px;
|
||||||
|
min-width: 180px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-tooltip h3 {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部操作区域 */
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设置营期按钮 */
|
||||||
|
.camp-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: linear-gradient(135deg, #28a745, #20c997);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camp-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #218838, #1ea085);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗样式 */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 480px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #6c757d;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a90e2;
|
||||||
|
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn, .save-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background: linear-gradient(135deg, #4a90e2, #357abd);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #357abd, #2968a3);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(74, 144, 226, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finish-camp {
|
||||||
|
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finish-camp:hover {
|
||||||
|
background: linear-gradient(135deg, #c0392b, #a93226);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 营期事件类型样式 */
|
||||||
|
.event-dot.event-data {
|
||||||
|
background: linear-gradient(135deg, #9c27b0, #7b1fa2);
|
||||||
|
box-shadow: 0 2px 4px rgba(156, 39, 176, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-dot.event-course {
|
||||||
|
background: linear-gradient(135deg, #fd7e14, #e55a00);
|
||||||
|
box-shadow: 0 2px 4px rgba(253, 126, 20, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-dot.event-rest {
|
||||||
|
background: linear-gradient(135deg, #28a745, #1e7e34);
|
||||||
|
box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 休息日文字样式 */
|
||||||
|
.rest-text {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
right: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #28a745;
|
||||||
|
background: rgba(40, 167, 69, 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 1px 3px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -24,23 +24,6 @@
|
|||||||
<p>统筹多组运营,优化资源配置,驱动业绩增长,实现团队协同发展。</p>
|
<p>统筹多组运营,优化资源配置,驱动业绩增长,实现团队协同发展。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 营期阶段信息与调控 -->
|
|
||||||
<div class="stage-info" style="margin-left: 100px;">
|
|
||||||
<span class="stage-label">营期所属阶段:</span>
|
|
||||||
<span class="stage-value">{{ currentStage }}</span>
|
|
||||||
|
|
||||||
<!-- 仅在"接数据"阶段显示调控UI -->
|
|
||||||
<div v-if="isDataReceivingStage" class="stage-control">
|
|
||||||
<span class="control-label">调整"接数据"天数:</span>
|
|
||||||
<input type="number" v-model.number="dataReceivingStage.days" min="1" class="days-input" />
|
|
||||||
<button @click="saveCampSettings" class="save-button">保存</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 非接数据阶段显示结束营期按钮 -->
|
|
||||||
<div v-if="!isDataReceivingStage" class="stage-control">
|
|
||||||
<button @click="finishCamp" class="finish-camp-button">结束营期</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="!isRouteNavigation">
|
<div v-if="!isRouteNavigation">
|
||||||
|
|||||||
Reference in New Issue
Block a user