图书管理系统(https://github.com/plusmultiply0/bookmanagesystem)

特意去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.data

    const [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}>
                                    <Card
                                        hoverable
                                        style={{
                                            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.data

    const [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>
            <Select
                style={{
                    width: 70,
                }}
            >
                <Option value="86">+86</Option>
                <Option value="87">+87</Option>
            </Select>
        </Form.Item>
    );

    return (
        <>
            {isAlertShow ? <Alert
                className='registeralert'
                message="Error"
                description="用户名重复!"
                type="error"
                showIcon
                closable
            /> : ''}
            {isRegister?
                <Result
                    status="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.Item
                    name="password"
                    label="密码"
                    rules={[
                        {
                            required: true,
                            message: '请输入你的密码!',
                        },
                    ]}
                    hasFeedback
                >
                    <Input.Password />
                </Form.Item>

                <Form.Item
                    name="confirm"
                    label="确认密码"
                    dependencies={['password']}
                    hasFeedback
                    rules={[
                        {
                            required: true,
                            message: '请确认你的密码!',
                        },
                        ({ getFieldValue }) => ({
                            validator(_, value) {
                                if (!value || getFieldValue('password') === value) {
                                    return Promise.resolve();
                                }
                                return Promise.reject(new Error('两次输入的密码不一致!'));
                            },
                        }),
                    ]}
                >
                    <Input.Password />
                </Form.Item>

                <Form.Item
                    name="nickname"
                    label="用户名"
                    tooltip="你想让其他人如何称呼你?"
                    rules={[
                        {
                            required: true,
                            message: '请输入你的用户名!',
                            whitespace: true,
                        },
                    ]}
                >
                    <Input />
                </Form.Item>

                <Form.Item
                    name="phone"
                    label="电话号码"
                    rules={[
                        {
                            required: true,
                            message: '请输入你的电话号码!',
                        },
                    ]}
                >
                    <Input
                        addonBefore={prefixSelector}
                        style={{
                            width: '100%',
                        }}
                    />
                </Form.Item>

                <Form.Item
                    name="gender"
                    label="性别"
                    rules={[
                        {
                            required: true,
                            message: '请选择你的性别!',
                        },
                    ]}
                >
                    <Select placeholder="选择你的性别">
                        <Option value="男性">男性</Option>
                        <Option value="女性">女性</Option>
                        <Option value="其他">其他</Option>
                    </Select>
                </Form.Item>

                <Form.Item
                    name="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": "用户名重复!"}), 401
    else:
        # 普通表写入信息
        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 = 0
                                return 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.args
    username = 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') 获取用户借阅列表数据,并过滤和计算是否超期。

写不下去了,快吐了,一会改一下上面提出的两个bug吧
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/677904.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Django使用正则表达式

本书1-7章样章及配套资源下载链接: https://pan.baidu.com/s/1OGmhHxEMf2ZdozkUnDkAkA?pwdnanc 源码、PPT课件、教学视频等&#xff0c;可以从前言给出的下载信息下载&#xff0c;大家可以评估一下。 在Django框架的新版本&#xff08;v2.0 &#xff09;中&#xff0c;URLc…

低比特大模型排行版暨AutoRoundV0.2发布

由于大量的量化模型没有精度数据&#xff0c;为了让用户更好地找到适配自己的模型或量化算法&#xff0c;最近推出了低比特大模型排行版&#xff0c;评估的指标主要涵盖10个zero shot的任务,如果有什么建议或者意见可以去社区提~目前支持13B以下模型的评估&#xff0c;后面可能…

2024专精特新趋势论坛,汉王友基分享数字化创新实践之路

5月31日&#xff0c;由深圳市中小企业服务局作为指导单位&#xff0c;36氪主办的“WISE新风向2024专精特新趋势论坛”在粤港澳大湾区顺利举办。 汉王友基作为国家级专精特新“小巨人”企业代表&#xff0c;受邀参加此次大会&#xff0c;企业CTO邓立明先生进行了《数字赋能&…

网易云音乐格式在线转换

应用分享&#xff1a;众所周知网易云下载的格式为 .NCM&#xff0c;只能在网易云音乐里播放。 今天提供在线转换为MP3格式 NCM TO MP3&#xff0c;无需安装&#xff0c;转换后就能在任意播放器使用。 使用地址&#xff1a; https://ncm.worthsee.com/ 网络研究观 数据泄露…

【力扣】矩阵中的最长递增路径

一、题目描述 二、解题思路 1、先求出以矩阵中的每个单元格为起点的最长递增路径 题目中说&#xff0c;对于每个单元格&#xff0c;你可以往上&#xff0c;下&#xff0c;左&#xff0c;右四个方向移动。那么以一个单元格为起点的最长递增路径就是&#xff1a;从该单元格往上…

PDF 文件的解析

1、文本 PDF 的解析 1.1、文本的提取 进行文本提取的 Python 库包括&#xff1a;pdfminer.six、PyMuPDF、PyPDF2 和 pdfplumber&#xff0c;效果最好的是 PyMuPDF&#xff0c;PyMuPDF 在进行文本提取时能够最大限度地保留 PDF 的阅读顺序&#xff0c;这对于双栏 PDF 文件的抽…

arduino 与 nodeMcu 之间的通信

一、前言 当在 arduino 板子处理好了传感器的数据应该发送给远程服务器这时候就需要用 nodeMcu 了&#xff0c;但是怎么把 arduino 的数据发送到 nodeMcu 呢&#xff0c;这就是本文要实现的。 两个板子之间通信很简单&#xff0c;直接使用 arduino IDE 提供的 Serial.println…

【C++】——list模拟实现(包懂的,细节满满)

前言 list的模拟实现和string和vector的有区别的&#xff0c;但是也有相同。 区别&#xff1a;list的迭代器底层和其他两个迭代器底层有很大区别&#xff0c;因为list的链式结构决定了与它们两个的不一样 相同&#xff1a;迭代器用法大致一样&#xff0c;其他成员函数的使用也…

解决Mac ~/.bash_profile 配置的环境变量重启终端后失效问题

在Mac系统中&#xff0c;配置环境变量通常是在~/.bash_profile文件中进行。然而&#xff0c;有时会遇到配置的环境变量在重启终端后失效的问题。 解决办法&#xff1a; 在~/.zshrc文件最后或最前面&#xff0c;增加一行 source ~/.bash_profile

Linux 搭建 ZeroTier 的 Moon 服务器

系统&#xff1a;centos 7.6 轻量云服务器&#xff1a;腾讯云 Moon是什么&#xff0c;为什么需要Moon&#xff1f; ZeroTier通过自己的多个根服务器帮助我们建立虚拟的局域网&#xff0c;让虚拟局域网内的各台设备可以打洞直连。这些根服务器的功能有些类似于通过域名查询找到…

SOFA-RPC学习记录

文章目录 需求分析模块划分微服务模块交互模块 可拓展架构插件机制 功能分析交互模块 学习微服务模块交互模块 dubbo与nacos集成学习Nacos配置中心实战 dubbo与apollo集成学习配置中心组件与k8s的抉择参考资料 结论 本报告旨在深入学习SOFA-RPC框架&#xff0c;特别是其动态配置…

基于小波区间相关的信号降噪方法(MATLAB 2021B)

在我们处理信号过程中最重要的任务就是找到信号隐藏的规律和信号的特征。常用的傅里叶分析法只能用于在时间域或者频率域上分析信号&#xff0c;而通常的数据会在时间域和频率域均有特征。而小波分析是继傅里叶分析之后的一大突破性创新&#xff0c;也是近年来在学术界非常热门…

python字符串的索引

上一篇回顾 上一关中&#xff0c;我们重识了 字符串&#xff0c;还了解了 字符串拼接 和 字符串格式化输出 的方法。 字符串的“乘法”可以很方便地“复读”字符串&#xff0c;让字符串与一个整数 n 相乘&#xff0c;字符串就会原样复制 n 次。 在体验课中我们学到&#xff…

嵌入式Linux系统编程 — 1.2 文件管理与错误处理

目录 1 Linux 系统如何管理文件 1.1 什么是静态文件 1.2 扇区&#xff08;Sector&#xff09;和块&#xff08;Block&#xff09;概念&#xff1f; 1.3 inode 1.4 进程控制块&#xff08;PCB&#xff09; 2 返回错误处理与 errno 2.1 errno变量介绍 2.3 perror函数介绍…

python-验证子串

题目描述 输入两个字符串&#xff0c;验证其中一个串是否为另一个串的子串。 输入两个字符串&#xff0c; 每个字符串占一行&#xff0c;长度不超过200且不含空格。 输出 若第一个串s1是第二个串s2的子串&#xff0c;则输出(s1) is substring of(s2)否则&#xff0c;若第二个串…

【云原生】kubernetes中secret原理详解与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

LayUI的暗淡:错误的押宝了前后端不分离

LayUI是一个不错的中后台UI框架&#xff0c;贝格前端工场用的CMS就是基于layUI的&#xff0c;可以说简单轻便。除此之外&#xff0c;贝格前端工场很少接到客户要求升级LayUI界面&#xff0c;或者采用LayUI框架的。 一、LayUI官网的谢幕&#xff0c;吹起了前后端不分离模式没落…

拓扑排序详解

目录 一、拓扑排序前置知识 1.1 定义&#xff1a; 1.2 AOV网&#xff1a; 二、如何拓扑排序 2.1 运用 kahn 算法&#xff1a; 2.2 实现拓扑排序&#xff1a; 2.3 拓扑排序的应用&#xff1a; 2.4 拓扑排序编写模板&#xff1a; 三、例题练习 3.1 例题1&#xff1a;课…

浙江大爱遮阳新材料股份有限公司新品发布会圆满成功

5月29日,浙江大爱遮阳新材料股份有限公司新品发布会在上海国家会展中心举办。本次会议出席的嘉宾有浙江大爱遮阳新材料股份有限公司总经理俞彬军,常务副总王志华,上海大爱益可美遮阳科技有限公司总经理陆俊青,浙江大爱遮阳新材料股份有限公司销售经理平鸿烈,销售经理蒋扬锋和玛…

【Python】轻松打包:CentOS7上使用PyInstaller将Shell脚本转换为可执行文件的完美指南

【Python】轻松打包&#xff1a;CentOS7上使用PyInstaller将Shell脚本转换为可执行文件的完美指南 大家好 我是寸铁&#x1f44a; 总结了一篇【Python】轻松打包&#xff1a;CentOS7上使用PyInstaller将Shell脚本转换为可执行文件的完美指南✨ 喜欢的小伙伴可以点点关注 &#…