feat: 初始化仓库

This commit is contained in:
2024-11-11 11:08:47 +08:00
commit 1809b49e6b
90 changed files with 24491 additions and 0 deletions

15
.eslintignore Normal file
View File

@@ -0,0 +1,15 @@
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
Dockerfile
.hbuilderx

3
.eslintrc Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": ["@antfu"]
}

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
vite.config.*.timestamp*
yarn.lock
pnpm-lock.yaml
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
.hbuilderx
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

25
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"cSpell.words": [
"antfu",
"dcloudio",
"demi",
"iconify",
"miniprogram",
"Pinia",
"postprocess",
"unocss",
"unplugin",
"vite",
"weixin"
],
"i18n-ally.localesPaths": [],
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
"**/node_modules": true
}
}

21
index.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="/src/static/favicon.ico" />
<script>
const coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
`<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0${
coverSupport ? ', viewport-fit=cover' : ''}" />`)
</script>
<title>邮电云</title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

91
package.json Normal file
View File

@@ -0,0 +1,91 @@
{
"name": "color-timetable",
"version": "2.1.0",
"scripts": {
"dev:app": "uni -p app",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4020420240722002",
"@dcloudio/uni-app-harmony": "3.0.0-4020420240722002",
"@dcloudio/uni-app-plus": "3.0.0-4020420240722002",
"@dcloudio/uni-components": "3.0.0-4020420240722002",
"@dcloudio/uni-h5": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-alipay": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-baidu": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-jd": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-lark": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-qq": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-toutiao": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-weixin": "3.0.0-4020420240722002",
"@dcloudio/uni-mp-xhs": "3.0.0-4020420240722002",
"@dcloudio/uni-quickapp-webview": "3.0.0-4020420240722002",
"@dcloudio/uni-ui": "^1.5.6",
"@fullcalendar/core": "^6.1.15",
"@fullcalendar/daygrid": "^6.1.15",
"@fullcalendar/interaction": "^6.1.15",
"@fullcalendar/list": "^6.1.15",
"@fullcalendar/timegrid": "^6.1.15",
"@fullcalendar/vue3": "^6.1.15",
"dayjs": "^1.11.13",
"less": "^4.2.0",
"lodash-es": "^4.17.21",
"pinia": "^2.2.2",
"pinia-plugin-persistedstate": "^3.2.3",
"preact": "^10.23.2",
"rimraf": "^6.0.1",
"vue": "^3.4.38",
"vue-demi": "^0.13.11",
"vue-i18n": "^9.14.0"
},
"devDependencies": {
"@antfu/eslint-config": "^0.27.0",
"@dcloudio/types": "^3.4.12",
"@dcloudio/uni-automator": "3.0.0-4020420240722002",
"@dcloudio/uni-cli-shared": "3.0.0-4020420240722002",
"@dcloudio/uni-stacktracey": "3.0.0-4020420240722002",
"@dcloudio/vite-plugin-uni": "3.0.0-4020420240722002",
"@iconify-json/carbon": "^1.1.37",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.19.47",
"@vue/runtime-core": "^3.4.38",
"eslint": "8.57.0",
"postcss": "8.4.41",
"sass": "^1.77.8",
"sass-loader": "10.1.1",
"terser": "^5.31.6",
"typescript": "4.8.3",
"unocss": "^0.60.2",
"unocss-applet": "^0.6.0",
"unocss-preset-extra": "^0.5.3",
"unplugin-auto-import": "^0.11.5",
"vite": "^5.4.2"
}
}

5
renovate.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}

198
src/App.vue Normal file
View File

@@ -0,0 +1,198 @@
<script setup lang="ts">
import { onLaunch, onShow } from '@dcloudio/uni-app'
import { useAppStore } from '~/stores/modules/app'
import { usePageStore } from '~/stores/modules/page'
const { setPageConfig } = usePageStore()
const { darkMode, statusBarHeight, menuButtonBounding } = storeToRefs(
useAppStore(),
)
onLaunch(() => {
// #ifdef MP-WEIXIN || MP-QQ
const systemInfo = uni.getSystemInfoSync()
// the systemInfo.theme is only support dark mode in WeChat and QQ
darkMode.value = systemInfo?.theme === 'dark'
statusBarHeight.value = systemInfo!.statusBarHeight || 44
menuButtonBounding.value = uni.getMenuButtonBoundingClientRect()
uni.onThemeChange(
(res: UniApp.OnThemeChangeCallbackResult) =>
(darkMode.value = res.theme === 'dark'),
)
// #endif
// #ifdef H5
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)')
darkMode.value = colorScheme.matches
colorScheme.addEventListener(
'change',
(e: MediaQueryListEvent) => (darkMode.value = e.matches),
)
// The data is obtained from iPhone13 miniprogram but statusBarHeight, top and bottom values are subtracted from the statusBarHeight value
statusBarHeight.value = 0
menuButtonBounding.value = {
width: 87,
height: 32,
left: 281,
top: 4,
right: 368,
bottom: 36,
}
// #endif
// #ifdef APP
const appSystemInfo = uni.getWindowInfo()
uni.onThemeChange(
(res: UniApp.OnThemeChangeCallbackResult) =>
(darkMode.value = res.theme === 'dark'),
)
statusBarHeight.value = appSystemInfo.statusBarHeight
menuButtonBounding.value = {
width: appSystemInfo.screenWidth,
height: appSystemInfo.statusBarHeight,
left: appSystemInfo.safeArea.left,
top: appSystemInfo.statusBarHeight * 2,
right: appSystemInfo.safeArea.right,
bottom: appSystemInfo.statusBarHeight,
}
// #endif
})
onShow(() => {
setPageConfig({ showNavBar: false })
})
onHide(() => {})
</script>
<style lang="scss">
.loader {
position: absolute;
}
.loader {
top: 72vh;
left: 50vw;
transform: rotate(165deg);
}
.loader:after,
.loader:before {
content: '';
position: absolute;
top: 50%;
left: 50%;
display: block;
width: 0.5em;
height: 0.5em;
border-radius: 0.25em;
transform: translate(-50%, -50%);
}
.loader:before {
animation: before 2s infinite;
}
.loader:after {
animation: after 2s infinite;
}
@-webkit-keyframes before {
0% {
width: 0.5em;
box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75),
-1em 0.5em rgba(111, 202, 220, 0.75);
}
35% {
width: 2.5em;
box-shadow: 0 -0.5em rgba(225, 20, 98, 0.75),
0 0.5em rgba(111, 202, 220, 0.75);
}
70% {
width: 0.5em;
box-shadow: -1em -0.5em rgba(225, 20, 98, 0.75),
1em 0.5em rgba(111, 202, 220, 0.75);
}
to {
box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75),
-1em 0.5em rgba(111, 202, 220, 0.75);
}
}
@keyframes before {
0% {
width: 0.5em;
box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75),
-1em 0.5em rgba(111, 202, 220, 0.75);
}
35% {
width: 2.5em;
box-shadow: 0 -0.5em rgba(225, 20, 98, 0.75),
0 0.5em rgba(111, 202, 220, 0.75);
}
70% {
width: 0.5em;
box-shadow: -1em -0.5em rgba(225, 20, 98, 0.75),
1em 0.5em rgba(111, 202, 220, 0.75);
}
to {
box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75),
-1em 0.5em rgba(111, 202, 220, 0.75);
}
}
@-webkit-keyframes after {
0% {
height: 0.5em;
box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75),
-0.5em -1em rgba(233, 169, 32, 0.75);
}
35% {
height: 2.5em;
box-shadow: 0.5em 0 rgba(61, 184, 143, 0.75),
-0.5em 0 rgba(233, 169, 32, 0.75);
}
70% {
height: 0.5em;
box-shadow: 0.5em -1em rgba(61, 184, 143, 0.75),
-0.5em 1em rgba(233, 169, 32, 0.75);
}
to {
box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75),
-0.5em -1em rgba(233, 169, 32, 0.75);
}
}
@keyframes after {
0% {
height: 0.5em;
box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75),
-0.5em -1em rgba(233, 169, 32, 0.75);
}
35% {
height: 2.5em;
box-shadow: 0.5em 0 rgba(61, 184, 143, 0.75),
-0.5em 0 rgba(233, 169, 32, 0.75);
}
70% {
height: 0.5em;
box-shadow: 0.5em -1em rgba(61, 184, 143, 0.75),
-0.5em 1em rgba(233, 169, 32, 0.75);
}
to {
box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75),
-0.5em -1em rgba(233, 169, 32, 0.75);
}
}
</style>

188
src/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,188 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
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 mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onError: typeof import('@dcloudio/uni-app')['onError']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onHide: typeof import('@dcloudio/uni-app')['onHide']
const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch']
const onLoad: typeof import('@dcloudio/uni-app')['onLoad']
const onMounted: typeof import('vue')['onMounted']
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']
const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound']
const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll']
const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh']
const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom']
const onReady: typeof import('@dcloudio/uni-app')['onReady']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onResize: typeof import('@dcloudio/uni-app')['onResize']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage']
const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline']
const onShow: typeof import('@dcloudio/uni-app')['onShow']
const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap']
const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange']
const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection']
const onUnload: typeof import('@dcloudio/uni-app')['onUnload']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
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 resolveDirective: typeof import('vue')['resolveDirective']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const stores: typeof import('./stores/index')['default']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
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 useSlots: typeof import('vue')['useSlots']
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 vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onAddToFavorites: UnwrapRef<typeof import('@dcloudio/uni-app')['onAddToFavorites']>
readonly onBackPress: UnwrapRef<typeof import('@dcloudio/uni-app')['onBackPress']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onError: UnwrapRef<typeof import('@dcloudio/uni-app')['onError']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onHide: UnwrapRef<typeof import('@dcloudio/uni-app')['onHide']>
readonly onLaunch: UnwrapRef<typeof import('@dcloudio/uni-app')['onLaunch']>
readonly onLoad: UnwrapRef<typeof import('@dcloudio/uni-app')['onLoad']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onNavigationBarButtonTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']>
readonly onNavigationBarSearchInputChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']>
readonly onNavigationBarSearchInputClicked: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']>
readonly onNavigationBarSearchInputConfirmed: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']>
readonly onNavigationBarSearchInputFocusChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']>
readonly onPageNotFound: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageNotFound']>
readonly onPageScroll: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageScroll']>
readonly onPullDownRefresh: UnwrapRef<typeof import('@dcloudio/uni-app')['onPullDownRefresh']>
readonly onReachBottom: UnwrapRef<typeof import('@dcloudio/uni-app')['onReachBottom']>
readonly onReady: UnwrapRef<typeof import('@dcloudio/uni-app')['onReady']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onResize: UnwrapRef<typeof import('@dcloudio/uni-app')['onResize']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onShareAppMessage: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareAppMessage']>
readonly onShareTimeline: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareTimeline']>
readonly onShow: UnwrapRef<typeof import('@dcloudio/uni-app')['onShow']>
readonly onTabItemTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onTabItemTap']>
readonly onThemeChange: UnwrapRef<typeof import('@dcloudio/uni-app')['onThemeChange']>
readonly onUnhandledRejection: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnhandledRejection']>
readonly onUnload: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnload']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveDirective: UnwrapRef<typeof import('vue')['resolveDirective']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stores: UnwrapRef<typeof import('./stores/index')['default']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
}
}

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { UNotifyOptions } from '../UNotify/types'
import type { UToastOptions } from '../UToast/types'
import { useAppStore } from '~/stores/modules/app'
import { usePageStore } from '~/stores/modules/page'
const { darkMode, customBarHeight, statusBarHeight } = storeToRefs(
useAppStore(),
)
const { pageReset } = usePageStore()
const {
showNavBar,
showBackAction,
showCustomAction,
pageTitle,
notifyRef: _notifyRef,
toastRef: _toastRef,
} = storeToRefs(usePageStore())
const handleNavigateBack = () => uni.navigateBack({})
const notifyRef = ref<{ handleShowNotify: (options: UNotifyOptions) => {} }>()
const toastRef = ref<{ handleShowToast: (options: UToastOptions) => {} }>()
onMounted(() => {
_notifyRef.value = notifyRef.value
_toastRef.value = toastRef.value
})
onUnmounted(() => pageReset())
</script>
<template>
<view :class="darkMode ? 'dark' : ''">
<view class="bg-base color-base text-base relative">
<!-- custom navigation bar -->
<view
v-if="showNavBar"
class="bg-primary text-white w-full top-0 z-200 fixed font-bold"
:style="{ height: `${customBarHeight}px` }"
>
<view
:style="{
'padding-top': `${statusBarHeight}px`,
'height': `${customBarHeight - statusBarHeight}px`,
}"
>
<view class="h-full text-center px-6 relative">
<view
v-if="showBackAction || showCustomAction"
class="flex h-full text-xl left-4 absolute justify-center items-center"
>
<slot name="navAction">
<view
v-if="showBackAction && !showCustomAction"
class="i-carbon-chevron-left"
@click="handleNavigateBack"
/>
</slot>
</view>
<view class="flex h-full text-lg justify-center items-center">
<slot name="navContent">
{{ pageTitle }}
</slot>
</view>
</view>
</view>
</view>
<UNotify ref="notifyRef" />
<UToast ref="toastRef" />
<!-- page container -->
<view
class="overflow-auto"
:style="{
'height': `calc(100vh - ${customBarHeight}px)`,
'padding-top': `${customBarHeight}px`,
}"
>
<slot />
</view>
</view>
</view>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import { defineEmits, defineProps, withDefaults } from 'vue'
interface Props {
type?: 'default' | 'success' | 'error' | 'warning' | 'primary'
bg?: string
icon?: string
disabled?: boolean
}
withDefaults(defineProps<Props>(), {
type: () => 'default',
bg: () => '',
icon: () => '',
disabled: () => false,
})
const emit = defineEmits(['click'])
const handleTap = () => emit('click')
const bgColor = {
default: 'bg-gray-5',
success: 'bg-green-5',
error: 'bg-red-5',
warning: 'bg-orange-5',
primary: 'bg-blue-5',
}
</script>
<template>
<button
class="rounded-lg text-white text-base flex justify-center items-center shadow py-2"
:class="[bg ? bg : bgColor[type], icon ? 'gap-1' : '']" hover-class="grayscale-20" :hover-stay-time="150"
@tap="handleTap"
>
<slot name="icon">
<view v-if="icon" :class="icon" />
</slot>
<slot />
</button>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { UNotifyOptions, UNotifyType } from './types'
import { useAppStore } from '~/stores/modules/app'
const { customBarHeight } = storeToRefs(useAppStore())
const timer = ref<number | undefined>(undefined)
const show = ref(false)
const notifyType = ref<UNotifyType>('default')
const message = ref('')
const handleShowNotify = (options: UNotifyOptions) => {
const { type = 'default', message: _message = 'Unable to connect to the server.', duration = 2000 } = options
clearTimeout(timer.value)
show.value = true
notifyType.value = type
message.value = _message
timer.value = setTimeout(() => {
show.value = false
clearTimeout(timer.value)
timer.value = undefined
}, duration) as unknown as number
}
defineExpose({
handleShowNotify,
})
const bgColor = {
default: 'bg-gray-5',
success: 'bg-green-4',
error: 'bg-red-5',
warning: 'bg-orange-5',
primary: 'bg-blue-5',
}
</script>
<template>
<view
class="flex h-8 text-white w-full py-1 px-2 transition-all z-100 justify-center items-center fixed"
:class="bgColor[notifyType]"
:style="{ top: show ? `${customBarHeight}px` : '-100%' }"
>
{{ message }}
</view>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,7 @@
export type UNotifyType = 'default' | 'success' | 'error' | 'warning' | 'primary'
export interface UNotifyOptions {
type?: UNotifyType
message: string
duration?: number
}

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { UToastOptions, UToastType } from './types'
const timer = ref<number | undefined>(undefined)
const show = ref(false)
const notifyType = ref<UToastType>('default')
const message = ref('')
const handleShowToast = (options: UToastOptions) => {
const { type = 'default', message: _message = 'Unable to connect to the server.', duration = 2000 } = options
clearTimeout(timer.value)
show.value = true
notifyType.value = type
message.value = _message
timer.value = setTimeout(() => {
show.value = false
clearTimeout(timer.value)
timer.value = undefined
}, duration) as unknown as number
}
defineExpose({
handleShowToast,
})
const ToastClass = {
default: 'bg-gray-5 border-gray-2',
success: 'bg-green-5 border-green-3',
error: 'bg-red-5 border-red-3',
warning: 'bg-orange-5 border-orange-3',
primary: 'bg-blue-5 border-blue-3',
}
</script>
<template>
<view
v-if="show"
class="z-100 fixed flex justify-center items-center top-0 bottom-0 left-0 right-0"
>
<view
class="flex justify-center items-center py-2 px-4 rounded-lg border animate-fade-in-up animate-duration-200 text-white"
:class="ToastClass[notifyType]"
>
{{ message }}
</view>
</view>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,7 @@
export type UToastType = 'default' | 'success' | 'error' | 'warning' | 'primary'
export interface UToastOptions {
type?: UToastType
message: string
duration?: number
}

View File

