react 的音乐 app(一)

366 阅读7分钟

开源移动端组件库具体可以查看这个:juejin.cn/post/740152… 参考文案:baijiahao.baidu.com/s?id=177814…

  • 采用 github 上开源的 NodeJS 版 api 接口 NeteaseCloudMusicApi,提供音乐数据。在此特别鸣谢 Binaryify 大佬开源接口!

  • create-vite: React 脚手架,快速搭建项目

  • eslint: 知名代码风格检查工具

  • antd: 阿里巴巴图标库

  • fastclick: 解决移动端点击延迟 300ms 的问题

  • zustand: 作为状态管理器

  • scss:处理样式

初始化项目

npx create-vite

选择 react,js 就好了

npm i 
npm run dev

image.png

image.png

文件结构

image.png

设置样式

npm install -D sass

基础样式设置src/index.scss

	html, body, div, span, applet, object, iframe,
	h1, h2, h3, h4, h5, h6, p, blockquote, pre,
	a, abbr, acronym, address, big, cite, code,
	del, dfn, em, img, ins, kbd, q, s, samp,
	small, strike, strong, sub, sup, tt, var,
	b, u, i, center,
	dl, dt, dd, ol, ul, li,
	fieldset, form, label, legend,
	table, caption, tbody, tfoot, thead, tr, th, td,
	article, aside, canvas, details, embed, 
	figure, figcaption, footer, header, hgroup, 
	menu, nav, output, ruby, section, summary,
	time, mark, audio, video {
		margin: 0;
		padding: 0;
		border: 0;
		font-size: 100%;
		font: inherit;
		vertical-align: baseline;
	}
	/* HTML5 display-role reset for older browsers */
	article, aside, details, figcaption, figure, 
	footer, header, hgroup, menu, nav, section {
		display: block;
	}
	body {
		line-height: 1;
	}
	html, body {
		background: #f2f3f4;;
	}
	ol, ul {
		list-style: none;
	}
	blockquote, q {
		quotes: none;
	}
	blockquote:before, blockquote:after,
	q:before, q:after {
		content: '';
		content: none;
	}
	table {
		border-collapse: collapse;
		border-spacing: 0;
	}
	a {
		text-decoration: none;
		color: #fff;
	}

提取共用样式, --theme-color代表样式变量,在组件里面直接color: var(--theme-color)使用就好了,在src\assets\global.css下定义文件

:root {
  --theme-color: #d44439;
  --theme-color-shadow: rgba (212; 68; 57; .5);
  --font-color-light: #f1f1f1;
  --font-color-desc: #2E3030;
  --font-color-desc-v2: #bba8a8;
  --font-size-ss: 10px;
  --font-size-s: 12px;
  --font-size-m: 14px;
  --font-size-l: 16px;
  --font-size-ll: 18px;
  --border-color: #e4e4e4;
  --background-color: #f2f3f4;
  --background-color-shadow: rgba (0; 0; 0; 0.3);
  --highlight-background-color: #fff;
  --primary-color: #3498db;
  --font-size-large: 18px;
}

/* 扩大可点击区域 */
.extend-Click {
  position: relative;

  &:before {
    content: '';
    position: absolute;
    top: -10px;
    bottom: -10px;
    left: -10px;
    right: -10px;
  }
}

/* 一行文字溢出部分用... 代替 */
.word-no-wrap {
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}

设置路由

路由文档地址: reactrouter.com/en/main/com…

npm install react-router-dom --save

react-router-dom里面包含react-router的所有东西,新的react-router-dom里面没有 Redirect 组件。

image.png


//routes/index.js
import React from 'react';
import Home from '../application/Home';
import Recommend from '../application/Recommend';
import Singers from '../application/Singers';
import Rank from '../application/Rank';

export default [
  {
    path: "/",
    element: <Home></Home>,
    children: [
      {
        path: "/recommend",
        element: <Recommend></Recommend>
      },
      {
        path: "/singers",
        element: <Singers></Singers>
      },
      {
        path: "/rank",
        element: <Rank></Rank>
      }
    ]
  }
];

main

import { createRoot } from 'react-dom/client';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import App from './App.jsx';
import routes from './routes/index.jsx';
import './index.css';

