WebUI 1.0.0
WebUI 的版本号直接从 1.0.0 开始,目前而言无需激烈地重构了。 WebUI 使用 pnpm 管理构建流程,构建需要使用 `pnpm build` 。Kimi 说使用 `npm run build` 可能也行,等待测试了。
This commit is contained in:
8
webui/.editorconfig
Normal file
8
webui/.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
||||||
1
webui/.gitattributes
vendored
Normal file
1
webui/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
39
webui/.gitignore
vendored
Normal file
39
webui/.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
# !.vscode/extensions.json
|
||||||
|
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Vitest
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
*.timestamp-*-*.mjs
|
||||||
10
webui/.idea/.gitignore
generated
vendored
Normal file
10
webui/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
10
webui/.idea/OxcSettings.xml
generated
Normal file
10
webui/.idea/OxcSettings.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="OxcSettings">
|
||||||
|
<option name="fixAllOnSave" value="true" />
|
||||||
|
<flags>
|
||||||
|
<entry key="fix_kind" value="ALL" />
|
||||||
|
</flags>
|
||||||
|
<option name="typeAware" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
19
webui/.idea/OxfmtSettings.xml
generated
Normal file
19
webui/.idea/OxfmtSettings.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="OxfmtSettings">
|
||||||
|
<option name="fixAllOnSave" value="true" />
|
||||||
|
<option name="supportedExtensions">
|
||||||
|
<list>
|
||||||
|
<option value=".js" />
|
||||||
|
<option value=".jsx" />
|
||||||
|
<option value=".cjs" />
|
||||||
|
<option value=".mjs" />
|
||||||
|
<option value=".ts" />
|
||||||
|
<option value=".tsx" />
|
||||||
|
<option value=".cts" />
|
||||||
|
<option value=".mts" />
|
||||||
|
<option value=".vue" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
webui/.idea/dictionaries/project.xml
generated
Normal file
7
webui/.idea/dictionaries/project.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="project">
|
||||||
|
<words>
|
||||||
|
<w>jwxt</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
8
webui/.idea/modules.xml
generated
Normal file
8
webui/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/webui.iml" filepath="$PROJECT_DIR$/.idea/webui.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
webui/.idea/vcs.xml
generated
Normal file
6
webui/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
webui/.idea/webui.iml
generated
Normal file
8
webui/.idea/webui.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
5
webui/.oxfmtrc.json
Normal file
5
webui/.oxfmtrc.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
10
webui/.oxlintrc.json
Normal file
10
webui/.oxlintrc.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"],
|
||||||
|
"env": {
|
||||||
|
"browser": true
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
78
webui/auto-imports.d.ts
vendored
Normal file
78
webui/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue').EffectScope
|
||||||
|
const computed: typeof import('vue').computed
|
||||||
|
const createApp: typeof import('vue').createApp
|
||||||
|
const customRef: typeof import('vue').customRef
|
||||||
|
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||||
|
const defineComponent: typeof import('vue').defineComponent
|
||||||
|
const effectScope: typeof import('vue').effectScope
|
||||||
|
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||||
|
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||||
|
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||||
|
const h: typeof import('vue').h
|
||||||
|
const inject: typeof import('vue').inject
|
||||||
|
const isProxy: typeof import('vue').isProxy
|
||||||
|
const isReactive: typeof import('vue').isReactive
|
||||||
|
const isReadonly: typeof import('vue').isReadonly
|
||||||
|
const isRef: typeof import('vue').isRef
|
||||||
|
const isShallow: typeof import('vue').isShallow
|
||||||
|
const markRaw: typeof import('vue').markRaw
|
||||||
|
const nextTick: typeof import('vue').nextTick
|
||||||
|
const onActivated: typeof import('vue').onActivated
|
||||||
|
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||||
|
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
|
||||||
|
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
|
||||||
|
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||||
|
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||||
|
const onDeactivated: typeof import('vue').onDeactivated
|
||||||
|
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||||
|
const onMounted: typeof import('vue').onMounted
|
||||||
|
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||||
|
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||||
|
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||||
|
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||||
|
const onUnmounted: typeof import('vue').onUnmounted
|
||||||
|
const onUpdated: typeof import('vue').onUpdated
|
||||||
|
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||||
|
const provide: typeof import('vue').provide
|
||||||
|
const reactive: typeof import('vue').reactive
|
||||||
|
const readonly: typeof import('vue').readonly
|
||||||
|
const ref: typeof import('vue').ref
|
||||||
|
const resolveComponent: typeof import('vue').resolveComponent
|
||||||
|
const shallowReactive: typeof import('vue').shallowReactive
|
||||||
|
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||||
|
const shallowRef: typeof import('vue').shallowRef
|
||||||
|
const toRaw: typeof import('vue').toRaw
|
||||||
|
const toRef: typeof import('vue').toRef
|
||||||
|
const toRefs: typeof import('vue').toRefs
|
||||||
|
const toValue: typeof import('vue').toValue
|
||||||
|
const triggerRef: typeof import('vue').triggerRef
|
||||||
|
const unref: typeof import('vue').unref
|
||||||
|
const useAttrs: typeof import('vue').useAttrs
|
||||||
|
const useCssModule: typeof import('vue').useCssModule
|
||||||
|
const useCssVars: typeof import('vue').useCssVars
|
||||||
|
const useId: typeof import('vue').useId
|
||||||
|
const useLink: typeof import('vue-router').useLink
|
||||||
|
const useModel: typeof import('vue').useModel
|
||||||
|
const useRoute: typeof import('vue-router').useRoute
|
||||||
|
const useRouter: typeof import('vue-router').useRouter
|
||||||
|
const useSlots: typeof import('vue').useSlots
|
||||||
|
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||||
|
const watch: typeof import('vue').watch
|
||||||
|
const watchEffect: typeof import('vue').watchEffect
|
||||||
|
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||||
|
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
74
webui/components.d.ts
vendored
Normal file
74
webui/components.d.ts
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// oxlint-disable
|
||||||
|
// ------
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import { GlobalComponents } from 'vue'
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||||
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
|
NCode: typeof import('naive-ui')['NCode']
|
||||||
|
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||||
|
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||||
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||||
|
NFlex: typeof import('naive-ui')['NFlex']
|
||||||
|
NForm: typeof import('naive-ui')['NForm']
|
||||||
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
|
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
|
||||||
|
NH1: typeof import('naive-ui')['NH1']
|
||||||
|
NH2: typeof import('naive-ui')['NH2']
|
||||||
|
NH3: typeof import('naive-ui')['NH3']
|
||||||
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
|
NMenu: typeof import('naive-ui')['NMenu']
|
||||||
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
|
NP: typeof import('naive-ui')['NP']
|
||||||
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
|
NText: typeof import('naive-ui')['NText']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For TSX support
|
||||||
|
declare global {
|
||||||
|
const NAlert: typeof import('naive-ui')['NAlert']
|
||||||
|
const NButton: typeof import('naive-ui')['NButton']
|
||||||
|
const NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||||
|
const NCard: typeof import('naive-ui')['NCard']
|
||||||
|
const NCode: typeof import('naive-ui')['NCode']
|
||||||
|
const NCollapse: typeof import('naive-ui')['NCollapse']
|
||||||
|
const NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||||
|
const NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
const NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||||
|
const NFlex: typeof import('naive-ui')['NFlex']
|
||||||
|
const NForm: typeof import('naive-ui')['NForm']
|
||||||
|
const NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
|
const NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
|
||||||
|
const NH1: typeof import('naive-ui')['NH1']
|
||||||
|
const NH2: typeof import('naive-ui')['NH2']
|
||||||
|
const NH3: typeof import('naive-ui')['NH3']
|
||||||
|
const NInput: typeof import('naive-ui')['NInput']
|
||||||
|
const NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
|
const NMenu: typeof import('naive-ui')['NMenu']
|
||||||
|
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
|
const NP: typeof import('naive-ui')['NP']
|
||||||
|
const NSelect: typeof import('naive-ui')['NSelect']
|
||||||
|
const NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
|
const NTag: typeof import('naive-ui')['NTag']
|
||||||
|
const NText: typeof import('naive-ui')['NText']
|
||||||
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
const RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
2
webui/env.d.ts
vendored
Normal file
2
webui/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
declare const __VERSION__: string
|
||||||
26
webui/eslint.config.ts
Normal file
26
webui/eslint.config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import pluginOxlint from 'eslint-plugin-oxlint'
|
||||||
|
import skipFormatting from 'eslint-config-prettier/flat'
|
||||||
|
|
||||||
|
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||||
|
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||||
|
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||||
|
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{vue,ts,mts,tsx}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
...pluginVue.configs['flat/essential'],
|
||||||
|
vueTsConfigs.recommended,
|
||||||
|
|
||||||
|
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
|
||||||
|
|
||||||
|
skipFormatting,
|
||||||
|
)
|
||||||
13
webui/index-schedule.html
Normal file
13
webui/index-schedule.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link href="/favicon.ico" rel="icon">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<title>NJUPT API Suan ScheduleTable</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="/src/main-schedule.ts" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
webui/index.html
Normal file
13
webui/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>NJUPT API Suan WebUI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
53
webui/package.json
Normal file
53
webui/package.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "webui",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build",
|
||||||
|
"lint": "run-s lint:*",
|
||||||
|
"lint:oxlint": "oxlint . --fix",
|
||||||
|
"lint:eslint": "eslint . --fix --cache",
|
||||||
|
"format": "oxfmt src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi_up": "^6.0.6",
|
||||||
|
"axios": "^1.15.0",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"typescript-cookie": "^1.0.6",
|
||||||
|
"vue": "^3.5.31",
|
||||||
|
"vue-router": "^5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node24": "^24.0.4",
|
||||||
|
"@types/node": "^24.12.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.5",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||||
|
"@vue/eslint-config-typescript": "^14.7.0",
|
||||||
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"eslint": "^10.1.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-oxlint": "~1.57.0",
|
||||||
|
"eslint-plugin-vue": "~10.8.0",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
|
"naive-ui": "^2.44.1",
|
||||||
|
"npm-run-all2": "^8.0.4",
|
||||||
|
"oxfmt": "^0.42.0",
|
||||||
|
"oxlint": "~1.57.0",
|
||||||
|
"sass-embedded": "^1.99.0",
|
||||||
|
"typescript": "~6.0.0",
|
||||||
|
"unplugin-auto-import": "^21.0.0",
|
||||||
|
"unplugin-vue-components": "^32.0.0",
|
||||||
|
"vite": "^8.0.3",
|
||||||
|
"vite-plugin-vue-devtools": "^8.1.1",
|
||||||
|
"vue-tsc": "^3.2.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
4397
webui/pnpm-lock.yaml
generated
Normal file
4397
webui/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
webui/pnpm-workspace.yaml
Normal file
18
webui/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
overrides:
|
||||||
|
'vue': 'beta'
|
||||||
|
'@vue/compiler-core': 'beta'
|
||||||
|
'@vue/compiler-dom': 'beta'
|
||||||
|
'@vue/compiler-sfc': 'beta'
|
||||||
|
'@vue/compiler-ssr': 'beta'
|
||||||
|
'@vue/compiler-vapor': 'beta'
|
||||||
|
'@vue/reactivity': 'beta'
|
||||||
|
'@vue/runtime-core': 'beta'
|
||||||
|
'@vue/runtime-dom': 'beta'
|
||||||
|
'@vue/runtime-vapor': 'beta'
|
||||||
|
'@vue/server-renderer': 'beta'
|
||||||
|
'@vue/shared': 'beta'
|
||||||
|
'@vue/compat': 'beta'
|
||||||
|
|
||||||
|
peerDependencyRules:
|
||||||
|
allowAny:
|
||||||
|
- 'vue'
|
||||||
BIN
webui/public/favicon.ico
Normal file
BIN
webui/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
121
webui/src/App.vue
Normal file
121
webui/src/App.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import HeaderNav from '@/coms/HeaderNav.vue'
|
||||||
|
import SidebarNav from '@/coms/SidebarNav.vue'
|
||||||
|
|
||||||
|
import {dateZhCN, zhCN} from 'naive-ui'
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
import javascript from 'highlight.js/lib/languages/javascript'
|
||||||
|
import LoginCard from '@/coms/LoginCard.vue'
|
||||||
|
import {onMounted} from 'vue'
|
||||||
|
import {getCookie, removeCookie, setCookie} from 'typescript-cookie'
|
||||||
|
import {validateAdminToken} from '@/tools/web.ts'
|
||||||
|
import {useConfig} from '@/stores/config.ts'
|
||||||
|
import FooterNav from '@/coms/FooterNav.vue'
|
||||||
|
|
||||||
|
const CONFIG = useConfig()
|
||||||
|
|
||||||
|
hljs.registerLanguage('javascript', javascript)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const token = getCookie('token')
|
||||||
|
if (token) {
|
||||||
|
if (await validateAdminToken(token)) {
|
||||||
|
CONFIG.tokenValidated = true
|
||||||
|
setCookie('token', token)
|
||||||
|
} else {
|
||||||
|
removeCookie('token')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-config-provider
|
||||||
|
:date-locale="dateZhCN"
|
||||||
|
:hljs
|
||||||
|
:locale="zhCN"
|
||||||
|
:theme-overrides="{
|
||||||
|
common: {
|
||||||
|
fontFamily: 'Maple Mono CN, sans-serif',
|
||||||
|
fontFamilyMono: 'Maple Mono CN, monospace',
|
||||||
|
fontWeightStrong: '700',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
inline-theme-disabled
|
||||||
|
>
|
||||||
|
<n-message-provider>
|
||||||
|
<div v-if="CONFIG.tokenValidated" class="main-container">
|
||||||
|
<div id="start-container">
|
||||||
|
<header-nav />
|
||||||
|
</div>
|
||||||
|
<div id="center-container">
|
||||||
|
<div id="center-sidebar">
|
||||||
|
<sidebar-nav />
|
||||||
|
</div>
|
||||||
|
<div id="center-content">
|
||||||
|
<div id="content-container">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<keep-alive>
|
||||||
|
<component :is="Component" />
|
||||||
|
</keep-alive>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="end-container" style="padding-top: 6px">
|
||||||
|
<footer-nav />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="main-container">
|
||||||
|
<login-card />
|
||||||
|
</div>
|
||||||
|
</n-message-provider>
|
||||||
|
<n-global-style />
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.main-container {
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 100vw;
|
||||||
|
height: 100svh;
|
||||||
|
max-height: 100svh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
#start-container,
|
||||||
|
#end-container {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#center-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
#center-sidebar {
|
||||||
|
flex: 0;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#center-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
#content-container {
|
||||||
|
border: 1px solid #519f72;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
19
webui/src/AppSchedule.vue
Normal file
19
webui/src/AppSchedule.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-config-provider
|
||||||
|
:theme-overrides="{
|
||||||
|
common: {
|
||||||
|
fontFamily: 'Maple Mono CN, sans-serif',
|
||||||
|
fontFamilyMono: 'Maple Mono CN, monospace',
|
||||||
|
fontWeightStrong: '700',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<router-view />
|
||||||
|
<n-global-style />
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
19
webui/src/assets/ansi-color.css
Normal file
19
webui/src/assets/ansi-color.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.ansi-green-fg {
|
||||||
|
color: #2fda2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ansi-blue-fg {
|
||||||
|
color: #86b6bc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ansi-cyan-fg {
|
||||||
|
color: #7ecce8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ansi-red-fg {
|
||||||
|
color: #ff4d4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ansi-yellow-fg {
|
||||||
|
color: #f5f543;
|
||||||
|
}
|
||||||
26
webui/src/assets/font.css
Normal file
26
webui/src/assets/font.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Maple Mono CN';
|
||||||
|
src: url('https://file.mangofanfan.cn/directlink/assets/maple-mono-cn/MapleMono-CN-Medium.woff2') format('woff2'),
|
||||||
|
url('https://file.mangofanfan.cn/directlink/assets/maple-mono-cn/MapleMono-CN-Medium.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Maple Mono CN';
|
||||||
|
src: url('https://file.mangofanfan.cn/directlink/assets/maple-mono-cn/MapleMono-CN-Bold.woff2') format('woff2'),
|
||||||
|
url('https://file.mangofanfan.cn/directlink/assets/maple-mono-cn/MapleMono-CN-Bold.ttf') format('truetype');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Smiley sans';
|
||||||
|
src: url('https://file.mangofanfan.cn/directlink/assets/smiley-sans/SmileySans-Oblique.ttf.woff2') format('woff2'),
|
||||||
|
url("https://file.mangofanfan.cn/directlink/assets/smiley-sans/SmileySans-Oblique.ttf") format("truetype");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
57
webui/src/assets/style.css
Normal file
57
webui/src/assets/style.css
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@import 'ansi-color.css';
|
||||||
|
@import "font.css";
|
||||||
|
|
||||||
|
/* 滚动条 */
|
||||||
|
div::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 0 5px rgb(108 108 108 / 0.2);
|
||||||
|
background: #519f72;
|
||||||
|
}
|
||||||
|
|
||||||
|
div::-webkit-scrollbar-track {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-margin-bottom {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-margin {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-1 {
|
||||||
|
width: 120px;
|
||||||
|
white-space: nowrap; /* 1. 强制文本不换行 */
|
||||||
|
overflow: hidden; /* 2. 隐藏超出部分 */
|
||||||
|
text-overflow: ellipsis; /* 3. 超出部分显示... */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-width {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-width {
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-width-large {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-width-large {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
24
webui/src/coms/ApiCard.vue
Normal file
24
webui/src/coms/ApiCard.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
method: 'GET' | 'POST'
|
||||||
|
path: string
|
||||||
|
description: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-card>
|
||||||
|
<template #header>
|
||||||
|
<n-flex>
|
||||||
|
<n-tag type="primary">{{ method }}</n-tag>
|
||||||
|
<n-tag type="info">{{ path }}</n-tag>
|
||||||
|
<n-text strong>{{ description }}</n-text>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
32
webui/src/coms/FooterNav.vue
Normal file
32
webui/src/coms/FooterNav.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="footer-container">
|
||||||
|
<div class="footer-card c1"><strong>NJUPT Suan API</strong></div>
|
||||||
|
<div class="footer-card c2">made with love by MangoFanFanw</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
div.footer-container {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
div.footer-card {
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&.c1 {
|
||||||
|
border: 1px solid #2bf8ff;
|
||||||
|
background-color: #d1fff1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.c2 {
|
||||||
|
border: 1px solid #c3ff2b;
|
||||||
|
background-color: #f7ffe2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
27
webui/src/coms/HeaderNav.vue
Normal file
27
webui/src/coms/HeaderNav.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="nav-container">
|
||||||
|
<div id="nav-title">
|
||||||
|
<n-h3 class="no-margin-bottom">NJUPT Suan API</n-h3>
|
||||||
|
</div>
|
||||||
|
<n-button secondary type="primary">概览</n-button>
|
||||||
|
<n-button secondary type="info">GitHub</n-button>
|
||||||
|
<n-button secondary type="error">芒果</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#nav-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
max-width: 100vw;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-title {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
56
webui/src/coms/LoginCard.vue
Normal file
56
webui/src/coms/LoginCard.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { validateAdminToken } from '@/tools/web.ts'
|
||||||
|
import { useConfig } from '@/stores/config.ts'
|
||||||
|
import { setCookie } from 'typescript-cookie'
|
||||||
|
|
||||||
|
const MESSAGE = useMessage()
|
||||||
|
const CONFIG = useConfig()
|
||||||
|
|
||||||
|
const token = ref('')
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
if (await validateAdminToken(token.value)) {
|
||||||
|
MESSAGE.success('令牌验证成功,欢迎使用 NJUPT Suan API WebUI ~')
|
||||||
|
CONFIG.tokenValidated = true
|
||||||
|
setCookie('token', token.value, { expires: 2 })
|
||||||
|
} else {
|
||||||
|
MESSAGE.error('令牌验证失败,请重新验证!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="login-card">
|
||||||
|
<n-h1 class="no-margin-bottom" prefix="bar" style="width: 90%">NJUPT Suan API 的 WebUI</n-h1>
|
||||||
|
<n-p style="font-size: 20px">请提供令牌(在启动日志中打印)</n-p>
|
||||||
|
<n-flex align="center" style="width: 90%" vertical>
|
||||||
|
<n-input v-model:value="token" />
|
||||||
|
<n-button-group>
|
||||||
|
<n-button type="primary" @click="login()">登录</n-button>
|
||||||
|
<n-button type="info">O.o?</n-button>
|
||||||
|
</n-button-group>
|
||||||
|
</n-flex>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#login-card {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
width: 460px;
|
||||||
|
background: linear-gradient(to bottom right, lightblue, #cbb4ff);
|
||||||
|
border: solid 2px #000000;
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
262
webui/src/coms/ScheduleTable.vue
Normal file
262
webui/src/coms/ScheduleTable.vue
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import {getGradientColor} from "@/tools/random.ts";
|
||||||
|
import type {CourseDatumDto, CourseDto} from '@/types/schedule.ts'
|
||||||
|
import {NH4, NTooltip} from "naive-ui";
|
||||||
|
import {onMounted, reactive} from 'vue'
|
||||||
|
import FooterNav from "@/coms/FooterNav.vue";
|
||||||
|
|
||||||
|
const { courses } = defineProps<{
|
||||||
|
courses: CourseDto[] | null
|
||||||
|
title?: string
|
||||||
|
subtitle?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 生成空的 courseData
|
||||||
|
const generateCourseKeys = () => {
|
||||||
|
const keys: Record<number, Record<number, CourseDatumDto | null>> = {}
|
||||||
|
for (let i = 1; i <= 7; i++) {
|
||||||
|
keys[i] ={}
|
||||||
|
for (let j = 1; j <= 12; j++) {
|
||||||
|
keys[i]![j] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
const courseData = reactive<Record<number, Record<number, CourseDatumDto | null>>>(generateCourseKeys())
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (courses === null) return
|
||||||
|
for (const course of courses) {
|
||||||
|
courseData[course.day]![course.classes.at(0)!] = {
|
||||||
|
name: course.name,
|
||||||
|
alias: course.alias,
|
||||||
|
teacher: course.teacher,
|
||||||
|
classroom: course.classroom,
|
||||||
|
during: course.classes.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成基础 Course 卡片。
|
||||||
|
function baseCourseCard(
|
||||||
|
title: string,
|
||||||
|
content?: string | null,
|
||||||
|
extraClass?: string | null,
|
||||||
|
extraStyle?: object | null,
|
||||||
|
during: number=1,
|
||||||
|
tooltip: string | null = null) {
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
return (
|
||||||
|
<div class={["base-card", extraClass]} style={{
|
||||||
|
'min-height': 60 * during + 2 * (during - 1) + 'px',
|
||||||
|
...extraStyle
|
||||||
|
}}>
|
||||||
|
<NTooltip>{{
|
||||||
|
trigger: () => <NH4 class={["no-margin-bottom", "line-1"]}>{title}</NH4>,
|
||||||
|
default: () => tooltip ? tooltip : "O.o?"
|
||||||
|
}}</NTooltip>
|
||||||
|
{ content ? <n-p class={["no-margin", "line-1"]} innerHTML={content}></n-p> : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexCardList() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from({length: 12}, (_, i) =>
|
||||||
|
baseCourseCard((i + 1).toString(), null, `index-card-${i + 1}`))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayCourseCardList(day: number) {
|
||||||
|
const items = Object.values(courseData[day]!)
|
||||||
|
let left = 0
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item, _) => {
|
||||||
|
if (item === null) {
|
||||||
|
if (left === 0) {
|
||||||
|
return baseCourseCard('FREE TIME', null, 'no-course-card')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
left -= 1
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left = item.during - 1
|
||||||
|
if (item.teacher === null) item.teacher = ''
|
||||||
|
return baseCourseCard(
|
||||||
|
item.alias ? item.alias : item.name,
|
||||||
|
item.teacher + ( item.classroom === null ? '' : '<br />' + item.classroom),
|
||||||
|
'course-card',
|
||||||
|
{'background-image': getGradientColor()},
|
||||||
|
item.during,
|
||||||
|
item.name
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="schedule-table-container">
|
||||||
|
<div class="schedule-table-header-container">
|
||||||
|
<div class="header-card">
|
||||||
|
<n-h1 class="title no-margin">{{ title ? title : '芒果酸的课程表' }}</n-h1>
|
||||||
|
<n-h3 class="no-margin">{{ subtitle ? subtitle : '我也要上吗?' }}</n-h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="schedule-table-body-container">
|
||||||
|
<!-- 标题列 -->
|
||||||
|
<div class="background-panel">
|
||||||
|
<component :is="indexCardList()" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="background-panel">
|
||||||
|
<component :is="dayCourseCardList(1)" />
|
||||||
|
</div>
|
||||||
|
<div class="background-panel">
|
||||||
|
<component :is="dayCourseCardList(2)" />
|
||||||
|
</div>
|
||||||
|
<div class="background-panel">
|
||||||
|
<component :is="dayCourseCardList(3)" />
|
||||||
|
</div>
|
||||||
|
<div class="background-panel">
|
||||||
|
<component :is="dayCourseCardList(4)" />
|
||||||
|
</div>
|
||||||
|
<div class="background-panel">
|
||||||
|
<component :is="dayCourseCardList(5)" />
|
||||||
|
</div>
|
||||||
|
<div class="background-panel">
|
||||||
|
<component :is="dayCourseCardList(6)" />
|
||||||
|
</div>
|
||||||
|
<div class="background-panel">
|
||||||
|
<component :is="dayCourseCardList(7)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer-nav />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
div.schedule-table-container {
|
||||||
|
div.schedule-table-header-container {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
// 真正的标题卡片
|
||||||
|
div.header-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: linear-gradient(8deg, #a6ffe9, #f2fffc);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 2px solid #2bdbff;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: 'Smiley sans', sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.header-card::before,
|
||||||
|
div.header-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 6px 0 0 0;
|
||||||
|
width: 300px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #2bdbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.header-card::before {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-radius: 0 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.header-card::after {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 6px 0 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.schedule-table-body-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
div.background-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
background-color: #efefef;
|
||||||
|
border-radius: 6px;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
div.base-card {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.index-card-1,
|
||||||
|
div.index-card-2 {
|
||||||
|
background-color: #ffefd1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.index-card-3,
|
||||||
|
div.index-card-4,
|
||||||
|
div.index-card-5 {
|
||||||
|
background-color: #dbf4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.index-card-6,
|
||||||
|
div.index-card-7,
|
||||||
|
div.index-card-8,
|
||||||
|
div.index-card-9 {
|
||||||
|
background-color: #d1ffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.index-card-10,
|
||||||
|
div.index-card-11,
|
||||||
|
div.index-card-12 {
|
||||||
|
background-color: #d7d2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.no-course-card {
|
||||||
|
background-color: rgb(255 255 255 / 0);
|
||||||
|
h4 {
|
||||||
|
color: #cdcdcd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.course-card {
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
webui/src/coms/SettingCard.vue
Normal file
58
webui/src/coms/SettingCard.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type {SelectOption} from 'naive-ui'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
message?: string
|
||||||
|
showSwitch?: boolean
|
||||||
|
showInput?: boolean
|
||||||
|
showNumber?: boolean
|
||||||
|
showSelect?: boolean
|
||||||
|
showDatePicker?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
selectionOptions?: SelectOption[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const booleanValue = defineModel<boolean | undefined>('booleanValue', { required: false })
|
||||||
|
const stringValue = defineModel<string | undefined>('stringValue', { required: false })
|
||||||
|
const numberValue = defineModel<number | undefined>('numberValue', { required: false })
|
||||||
|
const selectionValue = defineModel<string | undefined>('selectionValue', { required: false })
|
||||||
|
const dateValue = defineModel<string | undefined>('dateValue', { required: false })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-card :title="title">
|
||||||
|
<template v-if="showSwitch" #header-extra>
|
||||||
|
<n-switch v-model:value="booleanValue" :disabled size="large" />
|
||||||
|
</template>
|
||||||
|
<template v-if="showInput" #header-extra>
|
||||||
|
<n-input v-model:value="stringValue" :disabled size="large" />
|
||||||
|
</template>
|
||||||
|
<template v-if="showNumber" #header-extra>
|
||||||
|
<n-input-number v-model:value="numberValue" :disabled size="large" />
|
||||||
|
</template>
|
||||||
|
<template v-if="showDatePicker" #header-extra>
|
||||||
|
<n-date-picker
|
||||||
|
v-model:formatted-value="dateValue"
|
||||||
|
:disabled
|
||||||
|
format="yyyy-MM-dd"
|
||||||
|
size="large"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="showSelect" #header-extra>
|
||||||
|
<n-select
|
||||||
|
v-model:value="selectionValue"
|
||||||
|
:disabled
|
||||||
|
:options="selectionOptions"
|
||||||
|
class="min-width-large"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<n-p v-if="message">{{ message }}</n-p>
|
||||||
|
<slot />
|
||||||
|
<template #header-extra><slot name="header-extra" /></template>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
41
webui/src/coms/SidebarNav.vue
Normal file
41
webui/src/coms/SidebarNav.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type {MenuOption} from 'naive-ui'
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const activeKey = ref('/')
|
||||||
|
const options: MenuOption[] = [
|
||||||
|
{
|
||||||
|
label: '面板主页',
|
||||||
|
key: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '课表管理',
|
||||||
|
key: '/schedule',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '样式设计',
|
||||||
|
key: '/style',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '酸酸日志',
|
||||||
|
key: '/log',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '酸酸设置',
|
||||||
|
key: '/config',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function handleClick(key: string) {
|
||||||
|
router.push(key)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-menu v-model:value="activeKey" :options @update:value="handleClick" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
24
webui/src/coms/ToolCard.vue
Normal file
24
webui/src/coms/ToolCard.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
type: 'Tool' | 'Resource' | 'Prompt'
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-card>
|
||||||
|
<template #header>
|
||||||
|
<n-flex>
|
||||||
|
<n-tag type="primary">{{ type }}</n-tag>
|
||||||
|
<n-tag type="info">{{ name }}</n-tag>
|
||||||
|
<n-text strong>{{ description }}</n-text>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
11
webui/src/main-schedule.ts
Normal file
11
webui/src/main-schedule.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {createApp} from 'vue'
|
||||||
|
|
||||||
|
import '@/assets/style.css'
|
||||||
|
|
||||||
|
import AppSchedule from '@/AppSchedule.vue'
|
||||||
|
import scheduleRouter from '@/router/index-schedule.ts'
|
||||||
|
|
||||||
|
const app = createApp(AppSchedule)
|
||||||
|
app.use(scheduleRouter)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
14
webui/src/main.ts
Normal file
14
webui/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {createApp} from 'vue'
|
||||||
|
import {createPinia} from 'pinia'
|
||||||
|
|
||||||
|
import '@/assets/style.css'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
160
webui/src/pages/Config.vue
Normal file
160
webui/src/pages/Config.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<script lang="tsx" setup>
|
||||||
|
import SettingCard from '@/coms/SettingCard.vue'
|
||||||
|
import {useConfig} from '@/stores/config.ts'
|
||||||
|
import {onActivated, onDeactivated, onMounted, ref} from 'vue'
|
||||||
|
import {JwxtLoginMethodOptions} from '@/types/options.ts'
|
||||||
|
import {NCode, NTag, useMessage} from "naive-ui"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Config',
|
||||||
|
})
|
||||||
|
|
||||||
|
const CONFIG = useConfig()
|
||||||
|
const MESSAGE = useMessage()
|
||||||
|
|
||||||
|
const extraVisible = ref(true)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
CONFIG.update()
|
||||||
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
extraVisible.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
extraVisible.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
function varTag(code: string, description: string) {
|
||||||
|
return (
|
||||||
|
<NTag type="info">
|
||||||
|
<NCode inline>{code}</NCode> - {description}
|
||||||
|
</NTag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="config-container">
|
||||||
|
<n-h2 prefix="bar">酸酸设置</n-h2>
|
||||||
|
<n-alert title="设置注意" type="info">
|
||||||
|
<n-p class="no-margin">
|
||||||
|
<n-text strong type="warning">系统设置</n-text>
|
||||||
|
- 需要完全重新启动 Suan API 才能应用;
|
||||||
|
</n-p>
|
||||||
|
<n-p class="no-margin">其他的设置可以即时生效。</n-p>
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<n-collapse v-if="CONFIG.dataStatus" style="margin-top: 1rem">
|
||||||
|
<n-collapse-item name="system" title="系统设置">
|
||||||
|
<n-flex vertical>
|
||||||
|
<setting-card
|
||||||
|
v-model:string-value="CONFIG.data.system.host"
|
||||||
|
message="host"
|
||||||
|
show-input
|
||||||
|
title="监听地址"
|
||||||
|
/>
|
||||||
|
<setting-card
|
||||||
|
v-model:number-value="CONFIG.data.system.port"
|
||||||
|
message="port"
|
||||||
|
show-number
|
||||||
|
title="监听端口"
|
||||||
|
/>
|
||||||
|
<setting-card
|
||||||
|
v-model:boolean-value="CONFIG.data.system.reload"
|
||||||
|
message="使用 uvicorn.run(..., reload=True) ,不建议在正式部署时开启。(据说会降低性能且可能导致内存泄露)"
|
||||||
|
show-switch
|
||||||
|
title="热重载 Python 代码文件"
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
</n-collapse-item>
|
||||||
|
<n-collapse-item name="schedule" title="课表设置">
|
||||||
|
<n-flex vertical>
|
||||||
|
<setting-card
|
||||||
|
v-model:selection-value="CONFIG.data.schedule.jwxt_login_method"
|
||||||
|
:selection-options="JwxtLoginMethodOptions"
|
||||||
|
message="连接到校园网时,可以使用教务系统直接登录;在校园外则需要使用 SSO 统一身份认证。"
|
||||||
|
show-select
|
||||||
|
title="教务系统登录方式"
|
||||||
|
/>
|
||||||
|
<setting-card
|
||||||
|
v-model:date-value="CONFIG.data.schedule.semester_start_date"
|
||||||
|
message="当前学期从哪一天开始?我说的是第一周的星期一。给出非星期一的日期是未定义行为,请避免。"
|
||||||
|
show-date-picker
|
||||||
|
title="学期开始日期"
|
||||||
|
/>
|
||||||
|
<setting-card
|
||||||
|
v-model:string-value="CONFIG.data.schedule.schedule_title_template"
|
||||||
|
show-input
|
||||||
|
title="课表渲染图片的标题"
|
||||||
|
>
|
||||||
|
<n-p>标题中支持添加 Python 风格占位符,以代表动态填充的内容。</n-p>
|
||||||
|
<n-p>
|
||||||
|
具体来说,支持以下变量:
|
||||||
|
<component :is="varTag('week', '第几周课表')" />、
|
||||||
|
<component :is="varTag('week_start_day', '该周开始日期(YYYY-MM-DD)')" />、
|
||||||
|
<component :is="varTag('week_end_day', '该周结束日期(YYYY-MM-DD)')" />。
|
||||||
|
</n-p>
|
||||||
|
<n-p>
|
||||||
|
使用
|
||||||
|
<n-code inline>第 {week} 周</n-code>
|
||||||
|
来插入这些变量。
|
||||||
|
</n-p>
|
||||||
|
</setting-card>
|
||||||
|
<setting-card
|
||||||
|
v-model:string-value="CONFIG.data.schedule.schedule_subtitle_template"
|
||||||
|
message="简单来说,和上面一样可以使用变量。"
|
||||||
|
show-input
|
||||||
|
title="课表渲染图片的副标题"
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
</n-collapse-item>
|
||||||
|
<n-collapse-item name="log" title="日志设置">
|
||||||
|
<n-flex vertical>
|
||||||
|
<setting-card
|
||||||
|
v-model:boolean-value="CONFIG.data.log.log_api_request_details"
|
||||||
|
message="除去下面的特殊端点之外的所有端点"
|
||||||
|
show-switch
|
||||||
|
title="记录 API 调用详细日志"
|
||||||
|
/>
|
||||||
|
<setting-card
|
||||||
|
v-model:boolean-value="CONFIG.data.log.log_mcp_request_details"
|
||||||
|
message="端点 /mcp 的请求会被自动忽略,MCP 调用日志"
|
||||||
|
show-switch
|
||||||
|
title="记录 MCP 调用详细日志"
|
||||||
|
/>
|
||||||
|
<setting-card
|
||||||
|
v-model:boolean-value="CONFIG.data.log.log_assets_request"
|
||||||
|
message="端点位于 /assets(建议关闭,只有在生成图片和使用 WebUI 时才会访问其中资源。)"
|
||||||
|
show-switch
|
||||||
|
title="记录静态资源调用请求"
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
|
||||||
|
<teleport v-if="extraVisible" defer to="#center-container">
|
||||||
|
<div class="header-card">
|
||||||
|
<n-flex vertical>
|
||||||
|
<n-button
|
||||||
|
circle
|
||||||
|
size="large"
|
||||||
|
type="success"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
CONFIG.save()
|
||||||
|
MESSAGE.success('保存设置成功,后端会自动应用新的设置 ~')
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>保存</n-button
|
||||||
|
>
|
||||||
|
<n-button circle size="large" type="warning">重启</n-button>
|
||||||
|
<n-button circle size="large" type="error">关闭</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
29
webui/src/pages/IndexSchedule.vue
Normal file
29
webui/src/pages/IndexSchedule.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import ScheduleTable from '@/coms/ScheduleTable.vue'
|
||||||
|
import {onMounted, ref} from 'vue'
|
||||||
|
import {useRoute} from 'vue-router'
|
||||||
|
import type {CourseDto} from '@/types/schedule.ts'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const courses = ref<CourseDto[]>([])
|
||||||
|
const title = ref('')
|
||||||
|
const subtitle = ref('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.query.data) {
|
||||||
|
courses.value = JSON.parse(route.query.data as string)
|
||||||
|
} else {
|
||||||
|
console.warn('🐔 未提供 data query参数,无法渲染课程表》')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.query.title) title.value = route.query.title as string
|
||||||
|
if (route.query.subtitle) subtitle.value = route.query.subtitle as string
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<schedule-table v-if="courses.length !== 0" :courses :subtitle :title />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
106
webui/src/pages/Log.vue
Normal file
106
webui/src/pages/Log.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef} from 'vue'
|
||||||
|
import {AnsiUp} from 'ansi_up' // 用于解析日志颜色
|
||||||
|
import type {logDto} from '@/types/logger'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Log',
|
||||||
|
})
|
||||||
|
|
||||||
|
const logContent = ref('')
|
||||||
|
const logBox = useTemplateRef('logBox')
|
||||||
|
const ansiUp = new AnsiUp()
|
||||||
|
ansiUp.use_classes = true
|
||||||
|
let ws: WebSocket | null = null
|
||||||
|
|
||||||
|
const initWebSocket = () => {
|
||||||
|
ws = new WebSocket('ws://127.0.0.1:8000/ws/logs')
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
// 1. 解析 ANSI 颜色编码
|
||||||
|
const logs = JSON.parse(event.data)['logs'] as logDto[]
|
||||||
|
for (const log of logs) {
|
||||||
|
logContent.value += ansiUp.ansi_to_html(log.message) + '<br>'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 自动滚动到底部
|
||||||
|
nextTick(() => {
|
||||||
|
if (logBox.value) {
|
||||||
|
logBox.value.scrollTop = logBox.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = (e) => console.error('WebSocket Error:', e)
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket Closed, reconnecting...')
|
||||||
|
setTimeout(initWebSocket, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(initWebSocket)
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (ws) {
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="log-container">
|
||||||
|
<n-h2 prefix="bar">酸酸日志</n-h2>
|
||||||
|
<n-alert title="酸……吗?" type="info">
|
||||||
|
<n-p>
|
||||||
|
FastAPI 后端的日志会在此处显示,这样也许可以方便你检查酸 API 的运行情况。
|
||||||
|
<n-text strong type="warning">这不是 WebUI 的日志。</n-text>
|
||||||
|
</n-p>
|
||||||
|
</n-alert>
|
||||||
|
<n-alert title="如果遇到严重错误" type="warning">
|
||||||
|
<n-p>
|
||||||
|
如果 FastAPI 后端遇到严重错误,日志可能无法被顺利发送到 WebUI。所以如果你发现酸 API
|
||||||
|
出现事实性故障,那么你可能
|
||||||
|
<n-text strong type="warning">需要亲自查看终端和文件中的日志。</n-text>
|
||||||
|
</n-p>
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<div ref="logBox" class="log-container">
|
||||||
|
<pre id="log-content" v-html="logContent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-p>
|
||||||
|
日志会以追加模式(w)写入到
|
||||||
|
<n-code inline>data/app.log</n-code>
|
||||||
|
文件中。
|
||||||
|
</n-p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div.log-container {
|
||||||
|
padding: 6px;
|
||||||
|
width: 98%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
background-color: #191a1c;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre#log-content {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1;
|
||||||
|
color: #cdcdcd;
|
||||||
|
|
||||||
|
font-family:
|
||||||
|
Maple Mono CN,
|
||||||
|
sans-serif;
|
||||||
|
letter-spacing: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
57
webui/src/pages/Overview.vue
Normal file
57
webui/src/pages/Overview.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {onMounted, ref} from 'vue'
|
||||||
|
import {useConfig} from '@/stores/config.ts'
|
||||||
|
import type {FastApiDto} from '@/types/fastapi.ts'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Overview',
|
||||||
|
})
|
||||||
|
|
||||||
|
const config = useConfig()
|
||||||
|
|
||||||
|
const api_version = ref('unknown')
|
||||||
|
const webui_version = __VERSION__
|
||||||
|
|
||||||
|
interface VerDto {
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetch('/version', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data: FastApiDto) => data.result as VerDto)
|
||||||
|
.then((data: VerDto) => {
|
||||||
|
api_version.value = data.version
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="overview-container">
|
||||||
|
<n-h1 prefix="bar">NJUPT Suan API WebUI</n-h1>
|
||||||
|
|
||||||
|
<n-flex vertical>
|
||||||
|
<n-card title="FastAPI 后端状态">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-tag type="info">版本 {{ api_version }}</n-tag>
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
<n-card title="WebUI 前端状态">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-tag type="info">版本 {{ webui_version }}</n-tag>
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</n-flex>
|
||||||
|
|
||||||
|
<n-p>
|
||||||
|
欢迎使用 NJUPT Suan API。本项目仍然处于早期开发阶段,更多新功能和稳定性改善仍在计划中~
|
||||||
|
</n-p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
21
webui/src/pages/Schedule.vue
Normal file
21
webui/src/pages/Schedule.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import ScheduleDescription from '@/pages/ScheduleDescription.vue'
|
||||||
|
import ScheduleQuery from '@/pages/ScheduleQuery.vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Schedule',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="schedule-container">
|
||||||
|
<n-h2 prefix="bar">课表管理</n-h2>
|
||||||
|
<n-p>可以设置 API 以及 MCP 如何获取你的课表数据。</n-p>
|
||||||
|
|
||||||
|
<schedule-description />
|
||||||
|
|
||||||
|
<schedule-query />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
108
webui/src/pages/ScheduleDescription.vue
Normal file
108
webui/src/pages/ScheduleDescription.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import ApiCard from '@/coms/ApiCard.vue'
|
||||||
|
import ToolCard from '@/coms/ToolCard.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-alert title="通用参数" type="info">
|
||||||
|
<n-p class="no-margin">
|
||||||
|
<n-code inline>week</n-code> 数字,指示获取第几周的课表。可选,默认值为 0 表明获取全部。
|
||||||
|
</n-p>
|
||||||
|
<n-p class="no-margin">
|
||||||
|
<n-code inline>img</n-code> 布尔值,指示是否生成课表图片。可选,默认值为 False。若为
|
||||||
|
True,生成的图片的临时链接会在 <n-code inline>img_url</n-code> 中提供。
|
||||||
|
</n-p>
|
||||||
|
<n-p class="no-margin">
|
||||||
|
<n-text strong type="warning">
|
||||||
|
目前课表图片只考虑了生成某一周的课程表,因此
|
||||||
|
<n-code inline>week=0;img=true</n-code>
|
||||||
|
是未定义行为,请注意避免。
|
||||||
|
</n-text>
|
||||||
|
</n-p>
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<n-collapse style="margin-top: 1rem">
|
||||||
|
<n-collapse-item name="1" title="可用接口">
|
||||||
|
<n-flex vertical>
|
||||||
|
<api-card description="获取班级课表" method="POST" path="/api/schedule/class">
|
||||||
|
<n-p>
|
||||||
|
可以携带
|
||||||
|
<n-code inline>username</n-code>
|
||||||
|
和
|
||||||
|
<n-code inline>password</n-code>
|
||||||
|
两个参数,携带时将会使用它们登录教务系统并查询。携带时,将会查询该学生所在班级的课表。
|
||||||
|
</n-p>
|
||||||
|
<n-p>
|
||||||
|
不携带上述参数调用时,会返回数据库中存储的班级课表。你可以在下方的「课表查询」中保存一个班级课表。
|
||||||
|
</n-p>
|
||||||
|
</api-card>
|
||||||
|
<api-card description="获取学生课表" method="POST" path="/api/schedule/student">
|
||||||
|
<n-p>
|
||||||
|
需要携带
|
||||||
|
<n-code inline>username</n-code>
|
||||||
|
和
|
||||||
|
<n-code inline>password</n-code>
|
||||||
|
两个参数,确保学生本人查询。
|
||||||
|
</n-p>
|
||||||
|
</api-card>
|
||||||
|
<api-card description="获取课表图片" method="GET" path="/api/schedule/img/{name}">
|
||||||
|
<n-p>
|
||||||
|
<n-text strong type="warning">不接收包括上面的通用参数在内的任何参数。</n-text>
|
||||||
|
</n-p>
|
||||||
|
<n-p>
|
||||||
|
图片名需要被包含在请求路径中。如果你在调用上面两个方法时指示生成图片,那么图片的完整路径应当已经被提供给你。
|
||||||
|
</n-p>
|
||||||
|
</api-card>
|
||||||
|
</n-flex>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item name="2" title="可用工具">
|
||||||
|
<n-flex vertical>
|
||||||
|
<tool-card description="获取默认班级课表" name="tool_schedule_class" type="Tool">
|
||||||
|
返回数据库中存储的班级课表。你可以在下方的「课表查询」中保存一个班级课表。
|
||||||
|
</tool-card>
|
||||||
|
<tool-card
|
||||||
|
description="获取指定学生的班级课表"
|
||||||
|
name="tool_schedule_class_special"
|
||||||
|
type="Tool"
|
||||||
|
>
|
||||||
|
<n-p>
|
||||||
|
需要携带
|
||||||
|
<n-code inline>username</n-code>
|
||||||
|
和
|
||||||
|
<n-code inline>password</n-code>
|
||||||
|
两个参数,确保学生本人查询。
|
||||||
|
</n-p>
|
||||||
|
</tool-card>
|
||||||
|
<tool-card
|
||||||
|
description="获取指定学生的个人课表"
|
||||||
|
name="tool_schedule_student_special"
|
||||||
|
type="Tool"
|
||||||
|
>
|
||||||
|
<n-p>
|
||||||
|
需要携带
|
||||||
|
<n-code inline>username</n-code>
|
||||||
|
和
|
||||||
|
<n-code inline>password</n-code>
|
||||||
|
两个参数,确保学生本人查询。
|
||||||
|
</n-p>
|
||||||
|
</tool-card>
|
||||||
|
<tool-card description="直接获取课表图片" name="tool_schedule_image" type="Tool">
|
||||||
|
<n-p>
|
||||||
|
<n-text strong type="warning">本工具不接收上面的公共参数。</n-text>
|
||||||
|
</n-p>
|
||||||
|
<n-p>
|
||||||
|
与其他工具返回图片的不同点在于,此工具会以 MCP 协议直接返回图片本身,避免客户端及 LLM
|
||||||
|
二次下载。图片直接包含在会话中也可以避免酸 API 本地的图片被清理时导致 404。
|
||||||
|
</n-p>
|
||||||
|
<n-p>
|
||||||
|
本工具只接收一个参数 <n-code inline>img_name</n-code> 作为图片的
|
||||||
|
<n-text strong type="success">文件名</n-text>。
|
||||||
|
</n-p>
|
||||||
|
</tool-card>
|
||||||
|
</n-flex>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
73
webui/src/pages/ScheduleQuery.vue
Normal file
73
webui/src/pages/ScheduleQuery.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed, onMounted, ref} from 'vue'
|
||||||
|
import {scheduleTypeOptions} from '@/types/options.ts'
|
||||||
|
import {admin} from '@/tools/web.ts'
|
||||||
|
|
||||||
|
const formValue = ref({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
scheduleType: 'class',
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryTestResult = ref<object | null>(null)
|
||||||
|
const queryTestResultJson = computed(() => JSON.stringify(queryTestResult.value, null, 2))
|
||||||
|
|
||||||
|
function query() {
|
||||||
|
admin
|
||||||
|
.post('schedule/test', formValue.value)
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((data) => {
|
||||||
|
queryTestResult.value = data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
admin
|
||||||
|
.get('schedule/test')
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((data) => {
|
||||||
|
queryTestResult.value = data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-h3>课表查询</n-h3>
|
||||||
|
|
||||||
|
<n-p>
|
||||||
|
在这里尝试查询课表,检查是否成功。在此处查询的最后一次
|
||||||
|
<n-text strong type="info">班级课表</n-text>
|
||||||
|
会被保存在数据库中,以供无凭证无参数调用酸 API 获取班级课表时返回。
|
||||||
|
</n-p>
|
||||||
|
<n-p>
|
||||||
|
<n-text strong type="warning"
|
||||||
|
>出于隐私保护的考虑,个人课表不会被保存,需要每次都携带凭证查询。</n-text
|
||||||
|
>
|
||||||
|
</n-p>
|
||||||
|
|
||||||
|
<n-card>
|
||||||
|
<template #default>
|
||||||
|
<n-form :model="formValue" class="no-margin-bottom" inline label-width="auto">
|
||||||
|
<n-form-item label="学号" path="username">
|
||||||
|
<n-input v-model:value="formValue.username" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="密码" path="password">
|
||||||
|
<n-input v-model:value="formValue.password" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="课表类型" path="scheduleType">
|
||||||
|
<n-select
|
||||||
|
v-model:value="formValue.scheduleType"
|
||||||
|
:options="scheduleTypeOptions"
|
||||||
|
class="min-width"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item>
|
||||||
|
<n-button type="primary" @click="query()">查询</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<n-code :code="queryTestResultJson" language="json" />
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
163
webui/src/pages/Style.vue
Normal file
163
webui/src/pages/Style.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import ScheduleTable from '@/coms/ScheduleTable.vue'
|
||||||
|
import {onMounted, ref} from 'vue'
|
||||||
|
import type {AliasDto, CourseDto} from '@/types/schedule.ts'
|
||||||
|
import {admin, api} from '@/tools/web.ts'
|
||||||
|
import type {FastApiDto} from '@/types/fastapi.ts'
|
||||||
|
import SettingCard from '@/coms/SettingCard.vue'
|
||||||
|
import {scheduleTypeOptions} from '@/types/options.ts'
|
||||||
|
import {useMessage} from 'naive-ui'
|
||||||
|
|
||||||
|
const MESSAGE = useMessage()
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Style',
|
||||||
|
})
|
||||||
|
|
||||||
|
const courses = ref<CourseDto[] | null>(null)
|
||||||
|
const aliases = ref<AliasDto[]>([])
|
||||||
|
|
||||||
|
const originalName = ref('')
|
||||||
|
const aliasName = ref('')
|
||||||
|
|
||||||
|
const scheduleImgUrl = ref<string | null>(null)
|
||||||
|
|
||||||
|
function query() {
|
||||||
|
courses.value = null
|
||||||
|
console.log(formValue.value)
|
||||||
|
api
|
||||||
|
.post(scheduleType.value === 'class' ? 'schedule/class' : 'schedule/student', {
|
||||||
|
username: formValue.value.username === '' ? null : formValue.value.username,
|
||||||
|
password: formValue.value.password === '' ? null : formValue.value.password,
|
||||||
|
week: formValue.value.week,
|
||||||
|
img: formValue.value.img,
|
||||||
|
})
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((data: FastApiDto) => {
|
||||||
|
courses.value = data.result as CourseDto[]
|
||||||
|
scheduleImgUrl.value = data.img_url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAlias() {
|
||||||
|
if (originalName.value === '' || aliasName.value === '') {
|
||||||
|
MESSAGE.error('需要输入别名……')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
admin
|
||||||
|
.post('schedule/alias', {
|
||||||
|
originalName: originalName.value,
|
||||||
|
aliasName: aliasName.value,
|
||||||
|
})
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((data: FastApiDto) => {
|
||||||
|
if (data.success) {
|
||||||
|
MESSAGE.success('课程别名添加成功~')
|
||||||
|
originalName.value = ''
|
||||||
|
aliasName.value = ''
|
||||||
|
} else {
|
||||||
|
MESSAGE.error(`课程别名添加失败:${data.message}`)
|
||||||
|
console.error(`课程别名添加失败:${data.message}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
refreshAlias()
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAlias() {
|
||||||
|
admin
|
||||||
|
.get('schedule/alias')
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((data: FastApiDto) => (aliases.value = data.result as AliasDto[]))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
query()
|
||||||
|
refreshAlias()
|
||||||
|
})
|
||||||
|
|
||||||
|
const formValue = ref({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
week: 7,
|
||||||
|
img: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const scheduleType = ref('class')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="style-container">
|
||||||
|
<n-h2 prefix="bar">样式设计</n-h2>
|
||||||
|
<n-p>在这里预览酸 API 返回的课程表图片的样式效果。</n-p>
|
||||||
|
|
||||||
|
<n-flex>
|
||||||
|
<setting-card
|
||||||
|
message="看看谁的哪一周的课表呢?学号也可以为空,以预览默认班级课表。"
|
||||||
|
title="预览设置"
|
||||||
|
>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-flex justify="right" style="width: max-content">
|
||||||
|
<n-select
|
||||||
|
v-model:value="scheduleType"
|
||||||
|
:options="scheduleTypeOptions"
|
||||||
|
class="min-width max-width"
|
||||||
|
/>
|
||||||
|
<n-button type="success" @click="query()">查</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<n-form :model="formValue" class="no-margin-bottom" inline label-width="auto">
|
||||||
|
<n-form-item label="学号" path="username">
|
||||||
|
<n-input v-model:value="formValue.username" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="密码" path="password">
|
||||||
|
<n-input v-model:value="formValue.password" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="第()周" path="week">
|
||||||
|
<n-input-number v-model:value="formValue.week" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="生成图片" path="img">
|
||||||
|
<n-switch v-model:value="formValue.img" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<n-p v-if="scheduleImgUrl">
|
||||||
|
课表图片位于 <n-text strong type="success">{{ scheduleImgUrl }}</n-text>
|
||||||
|
</n-p>
|
||||||
|
</template>
|
||||||
|
</setting-card>
|
||||||
|
|
||||||
|
<setting-card
|
||||||
|
message="为前几个字易混淆的课程设置别名,方便区分。例如,将「计算机问题求解(使用C语言)」的别名设置为「C语言」。"
|
||||||
|
title="课程别名"
|
||||||
|
>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-flex justify="right" style="width: max-content">
|
||||||
|
<n-input
|
||||||
|
v-model:value="originalName"
|
||||||
|
class="min-width-large max-width-large"
|
||||||
|
placeholder="计算机问题求解(使用C语言)"
|
||||||
|
/>
|
||||||
|
<n-input v-model:value="aliasName" class="min-width max-width" placeholder="C" />
|
||||||
|
<n-button type="primary" @click="addAlias()">添加别名</n-button>
|
||||||
|
<n-button secondary type="info" @click="refreshAlias()">刷新</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<n-flex>
|
||||||
|
<n-tag v-for="alias in aliases" :key="alias.id" type="info">
|
||||||
|
{{ alias.originalName }} => {{ alias.aliasName }}
|
||||||
|
</n-tag>
|
||||||
|
</n-flex>
|
||||||
|
<n-p
|
||||||
|
>如需更新预览效果,添加别名之后需要重新<n-text strong type="success">查</n-text
|
||||||
|
>一次课表哦。</n-p
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</setting-card>
|
||||||
|
</n-flex>
|
||||||
|
|
||||||
|
<schedule-table v-if="courses !== null" :courses />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
9
webui/src/router/index-schedule.ts
Normal file
9
webui/src/router/index-schedule.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||||
|
import IndexSchedule from '@/pages/IndexSchedule.vue'
|
||||||
|
|
||||||
|
const scheduleRouter = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes: [{ path: '/', component: IndexSchedule }],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default scheduleRouter
|
||||||
19
webui/src/router/index.ts
Normal file
19
webui/src/router/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||||
|
import Overview from '@/pages/Overview.vue'
|
||||||
|
import Schedule from '@/pages/Schedule.vue'
|
||||||
|
import Log from '@/pages/Log.vue'
|
||||||
|
import Style from '@/pages/Style.vue'
|
||||||
|
import Config from '@/pages/Config.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{ path: '/', component: Overview, name: 'Overview' },
|
||||||
|
{ path: '/schedule', component: Schedule, name: 'Schedule' },
|
||||||
|
{ path: '/style', component: Style, name: 'Style' },
|
||||||
|
{ path: '/log', component: Log, name: 'Log' },
|
||||||
|
{ path: '/config', component: Config, name: 'Config' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
58
webui/src/stores/config.ts
Normal file
58
webui/src/stores/config.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import type {
|
||||||
|
ConfigDataDto,
|
||||||
|
ConfigLogDto,
|
||||||
|
ConfigScheduleDto,
|
||||||
|
ConfigSystemDto,
|
||||||
|
} from '@/types/config.ts'
|
||||||
|
import {admin} from '@/tools/web.ts'
|
||||||
|
import type {FastApiDto} from '@/types/fastapi.ts'
|
||||||
|
|
||||||
|
export const useConfig = defineStore('config', () => {
|
||||||
|
const tokenValidated = ref(false)
|
||||||
|
|
||||||
|
const dataStatus = ref(false)
|
||||||
|
|
||||||
|
const data = ref<{
|
||||||
|
system: ConfigSystemDto
|
||||||
|
schedule: ConfigScheduleDto
|
||||||
|
log: ConfigLogDto
|
||||||
|
}>({
|
||||||
|
system: {},
|
||||||
|
schedule: {},
|
||||||
|
log: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
dataStatus.value = false
|
||||||
|
admin
|
||||||
|
.get('config')
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((_data: FastApiDto) => _data.result)
|
||||||
|
// @ts-ignore
|
||||||
|
.then((_data: ConfigDataDto) => {
|
||||||
|
data.value.system = _data.system
|
||||||
|
data.value.schedule = _data.schedule
|
||||||
|
data.value.log = _data.log
|
||||||
|
})
|
||||||
|
.finally(() => (dataStatus.value = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
dataStatus.value = false
|
||||||
|
admin
|
||||||
|
.post('config', { data: data.value })
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((_data: FastApiDto) => _data.result)
|
||||||
|
// @ts-ignore
|
||||||
|
.then((_data: ConfigDataDto) => {
|
||||||
|
data.value.system = _data.system
|
||||||
|
data.value.schedule = _data.schedule
|
||||||
|
data.value.log = _data.log
|
||||||
|
})
|
||||||
|
.finally(() => (dataStatus.value = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tokenValidated, dataStatus, data, update, save }
|
||||||
|
})
|
||||||
6
webui/src/tools/random.ts
Normal file
6
webui/src/tools/random.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export function getGradientColor() {
|
||||||
|
const h = Math.floor(Math.random() * 200 + 20) // 20-220
|
||||||
|
const s = Math.floor(Math.random() * 20 + 80) // 70%-100% 饱和度
|
||||||
|
const l = Math.floor(Math.random() * 30 + 60) // 60%-90% 亮度
|
||||||
|
return `linear-gradient(45deg, hsl(${h}, ${s}%, ${l}%), hsl(${h}, ${s - 20}%, ${l + 10}%))`
|
||||||
|
}
|
||||||
34
webui/src/tools/web.ts
Normal file
34
webui/src/tools/web.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import type { FastApiDto } from '@/types/fastapi.ts'
|
||||||
|
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const admin = axios.create({
|
||||||
|
baseURL: '/admin',
|
||||||
|
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证给定的 Token 是否正确。
|
||||||
|
* 不读取、不保存,仅在正确时自动设置 admin axios 的验证头。
|
||||||
|
*/
|
||||||
|
export async function validateAdminToken(token: string) {
|
||||||
|
const success = await axios
|
||||||
|
.post(
|
||||||
|
'/admin/validateToken',
|
||||||
|
{ token: token },
|
||||||
|
{ headers: { 'Content-Type': 'application/json' } },
|
||||||
|
)
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((data: FastApiDto) => data.success)
|
||||||
|
if (success) {
|
||||||
|
admin.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
||||||
|
console.log('✅ Token 有效,已经为 admin axios 设置。')
|
||||||
|
} else {
|
||||||
|
console.log('❌ Token 验证失败。')
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
}
|
||||||
24
webui/src/types/config.ts
Normal file
24
webui/src/types/config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export interface ConfigSystemDto {
|
||||||
|
host?: string
|
||||||
|
port?: number
|
||||||
|
reload?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigScheduleDto {
|
||||||
|
jwxt_login_method?: 'jwxt' | 'sso'
|
||||||
|
semester_start_date?: string
|
||||||
|
schedule_title_template?: string
|
||||||
|
schedule_subtitle_template?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigLogDto {
|
||||||
|
log_api_request_details?: boolean
|
||||||
|
log_mcp_request_details?: boolean
|
||||||
|
log_assets_request?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigDataDto {
|
||||||
|
system: ConfigSystemDto
|
||||||
|
schedule: ConfigScheduleDto
|
||||||
|
log: ConfigLogDto
|
||||||
|
}
|
||||||
6
webui/src/types/fastapi.ts
Normal file
6
webui/src/types/fastapi.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface FastApiDto {
|
||||||
|
message: string
|
||||||
|
success: boolean
|
||||||
|
result: object
|
||||||
|
img_url: string | null
|
||||||
|
}
|
||||||
4
webui/src/types/logger.ts
Normal file
4
webui/src/types/logger.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface logDto {
|
||||||
|
id: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
23
webui/src/types/options.ts
Normal file
23
webui/src/types/options.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type {SelectOption} from 'naive-ui'
|
||||||
|
|
||||||
|
export const scheduleTypeOptions: SelectOption[] = [
|
||||||
|
{
|
||||||
|
label: '班级课表',
|
||||||
|
value: 'class',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '学生个人课表',
|
||||||
|
value: 'student',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const JwxtLoginMethodOptions: SelectOption[] = [
|
||||||
|
{
|
||||||
|
label: '教务系统直接登录',
|
||||||
|
value: 'jwxt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'SSO 统一身份认证',
|
||||||
|
value: 'sso',
|
||||||
|
},
|
||||||
|
]
|
||||||
23
webui/src/types/schedule.ts
Normal file
23
webui/src/types/schedule.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export interface CourseDto {
|
||||||
|
classroom: string
|
||||||
|
name: string
|
||||||
|
alias: string | null
|
||||||
|
teacher: string
|
||||||
|
day: number
|
||||||
|
weeks: number[]
|
||||||
|
classes: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CourseDatumDto {
|
||||||
|
name: string
|
||||||
|
alias: string | null
|
||||||
|
teacher: string
|
||||||
|
classroom: string
|
||||||
|
during: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AliasDto {
|
||||||
|
id: number
|
||||||
|
originalName: string
|
||||||
|
aliasName: string
|
||||||
|
}
|
||||||
22
webui/tsconfig.app.json
Normal file
22
webui/tsconfig.app.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
// Extra safety for array and object lookups, but may have false positives.
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
|
// Path mapping for cleaner imports.
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
// Specified here to keep it out of the root directory.
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
// JSX/TSX
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "vue"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
webui/tsconfig.json
Normal file
11
webui/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
webui/tsconfig.node.json
Normal file
27
webui/tsconfig.node.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node24/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"playwright.config.*",
|
||||||
|
"eslint.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
// Most tools use transpilation instead of Node.js's native type-stripping.
|
||||||
|
// Bundler mode provides a smoother developer experience.
|
||||||
|
"module": "preserve",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
|
||||||
|
// Include Node.js types and avoid accidentally including other `@types/*` packages.
|
||||||
|
"types": ["node"],
|
||||||
|
|
||||||
|
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
// Specified here to keep it out of the root directory.
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
62
webui/vite.config.ts
Normal file
62
webui/vite.config.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {fileURLToPath, URL} from 'node:url'
|
||||||
|
|
||||||
|
import {defineConfig} from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import {NaiveUiResolver} from 'unplugin-vue-components/resolvers'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import path from 'node:path/posix'
|
||||||
|
import {readFileSync} from "node:fs";
|
||||||
|
import {resolve} from 'path';
|
||||||
|
|
||||||
|
// 从 package.json 里搞到 WebUI 版本号
|
||||||
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'));
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueDevTools(),
|
||||||
|
vueJsx(),
|
||||||
|
AutoImport({
|
||||||
|
imports: ['vue', 'vue-router'],
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [NaiveUiResolver()],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
define: {
|
||||||
|
__VERSION__: JSON.stringify(pkg.version)
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rolldownOptions: {
|
||||||
|
input: {
|
||||||
|
index: path.resolve(__dirname, 'index.html'),
|
||||||
|
'index-schedule': path.resolve(__dirname, 'index-schedule.html'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/admin': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/version': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user