@@ -0,0 +1,432 @@
<script>
export default {
props: {
data: {
type: Array,
default: () => [],
},
valueType: {
type: Object,
default: () => {
return {
label: 'label',
value: 'value',
}
},
},
value: {
type: [String, Number],
default: '',
},
clearable: {
type: Boolean,
default: false,
},
filterable: {
type: Boolean,
default: false,
},
searchType: {
type: Number,
default: 1,
},
placeholder: {
type: String,
default: '请选择',
},
noDataText: {
type: String,
default: '暂无数据',
},
arrLeft: {
type: Number,
default: 20,
},
size: {
type: Number,
default: 240,
},
closeSelect: {
type: Boolean,
default: false,
},
},
data() {
return {
show: false,
readonly: true,
isClick: false,
totalArr: [],
showData: [],
selLabel: '',
}
},
watch: {
filterable: {
immediate: true,
deep: true,
handler(news) {
this.readonly = !news
},
},
data: {
immediate: true,
deep: true,
handler(news) {
this.showData = news
this.totalArr = news
},
},
value: {
immediate: true,
deep: true,
handler(news) {
if (news) {
const index = this.data.findIndex(
ite => ite[this.valueType.value] == news,
)
if (index == -1) {
uni.showToast({
title: '传入的value不存在',
icon: 'none',
duration: 1500,
})
}
else {
this.selLabel = this.data[index][this.valueType.label]
}
}
},
},
closeSelect: {
immediate: true,
deep: true,
handler(news) {
this.show = news
},
},
},
created() {},
beforeUnmount() {},
methods: {
openSelect() {
this.show = !this.show
this.$emit('update:closeSelect', this.show)
this.isClick = !this.isClick
},
change(item) {
if (this.value != item[this.valueType]) {
this.$emit('input', item[this.valueType.value])
this.$emit('change', item[this.valueType.value])
}
this.selLabel = item[this.valueType.label]
this.show = false
this.$emit('update:closeSelect', this.show)
this.isClick = false
this.showData = this.data
},
clearItem() {
if (this.clearable) {
this.$emit('input', '')
this.$emit('change', '')
}
this.selLabel = ''
},
selectData(e) {
const sel = e.detail.value
if (sel) {
const arrCons = []
const selVal = []
this.data.forEach((item) => {
arrCons.push(item)
})
arrCons.forEach((item) => {
if (this.searchType == 1) {
if (item[this.valueType.label].includes(sel))
selVal.push(item)
}
else {
if (item[this.valueType.label] == sel)
selVal.push(item)
}
})
this.show = true
this.$emit('update:closeSelect', this.show)
this.showData = selVal
}
else {
this.showData = this.data
}
},
},
}
</script>
<template>
<view class="select_wrap" style="width: 80%">
<view
ref="select-input"
class="select_input"
:class="[isClick ? 'select_input_select' : '']"
>
<view v-if="!readonly" class="input_info" @click.stop="openSelect">
<input
placeholder-style="font-size: 14px;color: #a0a9b0;"
:focus="isClick"
:value="selLabel"
type="text"
:readonly="readonly"
:disabled="readonly"
autocomplete="off"
:placeholder="placeholder"
class="text_tips"
@input="selectData"
>
</view>
<view v-else class="input_info" @click.stop="openSelect">
<view :placeholder="placeholder" class="text_tips">
{{ selLabel }}
<text v-if="!selLabel">
{{ placeholder }}
</text>
</view>
</view>
<view class="icon_arrow" @click="clearItem">
<view
v-if="
(!value && !clearable)
|| (value && !clearable)
|| (!value && clearable && !filterable)
"
class="arrow"
:class="[show ? 'arrow_down' : 'arrow_up']"
/>
<view v-if="value && clearable" class="arrow-clear">
x
</view>
</view>
</view>
<view
v-if="show"
class="select_modal_con"
@touchmove.stop.prevent="() => {}"
>
<scroll-view scroll-y="true" class="select_modal select_modal_scroll">
<view ref="select_content" class="select_content">
<view
v-for="(item, index) in showData"
:key="index"
class="select_content_li"
:class="{ selected: value == item[valueType.value] }"
@click="change(item)"
>
{{ item[valueType.label] }}
</view>
<view v-if="!showData.length" class="select_content_li">
{{ noDataText }}
</view>
</view>
</scroll-view>
<!-- #ifndef H5 -->
<!-- <scroll-view scroll-y="true" class="select_modal select_modal_scroll">
<view class="select_content" ref="select_content">
<view v-for="(item, index) in showData" :key="index" class="select_content_li" :class="{'selected': value == item[valueType.value]}" @click="change(item)">{{item[valueType.label]}}</view>
<view class="select_content_li" v-if="!showData.length">{{noDataText}}</view>
</view>
</scroll-view> -->
<!-- #endif -->
<!-- #ifdef H5 -->
<!-- <view class="select_modal">
<view class="select_content" ref="select_content">
<view v-for="(item, index) in showData" :key="index" class="select_content_li" :class="{'selected': value == item[valueType.value]}" @click="change(item)">{{item[valueType.label]}}</view>
<view class="select_content_li" v-if="!showData.length">{{noDataText}}</view>
</view>
</view> -->
<!-- #endif -->
<view class="cons_arrow" :style="{ left: `${arrLeft}px` }" />
</view>
</view>
</template>
<style lang="scss" scoped>
.select_wrap {
width: 240px;
display: inline-block;
position: relative;
.select_input {
-webkit-appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: inherit;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
box-sizing: border-box;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 100%;
padding-right: 30px;
.input_info {
font-size: 18px;
width: 100%;
height: 100%;
.text_tips {
height: 100%;
text {
font-size: 14px;
color: #a0a9b0;
}
}
}
.icon_arrow {
position: absolute;
width: 30px;
height: 40px;
right: 0;
top: 0;
text-align: center;
color: #c0c4cc;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
.arrow {
width: 10px;
height: 10px;
background-color: transparent;
/* 模块背景为透明 */
border-color: #c0c4cc;
border-style: solid;
border-width: 1px 1px 0 0;
box-sizing: border-box;
transition: all 0.3s;
box-sizing: border-box;
/*箭头方向可以自由切换角度*/
}
.arrow_down {
transform: rotate(-45deg);
margin-top: 5px;
}
.arrow_up {
transform: rotate(135deg);
margin-top: -5px;
}
.arrow-clear {
width: 14px;
height: 14px;
border: 1px solid #e4e7ed;
color: #e4e7ed;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
}
}
}
.select_input_select {
border-color: #409eff;
}
}
.select_modal_con {
width: 100%;
transform-origin: center top;
z-index: 2062;
position: absolute;
top: 40px;
left: 0;
border: 1px solid #e4e7ed;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
box-sizing: border-box;
margin-top: 12px;
.cons_arrow {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 10%;
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #ebeef5;
}
.cons_arrow:after {
content: ' ';
border-width: 6px;
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
}
.select_modal {
overflow: scroll;
height: 160px;
.select_content {
list-style: none;
padding: 6px 0;
margin: 0;
box-sizing: border-box;
.select_content_li {
font-size: 14px;
padding: 0 20px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #606266;
height: 34px;
line-height: 34px;
box-sizing: border-box;
cursor: pointer;
&.selected {
color: #409eff;
font-weight: 700;
background-color: #f5f7fa;
}
}
.select_content_li:hover {
background-color: #f5f7fa;
}
}
}
.select_modal_scroll {
overflow: hidden;
height: 160px;
}
// .select_content {
// background-color: #fff;
// .select_content_li {
// padding: 12rpx;
// }
// }
</style>

View File

@@ -0,0 +1,111 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { CourseModel } from '~/stores/modules/course'
import { useCourseStore, weekTitle } from '~/stores/modules/course'
const props = withDefaults(
defineProps<{ showActionSheet: boolean; courseItem: CourseModel }>(),
{
showActionSheet: false,
courseItem: undefined,
},
)
const emit = defineEmits(['cancel'])
const courseStore = useCourseStore()
const courseList = computed(() =>
courseStore.getConflictCourse(props.courseItem),
)
function getCourseTime(item: CourseModel) {
const { week, start, duration } = item
return `${weekTitle[week - 1]}${start}-${start + duration - 1}`
}
function navigateToDetail(courseItem: CourseModel) {
closeActionSheet()
uni.navigateTo({
url: `/pages/course/detail?id=${courseItem.id}`,
})
}
function closeActionSheet() {
emit('cancel')
}
</script>
<template>
<view @touchmove.prevent>
<view
class="bg-base w-full min-h-10 z-100 fixed"
transition="all duration-300 ease-in-out"
:class="
showActionSheet && courseList?.length ? 'bottom-0' : '-bottom-full'
"
>
<view class="py-6" flex="~ col gap6">
<!-- <view v-if="courseList?.length" class="font-medium text-xl px-4">
{{ courseTime }}
</view> -->
<template v-for="(course, index) of courseList" :key="index">
<view
class="px-4"
flex="~ col gap-2"
@click="navigateToDetail(course)"
>
<view
class="flex mb-1 w-full gap-2 justify-start items-center relative"
>
<view
class="rounded-full h-5 w-1 inline-block"
:style="`background-color:${course.color}`"
/>
<view class="font-medium text-lg">
{{ course.title }}
</view>
<view
class="text-xl top-0 right-4 bottom-0 z-20 absolute"
:class="index ? 'i-carbon-up-to-top' : ''"
@click.stop="courseStore.setCourseItemTop(course)"
/>
</view>
<view class="flex gap-1 justify-start items-center">
<view class="i-carbon-book" />
{{ course.teacher || '' }}
</view>
<view class="flex gap-1 justify-start items-center">
<view class="i-carbon-location" />
{{ course.location }}
</view>
<view class="flex gap-1 justify-start items-center">
<view class="i-carbon-alarm" />
{{ getCourseTime(course) }}
</view>
</view>
</template>
</view>
<view
class="flex pb-safe h-12"
text="center lg dark:!white"
b="t-4 gray-200 dark:op-20"
justify-center
items-center
hover-class="bg-gray-200 bg-opacity-50"
:hover-stay-time="150"
@click="closeActionSheet"
>
关闭
</view>
</view>
<view
class="bg-dark-100 bg-opacity-50 transition-all top-0 right-0 bottom-0 left-0 z-90 fixed"
:class="
showActionSheet && courseList?.length
? 'opacity-100 visible'
: 'opacity-0 invisible'
"
@click="closeActionSheet"
/>
</view>
</template>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useCourseStore } from '~/stores/modules/course'
const props = defineProps<{ showCourseAction: boolean }>()
const { parsedCourseList, originalWeekIndex, currentWeekIndex } = storeToRefs(useCourseStore())
const { setCurrentWeekIndex } = useCourseStore()
const scrollTo = ref('week0')
watch(
() => +props.showCourseAction + currentWeekIndex.value,
() => {
if (props.showCourseAction)
scrollTo.value = `week${currentWeekIndex.value - 1}`
})
</script>
<template>
<scroll-view
class="transition-height duration-300 overflow-scroll whitespace-nowrap"
:class="showCourseAction ? 'h-20' : 'h-0'" scroll-x scroll-with-animation :scroll-into-view="scrollTo"
>
<template v-for="(weeksTimetable, weeksIndex) of parsedCourseList" :key="weeksIndex">
<view
:id="`week${weeksIndex + 1}`" class="py-1 px-2 inline-block"
@click="setCurrentWeekIndex(weeksIndex)"
>
<view
class="rounded-lg py-1 px-2"
:class="originalWeekIndex === weeksIndex ? 'bg-gray-400/50 dark:!bg-op60' : currentWeekIndex === weeksIndex ? 'bg-gray-300 bg-op80 dark:!bg-op20' : ''"
>
<view class="text-xs text-center mb-1">
{{ `${weeksIndex + 1}` }}
</view>
<view class="h-10 w-10" grid="~ flow-col cols-5 rows-6">
<template v-for="(weekWeekTimetable, weekWeekIndex) of weeksTimetable" :key="weekWeekIndex">
<template v-if="weekWeekIndex < 8">
<template v-for="(item, _idx) of weekWeekTimetable" :key="_idx">
<view class="rounded-full mx-auto h-1.5 w-1.5" :class="item ? 'bg-light-blue-500' : 'bg-gray-200'" />
</template>
</template>
</template>
</view>
</view>
</view>
</template>
</scroll-view>
</template>

View File

@@ -0,0 +1,146 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import TimetableAction from './TimetableAction.vue'
import TimetableHeader from './TimetableHeader.vue'
import type { CourseModel } from '~/stores/modules/course'
import { useCourseStore } from '~/stores/modules/course'
import { useAppStore } from '~/stores/modules/app'
withDefaults(defineProps<{ showCourseAction: boolean }>(), {
showCourseAction: false,
})
const emit = defineEmits(['courseItemClick'])
const { customBarHeight, totalWeeks, timeSetting } = storeToRefs(useAppStore())
const { weekCourseList, currentWeekIndex, originalWeekIndex } = storeToRefs(
useCourseStore(),
)
const { hasConflictCourseByMap, setCurrentWeekIndex } = useCourseStore()
// delete a course when course at the same time
const deleteWeekCourse = computed(() => {
const weekCourse = Array.from(weekCourseList.value)
if (weekCourse.length <= 1)
return weekCourse
for (let i = 1; i < weekCourse.length; i++) {
const { start, week } = weekCourse[i]
const { start: prevStart, week: prevWeek } = weekCourse[i - 1]
if (start === prevStart && week === prevWeek) {
weekCourse.splice(i, 1)
i--
}
}
return weekCourse
})
const startX = ref(0)
const startY = ref(0)
const towardsX = ref(0)
const towardsY = ref(0)
function resetTouchStatus() {
startX.value = 0
startY.value = 0
towardsX.value = 0
towardsY.value = 0
}
function handleTouchStart(e: TouchEvent) {
startX.value = e.touches[0].clientX
startY.value = e.touches[0].clientY
}
function handleTouchMove(e: TouchEvent) {
towardsX.value = e.touches[0].clientX - startX.value
towardsY.value = e.touches[0].clientY - startY.value
}
function handleTouchEnd() {
let currentWeekIndexTemp = currentWeekIndex.value
if (towardsX.value === 0 || Math.abs(towardsY.value) > 50)
return
if (towardsX.value > 50) {
if (currentWeekIndexTemp === 0)
return
currentWeekIndexTemp--
}
else if (towardsX.value < -50) {
if (currentWeekIndexTemp === totalWeeks.value - 1)
return
currentWeekIndexTemp++
}
setCurrentWeekIndex(currentWeekIndexTemp)
resetTouchStatus()
}
/**
* get course position
* @param item course item
* @returns css style
*/
function getCoursePosition(item: CourseModel) {
return {
'grid-row': `${item.start} / ${item.start + item.duration}`,
'grid-column': `${item.week + 1} / ${item.week + 1 + 1}`,
}
}
</script>
<template>
<view class="overflow-y-scroll relative bg-base" :style="{ height: `calc(100vh - ${customBarHeight}px)` }">
<view class="w-full top-0 z-10 fixed bg-base" :style="{ 'padding-top': `${customBarHeight}px` }">
<TimetableAction :show-course-action="showCourseAction" />
<TimetableHeader />
</view>
<view
class="min-h-max pb-safe p-1 transition-all z-20 duration-300 bg-base bg-[linear-gradient(-225deg,_#FFFEFF_0%,_#D7FFFE_100%)]"
grid="~ flow-col rows-12 cols-[0.7fr_repeat(7,1fr)] gap-1" :class="showCourseAction ? 'pt-31' : 'pt-11'"
@touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd"
>
<template v-for="item in timeSetting" :key="item.index">
<view class="text-sm min-h-18" flex="~ col" justify-evenly items-center>
<view class="font-medium">
{{ item.index }}
</view>
<view class="px-0.5 text-10px">
{{ item.start }}<br>{{ item.end }}
</view>
</view>
</template>
<template v-for="(courseItem, _courseIndex) of deleteWeekCourse" :key="_courseIndex">
<view
class="rounded-lg p-0.5 relative dark:bg-op40" b="white 2 !op-50" :style="[
getCoursePosition(courseItem),
`background-color:${hasConflictCourseByMap(courseItem)[0].color}`,
]" @click="emit('courseItemClick', courseItem)"
>
<view class="h-full w-full" text="center white xs" flex="~ col" justify-center items-center>
<view class="font-medium break-all">
{{ hasConflictCourseByMap(courseItem)[0].title || '' }}
</view>
<view class="break-all">
@{{ hasConflictCourseByMap(courseItem)[0].teacher || '' }}
</view>
<view class="break-all">
<view class="text-8px i-carbon-location-current" />
{{ hasConflictCourseByMap(courseItem)[0].location || '' }}
</view>
<view
v-if="hasConflictCourseByMap(courseItem).length > 1"
class="rounded h-1 top-1 left-1 right-1 absolute bg-white/80"
/>
</view>
</view>
</template>
</view>
<view
class="bg-primary fixed top-40% z-30 rounded-l-full transition-all duration-300" text="white sm"
p="l-4 y-2 r-2" :class="
originalWeekIndex !== currentWeekIndex ? 'right-0' : '-right-full'
" @click="setCurrentWeekIndex(originalWeekIndex)"
>
返回本周
</view>
</view>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import { useCourseStore, weekTitle } from '~/stores/modules/course'
const {
isStart, currentMonth, originalWeekIndex, currentWeekIndex, currentWeekDayArray,
} = storeToRefs(useCourseStore())
const originalWeekWeekIndex = ref(new Date().getDay() === 0 ? 6 : new Date().getDay() - 1)
const isCurrentWeek = (weekIndex: number) => {
if (!isStart.value)
return false
if (!originalWeekIndex.value || !currentWeekIndex.value || !originalWeekWeekIndex.value)
return false
return originalWeekIndex.value === currentWeekIndex.value && originalWeekWeekIndex.value === weekIndex
}
</script>
<template>
<view class="h-10 shadow-sm px-1 top-0" grid="~ cols-[0.7fr_repeat(7,1fr)] gap-1">
<view class="flex font-medium text-sm items-center justify-center">
{{ `${currentMonth}` }}
</view>
<view
v-for="(item, index) in currentWeekDayArray" :key="index"
class="text-xs transition-all duration-300 !bg-op40"
flex="~ col" justify-evenly items-center
b="y-transparent x-none t-4 b-4"
:class="isCurrentWeek(index) ? 'bg-light-blue-300 !b-b-light-blue-500' : ''"
>
<text class="font-medium">
{{ weekTitle[index] }}
</text>
<text>{{ item }}</text>
</view>
</view>
</template>

View File

