1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-07-06 12:09:22 +00:00

Update .hintrc to disable no-inline-styles hint, modify GlobalMessage.vue to reposition snackbar, remove AppFooter from default layout, and enhance index.vue with a floating toolbar and ICP component. Clean up unused imports in settings.vue.

This commit is contained in:
SunWuyuan 2025-07-05 10:05:52 +08:00
parent 965f36cbf5
commit 53ed1f556f
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
8 changed files with 322 additions and 144 deletions

View File

@ -1,5 +1,8 @@
{
"extends": [
"development"
]
],
"hints": {
"no-inline-styles": "off"
}
}

View File

@ -14,5 +14,6 @@
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" style="display: none;">浙ICP备2024068645号-4</a>
</body>
</html>

View File

@ -0,0 +1,77 @@
<template>
<v-slide-x-transition>
<v-card
class="floating-icp"
elevation="2"
rounded="pill"
variant="tonal"
color="surface-variant"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
>
<v-btn
variant="text"
class="icp-button"
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
>
<v-icon
icon="mdi-shield-check"
size="small"
:class="{ 'rotate-icon': isHovered }"
class="mr-1"
/>
<span class="text-caption">浙ICP备2024068645号</span>
</v-btn>
</v-card>
</v-slide-x-transition>
</template>
<script>
export default {
name: 'FloatingICP',
data() {
return {
isHovered: false
}
}
}
</script>
<style scoped>
.floating-icp {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 100;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.floating-icp:hover {
transform: translateX(-4px);
}
.icp-button {
padding: 0 16px;
height: 32px;
min-width: unset;
}
.rotate-icon {
transform: rotate(360deg);
transition: transform 0.6s ease;
}
@media (max-width: 600px) {
.floating-icp {
right: 16px;
bottom: 80px; /* 避免与其他悬浮组件重叠 */
}
.icp-button {
padding: 0 12px;
}
}
</style>

View File

@ -0,0 +1,203 @@
<template>
<v-slide-y-transition>
<v-card
class="floating-toolbar"
elevation="4"
rounded="xl"
:class="{ 'toolbar-expanded': isExpanded }"
>
<v-btn-group variant="text" class="toolbar-buttons">
<v-btn
icon="mdi-chevron-left"
variant="text"
@click="$emit('prev-day')"
:title="'查看昨天'"
class="toolbar-btn"
v-ripple
/>
<v-btn
icon="mdi-format-font-size-decrease"
variant="text"
@click="$emit('zoom', 'out')"
:title="'缩小字体'"
class="toolbar-btn"
v-ripple
/>
<v-btn
icon="mdi-format-font-size-increase"
variant="text"
@click="$emit('zoom', 'up')"
:title="'放大字体'"
class="toolbar-btn"
v-ripple
/>
<v-menu location="top" :close-on-content-click="false">
<template #activator="{ props }">
<v-btn
icon="mdi-calendar"
variant="text"
v-bind="props"
:title="'选择日期'"
class="toolbar-btn"
v-ripple
/>
</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
icon="mdi-refresh"
variant="text"
:loading="loading"
@click="$emit('refresh')"
:title="'刷新数据'"
class="toolbar-btn"
v-ripple
/>
<v-btn
v-if="!isToday"
icon="mdi-chevron-right"
variant="text"
@click="$emit('next-day')"
:title="'查看明天'"
class="toolbar-btn"
v-ripple
/>
</v-btn-group>
</v-card>
</v-slide-y-transition>
</template>
<script>
export default {
name: "FloatingToolbar",
props: {
loading: {
type: Boolean,
default: false,
},
unreadCount: {
type: Number,
default: 0,
},
selectedDate: {
type: [String, Date],
required: true,
},
isToday: {
type: Boolean,
required: true,
},
},
data() {
return {
isExpanded: false,
};
},
methods: {
handleDateSelect(newDate) {
this.$emit("date-select", newDate);
},
},
};
</script>
<style scoped>
.floating-toolbar {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: rgba(255, 255, 255, 0.7) !important;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1) !important;
touch-action: none;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0px;
}
.floating-toolbar:hover {
transform: translateX(-50%) translateY(-4px);
background: rgba(255, 255, 255, 0.8) !important;
}
.toolbar-btn:hover {
background: rgba(255, 255, 255, 0.3) !important;
transform: scale(1.05);
}
.toolbar-btn:active {
transform: scale(0.95);
}
.date-picker-card {
border-radius: 16px;
overflow: hidden;
background: rgba(255, 255, 255, 0.9) !important;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
@media (max-width: 600px) {
.floating-toolbar {
bottom: 16px;
width: 95%;
padding: 2px;
}
.toolbar-buttons {
width: 100%;
justify-content: space-around;
padding: 4px;
}
.toolbar-btn {
margin: 0;
}
.nav-btn {
margin: 0 2px;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.floating-toolbar {
background: rgba(30, 30, 30, 0.7) !important;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.floating-toolbar:hover {
background: rgba(30, 30, 30, 0.8) !important;
}
.toolbar-btn:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
.date-picker-card {
background: rgba(30, 30, 30, 0.9) !important;
border: 1px solid rgba(255, 255, 255, 0.1);
}
}
</style>

View File

@ -3,7 +3,7 @@
v-model="snackbar"
:color="colors[message?.type] || colors.info"
:timeout="2000"
location="bottom"
location="top right"
multi-line
variant="tonal"
>

View File

@ -3,8 +3,6 @@
<v-main>
<router-view />
</v-main>
<AppFooter />
</v-app>
</template>

View File

@ -1,7 +1,5 @@
<template>
<v-app-bar class="no-select">
<v-app-bar-title>
{{ state.classNumber }} - {{ titleText }}
</v-app-bar-title>
@ -9,37 +7,7 @@
<v-spacer />
<template #append>
<namespace-access />
<v-btn
icon="mdi-format-font-size-decrease"
variant="text"
@click="zoom('out')"
/>
<v-btn
icon="mdi-format-font-size-increase"
variant="text"
@click="zoom('up')"
/>
<v-menu v-model="state.datePickerDialog" :close-on-content-click="false">
<template #activator="{ props }">
<v-btn icon="mdi-calendar" variant="text" v-bind="props" />
</template>
<v-card border>
<v-date-picker
v-model="state.selectedDateObj"
:model-value="state.selectedDateObj"
color="primary"
@update:model-value="handleDateSelect"
/>
</v-card>
</v-menu>
<v-btn
icon="mdi-refresh"
variant="text"
:loading="loading.download"
@click="downloadData"
/>
<v-btn
<namespace-access /> <v-btn
icon="mdi-bell"
variant="text"
:badge="unreadCount || undefined"
@ -559,6 +527,24 @@
<message-log ref="messageLog" />
<!-- 添加悬浮工具栏 -->
<floating-toolbar
:loading="loading.download"
:unread-count="unreadCount"
:selected-date="state.selectedDateObj"
:is-today="isToday"
@zoom="zoom"
@refresh="downloadData"
@open-messages="$refs.messageLog.drawer = true"
@open-settings="$router.push('/settings')"
@date-select="handleDateSelect"
@prev-day="navigateDay(-1)"
@next-day="navigateDay(1)"
/>
<!-- 添加ICP备案悬浮组件 -->
<FloatingICP />
<!-- 添加确认对话框 -->
<v-dialog v-model="confirmDialog.show" max-width="400">
<v-card>
@ -627,13 +613,15 @@
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-dialog><br/><br/><br/><br/><br/><br/>
</template>
<script>
import MessageLog from "@/components/MessageLog.vue";
import RandomPicker from "@/components/RandomPicker.vue"; //
import RandomPicker from "@/components/RandomPicker.vue";
import NamespaceAccess from "@/components/NamespaceAccess.vue";
import FloatingToolbar from "@/components/FloatingToolbar.vue";
import FloatingICP from "@/components/FloatingICP.vue";
import dataProvider from "@/utils/dataProvider";
import {
getSetting,
@ -643,7 +631,7 @@ import {
} from "@/utils/settings";
import { useDisplay } from "vuetify";
import "../styles/index.scss";
import "../styles/transitions.scss"; //
import "../styles/transitions.scss";
import "../styles/global.scss";
import { pinyin } from "pinyin-pro";
import { debounce, throttle } from "@/utils/debounce";
@ -653,8 +641,10 @@ export default {
name: "Classworks 作业板",
components: {
MessageLog,
RandomPicker, //
RandomPicker,
NamespaceAccess,
FloatingToolbar,
FloatingICP,
},
data() {
return {
@ -675,7 +665,7 @@ export default {
dialogVisible: false,
dialogTitle: "",
textarea: "",
dateString: "", // state
dateString: "",
synced: false,
attendDialogVisible: false,
contentStyle: { "font-size": `${getSetting("font.size")}px` },
@ -736,7 +726,6 @@ export default {
},
attendanceSearch: "",
attendanceFilter: [],
// URL
urlConfigDialog: {
show: false,
config: null,
@ -744,7 +733,6 @@ export default {
validSettings: {},
confirmHandler: null,
cancelHandler: null,
//
icons: {},
},
};
@ -832,15 +820,12 @@ export default {
return this.state.dateString === today;
},
canAutoSave() {
//
return this.autoSave && (!this.blockNonTodayAutoSave || this.isToday);
},
needConfirmSave() {
//
return !this.isToday && this.confirmNonTodaySave;
},
shouldShowBlockedMessage() {
//
return !this.isToday && this.autoSave && this.blockNonTodayAutoSave;
},
refreshBeforeEdit() {
@ -882,7 +867,6 @@ export default {
filteredStudents() {
let students = [...this.state.studentList];
//
if (this.attendanceSearch) {
const searchTerm = this.attendanceSearch.toLowerCase();
students = students.filter((student) =>
@ -890,7 +874,6 @@ export default {
);
}
//
if (this.attendanceFilter && this.attendanceFilter.length > 0) {
students = students.filter((student) => {
const index = this.state.studentList.indexOf(student);
@ -915,7 +898,6 @@ export default {
return students;
},
extractedSurnames() {
//
if (!this.state.studentList || this.state.studentList.length === 0) {
return [];
}
@ -924,7 +906,6 @@ export default {
this.state.studentList.forEach((student) => {
if (student && student.length > 0) {
//
const surname = student.charAt(0);
if (surnameMap.has(surname)) {
surnameMap.set(surname, surnameMap.get(surname) + 1);
@ -934,7 +915,6 @@ export default {
}
});
//
return Array.from(surnameMap.entries())
.map(([name, count]) => ({ name, count }))
.sort((a, b) => {
@ -965,9 +945,7 @@ export default {
},
created() {
//
this.debouncedUpload = debounce(this.uploadData, 2000);
//
this.throttledReflow = throttle(() => {
if (this.$refs.gridContainer) {
this.optimizeGridLayout(this.sortedItems);
@ -984,7 +962,6 @@ export default {
this.updateSettings();
});
//
document.addEventListener(
"fullscreenchange",
this.fullscreenChangeHandler
@ -1002,10 +979,8 @@ export default {
this.fullscreenChangeHandler
);
// URL#pick
this.checkHashForRandomPicker();
//
window.addEventListener("hashchange", this.checkHashForRandomPicker);
} catch (err) {
console.error("初始化失败:", err);
@ -1018,11 +993,9 @@ export default {
this.unwatchSettings();
}
if (this.state.refreshInterval) {
// state
clearInterval(this.state.refreshInterval);
}
//
document.removeEventListener(
"fullscreenchange",
this.fullscreenChangeHandler
@ -1040,12 +1013,10 @@ export default {
this.fullscreenChangeHandler
);
//
window.removeEventListener("hashchange", this.checkHashForRandomPicker);
},
methods: {
//
ensureDate(dateInput) {
if (dateInput instanceof Date) {
return dateInput;
@ -1056,7 +1027,7 @@ export default {
return date;
}
}
return new Date(); //
return new Date();
},
formatDate(dateInput) {
@ -1072,18 +1043,14 @@ export default {
},
async initializeData() {
// URL
const configApplied = await this.parseUrlConfig();
// URL 使
const urlParams = new URLSearchParams(window.location.search);
const dateFromUrl = urlParams.get("date");
const today = this.getToday();
//
let currentDate = today;
if (dateFromUrl) {
// yyyymmdd
if (/^\d{8}$/.test(dateFromUrl)) {
const year = dateFromUrl.substring(0, 4);
const month = dateFromUrl.substring(4, 6);
@ -1092,7 +1059,6 @@ export default {
} else {
currentDate = new Date(dateFromUrl);
}
// 使
if (isNaN(currentDate.getTime())) {
currentDate = today;
}
@ -1100,10 +1066,9 @@ export default {
this.state.dateString = this.formatDate(currentDate);
this.state.selectedDate = this.state.dateString;
this.state.selectedDateObj = currentDate; //
this.state.selectedDateObj = currentDate;
this.state.isToday =
this.formatDate(currentDate) === this.formatDate(today);
// URL使
if (!configApplied) {
this.provider = getSetting("server.provider");
const classNum = getSetting("server.classNumber");
@ -1126,7 +1091,6 @@ export default {
if (response.error.code === "NOT_FOUND") {
this.state.showNoDataMessage = true;
this.state.noDataMessage = response.error.message;
//
this.state.boardData = {
homework: {},
attendance: { absent: [], late: [], exclude: [] },
@ -1135,7 +1099,6 @@ export default {
throw new Error(response.error.message);
}
} else {
//
this.state.boardData = {
homework: response.homework || {},
attendance: {
@ -1149,7 +1112,6 @@ export default {
this.$message.success("下载成功", "数据已更新");
}
} catch (error) {
//
this.state.boardData = {
homework: {},
attendance: { absent: [], late: [], exclude: [] },
@ -1161,7 +1123,6 @@ export default {
},
async trySave(isAutoSave = false) {
//
if (isAutoSave && !this.canAutoSave) {
if (this.shouldShowBlockedMessage) {
this.showMessage(
@ -1173,7 +1134,6 @@ export default {
return false;
}
//
if (!isAutoSave && this.needConfirmSave) {
try {
await this.showConfirmDialog();
@ -1182,7 +1142,6 @@ export default {
}
}
//
try {
await this.uploadData();
return true;
@ -1199,16 +1158,13 @@ export default {
const originalContent =
this.state.boardData.homework[this.currentEditSubject]?.content || "";
// ()
if (content !== originalContent.trim()) {
//
this.state.boardData.homework[this.currentEditSubject] = {
content: content,
};
this.state.synced = false;
//
if (this.autoSave) {
await this.trySave(true);
}
@ -1240,11 +1196,9 @@ export default {
async loadConfig() {
try {
try {
// Try to get student list from the dedicated key
const response = await dataProvider.loadData("classworks-list-main");
if (response.success != false && Array.isArray(response)) {
// Transform the data into a simple list of names
this.state.studentList = response.map(
(student) => student.name
);
@ -1277,7 +1231,6 @@ export default {
}
this.currentEditSubject = subject;
//
if (!this.state.boardData.homework[subject]) {
this.state.boardData.homework[subject] = {
content: "",
@ -1371,7 +1324,6 @@ export default {
}
if (autoRefresh) {
this.state.refreshInterval = setInterval(() => {
//
if (!this.shouldSkipRefresh()) {
this.downloadData();
}
@ -1379,27 +1331,19 @@ export default {
}
},
//
shouldSkipRefresh() {
//
if (this.state.dialogVisible) return true;
//
if (this.state.attendanceDialog) return true;
//
if (this.confirmDialog.show) return true;
//
if (this.state.datePickerDialog) return true;
//
if (this.loading.upload || this.loading.download) return true;
//
if (!this.state.synced) return true;
//
return false;
},
@ -1417,7 +1361,6 @@ export default {
const selectedDate = this.ensureDate(newDate);
const formattedDate = this.formatDate(selectedDate);
//
if (this.state.dateString !== formattedDate) {
this.state.dateString = formattedDate;
this.state.selectedDate = formattedDate;
@ -1425,7 +1368,6 @@ export default {
this.state.isToday =
formattedDate === this.formatDate(this.getToday());
// 使 replace push
this.$router
.replace({
query: { date: formattedDate },
@ -1440,11 +1382,9 @@ export default {
},
optimizeGridLayout(items) {
//
const maxColumns = Math.min(3, Math.floor(window.innerWidth / 300));
if (maxColumns <= 1) return items;
// 使
const columns = Array.from({ length: maxColumns }, () => ({
height: 0,
items: [],
@ -1459,7 +1399,6 @@ export default {
columns[shortestColumn].height += item.rowSpan;
});
//
return columns
.flatMap((col) => col.items)
.map((item, index) => ({
@ -1566,7 +1505,6 @@ export default {
setPresent(index) {
const student = this.state.studentList[index];
//
this.state.boardData.attendance.absent =
this.state.boardData.attendance.absent.filter(
(name) => name !== student
@ -1582,27 +1520,21 @@ export default {
setAbsent(index) {
const student = this.state.studentList[index];
//
this.setPresent(index);
//
this.state.boardData.attendance.absent.push(student);
this.state.synced = false;
},
setLate(index) {
const student = this.state.studentList[index];
//
this.setPresent(index);
//
this.state.boardData.attendance.late.push(student);
this.state.synced = false;
},
setExclude(index) {
const student = this.state.studentList[index];
//
this.setPresent(index);
//
this.state.boardData.attendance.exclude.push(student);
this.state.synced = false;
},
@ -1679,19 +1611,16 @@ export default {
}
},
//
async manualUpload() {
return this.trySave(false);
},
async handleAttendanceDialogClose(newValue) {
if (!newValue && !this.state.synced) {
//
await this.trySave(true);
}
},
//
toggleFullscreen() {
if (!this.state.isFullscreen) {
this.enterFullscreen();
@ -1805,7 +1734,6 @@ export default {
}
},
// URL
parseUrlConfig() {
try {
const urlParams = new URLSearchParams(window.location.search);
@ -1814,24 +1742,18 @@ export default {
if (!configParam) return false;
try {
// base64
const binaryString = atob(configParam);
// Uint8Array
const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));
// Uint8ArrayUTF-8
const decodedString = new TextDecoder().decode(bytes);
const decodedConfig = JSON.parse(decodedString);
console.log("从URL读取配置:", decodedConfig);
//
const changes = [];
const validSettings = {};
const icons = {};
//
this.processSpecialSettings(decodedConfig, changes, validSettings);
//
this.processStandardSettings(
decodedConfig,
changes,
@ -1839,13 +1761,11 @@ export default {
icons
);
//
if (Object.keys(validSettings).length === 0) {
console.log("URL配置与当前配置相同无需应用");
return false;
}
//
return new Promise((resolve) => {
this.urlConfigDialog = {
show: true,
@ -1875,9 +1795,7 @@ export default {
}
},
//
processSpecialSettings(decodedConfig, changes, validSettings) {
//
if (decodedConfig.classNumber !== undefined) {
const current = getSetting("server.classNumber");
if (decodedConfig.classNumber !== current) {
@ -1897,7 +1815,6 @@ export default {
}
}
//
if (decodedConfig.date !== undefined) {
if (decodedConfig.date !== this.state.dateString) {
changes.push({
@ -1912,7 +1829,6 @@ export default {
}
}
//
if (decodedConfig.subjects && Array.isArray(decodedConfig.subjects)) {
changes.push({
key: "subjects",
@ -1926,21 +1842,16 @@ export default {
}
},
//
processStandardSettings(decodedConfig, changes, validSettings, icons) {
Object.entries(decodedConfig).forEach(([key, value]) => {
//
if (["classNumber", "date", "subjects"].includes(key)) {
return;
}
//
let settingKey = key;
let definition = settingsDefinitions[key];
//
if (!definition && !key.includes(".")) {
//
const prefixes = [
"server.",
"display.",
@ -1960,15 +1871,12 @@ export default {
}
}
//
if (definition) {
//
let typedValue = this.convertValueToCorrectType(
value,
definition.type
);
//
if (definition.validate && !definition.validate(typedValue)) {
console.warn(`URL配置项 ${settingKey} 的值无效: ${value}`);
return;
@ -1988,7 +1896,6 @@ export default {
icons[settingKey] = definition.icon || "mdi-cog";
}
} else {
//
changes.push({
key: key,
name: this.getSettingDisplayName(key),
@ -2003,7 +1910,6 @@ export default {
});
},
//
convertValueToCorrectType(value, type) {
if (type === "boolean") {
return Boolean(value);
@ -2014,7 +1920,6 @@ export default {
}
},
//
formatSettingValue(value) {
if (typeof value === "boolean") {
return value ? "开启" : "关闭";
@ -2024,20 +1929,15 @@ export default {
return value.toString();
},
//
getSettingDisplayName(key) {
// key
const parts = key.split(".");
const lastPart = parts[parts.length - 1];
// key
const nameMap = {
//
provider: "数据提供方",
domain: "服务器域名",
classNumber: "班级编号",
//
emptySubjectDisplay: "空科目显示方式",
dynamicSort: "动态排序",
showRandomButton: "随机按钮",
@ -2046,19 +1946,15 @@ export default {
enhancedTouchMode: "增强触摸模式",
showAntiScreenBurnCard: "防烧屏卡片",
//
mode: "主题模式",
//
size: "字体大小",
//
autoSave: "自动保存",
blockNonTodayAutoSave: "禁止自动保存非当日",
refreshBeforeEdit: "编辑前刷新",
confirmNonTodaySave: "非当日保存确认",
//
auto: "自动刷新",
interval: "刷新间隔",
};
@ -2066,7 +1962,6 @@ export default {
return nameMap[lastPart] || lastPart;
},
// Base64UTF-8
safeBase64Decode(base64String) {
try {
return Base64.decode(base64String);
@ -2077,7 +1972,6 @@ export default {
},
applyUrlConfig(validSettings) {
//
for (const [key, value] of Object.entries(validSettings)) {
if (key === "date") {
this.handleDateSelect(value);
@ -2089,20 +1983,23 @@ export default {
continue;
}
//
setSetting(key, value);
//
if (key === "server.classNumber") {
this.state.classNumber = value;
}
}
// URL
this.updateBackendUrl();
this.$message.success("URL配置已应用", "已从URL加载配置");
return true;
},
navigateDay(offset) {
const currentDate = new Date(this.state.selectedDateObj);
currentDate.setDate(currentDate.getDate() + offset);
this.handleDateSelect(currentDate);
},
},
};
</script>

View File

@ -232,7 +232,6 @@ import AboutCard from "@/components/settings/AboutCard.vue";
import "../styles/settings.scss";
import SettingsExplorer from "@/components/settings/SettingsExplorer.vue";
import SettingsLinkGenerator from "@/components/SettingsLinkGenerator.vue";
import dataProvider from "@/utils/dataProvider";
import NamespaceSettingsCard from "@/components/settings/cards/NamespaceSettingsCard.vue";
import RandomPickerCard from "@/components/settings/cards/RandomPickerCard.vue";
export default {