天生我材必有用,千金散尽还复来(React Hooks + Egg.js + Mysql古诗文全栈项目)

前言

纸上学来终觉浅,绝知此事要躬行。经过一段时间 ReactEgg.js 的学习,就打算动手写一个功能完善的古诗文app来实战一波,希望本文可以帮助到一些刚刚入门前端的同学,一起来看看吧👀👀👀。

成果预览

线上地址,点击看看,浏览器开手机模拟器效果更佳😀😀😀

gif成果图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e4X1QYZj-1670300259909)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d5755170918438fb12661883f2204af~tplv-k3u1fbpfcp-watermark.image?)]

项目简介

  • 技术栈:React Hooks + Ant Design Mobile+ Redux + Egg.js + Mysql
  • 使用 React Hooks 对前端页面的编写
  • 使用 Redux 进行全局数据流管理
  • 使用 styled-components 样式组件进行样式编写
  • 使用 React-Router V6 进行路由配置编写
  • 贯彻前端 MVVM 的设计理念,遵循 组件化、模块化 编程思想
  • 后端使用node的企业级框架Egg.js编写后端接口
  • 使用Mysql存储二十多万条古诗文数据,以及用户信息数据

前端部分

项目前端结构

├─ node_modules       // 依赖包 
├─ public            // 项目使用的公共文件
    favicon.ico      // 网站标签图标
    index.html       // 首页的模板文件
    manifest.json    // 移动端配置文件
├─ src 
    ├─ application   // 四个主页面
    ├─ assets        // 字体配置及图片
    ├─ components    // 其他子页面及组件
    ├─ config        // 请求路径配置
    ├─ routes        // 路由配置文件
    ├─ store         // redux 相关文件 
    App.js           // 根组件
    index.js         // 项目的入口文件
    style.js         // 全局样式
.babelrc             // babel配置文件
.gitignore           // 上传Git需要忽略的文件
package-lock.json    // 锁定安装时的包的版本号
package-json         // 项目及模块包的描述
README.md            // 项目介绍
yarn.lock            // 锁定安装时的包的版本号

路由配置

本项目较为简单就直接使用react-router-dom配置,若项目复杂可以使用react-routeruseRouter

  • front-end/src/App.js 代码如下:
<Routes>
    <Route path="/*" element={<ButtomTab />} ></Route>                  {/*tabbar界面(整个app的界面:tabbar及四个子路由)*/}

    <Route path="/login" element={<Login />} ></Route>                  {/*登录界面*/}
    <Route path="/register" element={<Register />} ></Route>            {/*注册界面*/}
    <Route path="/index/my/myDetail" element={<MyDetail />} ></Route>   {/*个人信息详情界面*/}
    <Route path="/index/index/poemDetail/:id" element={<PoemDetail />} ></Route>          {/*诗词详情界面*/}
    <Route path="/index/index/search" element={<Search />} ></Route>          {/*搜索详情界面*/}
    <Route path="/index/index/search/searchPoemDetail/:id" element={<SearchPoemDetail />} ></Route>          {/*搜索诗词详情界面*/}
    <Route path="/index/index/sort/sortDetail/:type/:id" element={<SortPoemDetail />} ></Route>          {/*分类诗词详情界面*/}

    <Route path="/index/sort/sortDetail/:type" element={<SortDetail />} ></Route>          {/*分类详情界面*/}
    <Route path="/index/find/findDetail" element={<FindDetail />} ></Route>          {/*发现详情界面*/}

    <Route path="/index/my/myBrowsing" element={<MyBrowsing />} ></Route>      {/*我的浏览历史界面*/}
    <Route path="/index/my/myBrowsing/onePoemDetail/:name" element={<OnePoemDetail />} ></Route>      {/*我的浏览历史界面*/}

    <Route path="/index/my/myCollection" element={<MyCollection />} ></Route>      {/*我的收藏界面*/}
    <Route path="/index/my/myPoemList" element={<MyPoemList />} ></Route>      {/*我的诗单界面*/}
    <Route path="/index/my/myPoemList/addMyPoemList" element={<AddMyPoemList />} ></Route>          {/*添加我的诗单界面*/}
    <Route path="/index/my/myPoemList/updateMyPoemList/:title/:listid" element={<UpdateMyPoemList />} ></Route>          {/*修改我的诗单界面*/}
    <Route path="/index/my/myRecitation" element={<MyRecitation />} ></Route>      {/*我的背诵界面*/}

    <Route path="/index/my/myDetail/updateHeadPic" element={<UpdateHeadPic />} ></Route>      {/*修改头像界面*/}
    <Route path="/index/my/myDetail/updateUserName" element={<UpdateUserName />} ></Route>    {/*修改用户名界面*/}
    <Route path="/index/my/myDetail/updatePassword" element={<UpdatePassword />} ></Route>    {/*修改密码界面*/}
    <Route path="/index/my/myDetail/updateSex" element={<UpdateSex />} ></Route>              {/*修改性别界面*/}
    <Route path="/index/my/myDetail/updatePersonalizedSig" element={<UpdatePersonalizedSig />} ></Route>       {/*修改个性签名界面*/}
    <Route path="/index/my/myDetail/updateBrith" element={<UpdateBrith />} ></Route>          {/*修改生日界面*/}


