特意去github找了一個用flask框架的項目,一起來學習它吧
這個系統包括很多功能:用戶權限管理模塊(管理員和普通用戶),注冊登錄模塊(滑塊驗證碼功能),圖書有關信息模塊(借閱,收藏,詳情),留言板模塊,用戶畫像和個性化推薦模塊
哇!真的是一個寶藏項目,一定要吃透它
我打算先前端,再后端
1.首先是起始頁
有三個按鈕,前端代碼在my-app/src/beforeLogin.js里面
import { Form, Button, Layout, Menu, theme, Dropdown, Typography, Space, Table, Modal, Input, Card, Row, Col, Switch, Pagination } from "antd";
import { Link, Navigate, useNavigate, Outlet } from "react-router-dom";
import { LaptopOutlined, UserOutlined, BookOutlined, AppstoreOutlined, ExclamationCircleFilled } from '@ant-design/icons';
import React, { useState, useEffect } from 'react';
import axios from 'axios'
import { Detail } from './bookdata'可以看到他引入了一些ant design里面的一些組件
引入了一些React Router的組件和鉤子(他用的是react18)
Link:創建導航鏈接。
Navigate:渲染時進行重定向。
useNavigate:編程式導航。
Outlet:嵌套路由內容的占位符。
引入axios來進行調用接口
Detail 是什么呢?是來自bookdata的一個組件,可以點擊跳轉過去看具體的代碼
const Detail = (props)=>{let detail = props.dataconst [isModalOpen, setIsModalOpen] = useState(false);const showModal = () => {setIsModalOpen(true);};const handleOk = () => {setIsModalOpen(false);};const handleCancel = () => {setIsModalOpen(false);};return(<><Button type="primary" onClick={showModal}>詳情</Button><Modal open={isModalOpen} onOk={handleOk} onCancel={handleCancel} okText="確認" cancelText="取消"><Descriptions title="圖書詳細信息" bordered><Descriptions.Item label="圖書名稱" span={2}>{detail.name}</Descriptions.Item><Descriptions.Item label="圖書作者">{detail.author}</Descriptions.Item><Descriptions.Item label="出版社" span={2}>{detail.publish}</Descriptions.Item><Descriptions.Item label="ISBN">{detail.isbn}</Descriptions.Item><Descriptions.Item label="價格" span={2}>{detail.price}元</Descriptions.Item><Descriptions.Item label="剩余數量">{detail.number}</Descriptions.Item><Descriptions.Item label="內容簡介" span={3}>{detail.intro}</Descriptions.Item><Descriptions.Item label="出版日期">{detail.pubdate}</Descriptions.Item><Descriptions.Item label="類別">{detail.type}</Descriptions.Item></Descriptions></Modal></>)
}
這段代碼定義了一個名為 Detail 的 React 函數組件,用于展示圖書的詳細信息。當用戶點擊按鈕時,會彈出一個模態框(Modal)顯示圖書的詳細信息。
2.進入隨便逛逛,可以看到下面的頁面
在登錄前可以預覽圖書的圖文信息
對了,我們該如何找到這個頁面對應的代碼在哪里呢,看網頁的路徑:http://127.0.0.1:5000/preview/books
然后去App.js里面找<Route path="/preview" element={<FrameForAll />}><Route path="/preview/books" element={<BookPreview />} /><Route path="/preview/hotRanking" element={<PreviewHotRanking />} /></Route>
然后直接點擊跳轉,按住ctrl+點擊(vscode中)
還是beforeLogin.js,不過是其中的BookPreview 組件const BookPreview = ()=>{const [bookData, setBookData] = useState([])const [savedata, setSaveData] = useState([])const [conponentshowstatus, setConponentShowStatus] = useState(true)const [current, setCurrent] = useState(1);const [pageSize, setPageSize] = useState(10);const {token: { colorBgContainer },} = theme.useToken();const baseUrl = 'http://127.0.0.1:5000/bookdata'useEffect(() => {// console.log('effect')axios.get(baseUrl).then(response => {const data = response.data// console.log(data)setBookData(data)setSaveData(data)})}, [])//只在第一次渲染時運行const handleChange = (e) => {const values = e.target.value// console.log(values)if (values) {// 這里保存一個原始數據,便于反復查找使用const filterdata = savedata.filter(item => {// console.log(item.value)return item.name.includes(values)})// console.log(filterdata)setBookData(filterdata)} else {setBookData(savedata)}}const onSwitchChange = (checked) => {setConponentShowStatus(!checked)}const onPageChange = (page) => {// console.log(page);setCurrent(page);};const handleShowSizeChange = (current, size) => {const newPage = Math.floor(start / size) + 1;// console.log(newPage,size)setPageSize(size);setCurrent(newPage);};// 對數據切片const start = (current - 1) * pageSize;const end = start + pageSize;const currentData = bookData.slice(start, end);return(<>{/* <p>此為預覽頁面,僅提供基本功能展示。若想體驗完整功能,請先注冊并登錄!</p> */}<Search placeholder="輸入書名" onChange={handleChange} enterButton style={{ width: 200, }} /><Switch checkedChildren="圖片版" unCheckedChildren="文字版" onChange={onSwitchChange} className="switch" /><br/><br />{/* 圖片組件和文字組件 */}{conponentshowstatus ?<><Row gutter={[8, 16]}>{currentData.map((item) => {return (<Col span={6} key={item.id}><Cardhoverablestyle={{width: 300,}}cover={<img alt="example" src={"http://127.0.0.1:5000/images/" + item.isbn + ".jpg"} />}actions={[<FakeBorrowCollect name="借閱"/>,<Detail data={item} />,<FakeBorrowCollect name="收藏" />]}><Meta title={item.name} description={item.author} /></Card></Col>)})}</Row><br /><Pagination className="pagination" current={current} showSizeChanger onShowSizeChange={handleShowSizeChange} onChange={onPageChange} total={bookData.length} /></>:<Table columns={columns} dataSource={bookData} locale={{ emptyText: '暫無數據' }} />}</>)
}
?一點點看,首先我們要搞清楚圖書列表是怎么回事
1)首先是展示,頁面這么多書的數據是從何而來,看下面的代碼數據應該是來自于下面的代碼
useEffect(() => {axios.get(baseUrl).then(response => {const data = response.data;setBookData(data);setSaveData(data);});
}, []);
然后設置? : 實現文字版和圖片版的切換(根據 conponentshowstatus 狀態決定顯示圖片版(用 Card 組件展示圖書信息)還是文字版(用 Table 組件展示圖書信息))2)再說下搜索功能const handleChange = (e) => {const values = e.target.value// console.log(values)if (values) {// 這里保存一個原始數據,便于反復查找使用const filterdata = savedata.filter(item => {// console.log(item.value)return item.name.includes(values)})// console.log(filterdata)setBookData(filterdata)} else {setBookData(savedata)}}
setBookData 更新 bookData 狀態變量,bookData 用于存儲和顯示當前過濾后的圖書數據。
setSaveData 更新 savedata 狀態變量,savedata 用于存儲從服務器獲取的原始圖書數據,不直接顯示,但用于搜索和過濾操作。3)再說下分頁
const [current, setCurrent] = useState(1); // 當前頁碼
const [pageSize, setPageSize] = useState(10); // 每頁顯示的條目數
const onPageChange = (page) => {setCurrent(page); // 更新當前頁碼
};const handleShowSizeChange = (current, size) => {setPageSize(size); // 更新每頁顯示的條目數setCurrent(1); // 更新 pageSize 時重置到第一頁
};
4)最后是前面說到的detail功能(詳情展示)
點擊詳情出來一個彈窗
const Detail = (props)=>{let detail = props.dataconst [isModalOpen, setIsModalOpen] = useState(false);const showModal = () => {setIsModalOpen(true);};const handleOk = () => {setIsModalOpen(false);};const handleCancel = () => {setIsModalOpen(false);};return(<><Button type="primary" onClick={showModal}>詳情</Button><Modal open={isModalOpen} onOk={handleOk} onCancel={handleCancel} okText="確認" cancelText="取消"><Descriptions title="圖書詳細信息" bordered><Descriptions.Item label="圖書名稱" span={2}>{detail.name}</Descriptions.Item><Descriptions.Item label="圖書作者">{detail.author}</Descriptions.Item><Descriptions.Item label="出版社" span={2}>{detail.publish}</Descriptions.Item><Descriptions.Item label="ISBN">{detail.isbn}</Descriptions.Item><Descriptions.Item label="價格" span={2}>{detail.price}元</Descriptions.Item><Descriptions.Item label="剩余數量">{detail.number}</Descriptions.Item><Descriptions.Item label="內容簡介" span={3}>{detail.intro}</Descriptions.Item><Descriptions.Item label="出版日期">{detail.pubdate}</Descriptions.Item><Descriptions.Item label="類別">{detail.type}</Descriptions.Item></Descriptions></Modal></>)
}
5)整體布局方面(FrameForAll)
使用了 Ant Design 的 Layout 組件來創建一個包含 Header、Sider、Content 和 Footer 的布局。
主頁面布局 (Layout):
最外層的 Layout 組件包含整個頁面的布局。
頭部 (Header):
使用了 Ant Design 的 Header 組件,類名為 header。
包含一個 Title 元素和一個自定義的 LoginRegisterButton 組件。
Title 組件使用 level={3} 和 type="success" 來設置其樣式。
側邊欄 (Sider):
寬度設置為 200 像素,背景色來自主題的 colorBgContainer。
包含一個 Menu 組件,設置了 inline 模式,默認選中的鍵為 ['1'],默認打開的子菜單為 ['sub1']。
主要內容區域 (Layout):
內部 Layout 設置了 padding 和 margin,確保內容區域有適當的間距。
包含 Content 和 Footer 兩個子組件。
內容區域 (Content):
設置了 padding 和 margin,最小高度為 280 像素,背景色來自主題的 colorBgContainer,確保內容區域有滾動條(overflow: 'auto')。
Outlet 組件用于顯示嵌套路由的內容。
頁腳 (Footer):
設置了居中對齊 (textAlign: 'center'),顯示版權信息。
非常好
然后是熱門排行頁面
const PreviewHotRanking = () => {return (<><HotBorrow /><NewBook /><HotCollect /></>)
}
看來是分三個組件
一個個看
1)HotBorrow
簡單的調取接口,獲取數據,展示
使用了 Ant Design 的 List 組件來顯示熱門借閱的圖書數據
組件結構:
Title 組件顯示 "熱門借閱" 標題。
List 組件用于顯示圖書列表。
itemLayout="vertical" 設置列表項為垂直布局。
size="small" 設置列表項大小為小。
dataSource={bookdata} 設置列表的數據源為狀態變量 bookdata。
footer 屬性用于顯示列表底部的額外內容。
renderItem 屬性用于自定義列表項的渲染。
2)NewBook HotCollect 和HotBorrow 也差不多一個道理,換了個布局
NewBook
網格布局 (Row 和 Col):
使用 Ant Design 的 Row 組件創建水平網格布局。
gutter={[16, 24]} 設置了列之間的水平間距和行之間的垂直間距。
使用 Col 組件來創建每個圖書卡片的列。
span={8} 設置每個圖書卡片列的寬度占比。
圖書卡片 (Card):
使用 Card 組件包裹每個圖書。
設置 hoverable 屬性,使得鼠標移動到卡片上時有浮動效果。
設置 cover 屬性,用于顯示圖書的封面圖片。
actions 屬性定義了操作按鈕,包括借閱、詳情和收藏。
使用 Meta 組件來顯示圖書的標題和作者。
3)HotCollect
同HotBorrow
隨便逛逛差不多了,但是還有很多沒有逛的,比如說借閱,收藏,管理員權限都需要登錄才行
我們進入下一部分吧
3.注冊功能

