mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-12-07 21:13:11 +00:00
Compare commits
5 Commits
ca3d7c9dbf
...
752f6b6bd1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
752f6b6bd1 | ||
|
|
8f7b3db552 | ||
|
|
3182699a78 | ||
|
|
46dffb02ca | ||
|
|
069f0a31c0 |
@ -3,13 +3,19 @@
|
|||||||
"Component": true,
|
"Component": true,
|
||||||
"ComponentPublicInstance": true,
|
"ComponentPublicInstance": true,
|
||||||
"ComputedRef": true,
|
"ComputedRef": true,
|
||||||
|
"DirectiveBinding": true,
|
||||||
"EffectScope": true,
|
"EffectScope": true,
|
||||||
"ExtractDefaultPropTypes": true,
|
"ExtractDefaultPropTypes": true,
|
||||||
"ExtractPropTypes": true,
|
"ExtractPropTypes": true,
|
||||||
"ExtractPublicPropTypes": true,
|
"ExtractPublicPropTypes": true,
|
||||||
"InjectionKey": true,
|
"InjectionKey": true,
|
||||||
|
"MaybeRef": true,
|
||||||
|
"MaybeRefOrGetter": true,
|
||||||
"PropType": true,
|
"PropType": true,
|
||||||
"Ref": true,
|
"Ref": true,
|
||||||
|
"ShallowRef": true,
|
||||||
|
"Slot": true,
|
||||||
|
"Slots": true,
|
||||||
"VNode": true,
|
"VNode": true,
|
||||||
"WritableComputedRef": true,
|
"WritableComputedRef": true,
|
||||||
"computed": true,
|
"computed": true,
|
||||||
@ -20,12 +26,14 @@
|
|||||||
"effectScope": true,
|
"effectScope": true,
|
||||||
"getCurrentInstance": true,
|
"getCurrentInstance": true,
|
||||||
"getCurrentScope": true,
|
"getCurrentScope": true,
|
||||||
|
"getCurrentWatcher": true,
|
||||||
"h": true,
|
"h": true,
|
||||||
"inject": true,
|
"inject": true,
|
||||||
"isProxy": true,
|
"isProxy": true,
|
||||||
"isReactive": true,
|
"isReactive": true,
|
||||||
"isReadonly": true,
|
"isReadonly": true,
|
||||||
"isRef": true,
|
"isRef": true,
|
||||||
|
"isShallow": true,
|
||||||
"markRaw": true,
|
"markRaw": true,
|
||||||
"nextTick": true,
|
"nextTick": true,
|
||||||
"onActivated": true,
|
"onActivated": true,
|
||||||
@ -43,6 +51,7 @@
|
|||||||
"onServerPrefetch": true,
|
"onServerPrefetch": true,
|
||||||
"onUnmounted": true,
|
"onUnmounted": true,
|
||||||
"onUpdated": true,
|
"onUpdated": true,
|
||||||
|
"onWatcherCleanup": true,
|
||||||
"provide": true,
|
"provide": true,
|
||||||
"reactive": true,
|
"reactive": true,
|
||||||
"readonly": true,
|
"readonly": true,
|
||||||
@ -60,20 +69,16 @@
|
|||||||
"useAttrs": true,
|
"useAttrs": true,
|
||||||
"useCssModule": true,
|
"useCssModule": true,
|
||||||
"useCssVars": true,
|
"useCssVars": true,
|
||||||
|
"useId": true,
|
||||||
"useLink": true,
|
"useLink": true,
|
||||||
|
"useModel": true,
|
||||||
"useRoute": true,
|
"useRoute": true,
|
||||||
"useRouter": true,
|
"useRouter": true,
|
||||||
"useSlots": true,
|
"useSlots": true,
|
||||||
|
"useTemplateRef": true,
|
||||||
"watch": true,
|
"watch": true,
|
||||||
"watchEffect": true,
|
"watchEffect": true,
|
||||||
"watchPostEffect": true,
|
"watchPostEffect": true,
|
||||||
"watchSyncEffect": true,
|
"watchSyncEffect": true
|
||||||
"DirectiveBinding": true,
|
|
||||||
"MaybeRef": true,
|
|
||||||
"MaybeRefOrGetter": true,
|
|
||||||
"onWatcherCleanup": true,
|
|
||||||
"useId": true,
|
|
||||||
"useModel": true,
|
|
||||||
"useTemplateRef": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
package.json
57
package.json
@ -13,45 +13,46 @@
|
|||||||
"@examaware-cs/core": "^1.0.0",
|
"@examaware-cs/core": "^1.0.0",
|
||||||
"@examaware-cs/player": "^1.0.2",
|
"@examaware-cs/player": "^1.0.2",
|
||||||
"@mdi/font": "7.4.47",
|
"@mdi/font": "7.4.47",
|
||||||
"@microsoft/clarity": "^1.0.0",
|
"@microsoft/clarity": "^1.0.2",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.13.2",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"js-base64": "^3.7.8",
|
"js-base64": "^3.7.8",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.1",
|
||||||
"lucide-vue-next": "^0.545.0",
|
"lucide-vue-next": "^0.555.0",
|
||||||
"marked": "^16.4.0",
|
"marked": "^17.0.1",
|
||||||
"pinyin-pro": "^3.27.0",
|
"pinyin-pro": "^3.27.0",
|
||||||
"ratelimit-header-parser": "^0.1.0",
|
"ratelimit-header-parser": "^0.1.0",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"typewriter-effect": "^2.21.0",
|
"typewriter-effect": "^2.22.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^13.0.0",
|
||||||
"vue": "^3.5.20",
|
"vue": "^3.5.25",
|
||||||
"vue-sonner": "^2.0.9",
|
"vue-sonner": "^2.0.9",
|
||||||
"vuetify": "^3.9.6"
|
"vuetify": "^3.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.39.1",
|
||||||
"@vite-pwa/assets-generator": "^1.0.0",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-n": "^16.6.2",
|
"eslint-plugin-n": "^17.23.1",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^6.4.0",
|
"eslint-plugin-promise": "^7.2.1",
|
||||||
"eslint-plugin-vue": "^9.30.0",
|
"eslint-plugin-vue": "^10.6.2",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.4",
|
||||||
"sass": "1.86.3",
|
"sass": "1.94.2",
|
||||||
"sass-embedded": "^1.86.3",
|
"sass-embedded": "^1.93.3",
|
||||||
"unplugin-auto-import": "^19.1.2",
|
"unplugin-auto-import": "^20.3.0",
|
||||||
"unplugin-fonts": "^1.3.1",
|
"unplugin-fonts": "^1.4.0",
|
||||||
"unplugin-vue-components": "^28.4.1",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"unplugin-vue-router": "^0.12.0",
|
"unplugin-vue-router": "^0.18.0",
|
||||||
"vite": "^5.4.17",
|
"vite": "^5.4.11",
|
||||||
"vite-plugin-pwa": "^1.0.3",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
|
"vite-plugin-vue-devtools": "^7.6.8",
|
||||||
"vite-plugin-vue-layouts": "^0.11.0",
|
"vite-plugin-vue-layouts": "^0.11.0",
|
||||||
"vite-plugin-vuetify": "^2.1.2",
|
"vite-plugin-vuetify": "^2.1.2",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5305
pnpm-lock.yaml
generated
5305
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,81 +1,96 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-slide-y-transition>
|
<div class="floating-toolbar-container">
|
||||||
<v-card
|
<v-slide-y-transition>
|
||||||
:class="{ 'toolbar-expanded': isExpanded }"
|
<v-card
|
||||||
class="floating-toolbar"
|
:class="{ 'toolbar-expanded': isExpanded }"
|
||||||
elevation="4"
|
class="floating-toolbar"
|
||||||
rounded="xl"
|
elevation="4"
|
||||||
>
|
rounded="xl"
|
||||||
|
>
|
||||||
|
<v-btn-group class="toolbar-buttons" variant="text">
|
||||||
|
<v-btn
|
||||||
|
v-ripple
|
||||||
|
:title="'查看昨天'"
|
||||||
|
class="toolbar-btn"
|
||||||
|
icon="mdi-chevron-left"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('prev-day')"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
v-ripple
|
||||||
|
:title="'缩小字体'"
|
||||||
|
class="toolbar-btn"
|
||||||
|
icon="mdi-format-font-size-decrease"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('zoom', 'out')"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
v-ripple
|
||||||
|
:title="'放大字体'"
|
||||||
|
class="toolbar-btn"
|
||||||
|
icon="mdi-format-font-size-increase"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('zoom', 'up')"
|
||||||
|
/>
|
||||||
|
<v-menu :close-on-content-click="false" location="top">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-ripple
|
||||||
|
:title="'选择日期'"
|
||||||
|
class="toolbar-btn"
|
||||||
|
icon="mdi-calendar"
|
||||||
|
v-bind="props"
|
||||||
|
variant="text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<v-card border class="date-picker-card">
|
||||||
|
<v-date-picker
|
||||||
|
:model-value="selectedDate"
|
||||||
|
color="primary"
|
||||||
|
@update:model-value="handleDateSelect"
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
<v-btn
|
||||||
|
v-ripple
|
||||||
|
:loading="loading"
|
||||||
|
:title="'刷新数据'"
|
||||||
|
class="toolbar-btn"
|
||||||
|
icon="mdi-refresh"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('refresh')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
v-if="!isToday"
|
||||||
|
v-ripple
|
||||||
|
:title="'查看明天'"
|
||||||
|
class="toolbar-btn"
|
||||||
|
icon="mdi-chevron-right"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('next-day')"
|
||||||
|
/>
|
||||||
|
</v-btn-group>
|
||||||
|
</v-card>
|
||||||
|
</v-slide-y-transition>
|
||||||
|
|
||||||
<v-btn-group class="toolbar-buttons" variant="text">
|
<!-- Side Action Button -->
|
||||||
<v-btn
|
<v-slide-x-reverse-transition>
|
||||||
v-ripple
|
<v-btn
|
||||||
:title="'查看昨天'"
|
v-if="!isToday"
|
||||||
class="toolbar-btn"
|
:loading="copyToTodayLoading"
|
||||||
icon="mdi-chevron-left"
|
:disabled="copyToTodayLoading"
|
||||||
variant="text"
|
class="side-action-btn"
|
||||||
@click="$emit('prev-day')"
|
color="primary"
|
||||||
/>
|
elevation="4"
|
||||||
<v-btn
|
prepend-icon="mdi-content-copy"
|
||||||
v-ripple
|
rounded="xl"
|
||||||
:title="'缩小字体'"
|
size="large"
|
||||||
class="toolbar-btn"
|
text="复制作业内容到今天"
|
||||||
icon="mdi-format-font-size-decrease"
|
@click="$emit('copy-to-today')"
|
||||||
variant="text"
|
>复制到今天</v-btn>
|
||||||
@click="$emit('zoom', 'out')"
|
</v-slide-x-reverse-transition>
|
||||||
/>
|
</div>
|
||||||
<v-btn
|
|
||||||
v-ripple
|
|
||||||
:title="'放大字体'"
|
|
||||||
class="toolbar-btn"
|
|
||||||
icon="mdi-format-font-size-increase"
|
|
||||||
variant="text"
|
|
||||||
@click="$emit('zoom', 'up')"
|
|
||||||
/>
|
|
||||||
<v-menu :close-on-content-click="false" location="top">
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<v-btn
|
|
||||||
v-ripple
|
|
||||||
:title="'选择日期'"
|
|
||||||
class="toolbar-btn"
|
|
||||||
icon="mdi-calendar"
|
|
||||||
v-bind="props"
|
|
||||||
variant="text"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<v-card border class="date-picker-card">
|
|
||||||
<v-date-picker
|
|
||||||
:model-value="selectedDate"
|
|
||||||
color="primary"
|
|
||||||
@update:model-value="handleDateSelect"
|
|
||||||
/>
|
|
||||||
</v-card>
|
|
||||||
</v-menu>
|
|
||||||
<v-btn
|
|
||||||
v-ripple
|
|
||||||
:loading="loading"
|
|
||||||
:title="'刷新数据'"
|
|
||||||
class="toolbar-btn"
|
|
||||||
icon="mdi-refresh"
|
|
||||||
variant="text"
|
|
||||||
@click="$emit('refresh')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
v-if="!isToday"
|
|
||||||
v-ripple
|
|
||||||
:title="'查看明天'"
|
|
||||||
class="toolbar-btn"
|
|
||||||
icon="mdi-chevron-right"
|
|
||||||
variant="text"
|
|
||||||
@click="$emit('next-day')"
|
|
||||||
/>
|
|
||||||
</v-btn-group>
|
|
||||||
|
|
||||||
|
|
||||||
</v-card>
|
|
||||||
</v-slide-y-transition>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -98,6 +113,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
copyToTodayLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -113,12 +132,24 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.floating-toolbar {
|
.floating-toolbar-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-toolbar {
|
||||||
|
position: absolute;
|
||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 100;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
background: rgba(255, 255, 255, 0.7) !important;
|
background: rgba(255, 255, 255, 0.7) !important;
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
@ -132,7 +163,9 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 0px;
|
padding: 0 4px;
|
||||||
|
pointer-events: auto;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-toolbar:hover {
|
.floating-toolbar:hover {
|
||||||
@ -140,6 +173,15 @@ export default {
|
|||||||
background: rgba(255, 255, 255, 0.8) !important;
|
background: rgba(255, 255, 255, 0.8) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-btn:hover {
|
.toolbar-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.3) !important;
|
background: rgba(255, 255, 255, 0.3) !important;
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
@ -149,6 +191,17 @@ export default {
|
|||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-action-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 101;
|
||||||
|
background: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.date-picker-card {
|
.date-picker-card {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -161,7 +214,8 @@ export default {
|
|||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.floating-toolbar {
|
.floating-toolbar {
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
width: 95%;
|
width: auto;
|
||||||
|
max-width: 95%;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,10 +227,12 @@ export default {
|
|||||||
|
|
||||||
.toolbar-btn {
|
.toolbar-btn {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
min-width: 40px; /* Ensure touch target */
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-btn {
|
.side-action-btn {
|
||||||
margin: 0 2px;
|
bottom: 80px; /* Move above toolbar on mobile */
|
||||||
|
right: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,5 +255,11 @@ export default {
|
|||||||
background: rgba(30, 30, 30, 0.9) !important;
|
background: rgba(30, 30, 30, 0.9) !important;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-action-btn {
|
||||||
|
background: rgba(30, 30, 30, 0.9) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -213,6 +213,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- 非今日编辑警告 -->
|
||||||
|
<v-alert
|
||||||
|
v-if="isEditingPastData"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
class="mx-4 mb-4"
|
||||||
|
border="start"
|
||||||
|
border-color="warning"
|
||||||
|
prominent
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
</template>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-h6 mb-1">你打算修改历史?</div>
|
||||||
|
<div class="text-body-2">
|
||||||
|
这是 {{ new Date(currentDateString.slice(0,4), currentDateString.slice(4,6)-1, currentDateString.slice(6,8)).toLocaleDateString() }} 的作业 • 请谨慎操作,确保不会覆盖重要数据
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
<div class="text-center text-body-2 text-disabled mb-5">
|
<div class="text-center text-body-2 text-disabled mb-5">
|
||||||
点击空白处完成编辑
|
点击空白处完成编辑
|
||||||
</div>
|
</div>
|
||||||
@ -244,6 +264,14 @@ export default {
|
|||||||
autoSave: {
|
autoSave: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
isEditingPastData: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
currentDateString: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue", "save"],
|
emits: ["update:modelValue", "save"],
|
||||||
|
|||||||
@ -43,8 +43,14 @@
|
|||||||
color="red"
|
color="red"
|
||||||
inset
|
inset
|
||||||
>
|
>
|
||||||
|
|
||||||
</v-switch>
|
</v-switch>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="notificationForm.isPersistent"
|
||||||
|
label="常驻展示"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
class="mt-0"
|
||||||
|
></v-checkbox>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-textarea
|
<v-textarea
|
||||||
@ -82,6 +88,42 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 常驻通知管理 -->
|
||||||
|
<v-row class="mt-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
<v-icon class="mr-2">mdi-pin</v-icon>
|
||||||
|
常驻通知管理
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div v-if="persistentNotifications.length === 0" class="text-center text-grey py-4">
|
||||||
|
暂无常驻通知
|
||||||
|
</div>
|
||||||
|
<v-list v-else>
|
||||||
|
<v-list-item
|
||||||
|
v-for="item in persistentNotifications"
|
||||||
|
:key="item.id"
|
||||||
|
:title="item.message"
|
||||||
|
:subtitle="formatTime(item.timestamp)"
|
||||||
|
lines="two"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon :color="item.isUrgent ? 'error' : 'primary'">
|
||||||
|
{{ item.isUrgent ? 'mdi-alert-circle' : 'mdi-information' }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-btn icon="mdi-pencil" variant="text" size="small" @click="openEditDialog(item)"></v-btn>
|
||||||
|
<v-btn icon="mdi-delete" variant="text" color="error" size="small" @click="deletePersistentNotification(item.id)"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<!-- 消息发送历史 -->
|
<!-- 消息发送历史 -->
|
||||||
<v-row class="mt-4">
|
<v-row class="mt-4">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
@ -222,6 +264,57 @@
|
|||||||
|
|
||||||
<ChatWidget />
|
<ChatWidget />
|
||||||
<EventSender ref="eventSender" />
|
<EventSender ref="eventSender" />
|
||||||
|
|
||||||
|
<!-- 编辑常驻通知对话框 -->
|
||||||
|
<v-dialog v-model="editDialog" max-width="500" :fullscreen="$vuetify.display.xs">
|
||||||
|
<v-card>
|
||||||
|
<v-toolbar flat density="compact">
|
||||||
|
<v-toolbar-title>编辑常驻通知</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn icon="mdi-close" @click="editDialog = false"></v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form>
|
||||||
|
<v-textarea
|
||||||
|
v-model="editForm.message"
|
||||||
|
label="通知内容"
|
||||||
|
rows="3"
|
||||||
|
auto-grow
|
||||||
|
></v-textarea>
|
||||||
|
<v-switch
|
||||||
|
v-model="editForm.isUrgent"
|
||||||
|
label="强调通知"
|
||||||
|
color="error"
|
||||||
|
hide-details
|
||||||
|
></v-switch>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="editForm.resend"
|
||||||
|
label="保存并重新发送通知"
|
||||||
|
hint="勾选后将作为新通知发送给所有在线设备"
|
||||||
|
persistent-hint
|
||||||
|
></v-checkbox>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn variant="text" @click="editDialog = false">取消</v-btn>
|
||||||
|
<v-btn color="primary" :loading="savingEdit" @click="saveEdit">保存</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<v-dialog v-model="deleteConfirmDialog" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">确认删除</v-card-title>
|
||||||
|
<v-card-text>确定要删除这条常驻通知吗?此操作无法撤销。</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="grey-darken-1" variant="text" @click="deleteConfirmDialog = false">取消</v-btn>
|
||||||
|
<v-btn color="error" variant="text" @click="executeDelete">删除</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -229,6 +322,7 @@
|
|||||||
import ChatWidget from '@/components/ChatWidget.vue'
|
import ChatWidget from '@/components/ChatWidget.vue'
|
||||||
import EventSender from '@/components/EventSender.vue'
|
import EventSender from '@/components/EventSender.vue'
|
||||||
import { on as socketOn } from '@/utils/socketClient'
|
import { on as socketOn } from '@/utils/socketClient'
|
||||||
|
import dataProvider from '@/utils/dataProvider'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UrgentTestDialog',
|
name: 'UrgentTestDialog',
|
||||||
@ -248,10 +342,22 @@ export default {
|
|||||||
sending: false,
|
sending: false,
|
||||||
notificationForm: {
|
notificationForm: {
|
||||||
isUrgent: false,
|
isUrgent: false,
|
||||||
message: ''
|
message: '',
|
||||||
|
isPersistent: false
|
||||||
},
|
},
|
||||||
sentMessages: [],
|
sentMessages: [],
|
||||||
receiptCleanup: []
|
receiptCleanup: [],
|
||||||
|
persistentNotifications: [],
|
||||||
|
editDialog: false,
|
||||||
|
editForm: {
|
||||||
|
id: null,
|
||||||
|
message: '',
|
||||||
|
isUrgent: false,
|
||||||
|
resend: false
|
||||||
|
},
|
||||||
|
savingEdit: false,
|
||||||
|
deleteConfirmDialog: false,
|
||||||
|
itemToDelete: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -266,6 +372,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setupEventListeners()
|
this.setupEventListeners()
|
||||||
|
this.loadPersistentNotifications()
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.cleanup()
|
this.cleanup()
|
||||||
@ -288,10 +395,13 @@ export default {
|
|||||||
try {
|
try {
|
||||||
// 生成32位随机通知ID
|
// 生成32位随机通知ID
|
||||||
const notificationId = this.generateNotificationId()
|
const notificationId = this.generateNotificationId()
|
||||||
|
const messageContent = this.notificationForm.message
|
||||||
|
const isUrgent = this.notificationForm.isUrgent
|
||||||
|
const isPersistent = this.notificationForm.isPersistent
|
||||||
|
|
||||||
const result = await this.$refs.eventSender.sendNotification(
|
const result = await this.$refs.eventSender.sendNotification(
|
||||||
this.notificationForm.message,
|
messageContent,
|
||||||
this.notificationForm.isUrgent,
|
isUrgent,
|
||||||
[],
|
[],
|
||||||
{ deviceName: '测试设备', deviceType: 'system', isReadOnly: false },
|
{ deviceName: '测试设备', deviceType: 'system', isReadOnly: false },
|
||||||
notificationId
|
notificationId
|
||||||
@ -302,8 +412,8 @@ export default {
|
|||||||
this.sentMessages.push({
|
this.sentMessages.push({
|
||||||
id: eventId,
|
id: eventId,
|
||||||
notificationId: notificationId,
|
notificationId: notificationId,
|
||||||
message: this.notificationForm.message,
|
message: messageContent,
|
||||||
isUrgent: this.notificationForm.isUrgent,
|
isUrgent: isUrgent,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
receipts: {
|
receipts: {
|
||||||
displayed: [],
|
displayed: [],
|
||||||
@ -311,6 +421,36 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 处理常驻通知
|
||||||
|
if (isPersistent) {
|
||||||
|
try {
|
||||||
|
const listKey = 'notification-list'
|
||||||
|
const existingData = await dataProvider.loadData(listKey)
|
||||||
|
let list = []
|
||||||
|
if (existingData && Array.isArray(existingData)) {
|
||||||
|
list = existingData
|
||||||
|
} else if (existingData && existingData.success !== false && Array.isArray(existingData.data)) {
|
||||||
|
// list = existingData.data
|
||||||
|
list = existingData.data
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNotification = {
|
||||||
|
id: notificationId,
|
||||||
|
message: messageContent,
|
||||||
|
isUrgent: isUrgent,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
list.unshift(newNotification)
|
||||||
|
await dataProvider.saveData(listKey, list)
|
||||||
|
// 更新本地列表
|
||||||
|
this.persistentNotifications = list
|
||||||
|
console.log('常驻通知已保存')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存常驻通知失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('通知已发送,事件ID:', eventId, '通知ID:', notificationId)
|
console.log('通知已发送,事件ID:', eventId, '通知ID:', notificationId)
|
||||||
this.resetForm()
|
this.resetForm()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -320,6 +460,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this.notificationForm = {
|
||||||
|
isUrgent: false,
|
||||||
|
message: '',
|
||||||
|
isPersistent: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.dialog = false
|
this.dialog = false
|
||||||
@ -414,6 +561,137 @@ export default {
|
|||||||
return receipts.displayed.filter(device =>
|
return receipts.displayed.filter(device =>
|
||||||
!readSenderIds.includes(device.senderId)
|
!readSenderIds.includes(device.senderId)
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
openEditDialog(notification) {
|
||||||
|
this.editForm = {
|
||||||
|
id: notification.id,
|
||||||
|
message: notification.message,
|
||||||
|
isUrgent: notification.isUrgent || false,
|
||||||
|
resend: false,
|
||||||
|
timestamp: notification.timestamp
|
||||||
|
}
|
||||||
|
this.editDialog = true
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveEdit() {
|
||||||
|
if (!this.editForm.message.trim()) return
|
||||||
|
|
||||||
|
this.savingEdit = true
|
||||||
|
try {
|
||||||
|
// 更新列表
|
||||||
|
const index = this.persistentNotifications.findIndex(n => n.id === this.editForm.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.persistentNotifications[index] = {
|
||||||
|
...this.persistentNotifications[index],
|
||||||
|
message: this.editForm.message,
|
||||||
|
isUrgent: this.editForm.isUrgent,
|
||||||
|
// 如果重新发送,更新时间戳?或者保持原样?通常编辑后更新时间戳比较合理
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
await dataProvider.saveData('notification-list', this.persistentNotifications)
|
||||||
|
|
||||||
|
// 如果需要重新发送
|
||||||
|
if (this.editForm.resend) {
|
||||||
|
const notificationId = this.editForm.id
|
||||||
|
const messageContent = this.editForm.message
|
||||||
|
const isUrgent = this.editForm.isUrgent
|
||||||
|
|
||||||
|
const result = await this.$refs.eventSender.sendNotification(
|
||||||
|
messageContent,
|
||||||
|
isUrgent,
|
||||||
|
[],
|
||||||
|
{ deviceName: '测试设备', deviceType: 'system', isReadOnly: false },
|
||||||
|
notificationId
|
||||||
|
)
|
||||||
|
|
||||||
|
const eventId = result?.eventId || `msg-${Date.now()}`
|
||||||
|
|
||||||
|
// 添加到发送记录
|
||||||
|
this.sentMessages.push({
|
||||||
|
id: eventId,
|
||||||
|
notificationId: notificationId,
|
||||||
|
message: messageContent,
|
||||||
|
isUrgent: isUrgent,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
receipts: {
|
||||||
|
displayed: [],
|
||||||
|
read: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editDialog = false
|
||||||
|
this.$message?.success('已更新')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存失败', e)
|
||||||
|
this.$message?.error('保存失败')
|
||||||
|
} finally {
|
||||||
|
this.savingEdit = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadPersistentNotifications() {
|
||||||
|
try {
|
||||||
|
const res = await dataProvider.loadData('notification-list')
|
||||||
|
if (res && Array.isArray(res)) {
|
||||||
|
this.persistentNotifications = res
|
||||||
|
} else if (res && res.success !== false && Array.isArray(res.data)) {
|
||||||
|
this.persistentNotifications = res.data
|
||||||
|
} else {
|
||||||
|
this.persistentNotifications = []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载常驻通知失败', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteNotification(notificationId) {
|
||||||
|
const confirmed = confirm('确定要删除这个通知吗?')
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从 sentMessages 中删除
|
||||||
|
this.sentMessages = this.sentMessages.filter(msg => msg.id !== notificationId)
|
||||||
|
|
||||||
|
// 从常驻通知列表中删除
|
||||||
|
this.persistentNotifications = this.persistentNotifications.filter(notif => notif.id !== notificationId)
|
||||||
|
|
||||||
|
// TODO: 调用接口删除通知(如果有的话)
|
||||||
|
|
||||||
|
console.log('通知已删除,通知ID:', notificationId)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除通知失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deletePersistentNotification(id) {
|
||||||
|
this.itemToDelete = id
|
||||||
|
this.deleteConfirmDialog = true
|
||||||
|
},
|
||||||
|
|
||||||
|
async executeDelete() {
|
||||||
|
if (!this.itemToDelete) return
|
||||||
|
|
||||||
|
const id = this.itemToDelete
|
||||||
|
this.deleteConfirmDialog = false
|
||||||
|
this.itemToDelete = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.persistentNotifications = this.persistentNotifications.filter(n => n.id !== id)
|
||||||
|
await dataProvider.saveData('notification-list', this.persistentNotifications)
|
||||||
|
this.$message?.success('已删除')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('删除失败', e)
|
||||||
|
this.$message?.error('删除失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deletePersistentNotification(id) {
|
||||||
|
this.itemToDelete = id
|
||||||
|
this.deleteConfirmDialog = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-col
|
<v-col
|
||||||
v-if="studentList && studentList.length"
|
v-if="studentList && studentList.length"
|
||||||
v-ripple="{
|
v-ripple="!isEditingDisabled ? {
|
||||||
class: `text-${
|
class: `text-${
|
||||||
['primary', 'secondary', 'info', 'success', 'warning', 'error'][
|
['primary', 'secondary', 'info', 'success', 'warning', 'error'][
|
||||||
Math.floor(Math.random() * 6)
|
Math.floor(Math.random() * 6)
|
||||||
]
|
]
|
||||||
}`,
|
}`,
|
||||||
}"
|
} : false"
|
||||||
|
:class="{ 'cursor-not-allowed': isEditingDisabled }"
|
||||||
class="attendance-area no-select"
|
class="attendance-area no-select"
|
||||||
cols="1"
|
cols="1"
|
||||||
@click="$emit('click')"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<h1>出勤</h1>
|
<h1>出勤</h1>
|
||||||
<h2>
|
<h2>
|
||||||
@ -94,12 +95,25 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
isEditingDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ["click"],
|
emits: ["click", "disabled-click"],
|
||||||
setup() {
|
setup() {
|
||||||
const display = useDisplay();
|
const display = useDisplay();
|
||||||
return { display };
|
return { display };
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick() {
|
||||||
|
if (this.isEditingDisabled) {
|
||||||
|
this.$emit('disabled-click');
|
||||||
|
} else {
|
||||||
|
this.$emit('click');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -13,11 +13,11 @@
|
|||||||
<!-- 出勤卡片 -->
|
<!-- 出勤卡片 -->
|
||||||
<v-card
|
<v-card
|
||||||
v-if="item.type === 'attendance'"
|
v-if="item.type === 'attendance'"
|
||||||
:class="{ 'glow-highlight': highlightedCards[item.key] }"
|
:class="{ 'glow-highlight': highlightedCards[item.key], 'cursor-not-allowed': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }"
|
||||||
border
|
border
|
||||||
class="glow-track"
|
class="glow-track"
|
||||||
height="100%"
|
height="100%"
|
||||||
@click="$emit('open-attendance')"
|
@click="handleCardClick('attendance', null)"
|
||||||
@mousemove="handleMouseMove"
|
@mousemove="handleMouseMove"
|
||||||
@touchmove="handleTouchMove"
|
@touchmove="handleTouchMove"
|
||||||
>
|
>
|
||||||
@ -82,11 +82,11 @@
|
|||||||
<!-- 自定义/测试卡片 -->
|
<!-- 自定义/测试卡片 -->
|
||||||
<v-card
|
<v-card
|
||||||
v-else-if="item.type === 'custom'"
|
v-else-if="item.type === 'custom'"
|
||||||
:class="{ 'glow-highlight': highlightedCards[item.key] }"
|
:class="{ 'glow-highlight': highlightedCards[item.key], 'cursor-not-allowed': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }"
|
||||||
border
|
border
|
||||||
class="glow-track"
|
class="glow-track"
|
||||||
height="100%"
|
height="100%"
|
||||||
@click="!isEditingDisabled && $emit('open-dialog', item.key)"
|
@click="handleCardClick('dialog', item.key)"
|
||||||
@mousemove="handleMouseMove"
|
@mousemove="handleMouseMove"
|
||||||
@touchmove="handleTouchMove"
|
@touchmove="handleTouchMove"
|
||||||
>
|
>
|
||||||
@ -102,11 +102,11 @@
|
|||||||
<!-- 普通作业卡片 -->
|
<!-- 普通作业卡片 -->
|
||||||
<v-card
|
<v-card
|
||||||
v-else
|
v-else
|
||||||
:class="{ 'glow-highlight': highlightedCards[item.key] }"
|
:class="{ 'glow-highlight': highlightedCards[item.key], 'cursor-not-allowed': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }"
|
||||||
border
|
border
|
||||||
class="glow-track"
|
class="glow-track"
|
||||||
height="100%"
|
height="100%"
|
||||||
@click="!isEditingDisabled && $emit('open-dialog', item.key)"
|
@click="handleCardClick('dialog', item.key)"
|
||||||
@mousemove="handleMouseMove"
|
@mousemove="handleMouseMove"
|
||||||
@touchmove="handleTouchMove"
|
@touchmove="handleTouchMove"
|
||||||
>
|
>
|
||||||
@ -133,11 +133,10 @@
|
|||||||
<v-chip
|
<v-chip
|
||||||
v-for="subject in unusedSubjects"
|
v-for="subject in unusedSubjects"
|
||||||
:key="subject.name"
|
:key="subject.name"
|
||||||
:disabled="isEditingDisabled"
|
|
||||||
class="ma-1"
|
class="ma-1"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
@click="$emit('open-dialog', subject.name)"
|
@click="handleCardClick('dialog', subject.name)"
|
||||||
>
|
>
|
||||||
<v-icon start size="small">mdi-plus</v-icon>
|
<v-icon start size="small">mdi-plus</v-icon>
|
||||||
{{ subject.name }}
|
{{ subject.name }}
|
||||||
@ -149,8 +148,7 @@
|
|||||||
<v-btn
|
<v-btn
|
||||||
v-for="subject in unusedSubjects"
|
v-for="subject in unusedSubjects"
|
||||||
:key="subject.name"
|
:key="subject.name"
|
||||||
:disabled="isEditingDisabled"
|
@click="handleCardClick('dialog', subject.name)"
|
||||||
@click="$emit('open-dialog', subject.name)"
|
|
||||||
>
|
>
|
||||||
<v-icon start> mdi-plus</v-icon>
|
<v-icon start> mdi-plus</v-icon>
|
||||||
{{ subject.name }}
|
{{ subject.name }}
|
||||||
@ -162,10 +160,9 @@
|
|||||||
<v-card
|
<v-card
|
||||||
v-for="subject in unusedSubjects"
|
v-for="subject in unusedSubjects"
|
||||||
:key="subject.name"
|
:key="subject.name"
|
||||||
:disabled="isEditingDisabled"
|
|
||||||
border
|
border
|
||||||
class="empty-subject-card"
|
class="empty-subject-card"
|
||||||
@click="$emit('open-dialog', subject.name)"
|
@click="handleCardClick('dialog', subject.name)"
|
||||||
>
|
>
|
||||||
<v-card-title class="text-subtitle-1">
|
<v-card-title class="text-subtitle-1">
|
||||||
{{ subject.name }}
|
{{ subject.name }}
|
||||||
@ -209,13 +206,25 @@ export default {
|
|||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["open-dialog", "open-attendance"],
|
emits: ["open-dialog", "open-attendance", "disabled-click"],
|
||||||
computed: {
|
computed: {
|
||||||
isMobile() {
|
isMobile() {
|
||||||
return this.$vuetify.display.mobile;
|
return this.$vuetify.display.mobile;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleCardClick(type, key) {
|
||||||
|
if (this.isEditingDisabled) {
|
||||||
|
this.$emit('disabled-click');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'attendance') {
|
||||||
|
this.$emit('open-attendance');
|
||||||
|
} else if (type === 'dialog') {
|
||||||
|
this.$emit('open-dialog', key);
|
||||||
|
}
|
||||||
|
},
|
||||||
splitPoint(content) {
|
splitPoint(content) {
|
||||||
return content.split("\n").filter((text) => text.trim());
|
return content.split("\n").filter((text) => text.trim());
|
||||||
},
|
},
|
||||||
@ -241,3 +250,17 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cursor-not-allowed {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card.cursor-not-allowed:hover {
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -66,6 +66,77 @@
|
|||||||
<div v-if="!shouldShowInit" class="d-flex">
|
<div v-if="!shouldShowInit" class="d-flex">
|
||||||
<!-- 主要内容区域 -->
|
<!-- 主要内容区域 -->
|
||||||
<v-container class="main-window flex-grow-1 no-select bloom-container" fluid>
|
<v-container class="main-window flex-grow-1 no-select bloom-container" fluid>
|
||||||
|
<!-- 常驻通知区域 -->
|
||||||
|
<v-row v-if="persistentNotifications.length > 0" class="mb-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card
|
||||||
|
v-for="notification in persistentNotifications"
|
||||||
|
:key="notification.id"
|
||||||
|
:color="notification.isUrgent ? 'error' : 'primary'"
|
||||||
|
class="mb-2 cursor-pointer"
|
||||||
|
variant="tonal"
|
||||||
|
@click="showNotificationDetail(notification)"
|
||||||
|
>
|
||||||
|
<v-card-text class="d-flex align-center py-3">
|
||||||
|
|
||||||
|
<span class="text-h6 text-truncate font-weight-bold">{{ notification.message }}</span>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn icon="mdi-chevron-right" variant="text"></v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 通知详情对话框 -->
|
||||||
|
<v-dialog v-model="notificationDetailDialog" max-width="700" scrollable>
|
||||||
|
<v-card v-if="currentNotification" class="rounded-xl">
|
||||||
|
<v-card-title class="d-flex align-center pa-4 text-h5">
|
||||||
|
|
||||||
|
<span :class="currentNotification.isUrgent ? 'text-error' : ''" class="font-weight-bold">
|
||||||
|
{{ currentNotification.isUrgent ? '强调通知' : '通知详情' }}
|
||||||
|
</span>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn icon="mdi-close" variant="text" @click="notificationDetailDialog = false"></v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<div class="text-h4 font-weight-medium mb-4" style="line-height: 1.5;">
|
||||||
|
{{ currentNotification.message }}
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle-1 text-grey">
|
||||||
|
发布时间:{{ formatTime(currentNotification.timestamp) }}
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
prepend-icon="mdi-delete"
|
||||||
|
size="x-large"
|
||||||
|
variant="tonal"
|
||||||
|
class="px-6"
|
||||||
|
@click="removePersistentNotification(currentNotification.id)"
|
||||||
|
>
|
||||||
|
删除通知
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
size="x-large"
|
||||||
|
variant="elevated"
|
||||||
|
class="px-8"
|
||||||
|
@click="notificationDetailDialog = false"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
<homework-grid
|
<homework-grid
|
||||||
:sorted-items="sortedItems"
|
:sorted-items="sortedItems"
|
||||||
:unused-subjects="unusedSubjects"
|
:unused-subjects="unusedSubjects"
|
||||||
@ -75,6 +146,7 @@
|
|||||||
:highlighted-cards="highlightedCards"
|
:highlighted-cards="highlightedCards"
|
||||||
@open-dialog="openDialog"
|
@open-dialog="openDialog"
|
||||||
@open-attendance="setAttendanceArea"
|
@open-attendance="setAttendanceArea"
|
||||||
|
@disabled-click="handleDisabledClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<home-actions
|
<home-actions
|
||||||
@ -100,7 +172,9 @@
|
|||||||
v-if="!mobile"
|
v-if="!mobile"
|
||||||
:student-list="state.studentList"
|
:student-list="state.studentList"
|
||||||
:attendance="state.boardData.attendance"
|
:attendance="state.boardData.attendance"
|
||||||
|
:is-editing-disabled="isEditingDisabled"
|
||||||
@click="setAttendanceArea"
|
@click="setAttendanceArea"
|
||||||
|
@disabled-click="handleDisabledClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -109,6 +183,8 @@
|
|||||||
:auto-save="autoSave"
|
:auto-save="autoSave"
|
||||||
:initial-content="state.textarea"
|
:initial-content="state.textarea"
|
||||||
:title="state.dialogTitle"
|
:title="state.dialogTitle"
|
||||||
|
:is-editing-past-data="isEditingPastData"
|
||||||
|
:current-date-string="state.dateString"
|
||||||
@save="handleHomeworkSave"
|
@save="handleHomeworkSave"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -122,7 +198,7 @@
|
|||||||
:attendance="state.boardData.attendance"
|
:attendance="state.boardData.attendance"
|
||||||
:date-string="state.dateString"
|
:date-string="state.dateString"
|
||||||
@save="saveAttendance"
|
@save="saveAttendance"
|
||||||
@change="state.synced = false"
|
@change="handleAttendanceChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<message-log ref="messageLog" />
|
<message-log ref="messageLog" />
|
||||||
@ -131,6 +207,7 @@
|
|||||||
<floating-toolbar
|
<floating-toolbar
|
||||||
:is-today="isToday"
|
:is-today="isToday"
|
||||||
:loading="loading.download"
|
:loading="loading.download"
|
||||||
|
:copy-to-today-loading="loading.copyToToday"
|
||||||
:selected-date="state.selectedDateObj"
|
:selected-date="state.selectedDateObj"
|
||||||
:unread-count="unreadCount"
|
:unread-count="unreadCount"
|
||||||
@refresh="downloadData"
|
@refresh="downloadData"
|
||||||
@ -140,6 +217,7 @@
|
|||||||
@date-select="handleDateSelect"
|
@date-select="handleDateSelect"
|
||||||
@prev-day="navigateDay(-1)"
|
@prev-day="navigateDay(-1)"
|
||||||
@next-day="navigateDay(1)"
|
@next-day="navigateDay(1)"
|
||||||
|
@copy-to-today="copyHomeworkToToday"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 添加ICP备案悬浮组件 -->
|
<!-- 添加ICP备案悬浮组件 -->
|
||||||
@ -220,6 +298,22 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
<!-- 通知详情对话框 -->
|
||||||
|
<v-dialog v-model="notificationDetailDialog" max-width="600">
|
||||||
|
<v-card v-if="currentNotification">
|
||||||
|
<v-card-title class="headline" :class="currentNotification.isUrgent ? 'text-error' : 'text-primary'">
|
||||||
|
{{ currentNotification.isUrgent ? '强调通知' : '通知详情' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="text-h5 py-4">
|
||||||
|
{{ currentNotification.message }}
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn color="error" variant="text" @click="removePersistentNotification(currentNotification.id)">删除</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" @click="notificationDetailDialog = false">关闭</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
<br /><br /><br />
|
<br /><br /><br />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -341,8 +435,10 @@ export default {
|
|||||||
download: false,
|
download: false,
|
||||||
upload: false,
|
upload: false,
|
||||||
students: false,
|
students: false,
|
||||||
|
copyToToday: false,
|
||||||
},
|
},
|
||||||
debouncedUpload: null,
|
debouncedUpload: null,
|
||||||
|
debouncedAttendanceSave: null,
|
||||||
throttledReflow: null,
|
throttledReflow: null,
|
||||||
sortedItemsCache: {
|
sortedItemsCache: {
|
||||||
key: "",
|
key: "",
|
||||||
@ -395,6 +491,11 @@ export default {
|
|||||||
urgentTestDialog: false,
|
urgentTestDialog: false,
|
||||||
// 令牌信息
|
// 令牌信息
|
||||||
tokenInfo: null,
|
tokenInfo: null,
|
||||||
|
|
||||||
|
// 常驻通知
|
||||||
|
persistentNotifications: [],
|
||||||
|
notificationDetailDialog: false,
|
||||||
|
currentNotification: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -527,7 +628,17 @@ export default {
|
|||||||
return getSetting("display.dynamicSort");
|
return getSetting("display.dynamicSort");
|
||||||
},
|
},
|
||||||
isEditingDisabled() {
|
isEditingDisabled() {
|
||||||
return this.state.uploadLoading || this.state.downloadLoading;
|
// 检查是否禁用编辑:加载中、没有编辑权限、或被配置禁止编辑过往数据
|
||||||
|
if (this.state.uploadLoading || this.state.downloadLoading) return true;
|
||||||
|
|
||||||
|
// 检查是否是只读 token
|
||||||
|
const manager = this.$refs.studentNameManager;
|
||||||
|
if (manager?.isReadOnly) return true;
|
||||||
|
|
||||||
|
// 检查是否禁止编辑过往数据
|
||||||
|
if (!this.canEditCurrentDate) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
unreadCount() {
|
unreadCount() {
|
||||||
return this.$refs.messageLog?.unreadCount || 0;
|
return this.$refs.messageLog?.unreadCount || 0;
|
||||||
@ -541,12 +652,25 @@ export default {
|
|||||||
confirmNonTodaySave() {
|
confirmNonTodaySave() {
|
||||||
return getSetting("edit.confirmNonTodaySave");
|
return getSetting("edit.confirmNonTodaySave");
|
||||||
},
|
},
|
||||||
|
blockPastDataEdit() {
|
||||||
|
return getSetting("edit.blockPastDataEdit");
|
||||||
|
},
|
||||||
shouldShowSaveConfirm() {
|
shouldShowSaveConfirm() {
|
||||||
return !this.isToday && this.confirmNonTodaySave;
|
return !this.isToday && this.confirmNonTodaySave;
|
||||||
},
|
},
|
||||||
shouldBlockAutoSave() {
|
shouldBlockAutoSave() {
|
||||||
return !this.isToday && this.autoSave && this.blockNonTodayAutoSave;
|
return !this.isToday && this.autoSave && this.blockNonTodayAutoSave;
|
||||||
},
|
},
|
||||||
|
canEditCurrentDate() {
|
||||||
|
// 检查是否可以编辑当前日期的数据
|
||||||
|
if (this.isToday) return true;
|
||||||
|
if (this.blockPastDataEdit) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
isEditingPastData() {
|
||||||
|
// 是否正在编辑过往数据(非今日数据)
|
||||||
|
return !this.isToday;
|
||||||
|
},
|
||||||
showFullscreenButton() {
|
showFullscreenButton() {
|
||||||
return getSetting("display.showFullscreenButton");
|
return getSetting("display.showFullscreenButton");
|
||||||
},
|
},
|
||||||
@ -615,10 +739,20 @@ export default {
|
|||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
|
"state.attendanceDialog": {
|
||||||
|
handler(newValue) {
|
||||||
|
this.handleAttendanceDialogClose(newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.debouncedUpload = debounce(this.uploadData, 2000);
|
this.debouncedUpload = debounce(this.uploadData, 2000);
|
||||||
|
this.debouncedAttendanceSave = debounce(async () => {
|
||||||
|
if (this.autoSave) {
|
||||||
|
await this.trySave(true);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
this.throttledReflow = throttle(() => {
|
this.throttledReflow = throttle(() => {
|
||||||
if (this.$refs.gridContainer) {
|
if (this.$refs.gridContainer) {
|
||||||
this.optimizeGridLayout(this.sortedItems);
|
this.optimizeGridLayout(this.sortedItems);
|
||||||
@ -693,6 +827,9 @@ export default {
|
|||||||
|
|
||||||
// 获取令牌信息
|
// 获取令牌信息
|
||||||
await this.loadTokenInfo();
|
await this.loadTokenInfo();
|
||||||
|
|
||||||
|
// 加载常驻通知
|
||||||
|
this.loadPersistentNotifications();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("初始化失败:", err);
|
console.error("初始化失败:", err);
|
||||||
this.showError("初始化失败,请刷新页面重试");
|
this.showError("初始化失败,请刷新页面重试");
|
||||||
@ -860,6 +997,11 @@ export default {
|
|||||||
return `${year}${month}${day}`;
|
return `${year}${month}${day}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
formatTime(timestamp) {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
return new Date(timestamp).toLocaleString();
|
||||||
|
},
|
||||||
|
|
||||||
getToday() {
|
getToday() {
|
||||||
return new Date();
|
return new Date();
|
||||||
},
|
},
|
||||||
@ -1083,6 +1225,19 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async openDialog(subject) {
|
async openDialog(subject) {
|
||||||
|
// 检查编辑权限
|
||||||
|
if (this.isEditingDisabled) {
|
||||||
|
const manager = this.$refs.studentNameManager;
|
||||||
|
if (manager?.isReadOnly) {
|
||||||
|
this.$message.warning("无法编辑", "当前使用的是只读令牌");
|
||||||
|
} else if (!this.canEditCurrentDate) {
|
||||||
|
this.$message.warning("无法编辑", "已禁止编辑过往数据");
|
||||||
|
} else {
|
||||||
|
this.$message.warning("无法编辑", "数据加载中,请稍候");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果是自定义卡片
|
// 如果是自定义卡片
|
||||||
if (subject.startsWith('custom-')) {
|
if (subject.startsWith('custom-')) {
|
||||||
this.currentEditSubject = subject;
|
this.currentEditSubject = subject;
|
||||||
@ -1134,10 +1289,25 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setAttendanceArea() {
|
setAttendanceArea() {
|
||||||
|
// 检查编辑权限
|
||||||
|
if (this.isEditingDisabled) {
|
||||||
|
this.handleDisabledClick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.state.attendanceDialog = true;
|
this.state.attendanceDialog = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleDisabledClick() {
|
||||||
|
// 处理点击禁用卡片/区域的情况
|
||||||
|
const manager = this.$refs.studentNameManager;
|
||||||
|
if (manager?.isReadOnly) {
|
||||||
|
this.$message.warning("无法编辑", "当前使用的是只读令牌");
|
||||||
|
} else if (!this.canEditCurrentDate) {
|
||||||
|
this.$message.warning("无法编辑", "已禁止编辑过往数据");
|
||||||
|
} else {
|
||||||
|
this.$message.warning("无法编辑", "数据加载中,请稍候");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
zoom(direction) {
|
zoom(direction) {
|
||||||
const step = 2;
|
const step = 2;
|
||||||
@ -1171,6 +1341,7 @@ export default {
|
|||||||
this.state.refreshInterval = setInterval(() => {
|
this.state.refreshInterval = setInterval(() => {
|
||||||
if (!this.shouldSkipRefresh()) {
|
if (!this.shouldSkipRefresh()) {
|
||||||
this.downloadData();
|
this.downloadData();
|
||||||
|
this.loadPersistentNotifications();
|
||||||
}
|
}
|
||||||
}, interval * 1000);
|
}, interval * 1000);
|
||||||
}
|
}
|
||||||
@ -1289,6 +1460,13 @@ export default {
|
|||||||
const handler = (msg) => {
|
const handler = (msg) => {
|
||||||
// Expect msg = { uuid, key, action, created?, updatedAt?, deletedAt?, batch? }
|
// Expect msg = { uuid, key, action, created?, updatedAt?, deletedAt?, batch? }
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
|
// 检查是否是通知列表更新
|
||||||
|
if (msg.key === 'notification-list') {
|
||||||
|
this.loadPersistentNotifications();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We only care about current date key changes
|
// We only care about current date key changes
|
||||||
const expectedKey = `classworks-data-${this.state.dateString}`;
|
const expectedKey = `classworks-data-${this.state.dateString}`;
|
||||||
if (msg.key !== expectedKey) return;
|
if (msg.key !== expectedKey) return;
|
||||||
@ -1400,6 +1578,11 @@ export default {
|
|||||||
return this.trySave(false);
|
return this.trySave(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleAttendanceChange() {
|
||||||
|
this.state.synced = false;
|
||||||
|
this.debouncedAttendanceSave();
|
||||||
|
},
|
||||||
|
|
||||||
async handleAttendanceDialogClose(newValue) {
|
async handleAttendanceDialogClose(newValue) {
|
||||||
if (!newValue && !this.state.synced) {
|
if (!newValue && !this.state.synced) {
|
||||||
await this.trySave(true);
|
await this.trySave(true);
|
||||||
@ -1734,6 +1917,67 @@ export default {
|
|||||||
this.handleDateSelect(currentDate);
|
this.handleDateSelect(currentDate);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async copyHomeworkToToday() {
|
||||||
|
if (this.loading.copyToToday) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading.copyToToday = true;
|
||||||
|
|
||||||
|
// 1. 保存当前选中日期的作业数据
|
||||||
|
const sourceDate = this.state.dateString;
|
||||||
|
const sourceHomework = JSON.parse(JSON.stringify(this.state.boardData.homework));
|
||||||
|
|
||||||
|
// 2. 切换到今天并加载今天的数据(主要是为了获取考勤等其他数据)
|
||||||
|
const today = this.getToday();
|
||||||
|
const todayString = this.formatDate(today);
|
||||||
|
|
||||||
|
// 临时切换到今天以加载数据
|
||||||
|
this.state.dateString = todayString;
|
||||||
|
await this.downloadData();
|
||||||
|
|
||||||
|
// 3. 直接替换今天的作业数据(删除原有作业,使用源日期的作业)
|
||||||
|
// 深拷贝源日期的作业数据
|
||||||
|
const newHomework = {};
|
||||||
|
for (const key in sourceHomework) {
|
||||||
|
if (sourceHomework[key] && sourceHomework[key].content) {
|
||||||
|
// 如果是自定义卡片,保留完整结构
|
||||||
|
if (sourceHomework[key].type === 'custom') {
|
||||||
|
newHomework[key] = JSON.parse(JSON.stringify(sourceHomework[key]));
|
||||||
|
} else {
|
||||||
|
// 普通作业,只复制内容
|
||||||
|
newHomework[key] = {
|
||||||
|
content: sourceHomework[key].content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接替换作业数据
|
||||||
|
this.state.boardData.homework = newHomework;
|
||||||
|
this.state.synced = false;
|
||||||
|
|
||||||
|
// 4. 保存到今天
|
||||||
|
await this.uploadData();
|
||||||
|
|
||||||
|
// 5. 更新视图状态为今天
|
||||||
|
this.state.selectedDate = todayString;
|
||||||
|
this.state.selectedDateObj = today;
|
||||||
|
this.state.isToday = true;
|
||||||
|
|
||||||
|
// 6. 更新URL
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.delete('date');
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
|
||||||
|
this.$message.success("复制成功", `已将 ${sourceDate} 的作业内容复制到今天(已替换原有作业)`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("复制作业失败:", error);
|
||||||
|
this.$message.error("复制失败", error.message || "请重试");
|
||||||
|
} finally {
|
||||||
|
this.loading.copyToToday = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 解析预配数据
|
// 解析预配数据
|
||||||
parsePreconfigData() {
|
parsePreconfigData() {
|
||||||
try {
|
try {
|
||||||
@ -1801,6 +2045,30 @@ export default {
|
|||||||
console.error("清理URL参数失败:", error);
|
console.error("清理URL参数失败:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadPersistentNotifications() {
|
||||||
|
try {
|
||||||
|
const res = await dataProvider.loadData('notification-list');
|
||||||
|
if (res && Array.isArray(res)) {
|
||||||
|
this.persistentNotifications = res;
|
||||||
|
} else if (res && res.success !== false && Array.isArray(res.data)) {
|
||||||
|
this.persistentNotifications = res.data;
|
||||||
|
} else {
|
||||||
|
this.persistentNotifications = [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载常驻通知失败', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showNotificationDetail(notification) {
|
||||||
|
this.currentNotification = notification;
|
||||||
|
this.notificationDetailDialog = true;
|
||||||
|
},
|
||||||
|
async removePersistentNotification(id) {
|
||||||
|
this.persistentNotifications = this.persistentNotifications.filter(n => n.id !== id);
|
||||||
|
await dataProvider.saveData('notification-list', this.persistentNotifications);
|
||||||
|
this.notificationDetailDialog = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
@import './glow.scss';
|
@use 'glow.scss';
|
||||||
|
|
||||||
// 添加卡片发光效果
|
// 添加卡片发光效果
|
||||||
.glow-track {
|
.glow-track {
|
||||||
|
|||||||
@ -289,6 +289,13 @@ const settingsDefinitions = {
|
|||||||
description: "保存非当天数据需确认",
|
description: "保存非当天数据需确认",
|
||||||
icon: "mdi-calendar-alert",
|
icon: "mdi-calendar-alert",
|
||||||
},
|
},
|
||||||
|
"edit.blockPastDataEdit": {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
description: "禁止编辑过往数据",
|
||||||
|
icon: "mdi-lock-clock",
|
||||||
|
// 启用后将禁止编辑非当天的历史数据,包括作业卡片和出勤统计
|
||||||
|
},
|
||||||
"edit.autoSavePromptText": {
|
"edit.autoSavePromptText": {
|
||||||
type: "string",
|
type: "string",
|
||||||
default: "喵?喵呜!",
|
default: "喵?喵呜!",
|
||||||
|
|||||||
@ -12,12 +12,14 @@ import { VitePWA } from 'vite-plugin-pwa'
|
|||||||
// Utilities
|
// Utilities
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './',
|
base: './',
|
||||||
plugins: [
|
plugins: [
|
||||||
VueRouter(),
|
VueRouter(),
|
||||||
|
vueDevTools(),
|
||||||
Layouts(),
|
Layouts(),
|
||||||
Vue({
|
Vue({
|
||||||
template: { transformAssetUrls }
|
template: { transformAssetUrls }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user