Version: 0.7.9.dev.260326
后端: 1.把最后一块拼图:schedule_refine也搬迁到了agent2,此时agent已经完全解耦。但是它没融入新架构,Codex只尝试把它调整了一部分,回退了一些错误的更改,保持着现在的可运行状态。下次继续改。 2.agent目录先保留,直到refine彻底融入新架构。 3.改善Codex主导的新史山结构:node文件夹里面大量文件,转而改成了module.go+module_tool.go的双文件格局,极大提升架构整洁度和代码可读性。 前端: 1.新开了日历界面,正在保持往前推进。做了很多更改,感觉越来越好了。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
305
frontend/src/components/schedule/CreateTaskClassDialog.vue
Normal file
305
frontend/src/components/schedule/CreateTaskClassDialog.vue
Normal file
@@ -0,0 +1,305 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import type { TaskClassCreatePayload, TaskClassCreateItemPayload } from '@/types/schedule'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
submit: [payload: TaskClassCreatePayload]
|
||||
}>()
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
total_slots: 8,
|
||||
allow_filler_course: true,
|
||||
strategy: 'steady',
|
||||
excluded_slots: [] as number[],
|
||||
items: [
|
||||
{ order: 1, content: '', embedded_time: null },
|
||||
{ order: 2, content: '', embedded_time: null },
|
||||
{ order: 3, content: '', embedded_time: null },
|
||||
] as TaskClassCreateItemPayload[],
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(visible) => {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
|
||||
form.name = ''
|
||||
form.start_date = ''
|
||||
form.end_date = ''
|
||||
form.total_slots = 8
|
||||
form.allow_filler_course = true
|
||||
form.strategy = 'steady'
|
||||
form.excluded_slots = []
|
||||
form.items = [
|
||||
{ order: 1, content: '', embedded_time: null },
|
||||
{ order: 2, content: '', embedded_time: null },
|
||||
{ order: 3, content: '', embedded_time: null },
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
function addItem() {
|
||||
form.items.push({
|
||||
order: form.items.length + 1,
|
||||
content: '',
|
||||
embedded_time: null,
|
||||
})
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
form.items.splice(index, 1)
|
||||
form.items.forEach((item, itemIndex) => {
|
||||
item.order = itemIndex + 1
|
||||
})
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
const filteredItems = form.items
|
||||
.map((item, index) => ({
|
||||
order: index + 1,
|
||||
content: item.content.trim(),
|
||||
embedded_time: null,
|
||||
}))
|
||||
.filter((item) => item.content)
|
||||
|
||||
if (!form.name.trim()) {
|
||||
ElMessage.warning('请先填写任务类名称')
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.start_date || !form.end_date) {
|
||||
ElMessage.warning('请先补齐开始与结束日期')
|
||||
return
|
||||
}
|
||||
|
||||
if (filteredItems.length === 0) {
|
||||
ElMessage.warning('至少添加一个任务块内容')
|
||||
return
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
name: form.name.trim(),
|
||||
start_date: form.start_date,
|
||||
end_date: form.end_date,
|
||||
mode: 'auto',
|
||||
config: {
|
||||
total_slots: form.total_slots,
|
||||
allow_filler_course: form.allow_filler_course,
|
||||
strategy: form.strategy,
|
||||
excluded_slots: form.excluded_slots,
|
||||
},
|
||||
items: filteredItems,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
title="创建任务类"
|
||||
width="720px"
|
||||
align-center
|
||||
class="task-class-dialog"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
>
|
||||
<div class="task-class-dialog__body">
|
||||
<div class="task-class-dialog__grid">
|
||||
<label class="task-class-dialog__field">
|
||||
<span>任务类名称</span>
|
||||
<el-input v-model="form.name" placeholder="例如:数据结构复习" maxlength="64" />
|
||||
</label>
|
||||
|
||||
<label class="task-class-dialog__field">
|
||||
<span>编排策略</span>
|
||||
<el-select v-model="form.strategy">
|
||||
<el-option value="steady" label="均衡推进" />
|
||||
<el-option value="rapid" label="快速冲刺" />
|
||||
</el-select>
|
||||
</label>
|
||||
|
||||
<label class="task-class-dialog__field">
|
||||
<span>开始日期</span>
|
||||
<el-date-picker
|
||||
v-model="form.start_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择开始日期"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="task-class-dialog__field">
|
||||
<span>结束日期</span>
|
||||
<el-date-picker
|
||||
v-model="form.end_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择结束日期"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="task-class-dialog__field">
|
||||
<span>总节数</span>
|
||||
<el-input-number v-model="form.total_slots" :min="1" :max="48" />
|
||||
</label>
|
||||
|
||||
<label class="task-class-dialog__field task-class-dialog__field--switch">
|
||||
<span>允许嵌入水课</span>
|
||||
<el-switch v-model="form.allow_filler_course" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="task-class-dialog__items">
|
||||
<div class="task-class-dialog__items-head">
|
||||
<strong>任务块列表</strong>
|
||||
<button type="button" class="task-class-dialog__add" @click="addItem">新增任务块</button>
|
||||
</div>
|
||||
|
||||
<div class="task-class-dialog__items-list">
|
||||
<div v-for="(item, index) in form.items" :key="index" class="task-class-dialog__item">
|
||||
<span class="task-class-dialog__item-order">{{ index + 1 }}</span>
|
||||
<el-input v-model="item.content" placeholder="填写任务块内容,例如:树与二叉树" />
|
||||
<button
|
||||
type="button"
|
||||
class="task-class-dialog__item-remove"
|
||||
:disabled="form.items.length <= 1"
|
||||
@click="removeItem(index)"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="task-class-dialog__footer">
|
||||
<el-button @click="emit('update:modelValue', false)">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">创建任务类</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.task-class-dialog__body {
|
||||
display: grid;
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.task-class-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.task-class-dialog__field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-class-dialog__field span,
|
||||
.task-class-dialog__items-head strong {
|
||||
color: #1d2940;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-class-dialog__field--switch {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.task-class-dialog__field :deep(.el-input),
|
||||
.task-class-dialog__field :deep(.el-select),
|
||||
.task-class-dialog__field :deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-class-dialog__items {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.task-class-dialog__items-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-class-dialog__add {
|
||||
height: 34px;
|
||||
border: 1px solid rgba(28, 98, 206, 0.18);
|
||||
border-radius: 12px;
|
||||
background: #f6f9ff;
|
||||
color: #1d64d2;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 0 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-class-dialog__items-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.task-class-dialog__item {
|
||||
display: grid;
|
||||
grid-template-columns: 36px minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-class-dialog__item-order {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 12px;
|
||||
background: #eef3f8;
|
||||
color: #354259;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-class-dialog__item-remove {
|
||||
height: 36px;
|
||||
border: 1px solid rgba(185, 42, 29, 0.16);
|
||||
border-radius: 12px;
|
||||
background: #fff6f5;
|
||||
color: #bd3e2f;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-class-dialog__item-remove:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.task-class-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.task-class-dialog__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
419
frontend/src/components/schedule/TaskClassSidebar.vue
Normal file
419
frontend/src/components/schedule/TaskClassSidebar.vue
Normal file
@@ -0,0 +1,419 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { TaskClassDetail, TaskClassListItem } from '@/types/schedule'
|
||||
|
||||
const props = defineProps<{
|
||||
taskClasses: TaskClassListItem[]
|
||||
loading?: boolean
|
||||
detailLoading?: boolean
|
||||
expandedTaskClassId: number | null
|
||||
expandedTaskClassDetail: TaskClassDetail | null
|
||||
selectedTaskClassIds: number[]
|
||||
taskClassMultiSelectMode: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
activate: [taskClassId: number]
|
||||
toggleMultiMode: []
|
||||
create: []
|
||||
deleteItem: [taskItemId: number]
|
||||
}>()
|
||||
|
||||
const taskClassCountLabel = computed(() => `共 ${props.taskClasses.length} 个`)
|
||||
|
||||
function isExpanded(taskClassId: number) {
|
||||
return props.expandedTaskClassId === taskClassId && !props.taskClassMultiSelectMode
|
||||
}
|
||||
|
||||
function isSelected(taskClassId: number) {
|
||||
return props.selectedTaskClassIds.includes(taskClassId)
|
||||
}
|
||||
|
||||
function formatEmbeddedTime(value: TaskClassDetail['items'][number]['embedded_time']) {
|
||||
if (!value?.date) {
|
||||
return '未安排'
|
||||
}
|
||||
|
||||
const date = new Date(value.date)
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${month}.${day} ${value.section_from}-${value.section_to}节`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="task-class-sidebar">
|
||||
<div class="task-class-sidebar__header">
|
||||
<div class="task-class-sidebar__title-row">
|
||||
<div class="task-class-sidebar__title-wrap">
|
||||
<span class="task-class-sidebar__title-icon" aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.40039 5.10742L7.99902 1.59961L13.5996 5.10742L7.99902 8.61523L2.40039 5.10742Z" fill="currentColor" />
|
||||
<path d="M2.40039 8.20312L7.99902 11.7109L13.5996 8.20312" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M2.40039 11.2891L7.99902 14.7969L13.5996 11.2891" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
<strong>任务类别列表</strong>
|
||||
</div>
|
||||
<span class="task-class-sidebar__count">{{ taskClassCountLabel }}</span>
|
||||
</div>
|
||||
|
||||
<button type="button" class="task-class-sidebar__mode" @click="emit('toggleMultiMode')">
|
||||
{{ taskClassMultiSelectMode ? '取消批量' : '批量选择' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="task-class-sidebar__skeleton">
|
||||
<div v-for="index in 4" :key="index" class="task-class-sidebar__skeleton-item" />
|
||||
</div>
|
||||
|
||||
<div v-else class="task-class-sidebar__list">
|
||||
<article
|
||||
v-for="taskClass in taskClasses"
|
||||
:key="taskClass.id"
|
||||
class="task-class-card"
|
||||
:class="{
|
||||
'task-class-card--expanded': isExpanded(taskClass.id),
|
||||
'task-class-card--selected': isSelected(taskClass.id),
|
||||
}"
|
||||
>
|
||||
<button type="button" class="task-class-card__summary" @click="emit('activate', taskClass.id)">
|
||||
<span
|
||||
v-if="taskClassMultiSelectMode"
|
||||
class="task-class-card__selector"
|
||||
:class="{ 'task-class-card__selector--active': isSelected(taskClass.id) }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div class="task-class-card__content">
|
||||
<strong>{{ taskClass.name }}</strong>
|
||||
<span>{{ taskClass.total_slots }}节课</span>
|
||||
</div>
|
||||
|
||||
<span class="task-class-card__corner" aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.22559 3.94922H12.0498V10.7734H10.7998V6.08301L4.3916 12.4912L3.50781 11.6074L9.91602 5.19922H5.22559V3.94922Z" fill="currentColor" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div v-if="isExpanded(taskClass.id)" class="task-class-card__detail">
|
||||
<div v-if="detailLoading" class="task-class-card__detail-loading">正在载入任务块…</div>
|
||||
|
||||
<div v-else-if="expandedTaskClassDetail" class="task-class-card__detail-list">
|
||||
<div
|
||||
v-for="item in expandedTaskClassDetail.items"
|
||||
:key="item.order"
|
||||
class="task-class-card__detail-item"
|
||||
>
|
||||
<span class="task-class-card__detail-order">{{ item.order }}</span>
|
||||
<span class="task-class-card__detail-text">{{ item.content }}</span>
|
||||
<span
|
||||
class="task-class-card__detail-status"
|
||||
:class="{ 'task-class-card__detail-status--arranged': item.embedded_time }"
|
||||
>
|
||||
{{ formatEmbeddedTime(item.embedded_time) }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="task-class-card__detail-delete"
|
||||
aria-label="删除任务块"
|
||||
:disabled="typeof item.id !== 'number'"
|
||||
@click="typeof item.id === 'number' && emit('deleteItem', item.id)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<button type="button" class="task-class-sidebar__create" @click="emit('create')">
|
||||
<span class="task-class-sidebar__create-icon" aria-hidden="true">+</span>
|
||||
<span>点击新建任务类</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.task-class-sidebar {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
border-right: 1px solid rgba(196, 209, 227, 0.55);
|
||||
background: linear-gradient(180deg, rgba(251, 253, 255, 0.96), rgba(247, 250, 254, 0.98));
|
||||
}
|
||||
|
||||
.task-class-sidebar__header {
|
||||
padding: 16px 24px 14px;
|
||||
border-bottom: 1px solid rgba(214, 223, 238, 0.68);
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-class-sidebar__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-class-sidebar__title-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #1f2c42;
|
||||
}
|
||||
|
||||
.task-class-sidebar__title-wrap strong {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-class-sidebar__title-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
color: #165fd0;
|
||||
}
|
||||
|
||||
.task-class-sidebar__count {
|
||||
padding: 5px 12px;
|
||||
border-radius: 10px;
|
||||
background: #eef3f9;
|
||||
color: #75839a;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.task-class-sidebar__mode {
|
||||
height: 34px;
|
||||
border: 1px solid rgba(25, 95, 213, 0.18);
|
||||
border-radius: 12px;
|
||||
background: #f6f9ff;
|
||||
color: #1d64d2;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
justify-self: start;
|
||||
padding: 0 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.16s ease, background-color 0.16s ease, color 0.16s ease;
|
||||
}
|
||||
|
||||
.task-class-sidebar__mode:hover {
|
||||
border-color: rgba(25, 95, 213, 0.34);
|
||||
background: #edf4ff;
|
||||
}
|
||||
|
||||
.task-class-sidebar__list,
|
||||
.task-class-sidebar__skeleton {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.task-class-sidebar__skeleton-item {
|
||||
height: 120px;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(90deg, rgba(234, 239, 246, 0.9), rgba(248, 251, 255, 1), rgba(234, 239, 246, 0.9));
|
||||
background-size: 200% 100%;
|
||||
animation: task-class-skeleton 1.25s linear infinite;
|
||||
}
|
||||
|
||||
.task-class-card {
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(216, 225, 238, 0.9);
|
||||
background: linear-gradient(180deg, #fdfefe 0%, #f8fbff 100%);
|
||||
box-shadow: 0 10px 22px rgba(19, 51, 107, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-class-card--selected {
|
||||
border-color: rgba(28, 98, 206, 0.28);
|
||||
box-shadow: 0 14px 24px rgba(22, 95, 208, 0.08);
|
||||
}
|
||||
|
||||
.task-class-card__summary {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 18px 20px 18px 18px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-class-card__summary:hover .task-class-card__corner {
|
||||
background: #edf4ff;
|
||||
color: #2067d5;
|
||||
}
|
||||
|
||||
.task-class-card__selector {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.55);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.task-class-card__selector--active {
|
||||
border-color: #1e66d4;
|
||||
background: #1e66d4;
|
||||
box-shadow: inset 0 0 0 3px #ffffff;
|
||||
}
|
||||
|
||||
.task-class-card__content {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-class-card__content strong {
|
||||
color: #182741;
|
||||
font-size: 16px;
|
||||
line-height: 1.35;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-class-card__content span {
|
||||
color: #71819a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-class-card__corner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(246, 249, 253, 0.9);
|
||||
color: #1e66d4;
|
||||
transition: background-color 0.16s ease, color 0.16s ease;
|
||||
}
|
||||
|
||||
.task-class-card__detail {
|
||||
padding: 0 14px 14px;
|
||||
}
|
||||
|
||||
.task-class-card__detail-loading {
|
||||
padding: 14px 12px 10px;
|
||||
color: #7b88a1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-class-card__detail-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-class-card__detail-item {
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(197, 209, 226, 0.8);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
display: grid;
|
||||
grid-template-columns: 28px minmax(0, 1fr) auto 24px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-class-card__detail-order {
|
||||
color: #17253d;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task-class-card__detail-text {
|
||||
min-width: 0;
|
||||
color: #1e293b;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.task-class-card__detail-status {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #74839a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.task-class-card__detail-status--arranged {
|
||||
background: #dcf4bd;
|
||||
color: #486a18;
|
||||
}
|
||||
|
||||
.task-class-card__detail-delete {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #bb3326;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-class-card__detail-delete:disabled {
|
||||
opacity: 0.32;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.task-class-sidebar__create {
|
||||
min-height: 108px;
|
||||
border: 1px dashed rgba(204, 216, 232, 0.92);
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(249, 251, 255, 0.98));
|
||||
color: #b1bccd;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-content: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.16s ease, background-color 0.16s ease, color 0.16s ease;
|
||||
}
|
||||
|
||||
.task-class-sidebar__create:hover {
|
||||
border-color: rgba(25, 95, 213, 0.22);
|
||||
background: #f7fbff;
|
||||
color: #6d7f99;
|
||||
}
|
||||
|
||||
.task-class-sidebar__create-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid currentColor;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@keyframes task-class-skeleton {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
327
frontend/src/components/schedule/WeekPlanningBoard.vue
Normal file
327
frontend/src/components/schedule/WeekPlanningBoard.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { ScheduleWeekData, ScheduleWeekEvent } from '@/types/schedule'
|
||||
|
||||
interface WeekDayHeader {
|
||||
dayOfWeek: number
|
||||
label: string
|
||||
dateLabel: string
|
||||
}
|
||||
|
||||
interface SectionSlot {
|
||||
order: number
|
||||
title: string
|
||||
timeRange: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
weekLabel: string
|
||||
weekHeaders: WeekDayHeader[]
|
||||
weekData: ScheduleWeekData | null
|
||||
scheduleSelectionMode: boolean
|
||||
selectedScheduleEventIds: number[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleScheduleEvent: [eventId: number]
|
||||
}>()
|
||||
|
||||
const sectionSlots: SectionSlot[] = [
|
||||
{ order: 1, title: '1-2', timeRange: '08:00\n09:40' },
|
||||
{ order: 2, title: '3-4', timeRange: '10:15\n11:55' },
|
||||
{ order: 3, title: '5-6', timeRange: '14:00\n15:40' },
|
||||
{ order: 4, title: '7-8', timeRange: '16:15\n17:55' },
|
||||
{ order: 5, title: '9-10', timeRange: '19:00\n20:40' },
|
||||
{ order: 6, title: '11-12', timeRange: '20:50\n22:30' },
|
||||
]
|
||||
|
||||
const eventLookup = computed(() => {
|
||||
const map = new Map<string, ScheduleWeekEvent>()
|
||||
|
||||
for (const event of props.weekData?.events ?? []) {
|
||||
map.set(`${event.day_of_week}-${event.order}`, event)
|
||||
}
|
||||
|
||||
return map
|
||||
})
|
||||
|
||||
function resolveEvent(dayOfWeek: number, order: number) {
|
||||
return eventLookup.value.get(`${dayOfWeek}-${order}`)
|
||||
}
|
||||
|
||||
function isSelected(eventId: number) {
|
||||
return props.selectedScheduleEventIds.includes(eventId)
|
||||
}
|
||||
|
||||
function resolveEventTone(event?: ScheduleWeekEvent) {
|
||||
if (!event || event.type === 'empty') {
|
||||
return 'empty'
|
||||
}
|
||||
|
||||
if (event.type === 'course') {
|
||||
return 'course'
|
||||
}
|
||||
|
||||
const toneByOrder: Record<number, string> = {
|
||||
1: 'amber',
|
||||
2: 'mint',
|
||||
3: 'emerald',
|
||||
4: 'rose',
|
||||
5: 'violet',
|
||||
6: 'sky',
|
||||
}
|
||||
|
||||
return toneByOrder[event.order] ?? 'task'
|
||||
}
|
||||
|
||||
function resolveCellTitle(event?: ScheduleWeekEvent) {
|
||||
if (!event || event.type === 'empty') {
|
||||
return '空'
|
||||
}
|
||||
return event.name
|
||||
}
|
||||
|
||||
function resolveCellMeta(event?: ScheduleWeekEvent) {
|
||||
if (!event || event.type === 'empty') {
|
||||
return ''
|
||||
}
|
||||
return event.location || '未定'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="planning-board">
|
||||
<header class="planning-board__header">
|
||||
<strong>{{ weekLabel }}</strong>
|
||||
</header>
|
||||
|
||||
<div class="planning-board__grid">
|
||||
<div class="planning-board__corner" />
|
||||
|
||||
<div v-for="header in weekHeaders" :key="header.dayOfWeek" class="planning-board__day-head">
|
||||
<span>{{ header.label }}</span>
|
||||
<small>{{ header.dateLabel }}</small>
|
||||
</div>
|
||||
|
||||
<template v-for="slot in sectionSlots" :key="slot.order">
|
||||
<div class="planning-board__time-cell">
|
||||
<strong>{{ slot.title }}</strong>
|
||||
<small>{{ slot.timeRange }}</small>
|
||||
</div>
|
||||
|
||||
<article
|
||||
v-for="header in weekHeaders"
|
||||
:key="`${header.dayOfWeek}-${slot.order}`"
|
||||
class="planning-board__cell"
|
||||
:class="[
|
||||
`planning-board__cell--${resolveEventTone(resolveEvent(header.dayOfWeek, slot.order))}`,
|
||||
{
|
||||
'planning-board__cell--selectable': scheduleSelectionMode && resolveEvent(header.dayOfWeek, slot.order)?.type !== 'empty',
|
||||
'planning-board__cell--selected': resolveEvent(header.dayOfWeek, slot.order) && isSelected(resolveEvent(header.dayOfWeek, slot.order)!.id),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<button
|
||||
v-if="scheduleSelectionMode && resolveEvent(header.dayOfWeek, slot.order)?.type !== 'empty'"
|
||||
type="button"
|
||||
class="planning-board__checkbox"
|
||||
:class="{ 'planning-board__checkbox--active': isSelected(resolveEvent(header.dayOfWeek, slot.order)!.id) }"
|
||||
@click="emit('toggleScheduleEvent', resolveEvent(header.dayOfWeek, slot.order)!.id)"
|
||||
/>
|
||||
|
||||
<template v-if="resolveEvent(header.dayOfWeek, slot.order)">
|
||||
<div class="planning-board__cell-main">
|
||||
<strong>{{ resolveCellTitle(resolveEvent(header.dayOfWeek, slot.order)) }}</strong>
|
||||
<span>{{ resolveCellMeta(resolveEvent(header.dayOfWeek, slot.order)) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.planning-board {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border-radius: 28px;
|
||||
border: 1px solid rgba(214, 223, 236, 0.82);
|
||||
background: linear-gradient(180deg, rgba(252, 253, 255, 0.98), rgba(248, 251, 255, 0.98));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.planning-board__header {
|
||||
padding: 18px 28px 16px;
|
||||
border-bottom: 1px solid rgba(221, 229, 240, 0.86);
|
||||
color: #1f2b42;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.planning-board__grid {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 74px repeat(7, minmax(0, 1fr));
|
||||
gap: 10px 12px;
|
||||
padding: 28px 24px 24px;
|
||||
}
|
||||
|
||||
.planning-board__corner {
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
.planning-board__day-head {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 4px;
|
||||
color: #8ca0bd;
|
||||
}
|
||||
|
||||
.planning-board__day-head span {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.planning-board__day-head small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.planning-board__time-cell {
|
||||
min-height: 112px;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: end;
|
||||
color: #9aacbf;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.planning-board__time-cell strong {
|
||||
font-size: 15px;
|
||||
color: #8da0bc;
|
||||
}
|
||||
|
||||
.planning-board__time-cell small {
|
||||
white-space: pre-line;
|
||||
text-align: right;
|
||||
line-height: 1.35;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.planning-board__cell {
|
||||
position: relative;
|
||||
min-height: 112px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid rgba(228, 234, 243, 0.92);
|
||||
padding: 18px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.planning-board__cell-main {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.planning-board__cell-main strong {
|
||||
color: #7387a3;
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.planning-board__cell-main span {
|
||||
color: #9badc5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.planning-board__cell--course {
|
||||
background: #acd6f4;
|
||||
}
|
||||
|
||||
.planning-board__cell--course .planning-board__cell-main strong,
|
||||
.planning-board__cell--course .planning-board__cell-main span {
|
||||
color: #2576cc;
|
||||
}
|
||||
|
||||
.planning-board__cell--amber {
|
||||
background: #ffe58b;
|
||||
}
|
||||
|
||||
.planning-board__cell--amber .planning-board__cell-main strong,
|
||||
.planning-board__cell--amber .planning-board__cell-main span {
|
||||
color: #7d6917;
|
||||
}
|
||||
|
||||
.planning-board__cell--mint {
|
||||
background: #d7f7a7;
|
||||
}
|
||||
|
||||
.planning-board__cell--mint .planning-board__cell-main strong,
|
||||
.planning-board__cell--mint .planning-board__cell-main span,
|
||||
.planning-board__cell--emerald .planning-board__cell-main strong,
|
||||
.planning-board__cell--emerald .planning-board__cell-main span {
|
||||
color: #72a91d;
|
||||
}
|
||||
|
||||
.planning-board__cell--emerald {
|
||||
background: #d3f3ac;
|
||||
}
|
||||
|
||||
.planning-board__cell--rose {
|
||||
background: #f6dfe2;
|
||||
}
|
||||
|
||||
.planning-board__cell--rose .planning-board__cell-main strong,
|
||||
.planning-board__cell--rose .planning-board__cell-main span {
|
||||
color: #e6696e;
|
||||
}
|
||||
|
||||
.planning-board__cell--violet {
|
||||
background: #e9dcfb;
|
||||
}
|
||||
|
||||
.planning-board__cell--sky {
|
||||
background: #d8ecfb;
|
||||
}
|
||||
|
||||
.planning-board__cell--empty {
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.planning-board__cell--selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.planning-board__cell--selected {
|
||||
box-shadow: inset 0 0 0 2px rgba(32, 102, 212, 0.52);
|
||||
}
|
||||
|
||||
.planning-board__checkbox {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(118, 133, 160, 0.46);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.planning-board__checkbox--active {
|
||||
border-color: #1e66d4;
|
||||
background: #1e66d4;
|
||||
box-shadow: inset 0 0 0 3px #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 1560px) {
|
||||
.planning-board__grid {
|
||||
grid-template-columns: 64px repeat(7, minmax(118px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user