registerForm.js
const RegisterForm = () => {const [form] = Form.useForm();const [isAlertShow, setAlertShow] = useState(false)const [isRegister,setRegister] = useState(false)// 注冊post函數const ifRegister = async res => {const response = await axios.post(baseUrl, res)// console.log('response.data:',response.data)return response.data}const onFinish = async (values) => {console.log('Received values of form: ', values);try{const res1 = await ifRegister(values)// console.log('res1:', res1)setRegister(true)}catch(exception){// console.log(exception)setAlertShow(true)setTimeout(() => { setAlertShow(false) }, 3000)}};const prefixSelector = (<Form.Item name="prefix" noStyle><Selectstyle={{width: 70,}}><Option value="86">+86</Option><Option value="87">+87</Option></Select></Form.Item>);return (<>{isAlertShow ? <AlertclassName='registeralert'message="Error"description="用戶名重復!"type="error"showIconclosable/> : ''}{isRegister?<Resultstatus="success"title="注冊成功!"extra={[<Link to="/login"><Button type="primary" key="console">前往登錄</Button></Link>,]}/>:<Form{...formItemLayout}form={form}className="registerform"name="register"onFinish={onFinish}initialValues={{prefix: '86',}}scrollToFirstError><Form.Itemname="password"label="密碼"rules={[{required: true,message: '請輸入你的密碼!',},]}hasFeedback><Input.Password /></Form.Item><Form.Itemname="confirm"label="確認密碼"dependencies={['password']}hasFeedbackrules={[{required: true,message: '請確認你的密碼!',},({ getFieldValue }) => ({validator(_, value) {if (!value || getFieldValue('password') === value) {return Promise.resolve();}return Promise.reject(new Error('兩次輸入的密碼不一致!'));},}),]}><Input.Password /></Form.Item><Form.Itemname="nickname"label="用戶名"tooltip="你想讓其他人如何稱呼你?"rules={[{required: true,message: '請輸入你的用戶名!',whitespace: true,},]}><Input /></Form.Item><Form.Itemname="phone"label="電話號碼"rules={[{required: true,message: '請輸入你的電話號碼!',},]}><InputaddonBefore={prefixSelector}style={{width: '100%',}}/></Form.Item><Form.Itemname="gender"label="性別"rules={[{required: true,message: '請選擇你的性別!',},]}><Select placeholder="選擇你的性別"><Option value="男性">男性</Option><Option value="女性">女性</Option><Option value="其他">其他</Option></Select></Form.Item><Form.Itemname="agreement"valuePropName="checked"rules={[{validator: (_, value) =>value ? Promise.resolve() : Promise.reject(new Error('應當同意服務條款!')),},]}{...tailFormItemLayout}><Checkbox>注冊即代表同意<a href="">服務條款</a></Checkbox></Form.Item><Form.Item {...tailFormItemLayout}><Button type="primary" htmlType="submit">注冊</Button></Form.Item></Form>}</>);
};使用 useState 鉤子來管理是否顯示警告信息和注冊成功的狀態。
定義了一個用于執行注冊操作的異步函數 ifRegister,并在表單提交時調用。
組件結構:
如果 isAlertShow 為 true,則顯示一個錯誤警告信息。
如果 isRegister 為 true,則顯示注冊成功的結果頁面,否則顯示注冊表單。
注冊表單包括密碼、確認密碼、用戶名、電話號碼、性別和服務條款同意復選框等字段。
使用 Ant Design 的 Form 組件包裹表單內容,設置表單的布局、名稱和提交處理函數。
在表單中定義了各種輸入框、密碼框、下拉框和復選框,以及提交按鈕。
我想我們得看一下后端代碼了
1)首先是一個__init__.py文件
from flask import Flask
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
app=Flask('libraryms',template_folder="../templates",static_folder="../static")
app.config.from_pyfile('settings.py')
cors = CORS(app)
db=SQLAlchemy(app)
jwt = JWTManager()
jwt.init_app(app)
from libraryms import views,commands
Flask('libraryms'): 創建一個名為 libraryms 的 Flask 應用實例。
template_folder="../templates": 設置模板文件夾的路徑。
static_folder="../static": 設置靜態文件夾的路徑。
從 settings.py 文件中加載應用程序配置。
啟用跨域資源共享(CORS),允許來自不同域的請求訪問此 Flask 應用程序。
使用 SQLAlchemy 進行數據庫操作并與 Flask 應用集成。
初始化 JWT 身份驗證管理器并與 Flask 應用集成。
導入應用程序的視圖和命令模塊。確保在 libraryms 包中有 views.py 和 commands.py 文件。2)setting.py
import os
from libraryms import app
SQLALCHEMY_DATABASE_URI=os.getenv('DATABASE_URL')
JWT_SECRET_KEY="super-secret"將 SQLALCHEMY_DATABASE_URI 設置為環境變量 os.getenv('DATABASE_URL') 可以幫助保護敏感信息(如數據庫連接字符串)不直接暴露在代碼中。
3)commands.py
為flask build 填充預置數據做準備
4)models.py
為flask initdb 初始化數據庫做準備
5)views.py
接口大集合
這里我們說注冊接口@app.route("/register", methods=["POST"])
@cross_origin()
def register():sth = request.json# print(sth)username = sth['nickname']password = sth['password']gender = sth['gender']phone = sth['phone']
# 判斷用戶名是否重合,普通用戶能注冊,所以檢查普通用戶表res1 = normalusr.query.filter(normalusr.username == username).first()if res1:return jsonify({"msg": "用戶名重復!"}), 401else:# 普通表寫入信息m1 = normalusr(username=username, password=password)db.session.add(m1)db.session.commit()# 信息表填入信息m2 = usrinfo(username=username,tel=phone,sex=gender)db.session.add(m2)db.session.commit()return jsonify({"msg": "注冊成功!","ok":"true"}), 200
很簡單,一目明了,加了一個判斷,巧妙的是他用了models.py里面的
# 普通用戶表
class normalusr(db.Model):nid=db.Column(db.Integer,primary_key=True,nullable=False,autoincrement=True)username=db.Column(db.String(30))password=db.Column(db.String(30))
這就大大方便了以后的編寫,抽象
4.登錄功能

