Update some files.

This commit is contained in:
skong 2024-01-13 15:48:52 +08:00
parent b885393f3a
commit 783260b334
19 changed files with 6352 additions and 6 deletions

20
.eslintrc.cjs Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

20
LICENSE
View File

@ -1,9 +1,21 @@
MIT License
Copyright (c) 2024 skong
Copyright (c) 2023 Fromsko
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,3 +1,65 @@
# good-frontend
# **课表查询**
好看的前端模板-自研( chakra-ui + react + emotion )
<div align="center">
## <img src="./src/assets/logo.png" height="80" style="border-radius: 50%;"/>
📅 **吉首大学课表查询**
🛠️ 简化查询流程
---
</div>
## 📑 功能特点
- 自动展示本周课表信息
- 可以直接下载
- 响应式布局
## 🚀 快速开始
1. 填写后端地址 `./src/components/CnameData.jsx`
```jsx
export const ApiURL = 'http://1.117.154.114:20000'
```
2. 📦 安装并启动应用
```bash
cd frontend
npm i
npm run dev
```
3. 展示
- `Loading`
<div align="center">
<img src="./src/assets/loading.png" height=""/>
</div>
* `Running`
<div align="center">
<img src="./src/assets/app-running.png" height=""/>
</div>
## 🔗 访问
- **请求地址:** `http://host:prot`
## 🙏 鸣谢
感谢以下开源项目,它们为本项目的开发提供了重要支持:
- [React](https://github.com/facebook/react): 一个 `js` 框架
- [chakra-ui](https://github.com/chakra-ui/chakra-uichakra-ui): 🌐 用于构建用户界面
- [Vite](https://github.com/vitejs/vite): 🚀 下一代前端工具,它的速度很快!
## ©️ 许可
本项目基于 MIT 许可证,请查阅 LICENSE 文件以获取更多信息。

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>课表查询</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

5693
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "small-web",
"private": true,
"version": "5.2.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.1",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@vue/runtime-core": "^3.3.4",
"axios": "^1.5.1",
"framer-motion": "^10.16.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.47.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"vite": "^4.4.11"
}
}

75
src/App.jsx Normal file
View File

@ -0,0 +1,75 @@
import {
Box,
ChakraProvider,
Divider,
Flex,
SimpleGrid
} from '@chakra-ui/react'
import React, { useState } from 'react'
import HeartLoading from './components/HeartLoading'
import { Navigation } from './components/Navigation'
import SendChoice from './components/SendChoice'
import TypingText from './components/TypingText'
function App () {
const [typingDone, setTypingDone] = useState(false)
const Load = () => {
setTypingDone(true)
}
return (
<ChakraProvider>
<Flex direction="column" align="center" justify="center" h="100vh">
{/* Loading 动画和打字效果 */}
<Flex direction="column" align="center">
{!typingDone && (
<Flex align="center" justify="center" h="20vh">
<HeartLoading />
</Flex>
)}
{!typingDone && (
<Flex align="center">
<TypingText
text="追风赶月莫停留,平芜尽处是春山。"
speed={100}
onFinish={Load}
timeOut={1500}
isclear={true}
/>
</Flex>
)}
</Flex>
{typingDone && (
<Box
bg="rgba(255, 255, 255, 0.1)"
borderRadius="8px"
p="4"
w={['100%', '80%', '60%']}
mx="auto" //
boxShadow="0px 2px 4px rgba(0, 0, 0, 0.1)" //
>
<SimpleGrid columns={1} spacing={4} alignItems="center">
{/* 状态栏组件 */}
<Box bg="rgba(255, 255, 255, 0.2)" p="2" borderRadius="8px">
<Navigation />
</Box>
{/* 选择和发送组件 */}
<SendChoice />
{/* 分割线 */}
<Divider />
{/* 备案信息 */}
{/* <Text align="center">xx备xxx号</Text> */}
</SimpleGrid>
</Box>
)}
</Flex>
</ChakraProvider>
)
}
export default App

BIN
src/assets/app-running.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
src/assets/loading.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,112 @@
import { Box, Table, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'
import axios from 'axios'
import TypingText from './TypingText'
export const ApiURL = "http://localhost:2000"
//
function getWeekly () {
const now = new Date()
const weekday = now.getDay()
const weekdayMap = {
1: "星期一",
2: "星期二",
3: "星期三",
4: "星期四",
5: "星期五",
6: "星期六",
0: "星期日",
}
return weekdayMap[weekday]
}
//
export function GetWeek (startMon) {
const now = new Date()
const startOfYear = new Date(now.getFullYear(), 0, 1)
const days = Math.floor((now - startOfYear) / (24 * 60 * 60 * 1000))
const weeks = Math.ceil((days + startOfYear.getDay() + 1) / 7)
return (weeks - startMon).toString()
}
//
export async function GetCnameData () {
const week = GetWeek(36)
const weekly = getWeekly()
let result = ""
let fetchUrl = `${ApiURL}/api/v1/get_cname_data?week=${week}`
try {
const response = await axios.get(fetchUrl)
const data = response
console.log(data)
//
if (data.status === 200) {
//
for (const [key, value] of Object.entries(data.data.课程信息.课程数据[weekly])) {
if (value !== "没课哟") {
result += `${key} ${value.课程名 || ""} ${value.老师.split('(')[0] || ""} ${value.教室 || ""}\n`
}
}
return result
}
} catch (error) {
console.error("Error fetching data:", error)
return result
}
console.error("课表数据获取失败!")
return result
}
export const CnameTable = ({ data }) => {
if (data === "") {
return (<Box>
<TypingText
text="一天都没有课呢,好好休息休息吧!"
speed={100}
onFinish={() => { }}
timeOut={1000}
/>
</Box>)
}
return (
<Box>
<Table variant="simple">
<Thead>
<Tr>
<Th textAlign="center">上课节次</Th>
<Th textAlign="center">课程名</Th>
<Th textAlign="center">老师</Th>
<Th textAlign="center">班级</Th>
</Tr>
</Thead>
<Tbody fontSize={"3xs"} whiteSpace="nowrap">
{data.map((item, index) => (
<Tr key={index}>
<Td>{item.time}</Td>
<Td>{item.course}</Td>
<Td>{item.teacher}</Td>
<Td>{item.class}</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
)
}
export const ParseCnameData = (data) => {
if (data === "") {
return data
}
const lines = data.split('\n').filter(Boolean)
return lines.map((line) => {
const [time, course, teacher, classInfo] = line.split(' ')
return { time, course, teacher, class: classInfo }
})
}

View File

@ -0,0 +1,36 @@
import React from 'react'
import { Box } from '@chakra-ui/react'
const HeartLoading = () => {
const heartStyle = {
position: 'relative',
width: '100px',
height: '90px',
left: '10px',
top: '10px',
animation: 'heart infinite 2s linear',
}
const heartBeforeAfterStyle = {
position: 'absolute',
top: '0',
left: '30px',
width: '30px',
height: '50px',
content: '""',
transform: 'rotate(-45deg)',
transformOrigin: '0 100%',
borderRadius: '30px 30px 0 0',
background: 'pink',
}
return (
<Box style={heartStyle} className="loading">
<Box style={heartBeforeAfterStyle}></Box>
<Box style={{ ...heartBeforeAfterStyle, left: '0', transform: 'rotate(45deg)', transformOrigin: '100% 100%' }}></Box>
</Box>
)
}
export default HeartLoading

View File

@ -0,0 +1,54 @@
import React, { useState } from 'react'
import { Flex, Text, Image, Box } from '@chakra-ui/react'
export const Navigation = () => {
const [isHovered, setIsHovered] = useState(false)
const logoStyle = {
height: '5em', // Logo
willChange: 'filter',
transition: 'filter 300ms',
borderRadius: '60%',
filter: isHovered ? 'drop-shadow(0 0 2em #61dafbaa)' : 'none',
}
const containerStyle = {
backgroundColor: isHovered ? '#f0f0f0' : 'transparent', //
margin: 'auto', //
borderRadius: '20px',
width: '100%', //
transition: 'background-color 300ms', //
textAlign: 'center', //
}
const textStyle = {
color: isHovered ? '#333' : '#000', //
transition: 'color 300ms', //
}
return (
<Box style={containerStyle} borderRadius="50px" overflow="hidden">
<Flex
align="center"
justify="space-between"
p="3"
height="60px" //
boxShadow={isHovered ? 'lg' : 'none'}
transition="box-shadow 300ms"
>
<Flex
align="center"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<a href="https://github.com/Fromsko/JishouSchedule" target="_blank">
<Image src="/src/assets/logo.png" style={logoStyle} ml="3" />
</a>
<Text fontSize="xl" noOfLines={1} lineHeight="2" verticalAlign="middle" ml="4" style={textStyle}>
吉首大学课表查询
</Text>
</Flex>
</Flex>
</Box>
)
}

View File

@ -0,0 +1,28 @@
// PeriodSelector.js
import { Menu, MenuButton, MenuList, MenuItem, Button } from '@chakra-ui/react'
import { ChevronDownIcon } from '@chakra-ui/icons'
import React, { useState } from 'react'
export const PeriodSelector = ({ onSelect, selectedPeriod }) => {
const [isOpen, setIsOpen] = useState(false)
const handleSelect = (period) => {
onSelect(period)
setIsOpen(false)
}
return (
<Menu isOpen={isOpen} onClose={() => setIsOpen(false)}>
<MenuButton as={Button} rightIcon={<ChevronDownIcon />} onClick={() => setIsOpen(!isOpen)}>
{selectedPeriod ? `${selectedPeriod}` : '选择周期'}
</MenuButton>
<MenuList>
{[...Array(20)].map((_, index) => (
<MenuItem key={index + 1} onClick={() => handleSelect(index + 1)}>
{index + 1}
</MenuItem>
))}
</MenuList>
</Menu>
)
}

View File

@ -0,0 +1,107 @@
import axios from 'axios'
import React, { useState, useEffect } from 'react'
import { Flex, Button, Card, Image, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, useDisclosure } from '@chakra-ui/react'
import { PeriodSelector } from './PeriodSelector'
import { GetCnameData, GetWeek, ApiURL, CnameTable, ParseCnameData } from './CnameData'
import TypingText from './TypingText'
//
const handleSend = async (WeeklyChoice, ImgStatus) => {
try {
const response = await axios.get(`${ApiURL}/api/v1/get_cname_table?week=${WeeklyChoice}`, {
responseType: 'arraybuffer',
})
const blob = new Blob([response.data], { type: 'image/png' })
ImgStatus(URL.createObjectURL(blob))
} catch (error) {
console.error('Error fetching schedule image:', error)
}
}
const SendChoice = () => {
const week = GetWeek(36)
const [Selected, setSelected] = useState(null) //
const [ImgStatus, setImgStatus] = useState(null) //
const [cnameData, setCnameData] = useState(null) //
const [loading, setLoading] = useState(true) // loading
const { isOpen, onOpen, onClose } = useDisclosure() //
useEffect(() => {
const fetchData = async () => {
try {
const data = await GetCnameData()
setCnameData(data)
} finally {
setLoading(false) // loading false
}
}
fetchData()
setSelected(week)
}, [])
useEffect(() => {
//
if (Selected !== null) {
handleSend(Selected, setImgStatus)
}
}, [Selected])
const handleStore = () => {
//
const link = document.createElement('a')
link.href = ImgStatus
link.download = `${Selected}周课表.png`
link.click()
onClose() //
}
return (
<Flex direction="column" align="center">
{/* Period Selector */}
<PeriodSelector onSelect={setSelected} selectedPeriod={Selected} />
{ImgStatus && (
<Card maxW={['100%', '500px']} mt="3" onClick={onOpen} cursor="pointer">
<Image src={ImgStatus} alt="课表" borderRadius="lg" />
</Card>
)}
<Flex direction="column" align="start" mt="5">
{loading ? (
<TypingText
text="Loading"
speed={100}
onFinish={() => { }}
timeOut={500}
/>
) : (
cnameData !== null && (
<CnameTable data={ParseCnameData(cnameData)} />
)
)}
</Flex>
{/* Modal */}
< Modal isOpen={isOpen} onClose={onClose} >
<ModalOverlay />
<ModalContent>
<ModalHeader>放大预览</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Image src={ImgStatus} alt="课表" borderRadius="lg" width="100%" height="auto" />
</ModalBody>
<Button colorScheme="blue" mt="4" onClick={handleStore}>
保存
</Button>
</ModalContent>
</Modal >
</Flex>
)
}
export default SendChoice

View File

@ -0,0 +1,51 @@
import React, { useEffect, useState } from 'react'
import { Text } from '@chakra-ui/react'
const TypingText = ({ text, speed, onFinish, timeOut, isclear }) => {
const [displayText, setDisplayText] = useState('')
useEffect(() => {
let index = 0
const intervalId = setInterval(() => {
setDisplayText((prev) => {
if (text[index] === undefined) {
return text
}
return prev + text[index]
})
index++
if (index === text.length) {
clearInterval(intervalId)
// onFinish
setTimeout(() => {
if (isclear) {
setDisplayText('')
}
onFinish()
}, timeOut) // timeOut displayText
}
}, speed)
//
return () => clearInterval(intervalId)
}, [text, speed, onFinish, timeOut])
return (
<Text
sx={{
fontFamily: 'monospace', // 使
background: 'linear-gradient(to right, rgba(238, 174, 202, 1), rgba(148, 187, 233, 1), rgba(179, 229, 252, 1))',
backgroundSize: '200% 100%',
animation: `rainbow ${text.length * speed}ms linear infinite`,
whiteSpace: 'pre',
borderRadius: '4px', //
padding: '8px', //
fontWeight: 'bold', //
}}
>
{displayText}
</Text>
)
}
export default TypingText

10
src/main.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

12
vite.config.js Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',//ip地址
port: 80, // 设置服务启动端口号
open: false, // 设置服务启动时是否自动打开浏览器
}
})