feat(ui): 补齐课程提醒进度并内嵌编辑器与重构后台导航
This commit is contained in:
1 parent
3379eb975c
commit
536379036a
34 files changed
+1559
-455
No files matched your search
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
defaultIslandLayouts,
|
||||
DEFAULT_ISLAND_APPEARANCE,
|
||||
type IslandState,
|
||||
type IslandAppearance,
|
||||
} from '@classhelper/shared';
|
||||
import CustomIsland from '../../../island/CustomIsland.vue';
|
||||
import { getIslandSchedules } from '../../island/bridge.js';
|
||||
const router = useRouter();
|
||||
const settings = ref(defaultIslandLayouts());
|
||||
const appearance = ref<IslandAppearance>({ ...DEFAULT_ISLAND_APPEARANCE });
|
||||
const schedules = ref(getIslandSchedules());
|
||||
const state = ref<IslandState>({
|
||||
mode: 'hidden',
|
||||
active: null,
|
||||
queued: [],
|
||||
inClass: false,
|
||||
currentPeriodEnd: null,
|
||||
reason: null,
|
||||
listExpanded: false,
|
||||
maxCardHeight: 1024,
|
||||
updatedAt: 0,
|
||||
});
|
||||
const ready = ref(false);
|
||||
const error = ref('');
|
||||
let refresh: ReturnType<typeof setInterval>;
|
||||
onMounted(async () => {
|
||||
const config = await window.desktop?.getConfig();
|
||||
if (config) {
|
||||
settings.value = { ...config.islandLayout, layout: 'custom', residentEnabled: true };
|
||||
appearance.value = config.island;
|
||||
await window.desktop?.saveConfig({ islandLayout: JSON.parse(JSON.stringify(settings.value)) });
|
||||
}
|
||||
ready.value = true;
|
||||
refresh = setInterval(() => {
|
||||
schedules.value = getIslandSchedules();
|
||||
}, 1000);
|
||||
});
|
||||
onUnmounted(() => clearInterval(refresh));
|
||||
// 保存按顺序发送;快速拖动不会让较早的响应覆盖较新的草稿。
|
||||
let saving = Promise.resolve();
|
||||
function save(value: typeof settings.value) {
|
||||
settings.value = value;
|
||||
const snapshot = JSON.parse(JSON.stringify(value));
|
||||
saving = saving
|
||||
.then(async () => {
|
||||
await window.desktop?.saveConfig({ islandLayout: snapshot });
|
||||
error.value = '';
|
||||
})
|
||||
.catch(() => {
|
||||
error.value = '保存失败,请重试';
|
||||
});
|
||||
}
|
||||
async function done() {
|
||||
await saving;
|
||||
if (!error.value) await router.push('/settings/island');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<section class="island-editor-page" aria-label="自定义灵动岛编辑器">
|
||||
<p v-if="error" role="alert">{{ error }}</p>
|
||||
<CustomIsland
|
||||
v-if="ready"
|
||||
:settings="settings"
|
||||
:appearance="appearance"
|
||||
:schedules="schedules"
|
||||
:state="state"
|
||||
editing
|
||||
embedded
|
||||
@save="save"
|
||||
@done="done"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
<style>
|
||||
.island-editor-page {
|
||||
height: 100%;
|
||||
min-height: 430px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.island-editor-page .layout-editor {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
flex: 1;
|
||||
min-height: 430px;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
grid-template-columns: 160px minmax(200px, 1fr) 248px;
|
||||
grid-template-rows: 64px minmax(0, 1fr);
|
||||
}
|
||||
.island-editor-page .layout-editor .canvas {
|
||||
padding: 110px 16px 80px;
|
||||
}
|
||||
.island-editor-page .layout-editor .library,
|
||||
.island-editor-page .layout-editor .settings {
|
||||
min-height: 0;
|
||||
padding: 14px 12px;
|
||||
}
|
||||
.island-editor-page .layout-editor .editor-header {
|
||||
padding: 10px 14px;
|
||||
gap: 12px;
|
||||
}
|
||||
.island-editor-page .layout-editor .editor-title p,
|
||||
.island-editor-page .layout-editor .save-state {
|
||||
display: none;
|
||||
}
|
||||
.island-editor-page .layout-editor .scheme-picker {
|
||||
margin-left: auto;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.island-editor-page .layout-editor {
|
||||
grid-template-columns: 130px minmax(140px, 1fr) 220px;
|
||||
}
|
||||
.island-editor-page .layout-editor .component-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.island-editor-page .layout-editor .scheme-picker > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { confirmAction } from '../../composables/confirm.js';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
DEFAULT_ISLAND_APPEARANCE,
|
||||
ISLAND_APPEARANCE_RANGES as RANGES,
|
||||
@@ -23,9 +24,10 @@ import {
|
||||
*/
|
||||
const island = ref<IslandAppearance>({ ...DEFAULT_ISLAND_APPEARANCE });
|
||||
const savingIsland = ref(false);
|
||||
const router = useRouter();
|
||||
function editLayout() {
|
||||
layoutMode.value = 'custom';
|
||||
void window.desktop?.islandEditLayout();
|
||||
void router.push('/settings/island/editor');
|
||||
}
|
||||
const layoutMode = ref<'fixed' | 'custom'>('fixed');
|
||||
const residentEnabled = ref(false);
|
||||
@@ -161,7 +163,7 @@ onMounted(async () => {
|
||||
@click="chooseLayout('fixed')"
|
||||
>
|
||||
<strong>经典灵动岛</strong>
|
||||
<span>有消息时展开,保持熟悉的提醒样式</span>
|
||||
<span>显示课程进度、上下课提醒与班级消息</span>
|
||||
</button>
|
||||
<button
|
||||
class="layout-choice"
|
||||
@@ -208,7 +210,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
<p class="text-muted">紧急消息立即展开;课中普通通知暂存,下课后展开。自定义岛可点击铃铛主动查看。</p>
|
||||
<p class="text-muted">
|
||||
上课前 60 秒显示准备提醒和倒计时,上下课时提示课程状态;课程时间显示在名称右侧。
|
||||
课前提示与倒计时、课中进度、即将下课倒数及上下课提醒,可在「提醒」页分别设置。课程时间显示在名称右侧。
|
||||
</p>
|
||||
<el-form label-width="110px" label-position="left">
|
||||
<el-form-item :label="`${layoutMode === 'custom' ? '通知高度' : '高度'} ${island.height}px`">
|
||||
|
||||
@@ -2,7 +2,26 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { classGradeQueryApi } from '../../api/index.js';
|
||||
import { useAuthStore } from '../../stores/auth.js';
|
||||
import { DEFAULT_LESSON_REMINDERS, type LessonReminderSettings } from '@classhelper/shared';
|
||||
import { ElMessage } from 'element-plus';
|
||||
const auth = useAuthStore();
|
||||
const lesson = ref({ ...DEFAULT_LESSON_REMINDERS });
|
||||
onMounted(async () => {
|
||||
const config = await window.desktop?.getConfig();
|
||||
if (config) lesson.value = { ...config.islandLayout.lessonReminders };
|
||||
});
|
||||
async function saveLesson(patch: Partial<LessonReminderSettings>) {
|
||||
lesson.value = { ...lesson.value, ...patch };
|
||||
const config = await window.desktop?.getConfig();
|
||||
if (!config) return;
|
||||
try {
|
||||
await window.desktop?.saveConfig({
|
||||
islandLayout: { ...config.islandLayout, lessonReminders: { ...lesson.value } },
|
||||
});
|
||||
} catch {
|
||||
ElMessage.error('提醒设置保存失败');
|
||||
}
|
||||
}
|
||||
/* ------------------------------------------------------------ 班级端成绩查询状态(只读)
|
||||
*
|
||||
* 「是否允许班级端查看成绩」是**服务端口径**:管理员在 Web 端「班级管理」里开关,
|
||||
@@ -34,7 +53,51 @@ onMounted(loadGradeQuery);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-card shadow="never" class="lesson-channel-settings">
|
||||
<template #header>上下课提醒</template>
|
||||
<p class="text-muted">
|
||||
沿用 ClassIsland 的遮罩、课前倒计时、上下课提示及下节课预告;课中显示课程进度,临近下课显示倒数。
|
||||
</p>
|
||||
<div
|
||||
v-for="channel in [
|
||||
{ key: 'prepareEnabled', title: '准备上课提醒' },
|
||||
{ key: 'onClassEnabled', title: '上课提醒' },
|
||||
{ key: 'endingEnabled', title: '即将下课倒数' },
|
||||
{ key: 'classProgressEnabled', title: '经典岛课中常显课程进度' },
|
||||
{ key: 'breakEnabled', title: '下课提醒与下节课预告' },
|
||||
] as const"
|
||||
:key="channel.key"
|
||||
class="lesson-setting-row"
|
||||
>
|
||||
<span>{{ channel.title }}</span>
|
||||
<el-switch
|
||||
:model-value="lesson[channel.key]"
|
||||
:aria-label="channel.title"
|
||||
@change="(v: boolean | string | number) => saveLesson({ [channel.key]: v === true })"
|
||||
/>
|
||||
</div>
|
||||
<div class="lesson-setting-row">
|
||||
<span>课前提前(秒)</span>
|
||||
<el-input-number
|
||||
:model-value="lesson.prepareSeconds"
|
||||
:min="5"
|
||||
:max="1800"
|
||||
:step="5"
|
||||
@change="(v: number | undefined) => saveLesson({ prepareSeconds: v ?? 60 })"
|
||||
/>
|
||||
</div>
|
||||
<div class="lesson-setting-row">
|
||||
<span>下课倒数提前(秒)</span>
|
||||
<el-input-number
|
||||
:model-value="lesson.endingSeconds"
|
||||
:min="5"
|
||||
:max="600"
|
||||
:step="5"
|
||||
@change="(v: number | undefined) => saveLesson({ endingSeconds: v ?? 60 })"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="mt-16">
|
||||
<template #header>通知提醒</template>
|
||||
<p class="text-muted">
|
||||
通知与叫人通过 ClassHelper 客户端和灵动岛提醒。普通通知在上课期间暂存,下课后补弹;紧急通知立即提醒。
|
||||
@@ -62,6 +125,17 @@ onMounted(loadGradeQuery);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.lesson-setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--ch-border);
|
||||
}
|
||||
.lesson-setting-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.channel-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in new issue
Block a user