@@ -0,0 +1,395 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>FullCalendar H5 Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="./images/favicon.ico" />
<link rel="stylesheet" href="./css/index.css" />
<script src="./js/vue.global.js"></script>
<script src="./js/axios.js"></script>
<script src="./js/element-plus-zh-cn.min.js"></script>
<script src="./js/element-plus-index.full.min.js"></script>
<script src="./js/index.js"></script>
<style>
.fc .fc-toolbar-title {
font-size: 16px;
}
.fc-col-header-cell-cushion {
font-size: 12px;
}
.fc .fc-toolbar.fc-header-toolbar {
margin-bottom: 10px;
}
.fc-timegrid-event .fc-event-main{
font-size: 8px;
}
.el-card__body{
padding: 5px;
}
.el-row {
margin-bottom: 20px;
}
.el-row:last-child {
margin-bottom: 0;
}
.el-col {
border-radius: 4px;
}
.centered-content {
display: flex;
justify-content: center;
align-items: center;
}
.centered-form {
text-align: center;
}
.centered-form-item {
text-align: center;
}
.form-row {
flex-wrap: wrap;
}
.card-item {
margin: 10px 0;
width: 100%;
}
@media (min-width: 768px) {
.form-row {
flex-wrap: nowrap;
}
.card-item {
width: auto;
}
}
</style>
</head>
<body>
<div id="app">
<el-card style="height: 100vh" shadow="never">
<el-card style="margin-top:20px;" shadow="never">
<el-form label-position="left" class="centered-form" label-width="80px">
<el-row
:gutter="20"
justify="space-around"
align="middle"
class="form-row"
>
<el-text type="primary" size="large" style="text-align: center;">教室课表查询</el-text>
<el-col :xs="24" :sm="12" :md="6">
<div class="card-item centered-content">
<el-form-item label="校区" class="centered-form-item">
<el-select
v-model="campusId"
placeholder="请选择需要查询的校区~"
filterable
placement="bottom"
@change="getBuildList"
>
<el-option
v-for="item in campusList"
:key="item"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="card-item centered-content">
<el-form-item label="教学楼" class="centered-form-item">
<el-select
v-model="buildId"
placeholder="请选择需要查询的教学楼~"
filterable
placement="bottom"
@change="getClassroomList"
>
<el-option
v-for="item in buildList"
:key="item"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="card-item centered-content">
<el-form-item label="教室" class="centered-form-item">
<el-select
v-model="classroomId"
placeholder="请选择需要查询的教室~"
filterable
placement="bottom"
@change="getClassroomCourseList"
>
<el-option
v-for="item in classroomList"
:key="item"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
</el-header>
<el-card shadow="never">
<div id="calendar"></div>
</el-card>
</el-container>
</div>
</body>
<script>
const app = Vue.createApp({
data() {
return {
startDate: '',
campusId: '',
campusList: [],
buildId: '',
buildList: [],
classroomId: '',
classroomList: [],
courseList: [],
currentEvents: [],
week: 0,
calendarApi: null,
// Your existing data properties here
}
},
methods: {
initCalendar() {
const calendarEl = document.getElementById('calendar')
this.calendarApi = new FullCalendar.Calendar(calendarEl, {
// 自定义按钮
customButtons: {
preWeekCustom: {
text: '上周',
click: () => this.prevWeek(),
},
nextWeekCustom: {
text: '下周',
click: () => this.nextWeek(),
},
},
/** 修改headerToolbar */
headerToolbar: {
left: 'preWeekCustom',
center: 'title', // 显示日历标题
right: 'nextWeekCustom',
},
dayHeaderFormat: {
weekday: 'short', // 显示周几,如"周日"
month: '2-digit', // 显示两位数的月份
day: '2-digit', // 显示两位数的日期
omitCommas: true, // 去除逗号
},
contentHeight: 500, // 动态计算高度
/** 设置日历高度 */
timeZone: 'Asia/Shanghai',
/** 默认视图 (月:dayGridMonth,周:timeGridWeek,日:timeGridDay) */
initialView: 'timeGridWeek', // 默认显示周视图
firstDay: 1, // 一周的第一天0表示星期天1表示星期一
slotMinTime: '08:00', // 最小时间段08:00
slotMaxTime: '22:10', // 最大时间段22:10
slotDuration: '00:15', // 时间间隔5分钟
slotLabelFormat: {
hour: '2-digit',
minute: '2-digit',
omitZeroMinute: false, // 忽略零分钟
hour12: false, // 24小时制
meridiem: 'short',
},
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit',
hour12: false, // 24小时制
},
eventColor: '#3BB2E3', // 全部日历日程背景色
themeSystem: 'bootstrap', // 主题色(本地测试未能生效)
events: [], // 事件列表
editable: false, // 是否可以拖动修改事件
allDaySlot: false, // 是否显示全天事件区域
nowIndicator: true, // 是否显示当前时间线
selectable: false, // 是否可以选择时间段
selectMirror: false, // 是否在选择时间段时显示虚影
handleWindowResize: true, // 是否在窗口大小变化时调整日历
navLinks: false, // 是否可以通过点击日期导航
fixedWeekCount: true, // 每月显示固定周数
showNonCurrentDates: true, // 是否显示非当前月的日期
dayMaxEvents: true, // 每天最大事件数,超过则显示更多按钮
weekends: true, // 是否显示周末
/** 切换语言 */
locale: 'zh-cn', // 语言设置为简体中文
buttonText: {
today: '今天',
week: '周视图',
day: '日',
list: '周列表',
},
// dateClick: (info) => {
// // 点击日期时跳转到天视图
// this.calendarApi.changeView('listDay', info.dateStr);
// },
eventContent(arg) {
// 自定义事件内容
const eventTitle = arg.event.title
const customHtml = `
<div style="text-align:center;">
<strong>${arg.timeText}</strong><br />
<span>${eventTitle}</span>
</div>
`
return { html: customHtml }
},
eventClick(info) {
let html = `
<div style="text-align:center;">
<div>上课时间: ${info.event.extendedProps.startTime.replace('T', ' ').replace('Z', '')}</div>
<div>下课时间: ${info.event.extendedProps.endTime.replace('T', ' ').replace('Z', '')}</div>
<div>课程名称: ${info.event.extendedProps.course}</div>
<div>上课教师: ${info.event.extendedProps.teacher}</div>
<div>上课教室: ${info.event.extendedProps.classroom}</div>
<b>----------上课班级---------</b>
</div>
`
info.event.extendedProps.classnames.forEach((element) => {
html += `<div>${element}</div>`
})
// 处理点击事件
ElementPlus.ElMessageBox.alert(html, '课程信息', {
confirmButtonText: '确认',
type: 'info',
center: true,
dangerouslyUseHTMLString: true,
})
// 你可以在这里实现更多逻辑,比如显示一个模态框或导航到一个新页面
},
})
this.calendarApi.render()
},
prevWeek() {
this.calendarApi.prev()
this.getClassroomCourseList()
},
nextWeek() {
this.calendarApi.next()
this.getClassroomCourseList()
},
today() {
this.calendarApi.today()
},
getTermSetting() {
axios
.get('http://101.200.126.63:9000/getTermSetting')
.then((response) => {
this.startDate = response.data.data.startDate
localStorage.setItem('termSetting', JSON.stringify(response.data.data))
})
.catch((error) => {
console.error(error)
})
},
getCampusList() {
axios.get('http://101.200.126.63:9000/getCampusList').then((response) => {
if (response.data.code === 200) {
this.campusList = response.data.data
localStorage.setItem('campusSetting', JSON.stringify({ campusId: this.campusId, campusList: response.data.data }))
}
}).catch((error) => {
console.error(error)
})
},
getBuildList() {
if (!this.campusId)
return
axios.get(`http://101.200.126.63:9000/getBuilds/${this.campusId}`).then((response) => {
if (response.data.code === 200) {
this.buildList = response.data.data
localStorage.setItem('buildSetting', JSON.stringify({ campusId: this.campusId, buildList: response.data.data }))
}
}).catch((error) => { console.error(error) })
},
getClassroomList() {
if (!this.campusId || !this.buildId)
return
axios.get(`http://101.200.126.63:9000/getClassroomList/${this.buildId}`).then((response) => {
if (response.data.code === 200) {
this.classroomList = response.data.data
localStorage.setItem('classroomSetting', JSON.stringify({ buildId: this.buildId, classroomList: response.data.data }))
}
}).catch((error) => { console.error(error) })
},
getClassroomCourseList() {
const week = new Date(this.calendarApi.view.activeStart).getTime()
this.week = Math.floor((week - new Date(this.startDate).getTime()) / (1000 * 60 * 60 * 24 * 7))
if (!this.classroomId || !this.week)
return
axios.get('http://101.200.126.63:9000/getClassroomCourses', {
params: { classroom_id: this.classroomId, week: this.week + 1 },
}).then((response) => {
this.calendarApi.removeAllEvents()
if (response.data.code === 200) {
this.currentEvents = response.data.data.map(data => ({
id: data.id,
title: `${data.course} - ${data.teacher}`,
start: data.startTime.replace('T', ' ').replace('Z', ''),
end: data.endTime.replace('T', ' ').replace('Z', ''),
extendedProps: {
...data,
},
}))
localStorage.setItem('currentEventSetting', JSON.stringify({ classroomId: this.classroomId, week: this.week, currentEvents: this.currentEvents }))
}
// 清空现有事件,并添加新的事件源
this.calendarApi.addEventSource(this.currentEvents)
this.calendarApi.render()
})
},
},
mounted() {
if (localStorage.getItem('termSetting'))
this.startDate = JSON.parse(localStorage.getItem('termSetting')).startDate
if (localStorage.getItem('campusSetting')) {
this.campusId = JSON.parse(localStorage.getItem('campusSetting')).campusId
this.campusList = JSON.parse(localStorage.getItem('campusSetting')).campusList
}
if (localStorage.getItem('buildSetting')) {
this.campusId = JSON.parse(localStorage.getItem('buildSetting')).campusId
this.buildList = JSON.parse(localStorage.getItem('buildSetting')).buildList
}
if (localStorage.getItem('classroomSetting')) {
this.buildId = JSON.parse(localStorage.getItem('classroomSetting')).buildId
this.classroomList = JSON.parse(localStorage.getItem('classroomSetting')).classroomList
}
if (localStorage.getItem('currentEventSetting')) {
this.classroomId = JSON.parse(localStorage.getItem('currentEventSetting')).classroomId
this.currentEvents = JSON.parse(localStorage.getItem('currentEventSetting')).currentEvents
this.week = JSON.parse(localStorage.getItem('currentEventSetting')).week
}
this.initCalendar()
this.getTermSetting()
this.getCampusList()
this.calendarApi.addEventSource(this.currentEvents)
this.calendarApi.render()
},
})
// 使用 Element Plus 并指定语言
app.use(ElementPlus, {
locale: ElementPlusLocaleZhCn,
})
app.mount('#app')
</script>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

2990
src/hybrid/html/js/axios.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
/*! Element Plus v2.4.0 */(function(u,e){typeof exports=="object"&&typeof module!="undefined"?module.exports=e():typeof define=="function"&&define.amd?define(e):(u=typeof globalThis!="undefined"?globalThis:u||self,u.ElementPlusLocaleZhCn=e())})(this,function(){"use strict";var u={name:"zh-cn",el:{colorpicker:{confirm:"\u786E\u5B9A",clear:"\u6E05\u7A7A"},datepicker:{now:"\u6B64\u523B",today:"\u4ECA\u5929",cancel:"\u53D6\u6D88",clear:"\u6E05\u7A7A",confirm:"\u786E\u5B9A",selectDate:"\u9009\u62E9\u65E5\u671F",selectTime:"\u9009\u62E9\u65F6\u95F4",startDate:"\u5F00\u59CB\u65E5\u671F",startTime:"\u5F00\u59CB\u65F6\u95F4",endDate:"\u7ED3\u675F\u65E5\u671F",endTime:"\u7ED3\u675F\u65F6\u95F4",prevYear:"\u524D\u4E00\u5E74",nextYear:"\u540E\u4E00\u5E74",prevMonth:"\u4E0A\u4E2A\u6708",nextMonth:"\u4E0B\u4E2A\u6708",year:"\u5E74",month1:"1 \u6708",month2:"2 \u6708",month3:"3 \u6708",month4:"4 \u6708",month5:"5 \u6708",month6:"6 \u6708",month7:"7 \u6708",month8:"8 \u6708",month9:"9 \u6708",month10:"10 \u6708",month11:"11 \u6708",month12:"12 \u6708",weeks:{sun:"\u65E5",mon:"\u4E00",tue:"\u4E8C",wed:"\u4E09",thu:"\u56DB",fri:"\u4E94",sat:"\u516D"},months:{jan:"\u4E00\u6708",feb:"\u4E8C\u6708",mar:"\u4E09\u6708",apr:"\u56DB\u6708",may:"\u4E94\u6708",jun:"\u516D\u6708",jul:"\u4E03\u6708",aug:"\u516B\u6708",sep:"\u4E5D\u6708",oct:"\u5341\u6708",nov:"\u5341\u4E00\u6708",dec:"\u5341\u4E8C\u6708"}},select:{loading:"\u52A0\u8F7D\u4E2D",noMatch:"\u65E0\u5339\u914D\u6570\u636E",noData:"\u65E0\u6570\u636E",placeholder:"\u8BF7\u9009\u62E9"},cascader:{noMatch:"\u65E0\u5339\u914D\u6570\u636E",loading:"\u52A0\u8F7D\u4E2D",placeholder:"\u8BF7\u9009\u62E9",noData:"\u6682\u65E0\u6570\u636E"},pagination:{goto:"\u524D\u5F80",pagesize:"\u6761/\u9875",total:"\u5171 {total} \u6761",pageClassifier:"\u9875",page:"\u9875",prev:"\u4E0A\u4E00\u9875",next:"\u4E0B\u4E00\u9875",currentPage:"\u7B2C {pager} \u9875",prevPages:"\u5411\u524D {pager} \u9875",nextPages:"\u5411\u540E {pager} \u9875",deprecationWarning:"\u4F60\u4F7F\u7528\u4E86\u4E00\u4E9B\u5DF2\u88AB\u5E9F\u5F03\u7684\u7528\u6CD5\uFF0C\u8BF7\u53C2\u8003 el-pagination \u7684\u5B98\u65B9\u6587\u6863"},messagebox:{title:"\u63D0\u793A",confirm:"\u786E\u5B9A",cancel:"\u53D6\u6D88",error:"\u8F93\u5165\u7684\u6570\u636E\u4E0D\u5408\u6CD5!"},upload:{deleteTip:"\u6309 delete \u952E\u53EF\u5220\u9664",delete:"\u5220\u9664",preview:"\u67E5\u770B\u56FE\u7247",continue:"\u7EE7\u7EED\u4E0A\u4F20"},table:{emptyText:"\u6682\u65E0\u6570\u636E",confirmFilter:"\u7B5B\u9009",resetFilter:"\u91CD\u7F6E",clearFilter:"\u5168\u90E8",sumText:"\u5408\u8BA1"},tree:{emptyText:"\u6682\u65E0\u6570\u636E"},transfer:{noMatch:"\u65E0\u5339\u914D\u6570\u636E",noData:"\u65E0\u6570\u636E",titles:["\u5217\u8868 1","\u5217\u8868 2"],filterPlaceholder:"\u8BF7\u8F93\u5165\u641C\u7D22\u5185\u5BB9",noCheckedFormat:"\u5171 {total} \u9879",hasCheckedFormat:"\u5DF2\u9009 {checked}/{total} \u9879"},image:{error:"\u52A0\u8F7D\u5931\u8D25"},pageHeader:{title:"\u8FD4\u56DE"},popconfirm:{confirmButtonText:"\u786E\u5B9A",cancelButtonText:"\u53D6\u6D88"}}};return u});
//# sourceMappingURL=zh-cn.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

19
src/main.ts Normal file
View File

@@ -0,0 +1,19 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
// modules
// 导入 pinia 实例
import pinia from './stores'
import 'uno.css'
export function createApp() {
const app = createSSRApp(App)
// Configure store
// https://pinia.vuejs.org/
// 使用 pinia
app.use(pinia)
return {
app,
}
}

91
src/manifest.json Normal file
View File