</Routes>

数据流管理 Redux

  • action 代码如下
const action = {
    type: 'changeSearchPoemList',
    value: res.data.res,
}
store.dispatch(action)
  • store/index.js 代码如下
import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) // 创建数据存储仓库

export default store
  • store/reducer.js 代码如下
const defaultState = {      // 默认数据
    itemCount: 0,
    poemList: [[], [], [], [], [], [], [], [], [], [], [], [], []],
    searchPoemList: [],
    key: '2',
    index: 0,
    feihualing:[]
}

export default (state = defaultState, action) => {  // 就是一个方法函数
    if (action.type === 'changePoemList') {
        let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
        newState.poemList = action.value
        newState.itemCount = action.itemCount
        newState.key = action.key
        return newState
    }
    if (action.type === 'changeSearchPoemList') {
        let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
        newState.searchPoemList = action.value
        return newState
    }
    if (action.type === 'changeIndex') {
        let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
        newState.index = action.index
        return newState
    }
    if (action.type === 'changeFeiHuaLing') {
        let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
        newState.feihualing = action.feihualing
        return newState
    }
    return state
}

页面编写(简介四个主页面)

首页页面

在这里插入图片描述

  • 首页页面结构
<div className='Container '>
      <List>                                                              // 搜索框
        <List.Item>
          <SearchBar placeholder='搜索' onFocus={goSearch} />
        </List.Item>
      </List>
      <JumboTabs defaultActiveKey={dataInit.key} onChange={queryPoem}>    // 导航栏
        <JumboTabs.Tab title='足迹' description='' key='1'>               // 导航栏每个tab对应的内容
          <PullToRefresh
            onRefresh={async () => {
            }}
          >
            {poem.map((item, index) => {                                 // 循环输出每个tab对应的自定义组件PoemCard
              // {console.log(item);}
              return <PoemCard key={index} id={index} title={item.title} poet={item.poet} content={item.content} isShowPop={isShowPop} />
            })}

            {/* <InfiniteScroll loadMore={loadMore} hasMore={hasMore} /> */}
          </PullToRefresh>

        </JumboTabs.Tab>
        ...                                                              // 此处省略了其他JumboTabs.Tab
      </JumboTabs>

分类页面

在这里插入图片描述

  • 分类页面结构
<div>
    <Tabs>
        <Tabs.Tab title='诗文' key='1'>
            <div style={{ height: window.innerHeight }}>
                <IndexBar>
                    {groups.map(group => {
                        const { title, items } = group
                        return (
                            <IndexBar.Panel
                                index={title}
                                title={`${title}`}
                                key={`标题${title}`}
                            >
                                <Grid columns={4} gap={18}>
                                    {items.map((item, index) => (
                                        <Grid.Item key={index} onClick={(e) => { goSortDetail(e) }}>
                                            <div className='grid-demo-item-block'>{item.name}</div>
                                        </Grid.Item>
                                    ))}
                                </Grid>
                            </IndexBar.Panel>
                        )
                    })}
                </IndexBar>
            </div>
        </Tabs.Tab>
        <Tabs.Tab title='名句' key='2'>
            诗文
        </Tabs.Tab>
        <Tabs.Tab title='古籍' key='3'>
            古籍
        </Tabs.Tab>
        <Tabs.Tab title='作者' key='4'>
            <div style={{ height: window.innerHeight }}>
                <IndexBar>
                    {groups2.map(group => {
                        console.log('group is:', group);
                        const { title, items } = group
                        console.log('items is:', items)
                        return (
                            <IndexBar.Panel
                                index={title}
                                title={`${title}`}
                                key={`标题${title}`}
                            >
                                <Grid columns={4} gap={18}>

                                    {items.map((item, index) => (
                                        <Grid.Item key={index} onClick={(e) => { goSortDetail(e) }}>
                                            <div className='grid-demo-item-block'>{item.name}</div>
                                        </Grid.Item>
                                    ))}
                                </Grid>
                            </IndexBar.Panel>
                        )
                    })}
                </IndexBar>
            </div>
        </Tabs.Tab>
    </Tabs>
