This commit is contained in:
SunWuyuan
2024-11-17 17:04:36 +08:00
parent b65cbdeefc
commit 72cee17d7f
31 changed files with 4591 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
<template>
<v-app>
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<script setup>
//
</script>
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

+79
View File
@@ -0,0 +1,79 @@
<template>
<v-footer height="40" app>
<a
v-for="item in items"
:key="item.title"
:href="item.href"
:title="item.title"
class="d-inline-block mx-2 social-link"
rel="noopener noreferrer"
target="_blank"
>
<v-icon
:icon="item.icon"
:size="item.icon === '$vuetify' ? 24 : 16"
/>
</a>
<div
class="text-caption text-disabled"
style="position: absolute; right: 16px;"
>
&copy; 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
—
<a
class="text-decoration-none on-surface"
href="https://vuetifyjs.com/about/licensing/"
rel="noopener noreferrer"
target="_blank"
>
MIT License
</a>
</div>
</v-footer>
</template>
<script setup>
const items = [
{
title: 'Vuetify Documentation',
icon: `$vuetify`,
href: 'https://vuetifyjs.com/',
},
{
title: 'Vuetify Support',
icon: 'mdi-shield-star-outline',
href: 'https://support.vuetifyjs.com/',
},
{
title: 'Vuetify X',
icon: ['M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z'],
href: 'https://x.com/vuetifyjs',
},
{
title: 'Vuetify GitHub',
icon: `mdi-github`,
href: 'https://github.com/vuetifyjs/vuetify',
},
{
title: 'Vuetify Discord',
icon: ['M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z'],
href: 'https://community.vuetifyjs.com/',
},
{
title: 'Vuetify Reddit',
icon: `mdi-reddit`,
href: 'https://reddit.com/r/vuetifyjs',
},
]
</script>
<style scoped lang="sass">
.social-link :deep(.v-icon)
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
text-decoration: none
transition: .2s ease-in-out
&:hover
color: rgba(25, 118, 210, 1)
</style>
+157
View File
@@ -0,0 +1,157 @@
<template>
<v-container class="fill-height">
<v-responsive
class="align-centerfill-height mx-auto"
max-width="900"
>
<v-img
class="mb-4"
height="150"
src="@/assets/logo.png"
/>
<div class="text-center">
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
</div>
<div class="py-4" />
<v-row>
<v-col cols="12">
<v-card
class="py-4"
color="surface-variant"
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
prepend-icon="mdi-rocket-launch-outline"
rounded="lg"
variant="outlined"
>
<template #image>
<v-img position="top right" />
</template>
<template #title>
<h2 class="text-h5 font-weight-bold">Get started</h2>
</template>
<template #subtitle>
<div class="text-subtitle-1">
Replace this page by removing <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>pages/index.vue</v-kbd>.
</div>
</template>
<v-overlay
opacity=".12"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/"
prepend-icon="mdi-text-box-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Learn about all things Vuetify in our documentation."
target="_blank"
title="Documentation"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
prepend-icon="mdi-star-circle-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Explore available framework Features."
target="_blank"
title="Features"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/components/all"
prepend-icon="mdi-widgets-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Discover components in the API Explorer."
target="_blank"
title="Components"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://discord.vuetifyjs.com"
prepend-icon="mdi-account-group-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Connect with Vuetify developers."
target="_blank"
title="Community"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
</v-row>
</v-responsive>
</v-container>
</template>
<script setup>
//
</script>
+35
View File
@@ -0,0 +1,35 @@
# Components
Vue template files in this folder are automatically imported.
## 🚀 Usage
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
The following example assumes a component located at `src/components/MyComponent.vue`:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
//
</script>
```
When your template is rendered, the component's import will automatically be inlined, which renders to this:
```vue
<template>
<div>
<MyComponent />
</div>
</template>
<script lang="ts" setup>
import MyComponent from '@/components/MyComponent.vue'
</script>
```
+5
View File
@@ -0,0 +1,5 @@
# Layouts
Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages.
Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) repository.
+13
View File
@@ -0,0 +1,13 @@
<template>
<v-app>
<v-main>
<router-view />
</v-main>
<AppFooter />
</v-app>
</template>
<script setup>
//
</script>
+20
View File
@@ -0,0 +1,20 @@
/**
* main.js
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Plugins
import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
// Composables
import { createApp } from 'vue'
const app = createApp(App)
registerPlugins(app)
app.mount('#app')
+5
View File
@@ -0,0 +1,5 @@
# Pages
Vue components created in this folder will automatically be converted to navigatable routes.
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.
+346
View File
@@ -0,0 +1,346 @@
<template>
{{ studentList }}<br /><br />{{ homeworkData }}<br /><br />{{ selectedSet }}
<v-container class="main-window" fluid>
<v-row>
<v-col cols="11">
<h1>
作业
<div>{{ dateString }}</div>
</h1>
<v-container fluid style="padding-left: 2px; padding-right: 2px">
<v-row v-for="subjects in homeworkArrange" :key="subjects.name">
<v-col
v-for="subject in subjects"
:key="subject"
cols="4"
style="padding: 2px !important"
@click="openDialog(subject)"
>
<v-card>
<v-card-title>{{ homeworkData[subject].name }}</v-card-title>
<v-card-text :style="contentStyle">
<v-list>
<v-list-item
v-for="text in splitPoint(homeworkData[subject].content)"
:key="text"
>
{{ text }}
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
<div />
</v-col>
<v-col
v-if="studentList.length"
class="attendance-area"
cols="1"
@click="setAttendanceArea"
>
<h1>出勤</h1>
<h2>应到:{{ studentList.length }}人</h2>
<h2>实到:{{ studentList.length - selectedSet.size }}人</h2>
<h2 style="margin-bottom: 5px">请假:{{ selectedSet.size }} 人</h2>
<h3 v-for="(i, index) in selectedSet" :key="index">
{{ `${index + 1}. ${studentList[i]}` }}
</h3>
</v-col>
</v-row>
</v-container>
<v-dialog v-model="dialogVisible" width="500" @click:outside="handleClose">
<v-card>
<v-card-title>{{ dialogTitle }}</v-card-title>
<v-card-text>
<v-textarea
ref="inputRef"
v-model="textarea"
auto-grow
placeholder="使用换行表示分条"
rows="5"
/>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="attendDialogVisible" width="800">
<v-card>
<v-card-title>设置未到学生列表</v-card-title>
<v-card-text>
<v-container fluid>
<v-row>
<v-col
v-for="(name, i) in studentList"
:key="i"
cols="12"
sm="6"
md="4"
>
<v-card
:class="{ selected: selectedSet.has(i) }"
variant="outlined"
:color="selectedSet.has(i) ? 'primary' : ''"
@click="toggleStudent(i)"
>
<v-card-text>
{{ `${i + 1}. ${name}` }}
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</v-card-text>
</v-card>
</v-dialog>
<v-container class="upload">
<v-btn icon="mdi-plus" variant="text" @click="zoom('up')" />
<v-btn icon="mdi-minus" variant="text" @click="zoom('out')" />
<v-btn
v-if="!synced"
color="primary"
size="large"
:loading="downloadLoading"
@click="downloadData"
>
下载
</v-btn>
<v-btn
v-if="!synced"
color="error"
size="large"
:loading="uploadLoading"
class="ml-2"
@click="uploadData"
>
上传
</v-btn>
<v-btn v-else color="success" size="large" @click="showSyncMessage">
同步完成
</v-btn>
</v-container>
</template>
<script>
import axios from "axios";
import { useDisplay } from "vuetify";
export default {
name: "HomeworkBoard",
data() {
return {
backurl: "http://localhost:3030",
currentEditSubject: null,
studentList: ["加载中"],
selectedSet: new Set(),
dialogVisible: false,
dialogTitle: "",
textarea: "",
dateString: "",
synced: false,
attendDialogVisible: false,
contentStyle: { "font-size": "28px" },
uploadLoading: false,
downloadLoading: false,
homeworkData: {},
homeworkArrange: [[], []],
snackbar: false,
snackbarText: "",
fontSize: 28,
};
},
computed: {
isMobile() {
return useDisplay().mobile.value;
},
},
async mounted() {
try {
await this.initializeData();
} catch (err) {
console.error("初始化失败:", err);
this.showError("初始化失败,请刷新页面重试");
}
},
methods: {
async initializeData() {
const res = await axios.get(this.backurl + "/config");
this.studentList = res.data.studentList;
this.homeworkArrange = res.data.homeworkArrange;
this.initializeHomeworkData();
this.setCurrentDate();
await this.downloadDataDirectly();
},
initializeHomeworkData() {
this.homeworkArrange.forEach((subjects) => {
subjects.forEach((subject) => {
this.homeworkData[subject] = {
name: subject,
content: "",
};
});
});
},
setCurrentDate() {
const today = new Date();
this.dateString = today.toISOString().split("T")[0];
},
handleClose() {
if (this.currentEditSubject) {
this.homeworkData[this.currentEditSubject].content = this.textarea;
this.synced = false;
}
this.dialogVisible = false;
},
showSyncMessage() {
this.snackbar = true;
this.snackbarText = "数据已完成与服务器同步";
},
showError(message) {
this.snackbar = true;
this.snackbarText = message;
},
openDialog(subject) {
this.currentEditSubject = subject;
this.dialogTitle = this.homeworkData[subject].name;
this.textarea = this.homeworkData[subject].content;
this.dialogVisible = true;
// 在下一个 tick 后聚焦输入框
this.$nextTick(() => {
if (this.$refs.inputRef) {
this.$refs.inputRef.focus();
}
});
},
splitPoint(content) {
return content.split("\n").filter((text) => text.trim());
},
setAttendanceArea() {
this.attendDialogVisible = true;
},
toggleStudent(index) {
if (this.selectedSet.has(index)) {
this.selectedSet.delete(index);
} else {
this.selectedSet.add(index);
}
this.synced = false;
},
zoom(direction) {
const step = 2;
if (direction === "up" && this.fontSize < 100) {
this.fontSize += step;
} else if (direction === "out" && this.fontSize > 16) {
this.fontSize -= step;
}
this.contentStyle = {
"font-size": `${this.fontSize}px`,
};
},
async uploadData() {
try {
this.uploadLoading = true;
await axios.post(this.backurl + "/upload", {
date: this.dateString,
data: this.homeworkData,
attendance: Array.from(this.selectedSet),
});
this.synced = true;
this.showSyncMessage();
} catch (err) {
console.error("上传失败:", err);
this.showError("上传失败,请重试");
} finally {
this.uploadLoading = false;
}
},
async downloadData() {
try {
this.downloadLoading = true;
await this.downloadDataDirectly();
} catch (err) {
console.error("下载失败:", err);
this.showError("下载失败,请重试");
} finally {
this.downloadLoading = false;
}
},
async downloadDataDirectly() {
const res = await axios.get(
this.backurl + "/download?date=" + this.dateString
);
this.homeworkData = res.data.data || this.homeworkData;
const subjectOrder = [
"语文",
"数学",
"英语",
"物理",
"化学",
"生物",
"政治",
"历史",
"地理",
"其他",
];
// 1. 将对象转换成数组,方便排序
let sortedSubjects = Object.keys(this.homeworkData).map((key) => ({
key,
...this.homeworkData[key],
}));
// 2. 按照指定的顺序排序
sortedSubjects.sort((a, b) => {
const indexA = subjectOrder.indexOf(a.key);
const indexB = subjectOrder.indexOf(b.key);
// 优先根据学科顺序排序
if (indexA !== indexB) {
return indexA - indexB;
}
return 0;
});
// 3. 进一步排序:将 content 为空的项移动到最后
sortedSubjects = [
// 先放置有 content 的项
...sortedSubjects.filter((item) => item.content != ""),
// 再放置 content 为空的项
...sortedSubjects.filter((item) => item.content == ""),
];
// 4. 将排序后的数组转换回对象
this.homeworkData = sortedSubjects.reduce((acc, curr) => {
acc[curr.key] = { name: curr.name, content: curr.content };
return acc;
}, {});
this.selectedSet = new Set(res.data.attendance || []);
this.synced = true;
},
},
};
</script>
+3
View File
@@ -0,0 +1,3 @@
# Plugins
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.
+17
View File
@@ -0,0 +1,17 @@
/**
* plugins/index.js
*
* Automatically included in `./src/main.js`
*/
// Plugins
import vuetify from './vuetify'
import pinia from '@/stores'
import router from '@/router'
export function registerPlugins (app) {
app
.use(vuetify)
.use(router)
.use(pinia)
}
+19
View File
@@ -0,0 +1,19 @@
/**
* plugins/vuetify.js
*
* Framework documentation: https://vuetifyjs.com`
*/
// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import { createVuetify } from 'vuetify'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
theme: {
defaultTheme: 'dark',
},
})
+36
View File
@@ -0,0 +1,36 @@
/**
* router/index.ts
*
* Automatic routes for `./src/pages/*.vue`
*/
// Composables
import { createRouter, createWebHistory } from 'vue-router/auto'
import { setupLayouts } from 'virtual:generated-layouts'
import { routes } from 'vue-router/auto-routes'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: setupLayouts(routes),
})
// Workaround for https://github.com/vitejs/vite/issues/11804
router.onError((err, to) => {
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
if (!localStorage.getItem('vuetify:dynamic-reload')) {
console.log('Reloading page to fix dynamic import error')
localStorage.setItem('vuetify:dynamic-reload', 'true')
location.assign(to.fullPath)
} else {
console.error('Dynamic import error, reloading page did not fix it', err)
}
} else {
console.error(err)
}
})
router.isReady().then(() => {
localStorage.removeItem('vuetify:dynamic-reload')
})
export default router
+5
View File
@@ -0,0 +1,5 @@
# Store
Pinia stores are used to store reactive state and expose actions to mutate it.
Full documentation for this feature can be found in the Official [Pinia](https://pinia.esm.dev/) repository.
+8
View File
@@ -0,0 +1,8 @@
// Utilities
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
//
}),
})
+4
View File
@@ -0,0 +1,4 @@
// Utilities
import { createPinia } from 'pinia'
export default createPinia()
+3
View File
@@ -0,0 +1,3 @@
# Styles
This directory is for configuring the styles of the application.
+10
View File
@@ -0,0 +1,10 @@
/**
* src/styles/settings.scss
*
* Configures SASS variables and Vuetify overwrites
*/
// https://vuetifyjs.com/features/sass-variables/`
// @use 'vuetify/settings' with (
// $color-pack: false
// );