@@ -0,0 +1,91 @@
{
"name": "智享通",
"appid": "__UNI__F8BB51F",
"description": "用于查询武昌校区,流芳校区课表,空教室,教室课表",
"versionName": "1.0.2",
"versionCode": 20240912,
"transformPx": false,
"mp-weixin": {
"appid": "wxa38f446ef3aab736",
"setting": {
"urlCheck": false,
"es6": true,
"minified": true,
"postcss": true
},
"usingComponents": true,
"darkmode": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"locale": "auto",
"app-plus": {
"distribute": {
"icons": {
"android": {
"hdpi": "unpackage/res/icons/72x72.png",
"xhdpi": "unpackage/res/icons/96x96.png",
"xxhdpi": "unpackage/res/icons/144x144.png",
"xxxhdpi": "unpackage/res/icons/192x192.png"
},
"ios": {
"appstore": "unpackage/res/icons/1024x1024.png",
"ipad": {
"app": "unpackage/res/icons/76x76.png",
"app@2x": "unpackage/res/icons/152x152.png",
"notification": "unpackage/res/icons/20x20.png",
"notification@2x": "unpackage/res/icons/40x40.png",
"proapp@2x": "unpackage/res/icons/167x167.png",
"settings": "unpackage/res/icons/29x29.png",
"settings@2x": "unpackage/res/icons/58x58.png",
"spotlight": "unpackage/res/icons/40x40.png",
"spotlight@2x": "unpackage/res/icons/80x80.png"
},
"iphone": {
"app@2x": "unpackage/res/icons/120x120.png",
"app@3x": "unpackage/res/icons/180x180.png",
"notification@2x": "unpackage/res/icons/40x40.png",
"notification@3x": "unpackage/res/icons/60x60.png",
"settings@2x": "unpackage/res/icons/58x58.png",
"settings@3x": "unpackage/res/icons/87x87.png",
"spotlight@2x": "unpackage/res/icons/80x80.png",
"spotlight@3x": "unpackage/res/icons/120x120.png"
}
}
},
"ios": {
"dSYMs": false,
"idfa": false
},
"sdkConfigs": {
"ad": {}
},
"splashscreen": {
"useOriginalMsgbox": false
},
"android": {
"minSdkVersion": 21,
"abiFilters": ["armeabi-v7a", "arm64-v8a", "x86"]
}
},
"modules": {
"SQLite": {},
"Webview-x5": {},
"UIWebview": {}
}
},
"h5": {
"router": {
"mode": "hash"
},
"devServer": {
"https": false
}
},
"mp-qq": {
"appid": "1112318877"
},
"fallbackLocale": "zh-Hans"
}

166
src/modules/course.ts Normal file
View File

@@ -0,0 +1,166 @@
import type { GetClassrommsParams } from '~/services/course'
export interface ClassRoom {
_id: string
campus: string
name: string
status: number
}
export interface CourseData {
_id: string
classTime: number
classname_id: string
classroom_id: string
course: string
day: number
endStamp: number
endTime: string
startStamp: number
startTime: string
status: number
teacher: string
week: number
}
export interface CourseInfo {
courseName: string
classroom: string
teacher: string
weeks: number[]
day: number
time: number[]
id: string
}
// 简单的 UUID 生成函数
export const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
export const getCourseData = (courseData: CourseData[], classroomData: ClassRoom[]) => {
// 创建一个 classroom_id 到 classroom_name 的映射
const classroomMap = new Map()
classroomData.forEach((item) => {
classroomMap.set(item._id, item.name)
})
// 第一步:将 courseData 中的 classroom_id 替换为教室名称
const result = courseData.map(item => ({
...item,
classroom: classroomMap.get(item.classroom_id) || '未知教室', // 替换 classroom_id 为教室名称
}))
// 转换为 DataFrame 样式数据结构
const df = result
// 第二步:根据指定字段去重并将 week 字段的值添加到 weeks 字段中
const groupedData = {}
df.forEach((item) => {
const key = `${item.course}_${item.day}_${item.classroom}_${item.classTime}`
if (!groupedData[key]) {
groupedData[key] = {
course: item.course,
classroom: item.classroom,
teacher: item.teacher,
weeks: [item.week],
day: item.day,
classTime: item.classTime,
startTime: item.startTime,
endTime: item.endTime,
startStamp: item.startStamp,
endStamp: item.endStamp,
}
}
else {
groupedData[key].weeks.push(item.week)
}
})
const dfUnique = Object.values(groupedData).map((item: any) => {
item.weeks = [...new Set(item.weeks)] // 去重和排序 weeks
item.weeks.sort()
return item
})
// 第三步:再进一步的合并 classTime 数据
const courseList = dfUnique.map(item => ({
courseName: item.course,
classroom: item.classroom,
teacher: item.teacher,
weeks: item.weeks,
day: item.day,
classTime: item.classTime,
}))
// 将 weeks 转为 tuple 用于分组
const dfGroupedBy = {}
courseList.forEach((item) => {
const key = `${item.courseName}_${item.classroom}_${item.weeks.join(',')}_${
item.day
}`
if (!dfGroupedBy[key]) {
dfGroupedBy[key] = {
courseName: item.courseName,
classroom: item.classroom,
teacher: item.teacher,
weeks: item.weeks,
day: item.day,
time: [item.classTime],
}
}
else {
dfGroupedBy[key].time.push(item.classTime)
}
})
const resultList: CourseInfo[] = Object.values(dfGroupedBy).map((item: any) => {
const classTimeList = [
[1, 2],
[3, 4],
[5, 5],
[6, 7],
[8, 9],
[10, 11, 12],
]
const timeRange = item.time.reduce((acc, cur) => {
acc.push(...classTimeList[cur])
return acc
}, [])
item.weeks.sort((a, b) => a - b)
timeRange.sort((a, b) => a - b)
return {
...item,
time: [timeRange[0], timeRange[timeRange.length - 1]],
day: item.day + 1,
id: generateUUID(), // 生成唯一 ID
}
})
return resultList
}
export const getEmptyClassroomList = (
params: GetClassrommsParams,
courseData: CourseData[],
classroomData: ClassRoom[],
) => {
// 创建一个 classroom_id 到 classroom_name 的映射
const classroomMap = new Map()
const campusMap = new Map()
classroomData.forEach((item) => {
classroomMap.set(item._id, item.name)
campusMap.set(item._id, item.campus)
})
// 第一步:将 courseData 中的 classroom_id 替换为教室名称
const result = courseData.map(item => ({
...item,
classroom: classroomMap.get(item.classroom_id) || '', // 替换 classroom_id 为教室名称
campus: campusMap.get(item.classroom_id) || '',
}))
result.forEach((course) => {
classroomData = classroomData.filter(item => item.campus.includes(params.campus as string)).filter(item => item.name !== course.classroom)
})
const classroom: string[] = []
classroomData.forEach((course) => {
classroom.push(course.name)
})
return classroom
}

85
src/modules/http.ts Normal file
View File

@@ -0,0 +1,85 @@
/**
* 添加拦截器:
* 拦截 request 请求
* 拦截 uploadFile 文件上传
*
* TODO:
* 1. 非 http 开头需拼接地址
* 2. 请求超时
* 3. 添加小程序端请求头标识
* 4. 添加 token 请求头标识
*/
const baseURL = 'http://101.200.126.63:9000'
// const baseURL = 'http://192.168.1.198:8080'
// 添加拦截器
const httpInterceptor = {
// 拦截前触发
invoke(options: UniApp.RequestOptions) {
// 1. 非 http 开头需拼接地址
if (!options.url.startsWith('http'))
options.url = baseURL + options.url
// 2. 请求超时, 默认 60s
options.timeout = 60000
// 3. 添加小程序端请求头标识
options.header = {
...options.header,
}
},
}
uni.addInterceptor('request', httpInterceptor)
uni.addInterceptor('uploadFile', httpInterceptor)
/**
* 请求函数
* @param UniApp.RequestOptions
* @returns Promise
* 1. 返回 Promise 对象
* 2. 获取数据成功
* 2.1 提取核心数据 res.data
* 2.2 添加类型,支持泛型
* 3. 获取数据失败
* 3.1 401错误 -> 清理用户信息,跳转到登录页
* 3.2 其他错误 -> 根据后端错误信息轻提示
* 3.3 网络错误 -> 提示用户换网络
*/
interface Data<T> {
code: number
msg: string
data: T
}
// 2.2 添加类型,支持泛型
export const http = <T>(options: UniApp.RequestOptions) => {
// 1. 返回 Promise 对象
return new Promise<Data<T>>((resolve, reject) => {
uni.request({
...options,
// 响应成功
success(res) {
// 状态码 2xx axios 就是这样设计的
if (res.statusCode >= 200 && res.statusCode < 300) {
// 2.1 提取核心数据 res.data
resolve(res.data as Data<T>)
}
else {
// 其他错误 -> 根据后端错误信息轻提示
uni.showToast({
icon: 'none',
title: (res.data as Data<T>).msg || '请求错误',
})
reject(res)
}
},
// 响应失败
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误,换个网络试试',
})
reject(err)
},
})
})
}

108
src/pages.json Normal file
View File