</div>

发现页面

在这里插入图片描述

  • 分类页面结构
<div>
    <Swiper autoplay>{items}</Swiper>
    <Divider />
    <Grid columns={2} gap={8}>
        <Grid.Item onClick={goFindDetail}>
            <div className='grid-demo-item-block find-grid ' style={{ 'backgroundColor': '#fbf7ee' }}>飞花令</div>
        </Grid.Item>
        <Grid.Item onClick={goFindDetail}>
            <div className='grid-demo-item-block find-grid' style={{ 'backgroundColor': '#fef2ef' }}>诗词接龙</div>
        </Grid.Item>
        <Grid.Item onClick={goFindDetail}>
            <div className='grid-demo-item-block find-grid' style={{ 'backgroundColor': '#f2fdee' }}>成语接龙</div>
        </Grid.Item>
        <Grid.Item>
            <div className='grid-demo-item-block find-grid' style={{ 'backgroundColor': '#f6effc' }}>考一考</div>
        </Grid.Item>
        <Grid.Item>
            <div className='grid-demo-item-block find-grid' style={{ 'backgroundColor': '#fff' }}>敬请期待</div>
        </Grid.Item>
    </Grid>

</div>

我的页面

  • 我的页面结构
<div>
    {userName === null
        ?
        <div className="">
            <List>
                <Button block color='primary' size='large' onClick={goLogin}>未登录,去登录</Button>
            </List>

            <List header='     '>
                <List.Item prefix={<EyeInvisibleOutline />} onClick={goMyBrowsing}>
                    我的浏览
                </List.Item>
                <List.Item prefix={<StarOutline />} onClick={goMyCollection}>
                    我的收藏
                </List.Item>
                <List.Item prefix={<FileOutline />} onClick={goMyPoemList}>
                    我的诗单
                </List.Item>
                <List.Item prefix={<ContentOutline />} onClick={goMyRecitation}>
                    我的背诵
                </List.Item>
                <List.Item prefix={<FillinOutline />} onClick={() => { }}>
                    我的标注
                </List.Item>
            </List>

            <List header='     '>
                <List.Item prefix={<EyeOutline />} onClick={() => { }}>
                    护眼模式
                </List.Item>
                <List.Item prefix={<AntOutline />} onClick={() => { }}>
                    深色模式
                </List.Item>
                <List.Item prefix={<CollectMoneyOutline />} onClick={() => { }}>
                    列表模式
                </List.Item>
                <List.Item prefix={<EditSOutline />} onClick={() => { }}>
                    字体选择
                </List.Item>
                <List.Item prefix={<SetOutline />} onClick={() => { }}>
                    更多设置
                </List.Item>
            </List>
        </div>
        : <div className="">
            <List>
                <List.Item extra='主页' onClick={goMyDetail} >
                    <div className="myInfo">
                        <div className=""><Avatar size={80} src={headPicPath} className='myPic' /></div>
                        <div className="myName">
                            <p className='myName-userName'>用户:{userName}</p>
                            <p className='myName-personalizedSig'>个性签名:{personalizedSig}</p>
                        </div>
                    </div>
                </List.Item>
            </List>

            <List header='     '>
                <List.Item prefix={<EyeInvisibleOutline />} onClick={goMyBrowsing}>
                    我的浏览
                </List.Item>
                <List.Item prefix={<StarOutline />} onClick={goMyCollection}>
                    我的收藏
                </List.Item>
                <List.Item prefix={<FileOutline />} onClick={goMyPoemList}>
                    我的诗单
                </List.Item>
                <List.Item prefix={<ContentOutline />} onClick={goMyRecitation}>
                    我的背诵
                </List.Item>
                <List.Item prefix={<FillinOutline />} onClick={() => { }}>
                    我的标注
                </List.Item>
            </List>

            <List header='     '>
                <List.Item prefix={<EyeOutline />} onClick={() => { }}>
                    护眼模式
                </List.Item>
                <List.Item prefix={<AntOutline />} onClick={() => { }}>
                    深色模式
                </List.Item>
                <List.Item prefix={<CollectMoneyOutline />} onClick={() => { }}>
                    列表模式
                </List.Item>
                <List.Item prefix={<EditSOutline />} onClick={() => { }}>
                    字体选择
                </List.Item>
                <List.Item prefix={<SetOutline />} onClick={() => { }}>
                    更多设置
                </List.Item>
            </List>
        </div>
    }
