diff --git a/musicBackend/src/main/java/com/cph/musicbackend/Test.java b/musicBackend/src/main/java/com/cph/musicbackend/Test.java deleted file mode 100644 index c69558c..0000000 --- a/musicBackend/src/main/java/com/cph/musicbackend/Test.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.cph.musicbackend; - -import java.io.*; -import java.util.Map; -import java.util.HashMap; - -import com.acrcloud.utils.ACRCloudRecognizer; - -public class Test { - - public static void main(String[] args) { - Map config = new HashMap(); - - config.put("host", "identify-cn-north-1.acrcloud.cn"); - config.put("access_key", "3076056eb203a361d9341411191e1e25"); - config.put("access_secret", "TRlwfLkzv8orvm7gIePYvaM8wJvLWMJBBTrJujvQ"); - - config.put("debug", false); - config.put("timeout", 10); // seconds - - ACRCloudRecognizer re = new ACRCloudRecognizer(config); - - // It will skip 80 seconds. - String filename = "D:\\audio\\1.mp3"; - String result = re.recognizeByFile(filename, 1); - System.out.println(result); - - File file = new File(filename); - byte[] buffer = new byte[3 * 1024 * 1024]; - if (!file.exists()) { - return; - } - FileInputStream fin = null; - int bufferLen = 0; - try { - fin = new FileInputStream(file); - bufferLen = fin.read(buffer, 0, buffer.length); - } catch (Exception e) { - e.printStackTrace(); - } finally { - try { - if (fin != null) { - fin.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - System.out.println("bufferLen=" + bufferLen); - - if (bufferLen <= 0) - return; - - // It will skip 80 seconds from the begginning of (buffer). - result = re.recognizeByFileBuffer(buffer, bufferLen, 1); - System.out.println(result); - } -} \ No newline at end of file diff --git a/musicBackend/src/main/java/com/cph/musicbackend/aspect/LoginAspect.java b/musicBackend/src/main/java/com/cph/musicbackend/aspect/LoginAspect.java index a31303a..d0c0f7f 100644 --- a/musicBackend/src/main/java/com/cph/musicbackend/aspect/LoginAspect.java +++ b/musicBackend/src/main/java/com/cph/musicbackend/aspect/LoginAspect.java @@ -2,6 +2,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.cph.musicbackend.entity.Music; import com.cph.musicbackend.entity.User; import com.cph.musicbackend.mapper.MusicMapper; @@ -40,7 +41,6 @@ private void pointCutMethodController() {} @Around(value = "pointCutMethodController()") public Object doAroundService(ProceedingJoinPoint joinPoint) throws Throwable { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - User user = new User(); if (requestAttributes != null) { HttpServletRequest request = requestAttributes.getRequest(); @@ -56,21 +56,16 @@ public Object doAroundService(ProceedingJoinPoint joinPoint) throws Throwable { ipAddress = request.getRemoteAddr(); } ipAddress = ipAddress.split(",")[0]; // 如果有多个代理IP,取第一个 + + String token = request.getHeader("authorization"); + if(StringUtils.isBlank(token)){ + throw new Exception("请先登录"); + } + User user = userMapper.selectOne(new QueryWrapper().eq("token", token)); + if(user == null )throw new Exception("请先登录"); user.setIpAddress(ipAddress); - } - // 没有用户,创建用户,添加默认歌曲 - User ipAddress = userMapper.selectOne(new QueryWrapper().eq("ip_address", user.getIpAddress())); - if(ipAddress == null){ - userMapper.insert(user); - List musics = musicMapper.selectList(new QueryWrapper() - .like("url", "https://app102.acapp.acwing.com.cn").last("limit 20").orderByDesc("id")); - user.setMusics(musics); - userMapper.addDefaultMusics(user, new Date()); UserContext.setCurrentUser(user); - }else{ - UserContext.setCurrentUser(ipAddress); } - // 执行方法 Object result = joinPoint.proceed(); return result; diff --git a/musicBackend/src/main/java/com/cph/musicbackend/common/CommonResult.java b/musicBackend/src/main/java/com/cph/musicbackend/common/CommonResult.java new file mode 100644 index 0000000..384c1c6 --- /dev/null +++ b/musicBackend/src/main/java/com/cph/musicbackend/common/CommonResult.java @@ -0,0 +1,32 @@ +package com.cph.musicbackend.common; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +@Data +@Accessors(chain = true) +public class CommonResult { + private Integer code; + private String message; + private T data; + private List datas; + + public CommonResult() { + } + + public CommonResult(Integer code, String message, T data, List datas) { + this.code = code; + this.message = message; + this.data = data; + this.datas = datas; + } + + public CommonResult(Integer code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } +} diff --git a/musicBackend/src/main/java/com/cph/musicbackend/WebConfig.java b/musicBackend/src/main/java/com/cph/musicbackend/config/WebConfig.java similarity index 94% rename from musicBackend/src/main/java/com/cph/musicbackend/WebConfig.java rename to musicBackend/src/main/java/com/cph/musicbackend/config/WebConfig.java index fd074af..be6edf8 100644 --- a/musicBackend/src/main/java/com/cph/musicbackend/WebConfig.java +++ b/musicBackend/src/main/java/com/cph/musicbackend/config/WebConfig.java @@ -1,4 +1,4 @@ -package com.cph.musicbackend; +package com.cph.musicbackend.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; diff --git a/musicBackend/src/main/java/com/cph/musicbackend/controller/MusicController.java b/musicBackend/src/main/java/com/cph/musicbackend/controller/MusicController.java index a4a605c..294cb68 100644 --- a/musicBackend/src/main/java/com/cph/musicbackend/controller/MusicController.java +++ b/musicBackend/src/main/java/com/cph/musicbackend/controller/MusicController.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.cph.musicbackend.aspect.RecognizeAddress; import com.cph.musicbackend.aspect.UserContext; +import com.cph.musicbackend.common.CommonResult; import com.cph.musicbackend.entity.Music; import com.cph.musicbackend.entity.User; import com.cph.musicbackend.mapper.MusicMapper; @@ -44,10 +45,6 @@ public class MusicController { @RecognizeAddress public List getMusciList() { User currentUser = UserContext.getCurrentUser(); - if (!CollectionUtils.isEmpty(currentUser.getMusics())) { - return currentUser.getMusics(); - } - //个性化音乐列表 User personalMuicList = userMapper.getPersonalMuicList(currentUser); return personalMuicList.getMusics(); } @@ -93,6 +90,18 @@ public Object add(@RequestBody Music music) { return "添加成功"; } + /** + * 我的喜欢列表 + * @return + */ + @PostMapping("/api/likeList") + @RecognizeAddress + public CommonResult likeList() { + User currentUser = UserContext.getCurrentUser(); + User personalMuicList = userMapper.getLikeMuicList(currentUser); + return new CommonResult(200,"查新成功",null,personalMuicList.getMusics()); + } + @PostMapping("/api/like") @RecognizeAddress public Object like(@RequestBody Music song) { diff --git a/musicBackend/src/main/java/com/cph/musicbackend/controller/UserController.java b/musicBackend/src/main/java/com/cph/musicbackend/controller/UserController.java new file mode 100644 index 0000000..74c5a23 --- /dev/null +++ b/musicBackend/src/main/java/com/cph/musicbackend/controller/UserController.java @@ -0,0 +1,62 @@ +package com.cph.musicbackend.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.cph.musicbackend.aspect.RecognizeAddress; +import com.cph.musicbackend.common.CommonResult; +import com.cph.musicbackend.entity.Music; +import com.cph.musicbackend.entity.User; +import com.cph.musicbackend.mapper.MusicMapper; +import com.cph.musicbackend.mapper.UserMapper; +import com.cph.musicbackend.utils.MD5Utils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.rmi.server.UID; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@RestController +public class UserController { + + @Autowired + UserMapper userMapper; + + @Value("${md5.salt}") + private String salt; + + @Autowired + MusicMapper musicMapper; + + @PostMapping("/api/login") + public CommonResult login(@RequestBody User loginUser) { + User user = userMapper.selectOne(new QueryWrapper().eq("username", loginUser.getUsername())); + Assert.notNull(user,"用户不存在"); + String s = MD5Utils.MD5Lower(loginUser.getPassword(), salt); + if(s.equals(user.getPassword())){ + user.setToken( UUID.randomUUID().toString()); + userMapper.updateById(user); + return new CommonResult(200,"登录成功",user); + }else{ + return new CommonResult(400,"账号或密码错误",null); + } + } + + @PostMapping("/api/register") + public CommonResult register(@RequestBody User registerUser) { + User user = userMapper.selectOne(new QueryWrapper().eq("username", registerUser.getUsername())); + Assert.isNull(user,"用户已存在"); + registerUser.setPassword(MD5Utils.MD5Lower(registerUser.getPassword(), salt)); + registerUser.setCover("https://yup1.oss-cn-hangzhou.aliyuncs.com/images/images/3.png"); + userMapper.insert(registerUser); + List musics = musicMapper.selectList(new QueryWrapper() + .like("url", "https://app102.acapp.acwing.com.cn").eq("is_save",1).last("limit 20").orderByDesc("id")); + registerUser.setMusics(musics); + userMapper.addDefaultMusics(registerUser, new Date()); + return new CommonResult(200,"注册成功",null); + } +} \ No newline at end of file diff --git a/musicBackend/src/main/java/com/cph/musicbackend/entity/User.java b/musicBackend/src/main/java/com/cph/musicbackend/entity/User.java index 4062e85..e7da219 100644 --- a/musicBackend/src/main/java/com/cph/musicbackend/entity/User.java +++ b/musicBackend/src/main/java/com/cph/musicbackend/entity/User.java @@ -16,6 +16,15 @@ public class User { private String mac; private String ipAddress; + private String password; + private String phone; + private String email; + private String nickname; + private String cover; + + private String token; + @TableField(exist = false) private List musics; + } \ No newline at end of file diff --git a/musicBackend/src/main/java/com/cph/musicbackend/mapper/UserMapper.java b/musicBackend/src/main/java/com/cph/musicbackend/mapper/UserMapper.java index 978e59f..186031d 100644 --- a/musicBackend/src/main/java/com/cph/musicbackend/mapper/UserMapper.java +++ b/musicBackend/src/main/java/com/cph/musicbackend/mapper/UserMapper.java @@ -18,6 +18,13 @@ public interface UserMapper extends BaseMapper { */ public User getPersonalMuicList(@Param("u") User user); + /** + * 查询喜欢的音乐 + * @param user + * @return + */ + public User getLikeMuicList(@Param("u") User user); + /** * 添加默认歌曲 * diff --git a/musicBackend/src/main/java/com/cph/musicbackend/utils/MD5Utils.java b/musicBackend/src/main/java/com/cph/musicbackend/utils/MD5Utils.java new file mode 100644 index 0000000..f808546 --- /dev/null +++ b/musicBackend/src/main/java/com/cph/musicbackend/utils/MD5Utils.java @@ -0,0 +1,179 @@ +package com.cph.musicbackend.utils; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * MD5加密/验证工具类 + * + */ +public class MD5Utils { + + static final char hexDigits[] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; + static final char hexDigitsLower[] = { '0', '1', '2', '3', '4', '5', '6', '7','8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + /** + * 对字符串 MD5 无盐值加密 + * + * @param plainText + * 传入要加密的字符串 + * @return + * MD5加密后生成32位(小写字母+数字)字符串 + */ + public static String MD5Lower(String plainText) { + try { + // 获得MD5摘要算法的 MessageDigest 对象 + MessageDigest md = MessageDigest.getInstance("MD5"); + + // 使用指定的字节更新摘要 + md.update(plainText.getBytes()); + + // digest()最后确定返回md5 hash值,返回值为8位字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符 + // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值。1 固定值 + return new BigInteger(1, md.digest()).toString(16); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + + + + /** + * 对字符串 MD5 加密 + * + * @param plainText + * 传入要加密的字符串 + * @return + * MD5加密后生成32位(大写字母+数字)字符串 + */ + public static String MD5Upper(String plainText) { + try { + // 获得MD5摘要算法的 MessageDigest 对象 + MessageDigest md = MessageDigest.getInstance("MD5"); + + // 使用指定的字节更新摘要 + md.update(plainText.getBytes()); + + // 获得密文 + byte[] mdResult = md.digest(); + // 把密文转换成十六进制的字符串形式 + int j = mdResult.length; + char str[] = new char[j * 2]; + int k = 0; + for (int i = 0; i < j; i++) { + byte byte0 = mdResult[i]; + str[k++] = hexDigits[byte0 >>> 4 & 0xf];// 取字节中高 4 位的数字转换, >>> 为逻辑右移,将符号位一起右移 + str[k++] = hexDigits[byte0 & 0xf]; // 取字节中低 4 位的数字转换 + } + return new String(str); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 对字符串 MD5 加盐值加密 + * + * @param plainText + * 传入要加密的字符串 + * @param saltValue + * 传入要加的盐值 + * @return + * MD5加密后生成32位(小写字母+数字)字符串 + */ + public static String MD5Lower(String plainText, String saltValue) { + try { + // 获得MD5摘要算法的 MessageDigest 对象 + MessageDigest md = MessageDigest.getInstance("MD5"); + + // 使用指定的字节更新摘要 + md.update(plainText.getBytes()); + md.update(saltValue.getBytes()); + + // digest()最后确定返回md5 hash值,返回值为8位字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符 + // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值。1 固定值 + return new BigInteger(1, md.digest()).toString(16); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 对字符串 MD5 加盐值加密 + * + * @param plainText + * 传入要加密的字符串 + * @param saltValue + * 传入要加的盐值 + * @return + * MD5加密后生成32位(大写字母+数字)字符串 + */ + public static String MD5Upper(String plainText, String saltValue) { + try { + // 获得MD5摘要算法的 MessageDigest 对象 + MessageDigest md = MessageDigest.getInstance("MD5"); + + // 使用指定的字节更新摘要 + md.update(plainText.getBytes()); + md.update(saltValue.getBytes()); + + // 获得密文 + byte[] mdResult = md.digest(); + // 把密文转换成十六进制的字符串形式 + int j = mdResult.length; + char str[] = new char[j * 2]; + int k = 0; + for (int i = 0; i < j; i++) { + byte byte0 = mdResult[i]; + str[k++] = hexDigits[byte0 >>> 4 & 0xf]; + str[k++] = hexDigits[byte0 & 0xf]; + } + return new String(str); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * MD5加密后生成32位(小写字母+数字)字符串 + * 同 MD5Lower() 一样 + */ + public final static String MD5(String plainText) { + try { + MessageDigest mdTemp = MessageDigest.getInstance("MD5"); + + mdTemp.update(plainText.getBytes("UTF-8")); + + byte[] md = mdTemp.digest(); + int j = md.length; + char str[] = new char[j * 2]; + int k = 0; + for (int i = 0; i < j; i++) { + byte byte0 = md[i]; + str[k++] = hexDigitsLower[byte0 >>> 4 & 0xf]; + str[k++] = hexDigitsLower[byte0 & 0xf]; + } + return new String(str); + } catch (Exception e) { + return null; + } + } + + /** + * 校验MD5码 + * + * @param text + * 要校验的字符串 + * @param md5 + * md5值 + * @return 校验结果 + */ + public static boolean valid(String text, String md5) { + return md5.equals(MD5(text)) || md5.equals(MD5(text).toUpperCase()); + } +} \ No newline at end of file diff --git a/musicBackend/src/main/resources/application.yaml b/musicBackend/src/main/resources/application.yaml index 7686d0f..c2afde8 100644 --- a/musicBackend/src/main/resources/application.yaml +++ b/musicBackend/src/main/resources/application.yaml @@ -46,4 +46,7 @@ file: logging: level: com.cph.musicbackend.mapper: TRACE - org.mybatis: DEBUG \ No newline at end of file + org.mybatis: DEBUG + +md5: + salt: "ccpphh130." \ No newline at end of file diff --git a/musicBackend/src/main/resources/mybatis/UserMapper.xml b/musicBackend/src/main/resources/mybatis/UserMapper.xml index 4fb67ae..feaea25 100644 --- a/musicBackend/src/main/resources/mybatis/UserMapper.xml +++ b/musicBackend/src/main/resources/mybatis/UserMapper.xml @@ -39,11 +39,34 @@ FROM user a LEFT JOIN user_music b ON a.id = b.uid LEFT JOIN music c ON b.mid = c.id - WHERE a.ip_address = #{u.ipAddress} + WHERE a.username = #{u.username} AND c.url LIKE '%https://app102.acapp.acwing.com.cn%' order by c.id desc + + insert into user_music(uid,mid,created_time) values diff --git a/package.json b/package.json index ea97394..f15a104 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "ease-music", "version": "0.1.0", "private": true, + "homepage": "./", "dependencies": { "@react-vant/icons": "^0.1.0", "@testing-library/jest-dom": "^5.17.0", diff --git a/src/App.css b/src/App.css index 2809228..5e60191 100644 --- a/src/App.css +++ b/src/App.css @@ -18,15 +18,17 @@ } .App-header { - background: linear-gradient(to bottom,#DDA0DD, #FFB6C1); + background: linear-gradient(to bottom, #DDA0DD, #FFB6C1); flex: 1; display: flex; flex-direction: column; align-items: center; - justify-content: center; /* 改回 center */ + justify-content: center; + /* 改回 center */ font-size: calc(10px + 2vmin); color: black; - padding: 5vh 0; /* 上下都添加一些内边距 */ + padding: 5vh 0; + /* 上下都添加一些内边距 */ } .music-container { @@ -35,7 +37,8 @@ align-items: center; width: 100%; max-width: 400px; - margin: auto 0; /* 使用 auto 使容器在垂直方向上居中 */ + margin: auto 0; + /* 使用 auto 使容器在垂直方向上居中 */ height: 100%; } @@ -47,6 +50,7 @@ from { transform: rotate(0deg); } + to { transform: rotate(360deg); } @@ -74,7 +78,8 @@ } /* 添加以下代码以仅删除主页面滚条 */ -html, body { +html, +body { overflow: hidden; } @@ -85,23 +90,30 @@ body::-webkit-scrollbar { } /* 对于 Firefox */ -html, body { +html, +body { scrollbar-width: none; } /* 对于 IE 和 Edge */ -html, body { +html, +body { -ms-overflow-style: none; } .image-container { - width: 100vw; /* 稍微减小宽度 */ - height: 100vw; /* 稍微减小高度 */ - max-width: 300px; /* 减小最大宽度 */ - max-height: 300px; /* 减小最大高度 */ + width: 100vw; + /* 稍微减小宽度 */ + height: 100vw; + /* 稍微减小高度 */ + max-width: 300px; + /* 减小最大宽度 */ + max-height: 300px; + /* 减小最大高度 */ overflow: hidden; border-radius: 50%; - margin: 20px auto; /* 上下间距20px,左右自动居中 */ + margin: 20px auto; + /* 上下间距20px,左右自动居中 */ } .App-logo { @@ -133,10 +145,12 @@ html, body { transform: scale(1); opacity: 0.8; } + 50% { transform: scale(1.05); opacity: 1; } + 100% { transform: scale(1); opacity: 0.8; @@ -150,20 +164,39 @@ html, body { /* 为较大的屏幕添加额外的样式 */ @media screen and (min-height: 700px) { .App-header { - justify-content: center; /* 保持居中 */ + justify-content: center; + /* 保持居中 */ } .music-container { - margin: auto 0; /* 在较高的屏幕上保持垂直居中 */ + margin: auto 0; + /* 在较高的屏幕上保持垂直居中 */ } } @media screen and (min-height: 800px) { .App-header { - padding: 8vh 0; /* 在更大的屏幕上增加上下内边距 */ + padding: 8vh 0; + /* 在更大的屏幕上增加上下内边距 */ } .image-container { - margin-bottom: 30px; /* 稍微减少图片容器的底部边距 */ + margin-bottom: 30px; + /* 稍微减少图片容器的底部边距 */ } +} + +.loginBox { + width: 80%; + position: absolute; + z-index: 999; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} +.loginHeader{ + display: flex; + justify-content: space-between; + text-align: center; + width: 100%; } \ No newline at end of file diff --git a/src/App.js b/src/App.js index 009238d..76a3693 100644 --- a/src/App.js +++ b/src/App.js @@ -5,63 +5,52 @@ import PlayList from './components/PlayList.tsx'; import 'react-vant/es/styles'; import MusicPlayer from './components/MusicPlayer.tsx'; import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { Image, Toast, Search } from 'react-vant' +import { Image, Toast } from 'react-vant' import { instance } from './utils/api'; -import tinggeshiqu from './assets/images/tinggeshiqu.png'; -import tinggeshiqu40x40 from './assets/images/tinggeshiqu40x40.png'; - -// 添加获取 URL 参数的函数 -function getParameterByName(name, url = window.location.href) { - name = name.replace(/[\[\]]/g, '\\$&'); - var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), - results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, ' ')); -} +import HomeHeader from './components/HomeHeader.tsx'; +import LocalStorageUtil from './utils/LocalStorageUtil'; +import { Card, Button, Overlay, Input, Form, Tabbar } from 'react-vant'; +import { FriendsO, HomeO, Search, SettingO } from '@react-vant/icons' function App() { const [currentSong, setCurrentSong] = useState(null); const [playlist, setPlaylist] = useState([]); const [search, setSearch] = useState(''); const [showSearch, setShowSearch] = useState(false); - const searchRef = useRef(null); - const searchInputRef = useRef(null); const [isShowPlayList, setIsShowPlayList] = useState(false); - const [sourceEnv, setSourceEnv] = useState(null); const playlistRef = useRef(null); + const [userinfo, setUserinfo] = useState({}); // 定义一个 state 变量存储用户名 + const [loginState, setLoginState] = useState(false); // 定义一个 state 变量存储用户名 + const [form] = Form.useForm() + const [loginOrRegister, setLoginOrRegister] = useState('login'); + const [list, setList] = useState([]) + const [activeTab, setActiveTab] = useState('home'); - useEffect(() => { - const envParam = getParameterByName('sourceEnv'); - setSourceEnv(envParam); - console.log('sourceEnv:', envParam); - - if (envParam) { - switch (envParam) { - case 'production': - console.log('Running in production environment'); - break; - case 'plugin': - console.log('Running in development environment'); - break; - default: - console.log('Running in default environment'); + const onFinish = values => { + console.log(values) + } + const fetchPlaylist = async () => { + try { + const response = await instance.get("/musicList"); + setPlaylist(response.data); + if (response.data.length > 0) { + setCurrentSong(response.data[0]); } + } catch (error) { + console.error("Error fetching music list:", error); + Toast.fail('获取播放列表失败,请稍后重试'); } + }; - const fetchPlaylist = async () => { - try { - const response = await instance.get("/musicList"); - setPlaylist(response.data); - if (response.data.length > 0) { - setCurrentSong(response.data[0]); - } - } catch (error) { - console.error("Error fetching music list:", error); - Toast.fail('获取播放列表失败,请稍后重试'); - } - }; - fetchPlaylist(); + useEffect(() => { + setUserinfo(LocalStorageUtil.getItem('userinfo')); //异步的 + if (LocalStorageUtil.getItem('userinfo') === null || JSON.stringify(LocalStorageUtil.getItem('userinfo')) === '{}') { + Toast.fail('请先登录'); + setLoginState(false); + } else { + setLoginState(true); + fetchPlaylist(); + } }, []); const handlePrevSong = () => { @@ -74,6 +63,48 @@ function App() { setCurrentSong(playlist[prevIndex]); }; + const handleLogin = () => { + const username = form.getFieldValue("username"); + const password = form.getFieldValue("password"); + instance.post("/login", { + username: username, + password: password + }).then(res => { + if (res.data.code === 200) { + LocalStorageUtil.setItem('userinfo', res.data.data); + } + setUserinfo(res.data.data); + setLoginState(true); + fetchPlaylist(); + Toast.success('登录成功'); + }).catch(error => { + setLoginState(false); + console.log("error", error) + }) + }; + + const handleRegister = () => { + const username = form.getFieldValue("username"); + const password = form.getFieldValue("password"); + const phone = form.getFieldValue("phone"); + const nickname = form.getFieldValue("nickname"); + + instance.post("/register", { + username: username, + password: password, + nickname: nickname, + phone: phone + }).then(res => { + if (res.data.code === 200) { + setLoginOrRegister('login') + } + Toast.success('注册成功,请登录'); + }).catch(error => { + setLoginState(false); + console.log("error", error) + }) + }; + const handleNextSong = () => { if (playlist.length === 0) { Toast.info('播放列表为空'); @@ -89,10 +120,6 @@ function App() { Toast.fail(currentSong.title + ' 播放出错,请稍后重试'); }; - const isInChromeExtension = () => { - return sourceEnv === 'plugin' - }; - const handleKeyDown = useCallback((event) => { if (event.ctrlKey && event.key === 'x') { event.preventDefault(); @@ -103,31 +130,15 @@ function App() { } }, [showSearch]); - const handleClickOutside = useCallback((event) => { - if (searchRef.current && !searchRef.current.contains(event.target)) { - setShowSearch(false); - setSearch(''); - } - if (playlistRef.current && !playlistRef.current.contains(event.target) && isShowPlayList) { - setIsShowPlayList(false); - } - }, [showSearch, isShowPlayList]); + useEffect(() => { document.addEventListener('keydown', handleKeyDown); - document.addEventListener('mousedown', handleClickOutside); - return () => { document.removeEventListener('keydown', handleKeyDown); - document.removeEventListener('mousedown', handleClickOutside); }; - }, [handleKeyDown, handleClickOutside]); + }, [handleKeyDown]); - useEffect(() => { - if (showSearch && searchInputRef.current) { - searchInputRef.current.focus(); - } - }, [showSearch]); const updateSongInPlaylist = (updatedSong) => { setPlaylist(prevPlaylist => @@ -138,46 +149,89 @@ function App() { }; return ( -
+
- {showSearch && ( -
- { - const response = await instance.post("/search", { "title": val }); - if (response.data instanceof Array) { - if (response.data.length > 0) { - setCurrentSong(response.data[0]); + + + +
+ + +
+ + +
+
+ +
+ + + + + + + { + loginOrRegister === 'register' ? + <> + + + + + + + + + : <> } - } else { - Toast.fail(response.data) +
+
+ + {loginOrRegister === 'login' ? + : } - }} - onCancel={() => { - setShowSearch(false); - setSearch(''); - }} - onClear={() => { - setSearch(''); - }} - /> + +
- )} +
+ +
{currentSong && ( <> @@ -200,6 +254,8 @@ function App() { setIsShowPlayList={setIsShowPlayList} setCurrentSong={setCurrentSong} updateSongInPlaylist={updateSongInPlaylist} + likeList={list} + setLikeList={setList} /> {isShowPlayList && (
@@ -214,6 +270,21 @@ function App() { )} )} + + + } name="home"> + 首页 + + } name="search" badge={{ dot: true }}> + 搜索 + + } name="chat" badge={{ content: 5 }}> + 聊天 + + } name="profile"> + 我的 + +
diff --git a/src/components/HomeHeader.css b/src/components/HomeHeader.css new file mode 100644 index 0000000..a2e1445 --- /dev/null +++ b/src/components/HomeHeader.css @@ -0,0 +1,15 @@ +.self-container { + display: center; +} + +.self-container-header { + width: 50%; + position: absolute; + top: 5.8%; + left: 50% +} + +.like-list { + margin-top: 3px; + border-radius: 5%; +} \ No newline at end of file diff --git a/src/components/HomeHeader.tsx b/src/components/HomeHeader.tsx new file mode 100644 index 0000000..8e1c1a3 --- /dev/null +++ b/src/components/HomeHeader.tsx @@ -0,0 +1,244 @@ +import React from 'react'; +import { ServiceO } from '@react-vant/icons'; +import { instance } from '../utils/api'; +import { Image, Toast, Search, Popup, PopupPosition, ConfigProvider, List, Cell } from 'react-vant' +import { useState, useEffect, useCallback } from 'react'; +import defaultIcon from '../assets/images/3.png'; +import './HomeHeader.css' +import { Like } from '@react-vant/icons'; + + +const HomeHeader = ({ setCurrentSong, search, setSearch, userinfo, list, setList }) => { + const [isRecording, setIsRecording] = useState(false); + const [mediaRecorder, setMediaRecorder] = useState(null); + const [audioContext, setAudioContext] = useState(null); + const [audioInput, setAudioInput] = useState(null); + const [recorder, setRecorder] = useState(null); + const [state, setState] = useState('') + const wrapperRef = React.useRef(null); + + const [finished, setFinished] = useState(false) + + const onLoad = async () => { + const data = await instance.post('/likeList') + if (data.data.code === 200) { + setList(data.data.datas); + setFinished(true); + } + } + + const theme = { + // '--rv-popup-title-color': '#ffffff', + '--rv-popup-background-color': '#FFB6C1', + } + + const handleLikeClick = (music) => { + console.log("click song", music) + setCurrentSong(music); + } + + function writeString(view, offset, string) { + for (let i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + } + function floatTo16BitPCM(output, offset, input) { + for (let i = 0; i < input.length; i++, offset += 2) { + let s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } + } + function writeAudioBufferToWav(audioBuffer, numChannels, sampleRate) { + const buffer = new ArrayBuffer(44 + audioBuffer.length * 2); + const view = new DataView(buffer); + writeString(view, 0, 'RIFF'); + view.setUint32(4, 36 + audioBuffer.length * 2, true); + writeString(view, 8, 'WAVE'); + writeString(view, 12, 'fmt '); + view.setUint32(16, 16, true); + view.setUint16(20, 1, true); + view.setUint16(22, numChannels, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate * numChannels * 2, true); + view.setUint16(32, numChannels * 2, true); + view.setUint16(34, 16, true); + writeString(view, 36, 'data'); + view.setUint32(40, audioBuffer.length * 2, true); + floatTo16BitPCM(view, 44, audioBuffer); + return new Blob([view], { type: 'audio/wav' }); + } + const stopRecording = async () => { + if (audioContext && audioInput && recorder && mediaRecorder && isRecording) { + setIsRecording(false); // 立即设置为非录音状态 + if (mediaRecorder.stream) { + mediaRecorder.stream.getTracks().forEach(track => track.stop()); + } + recorder.disconnect(); + audioInput.disconnect(); + if (audioContext.state !== 'closed') { + await audioContext.close(); + } + const audioData = mediaRecorder.audioChunks.reduce((acc, chunk) => { + return new Float32Array([...acc, ...chunk]); + }, new Float32Array()); + const wavBlob = writeAudioBufferToWav(audioData, 1, audioContext.sampleRate); + setAudioContext(null); + setAudioInput(null); + setRecorder(null); + setMediaRecorder(null); + Toast.success('识别中...'); + await sendAudioToBackend(wavBlob); + } + }; + const startRecording = useCallback(async () => { + if (isRecording) { + await stopRecording(); + } + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const context = new (window.AudioContext || window.webkitAudioContext)(); + const source = context.createMediaStreamSource(stream); + const node = context.createScriptProcessor(4096, 1, 1); + let audioChunks = []; + node.onaudioprocess = (e) => { + const inputBuffer = e.inputBuffer; + const channelData = inputBuffer.getChannelData(0); + audioChunks.push(new Float32Array(channelData)); + }; + source.connect(node); + node.connect(context.destination); + setAudioContext(context); + setAudioInput(source); + setRecorder(node); + setMediaRecorder({ audioChunks, stream }); + setIsRecording(true); + Toast.success('开始识别,再次点击右上角按钮结束识别'); + } catch (error) { + console.error('录音失败:', error); + Toast.fail('无法访问麦克风'); + } + }, [isRecording, stopRecording]); + const sendAudioToBackend = async (audioBlob) => { + try { + const formData = new FormData(); + const timestamp = Date.now(); + formData.append('audio', audioBlob, `recording_${timestamp}.wav`); + const response = await instance.post('/uploadAudio', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + Toast.success({ + message: "识别结果:《" + response.data.title + "》," + response.data.artist, + duration: 5000 // 设置持续时间为 5000 毫秒(5 秒) + }); + } catch (error) { + console.error('音频上传失败:', error); + Toast.fail({ + message: '音频上传失败', + duration: 5000 // 错误消息也设置为 5 秒 + }); + } + }; + useEffect(() => { + return () => { + if (audioContext && audioContext.state !== 'closed') { + audioContext.close(); + } + if (audioInput) { + audioInput.disconnect(); + } + if (recorder) { + recorder.disconnect(); + } + if (mediaRecorder && mediaRecorder.stream) { + mediaRecorder.stream.getTracks().forEach(track => track.stop()); + } + }; + }, [audioContext, audioInput, recorder, mediaRecorder]); + + return <> + { + userinfo !== null && JSON.stringify(userinfo) !== '{}' && ( + +
+ wrapperRef.current} + visible={state === 'left'} + title='个人中心' + style={{ width: '43%', height: '100%' }} + position='left' + round + onClose={() => setState('')} + > + + +
+

{userinfo.nickname}

+

{userinfo.username}

+
+
+ + + {list.map((a, i) => ( + { handleLikeClick(a) }}>{a.title} {a.artist} + ))} + +
+ +
+ +
+
+
+ ) + } + + +
+ setState('left')} style={{ + marginTop: '6px', + }} /> + { + const response = await instance.post("/search", { "title": val }); + if (response.data instanceof Array) { + if (response.data.length > 0) { + setCurrentSong(response.data[0]); + } + } else { + Toast.fail(response.data) + } + }} + onCancel={() => { + setSearch(''); + }} + onClear={() => { + setSearch(''); + }} + /> + + +
+ +} + +export default HomeHeader; diff --git a/src/components/MusicPlayer.tsx b/src/components/MusicPlayer.tsx index c0b5529..d513e54 100644 --- a/src/components/MusicPlayer.tsx +++ b/src/components/MusicPlayer.tsx @@ -12,7 +12,7 @@ const formatTime = (seconds: number): string => { return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; }; -const MusicPlayer = ({ currentSong, onPrevSong, onNextSong, onError, setIsShowPlayList, setCurrentSong, updateSongInPlaylist }) => { +const MusicPlayer = ({ currentSong, onPrevSong, onNextSong, onError, setIsShowPlayList, setCurrentSong, updateSongInPlaylist, likeList, setLikeList }) => { const [playingMusic, setPlayingMusic] = useState(currentSong); const [isPlaying, setIsPlaying] = useState(true); const playerRef = useRef(null); @@ -23,8 +23,11 @@ const MusicPlayer = ({ currentSong, onPrevSong, onNextSong, onError, setIsShowPl const handleLike = async (status) => { const updatedSong = { ...currentSong, likeState: status }; setCurrentSong(updatedSong); - console.log('准备发送的数据:', { song: updatedSong }); - + if (status === 1) likeList.push(updatedSong); + else if (status === 0) { + const index = likeList.findIndex(song => song.id === updatedSong.id); + likeList.splice(index, 1); + } try { const response = await instance.post('/like', { id: updatedSong.id, diff --git a/src/utils/LocalStorageUtil.js b/src/utils/LocalStorageUtil.js new file mode 100644 index 0000000..d2d5e8d --- /dev/null +++ b/src/utils/LocalStorageUtil.js @@ -0,0 +1,41 @@ +class LocalStorageUtil { + // 设置键值对 + static setItem(key, value) { + if (typeof value === 'object') { + value = JSON.stringify(value); // 将对象转换为字符串存储 + } + localStorage.setItem(key, value); + } + + // 获取值 + static getItem(key) { + const value = localStorage.getItem(key); + try { + return JSON.parse(value); // 尝试将字符串转换回对象 + } catch (e) { + return value; // 如果不是 JSON 字符串,则返回原始值 + } + } + + // 删除键值对 + static removeItem(key) { + localStorage.removeItem(key); + } + + // 清空所有 localStorage 的内容 + static clear() { + localStorage.clear(); + } + + // 判断某个 key 是否存在 + static hasItem(key) { + return localStorage.getItem(key) !== null; + } + + // 获取所有键名 + static getAllKeys() { + return Object.keys(localStorage); + } + } + + export default LocalStorageUtil; \ No newline at end of file diff --git a/src/utils/api.js b/src/utils/api.js index 7a13472..087125d 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,10 +1,31 @@ import axios from 'axios'; +import LocalStorageUtil from './LocalStorageUtil'; export const instance = axios.create({ - baseURL: 'https://app102.acapp.acwing.com.cn/api', - // baseURL: 'http://localhost:8809/api', - + // baseURL: 'https://app102.acapp.acwing.com.cn/api', + baseURL: 'http://localhost:8809/api', headers: { 'Content-Type': 'application/json', } -}); \ No newline at end of file +}); + +// 添加请求拦截器,在每次发送请求之前,检查是否有 token +instance.interceptors.request.use( + (config) => { + // 从 LocalStorageUtil 中获取 userinfo + const userinfo = LocalStorageUtil.getItem('userinfo'); + + // 如果存在 token,则将其添加到请求头中 + if (userinfo && userinfo.token) { + config.headers['Authorization'] = userinfo.token; + } + + return config; + }, + (error) => { + // 处理请求错误 + return Promise.reject(error); + } +); + +export default instance;