@@ -0,0 +1,108 @@
{
"globalStyle": {
"backgroundColor": "@bgColor",
"backgroundColorBottom": "@bgColorBottom",
"backgroundColorTop": "@bgColorTop",
"backgroundTextStyle": "@bgTxtStyle",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "@navTxtStyle",
"navigationBarTitleText": "ColorTimetable",
"navigationStyle": "custom"
},
"pages": [{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
}
},
{
"path": "pages/course/detail",
"style": {
"navigationBarTitleText": "课程详情"
}
},
{
"path": "pages/course/course",
"style": {
"navigationBarTitleText": "课程表"
}
},
{
"path": "pages/classroom/course",
"style": {
"navigationBarTitleText": "教室课表"
}
},
{
"path": "pages/classroom/classroom",
"style": {
"navigationBarTitleText": "空闲教室"
}
},
{
"path": "pages/classroom/detail",
"style": {
"navigationBarTitleText": "空闲教室详情"
}
},
{
"path": "pages/setting/setting",
"style": {
"navigationBarTitleText": "设置"
}
}
],
"tabBar": {
"color": "#333",
"selectedColor": "#1296db",
"backgroundColor": "#fff",
"borderStyle": "white",
"list": [{
"iconPath": "static/tabs/home_default.png",
"selectedIconPath": "static/tabs/home_selected.png",
"pagePath": "pages/index/index",
"text": "首页"
},
{
"iconPath": "static/tabs/course_default.png",
"selectedIconPath": "static/tabs/course_selected.png",
"pagePath": "pages/course/course",
"text": "课程表"
},
{
"iconPath": "/static/tabs/classroom_default.png",
"pagePath": "pages/classroom/classroom",
"selectedIconPath": "/static/tabs/classroom_selected.png",
"text": "空教室"
},
{
"iconPath": "/static/tabs/classroom_course_default.png",
"pagePath": "pages/classroom/course",
"selectedIconPath": "/static/tabs/classroom_course_selected.png",
"text": "教室课表"
},
{
"iconPath": "static/tabs/setting_default.png",
"selectedIconPath": "static/tabs/setting_selected.png",
"pagePath": "pages/setting/setting",
"text": "设置"
}
]
},
"easycom": {
"autoscan": true,
"custom": {
"^U(.*)": "~/components/UnoUI/U$1/U$1.vue",
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"condition": { //模式配置,仅开发期间生效
"current": 0, //当前激活的模式(list 的索引项)
"list": [{
"name": "教室课表", //模式名称
"path": "pages/classroom/course", //启动页面,必选
"query": "" //启动参数在页面的onLoad函数里面得到
}]
}
}

View File

@@ -0,0 +1,514 @@
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { usePageStore } from '~/stores'
import { useAppStore } from '~/stores/modules/app'
import cusSelects from '~/components/cus-selects-fan.vue'
import UBasePage from '~/components/UnoUI/UBasePage/UBasePage.vue'
import { type BuildInfo, getBuildListAPI } from '~/services/course'
const { timeSetting, startDate, endDate, campusList, campusId } = storeToRefs(
useAppStore(),
)
const { setPageConfig } = usePageStore()
const { getEmptyClassroom } = useAppStore()
const dateStr = ref<string>('')
const selectDate = ref<string | Date>(new Date())
const timeRange = ref<number[]>([1, 2])
const campus = ref<string>('')
const classroom = ref<string>('')
const classroomList = ref<BuildInfo[]>([
{
name: '全部',
id: '',
campus: '',
campus_id: '',
},
])
const getBuilds = async () => {
uni.showLoading({ title: '加载中~' })
const res = await getBuildListAPI(campus.value)
if (res.code === 200) {
classroomList.value = res.data
classroomList.value.push(
{
name: '全部',
id: '',
campus: '',
campus_id: '',
},
)
classroom.value = ''
uni.hideLoading()
uni.showToast({ icon: 'success', title: '加载成功!' })
}
else {
uni.hideLoading()
}
}
const onCampusSelectChange = async (e) => {
campus.value = e
await getBuilds()
}
const onClassroomSelectChange = async (e) => {
classroom.value = e
}
/** 起始节数类别 -2未知 -1全天 0上午 1下午 2晚上 */
const timeRangeType = ref<number>(-1)
/** 日历组件 */
const calendarRef = ref()
const timeList = [
[
'开始节次',
'第1节',
'第2节',
'第3节',
'第4节',
'第5节',
'第6节',
'第7节',
'第8节',
'第9节',
'第10节',
'第11节',
'第12节',
],
[
'持续节次',
'持续1节',
'持续2节',
'持续3节',
'持续4节',
'持续5节',
'持续6节',
'持续7节',
'持续8节',
'持续9节',
'持续10节',
'持续11节',
'持续12节',
],
]
const showTimeActionSheet = ref(false)
const timeValue = ref([1, 1])
/** 设置起始时间段 */
const setTimeRange = (e: number) => {
const all = timeSetting.value.map(item => item.index).sort((a, b) => a - b)
const morinig = timeSetting.value
.filter(item => item.time === 0)
.map(item => item.index)
.sort((a, b) => a - b)
const afternoon = timeSetting.value
.filter(item => item.time === 1)
.map(item => item.index)
.sort((a, b) => a - b)
const night = timeSetting.value
.filter(item => item.time === 2)
.map(item => item.index)
.sort((a, b) => a - b)
switch (e) {
case 0:
timeRange.value = [morinig[0], morinig.slice(-1)[0]]
timeRangeType.value = 0
return
case 1:
timeRange.value = [afternoon[0], afternoon.slice(-1)[0]]
timeRangeType.value = 1
return
case 2:
timeRange.value = [night[0], night.slice(-1)[0]]
timeRangeType.value = 2
return
case 3:
timeRange.value = [all[0], all.slice(-1)[0]]
timeRangeType.value = 3
return
default:
timeRangeType.value = -1
}
}
/** 获取使用时间 */
const getUseTime = (nowDate = new Date()) => {
let year: string | number = nowDate.getFullYear()
year = year.toString().slice(-2)
let month: string | number = nowDate.getMonth() + 1
if (month < 10)
month = `0${month}`
let date: string | number = nowDate.getDate()
if (date < 10)
date = `0${date}`
selectDate.value = `${nowDate.getFullYear()}-${month}-${date}`
const day = nowDate.getDay()
const dayList = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const time = nowDate.getTime() - new Date(startDate.value).getTime()
let nowWeek = time / 1000 / 60 / 60 / 24 / 7
if (nowWeek < 0) {
dateStr.value = `未开学-${dayList[day]}(${year}/${month}/${date})`
return
}
if (nowDate > new Date(endDate.value)) {
dateStr.value = `学期已结束-${dayList[day]}(${year}/${month}/${date})`
return
}
nowWeek = Math.floor(nowWeek) + 1
dateStr.value = `${nowWeek}周-${dayList[day]}(${year}/${month}/${date})`
}
/** 启动日历 */
const onOpenCalender = () => {
calendarRef.value.open()
}
/** 日期变动 */
const onCalenderConfirm = (e: any) => {
selectDate.value = e.fulldate
getUseTime(new Date(e.fulldate))
}
function handleConfirmTimeActionSheet() {
showTimeActionSheet.value = false
}
function handleTimeChange(e: any) {
const value = e.detail.value
const start = value[0] ? value[0] : 1
const duration = value[1] ? value[1] : 1
timeValue.value = [start, duration]
const all = timeSetting.value.map(item => item.index).sort((a, b) => a - b)
const morinig = timeSetting.value
.filter(item => item.time === 0)
.map(item => item.index)
.sort((a, b) => a - b)
const afternoon = timeSetting.value
.filter(item => item.time === 1)
.map(item => item.index)
.sort((a, b) => a - b)
const night = timeSetting.value
.filter(item => item.time === 2)
.map(item => item.index)
.sort((a, b) => a - b)
if (start + duration <= timeSetting.value.length)
timeRange.value = [start, start + duration]
else timeRange.value = [start, start]
if (
timeRange.value[0] === morinig[0]
&& timeRange.value[1] === morinig.slice(-1)[0]
)
timeRangeType.value = 0
else if (
timeRange.value[0] === afternoon[0]
&& timeRange.value[1] === afternoon.slice(-1)[0]
)
timeRangeType.value = 1
else if (
timeRange.value[0] === night[0]
&& timeRange.value[1] === night.slice(-1)[0]
)
timeRangeType.value = 2
else if (
timeRange.value[0] === all[0]
&& timeRange.value[1] === all.slice(-1)[0]
)
timeRangeType.value = 3
else timeRangeType.value = -1
}
/** 查询空教室 */
const queryClassroom = async () => {
const startTime
= new Date(
`${selectDate.value}T${
timeSetting.value[timeRange.value[0] - 1].start
}:00+08:00`,
).getTime() / 1000
const endTime
= new Date(
`${selectDate.value}T${
timeSetting.value[timeRange.value[1] - 1].end
}:00+08:00`,
).getTime() / 1000
const flag = await getEmptyClassroom(
{ campus_id: campus.value, build_id: classroom.value, startStamp: startTime, endStamp: endTime },
dateStr.value,
timeRange.value,
)
if (flag) {
uni.navigateTo({
url: '/pages/classroom/detail',
})
}
}
/** 页面挂载 */
onLoad(async () => {
getUseTime()
campus.value = campusId.value
if (campus.value)
await getBuilds()
})
onShow(() => {
setPageConfig({ showNavBar: false, pageTitle: '空教室查询' })
})
</script>
<template>
<UBasePage>
<view class="title">
<text>空闲教室查询</text>
</view>
<view class="container">
<view class="query-form">
<view class="query-form-item" @click="onOpenCalender">
<view class="query-form-item-left">
<uni-icons type="calendar" color="#A0E2AA" size="24" />
<text>使用时间</text>
</view>
<view class="query-form-item-right">
{{ dateStr }}
</view>
</view>
<view class="query-form-item" @click="showTimeActionSheet = true">
<view class="query-form-item-left">
<uni-icons type="settings" color="#C8CD9A" size="24" />
<text>起始节数</text>
</view>
<view class="query-form-item-right">
{{ timeRange[0] }}-{{ timeRange[1] }}
</view>
</view>
<view class="query-form-item">
<view class="query-form-item-left">
<uni-icons type="map" color="#F9BD56" size="24" />
<text>选择校区</text>
</view>
<view>
<cus-selects
:value="campus"
filterable
:data="campusList"
:value-type="{ label: 'name', value: 'id' }"
placeholder="请选择校区~"
style="width: 100%"
@change="onCampusSelectChange"
/>
</view>
</view>
<view class="query-form-item">
<view class="query-form-item-left">
<uni-icons type="location" color="#FEC2BF" size="24" />
<text>教室位置</text>
</view>
<view>
<cus-selects
:value="classroom"
filterable
:data="classroomList"
:value-type="{ label: 'name', value: 'id' }"
placeholder="请选择教室~"
style="width: 100%"
@change="onClassroomSelectChange"
/>
</view>
</view>
</view>
<!-- 查询范围 -->
<view class="query-time-range">
<text
:class="timeRangeType === 0 ? 'active' : ''"
@click="setTimeRange(0)"
>
上午
</text>
<text
:class="timeRangeType === 1 ? 'active' : ''"
@click="setTimeRange(1)"
>
下午
</text>
<text
:class="timeRangeType === 2 ? 'active' : ''"
@click="setTimeRange(2)"
>
晚上
</text>
<text
:class="timeRangeType === 3 ? 'active' : ''"
@click="setTimeRange(3)"
>
全天
</text>
</view>
</view>
<view
class="qurry-button"
bind:tap="queryClassroom"
@click="queryClassroom"
>
查询
</view>
</UBasePage>
<uni-calendar
ref="calendarRef"
:date="selectDate"
:insert="false"
:start-date="startDate"
:end-date="endDate"
@confirm="onCalenderConfirm"
/>
<!-- time action sheet -->
<view @touchmove.prevent>
<view
class="bg-white w-full min-h-10 transition-all ease-in-out z-100 duration-300 fixed dark:bg-#121212"
:class="showTimeActionSheet ? 'bottom-0' : '-bottom-full'"
>
<view class="flex flex-col py-6 gap-6">
<view
class="flex font-medium text-xl px-4 justify-between items-center"
>
选择上课时间
<view
class="font-normal text-base text-green-500"
@click="handleConfirmTimeActionSheet"
>
确定
</view>
</view>
<picker-view class="h-40" :value="timeValue" @change="handleTimeChange">
<picker-view-column>
<view
v-for="(item, index) in timeList[0]"
:key="index"
class="flex justify-center items-center"
>
{{ item }}
</view>
</picker-view-column>
<picker-view-column>
<view
v-for="(item, index) in timeList[1]"
:key="index"
class="flex justify-center items-center"
>
{{ item }}
</view>
</picker-view-column>
</picker-view>
</view>
<view
class="flex pb-safe border-t-4 border-gray-200 h-12 text-lg justify-center items-center dark:border-opacity-20"
hover-class="bg-gray-200 bg-opacity-50"
:hover-stay-time="150"
@click="showTimeActionSheet = false"
>
关闭
</view>
</view>
<view
class="bg-dark-100 bg-opacity-50 transition-all top-0 right-0 bottom-0 left-0 z-90 fixed"
:class="
showTimeActionSheet ? 'opacity-100 visible' : 'opacity-0 invisible'
"
@click="showTimeActionSheet = false"
/>
</view>
</template>
<style lang="scss">
page {
background: #f1f2f4;
}
.title {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
width: 45%;
height: 10%;
background: #fff;
top: 8%;
left: 32%;
z-index: 1;
border-radius: 50rpx;
transform: translate(-8%, -0%);
box-shadow: 0rpx 4rpx 8rpx rgba(0, 0, 0, 0.1);
text {
font-size: 36rpx;
font-weight: bold;
color: #297fe4;
}
}
.container {
position: fixed;
top: 25%;
left: 50%;
transform: translate(-50%, -20%);
/* border: solid red 2rpx; */
background: #fff;
width: 80%;
height: 60%;
border-radius: 20rpx;
box-shadow: 0rpx 4rpx 8rpx rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
justify-content: center;
.query-form {
padding: 30rpx;
.query-form-item {
border-bottom: 2rpx solid #e9e9e9;
padding: 20rpx 0;
margin-bottom: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
.query-form-item-left {
width: 40%;
text {
margin-left: 5rpx;
color: #29a1f7;
}
}
}
}
.query-time-range {
display: flex;
justify-content: space-around;
align-items: center;
padding: 30rpx;
text {
background: #69b8f0;
width: 130rpx;
height: 90rpx;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
border-radius: 40rpx;
}
.active {
background-color: orange;
}
}
}
.qurry-button {
position: fixed;
width: 30%;
height: 5%;
top: 75%;
left: 45%;
transform: translate(-35%, -15%);
background: #29a1f7;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 36rpx;
border-radius: 30rpx;
}
</style>

View File

@@ -0,0 +1,310 @@
<script setup lang="ts">
import { onShow } from '@dcloudio/uni-app'
import FullCalendar from '@fullcalendar/vue3'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'
import listPlugin from '@fullcalendar/list'
import dayjs from 'dayjs'
import { onMounted, ref } from 'vue'
import type {
CalendarOptions,
} from '@fullcalendar/core'
import cusSelects from '~/components/cus-selects-fan.vue'
import { usePageStore } from '~/stores/modules/page'
import { useAppStore } from '~/stores/modules/app'
import { type BuildInfo, type ClassroomCourseInfo, type ClassroomInfo, getBuildListAPI, getClassroomCourseListAPI, getClassroomListAPI } from '~/services/course'
const { startDate, campusList, campusId, buildId, classroomId } = storeToRefs(useAppStore())
const { setPageConfig } = usePageStore()
onShow(() => {
setPageConfig({
showNavBar: true,
pageTitle: '教室课表',
showBackAction: true,
})
})
// #ifdef H5
const campus = ref('')
const buildList = ref<BuildInfo[]>([])
const classroomList = ref<ClassroomInfo[]>([])
const getClassroomList = async () => {
if (buildId.value) {
uni.showLoading({ title: '加载中~' })
const res = await getClassroomListAPI(buildId.value)
if (res.code === 200)
classroomList.value = res.data
uni.hideLoading()
uni.showToast({ icon: res.code === 200 ? 'success' : 'error', title: res.msg })
}
}
const getBuildList = async () => {
if (campusId.value) {
uni.showLoading({ title: '加载中~' })
const res = await getBuildListAPI(campus.value)
if (res.code === 200)
buildList.value = res.data
uni.hideLoading()
uni.showToast({ icon: res.code === 200 ? 'success' : 'error', title: res.msg })
}
}
const onCampusSelectChange = async (e: string) => {
campus.value = e
await getBuildList()
}
const onBuildSelectChange = async (e: string) => {
buildId.value = e
await getClassroomList()
}
/** 当前事件列表 */
const currentEvents = ref([])
/** 日历对象 */
const fullCalendar = ref()
const courseList = ref<ClassroomCourseInfo[]>([])
const getClassroomCourses = async () => {
const startTime = dayjs(startDate.value).unix()
// const start = dayjs(fullCalendar.value.getApi().view.activeStart).unix()
const endTime = dayjs(fullCalendar.value.getApi().view.activeEnd).unix()
const week = Math.floor((endTime - startTime) / (60 * 60 * 24 * 7))
if (classroomId.value) {
const res = await getClassroomCourseListAPI(classroomId.value, week)
if (res.code === 200) {
courseList.value = res.data
if (res.data) {
fullCalendar.value.getApi().removeAllEvents() // 清除所有事件
currentEvents.value = courseList.value.map(data => ({
id: data.id,
title: `${data.course} - ${data.teacher}`,
start: data.startTime.replace('T', ' ').replace('Z', ''),
end: data.endTime.replace('T', ' ').replace('Z', ''),
extendedProps: {
classnames: data.classnames,
campus: data.campus,
build: data.build,
},
}))
fullCalendar.value.getApi().addEventSource(currentEvents.value) // 添加新的事件源
}
}
fullCalendar.value.getApi().render() // 手动刷新日历
}
}
const onClassroomSelectChange = async (e: string) => {
classroomId.value = e
await getClassroomCourses()
}
/** 自定义上周按钮点击事件 */
const preWeekCustomClick = async () => {
fullCalendar.value.getApi().prev() // 切换到上一周
await getClassroomCourses()
}
/** 自定义下周按钮点击事件 */
const nextWeekCustomClick = async () => {
fullCalendar.value.getApi().next()
await getClassroomCourses()
}
/** 日历组件属性 */
const calendarOptions = ref<CalendarOptions>({
// 插件
plugins: [
listPlugin, // 列表视图插件
timeGridPlugin, // 周视图和日视图插件
interactionPlugin, // 交互插件,支持点击事件
],
// 自定义按钮
customButtons: {
preWeekCustom: {
text: '上周',
click() {
preWeekCustomClick() // 点击调用 preWeekCustomClick 函数
},
},
nextWeekCustom: {
text: '下周',
click() {
nextWeekCustomClick() // 点击调用 nextWeekCustomClick 函数
},
},
},
/** 修改headerToolbar */
headerToolbar: {
left: 'preWeekCustom',
center: 'title', // 显示日历标题
right: 'nextWeekCustom',
},
dayHeaderFormat: {
weekday: 'short', // 显示周几,如"周日"
month: '2-digit', // 显示两位数的月份
day: '2-digit', // 显示两位数的日期
omitCommas: true, // 去除逗号
},
/** 设置日历高度 */
contentHeight: uni.getWindowInfo().windowHeight - 260, // 动态计算高度
timeZone: 'Asia/Shanghai',
/** 默认视图 (月:dayGridMonth,周:timeGridWeek,日:timeGridDay) */
initialView: 'timeGridWeek', // 默认显示周视图
firstDay: 1, // 一周的第一天0表示星期天1表示星期一
slotMinTime: '08:00', // 最小时间段08:00
slotMaxTime: '22:10', // 最大时间段22:10
slotDuration: '00:05', // 时间间隔5分钟
slotLabelFormat: {
hour: '2-digit',
minute: '2-digit',
omitZeroMinute: false, // 忽略零分钟
hour12: false, // 24小时制
meridiem: 'short',
},
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit',
hour12: false, // 24小时制
},
eventColor: '#3BB2E3', // 全部日历日程背景色
themeSystem: 'bootstrap', // 主题色(本地测试未能生效)
events: currentEvents.value, // 事件列表
editable: false, // 是否可以拖动修改事件
allDaySlot: false, // 是否显示全天事件区域
nowIndicator: true, // 是否显示当前时间线
selectable: true, // 是否可以选择时间段
selectMirror: true, // 是否在选择时间段时显示虚影
handleWindowResize: true, // 是否在窗口大小变化时调整日历
navLinks: false, // 是否可以通过点击日期导航
fixedWeekCount: true, // 每月显示固定周数
showNonCurrentDates: true, // 是否显示非当前月的日期
dayMaxEvents: true, // 每天最大事件数,超过则显示更多按钮
weekends: true, // 是否显示周末
/** 切换语言 */
locale: 'zh-cn', // 语言设置为简体中文
buttonText: {
today: '今天',
week: '周视图',
day: '日',
list: '周列表',
},
})
onMounted(async () => {
campus.value = campusId.value
await getBuildList()
await getClassroomList()
await getClassroomCourses()
fullCalendar.value.getApi().render()
})
// #endif
</script>
<template>
<!-- #ifdef H5 -->
<UBasePage>
<view class="query-select">
<view class="query-select-item">
<view class="query-select-item-left">
<uni-icons type="map" color="#A0E2AA" size="24" />
<text>校区</text>
</view>
<view>
<cus-selects
:value="campus"
filterable
:data="campusList"
:value-type="{ label: 'name', value: 'id' }"
placeholder="请选择校区~"
style="width: 100%"
@change="onCampusSelectChange"
/>
</view>
</view>
<view class="query-select-item">
<view class="query-select-item-left">
<uni-icons type="home" color="#F9BD56" size="24" />
<text>教学楼</text>
</view>
<view>
<cus-selects
:value="buildId"
filterable
:data="buildList"
:value-type="{ label: 'name', value: 'id' }"
placeholder="请选择教学楼~"
style="width: 100%"
@change="onBuildSelectChange"
/>
</view>
</view>
<view class="query-select-item">
<view class="query-select-item-left">
<uni-icons type="location" color="#FEC2BF" size="24" />
<text>教室</text>
</view>
<view>
<cus-selects
:value="classroomId"
filterable
:data="classroomList"
:value-type="{ label: 'name', value: 'id' }"
placeholder="请选择教室~"
style="width: 100%"
@change="onClassroomSelectChange"
/>
</view>
</view>
</view>
<FullCalendar ref="fullCalendar" :options="calendarOptions">
<template #eventContent="arg">
<view class="calender-content">
{{ arg.timeText }}
</view>
<view class="calender-content">
{{ arg.event.title }}
</view>
<view class="calender-content">
上课班级数量: {{ arg.event.extendedProps.classnames.length }}
</view>
</template>
</FullCalendar>
</UBasePage>
<!-- #endif -->
<!-- #ifndef H5 -->
<web-view src="/hybrid/html/calender.html" :fullscreen="false" style="width: 100%; height: 500px;" />
<!-- #endif -->
</template>
<style lang="scss">
.query-select{
padding: 10rpx 20rpx;
background-image: linear-gradient(-225deg, #FFFEFF 0%, #D7FFFE 100%);
.query-select-item{
border-bottom: 2rpx solid #e9e9e9;
padding: 10rpx 0;
display: flex;
justify-content: space-between;
align-items: center;
.query-select-item-left {
width: 30%;
text {
margin-left: 5rpx;
color: #29a1f7;
}
}
}
}
.calender-content{
text-align: center;
}
:deep(.fc .fc-toolbar-title){
font-size: 30rpx;
}
:deep(.fc){
font-size: 26rpx;
}
:deep(.fc .fc-toolbar.fc-header-toolbar){
margin-bottom: 10rpx;
}
</style>

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
import { onShow } from '@dcloudio/uni-app'
import { useAppStore } from '~/stores/modules/app'
import { usePageStore } from '~/stores/modules/page'
const { emptyClassroom } = storeToRefs(useAppStore())
const { setPageConfig } = usePageStore()
onShow(() => {
setPageConfig({
showNavBar: true,
pageTitle: '空闲教室',
showBackAction: true,
})
})
</script>
<template>
<UBasePage>
<view class="tilte-content">
<view class="title-content-left">
<text>查询结果:</text><text style="margin-left: 20rpx">
{{ emptyClassroom.data.length }}
</text>
</view>
<view class="title-content-right">
<text>{{ emptyClassroom.date }}</text>
</view>
</view>
<view
v-for="(item, index) in emptyClassroom.data"
:key="index"
class="empty-content-item"
>
<view class="empty-content-item-left">
{{ item.campus }}
</view>
<view class="empty-content-item-right">
<view class="empty-content-item-right-item">
<uni-icons type="map" color="#F9BD56" size="24" />
<text>{{ item.build }}</text>
</view>
<view class="empty-content-item-right-item">
<uni-icons type="location" color="#FEC2BF" size="24" />
<text>{{ item.name }}</text>
</view>
<view class="empty-content-item-right-item">
<uni-icons type="settings" color="#C8CD9A" size="24" />
<text>
{{ emptyClassroom.timeRange[0] }}-{{
emptyClassroom.timeRange[1]
}}
</text>
</view>
</view>
</view>
</UBasePage>
</template>
<style lang="scss">
page {
background: #f5f7f8;
}
.tilte-content {
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-content-left {
width: 220rpx;
height: 80rpx;
background: white;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 30rpx;
color: #2095f2;
font-weight: bold;
border-radius: 30rpx;
}
.title-content-right {
width: 400rpx;
height: 80rpx;
background: white;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 30rpx;
color: #2095f2;
font-weight: bold;
border-radius: 30rpx;
}
.empty-content-item {
background: white;
margin: 30rpx;
border-radius: 30rpx;
height: 200rpx;
box-shadow: 0px 0px 24rpx -4rpx rgba(0, 0, 0, 0.1);
display: flex;
justify-content: flex-start;
align-items: center;
}
.empty-content-item-left {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100rpx;
height: 100%;
font-size: 40rpx;
background-color: #2095f2;
color: #fff;
text-align: center;
border-top-left-radius: 30rpx;
border-bottom-left-radius: 30rpx;
writing-mode: vertical-rl;
}
.empty-content-item-right {
display: flex;
flex-direction: column;
justify-content: space-around;
margin-left: 20rpx;
width: 80%;
height: 100%;
}
.empty-content-item-right-item {
display: flex;
justify-content: flex-start;
align-items: center;
}
.empty-content-item-right-item text {
margin-left: 5rpx;
color: #2095f2;
}
</style>

105
src/pages/course/course.vue Normal file
View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { CourseModel } from '~/stores/modules/course'
import CourseActionSheet from '~/components/timetable/CourseActionSheet.vue'
import TimetableContent from '~/components/timetable/TimetableContent.vue'
import UBasePage from '~/components/UnoUI/UBasePage/UBasePage.vue'
import { useAppStore } from '~/stores/modules/app'
import { usePageStore } from '~/stores/modules/page'
import { useCourseStore } from '~/stores/modules/course'
const { customBarHeight, statusBarHeight, classinfo } = storeToRefs(useAppStore())
const { setPageConfig } = usePageStore()
const { currentWeekIndex, isStart } = storeToRefs(useCourseStore())
const {
getCourseList,
setStartDay,
} = useCourseStore()
onShow(() => {
setPageConfig({ showNavBar: false })
})
setStartDay()
const showCourseAction = ref(false)
function handleCreateCourse() {
uni.navigateTo({
url: '/pages/course/detail',
})
}
// handle course item click
const showActionSheet = ref(false)
const clickedCourseItem = ref<CourseModel>()
function handleShowActionSheet(courseItem: CourseModel) {
showActionSheet.value = true
clickedCourseItem.value = courseItem
}
function handleCloseActionSheet() {
showActionSheet.value = false
}
const onFabClick = async () => {
uni.showModal({
cancelText: '取消',
confirmText: '确认',
title: '刷新课表?',
content: '是否在线刷新课表,确认后将会覆盖课表',
success: async () => {
if (classinfo.value) {
await getCourseList(classinfo.value.value)
}
else {
uni.showToast({
icon: 'error',
title: '请先配置班级~',
})
}
},
})
}
const formattedWeek = computed(() => {
return `${currentWeekIndex.value + 1}${!isStart.value ? '(未开学)' : ''}`
})
</script>
<template>
<UBasePage>
<view
class="bg-primary text-white w-full top-0 z-200 fixed font-bold"
:style="{ height: `${customBarHeight}px` }"
>
<view
:style="{
'padding-top': `${statusBarHeight}px`,
'height': `${customBarHeight - statusBarHeight}px`,
}"
>
<view class="h-full text-center px-6 relative">
<view class="h-full text-xl left-4 i-carbon-add absolute" @click="handleCreateCourse" />
<view
class="flex h-full mx-auto justify-center items-center inline-block text-lg"
@click="showCourseAction = !showCourseAction"
>
{{ formattedWeek }}
<view
class="transition-transform duration-300 i-carbon-chevron-up"
:class="showCourseAction ? 'rotate-180' : 'rotate-0'"
/>
</view>
</view>
</view>
</view>
<!-- timetable main content -->
<TimetableContent :show-course-action="showCourseAction" @course-item-click="handleShowActionSheet" />
<!-- course card -->
<CourseActionSheet
:show-action-sheet="showActionSheet" :course-item="clickedCourseItem"
@cancel="handleCloseActionSheet"
/>
</UBasePage>
<uni-fab horizontal="right" :pattern="{ icon: 'loop' }" @fab-click="onFabClick" />
</template>

333
src/pages/course/detail.vue Normal file
View File

@@ -0,0 +1,333 @@
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { cloneDeep } from 'lodash-es'
import type { CourseModel } from '~/stores/modules/course'
import { usePageStore } from '~/stores/modules/page'
import { useCourseStore } from '~/stores/modules/course'
import { generateUUID } from '~/modules/course'
const { setPageConfig } = usePageStore()
const defaultCourse: CourseModel = {
id: generateUUID(),
title: '',
location: '课程地点',
teacher: '',
week: 1,
weeks: [1, 2, 3, 4, 5],
start: 1,
duration: 2,
}
const courseStore = useCourseStore()
const courseList = ref<CourseModel[]>([])
const originalCourseId = ref('')
const courseTitle = ref('')
const courseTeacher = ref('')
const isUpdate = ref(false)
onLoad((option: any) => {
isUpdate.value = !!option?.id
if (isUpdate.value) {
const courseListTemp = courseStore.courseList.filter(item => item.id === option?.id)
for (const courseItem of courseListTemp)
courseList.value.push(cloneDeep(courseItem))
originalCourseId.value = option?.id
courseTitle.value = courseListTemp[0].title || ''
courseTeacher.value = courseListTemp[0].teacher || ''
}
else {
courseList.value = [defaultCourse]
}
})
onShow(() => {
setPageConfig({ pageTitle: isUpdate.value ? '编辑课程' : '添加课程', showBackAction: true, showNavBar: true })
})
function handleDeleteCourseItem(courseIndex: number) {
uni.showModal({
title: '警告',
content: '确定删除该时间段的课程吗?',
success: (res) => {
if (res.confirm)
courseList.value.splice(courseIndex, 1)
},
})
}
function handleAddNewTime() {
courseList.value.push(cloneDeep(courseList.value[courseList.value.length - 1]))
}
function handleSaveCourse() {
if (!courseTitle.value) {
uni.showToast({
title: '请输入课程名称',
icon: 'none',
})
return
}
for (const courseItem of courseList.value)
Object.assign(courseItem, { title: courseTitle.value, teacher: courseTeacher.value })
if (isUpdate.value)
courseStore.deleteCourseItemById(originalCourseId.value)
courseStore.setCourseList(courseStore.courseList.concat(courseList.value))
uni.showModal({
title: '提示',
content: '保存成功',
showCancel: false,
})
uni.navigateBack()
}
const showWeekActionSheet = ref(false)
const clickedIndex = ref(-1)
const clickedWeeks = ref<number[]>([])
function handleShowWeekActionSheet(clickIndex: number) {
showWeekActionSheet.value = true
clickedIndex.value = clickIndex
clickedWeeks.value = courseList.value[clickIndex].weeks
}
/**
* transform weeks to string eg: [1, 2, 3, 5, 6, 8] to '1-3,5-6,8'
* @param weeks week list
*/
function transformArray2String(weeks: number[]): string {
let weeksString = ''
for (let i = 0; i < weeks.length; i++) {
if (i === 0) {
weeksString += weeks[i]
}
else {
if (weeks[i] !== weeks[i - 1] + 1) {
const last = weeksString.split(',')[weeksString.split(',').length - 1]
if (Number.parseInt(last) !== weeks[i - 1])
weeksString += `-${weeks[i - 1]}`
weeksString += `,${weeks[i]}`
}
else {
if (i === weeks.length - 1)
weeksString += `-${weeks[i]}`
}
}
}
return weeksString
}
function handleClickWeek(week: number) {
if (!clickedWeeks.value.includes(week)) {
clickedWeeks.value.push(week)
clickedWeeks.value.sort((a, b) => a - b)
}
else {
const index = clickedWeeks.value.indexOf(week)
clickedWeeks.value.splice(index, 1)
}
}
function handleConfirmWeekActionSheet() {
showWeekActionSheet.value = false
courseList.value[clickedIndex.value].weeks = clickedWeeks.value
}
const showTimeActionSheet = ref(false)
const timeValue = ref([1, 1, 1])
function handleShowTimeActionSheet(clickIndex: number) {
showTimeActionSheet.value = true
clickedIndex.value = clickIndex
const { week, start, duration } = courseList.value[clickIndex]
timeValue.value = [week, start, duration]
}
const timeList = [
['星期数', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'],
['开始节次', '第1节', '第2节', '第3节', '第4节', '第5节', '第6节', '第7节', '第8节', '第9节'],
['持续节次', '持续1节', '持续2节', '持续3节', '持续4节', '持续5节', '持续6节', '持续7节', '持续8节'],
]
function handleTimeChange(e: any) {
const value = e.detail.value
courseList.value[clickedIndex.value].week = value[0] ? value[0] : 1
courseList.value[clickedIndex.value].start = value[1] ? value[1] : 1
courseList.value[clickedIndex.value].duration = value[2] ? value[2] : 1
}
function handleConfirmTimeActionSheet() {
showTimeActionSheet.value = false
}
</script>
<template>
<UBasePage>
<view>
<view class="bg-white mb-4 py-1 justify-center items-start dark:bg-#121212">
<view class="px-4">
<view class="text-lg">
课程信息
</view>
</view>
<view class="flex px-4 justify-start items-center">
<view class="min-w-14">
名称
</view>
<input v-model="courseTitle" class="w-full" type="text" placeholder="输入课程名(必填)">
</view>
<view class="flex px-4 justify-start items-center">
<view class="min-w-14">
教师
</view>
<input v-model="courseTeacher" class="w-full" type="text" placeholder="输入上课教师">
</view>
</view>
<template v-for="(courseItem, courseIndex) of courseList" :key="courseIndex">
<view class="bg-white flex flex-col mb-4 py-1 gap-2 justify-center dark:bg-#121212">
<view class="flex px-4 justify-between items-center">
<view class="text-lg">
{{ `时间段${courseIndex + 1}` }}
</view>
<view class="text-red-500 i-carbon-delete" @click="handleDeleteCourseItem(courseIndex)" />
</view>
<view class="flex px-4 justify-start items-center">
<view class="min-w-14">
地点
</view>
<input v-model="courseItem.location" class="w-full" type="text" placeholder="输入上课地点(选填)">
</view>
<view class="flex px-4 justify-start items-center">
<view class="min-w-14">
周数
</view>
<view class="w-full" @click="handleShowWeekActionSheet(courseIndex)">
{{ transformArray2String(courseItem.weeks) }}
</view>
</view>
<view class="flex px-4 justify-start items-center">
<view class="min-w-14">
时间
</view>
<view class="w-full" @click="handleShowTimeActionSheet(courseIndex)">
{{ `${timeList[0][courseItem.week]} ${courseItem.start}-${courseItem.start + courseItem.duration - 1}` }}
</view>
</view>
</view>
</template>
<view class="flex flex-col pb-safe gap-1 justify-center">
<view
class="flex bg-green-500 h-12 text-white text-center justify-center items-center"
hover-class="bg-green-600" :hover-stay-time="150" @click="handleAddNewTime"
>
<view class="i-carbon-add" />
添加其他时间段
</view>
<view
class="flex bg-blue-500 h-12 text-white text-center justify-center items-center"
hover-class="bg-blue-600" :hover-stay-time="150" @click="handleSaveCourse"
>
保存
</view>
</view>
</view>
<!-- week action sheet -->
<view @touchmove.prevent>
<view
class="bg-white w-full min-h-10 transition-all ease-in-out z-100 duration-300 fixed dark:bg-#121212"
:class="showWeekActionSheet ? 'bottom-0' : '-bottom-full'"
>
<view class="flex flex-col py-6 gap-6">
<view class="flex font-medium text-xl px-4 justify-between items-center">
选择上课周
<view class="font-normal text-base text-green-500" @click="handleConfirmWeekActionSheet">
确定
</view>
</view>
<view class="grid p-1 gap-1 grid-cols-5 justify-center items-center">
<template v-for="(item, index) of 20" :key="index">
<view
class="flex h-8 text-center text-white transition-all inline-block justify-center items-center"
:class="clickedWeeks.includes(index + 1) ? 'bg-blue-500' : 'bg-gray-300'"
@click="handleClickWeek(index + 1)"
>
{{ item }}
</view>
</template>
</view>
</view>
<view
class="flex pb-safe border-t-4 border-gray-200 h-12 text-lg justify-center items-center dark:border-opacity-20"
hover-class="bg-gray-200 bg-opacity-50" :hover-stay-time="150" @click="showWeekActionSheet = false"
>
关闭
</view>
</view>
<view
class=" bg-dark-100 bg-opacity-50 transition-all top-0 right-0 bottom-0 left-0 z-90 fixed"
:class="showWeekActionSheet ? 'opacity-100 visible' : 'opacity-0 invisible'"
@click="showWeekActionSheet = false"
/>
</view>
<!-- time action sheet -->
<view @touchmove.prevent>
<view
class="bg-white w-full min-h-10 transition-all ease-in-out z-100 duration-300 fixed dark:bg-#121212"
:class="showTimeActionSheet ? 'bottom-0' : '-bottom-full'"
>
<view class="flex flex-col py-6 gap-6">
<view class="flex font-medium text-xl px-4 justify-between items-center">
选择上课时间
<view class="font-normal text-base text-green-500" @click="handleConfirmTimeActionSheet">
确定
</view>
</view>
<picker-view class="h-40" :value="timeValue" @change="handleTimeChange">
<picker-view-column>
<view
v-for="(item, index) in timeList[0]" :key="index"
class="flex justify-center items-center"
>
{{ item }}
</view>
</picker-view-column>
<picker-view-column>
<view
v-for="(item, index) in timeList[1]" :key="index"
class="flex justify-center items-center"
>
{{ item }}
</view>
</picker-view-column>
<picker-view-column>
<view
v-for="(item, index) in timeList[2]" :key="index"
class="flex justify-center items-center"
>
{{ item }}
</view>
</picker-view-column>
</picker-view>
</view>
<view
class="flex pb-safe border-t-4 border-gray-200 h-12 text-lg justify-center items-center dark:border-opacity-20"
hover-class="bg-gray-200 bg-opacity-50" :hover-stay-time="150" @click="showTimeActionSheet = false"
>
关闭
</view>
</view>
<view
class=" bg-dark-100 bg-opacity-50 transition-all top-0 right-0 bottom-0 left-0 z-90 fixed"
:class="showTimeActionSheet ? 'opacity-100 visible' : 'opacity-0 invisible'"
@click="showTimeActionSheet = false"
/>
</view>
</UBasePage>
</template>

451
src/pages/index/index.vue Normal file
View File

@@ -0,0 +1,451 @@
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
import type { CourseModel } from '~/stores/modules/course'
import { useAppStore } from '~/stores/modules/app'
import { useCourseStore } from '~/stores/modules/course'
import { getTermSettingAPI } from '~/services/course'
const { courseList } = storeToRefs(useCourseStore())
const { startDate, timeSetting, endDate, totalWeeks } = storeToRefs(
useAppStore(),
)
const { setStartDay, setCourseSetting } = useCourseStore()
const month = ref<number>(new Date().getMonth() + 1)
const date = ref<number>(new Date().getDate())
const today = ref<number>(new Date().getDay())
const weekday = [
'星期天',
'星期一',
'星期二',
'星期三',
'星期四',
'星期五',
'星期六',
]
const currentWeek = ref<number>(1)
interface CourseType {
startTime: string
endTime: string
data: CourseModel[]
}
const morningCourse = ref<CourseType>()
const afternoonCourse = ref<CourseType>()
const eveningCourse = ref<CourseType>()
const initCourse = async () => {
const time = new Date().getTime() - new Date(startDate.value).getTime()
currentWeek.value = Math.ceil(time / 1000 / 60 / 60 / 24 / 7)
morningCourse.value = {
startTime: '08:00',
endTime: '12:20',
data: courseList.value.filter(
item =>
item.week === today.value
&& item.weeks.includes(currentWeek.value)
&& item.start + item.duration <= 6,
),
}
afternoonCourse.value = {
startTime: '14:00',
endTime: '17:30',
data: courseList.value.filter(
item =>
item.week === today.value
&& item.weeks.includes(currentWeek.value)
&& item.start + item.duration <= 10
&& item.start + item.duration > 6,
),
}
eveningCourse.value = {
startTime: '19:00',
endTime: '21:25',
data: courseList.value.filter(
item =>
item.week === today.value
&& item.weeks.includes(currentWeek.value)
&& item.start + item.duration >= 11,
),
}
}
const getTermSetting = async () => {
uni.showLoading({ title: '加载中~' })
const res = await getTermSettingAPI()
if (res.code === 200) {
startDate.value = res.data.startDate
endDate.value = res.data.endDate
totalWeeks.value = res.data.totalWeeks
timeSetting.value = res.data.timeSetting
setCourseSetting(res.data.startDate, res.data.totalWeeks)
uni.hideLoading()
uni.showToast({ icon: 'success', title: '加载成功~' })
}
else {
uni.hideLoading()
}
}
onLoad(async () => {
if (!startDate.value) {
await getTermSetting()
setStartDay()
}
initCourse()
})
onPullDownRefresh(async () => {
initCourse()
setTimeout(() => {
// 结束下拉刷新
uni.stopPullDownRefresh ()
}, 500)
})
</script>
<template>
<view class="page-box">
<view class="header">
<view class="currentStatus">
今日课表
</view>
<view class="todayDate">
{{ month }}{{ date }} | {{ weekday[today] }} |
{{ currentWeek > 0 ? `${currentWeek}` : '未开学' }}
</view>
</view>
<!-- <view class="headerFore"></view> -->
<view class="content">
<!-- 上午 -->
<view class="section">
上午课程
</view>
<view
v-for="(item, index) in morningCourse?.data"
:key="index"
class="class"
>
<view class="time">
<view class="start">
{{ timeSetting.filter((x) => x.index === item.start)[0].start }}
</view>
<view class="end opa">
{{
timeSetting.filter(
(x) => x.index === (item.start + item.duration - 1),
)[0].end
}}
</view>
</view>
<view class="viewider" />
<view class="infoCan">
<view class="infoLesson text-cut">
{{ item.title }}
</view>
<view class="infoOther opa">
{{ item.location }} | {{ item.teacher }}
</view>
</view>
</view>
<block v-if="morningCourse?.data.length === 0">
<view class="class noclass">
<view class="time">
<view class="start">
08:00
</view>
<view class="end">
12:20
</view>
</view>
<view class="viewider" />
<view class="infoCan">
<view class="infoLesson">
</view>
<view class="infoOther">
第1节 - 第5节
</view>
</view>
</view>
</block>
<!-- 下午 -->
<view class="section">
下午课程
</view>
<view
v-for="(item, index) in afternoonCourse?.data"
:key="index"
class="class"
>
<view class="time">
<view class="start">
{{ timeSetting.filter((x) => x.index === item.start)[0].start }}
</view>
<view class="end opa">
{{
timeSetting.filter(
(x) => x.index === (item.start + item.duration - 1),
)[0].end
}}
</view>
</view>
<view class="viewider" />
<view class="infoCan">
<view class="infoLesson text-cut">
{{ item.title }}
</view>
<view class="infoOther opa">
{{ item.location }} | {{ item.teacher }}
</view>
</view>
</view>
<block v-if="afternoonCourse?.data.length === 0">
<view class="class noclass">
<view class="time">
<view class="start">
14:00
</view>
<view class="end">
17:30
</view>
</view>
<view class="viewider" />
<view class="infoCan">
<view class="infoLesson">
</view>
<view class="infoOther">
第6节 - 第9节
</view>
</view>
</view>
</block>
<!-- 晚上 -->
<view class="section">
晚上课程
</view>
<view
v-for="(item, index) in eveningCourse?.data"
:key="index"
class="class"
>
<view class="time">
<view class="start">
{{ timeSetting.filter((x) => x.index === item.start)[0].start }}
</view>
<view class="end opa">
{{
timeSetting.filter(
(x) => x.index === (item.start + item.duration - 1),
)[0].end
}}
</view>
</view>
<view class="viewider" />
<view class="infoCan">
<view class="infoLesson text-cut">
{{ item.title }}
</view>
<view class="infoOther opa">
{{ item.location }} | {{ item.teacher }}
</view>
</view>
</view>
<block v-if="eveningCourse?.data.length === 0">
<view class="class noclass">
<view class="time">
<view class="start">
19:00
</view>
<view class="end">
21:25
</view>
</view>
<view class="viewider" />
<view class="infoCan">
<view class="infoLesson">
</view>
<view class="infoOther">
第10节 - 第12节
</view>
</view>
</view>
</block>
</view>
</view>
</template>
<style lang="scss">
.box {
margin: 0;
padding: 0;
border: 0;
outline: 0;
background: transparent;
touch-action: auto;
line-height: 1;
color: #333;
}
.wrapper {
height: 100vh;
position: relative;
}
.page-box {
position: relative;
overflow: hidden;
}
.header {
height: 650rpx;
background: url('https://cdn.cuuo.cn/images/course-home.png');
position: relative;
background-size: cover;
overflow: auto;
z-index: 1;
}
.todayDate {
margin-top: 3vw;
margin-left: 8vw;
font-weight: 400;
font-size: 3.5vw;
color: #f1faff;
letter-spacing: 0.9px;
}
.currentStatus {
margin-top: 200rpx;
margin-left: 8vw;
font-weight: 600;
font-size: 5vw;
color: #fff;
}
.tips {
position: absolute;
background-color: #fff;
border-radius: 15rpx;
padding: 10rpx;
top: 330rpx;
right: 18vw;
max-width: 500rpx;
z-index: 100;
opacity: 0.8;
&::after {
content: '';
position: absolute;
height: 20rpx;
width: 50rpx;
right: -15rpx;
top: 25rpx;
border-radius: 0rpx 50rpx 0rpx 50rpx;
background-color: #fff;
z-index: -10;
opacity: 0.8;
}
}
.headerFore {
position: absolute;
width: 300rpx;
height: 235rpx;
top: 330rpx;
right: -10vw;
background: url('https://cdn.cuuo.cn/images/course-tips.gif');
background-size: cover;
z-index: 98;
}
.content {
position: relative;
overflow: auto;
margin-top: -100rpx;
margin-bottom: calc(100rpx + env(safe-area-inset-bottom));
padding: 2vw 7vw;
box-sizing: border-box;
background-color: #fff;
min-height: calc(100vh - 30vw);
border-radius: 5vw 5vw 0px 0px;
z-index: 10;
}
.section {
margin-top: 6vw;
margin-bottom: 3vw;
font-size: 3vw;
font-weight: 700;
letter-spacing: 0;
color: #8c93b0;
}
.class {
margin: 20rpx 10rpx;
height: 10vw;
display: flex;
align-items: center;
}
.opa {
opacity: 0.6;
}
.noclass {
color: #3a526f;
opacity: 0.4;
}
.time {
color: #000;
width: 9vw;
min-width: 9vw;
display: inline-flex;
flex-direction: column;
margin-right: 1.5vw;
}
.start {
padding-bottom: 10rpx;
}
.start,
.end {
font-size: 3vw;
}
.viewider {
background-color: #2196f3;
width: 1vw;
min-width: 1vw;
height: 8vw;
border-radius: 10rpx;
color: #0000ff8c;
}
.infoCan {
flex-grow: 1;
display: flex;
flex-direction: column;
margin-left: 2vw;
margin-right: 1vw;
}
.infoLesson,
.infoOther {
color: #000;
letter-spacing: 0;
margin-bottom: 0.7vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.infoLesson {
font-size: 4vw;
max-width: 590rpx;
}
.infoOther {
font-size: 3vw;
}
</style>

View File

@@ -0,0 +1,155 @@
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import UBasePage from '~/components/UnoUI/UBasePage/UBasePage.vue'
import cusSelects from '~/components/cus-selects-fan.vue'
import { useAppStore } from '~/stores/modules/app'
import { useCourseStore } from '~/stores/modules/course'
import { getTermSettingAPI } from '~/services/course'
import { usePageStore } from '~/stores'
const { startDate, endDate, totalWeeks, classNames, classinfo, campusId, timeSetting, campusList }
= storeToRefs(useAppStore())
const { setPageConfig } = usePageStore()
const { getClassNames, getCampusList } = useAppStore()
const { getCourseList, setStartDay, setCourseSetting } = useCourseStore()
const classId = ref<string>('')
const getTermSetting = async () => {
uni.showLoading({ title: '加载中~' })
const res = await getTermSettingAPI()
if (res.code === 200) {
startDate.value = res.data.startDate
endDate.value = res.data.endDate
totalWeeks.value = res.data.totalWeeks
timeSetting.value = res.data.timeSetting
setCourseSetting(res.data.startDate, res.data.totalWeeks)
uni.hideLoading()
uni.showToast({ icon: 'success', title: '加载成功~' })
}
else {
uni.hideLoading()
}
}
onLoad(async () => {
classId.value = classinfo.value.value
await getTermSetting()
setStartDay()
await getCampusList()
})
const resetTerm = async () => {
uni.showModal({
cancelText: '取消',
title: '刷新提醒',
content: '是否刷新学期配置?',
confirmText: '确认',
success: async () => {
await getTermSetting()
setStartDay()
},
fail: (fail) => {
uni.showToast({
title: '刷新失败~',
icon: 'error',
})
},
})
}
const saveClassInfo = async () => {
uni.showModal({
cancelText: '取消',
confirmText: '确认',
title: '刷新课程?',
content: '是否刷新课程信息?',
success: async (success) => {
if (classId.value) {
classinfo.value = classNames.value.filter(
item => item.value === classId.value,
)[0]
await getCourseList(classId.value)
}
else {
uni.showToast({
icon: 'error',
title: '请选择班级!',
})
}
},
fail: (fail) => {
uni.showToast({
title: '刷新失败~',
icon: 'error',
})
},
})
}
const onCampusSelectChange = async (e) => {
campusId.value = e
await getClassNames(e)
}
const onselectChange = async (e) => {
classId.value = e
}
onShow(() => {
setPageConfig({ showNavBar: true, pageTitle: '课表配置' })
})
</script>
<template>
<UBasePage>
<uni-section title="学期配置" type="line">
<uni-card title="" is-full is-shadow>
<uni-forms label-position="top">
<uni-forms-item label="开学日期">
<uni-easyinput
v-model="startDate"
placeholder="请输入开学日期"
disabled
/>
</uni-forms-item>
<uni-forms-item label="结束日期">
<uni-easyinput
v-model="endDate"
placeholder="请输入结束日期"
disabled
/>
</uni-forms-item>
<uni-forms-item label="学期周数">
<uni-easyinput
v-model="totalWeeks"
placeholder="请输入学期周数"
disabled
/>
</uni-forms-item>
</uni-forms>
<button type="primary" @click="resetTerm">
更新学期配置
</button>
</uni-card>
</uni-section>
<uni-section title="班级配置" type="line">
<uni-card title="" is-full is-shadow class="classinfo">
<uni-forms label-position="top">
<uni-forms-item label="校区">
<cus-selects :value="campusId" filterable :data="campusList" :value-type="{ label: 'name', value: 'id' }" style="width: 100%;" @change="onCampusSelectChange" />
</uni-forms-item>
<uni-forms-item label="班级">
<cus-selects :value="classId" filterable :data="classNames" :value-type="{ label: 'text', value: 'value' }" style="width: 100%;" @change="onselectChange" />
</uni-forms-item>
</uni-forms>
<button type="primary" @click="saveClassInfo">
保存班级信息
</button>
</uni-card>
</uni-section>
</UBasePage>
</template>
<style lang="scss">
.classinfo {
height: 700rpx;
}
.select_wrap{
width: 100%;
}
</style>

153
src/pages/splash/splash.vue Normal file
View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
import { usePageStore } from '~/stores/modules/page'
const { setPageConfig } = usePageStore()
onShow(() => {
setPageConfig({ showNavBar: false })
})
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index',
})
}, 500)
</script>
<template>
<UBasePage>
<view class="loader dark:bg-#121212" />
</UBasePage>
</template>
<style>
.loader {
position: absolute;
}
.loader {
top: 72vh;
left: 50vw;
transform: rotate(165deg);
}
.loader:after,
.loader:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
display: block;
width: 0.5em;
height: 0.5em;
border-radius: 0.25em;
transform: translate(-50%, -50%);
}
.loader:before {
animation: before 2s infinite;
}
.loader:after {
animation: after 2s infinite;
}
@-webkit-keyframes before {
0% {
width: 0.5em;
box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75),
-1em 0.5em rgba(111, 202, 220, 0.75);
}
35% {
width: 2.5em;
box-shadow: 0 -0.5em rgba(225, 20, 98, 0.75),
0 0.5em rgba(111, 202, 220, 0.75);
}
70% {
width: 0.5em;
box-shadow: -1em -0.5em rgba(225, 20, 98, 0.75),
1em 0.5em rgba(111, 202, 220, 0.75);
}
to {
box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75),
-1em 0.5em rgba(111, 202, 220, 0.75);
}
}
@keyframes before {
0% {
width: 0.5em;
box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75),
-1em 0.5em rgba(111, 202, 220, 0.75);
}
35% {
width: 2.5em;
box-shadow: 0 -0.5em rgba(225, 20, 98, 0.75),
0 0.5em rgba(111, 202, 220, 0.75);
}
70% {
width: 0.5em;
box-shadow: -1em -0.5em rgba(225, 20, 98, 0.75),
1em 0.5em rgba(111, 202, 220, 0.75);
}
to {
box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75),
-1em 0.5em rgba(111, 202, 220, 0.75);
}
}
@-webkit-keyframes after {
0% {
height: 0.5em;
box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75),
-0.5em -1em rgba(233, 169, 32, 0.75);
}
35% {
height: 2.5em;
box-shadow: 0.5em 0 rgba(61, 184, 143, 0.75),
-0.5em 0 rgba(233, 169, 32, 0.75);
}
70% {
height: 0.5em;
box-shadow: 0.5em -1em rgba(61, 184, 143, 0.75),
-0.5em 1em rgba(233, 169, 32, 0.75);
}
to {
box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75),
-0.5em -1em rgba(233, 169, 32, 0.75);
}
}
@keyframes after {
0% {
height: 0.5em;
box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75),
-0.5em -1em rgba(233, 169, 32, 0.75);
}
35% {
height: 2.5em;
box-shadow: 0.5em 0 rgba(61, 184, 143, 0.75),
-0.5em 0 rgba(233, 169, 32, 0.75);
}
70% {
height: 0.5em;
box-shadow: 0.5em -1em rgba(61, 184, 143, 0.75),
-0.5em 1em rgba(233, 169, 32, 0.75);
}
to {
box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75),
-0.5em -1em rgba(233, 169, 32, 0.75);
}
}
</style>