const router = createBrowserRouter(routes);
createRoot(document.getElementById('root')).render(
  <RouterProvider router={router}>
    <App />
  </RouterProvider>
);

测试

image.png

配置路由占位,根组件是Home,所以在Home利用Outlet占位

import React from 'react';
import { Outlet } from 'react-router-dom';

export default function Home() {
  return <>
    Home2222222222
    <Outlet></Outlet>
  </>;
}

测试

image.png

状态管理器

npm i jotai -S

组件库

组件库地址:mobile.ant.design/zh/guide/qu…

npm install --save antd-mobile
npm install --save antd-mobile-icons

image.png 自定义icon组件,进入阿里矢量库:www.iconfont.cn/

image.png

登录组件

目标:

image.png

login.jsx

import React, { useState } from 'react';
import { Form, Input, Button, Checkbox } from 'antd-mobile';
import classnames from 'classnames';
import Captcha from './Captcha';
import './index.scss';

const Login = () => {
  const [isActive, setIsActive] = useState(true);

  const handleClick = () => {
    setIsActive(!isActive);
  };

  const loginClass = classnames({
    'active': isActive,
    'inactive': !isActive,
  });

  const registerClass = classnames({
    'active': !isActive,
    'inactive': isActive,
  });

  const handleSubmit = async (values) => {
    console.log(values, 9090);

  };

  return <div className='login-container'>
    <div className='login-content'>
      <div className='title'>
        <span className={loginClass} onClick={handleClick}>登录</span>
        <span className={registerClass} onClick={handleClick} >注册</span>
      </div>
      <div className='form-content'>
        <Form
          form={form}
          layout='horizontal'
          mode='card'
          onFinish={handleSubmit}
          footer={
            <Button block type='submit' size='large' >
              {isActive ? '登录' : '注册'}
            </Button>
          }>
          <Form.Item label="用户名" name="user" rules={[{ required: true, message: '姓名不能为空' }]}>
            <Input placeholder='请输入' />
          </Form.Item>
          <Form.Item label="密码" name="password" rules={[{ required: true, message: '姓名不能为空' }]}>
            <Input placeholder='请输入' />
          </Form.Item>
          <Form.Item label="验证码" name="registerId" hidden={isActive} >
            <Captcha />
          </Form.Item>
        </Form>
      </div>
      <div >
        <div className='foot-content'>
          <Checkbox />
          <label className="text-right">阅读并同意了《这个条款》并愿意承担相关的责任</label>
        </div>
      </div>
    </div>

  </div>;
};

export default Login;

Captcha.jsx

import React from 'react';
import Captcha from "react-captcha-code";
import { Form, Input, Button, Checkbox } from 'antd-mobile';

export default function CaptchaCom(props) {
  console.log(props);

  return <div>
    <Input {...props} placeholder='请输入' />
    <Captcha charNum={4} />
  </div>;
}

验证码

npm i react-captcha-code -S

代码

...
import Captcha from "react-captcha-code"
...
<Input
  clearable
  type="text"
  placeholder="请输入验证码"
  onChange={(value) => setVerify(value)}
/>
<Captcha charNum={4} />

样式:

.login-container {
  background: url('@/assets/img/login2.jpg');
  height: 100vh;
  overflow: hidden;



  .login-content {
    background: #fff;
    color: #f83b49;
    margin: 12px;
    margin-top: 30vh;
    padding: 16px;
    border-radius: 5px;

    .title {
      height: 60px;
      font-size: 26px;
      padding-left: 10px;

      &>span {
        margin-right: 24px;
      }

      .active {
        font-size: 26px;
      }

      .inactive {
        font-size: 20px;
        color: #ccc;
      }
    }

    .form-content {
      .adm-form {
        --adm-border-color: #fff;
      }

      .adm-button {
        background: #fd7c90;
      }

      .adm-list-item-content {
        border-bottom: 1px solid #ccc;
      }
    }
  }

  .foot-content {
    margin-left: 8px;

    .text-right {
      margin-left: 24px;
    }
  }

  .react-captcha {
    position: absolute;
    top: 4px;
    left: 48vw;
  }
}

.login-label-container {
  display: inline-block;
  margin-left: 8px;
}

效果

image.png