</div>

后端部分

项目后端结构

├─ node_modules       // 依赖包 
├─ app                
    ├─ controller    // 控制层
    ├─ middleware    // 中间件
    ├─ public/upload // 上传图片保存
    ├─ config        // 请求路径配置
    router.js        // 路由接口配置文件
├─ config 
    config.default.js// 插件配置文件
    plugin.js        // 插件引入文件
├─ test/app/controller// 测试相关文件
.autod.conf.js
.eslintignore
.eslintrc
.gitignore           // 上传Git需要忽略的文件
.travis.yml
README.md            // 项目介绍
appveyor.yml
jsconfig.json
package-json         // 项目及模块包的描述

数据库sql数据

chinese-poetry

数据库连接及跨域配置

  • 导出插件 /back-end/config/plugin.js
module.exports.mysql = {    // mysql插件
    enable: true,
    package: 'egg-mysql'
}
module.exports.cors = {     // cors插件
    enable: true,
    package: 'egg-cors'
}
  • 配置插件 back-end/config/config.default.js
config.mysql = {    // 配置mysql数据库
    // database configuration
    // client: {
    //   // host
    //   host: '127.0.0.1',
    //   // port
    //   port: '3306',
    //   // username
    //   user: 'root',
    //   // password
    //   password: '',
    //   // database
    //   database: 'ancientpoems',
    // },
    client: {
        // host
        host: '127.0.0.1',
        // port
        port: '3306',
        // username
        user: 'root',
        // password
        password: '',
        // database
        database: 'ancientpoems',
    },
    // load into app, default is open
    app: true,
    // load into agent, default is close
    agent: false,
};
config.cors = {
    origin: ctx => ctx.get('origin'),
    // origin: '*', //只允许这个域进行访问接口
    credentials: true,   // 开启认证 支持cookie跨域
    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS'
};

路由接口配置

'use strict';

/**
 * @param {Egg.Application} app - egg application
 * @description 后端路由文件
 */