147
src/services/course.ts Normal file
View File

@@ -0,0 +1,147 @@
import { http } from '~/modules/http'
import type { TimeIndex } from '~/stores/modules/app'
export interface ClassName {
text: string
value: string
disable: boolean
}
interface ClassResult {
id: string
name: string
}
export interface CourseInfo {
classroom: string
course: string
day: number
id: string
teacher: string
time: number[]
weeks: number[]
}
export interface ClassroomInfo {
id: string
name: string
build: string
build_id: string
campus_id: string
campus: string
}
export interface ClassroomCourseInfo {
week: number
day: number
classTime: number
classnames: string[]
campus: string
build: string
startTime: string
endTime: string
startStamp: number
endStamp: number
classroom: string
course: string
teacher: string
id: string
}
/**
* 获取校区班级
* @param id
* @returns
*/
export const getClassNamesAPI = async (id: string) => {
return http<ClassResult[]>({
method: 'GET',
url: `/getCampusClass/${id}`,
})
}
/**
* 获取班级课程
* @param 班级ID
* @returns
*/
export const getCourseAPI = async (id: string) => {
return http<CourseInfo[]>({
method: 'GET',
url: `/getCourse/${id}`,
})
}
export interface GetClassrommsParams {
campus_id: string
build_id?: string
startStamp: number
endStamp: number
}
export const getEmptyClassroomAPI = async (params: GetClassrommsParams) => {
return http<ClassroomInfo[]>({
method: 'GET',
url: '/getEmptyClassroom',
data: { ...params },
})
}
interface TermSetting {
startDate: string
totalWeeks: number
endDate: string
timeSetting: TimeIndex[]
}
export const getTermSettingAPI = async () => {
return http<TermSetting>({
method: 'GET',
url: '/getTermSetting',
})
}
export interface CampusInfo {
id: string
name: string
}
export const getCampusListAPI = async () => {
return http<CampusInfo[]>({
method: 'GET',
url: '/getCampusList',
})
}
export interface BuildInfo {
id: string
name: string
campus: string
campus_id: string
}
export const getBuildListALLAPI = async () => {
return http<BuildInfo[]>({
method: 'GET',
url: '/getBuildList',
})
}
export const getBuildListAPI = async (id: string) => {
return http<BuildInfo[]>({
method: 'GET',
url: `/getBuilds/${id}`,
})
}
export const getClassroomCourseListAPI = async (id: string, week: number) => {
return http<ClassroomCourseInfo[]>({
method: 'GET',
url: '/getClassroomCourses',
data: { classroom_id: id, week },
})
}
export const getClassroomListAPI = async (id: string) => {
return http<ClassroomInfo[]>({
method: 'GET',
url: `/getClassroomList/${id}`,
})
}