image.png

Home组件

实现效果

image.png

Home/index.jsx文件

import React, { useState } from 'react';
import { Outlet, NavLink } from 'react-router-dom';
import { MenuOutlined, SearchOutlined } from '@ant-design/icons';
import '../../assets/global.css';
import './index.scss';

export default function Home() {

  return <>
    <div className="top-container style.global">
      <MenuOutlined />
      <span className="title">音乐吧</span>
      <SearchOutlined />
    </div>
    <div className='tab' >
      <NavLink to="/recommend" className={({ isActive }) => isActive ? "selected" : ""}>
        <div className="tab-item"><span > 推荐 </span></div>
      </NavLink>
      <NavLink to="/singers" className={({ isActive }) => isActive ? "selected" : ""}>
        <div className="tab-item"><span > 歌手 </span></div>
      </NavLink>
      <NavLink to="/rank" className={({ isActive }) => isActive ? "selected" : ""}>
        <div className="tab-item"><span> 排行榜 </span></div>
      </NavLink>
    </div>
    <Outlet></Outlet>
  </>;
}

Home/index.css

.top-container {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 5px 10px;
  background: var(--theme-color);

  &>span {
    line-height: 40px;
    color: #f1f1f1;
    font-size: 20px;

    &.iconfont {
      font-size: 25px;
    }
  }
}

.tab {
  height: 44px;
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  background: var(--theme-color);

  a {
    flex: 1;
    padding: 2px 0;
    font-size: 14px;
    color: #e4e4e4;

    &.selected {
      span {
        padding: 3px 0;
        font-weight: 700;
        color: #f1f1f1;
        border-bottom: 2px solid #f1f1f1;
      }
    }
  }

  .tab-item {
    height: 100%;
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
  }
}

image.png

image.png 注意点:

  1. 声明变量的时候,变量名前面要加两根连词线(--)
  2. var()函数用于读取变量。
  3. :root表示的根标签。
body {
  --foo: #7F583F;
  --bar: #F7EFD2;
}

测试:

image.png

轮播图

实现目标:

image.png

实现办法有2个你可以用swiper,也可以用antd里面的组件。

react-swipeable 是一个React组件,可以让你的元素响应滑动事件。以下是一个简单的例子,展示如何使用 react-swipeable 来创建一个可以通过滑动来切换图片的简单轮播组件。

image.png

下载工具包:

  npm i swiper@8.4.6 -S

文档地址: v8.swiperjs.com/react

我选用antd的Swiper组件

image.png

import React from 'react';
import { Swiper } from 'antd-mobile';
import imgSrc1 from '@/assets/img/005.jpg';
import imgSrc2 from '@/assets/img/002.jpg';
import imgSrc3 from '@/assets/img/003.jpg';
import imgSrc4 from '@/assets/img/004.jpg';
import './index.css';

const Recommend = () => {
  return <div className='recommend-container'>
    <div className='mark'></div>
    <div className='carousel-content'>
      <Swiper autoplay>
        <Swiper.Item>
          <img src={imgSrc1} alt="图片" />
        </Swiper.Item>
        <Swiper.Item>
          <img src={imgSrc2} alt="图片" />
        </Swiper.Item>
        <Swiper.Item>
          <img src={imgSrc3} alt="图片" />
        </Swiper.Item>
        <Swiper.Item>
          <img src={imgSrc4} alt="图片" />
        </Swiper.Item>
      </Swiper>
    </div>
  </div>;

};
export default Recommend;

样式

.recommend-container {
  position: relative;
  height: 300px;

  .mark {
    height: 50%;
    background: var(--theme-color);
  }

  .carousel-content {
    position: absolute;
    top: 0px;
    left: 5px;
    width: calc(100% - 10px);
    margin: 0 auto;

    .ant-carousel {
      border-radius: 2%;
    }
  }
}

.slick-list {
  border-radius: 5px;
}

优化后

image.png

测试

image.png

推荐列表

预计实现效果

image.png

基础实现如下:

image.png

Recommend/index.jsx

