mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-01 16:49:22 +00:00
1
This commit is contained in:
parent
b65cbdeefc
commit
72cee17d7f
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
79
.eslintrc-auto-import.json
Normal file
79
.eslintrc-auto-import.json
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"effectScope": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeRouteLeave": true,
|
||||
"onBeforeRouteUpdate": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useLink": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSlots": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true,
|
||||
"DirectiveBinding": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true,
|
||||
"onWatcherCleanup": true,
|
||||
"useId": true,
|
||||
"useModel": true,
|
||||
"useTemplateRef": true
|
||||
}
|
||||
}
|
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{js,mjs,jsx,vue}'],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'app/files-to-ignore',
|
||||
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
|
||||
{
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
}
|
||||
]
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Vuetify 3</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
20
jsconfig.json
Normal file
20
jsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
40
package.json
Normal file
40
package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "HomeworkPage",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.4.47",
|
||||
"axios": "^1.7.7",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^3.4.31",
|
||||
"vuetify": "^3.6.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.4.0",
|
||||
"eslint-plugin-vue": "^9.30.0",
|
||||
"pinia": "^2.1.7",
|
||||
"sass": "1.77.8",
|
||||
"sass-embedded": "^1.77.8",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"unplugin-vue-components": "^0.27.2",
|
||||
"unplugin-vue-router": "^0.10.0",
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vue-router": "^4.4.0"
|
||||
}
|
||||
}
|
3529
pnpm-lock.yaml
generated
Normal file
3529
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
11
src/App.vue
Normal file
11
src/App.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
6
src/assets/logo.svg
Normal file
6
src/assets/logo.svg
Normal 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
src/components/AppFooter.vue
Normal file
79
src/components/AppFooter.vue
Normal 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;"
|
||||
>
|
||||
© 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
src/components/HelloWorld.vue
Normal file
157
src/components/HelloWorld.vue
Normal 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
src/components/README.md
Normal file
35
src/components/README.md
Normal 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
src/layouts/README.md
Normal file
5
src/layouts/README.md
Normal 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
src/layouts/default.vue
Normal file
13
src/layouts/default.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
|
||||
<AppFooter />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
20
src/main.js
Normal file
20
src/main.js
Normal 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
src/pages/README.md
Normal file
5
src/pages/README.md
Normal 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
src/pages/index.vue
Normal file
346
src/pages/index.vue
Normal 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
src/plugins/README.md
Normal file
3
src/plugins/README.md
Normal 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
src/plugins/index.js
Normal file
17
src/plugins/index.js
Normal 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
src/plugins/vuetify.js
Normal file
19
src/plugins/vuetify.js
Normal 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
src/router/index.js
Normal file
36
src/router/index.js
Normal 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
src/stores/README.md
Normal file
5
src/stores/README.md
Normal 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
src/stores/app.js
Normal file
8
src/stores/app.js
Normal file
@ -0,0 +1,8 @@
|
||||
// Utilities
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
//
|
||||
}),
|
||||
})
|
4
src/stores/index.js
Normal file
4
src/stores/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
// Utilities
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export default createPinia()
|
3
src/styles/README.md
Normal file
3
src/styles/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Styles
|
||||
|
||||
This directory is for configuring the styles of the application.
|
10
src/styles/settings.scss
Normal file
10
src/styles/settings.scss
Normal 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
|
||||
// );
|
74
vite.config.mjs
Normal file
74
vite.config.mjs
Normal file
@ -0,0 +1,74 @@
|
||||
// Plugins
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import Fonts from 'unplugin-fonts/vite'
|
||||
import Layouts from 'vite-plugin-vue-layouts'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||
|
||||
// Utilities
|
||||
import { defineConfig } from 'vite'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
VueRouter(),
|
||||
Layouts(),
|
||||
Vue({
|
||||
template: { transformAssetUrls }
|
||||
}),
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
||||
Vuetify({
|
||||
autoImport: true,
|
||||
styles: {
|
||||
configFile: 'src/styles/settings.scss',
|
||||
},
|
||||
}),
|
||||
Components(),
|
||||
Fonts({
|
||||
google: {
|
||||
families: [{
|
||||
name: 'Roboto',
|
||||
styles: 'wght@100;300;400;500;700;900',
|
||||
}],
|
||||
},
|
||||
}),
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
],
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
},
|
||||
vueTemplate: true,
|
||||
}),
|
||||
],
|
||||
define: { 'process.env': {} },
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
extensions: [
|
||||
'.js',
|
||||
'.json',
|
||||
'.jsx',
|
||||
'.mjs',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.vue',
|
||||
],
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
sass: {
|
||||
api: 'modern-compiler',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user