12
src/shims.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
// with vite-plugin-md, markdowns can be treat as Vue components
declare module "*.md" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

BIN
src/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

16
src/stores/index.ts Normal file
View File

@@ -0,0 +1,16 @@
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
// 创建 pinia 实例
const pinia = createPinia()
// 使用持久化存储插件
pinia.use(persist)
// 默认导出,给 main.ts 使用
export default pinia
// 模块统一导出
export * from './modules/app'
export * from './modules/course'
export * from './modules/page'

233
src/stores/modules/app.ts Normal file
View File

@@ -0,0 +1,233 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type {
CampusInfo,
ClassName,
ClassroomInfo,
GetClassrommsParams,
} from '~/services/course'
import {
getCampusListAPI,
getClassNamesAPI,
getEmptyClassroomAPI,
} from '~/services/course'
interface MenuButtonBoundingClientRect {
width: number
height: number
top: number
left: number
right: number
bottom: number
}
export interface TimeIndex {
index: number
start: string
end: string
time: number
}
export interface EmptyClass {
date: string
timeRange: number[]
data: ClassroomInfo[]
}
export const useAppStore = defineStore(
'app',
() => {
const darkMode = ref(false)
const statusBarHeight = ref(0)
const classroomId = ref('')
const buildId = ref('')
const menuButtonBounding = ref<MenuButtonBoundingClientRect>()
const customBarHeight = computed(() =>
!menuButtonBounding.value
? 0
: menuButtonBounding.value.bottom
+ menuButtonBounding.value.top
- statusBarHeight.value,
)
const startDate = ref<string | Date>('')
const endDate = ref<string>('')
const timeSetting = ref<TimeIndex[]>([
{
index: 1,
start: '08:00',
end: '08:45',
time: 0,
},
{
index: 2,
start: '08:50',
end: '09:35',
time: 0,
},
{
index: 3,
start: '09:55',
end: '10:40',
time: 0,
},
{
index: 4,
start: '10:45',
end: '11:30',
time: 0,
},
{
index: 5,
start: '11:35',
end: '12:20',
time: 0,
},
{
index: 6,
start: '14:00',
end: '14:45',
time: 1,
},
{
index: 7,
start: '14:50',
end: '15:35',
time: 1,
},
{
index: 8,
start: '15:55',
end: '16:40',
time: 1,
},
{
index: 9,
start: '16:45',
end: '17:30',
time: 1,
},
{
index: 10,
start: '19:00',
end: '19:45',
time: 2,
},
{
index: 11,
start: '19:50',
end: '20:35',
time: 2,
},
{
index: 12,
start: '20:40',
end: '21:25',
time: 2,
},
])
const totalWeeks = ref<number>(0)
const classinfo = ref<ClassName>({
text: '',
value: '',
disable: false,
})
const campusId = ref<string>('')
const campusList = ref<CampusInfo[]>([])
const classNames = ref<ClassName[]>([])
const emptyClassroom = ref<EmptyClass>({
date: '',
data: [],
timeRange: [1, 2],
})
const getClassNames = async (id: string) => {
uni.showLoading({ title: '加载中~' })
const res = await getClassNamesAPI(id)
if (res.code === 200) {
classNames.value = res.data.map((item) => {
return {
text: item.name,
value: item.id,
disable: false,
}
})
uni.hideLoading()
uni.showToast({ icon: 'success', title: '加载成功~' })
}
else {
uni.hideLoading()
}
}
const getCampusList = async () => {
uni.showLoading({ title: '加载中~' })
const res = await getCampusListAPI()
if (res.code === 200) {
campusList.value = res.data
uni.hideLoading()
uni.showToast({ icon: 'success', title: '加载成功~' })
}
else {
uni.hideLoading()
}
}
const getEmptyClassroom = async (
params: GetClassrommsParams,
date: string,
timeRange: number[],
) => {
uni.showLoading({ title: '加载中~' })
const res = await getEmptyClassroomAPI(params)
if (res.code === 200) {
emptyClassroom.value = {
date,
data: res.data,
timeRange,
}
uni.hideLoading()
uni.showToast({ icon: 'success', title: '加载成功~' })
return true
}
else {
uni.hideLoading()
return false
}
}
return {
darkMode,
startDate,
endDate,
timeSetting,
totalWeeks,
classNames,
campusList,
campusId,
buildId,
classinfo,
classroomId,
emptyClassroom,
statusBarHeight,
customBarHeight,
menuButtonBounding,
getClassNames,
getCampusList,
getEmptyClassroom,
}
},
{
// 配置持久化
persist: {
// 调整为兼容多端的API
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
},
},
},
},
)
// Need to be used outside the setup
// export function useAppStoreWidthOut() {
// return useAppStore(pinia)
// }

View File