1)先看一下這個滑動驗證碼,之前我做過正常的那種驗證碼,就是輸入數字和字母那種,用的是canvas<SliderCaptcha初始化背景和拼圖圖像request={() =>createPuzzle(DemoImage).then(async (res) => {offsetXRef.current = res.x;await waitTime();return {bgUrl: res.bgUrl,puzzleUrl: res.puzzleUrl};})}驗證滑動位置onVerify={async (data) => {await waitTime();// console.log(data);if (data.x >= offsetXRef.current - 5 && data.x < offsetXRef.current + 5) {setDuration(data.duration);setVisible(true);await waitTime();setResult(true);offsetXRef.current = 0return Promise.resolve();}return Promise.reject();}}顯示滑塊bgSize={{width: 250,height: 110}}mode="float"limitErrorCount={3}jigsawContent={visible && (<div className={"successTip"}>{Number((duration / 1000).toFixed(2))}秒內完成,打敗了98%用戶</div>)}actionRef={actionRef}/>
請求背景和拼圖圖像
SliderCaptcha 組件的 request 屬性定義了獲取背景圖像和拼圖圖像的函數。
createPuzzle(DemoImage) 是模擬生成拼圖的函數,返回背景圖和拼圖的位置。
驗證滑動位置
onVerify 屬性定義了驗證滑動位置的函數。
用戶滑動完成后,將實際滑動位置 data.x 與預期位置 offsetXRef.current 進行比較。如果在誤差范圍內(5個像素),則驗證通過,否則驗證失敗。
顯示滑塊
SliderCaptcha 組件通過 bgUrl 和 puzzleUrl 顯示背景圖和拼圖。
滑塊驗證結果
如果驗證通過,setDuration 和 setVisible 更新滑塊驗證的狀態。
2)然后看看失敗次數限制以及賬戶鎖定功能
登錄函數 onFinish 中的失敗次數處理:
在 onFinish 函數中,如果失敗次數超過2 (failAttempt > 1),會顯示鎖定消息并鎖定賬戶。
鎖定時長設置為31秒(const time = 31 * 1000;),并在本地存儲中記錄鎖定時間。
登錄失敗時,失敗次數增加,并顯示相應警告消息。locked 狀態用于指示賬戶是否被鎖定。
remainingTime 和 lockTime 用于計算和顯示剩余鎖定時間。
useEffect 中的計時器每秒更新一次鎖定狀態,如果鎖定時間到期,則解鎖賬戶并重置失敗次數。
?5.普通用戶功能
登錄進去發現,好多功能,用戶畫像和個性化推薦都有,我們一點點來,先看個人中心
個人中心的基本信息頁面 self.js
const Self = ()=>{const [info,setInfo] = useState({})// 獲取用戶信息const self = window.localStorage.getItem('loggedUser')const res = {"username":self}useEffect(() => {// console.log('self info')axios.get(baseUrl,{params:res}).then(response => {const data = response.data// console.log(data)setInfo(data)// console.log('info',data)})}, [])//只在第一次渲染時運行return(<><Descriptions title="用戶信息" bordered extra={<EditSelf data={info} handleChange={setInfo}/> }><Descriptions.Item label="用戶名" span={3}>{info.username}</Descriptions.Item><Descriptions.Item label="電話號碼" span={3}>{info.tel}</Descriptions.Item><Descriptions.Item label="性別" span={3}>{info.sex}</Descriptions.Item><Descriptions.Item label="簡介" span={3}>{info.intro}</Descriptions.Item></Descriptions><br/><EditPwd/><br/><br/><IdeaRelease/></>);
}
它這個用戶信息在登錄后保存到localstorage中,修改密碼的時候帶過去,然后調用接口直接修改
后端接口有一個判斷來確定是否是管理員,然后查詢不同的表,這也是登陸時為什么有哪個選項的原因
個人用戶 個性化推薦
1)用戶畫像
這里他還寫了一個通用函數
// 通用post函數
const uniPost = async (url, res) => {const response = await axios.post(url, res)// console.log('response.data:',response.data)return response.data
}
好事情
用戶畫像需要著重于后端代碼,讓我們看看他是如何實現的'http://127.0.0.1:5000/userprofile'# 用戶畫像
@app.route('/userprofile', methods=["GET"])
@cross_origin()
def userprofile():sth = request.argsusername = sth['username']tagarray = []bookarray = []# 統計借閱歷史res1 = bookBorrowHistory.query.filter(bookBorrowHistory.borrowusr == username).all()for x in res1:bookarray.append(x.name)# 統計收藏信息res2 = bookCollect.query.filter(bookCollect.username == username).all()for x in res2:res3 = bookitem.query.filter(bookitem.isbn == x.isbn).first()bookarray.append(res3.name)# 統計所有標簽for x in bookarray:res4 = bookitem.query.filter(bookitem.name == x).first()tagarray.append(res4.type)# print(bookarray)# print(tagarray)# 計算出數量最多的3個標簽dict = {}tag = []for x in tagarray:dict[x] = dict.get(x,0)+1# print(dict)if len(dict)>3:while (len(tag)<3):tag.append(max(dict,key=dict.get))dict.pop(max(dict,key=dict.get))else:for key in dict:tag.append(key)# print(tag)return tag
沒有我想的那么復雜,沒有用到模型,只是通過統計來實現生成用戶標簽
統計每個標簽出現的次數,然后選取出現次數最多的前三個標簽作為用戶的標簽信息。2)個性推薦
收前端傳遞的包含用戶標簽信息的請求,根據這些標簽信息生成推薦的圖書清單,并將其作為 JSON 格式的響應返回給前端
根據類比返回罷了
普通用戶心心念的收藏和借閱功能,這里的借閱有些復雜,涉及到還款什么的,先看收藏吧
這里有個問題:圖書列表中已收藏和借閱的書籍在跳轉頁面后再次回到圖書列表頁面會不再顯示已經借閱或者已經收藏(bookdata.js里面的BookList)解決辦法:在圖書列表中保存收藏和借閱的狀態:在獲取圖書列表時,同時獲取每本書籍是否已被當前用戶收藏或借閱的狀態,并將這些狀態存儲在 bookData 中。在圖書列表加載時檢查狀態:在 useEffect 中獲取圖書數據時,額外獲取用戶的收藏和借閱信息,并更新每本書的狀態。在借閱和收藏操作后更新狀態:當用戶進行借閱或收藏操作時,更新 bookData 中對應書籍的狀態。
需要加個狀態,一會加一下1.收藏 collect.js
1)搜索功能
搜索功能通過兩個主要函數實現:onSearch 和 handleChange。
onSearch:
當用戶在搜索框中輸入內容并點擊搜索按鈕時觸發。
該函數接收用戶輸入的值作為參數。
它首先檢查用戶是否輸入了內容,如果輸入了內容,則使用 filter 方法過濾 collectdata 數組,只保留包含用戶輸入內容的項,并將過濾后的結果設置為新的 collectdata。
如果用戶沒有輸入內容,則將 savedata(原始數據)設置回 collectdata,這樣就重新
handleChange:
當用戶在搜索框中輸入內容但不點擊搜索按鈕時觸發(即在輸入內容時實時搜索)。
它與 onSearch 相似,不同之處在于它是實時響應輸入的變化。
它也首先檢查用戶是否輸入了內容。如果輸入了內容,則根據當前選擇的搜索項 selectdata 使用 filter 方法過濾 savedata 數組,并將過濾后的結果設置為新的 collectdata。
如果用戶沒有輸入內容,則將 savedata(原始數據)設置回 collectdata。2)導出功能
首先,將表格數據 collectdata 進行清理,以確保導出的數據格式符合預期。在這里,將每一項的屬性名稱進行映射,并對價格屬性進行調整,添加單位。
使用 XLSX.utils.json_to_sheet 方法將清理后的數據轉換為 Excel 表格的工作表。
創建一個新的工作簿,并將工作表添加到該工作簿中。
使用 XLSX.writeFile 方法將工作簿寫入到名為 "book.xlsx" 的 Excel 文件中。
在導出完成后,使用 Ant Design 的 message 組件顯示導出成功的消息。3)取消收藏功能,收藏功能
CancelCollect 組件接收一個 data 屬性,其中包含了要取消收藏的圖書數據。
組件內部維護了一個 isCollect 狀態,用于表示當前是否已收藏。默認為 true。
當用戶點擊按鈕時,觸發 info 函數,該函數首先獲取當前用戶信息和圖書的 ISBN 號,然后通過 uniPost 函數向后端發送請求,告知服務器取消收藏。
如果取消成功,顯示提示消息,并更新 isCollect 狀態為 false,同時刷新頁面以更新數據。2.借閱功能 borrow.js
1)在 useEffect 鉤子中,組件首次渲染時從后端獲取借閱圖書數據,并將其保存在 borrowdata 狀態中。然后,根據 ischecking 屬性篩選出待審核的借閱申請數據,保存在 applyborrowdata 狀態中,以便顯示在歸還圖書申請的表格中。
表格使用了 borrowColumns 和 applyBorrowColumns,這些列的配置與 collectColumns 類似,用于指定表格中各列的標題和渲染方式。
這段代碼中的主要邏輯是:
通過 axios 發送請求獲取借閱圖書數據。
將數據保存到相應的狀態變量中。
渲染兩個表格,分別用于展示借閱圖書和歸還圖書申請。
2)圖書違約
獲取數據后,計算每本書是否超期及超期天數和罰金:
當前時間戳通過 new Date().getTime() 獲取。
遍歷數據,判斷是否已還書。如果已還書,根據歸還日期計算借閱天數;否則,使用當前時間計算借閱天數。
判斷借閱天數是否超過 31 天,計算超期天數和罰金。
過濾出所有超期的記錄。
后端接口 http://127.0.0.1:5000/defaultdata
使用 Flask 框架定義了一個 API 路由 /defaultdata,接受 GET 請求。
從數據庫中查詢用戶的違約記錄,并返回一個包含違約記錄的列表。
6.留言板功能(這里我單獨寫,是因為感覺不錯,有一個評論下回復的功能,需要記一下以及上傳圖片)但是他只是實現了一個父評論,n個子評論,并沒有無限延伸,比如孫評論,一會加一下
messageBoard.js
1)評論
前端代碼:
MessageBoard 組件:主留言板組件,包含輸入框、表情組件、圖片上傳組件,以及留言和評論顯示。
Comment 組件:遞歸渲染評論和子評論,并處理回復邏輯。
handleReplySubmit 方法:處理子評論的提交,將新回復添加到對應的父評論中,并發送到服務器。
handleClick 方法:處理新的父評論的提交,并發送到服務器。
后端代碼:
get_comments 方法:返回所有評論。
add_comment 方法:根據評論類型(父評論或子評論)將新的評論或回復添加到相應的位置。
2)上傳圖片前端實現:
文件上傳組件:
使用 Ant Design 的 Upload 組件,用戶可以通過點擊按鈕選擇要上傳的圖片文件。
設置 action 屬性為上傳圖片的后端接口地址。
通過 onChange 事件監聽文件上傳狀態的變化,并根據上傳結果進行相應的處理。
上傳圖片處理:用戶選擇圖片后,觸發 handleChange 函數。
在 handleChange 函數中,根據文件上傳狀態(done、uploading、error),對上傳的圖片進行相應的處理。
如果文件上傳成功(status 為 done),從響應中獲取上傳成功的圖片路徑,并將圖片路徑插入到留言內容中,以顯示圖片。
刪除圖片處理:用戶可以點擊已上傳圖片的刪除按鈕,觸發 handleRemovePics 函數。
在 handleRemovePics 函數中,向后端發送刪除圖片的請求,根據后端的響應決定是否刪除前端對應的圖片。
后端實現:
接收上傳圖片請求:后端提供一個接口來接收前端上傳圖片的請求,一般是通過 POST 方法發送圖片數據。
在 Flask 中,可以使用 Flask-Uploads 或直接處理 request.files 來接收上傳的圖片文件。
保存圖片:后端接收到圖片文件后,根據需求將圖片保存到服務器的指定目錄中。
保存成功后,返回圖片的訪問路徑給前端,以便前端顯示上傳成功的圖片。
刪除圖片:后端提供一個接口用于刪除指定的圖片文件,一般是通過 DELETE 方法發送圖片文件名或路徑。
在刪除圖片接口中,根據文件名或路徑定位到要刪除的圖片文件,并刪除它。
7.管理員功能(信息審核與讀者管理)
selfadmin.js
1.讀者管理組件
讀者信息展示:
通過請求 /usrdata 接口獲取讀者信息數據,并將其顯示在表格中。
如果沒有讀者信息,則表格顯示暫無數據。
性別統計分析圖:
通過請求 /usrsexdata 接口獲取讀者性別數據,并將其用餅圖進行可視化展示。
餅圖顯示了不同性別的讀者比例。
圖書收藏信息展示及分析圖:
通過請求 /usrcollectdata 接口獲取讀者收藏信息數據,并將其顯示在表格中。
通過請求 /usrcollectanalysisdata 接口獲取圖書收藏排行數據,并將其用柱狀圖進行可視化展示。
柱狀圖顯示了圖書收藏量排名前十的圖書信息。
圖書借閱信息展示及分析圖:
通過請求 /usrborrowdata 接口獲取讀者借閱信息數據,并將其顯示在表格中。
通過請求 /usrborrowanalysisdata 接口獲取圖書借閱排行數據,并將其用柱狀圖進行可視化展示。
柱狀圖顯示了圖書借閱量排名前十的圖書信息。
前端大同小異,圖片生成可以關注一下
他這里是用接口返回的數據和import { Pie, Column } from '@ant-design/plots';的組件來生成餅狀圖和條狀圖圖片
2.信息審核
bookdata、newbookdata、applyborrowdata:分別用于存儲所有書籍數據、新書數據和待審核的借閱申請數據。
axios.get('http://127.0.0.1:5000/bookdata') 獲取所有書籍數據。
axios.get('http://127.0.0.1:5000/newbookdata') 獲取新書數據。
axios.get('http://127.0.0.1:5000/usrborrowlistdata') 獲取用戶借閱列表數據,并過濾和計算是否超期。