import React from 'react';
import Slider from '@/components/Slider';
import imgSrc1 from '@/assets/img/005.jpg';
import imgSrc2 from '@/assets/img/002.jpg';
import imgSrc3 from '@/assets/img/003.jpg';
import imgSrc4 from '@/assets/img/004.jpg';
import RecommendList from '@/components/RecommendList';
import img1 from '@/assets/img/101.jpg';
import img2 from '@/assets/img/102.jpg';
import img3 from '@/assets/img/103.jpg';
import img4 from '@/assets/img/104.jpg';
import img5 from '@/assets/img/105.jpg';
import img6 from '@/assets/img/106.jpg';

const Recommend = () => {
  const sliderList = [imgSrc1, imgSrc2, imgSrc3, imgSrc4];
  const recommendList = [img1, img2, img3, img4, img5, img6, img1, img2, img3,].map(item => {
    return {
      id: 1,
      picUrl: item,
      playCount: 17171122,
      name: "朴树、许巍、李健、郑钧、老狼、赵雷"
    };
  });

  return <div>
    <Slider sliderList={sliderList}></Slider>
    <RecommendList recommendList={recommendList}></RecommendList>
  </div>;
};
export default Recommend;

RecommendList/index.jsx

import React from 'react';
import { getCount } from "../../api/utils";
import { AudioOutline } from 'antd-mobile-icons';
import './index.scss';

function RecommendList(props) {
  return (
    <div className='list-wrapper'>
      <h1 className="title">
        <div className='circle'>
          <div className='white'></div>
        </div>
        热门推荐
      </h1>
      <div className='list'>
        {
          props.recommendList?.map((item, index) => {
            return (
              <div className='list-item' key={item.id + index}>
                <div className="img_wrapper">
                  <div className="decorate"></div>
                  {/* 加此参数可以减小请求的图片资源大小 */}
                  <img src={item.picUrl + "?param=300x300"} width="100%" height="100%" alt="music" />
                  <div className="play_count">
                    <AudioOutline />
                    <span className="count">{getCount(item.playCount)}</span>
                  </div>
                </div>
                <div className="desc">{item.name}</div>
              </div>
            );
          })
        }
      </div>
    </div>
  );
}

export default React.memo(RecommendList);

RecommendList/index.scss

.list-wrapper {
  max-width: 100%;

  .title {
    display: flex;
    align-items: center;
    font-weight: 700;
    padding-left: 6px;
    font-size: 14px;
    line-height: 60px;

    .circle {
      width: 16px;
      height: 16px;
      background-color: var(--theme-color);
      border-radius: 50%;
      margin-right: 8px;

      .white {
        width: 8px;
        height: 8px;
        background-color: var(--font-color-light);
        border-radius: 50%;
        position: relative;
        top: 4px;
        left: 4px;
      }
    }
  }
}

.list {
  width: 100%;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-around;
}

.list-item {
  position: relative;
  width: 32%;

  .img_wrapper {
    .decorate {
      position: absolute;
      top: 0;
      width: 100%;
      height: 35px;
      border-radius: 3px;
      background: linear-gradient (hsla (0, 0%, 43%, .4), hsla (0, 0%, 100%, 0));
    }

    position: relative;
    height: 0;
    padding-bottom: 100%;

    .play_count {
      position: absolute;
      right: 2px;
      top: 2px;

      font-size: var(--font-size-s);
      line-height: 15px;

      color: var(--font-color-light);

      .play {
        vertical-align: top;
      }
    }

    img {
      position: absolute;
      width: 100%;
      height: 100%;
      border-radius: 3px;
    }
  }

  .desc {
    overflow: hidden;
    margin-top: 2px;
    padding: 0 2px;
    height: 50px;
    text-align: left;

    font-size: var(--font-size-s);
    line-height: 1.4;

    color: var(--font-color-desc);
  }
}

效果

image.png

后台数据

数据选择

image.png 文档在线:front-end-class.github.io/uniapp-musi…

$ git clone https://github.com/front-end-class/uniapp-music-back-code.git
$ npm install
$ node app.js

image.png

image.png

进入下载下来的项目的app.js

image.png

api地址: front-end-class.github.io/uniapp-musi… 你知道启动了然后既可以在前端里面使用了。

调试接口

npm install axios --save

image.png

一个前端一个后端,他们的接口不要一样就好了

在api文档里面找到推荐歌单 image.png