@@ -0,0 +1,340 @@
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { getCourseAPI } from '~/services/course'
// import { getCourseAPI } from '~/services/course'
// import { useAppStore } from '~/stores/modules/app'
// const { startDate, totalWeeks } = storeToRefs(useAppStore())
export interface CourseModel {
id: string
title: string
location: string
teacher?: string
start: number
duration: number
// [1-7]
week: number
// [[1-20]]
weeks: number[]
color?: string
}
export interface ClassName {
id: string
name: string
}
export const weekTitle = [
'周一',
'周二',
'周三',
'周四',
'周五',
'周六',
'周日',
]
const colorMap = new Map<string, string>()
// @unocss-include
export const colorList = [
[
'#FFDC72',
'#CE7CF4',
'#FF7171',
'#66CC99',
'#FF9966',
'#66CCCC',
'#6699CC',
'#99CC99',
'#669966',
'#66CCFF',
'#99CC66',
'#FF9999',
'#81CC74',
],
[
'#99CCFF',
'#FFCC99',
'#CCCCFF',
'#99CCCC',
'#A1D699',
'#7397db',
'#ff9983',
'#87D7EB',
'#99CC99',
],
]
const conflictCourseMap = new Map<CourseModel, CourseModel[]>()
export const useCourseStore = defineStore(
'course',
() => {
const isStart = ref<boolean>(false)
const courseList = ref<CourseModel[]>([])
const currentMonth = ref<number>(0)
const originalWeekIndex = ref<number>(0)
const startDate = ref<Date | string>('')
const totalWeeks = ref<number>(0)
const currentWeekIndex = ref<number>(0)
const colorArrayIndex = ref<number>(0)
/**
* set start date
* @param someDate the start date of the semester
*/
function setStartDay() {
const start = new Date(startDate.value)
const days = new Date().getTime() - start.getTime()
isStart.value = days > 0
const week = Math.floor(days / (1000 * 60 * 60 * 24 * 7))
originalWeekIndex.value = week < 0 ? 0 : week
setCurrentWeekIndex(originalWeekIndex.value)
}
/**
* change current week index
* @param weekIndex the new week index
*/
function setCurrentWeekIndex(weekIndex: number) {
conflictCourseMap.clear()
currentWeekIndex.value = weekIndex
// change current month
const someDate = new Date(startDate.value)
someDate.setDate(someDate.getDate() + weekIndex * 7)
currentMonth.value = someDate.getMonth() + 1
}
/**
* init course list
* @param newCourseList new course list
*/
function setCourseList(newCourseList: CourseModel[]) {
conflictCourseMap.clear()
// sort by week and start
courseList.value = newCourseList.sort(
(a, b) => a.week - b.week || a.start - b.start,
)
resetCourseBgColor()
}
// current week course list
const weekCourseList = computed(() => {
if (courseList.value) {
return courseList.value.filter(item =>
item.weeks.includes(currentWeekIndex.value + 1),
)
}
return []
})
// data for course action
const parsedCourseList = computed(() => {
// init a course array
const parsedCourseList = Array.from({ length: totalWeeks.value }, () =>
Array.from({ length: 7 }, () => Array.from({ length: 6 }, () => 0)),
)
if (courseList.value) {
// process course list
for (const courseItem of courseList.value) {
const { start, duration, week, weeks } = courseItem
for (const w of weeks) {
const dayCourseList = parsedCourseList[w - 1][week - 1]
dayCourseList[Math.floor(start / 2)]++
// some courses may last more than 2 times
if (duration > 2)
dayCourseList[Math.floor(start / 2 + 1)]++
}
}
}
return parsedCourseList
})
// current week date list
const currentWeekDayArray = computed(() => {
const weekIndex = currentWeekIndex.value
const someDate = new Date(startDate.value)
someDate.setDate(someDate.getDate() - 1 + weekIndex * 7)
// Helper function to format date
const formatDate = (date) => {
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${month}/${day}`
}
// Generate the array of days for the week
const dayArray = Array.from({ length: 7 }, () => {
someDate.setDate(someDate.getDate() + 1)
return formatDate(someDate)
})
return dayArray
})
/**
* list of course for a certain course item time
* @param courseItem the course item
*/
function getConflictCourse(courseItem: CourseModel): CourseModel[] {
if (!courseItem)
return []
const { week, start } = courseItem
return courseList.value.filter((item) => {
return (
item.weeks.includes(currentWeekIndex.value + 1)
&& item.week === week
&& item.start === start
)
})
}
/**
* list of course for a certain course item time with map
* @param courseItem the course item
*/
function hasConflictCourseByMap(courseItem: CourseModel): CourseModel[] {
if (!conflictCourseMap.has(courseItem))
conflictCourseMap.set(courseItem, getConflictCourse(courseItem))
return conflictCourseMap.get(courseItem) || []
}
/**
* reset course bg color
*/
function resetCourseBgColor() {
colorMap.clear()
if (courseList.value) {
courseList.value.map(courseItem =>
Object.assign(courseItem, { color: getCourseColor(courseItem) }),
)
}
}
/**
* get course item color
* @param courseItem course item
* @returns course color
*/
function getCourseColor(courseItem: CourseModel): string {
const colorArray = colorList[colorArrayIndex.value]
const { title } = courseItem
if (!colorMap.has(title))
colorMap.set(title, colorArray[colorMap.size % colorArray.length])
return colorMap.get(title) || 'bg-white'
}
watch(
() => colorArrayIndex.value,
() => resetCourseBgColor(),
)
/**
* set a course to top when there have more than one course in the same time
* @param courseItem course item
*/
function setCourseItemTop(courseItem: CourseModel) {
deleteCourseItem(courseItem)
courseList.value.unshift(courseItem)
}
/**
* delete a course
* @param courseItem course item
*/
function deleteCourseItem(courseItem: CourseModel) {
conflictCourseMap.clear()
const { id, week, start } = courseItem
for (let i = 0; i < courseList.value.length; i++) {
const item = courseList.value[i]
if (item.id === id && item.week === week && item.start === start)
courseList.value.splice(i, 1)
}
}
/**
* delete a course by title
* @param courseTitle course title
*/
function deleteCourseItemById(id: string) {
conflictCourseMap.clear()
for (let i = 0; i < courseList.value.length; i++) {
const item = courseList.value[i]
if (item.id === id)
courseList.value.splice(i, 1)
}
}
const getCourseList = async (id: string) => {
uni.showLoading({ title: '加载中~' })
const res = await getCourseAPI(id)
if (res.code === 200) {
const courses: CourseModel[] = res.data.map((item) => {
return {
id: item.id,
title: item.course,
location: item.classroom,
teacher: item.teacher,
start: item.time[0],
duration: item.time.length,
week: item.day,
weeks: item.weeks,
}
})
setCourseList(courses)
courseList.value = courses
uni.hideLoading()
uni.showToast({ icon: 'success', title: '加载成功~' })
}
else {
uni.hideLoading()
}
}
const setCourseSetting = (start: string, total: number) => {
startDate.value = start
totalWeeks.value = total
}
return {
isStart,
startDate,
currentMonth,
courseList,
totalWeeks,
setCourseList,
weekCourseList,
parsedCourseList,
originalWeekIndex,
currentWeekIndex,
currentWeekDayArray,
colorArrayIndex,
setStartDay,
setCurrentWeekIndex,
getConflictCourse,
hasConflictCourseByMap,
setCourseItemTop,
deleteCourseItem,
deleteCourseItemById,
getCourseList,
setCourseSetting,
}
},
{
// 配置持久化
persist: {
// 调整为兼容多端的API
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
},
},
},
},
)
// Need to be used outside the setup
// export function useCourseStoreWidthOut() {
// return useCourseStore(pinia)
// }

View File

@@ -0,0 +1,86 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { UNotifyOptions } from '~/components/UnoUI/UNotify/types'
import type { UToastOptions } from '~/components/UnoUI/UToast/types'
interface PageConfig {
showNavBar?: boolean
showBackAction?: boolean
showCustomAction?: boolean
pageTitle?: string
}
export const usePageStore = defineStore(
'page',
() => {
const showNavBar = ref(true)
const showBackAction = ref(false)
const showCustomAction = ref(false)
const pageTitle = ref('')
const notifyRef = ref<{
handleShowNotify: (options: UNotifyOptions) => {}
}>()
const toastRef = ref<{ handleShowToast: (options: UToastOptions) => {} }>()
const setPageConfig = (config: PageConfig) => {
const {
showNavBar: _showNavBar = true,
showBackAction: _showBackAction = false,
showCustomAction: _showCustomAction = false,
pageTitle: _pageTitle = '',
} = config
showNavBar.value = _showNavBar
showBackAction.value = _showBackAction
showCustomAction.value = _showCustomAction
pageTitle.value = _pageTitle
}
const showNotify = (options: UNotifyOptions) =>
notifyRef.value!.handleShowNotify(options)
const showToast = (options: UToastOptions) =>
toastRef.value!.handleShowToast(options)
const pageReset = () => {
showNavBar.value = true
showBackAction.value = false
showCustomAction.value = false
pageTitle.value = ''
notifyRef.value = undefined
toastRef.value = undefined
}
return {
setPageConfig,
showNavBar,
pageTitle,
showBackAction,
showCustomAction,
notifyRef,
toastRef,
showNotify,
showToast,
pageReset,
}
},
{
// 配置持久化
persist: {
// 调整为兼容多端的API
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
},
},
},
},
)
// Need to be used outside the setup
// export function usePageStoreWidthOut() {
// return usePageStore(pinia)
// }

22
src/theme.json Normal file
View File

@@ -0,0 +1,22 @@
{
"dark": {
"bgColor": "#222222",
"bgColorBottom": "#222222",
"bgColorTop": "#222222",
"bgTxtStyle": "light",
"navBgColor": "#222222",
"navTxtStyle": "white",
"tabBgColor": "#222222",
"tabBorderStyle": "white"
},
"light": {
"bgColor": "#F3F4F6",
"bgColorBottom": "#F3F4F6",
"bgColorTop": "#F3F4F6",
"bgTxtStyle": "dark",
"navBgColor": "#F3F4F6",
"navTxtStyle": "white",
"tabBgColor": "#F3F4F6",
"tabBorderStyle": "black"
}
}

76
src/uni.scss Normal file
View File

@@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:24rpx;
$uni-font-size-base:28rpx;
$uni-font-size-lg:32rpx;
/* 图片尺寸 */
$uni-img-size-sm:40rpx;
$uni-img-size-base:52rpx;
$uni-img-size-lg:80rpx;
/* Border Radius */
$uni-border-radius-sm: 4rpx;
$uni-border-radius-base: 6rpx;
$uni-border-radius-lg: 12rpx;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 10px;
$uni-spacing-row-base: 20rpx;
$uni-spacing-row-lg: 30rpx;
/* 垂直间距 */
$uni-spacing-col-sm: 8rpx;
$uni-spacing-col-base: 16rpx;
$uni-spacing-col-lg: 24rpx;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:40rpx;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:36rpx;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:30rpx;

View File

@@ -0,0 +1,6 @@
## 0.0.32022-11-11
- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug
## 0.0.22021-04-16
- 修改插件package信息
## 0.0.12021-03-15
- 初始化项目

View File

@@ -0,0 +1,81 @@
{
"id": "uni-config-center",
"displayName": "uni-config-center",
"version": "0.0.3",
"description": "uniCloud 配置中心",
"keywords": [
"配置",
"配置中心"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"directories": {
"example": "../../../scripts/dist"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}

View File

@@ -0,0 +1,93 @@
# 为什么使用uni-config-center
实际开发中很多插件需要配置文件才可以正常运行,如果每个插件都单独进行配置的话就会产生下面这样的目录结构
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ ├─index.js
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b // plugin-b对应的目录
├─index.js
└─config.json // plugin-b对应的配置文件
```
假设插件作者要发布一个项目模板,里面使用了很多需要配置的插件,无论是作者发布还是用户使用都是一个大麻烦。
uni-config-center就是用了统一管理这些配置文件的使用uni-config-center后的目录结构如下
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ └─index.js
├─plugin-b // plugin-b对应的目录
│ └─index.js
└─uni-config-center
├─index.js // config-center入口文件
├─plugin-a
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b
└─config.json // plugin-b对应的配置文件
```
使用uni-config-center后的优势
- 配置文件统一管理,分离插件主体和配置信息,更新插件更方便
- 支持对config.json设置schema插件使用者在HBuilderX内编写config.json文件时会有更好的提示后续HBuilderX会提供支持
# 用法
在要使用uni-config-center的公共模块或云函数内引入uni-config-center依赖请参考[使用公共模块](https://uniapp.dcloud.net.cn/uniCloud/cf-common)
```js
const createConfig = require('uni-config-center')
const uniIdConfig = createConfig({
pluginId: 'uni-id', // 插件id
defaultConfig: { // 默认配置
tokenExpiresIn: 7200,
tokenExpiresThreshold: 600,
},
customMerge: function(defaultConfig, userConfig) { // 自定义默认配置和用户配置的合并规则,不设置的情况侠会对默认配置和用户配置进行深度合并
// defaudltConfig 默认配置
// userConfig 用户配置
return Object.assign(defaultConfig, userConfig)
}
})
// 以如下配置为例
// {
// "tokenExpiresIn": 7200,
// "passwordErrorLimit": 6,
// "bindTokenToDevice": false,
// "passwordErrorRetryTime": 3600,
// "app-plus": {
// "tokenExpiresIn": 2592000
// },
// "service": {
// "sms": {
// "codeExpiresIn": 300
// }
// }
// }
// 获取配置
uniIdConfig.config() // 获取全部配置注意uni-config-center内不存在对应插件目录时会返回空对象
uniIdConfig.config('tokenExpiresIn') // 指定键值获取配置返回7200
uniIdConfig.config('service.sms.codeExpiresIn') // 指定键值获取配置返回300
uniIdConfig.config('tokenExpiresThreshold', 600) // 指定键值获取配置如果不存在则取传入的默认值返回600
// 获取文件绝对路径
uniIdConfig.resolve('custom-token.js') // 获取uni-config-center/uni-id/custom-token.js文件的路径
// 引用文件require
uniIDConfig.requireFile('custom-token.js') // 使用require方式引用uni-config-center/uni-id/custom-token.js文件。文件不存在时返回undefined文件内有其他错误导致require失败时会抛出错误。
// 判断是否包含某文件
uniIDConfig.hasFile('custom-token.js') // 配置目录是否包含某文件true: 文件存在false: 文件不存在
```

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
{
"name": "uni-config-center",
"version": "0.0.3",
"description": "配置中心",
"main": "index.js",
"keywords": [],
"author": "DCloud",
"license": "Apache-2.0"
}

View File

@@ -0,0 +1,36 @@
## 1.0.182024-07-08
- checkToken时如果传入的token为空则返回uni-id-check-token-failed错误码以便uniIdRouter能正常跳转
## 1.0.172024-04-26
- 兼容uni-app-x对客户端uniPlatform的调整uni-app-x内uniPlatform区分app-android、app-ios
## 1.0.162023-04-25
- 新增maxTokenLength配置用于限制数据库用户记录token数组的最大长度
## 1.0.152023-04-06
- 修复部分语言国际化出错的Bug
## 1.0.142023-03-07
- 修复 admin用户包含其他角色时未包含在token的Bug
## 1.0.132022-07-21
- 修复 创建token时未传角色权限信息生成的token不正确的bug
## 1.0.122022-07-15
- 提升与旧版本uni-id的兼容性补充读取配置文件时回退平台app-plus、h5但是仍推荐使用新平台名进行配置app、web
## 1.0.112022-07-14
- 修复 部分情况下报`read property 'reduce' of undefined`的错误
## 1.0.102022-07-11
- 将token存储在用户表的token字段内与旧版本uni-id保持一致
## 1.0.92022-07-01
- checkToken兼容token内未缓存角色权限的情况此时将查库获取角色权限
## 1.0.82022-07-01
- 修复clientDB默认依赖时部分情况下获取不到uni-id配置的Bug
## 1.0.72022-06-30
- 修复config文件不合法时未抛出具体错误的Bug
## 1.0.62022-06-28
- 移除插件内的数据表schema
## 1.0.52022-06-27
- 修复使用多应用配置时报`Cannot read property 'appId' of undefined`的Bug
## 1.0.42022-06-27
- 修复使用自定义token内容功能报错的Bug [详情](https://ask.dcloud.net.cn/question/147945)
## 1.0.22022-06-23
- 对齐旧版本uni-id默认配置
## 1.0.12022-06-22
- 补充对uni-config-center的依赖
## 1.0.02022-06-21
- 提供uni-id token创建、校验、刷新接口简化旧版uni-id公共模块

View File

@@ -0,0 +1,85 @@
{
"id": "uni-id-common",
"displayName": "uni-id-common",
"version": "1.0.18",
"description": "包含uni-id token生成、校验、刷新功能的云函数公共模块",
"keywords": [
"uni-id-common",
"uniCloud",
"token",
"权限"
],
"repository": "https://gitcode.net/dcloud/uni-id-common",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": ["uni-config-center"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
# uni-id-common
文档请参考:[uni-id-common](https://uniapp.dcloud.net.cn/uniCloud/uni-id-common.html)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
{
"name": "uni-id-common",
"version": "1.0.18",
"description": "uni-id token生成、校验、刷新",
"main": "index.js",
"homepage": "https:\/\/uniapp.dcloud.io\/uniCloud\/uni-id-common.html",
"repository": {
"type": "git",
"url": "git+https:\/\/gitee.com\/dcloud\/uni-id-common.git"
},
"author": "DCloud",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:..\/..\/..\/..\/..\/uni-config-center\/uniCloud\/cloudfunctions\/common\/uni-config-center"
},
"origin-plugin-dev-name": "uni-id-common",
"origin-plugin-version": "1.0.18",
"plugin-dev-name": "uni-id-common",
"plugin-version": "1.0.18"
}

36
tsconfig.json Normal file
View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": false,
"jsx": "preserve",
"importHelpers": true,
"experimentalDecorators": true,
"strictFunctionTypes": false,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"baseUrl": ".",
"allowJs": false,
"resolveJsonModule": true,
"lib": ["ESNext", "DOM"],
"paths": {
"~/*": ["src/*"],
"@build/*": ["build/*"]
},
"types": ["@dcloudio/types", "vite/client"]
},
"include": [
"mock/*.ts",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/*.d.ts",
"vite.config.ts"
],
"exclude": ["dist", "**/*.js", "node_modules"]
}

95
uno.config.ts Normal file
View File

@@ -0,0 +1,95 @@
import type { Preset, SourceCodeTransformer } from 'unocss'
import {
defineConfig,
presetAttributify,
presetIcons,
presetUno,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
import {
presetApplet,
presetRemRpx,
transformerAttributify,
} from 'unocss-applet'
const isApplet = process.env?.UNI_PLATFORM?.startsWith('mp-') ?? false
const presets: Preset[] = []
const transformers: SourceCodeTransformer[] = []
if (isApplet) {
presets.push(presetApplet())
presets.push(presetRemRpx())
transformers.push(transformerAttributify({ ignoreAttributes: ['block'] }))
}
else {
presets.push(presetUno())
presets.push(presetRemRpx({ mode: 'rpx2rem' }))
}
const courseColors = [
'rose',
'pink',
'fuchsia',
'purple',
'violet',
'indigo',
'blue',
'cyan',
'teal',
'emerald',
'green',
'lime',
'yellow',
'amber',
'orange',
'red',
]
export default defineConfig({
shortcuts: {
'bg-base': 'bg-gray-100 dark:bg-dark',
'bg-base-second': 'bg-white dark:bg-dark-100',
'color-base': 'text-gray-700 dark:text-white/80',
'color-base-second': 'text-gray-400 dark:text-gray-500/50',
'border-base': 'border border-gray-200 dark:border-gray/50',
'bg-primary': 'bg-light-blue-500 dark:bg-light-blue-600/80',
},
presets: [
presetIcons({
scale: 1.2,
extraProperties: {
'display': 'inline-block',
'vertical-align': 'middle',
},
}),
/**
* you can add `presetAttributify()` here to enable unocss attributify mode prompt
* although preset is not working for applet, but will generate useless css
*/
presetAttributify(),
...presets,
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
...transformers,
],
rules: [
[
'p-safe',
{
padding:
'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)',
},
],
['pt-safe', { 'padding-top': 'env(safe-area-inset-top)' }],
['pb-safe', { 'padding-bottom': 'env(safe-area-inset-bottom)' }],
],
safelist: [
...courseColors.map(c => `bg-${c}`),
...courseColors.map(c => `bg-${c}-3`),
...courseColors.map(c => `text-${c}-5`),
],
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

43
vite.config.ts Normal file
View File

@@ -0,0 +1,43 @@
import path from 'path'
import { defineConfig } from 'vite'
import uniModule from '@dcloudio/vite-plugin-uni'
import AutoImport from 'unplugin-auto-import/vite'
// @ts-expect-error missing types
const Uni = uniModule.default || uniModule
// https://vitejs.dev/config/
export default defineConfig(async () => {
const UnoCss = await import('unocss/vite').then(i => i.default)
return {
root: process.cwd(),
resolve: {
alias: {
'~/': `${path.resolve(__dirname, 'src')}/`,
'react': 'preact/compat',
'react-dom': 'preact/compat',
'preact': 'preact',
'preact/compat': 'preact/compat',
},
},
plugins: [
Uni(),
// https://github.com/antfu/unocss
// see unocss.config.ts for config
UnoCss(),
// https://github.com/antfu/unplugin-auto-import
AutoImport({
imports: ['vue', 'pinia', 'uni-app'],
dts: 'src/auto-imports.d.ts',
dirs: ['src/composables', 'src/stores'],
vueTemplate: true,
}),
],
optimizeDeps: {
include: ['preact', 'preact/compat'],
},
// 如果你需要更详细的调试输出,可以启用以下选项
logLevel: 'info',
}
},
)