module.exports = app => {
    const { router, controller } = app;
    let loginAuth = app.middleware.loginAuth()

    // 登录注册
    router.get('/login', controller.login.login)
    router.post('/checkLogin', controller.login.checkLogin)
    router.post('/register', controller.register.register)

    // 用户个人信息修改
    router.post('/userNameUpdate', controller.userInfoUpdate.userNameUpdate)
    router.post('/passwordUpdate', controller.userInfoUpdate.passwordUpdate)
    router.post('/sexUpdate', controller.userInfoUpdate.sexUpdate)
    router.post('/personalizedSigUpdate', controller.userInfoUpdate.personalizedSigUpdate)
    router.post('/birthUpdate', controller.userInfoUpdate.birthUpdate)
    router.post('/headPicUpdate', controller.userInfoUpdate.headPicUpdate)

    // 查询诗词
    router.post('/QuerySomeonePoems', controller.queryPoems.QuerySomeonePoems)
    router.post('/QueryRandomTenPoems', controller.queryPoems.QueryRandomTenPoems)
    router.post('/QuerySomeTypePoem', controller.queryPoems.QuerySomeTypePoem)

    // 搜索功能
    router.post('/FuzzySearch', loginAuth, controller.search.FuzzySearch)
    router.post('/Search', loginAuth, controller.search.Search)
    router.post('/NameSearch', controller.search.NameSearch)
    router.post('/SearchType', controller.search.SearchType)
    router.post('/SearchPoet', controller.search.SearchPoet)
    router.post('/SearchDynasty', controller.search.SearchDynasty)

    // 查询诗人
    router.post('/QueryADynastyPoets', controller.queryPoets.QueryADynastyPoets)
    router.post('/QuerySortDynastyPoets', controller.queryPoets.QuerySortDynastyPoets)

    // 收藏
    router.post('/AddCollection', controller.collection.AddCollecions)
    router.post('/QueryCollection', controller.collection.QueryCollections)

    // 浏览记录
    router.post('/AddHistory', controller.browseHistory.AddHistory) // 添加历史记录
    router.post('/SearchHistory', controller.browseHistory.SearchHistory) //搜索某用户id的历史记录 

    // 诗单的创建
    router.post('/CreateList', controller.poemList.CreateList);
    router.post('/UpdateList', controller.poemList.UpdateList);
    router.post('/DeleteList', controller.poemList.DeleteList);
    router.post('/CheckList', controller.poemList.CheckList)  // 查看某用户的所以诗单

    // 添加诗词到诗单
    router.post('/AddPoemToList', controller.poemList.AddPoemToList);
    router.post('/CheckPoemList', controller.poemList.CheckPoemList);
    router.post('/DeletePoemList', controller.poemList.DeletePoemList);

    // 用户自创作品 
    router.post('/CreateWork', controller.forum.CreateWork);  // 创建作品,保存在草稿箱中, 默认状态为0,在草稿箱中
    router.post('/CreateComment', controller.forum.CreateComment); // 创建评论
    router.post('/CheckWork', controller.forum.CheckWork); // 按时间顺序 查看论坛中 state = 1 的发布作品
    router.post('/PostWork', controller.forum.PostWork)  // 发布作品,将草稿箱中的作品进行发布 state改为1

    // 添加考一考题目路由
    router.post('/AddTest', controller.test.AddTest)
    router.get('/QueryTest', controller.test.QueryTest)

    // 飞花令
    router.post('/GetAnswer', controller.feiHuaLing.GetAnswer)  // 用来根据随机数活得答案
    router.post('/CheckAnswer', controller.feiHuaLing.CheckAnswer) // 用来检测用户的答案
    router.get('/ChangeAllAnswer', controller.feiHuaLing.ChangeAllAnswer) // 用来改变所有答案状态,飞花令结束时候出触发

};

具体接口方法实现

  • 示例查询诗词
/* 'use strict';
/**
 * @description 查询诗词controller
 */
const Controller = require('egg').Controller;

class QueryPoemsController extends Controller {

    async QuerySomeonePoems() {   // 查询某个诗人的诗文

        let author = this.ctx.request.body.author

        // let author = '李白'
        const sql = " SELECT id, author, name, dynasty, content, annotation, translation, analyse, background FROM t_poems_poem WHERE author = '" + author + "' ORDER BY RAND() LIMIT 10 OFFSET 0 "
        const res = await this.app.mysql.query(sql)

        if (res.length > 0) {
            this.ctx.body = { 'data': '查询诗词十首成功', 'res': res }
        }
    }

    async QuerySomeTypePoem() {   // 查询某种类型的诗文
        let type = this.ctx.request.body.type

        // type = '夏天'
        const sql = "SELECT * FROM t_poems_poem WHERE type = '" + type + "' ORDER BY RAND() LIMIT 10"
        const res = await this.app.mysql.query(sql)

        if (res.length > 0) {
            this.ctx.body = { 'data': '查询某种类型诗词十首成功', 'res': res }
        }
    }
    async QueryRandomTenPoems() {   // 随机查询十首古诗
        const sql = 'SELECT * FROM t_poems_poem ORDER BY RAND() LIMIT 10'
        const res = await this.app.mysql.query(sql)

        if (res.length > 0) {
            this.ctx.body = { 'data': '随机查询诗词十首成功', 'res': res }
        }
    }


}

module.exports = QueryPoemsController; 

部署部分

部署部分记录在另外一篇文章内,想要部署的同学可以看看,使用的是阿里云的服务器来部署的,使用宝塔来管理服务器的软件资源,非常简单方便。(点这里去看看

源码

gitee源码

github源码

总结

本项目总的来说比较简单,但是前端后端部署都有涉及到,如果你需要一个项目来学习学习,参考参考,这么写出一个可以跑的全栈项目,本项目还是有一定的参考价值🤣🤣🤣。根据这个项目,你去做一个什么管理系统,购物系统啥的都是可以的,简单的增删改查就可以。最后,祝你成功嘿嘿嘿~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值