Merge pull request #62 from kenken-xr/master

react 、 readme
This commit is contained in:
若如意
2022-04-11 09:41:54 +08:00
committed by GitHub
79 changed files with 36946 additions and 880 deletions

View File

@@ -50,6 +50,18 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
- 测试用户名: `11` 测试密码: `111111` (连接远程服务器,具有推荐功能,优先使用这个)
- 测试用户名: `user` 测试密码: `pass` (mock数据模拟远程服务器获取不到数据时使用没有推荐功能)
<br>
#### react
**源代码:**
[GitHub](https://github.com/datawhalechina/fun-rec/tree/master/codes/news_recsys/news_rec_web/react-fun-rec)  [Gitee](https://gitee.com/theNeverLemon/react-fun-rec)
**演示链接:**
http://theneverlemon.gitee.io/react-fun-rec/
- 测试用户名: `user` 测试密码: `pass` (mock数据模拟远程服务器获取不到数据时使用没有推荐功能)
-
---
@@ -62,7 +74,7 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/登录.jpg" width = "30%" height = "30%" alt="登录"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%99%BB%E5%BD%95.jpg" width = "30%" height = "30%" alt="登录"/>
</div>
@@ -77,7 +89,7 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
- 注册成功后将跳转至主页面
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/注册.jpg" width = "30%" height = "30%" alt="注册"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%B3%A8%E5%86%8C.jpg" width = "30%" height = "30%" alt="注册"/>
</div>
@@ -88,8 +100,8 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
- 点进新闻详情页后阅读次数会实时增加
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/推荐.jpg" width = "30%" height = "30%" alt="推荐"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/热门.jpg" width = "30%" height = "30%" alt="热门"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%8E%A8%E8%8D%90.jpg" width = "30%" height = "30%" alt="推荐"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%83%AD%E9%97%A8.jpg" width = "30%" height = "30%" alt="热门"/>
</div>
@@ -100,8 +112,8 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
- 底部点击`喜欢`或者`收藏`可以记录将当前用户行为,并在列表页相应增加
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/新闻详情.jpg" width = "30%" height = "30%" alt="新闻详情"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/新闻详情2.jpg" width = "30%" height = "30%" alt="新闻详情2"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%85.jpg" width = "30%" height = "30%" alt="新闻详情"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%852.jpg" width = "30%" height = "30%" alt="新闻详情2"/>
</div>
- [X] **个人中心** —— 记录用户的头像和用户名
@@ -113,6 +125,6 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
- 点击`退出登录`回到登录页
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/个人中心1.jpg" width = "30%" height = "30%" alt="个人中心1"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/个人中心2.jpg" width = "30%" height = "30%" alt="个人中心2"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%831.jpg" width = "30%" height = "30%" alt="个人中心1"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%832.jpg" width = "30%" height = "30%" alt="个人中心2"/>
</div>

View File

@@ -44,8 +44,11 @@
- `忘记密码` 暂时没有写逻辑不能使用
如果获取不到远程数据库 将使用mock进行数据模拟
测试用户名`user` 密码`pass`
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/登录.jpg" width = "30%" height = "30%" alt="登录"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%99%BB%E5%BD%95.jpg" width = "30%" height = "30%" alt="登录"/>
</div>
@@ -60,7 +63,7 @@
- 注册成功后将跳转至主页面
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/注册.jpg" width = "30%" height = "30%" alt="注册"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%B3%A8%E5%86%8C.jpg" width = "30%" height = "30%" alt="注册"/>
</div>
@@ -70,9 +73,9 @@
- 点进新闻详情页后阅读次数会实时增加
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/推荐.jpg" width = "30%" height = "30%" alt="推荐"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/热门.jpg" width = "30%" height = "30%" alt="热门"/>
<div align="center">
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%8E%A8%E8%8D%90.jpg" width = "30%" height = "30%" alt="推荐"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%83%AD%E9%97%A8.jpg" width = "30%" height = "30%" alt="热门"/>
</div>
@@ -83,8 +86,8 @@
- 底部点击`喜欢`或者`收藏`可以记录将当前用户行为,并在列表页相应增加
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/新闻详情.jpg" width = "30%" height = "30%" alt="新闻详情"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/新闻详情2.jpg" width = "30%" height = "30%" alt="新闻详情2"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%85.jpg" width = "30%" height = "30%" alt="新闻详情"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%852.jpg" width = "30%" height = "30%" alt="新闻详情2"/>
</div>
- [X] **个人中心** —— 记录用户的头像和用户名
@@ -94,8 +97,8 @@
- 显示DataWhale相关介绍
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/个人中心1.jpg" width = "30%" height = "30%" alt="个人中心1"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/个人中心2.jpg" width = "30%" height = "30%" alt="个人中心2"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%831.jpg" width = "30%" height = "30%" alt="个人中心1"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%832.jpg" width = "30%" height = "30%" alt="个人中心2"/>
</div>

View File

@@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.DS_Store
node_modules
build
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"pkief.material-icon-theme",
"PKief.material-icon-theme"
]
}

View File

@@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
{
"name": "react-fun-rec",
"version": "0.1.0",
"private": true,
"homepage": ".",
"dependencies": {
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.4.1",
"@types/node": "^16.11.26",
"@types/react": "^17.0.41",
"@types/react-dom": "^17.0.14",
"antd-mobile": "^5.7.2",
"axios": "^0.26.1",
"crypto-js": "^4.1.1",
"js-pinyin": "^0.1.9",
"mockjs": "^1.1.0",
"node-sass": "^7.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.6",
"react-router-dom": "^6.2.2",
"react-scripts": "5.0.0",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.4.1",
"sass-loader": "^12.6.0",
"typescript": "^4.6.2",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"gh-pages": "^3.2.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>fun-rec</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,627 @@
**演示链接:**
http://theneverlemon.gitee.io/react-fun-rec
- 测试用户名: `user` 测试密码: `pass` (mock数据模拟远程服务器获取不到数据时使用没有推荐功能)
---
# 新闻推荐系统
+ 基于react的框架antd-mobile UI的基本使用。
+ TypeScript的使用。
+ 基于nodejs的npm包管理工具。
+ react路由的相关知识路由的配置和渲染以及路由守卫的使用。
+ axios的使用,项目里调用接口都是用的这个异步ajax插件。
+ 全局组件的使用。
---
### 运行
1. 跳转到前端项目文件目录:`cd react-fun-rec`
2. 本地安装node环境在项目根目录命令行输入命令`npm install`安装依赖包
如果因为版本或者网络问题下载失败请执行`npm install -g cnpm -registry=https://registry.npm.taobao.org/
``cnpm install`
1. 启动前端服务:`npm start`
2. 本机访问地址`http://localhost:3000/#/`
3. 点击`F12`或者右键选择`检查`打开`开发者模式`,选中移动端浏览(点击左上角箭头右边的手机按钮)开始体验
---
### 目标功能
- [X] **用户登录** —— 老用户登录
- `记住我`可以保存将登录信息保存7天
测试用户名`user` 密码`pass`
<div align="center">
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%99%BB%E5%BD%95.jpg" width = "30%" height = "30%" alt="登录"/>
</div>
- [X] **用户注册** —— 新用户注册信息
- 用户名可以为英文和数字
- 密码是大于6位的英文和数字
- 年龄是1-100的整数
- 注册成功后将跳转至主页面
<div align="center">
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%B3%A8%E5%86%8C.jpg" width = "30%" height = "30%" alt="注册"/>
</div>
- [X] **推荐页及热门页内容显示** —— 根据不同用户个性化显示不同新闻内容
- 推荐页和热门页之间的切换
- 点进新闻详情页后阅读次数会实时增加
<div align="center">
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%8E%A8%E8%8D%90.jpg" width = "30%" height = "30%" alt="推荐"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%83%AD%E9%97%A8.jpg" width = "30%" height = "30%" alt="热门"/>
</div>
- [x] **新闻详情** —— 显示当前新闻的详细信息
- 显示标题、内容等信息
- 底部点击`喜欢`或者`收藏`可以记录将当前用户行为,并在列表页相应增加
<div align="center">
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%85.jpg" width = "30%" height = "30%" alt="新闻详情"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%852.jpg" width = "30%" height = "30%" alt="新闻详情2"/>
</div>
- [X] **个人中心** —— 记录用户的头像和用户名
- 显示头像和登录名头像暂时统一为DataWhle图标
- 显示DataWhale相关介绍
<div align="center">
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%831.jpg" width = "30%" height = "30%" alt="个人中心1"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%832.jpg" width = "30%" height = "30%" alt="个人中心2"/>
</div>
### 项目目录
```markdown
├─package.json 项目配置文件
├─README.md 项目介绍
├─tsconfig.json ts配置文件
├─src
| ├─App.css 根组件样式
| ├─App.tsx 根组件
| ├─index.css 入口文件样式
| ├─index.tsx 入口文件
| ├─routes
| | └index.tsx 配置路由控制页面跳转
| ├─redux 状态管理
| | ├─actions.ts 定义更改状态时action的接收和返回值
| | ├─reducers.ts 汇总所有reducers
| | ├─store.ts 状态管理入口
| | ├─types
| | | ├─constant.ts 定义常量
| | | └─hooks.ts 重新定义useSelector
| | ├─reducers 根据action对状态进行更改
| | | ├─numChange.ts 阅读/喜欢/收藏次数的改变
| | | ├─pushLists.ts 列表添加数据
| | | └─userLog.ts 用户登录/注册/退出
| ├─pages
| | ├─SignUp 用户注册
| | | ├─index.scss
| | | └index.tsx
| | ├─SignIn 用户登录
| | | ├─index.scss
| | | └index.tsx
| | ├─RecLists 推荐页
| | | └index.tsx
| | ├─NewsInfo 新闻详情页
| | | ├─index.scss
| | | └index.tsx
| | ├─MySelf 个人中心
| | | ├─index.scss
| | | └index.tsx
| | ├─HotLists 热门页
| | | └index.tsx
| | ├─404 404页
| | | └index.tsx
| ├─mock
| | └index.ts 数据模拟
| ├─components
| | ├─BottomBar 底部导航
| | | ├─index.scss
| | | └index.tsx
| ├─assets
| | ├─type
| | | └─index.ts 定义所有数据的类型
| | ├─js
| | | ├─cookie.js 定义cookie的相关操作
| | | ├─encrypt.js 密码加密
| | ├─images
│ │ │ ├─ collects.png 未选中收藏
│ │ │ ├─ collects1.png 选中收藏
│ │ │ ├─ datawhale.png DataWhale头像
│ │ │ ├─ dw.png DataWhale二维码
│ │ │ ├─ favicon.ico 浏览器小图标
│ │ │ ├─ likes.png 未选中喜欢
│ │ │ └─ likes1.png 选中喜欢
| | ├─data
| | | ├─city.ts 城市选择数据
| | ├─css
| | | └─topBar.scss 顶部导航样式
└─public
├─favicon.ico 浏览器小图标
└─index.html 首页入口文件
```
---
### 数据获取
通过`mockjs`进行数据的模拟
定义获取数据的url和出现错误时的状态码
``` typescript
import { NewsInfo } from '../assets/type'
import store from '../redux/store'
let Mock = require('mockjs')
// 登录
Mock.mock('/login', 'post', (req: { body: string }) => {
const { username, passwd } = JSON.parse(req.body)
// 用户名默认为user 密码默认为pass
// 密码由前端加密后转换为h
if (username === 'user' && passwd === 'h') {
return {
code:200
}
} else if(username !== 'user'){
return {
code:502
}
}else if(passwd !== 'h'){
return {
code:501
}
} else {
return {
code:500
}
}
})
export { }
```
在登录时使用
```typescript
let successData = await axios.post("/login", res).then((res) => {
return res.data
})
if (successData.code === 200) {
Toast.show({
icon: 'success',
content: '登录成功',
})
} else if (successData.code === 500) {
Toast.show({
icon: 'fail',
content: '登陆失败',
})
} else if (successData.code === 501) {
Toast.show({
icon: 'fail',
content: '密码输入错误',
})
} else if (successData.code === 502) {
Toast.show({
icon: 'fail',
content: '用户名不存在',
})
}
```
### 路由守卫
用户为登录时通过外链访问推荐/热门/详情页会跳转至首页进行登录
#### App.tsx
在 `App` 组件中注册路由
在这里区分`auth`,通过`router.map`遍历所有路由
当`auth`值为`false`时,也就是(`!item.auth`),即不需要身份就可以进入的页面(登录和注册页),那么就渲染当前组件`< item.component />`
```typescript
if (!item.auth) {
return (
<Route key={i} path={item.path} element={
<Suspense fallback={
<>
<Skeleton.Title animated />
<Skeleton.Paragraph lineCount={20} animated />
</>
}>
< item.component />
</Suspense>
} />
)
```
当`auth`值为`true`时,也就是(`item.auth`),即需要身份才可以进入的页面(内容页),那么就通过`RequireAuth`函数判断是否有身份权限,如果有,渲染当前组件;如果没有,定位到登录
RequireAuth函数
通过是否有cookie判断在登录或者注册时候会存储一个cookie退出登录时再删掉
```typescript
function RequireAuth({ children }: { children: JSX.Element }) {
let location = useLocation()
if (!getCookie('openId')) {
Toast.show({
content: '暂未登录,请先登录',
})
let second = 1;
// 延迟一秒执行
const timer = setInterval(() => {
second--;
if (!second) {
clearInterval(timer);
// 手动清除 Toast
Toast.clear();
}
}, 1000);
return <Navigate to="/signIn" state={{ from: location }} replace />;
}
return children;
}
```
渲染的代码
```typescript
else {
return (
<Route key={i} path={item.path} element={
<Suspense fallback={
<>
<Skeleton.Title animated />
<Skeleton.Paragraph lineCount={20} animated />
</>
}>
<RequireAuth>
<item.component />
</RequireAuth>
</Suspense>
}>
</Route>
)
}
```
### 状态管理
![redux](https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-imgredux.png)
- action:动作的对象包含2个属性
- type标识属性, 值为字符串, 唯一, 必要属性
- data数据属性, 值类型任意, 可选属性
- reducer用于初始化状态、加工状态
- store将state、action、reducer联系在一起的对象
#### redux/actions.ts
引入常量变量
``` typescript
import { COLLECT_DECREASE,COLLECT_INCREASE, CLEAR_INFO } from './types/constant'
```
用户登录注册时的action
- type`signIn` / `signUp` / `CLEAR_INFO`
- data登录或者注册时提交的表单数据退出时清空数据
返回对应的type和data
``` typescript
export const userLog = (type: string, data: object) => ({ type, data })
```
#### redux/reducers/userLog.ts
引入常量和类型约束
``` typescript
import { userLogState } from '../../assets/type'
import { CLEAR_INFO, SIGNIN, SIGNUP } from '../types/constant'
```
定义初始状态
``` typescript
const userLogDefault: userLogState = {
type: SIGNIN,
data: {
username: '',
passwd: '',
}
}
```
通过用户的action定义对应的reducer
通过action.type判断不同的登录状态
``` typescript
export function userLog(preState = userLogDefault, action: userLogState) {
switch(action.type) {
case SIGNIN:
return ({ type: action.type, data: action.data })
case SIGNUP:
return ({ type: action.type, data: action.data })
case CLEAR_INFO:
return ({ type: action.type, data: action.data })
default:
return preState
}
}
```
#### redux/redecers.ts
汇总所有的reducer变为一个总的reducer
``` typescript
//引入combineReducers用于汇总多个reducer
import { combineReducers } from 'redux'
import {userLog} from './reducers/userLog'
import { pushRecLists, pushHotLists } from './reducers/pushLists'
import { readChange, likesChange, collectionsChange } from './reducers/numChange'
export default combineReducers({
userLog,
pushRecLists, pushHotLists,
readChange, likesChange, collectionsChange
})
```
#### redux/store.ts
创建redux的store对象只有一个store对象
引入
- createStore创建store对象
- applyMiddleware中间件对redux的扩展
- redux-thunk支持异步action
- rootReducer汇总之后的所有reducer
- redux-persist使用sessionstorage持久化数据解决页面刷新数据丢失的问题
- redux-devtools-extension在浏览器中可以使用redux扩展插件
``` typescript
import {createStore,applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'
import { composeWithDevTools } from 'redux-devtools-extension'
import { persistStore, persistReducer } from 'redux-persist'
import storageSession from 'redux-persist/lib/storage/session'
const storageConfig = {
key: 'root', // 必须
storage: storageSession, // 缓存机制
}
const myPersistReducer = persistReducer(storageConfig, rootReducer);
const store = createStore(myPersistReducer, composeWithDevTools(applyMiddleware(thunk)));
export const persistor = persistStore(store)
export default store;
```
#### 使用
##### 登录
``` typescript
let res = {
username: values.name,
passwd: encrypt.Decrypt(values.passwd)
};
const dispatch = useDispatch();
dispatch(userLog(SIGNIN, res))
```
##### 注册
``` typescript
const dispatch = useDispatch();
dispatch(userLog(SIGNUP, values))
```
##### 退出
``` typescript
const dispatch = useDispatch();
dispatch(userLog(CLEAR_INFO, []))
```
---
### 主要文件说明
#### assets/js/cookie.js
定义cookie的相关操作
定义了`setCookie`,`getCookie`,`clearCookie`三个函数
在用户登录注册时存入cookie
``` javascript
function setCookie(json, days) {
// 设置过期时间
let data = new Date(
new Date().getTime() + days * 24 * 60 * 60 * 1000
).toUTCString();
for (var key in json) {
document.cookie = key + "=" + json[key] + "; expires=" + data
}
}
```
获取cookie
``` javascript
function getCookie(name) {
var arr = document.cookie.match(new RegExp("(^| )" + name + "=([^;]*)(;|$)"));
if (arr != null) {
return unescape(arr[2])
} else {
return null
}
}
```
用户退出登录时删除cookie
``` javascript
function clearCookie(name) {
let json = {};
json[name] = '';
setCookie(json, -1)
}
```
#### router/index.tsx
定义路由相关配置,控制页面跳转
``` typescript
// 路由懒加载
import { lazy } from 'react'
// 引入路由的类型
import { Router } from '../assets/type'
const router: Array<Router> = [
{
path: '/signIn', // 路径
auth: false, // 是否需要身份验证
component: lazy(() => import('../pages/SignIn')) // 组件
},
{
path: '/recLists',
auth: true, // 登录后才可以访问
component: lazy(() => import('../pages/RecLists'))
}
]
export default router
```
#### SignIn.tsx/SignUp.tsx
存入cookie值
``` typescript
let loginInfo = {
LoginName: res.username,
openId: "asfafsfsfsdfsdfsdfdsf"
}
// checke:true--选中记住我 checke:false--未选中记住我
if (values.remember) {
// 调用setCookie方法同时传递需要存储的数据保存天数
setCookie(loginInfo, 7)
} else {
setCookie(loginInfo, 1)
}
```
#### NewsInfo.tsx
发送action请求
``` javascript
sendInfo() {
// 阅读
let val = {
user_name: user_name,
news_id: id,
action_time: Date.now(),
action_type: 'read',
}
// 喜欢
let val = {
user_name: user_name,
news_id: id,
action_time: Date.now(),
action_type: `likes:${isLike}`,
}
// 收藏
let val = {
user_name: user_name,
news_id: id,
action_time: Date.now(),
action_type: `collections::${isCollection}`,
}
// 发送对应请求
let successData = await axios.post("/action", val).then(res => {
return res
})
if (successData.status === 200) {
setIsCollection(!isCollection)
// 调用store中的actionChange函数控制次数的变化
if (isCollection === true) {
dispatch(collectionsDecrease({ lists, id }))
} else {
dispatch(collectionsIncrease({ lists, id }))
}
} else {
Toast.show({
content: '加载数据失败',
})
}
},
```

View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,77 @@
import './App.css';
import { Suspense } from 'react'
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
import { Skeleton, Toast } from 'antd-mobile'
import router from './routes'
import { getCookie } from './assets/js/cookie';
function App() {
return (
<Routes>
{
router.map((item, i) => {
if (!item.auth) {
return (
<Route key={i} path={item.path} element={
<Suspense fallback={
<>
<Skeleton.Title animated />
<Skeleton.Paragraph lineCount={20} animated />
</>
}>
< item.component />
</Suspense>
} />
)
} else {
return (
<Route key={i} path={item.path} element={
<Suspense fallback={
<>
<Skeleton.Title animated />
<Skeleton.Paragraph lineCount={20} animated />
</>
}>
<RequireAuth>
<item.component />
</RequireAuth>
</Suspense>
}>
</Route>
)
}
})
}
</Routes>
);
}
function RequireAuth({ children }: { children: JSX.Element }) {
let location = useLocation()
if (!getCookie('openId')) {
Toast.show({
content: '暂未登录,请先登录',
})
let second = 1;
// 延迟一秒执行
const timer = setInterval(() => {
second--;
if (!second) {
clearInterval(timer);
// 手动清除 Toast
Toast.clear();
}
}, 1000);
return <Navigate to="/signIn" state={{ from: location }} replace />;
}
return children;
}
export default App;

View File

@@ -0,0 +1,131 @@
.tabs {
left: 50%;
transform: translateX(-50%);
background: white;
width: 70%;
border-radius: 5px;
width: 100vw;
position: fixed;
top: 0;
background-color: white;
z-index: 9;
}
.tabs input[name=tab-control] {
display: none;
}
.tabs .content section h2,
.tabs ul li label {
font-family: "Montserrat";
font-weight: bold;
font-size: 18px;
}
.tabs ul {
list-style-type: none;
padding-left: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
align-content: flex-end;
}
.tabs ul li {
box-sizing: border-box;
flex: 1;
width: 25%;
padding: 8px;
text-align: center;
}
.tabs ul li label {
transition: all 0.3s ease-in-out;
padding: 5px auto;
overflow: hidden;
text-overflow: ellipsis;
display: block;
cursor: pointer;
transition: all 0.2s ease-in-out;
white-space: nowrap;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tabs ul li label svg {
height: 2em;
vertical-align: bottom;
margin-right: 0.2em;
transition: all 0.2s ease-in-out;
}
@-webkit-keyframes content {
from {
opacity: 0;
transform: translateY(5%);
}
to {
opacity: 1;
transform: translateY(0%);
}
}
@keyframes content {
from {
opacity: 0;
transform: translateY(5%);
}
to {
opacity: 1;
transform: translateY(0%);
}
}
@media (max-width: 600px) {
.tabs ul li label svg {
height: 1.5em;
}
.tabs ul li label br {
display: initial;
}
.tabs ul li label {
padding: 5px;
border-radius: 5px;
}
.tabs ul li label span {
display: initial;
}
}
.lists {
position: relative;
top: 70px;
padding: 0 10px;
}
.title {
font-size: 1.2rem;
color: black;
font-weight: bolder;
}
.cate {
font-size: 1rem;
color: #00F;
margin-right: .5rem;
padding: .08rem;
border: 1px solid #00F;
}
.discribe {
display: flex;
justify-content: space-between;
font-size: 1rem;
color: #918d8d;
padding-top: 12px;
}

View File

@@ -0,0 +1 @@
.tabs{left:50%;transform:translateX(-50%);background:white;width:70%;border-radius:5px;width:100vw;position:fixed;top:0;background-color:white;z-index:9}.tabs input[name=tab-control]{display:none}.tabs .content section h2,.tabs ul li label{font-family:"Montserrat";font-weight:bold;font-size:18px}.tabs ul{list-style-type:none;padding-left:0;display:flex;flex-direction:row;justify-content:space-between;align-items:center;flex-wrap:wrap;align-content:flex-end}.tabs ul li{box-sizing:border-box;flex:1;width:25%;padding:8px;text-align:center}.tabs ul li label{transition:all 0.3s ease-in-out;padding:5px auto;overflow:hidden;text-overflow:ellipsis;display:block;cursor:pointer;transition:all 0.2s ease-in-out;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tabs ul li label svg{height:2em;vertical-align:bottom;margin-right:0.2em;transition:all 0.2s ease-in-out}@-webkit-keyframes content{from{opacity:0;transform:translateY(5%)}to{opacity:1;transform:translateY(0%)}}@keyframes content{from{opacity:0;transform:translateY(5%)}to{opacity:1;transform:translateY(0%)}}@media (max-width: 600px){.tabs ul li label svg{height:1.5em}.tabs ul li label br{display:initial}.tabs ul li label{padding:5px;border-radius:5px}.tabs ul li label span{display:initial}}.lists{position:relative;top:70px;padding:0 10px}.title{font-size:1.2rem;color:black;font-weight:bolder}.cate{font-size:1rem;color:#00F;margin-right:.5rem;padding:.08rem;border:1px solid #00F}.discribe{display:flex;justify-content:space-between;font-size:1rem;color:#918d8d;padding-top:12px}

View File

@@ -0,0 +1,138 @@
.tabs {
left: 50%;
transform: translateX(-50%);
background: white;
width: 70%;
border-radius: 5px;
width: 100vw;
position: fixed;
top:0;
background-color: white;
z-index: 9
}
// 隐藏input默认样式
.tabs input[name=tab-control] {
display: none;
}
.tabs .content section h2,
.tabs ul li label {
font-family: "Montserrat";
font-weight: bold;
font-size: 18px;
}
.tabs ul {
list-style-type: none;
padding-left: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
align-content: flex-end;
}
.tabs ul li {
box-sizing: border-box;
flex: 1;
width: 25%;
padding: 8px;
text-align: center;
}
.tabs ul li label {
transition: all 0.3s ease-in-out;
padding: 5px auto;
overflow: hidden;
text-overflow: ellipsis;
display: block;
cursor: pointer;
transition: all 0.2s ease-in-out;
white-space: nowrap;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tabs ul li label svg {
height: 2em;
vertical-align: bottom;
margin-right: 0.2em;
transition: all 0.2s ease-in-out;
}
@-webkit-keyframes content {
from {
opacity: 0;
transform: translateY(5%);
}
to {
opacity: 1;
transform: translateY(0%);
}
}
@keyframes content {
from {
opacity: 0;
transform: translateY(5%);
}
to {
opacity: 1;
transform: translateY(0%);
}
}
@media (max-width: 600px) {
.tabs ul li label svg {
height: 1.5em;
}
.tabs ul li label br {
display: initial;
}
.tabs ul li label {
padding: 5px;
border-radius: 5px;
}
.tabs ul li label span {
display: initial;
}
}
.lists {
position: relative;
top: 70px;
padding: 0 10px;
}
.title {
font-size: 1.2rem;
color: black;
font-weight: bolder;
}
.cate {
font-size: 1rem;
color: #00F;
margin-right: .5rem;
padding: .08rem;
border: 1px solid #00F
}
.discribe {
display: flex;
justify-content: space-between;
font-size: 1rem;
color: #918d8d;
padding-top: 12px;
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

View File

@@ -0,0 +1,35 @@
// 保存cookie
// json 需要存储cookie的对象
// days 默认存储多少天
export function setCookie(json, days) {
// 设置过期时间
let data = new Date(
new Date().getTime() + days * 24 * 60 * 60 * 1000
).toUTCString();
for (var key in json) {
document.cookie = key + "=" + json[key] + "; expires=" + data
}
}
// 获取cookie
// name 需要获取cookie的key
export function getCookie(name) {
var arr = document.cookie.match(new RegExp("(^| )" + name + "=([^;]*)(;|$)"));
if (arr != null) {
return unescape(arr[2])
} else {
return null
}
}
// 删除cookie
// name 需要删除cookie的key
export function clearCookie(name) {
let json = {};
json[name] = '';
setCookie(json, -1)
}

View File

@@ -0,0 +1,27 @@
//引用AES源码js
const CryptoJS = require('crypto-js');
//十六位十六进制数作为密钥
const key = CryptoJS.enc.Utf8.parse("1234123412ABCDEF");
//十六位十六进制数作为密钥偏移量
const iv = CryptoJS.enc.Utf8.parse('ABCDEF1234123412');
//解密方法
function Decrypt(word) {
let encryptedHexStr = CryptoJS.enc.Hex.parse(word);
let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
}
//加密方法
function Encrypt(word) {
let srcs = CryptoJS.enc.Utf8.parse(word);
let encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return encrypted.ciphertext.toString().toUpperCase();
}
export default {
Decrypt ,
Encrypt
}

View File

@@ -0,0 +1,69 @@
// redux存的用户信息
export interface userLogState {
type: string,
data: {
username: string,
passwd: string,
passwd2?: string,
age?: string,
area?: string,
gender?: string,
remember?: boolean
}
}
// 用户登录的信息
export interface UserSignIn {
name: string
passwd: string
remember: boolean
}
// 用户注册的信息
export interface UserSignUp {
name: string
passwd: string
passwd2: string
age: string
gender: string
area: string
}
// 从列表页进入内容页传入的params类型
export interface ToListInfo {
id: number
cate: string
}
// 列表页的展示数据
export interface Lists {
news_id: number
cate: string
title: string
ctime: string
read_num: number
likes: number
collections: number
}
// 新闻详情页的数据
export interface NewsInfo {
cate: string
news_id: number
title: string
content: string
ctime: string
read_num: number
likes: number
collections: number
}
// 路由
export interface Router {
name ?: string,
path: string,
auth: boolean,
children ?: Array < Router >,
component: any
}

View File

@@ -0,0 +1,7 @@
.adm-tab-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: white;
}

View File

@@ -0,0 +1 @@
.adm-tab-bar{position:fixed;bottom:0;left:0;width:100%;background-color:white}

View File

@@ -0,0 +1,8 @@
// tabBar固定在底部
.adm-tab-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: white;
}

View File

@@ -0,0 +1,52 @@
import React from 'react'
import {
useNavigate,
useLocation,
} from 'react-router-dom'
import { NavBar, TabBar } from 'antd-mobile'
import {
AppOutline,
UserOutline,
} from 'antd-mobile-icons'
import './index.scss'
const BottomBar = () => {
const navigate = useNavigate()
const location = useLocation()
const { pathname } = location
const setRouteActive = (value: string) => {
navigate(value, { replace: true })
}
const tabs = [
{
key: '/recLists',
title: '首页',
icon: <AppOutline />,
},
{
key: '/mySelf',
title: '我的',
icon: <UserOutline />
},
]
return (
<TabBar activeKey={pathname} onChange={value => setRouteActive(value)} safeArea>
{tabs.map(item => (
<TabBar.Item key={item.key} icon={item.icon} title={item.title} />
))}
</TabBar>
)
}
export default () => {
return (
<div className='bottom'>
<BottomBar />
</div>
)
}

View File

@@ -0,0 +1,4 @@
*{
padding:0;
margin:0;
}

View File

@@ -0,0 +1,22 @@
import ReactDOM from 'react-dom';
import { BrowserRouter } from "react-router-dom";
import './index.css';
import App from './App';
import store, { persistor } from './redux/store'
import { Provider } from 'react-redux'
import './mock'
import { PersistGate } from 'redux-persist/lib/integration/react';
ReactDOM.render(
/* 此处需要用Provider包裹App目的是让App所有的后代容器组件都能接收到store */
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<BrowserRouter>
<App/>
</BrowserRouter>
</PersistGate>
</Provider>,
document.getElementById('root')
);

View File

@@ -0,0 +1,106 @@
import { NewsInfo } from '../assets/type'
import store from '../redux/store'
let Mock = require('mockjs')
// 登录
Mock.mock('/login', 'post', (req: { body: string }) => {
const { username, passwd } = JSON.parse(req.body)
// 用户名默认为user 密码默认为pass
// 密码由前端加密后转换为h
if (username === 'user' && passwd === 'h') {
return {
code:200
}
} else if(username !== 'user'){
return {
code:502
}
}else if(passwd !== 'h'){
return {
code:501
}
} else {
return {
code:500
}
}
})
// 注册
Mock.mock('/register', 'post', (req: { body: string }) => {
const { username } = JSON.parse(req.body)
if (username === 'user') {
return {
code:500
}
} else if(username !== ''){
return {
code:200
}
}
})
let Data: NewsInfo[] = []
for(let i = 0; i < 10; i ++){
Data.push({
cate:Mock.Random.cword(2),
// news_id:Mock.Random.string('number', 9),
news_id: Number(Mock.mock('@id')),
title:Mock.Random.ctitle(5,12),
content:Mock.Random.cparagraph(3)+'责任编辑'+Mock.Random.cname(),
ctime:Mock.Random.date('yyyy-MM-dd'),
read_num:Mock.Random.natural(100, 1000),
likes:Mock.Random.natural(1, 100),
collections:Mock.Random.natural(1, 100),
})
}
Mock.mock('/recList', 'get', () => {
return {
code:200,
data:Data
}
})
Mock.mock('/hotList', 'get', () => {
return {
code:200,
data:Data
}
})
Mock.mock(RegExp("/newsInfo" + ".*"), 'get', (option: { url: string }) => {
var res,
id = Number(option.url.split(/news_id?=/)[1].split(/&user_name=/)[0]),
// data = (store.state.recList.length > store.state.hotList.length) ? store.state.recList : store.state.hotList
data = store.getState().pushRecLists
for(let i=0; i< data.length; i++){
if (id === data[i].news_id) {
res = data[i]
}
}
return {
status:200,
data:{
cate:res.cate,
news_id:res.news_id,
title:res.title,
content:res.content,
ctime:res.ctime,
read_num:res.read_num,
likes:res.likes,
collections:res.collections,
}
}
})
Mock.mock('/action', 'post', () => {
return {
status:200,
}
})
export { }

View File

@@ -0,0 +1,81 @@
@import url("https://fonts.googleapis.com/css?family=Noto+Sans:700");
@import url("https://fonts.googleapis.com/css?family=Lato");
#main {
transition: all 0.6s;
font-family: 'Lato', sans-serif;
background: #D4DFE6;
color: rgba(88, 85, 85, 0.805);
margin: 0;
display: flex;
width: 100%;
height: 100vh;
text-align: center;
flex-direction: column;
justify-content: center;
align-items: center;
}
.fof h1 {
font-size: 50px;
display: inline-block;
padding-right: 12px;
animation: type .5s alternate infinite;
}
.MainDescription {
font-size: 1.2rem;
font-weight: lighter;
padding: 3rem;
}
@keyframes type {
from {
box-shadow: inset -3px 0px 0px #888;
}
to {
box-shadow: inset -3px 0px 0px transparent;
}
}
.MainGraphic {
position: relative;
}
.Cog {
width: 10rem;
height: 10rem;
fill: #6AAFE6;
transition: easeInOutQuint();
animation: CogAnimation 5s infinite;
}
.Hummingbird {
position: absolute;
width: 3rem;
height: 3rem;
fill: #30A9DE;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.err_btn {
background: #fff;
color: rgba(88, 85, 85, 0.805);
border: 2px solid #6AAFE6;
padding: 15px 30px;
border-radius: 5px;
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);
cursor: pointer;
font-size: 13pt;
transition: background 0.5s;
}
@keyframes CogAnimation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1 @@
@import url("https://fonts.googleapis.com/css?family=Noto+Sans:700");@import url("https://fonts.googleapis.com/css?family=Lato");#main{transition:all 0.6s;font-family:'Lato', sans-serif;background:#D4DFE6;color:rgba(88,85,85,0.805);margin:0;display:flex;width:100%;height:100vh;text-align:center;flex-direction:column;justify-content:center;align-items:center}.fof h1{font-size:50px;display:inline-block;padding-right:12px;animation:type .5s alternate infinite}.MainDescription{font-size:1.2rem;font-weight:lighter;padding:3rem}@keyframes type{from{box-shadow:inset -3px 0px 0px #888}to{box-shadow:inset -3px 0px 0px transparent}}.MainGraphic{position:relative}.Cog{width:10rem;height:10rem;fill:#6AAFE6;transition:easeInOutQuint();animation:CogAnimation 5s infinite}.Hummingbird{position:absolute;width:3rem;height:3rem;fill:#30A9DE;left:50%;top:50%;transform:translate(-50%, -50%)}.err_btn{background:#fff;color:rgba(88,85,85,0.805);border:2px solid #6AAFE6;padding:15px 30px;border-radius:5px;box-shadow:0px 8px 15px rgba(0,0,0,0.1);cursor:pointer;font-size:13pt;transition:background 0.5s}@keyframes CogAnimation{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}

View File

@@ -0,0 +1,73 @@
@import url('https://fonts.googleapis.com/css?family=Noto+Sans:700');
@import url('https://fonts.googleapis.com/css?family=Lato');
#main{
transition: all 0.6s;
font-family: 'Lato', sans-serif;
background: #D4DFE6;
color: rgba(88, 85, 85, 0.805);
margin: 0;
display: flex;
width: 100%;
height: 100vh;
text-align: center;
flex-direction: column;
justify-content: center;
align-items: center;
}
.fof h1{
font-size: 50px;
display: inline-block;
padding-right: 12px;
animation: type .5s alternate infinite;
}
.MainDescription {
font-size: 1.2rem;
font-weight: lighter;
padding: 3rem;
}
@keyframes type{
from{box-shadow: inset -3px 0px 0px #888;}
to{box-shadow: inset -3px 0px 0px transparent;}
}
.MainGraphic {
position: relative;
}
.Cog {
width: 10rem;
height: 10rem;
fill: #6AAFE6;
transition: easeInOutQuint();
animation: CogAnimation 5s infinite;
}
.Hummingbird{
position: absolute;
width: 3rem;
height: 3rem;
fill: #30A9DE;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
.err_btn{
background:#fff;
color: rgba(88, 85, 85, 0.805);
border:2px solid #6AAFE6;
padding:15px 30px;
border-radius:5px;
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);
cursor:pointer;
font-size:13pt;
transition:background 0.5s;
}
@keyframes CogAnimation {
0% {transform: rotate(0deg);}
100% {transform: rotate(360deg);}
}

View File

@@ -0,0 +1,28 @@
import { useNavigate } from 'react-router-dom'
import './index.scss'
export default function NotFound() {
const navigate = useNavigate()
return (
<div id="main">
<div className="MainGraphic">
<svg className="Hummingbird" viewBox="0 0 55 41" xmlns="http://www.w3.org/2000/svg"><path d="M35.5 5L54.7.6H32.3L35.5 5zM12.4 40.8l10.3-10.1-6.2-6.7-4.1 16.8zM33.8 5.3L30.5.8l-5.4 4 8.7.5zM20.8 4.6L8.8 0l1.9 4.1 10.1.5zM0 5l15.2 15.4 7.5-14.2L0 5zM34.2 6.8l-9.9-.5-8 15.2 7.4 8.1 8-7.9 2.5-14.9z"/></svg>
<svg className="Cog" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M29.18 19.07c-1.678-2.908-.668-6.634 2.256-8.328L28.29 5.295c-.897.527-1.942.83-3.057.83-3.36 0-6.085-2.743-6.085-6.126h-6.29c.01 1.043-.25 2.102-.81 3.07-1.68 2.907-5.41 3.896-8.34 2.21L.566 10.727c.905.515 1.69 1.268 2.246 2.234 1.677 2.904.673 6.624-2.24 8.32l3.145 5.447c.895-.522 1.935-.82 3.044-.82 3.35 0 6.066 2.725 6.083 6.092h6.29c-.004-1.035.258-2.08.81-3.04 1.676-2.902 5.4-3.893 8.325-2.218l3.145-5.447c-.9-.515-1.678-1.266-2.232-2.226zM16 22.48c-3.578 0-6.48-2.902-6.48-6.48S12.423 9.52 16 9.52c3.578 0 6.48 2.902 6.48 6.48s-2.902 6.48-6.48 6.48z"/></svg>
</div>
<div className="fof">
<h1>Error 404</h1>
</div>
<p className="MainDescription">
OOPS. Looks like the page you're looking for no longer exists
</p>
<button className="err_btn" onClick={() => { navigate('/signIn') }}></button>
</div>
)
}

View File

@@ -0,0 +1,99 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { InfiniteScroll, List } from 'antd-mobile'
import { useSelector } from '../../redux/types/hooks'
import { useDispatch } from 'react-redux'
import axios from 'axios'
import BottomBar from '../../components/BottomBar'
import '../../assets/css/topBar.scss'
//引入action
import { pushHotLists } from '../../redux/actions'
import { Lists } from '../../assets/type'
export default function HotLists(){
const navigate = useNavigate()
const dispatch = useDispatch();
const formData = useSelector(state => {
return state.pushHotLists
})
const [hasMore, setHasMore] = useState(true)
async function loadMore() {
let url = '/recList'
let successData = await axios.get(url).then((res) => {
return res.data
})
dispatch(pushHotLists([...formData, ...successData.data]))
setHasMore(formData.length < 40)
}
return (
<>
<div className="tabs">
<input type="radio" id="tab1" name="tab-control" />
<input type="radio" id="tab2" name="tab-control" />
<ul>
<li title="Features" style={{borderBottom: '2px solid #bec5cf'}} onClick={() => {navigate('/recLists')}}>
<label htmlFor="tab1" role="button" style={{cursor: 'default',color: '#bec5cf'}}>
<svg viewBox="0 0 24 24" style={{fill: '#bec5cf'}}>
<path d="M14,2A8,8 0 0,0 6,10A8,8 0 0,0 14,18A8,8 0 0,0 22,10H20C20,13.32 17.32,16 14,16A6,6 0 0,1 8,10A6,6 0 0,1 14,4C14.43,4 14.86,4.05 15.27,4.14L16.88,2.54C15.96,2.18 15,2 14,2M20.59,3.58L14,10.17L11.62,7.79L10.21,9.21L14,13L22,5M4.93,5.82C3.08,7.34 2,9.61 2,12A8,8 0 0,0 10,20C10.64,20 11.27,19.92 11.88,19.77C10.12,19.38 8.5,18.5 7.17,17.29C5.22,16.25 4,14.21 4,12C4,11.7 4.03,11.41 4.07,11.11C4.03,10.74 4,10.37 4,10C4,8.56 4.32,7.13 4.93,5.82Z" />
</svg>
<span></span>
</label>
</li>
<li title="Delivery Contents" style={{borderBottom: '2px solid #428bff'}}>
<label htmlFor="tab2" role="button" style={{cursor: 'default',color: '#428bff'}}>
<svg viewBox="0 0 24 24" style={{fill: '#428bff'}}>
<path d="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" />
</svg>
<span></span>
</label>
</li>
</ul>
</div>
<List>
{formData.map((item: Lists, index: number) => (
<Link
style={{ textDecoration:'none'}}
key={index}
to={`/newsInfo/${item.news_id}`}
state={{
id: item.news_id,
likes: item.likes,
collections: item.collections,
cate: item.cate,
read_num:item.read_num,
}}>
<List.Item key={index}>
<p>
<span className="cate">{item.cate}</span>
<span className='title'>{ item.title }</span>
</p>
<p className="discribe">
<span className="ctime">{ item.ctime} </span>
<span className="read_num">{item.read_num}</span>
<span className="likes">:{item.likes}</span>
<span className="collections">:{item.collections}</span>
</p>
</List.Item>
</Link>
))}
</List>
<InfiniteScroll loadMore={loadMore} hasMore={hasMore} />
<BottomBar />
</>
)
}

View File

@@ -0,0 +1,95 @@
.profile {
margin: auto;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px;
-ms-overflow-style: none;
scrollbar-width: none;
overflow-y: scroll;
}
::-webkit-scrollbar {
display: none;
}
.header-color {
padding-bottom: 150px;
width: 100vh;
background: #4f759b;
}
.profile-pic img {
height: 40vw;
width: 40vw;
border-radius: 50%;
border: 10px solid #ffffff;
margin-top: -100px;
}
.myself-title {
margin-bottom: 25px;
}
h1 {
font-size: 32px;
font-weight: 700;
color: #131b23;
margin-bottom: 10px;
letter-spacing: 0.025em;
}
.description {
margin-bottom: 25px;
color: #131b23;
letter-spacing: 0.01em;
}
.description p:not(:last-child) {
margin-bottom: 5px;
}
button {
font-family: "Open Sans", sans-serif;
color: #ffffff;
background: #4f759b;
font-size: 18px;
font-weight: 600;
letter-spacing: 0.025em;
border: none;
border-radius: 15px;
min-height: 35px;
width: 100px;
margin-bottom: 25px;
transition: all 0.2s ease;
cursor: pointer;
}
button:hover {
width: 115px;
background: #4f759b;
}
.about {
padding-top: 20px;
padding-bottom: 20px;
text-align: center;
font-weight: bold;
font-size: 1.5rem;
}
.datawhale {
font-size: 1.3rem;
padding: 0 20px 20px 20px;
text-indent: 2em;
line-height: 2.4rem;
}
.dwimg {
width: 150px;
height: 150px;
display: block;
margin: 15px auto 100px auto;
}

View File

@@ -0,0 +1 @@
.profile{margin:auto;background:#ffffff;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:rgba(0,0,0,0.19) 0px 10px 20px,rgba(0,0,0,0.23) 0px 6px 6px;-ms-overflow-style:none;scrollbar-width:none;overflow-y:scroll}::-webkit-scrollbar{display:none}.header-color{padding-bottom:150px;width:100vh;background:#4f759b}.profile-pic img{height:40vw;width:40vw;border-radius:50%;border:10px solid #ffffff;margin-top:-100px}.myself-title{margin-bottom:25px}h1{font-size:32px;font-weight:700;color:#131b23;margin-bottom:10px;letter-spacing:0.025em}.description{margin-bottom:25px;color:#131b23;letter-spacing:0.01em}.description p:not(:last-child){margin-bottom:5px}button{font-family:"Open Sans", sans-serif;color:#ffffff;background:#4f759b;font-size:18px;font-weight:600;letter-spacing:0.025em;border:none;border-radius:15px;min-height:35px;width:100px;margin-bottom:25px;transition:all 0.2s ease;cursor:pointer}button:hover{width:115px;background:#4f759b}.about{padding-top:20px;padding-bottom:20px;text-align:center;font-weight:bold;font-size:1.5rem}.datawhale{font-size:1.3rem;padding:0 20px 20px 20px;text-indent:2em;line-height:2.4rem}.dwimg{width:150px;height:150px;display:block;margin:15px auto 100px auto}

View File

@@ -0,0 +1,95 @@
.profile {
margin: auto;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px;
-ms-overflow-style: none;
scrollbar-width: none;
overflow-y: scroll;
}
::-webkit-scrollbar {
display: none;
}
.header-color {
padding-bottom: 150px;
width: 100vh;
background: #4f759b;
}
.profile-pic img {
height: 40vw;
width: 40vw;
border-radius: 50%;
border: 10px solid #ffffff;
margin-top: -100px;
}
.myself-title {
margin-bottom: 25px;
}
h1 {
font-size: 32px;
font-weight: 700;
color: #131b23;
margin-bottom: 10px;
letter-spacing: 0.025em;
}
.description {
margin-bottom: 25px;
color: #131b23;
letter-spacing: 0.01em;
}
.description p:not(:last-child) {
margin-bottom: 5px;
}
button {
font-family: "Open Sans", sans-serif;
color: #ffffff;
background: #4f759b;
font-size: 18px;
font-weight: 600;
letter-spacing: 0.025em;
border: none;
border-radius: 15px;
min-height: 35px;
width: 100px;
margin-bottom: 25px;
transition: all 0.2s ease;
cursor: pointer;
}
button:hover {
width: 115px;
background: #4f759b;
}
.about {
padding-top: 20px;
padding-bottom: 20px;
text-align: center;
font-weight: bold;
font-size: 1.5rem;
}
.datawhale {
font-size: 1.3rem;
padding: 0 20px 20px 20px;
text-indent: 2em;
line-height: 2.4rem;
}
.dwimg {
width: 150px;
height: 150px;
display: block;
margin: 15px auto 100px auto;
}

View File

@@ -0,0 +1,70 @@
import BottomBar from '../../components/BottomBar'
import './index.scss'
import { useNavigate } from 'react-router-dom'
import { Toast } from 'antd-mobile'
import { useSelector } from '../../redux/types/hooks'
import { useDispatch } from 'react-redux'
import { pushHotLists, pushRecLists, userLog } from '../../redux/actions'
import { CLEAR_INFO } from '../../redux/types/constant'
import { clearCookie } from '../../assets/js/cookie'
export default function MySelf() {
const navigate = useNavigate()
const dispatch = useDispatch()
const quit = () => {
dispatch(pushRecLists([]))
dispatch(pushHotLists([]))
dispatch(userLog(CLEAR_INFO, []))
clearCookie('LoginName')
clearCookie('openId')
Toast.show({
icon: 'success',
content: '退出成功',
})
let second = 1;
// 延迟一秒执行
const timer = setInterval(() => {
second--;
if (!second) {
clearInterval(timer);
// 手动清除 Toast
Toast.clear();
navigate('/signIn')
}
}, 1000);
}
const username = useSelector(state => state.userLog.data.username)
return (
<div className="my-info">
<div className="profile">
<div className="profile-pic">
<div className="header-color"></div>
<img className="tx" src={require('../../assets/images/datawhale.png')} alt="头像" />
</div>
<div className="myself-title">
<h1>{username}</h1>
</div>
<button className="follow" onClick={quit}>退</button>
<div className="description">
<h4 className="about">DataWhale </h4>
<p className="datawhale">Datawhale是一个专注于数据科学与AI领域的开源组织
Datawhale for the learner
Datawhale
</p>
<img src={require('../../assets/images/dw.png')} alt="二维码" className="dwimg" />
</div>
<BottomBar></BottomBar>
</div>
</div>
)
}

View File

@@ -0,0 +1,78 @@
@charset "UTF-8";
/* 标题 */
.newsTitle {
padding: 3.5rem 1.5rem 0 1.5rem;
}
/* 大标题 */
.newsTitle h1 {
text-align: center;
color: #4d4f53;
font-weight: 600;
padding: 40px 0 20px 0;
margin: 0;
font-size: 1.5rem;
}
/* 副标题 */
.newsTitle p {
font-size: 12px;
display: flex;
justify-content: space-between;
padding-top: 1rem;
padding-bottom: 1rem;
color: #4679e3;
}
/* 内容 距离底部padding 遮挡action */
.newsinfo-continer {
padding-bottom: 5rem;
background-color: white;
}
/* 具体内容 */
.content {
font-size: 1rem;
padding: 0 20px 20px 20px;
text-indent: 2em;
line-height: 2.2rem;
/* 首行文本缩进 */
color: #1c1b1d;
}
/* 责任编辑 */
.editor {
font-size: 1rem;
padding: 0 20px 0 20px;
line-height: 2.2rem;
text-align: end;
}
#action {
/* margin: 20px 0 20px 0; */
display: flex;
justify-content: space-evenly;
}
#action > span {
display: flex;
padding-top: 2rem;
padding-bottom: 2rem;
font-size: 1rem;
}
.blank {
width: 100vw;
height: 50px;
position: fixed;
bottom: 0;
background-color: white;
}
.block-style {
display: block;
}
.hide-style {
display: none;
}

View File

@@ -0,0 +1 @@
.newsTitle{padding:3.5rem 1.5rem 0 1.5rem}.newsTitle h1{text-align:center;color:#4d4f53;font-weight:600;padding:40px 0 20px 0;margin:0;font-size:1.5rem}.newsTitle p{font-size:12px;display:flex;justify-content:space-between;padding-top:1rem;padding-bottom:1rem;color:#4679e3}.newsinfo-continer{padding-bottom:5rem;background-color:white}.content{font-size:1rem;padding:0 20px 20px 20px;text-indent:2em;line-height:2.2rem;color:#1c1b1d}.editor{font-size:1rem;padding:0 20px 0 20px;line-height:2.2rem;text-align:end}#action{display:flex;justify-content:space-evenly}#action>span{display:flex;padding-top:2rem;padding-bottom:2rem;font-size:1rem}.blank{width:100vw;height:50px;position:fixed;bottom:0;background-color:white}.block-style{display:block}.hide-style{display:none}

View File

@@ -0,0 +1,76 @@
/* 标题 */
.newsTitle {
padding: 3.5rem 1.5rem 0 1.5rem;
}
/* 大标题 */
.newsTitle h1 {
text-align: center;
color: rgb(77,79,83);
font-weight: 600;
padding: 40px 0 20px 0;
margin: 0;
font-size: 1.5rem;
}
/* 副标题 */
.newsTitle p {
font-size: 12px;
display: flex;
justify-content: space-between;
padding-top: 1rem;
padding-bottom: 1rem;
color: #4679e3;
}
/* 内容 距离底部padding 遮挡action */
.newsinfo-continer{
padding-bottom: 5rem;
background-color: white;
}
/* 具体内容 */
.content {
font-size: 1rem;
padding: 0 20px 20px 20px;
text-indent: 2em;
line-height: 2.2rem;
/* 首行文本缩进 */
color: rgb(28,27,29);
}
/* 责任编辑 */
.editor {
font-size: 1rem;
padding: 0 20px 0 20px;
line-height: 2.2rem;
text-align: end;
}
#action {
/* margin: 20px 0 20px 0; */
display: flex;
justify-content: space-evenly;
}
#action>span {
display: flex;
padding-top: 2rem;
padding-bottom: 2rem;
font-size: 1rem;
}
.blank {
width: 100vw;
height: 50px;
position: fixed;
bottom: 0;
background-color: white;
}
.block-style{
display: block
}
.hide-style{
display: none
}

View File

@@ -0,0 +1,181 @@
import { NavBar, Toast } from 'antd-mobile';
import axios from 'axios';
import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux';
import { useLocation, useNavigate } from 'react-router-dom';
import { ToListInfo } from '../../assets/type';
import { collectionsDecrease, collectionsIncrease, likesDecrease, likesIncrease, readChange } from '../../redux/actions';
import { useSelector } from '../../redux/types/hooks';
import './index.scss'
export default function NewsInfo(){
const navigate = useNavigate()
const back = () => {
navigate(-1)
}
const dispatch = useDispatch()
const location = useLocation()
const { id,cate } = location.state as ToListInfo;
const [news_content, setNewsContent] = useState<any>({})
const [content, setContent] = useState('')
const [editor, setEditor] = useState('')
const [isLike, setIsLike] = useState(false)
const [isCollection, setIsCollection] = useState(false)
// let isFixed: true,
let reg = /责任编辑/
let user_name = useSelector(state => {
return state.userLog.data.username
});
let lists = useSelector(state => state.pushRecLists)
// 点击喜欢时发送请求
async function ifLike() {
let val = {
user_name: user_name,
news_id: id,
action_time: Date.now(),
action_type: `likes:${isLike}`,
}
let successData = await axios.post("/action", val).then(res => {
return res
})
if (successData.status === 200) {
setIsLike(!isLike)
// 调用store中的actionChange函数控制次数的变化
if (isLike === true) {
dispatch(likesDecrease({ lists, id }))
} else {
dispatch(likesIncrease({ lists, id }))
}
} else {
Toast.show({
content: '加载数据失败',
})
}
};
// 点击收藏时发送请求
async function ifCollection() {
let val = {
user_name: user_name,
news_id: id,
action_time: Date.now(),
action_type: `collections::${isCollection}`,
}
let successData = await axios.post("/action", val).then(res => {
return res
})
if (successData.status === 200) {
setIsCollection(!isCollection)
// 调用store中的actionChange函数控制次数的变化
if (isCollection === true) {
dispatch(collectionsDecrease({ lists, id }))
} else {
dispatch(collectionsIncrease({ lists, id }))
}
} else {
Toast.show({
content: '加载数据失败',
})
}
};
useEffect(() => {
// 获取新闻详情
(async function getNewsInfo () {
let successData = await axios.get("/newsInfo?news_id=" + id + '&user_name=' + user_name).then(res => {
return res
})
if (successData.status === 200) {
setNewsContent(successData.data.data)
setContent(successData.data.data.content.split(reg)[0])
setEditor('责任编辑:' + successData.data.data.content.split(reg)[1])
if (successData.data.data.likes === true) {
setIsLike(true)
} else {
setIsLike(false)
}
if (successData.data.data.collections === true) {
setIsCollection(true)
} else {
setIsCollection(false)
}
} else {
Toast.show({
content: '加载数据失败',
})
}
})();
// 发送action为read的请求
(async function sendInfo() {
let val = {
user_name: user_name,
news_id: id,
action_time: Date.now(),
action_type: 'read',
}
let successData = await axios.post("/action", val).then(res => {
return res
})
if (successData.status === 200) {
dispatch(readChange({ lists, id }))
} else {
Toast.show({
content: '加载数据失败',
})
}
})();
},[])
return (
<>
<NavBar back='返回' onBack={back}></NavBar>
<div className="newsinfo-continer">
<div className="newsTitle">
{/* 大标题 */}
<h1>{ news_content.title }</h1>
{/* 子标题 */}
<p>
<span>{ news_content.ctime}</span>
<span>{cate}</span>
</p>
<hr />
</div>
{/* 内容区域 */}
<div className="content" dangerouslySetInnerHTML = {{ __html : content }}></div>
<div className="editor" dangerouslySetInnerHTML = {{ __html : editor }}></div>
<div id="action">
<span>:
<img src={require("../../assets/images/likes.png")} alt='likes' className={!isLike ? 'block-style' : 'hide-style'} onClick={ifLike} />
<img src={require("../../assets/images/likes1.png")} alt='likes1' className={isLike?'block-style':'hide-style'} onClick={ifLike} />
</span>
<span>:
<img src={require("../../assets/images/collects.png")} alt='collects' className={!isCollection ? 'block-style' : 'hide-style'} onClick={ifCollection} />
<img src={require("../../assets/images/collects1.png")} alt='collects1' className={isCollection ? 'block-style' : 'hide-style'} onClick={ifCollection} />
</span>
</div>
</div>
<div className="blank"></div>
</>
)
}

View File

@@ -0,0 +1,98 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { InfiniteScroll, List } from 'antd-mobile'
import { useSelector } from '../../redux/types/hooks'
import { useDispatch } from 'react-redux'
import axios from 'axios'
import BottomBar from '../../components/BottomBar'
import '../../assets/css/topBar.scss'
//引入action
import { pushRecLists } from '../../redux/actions'
import { Lists } from '../../assets/type'
export default function RecLists(){
const navigate = useNavigate()
const dispatch = useDispatch();
const formData = useSelector(state => state.pushRecLists)
const [hasMore, setHasMore] = useState(true)
async function loadMore() {
let url = '/recList'
let successData = await axios.get(url).then((res) => {
return res.data
})
dispatch(pushRecLists([...formData, ...successData.data]))
setHasMore(formData.length < 40)
}
return (
<>
<div className="tabs">
<input type="radio" id="tab1" name="tab-control" />
<input type="radio" id="tab2" name="tab-control" />
<ul>
<li title="Features" style={{borderBottom: '2px solid #428bff'}}>
<label htmlFor="tab1" role="button" style={{cursor: 'default',color: '#428bff'}}>
<svg viewBox="0 0 24 24" style={{fill: '#428bff'}}>
<path d="M14,2A8,8 0 0,0 6,10A8,8 0 0,0 14,18A8,8 0 0,0 22,10H20C20,13.32 17.32,16 14,16A6,6 0 0,1 8,10A6,6 0 0,1 14,4C14.43,4 14.86,4.05 15.27,4.14L16.88,2.54C15.96,2.18 15,2 14,2M20.59,3.58L14,10.17L11.62,7.79L10.21,9.21L14,13L22,5M4.93,5.82C3.08,7.34 2,9.61 2,12A8,8 0 0,0 10,20C10.64,20 11.27,19.92 11.88,19.77C10.12,19.38 8.5,18.5 7.17,17.29C5.22,16.25 4,14.21 4,12C4,11.7 4.03,11.41 4.07,11.11C4.03,10.74 4,10.37 4,10C4,8.56 4.32,7.13 4.93,5.82Z" />
</svg>
<span></span>
</label>
</li>
<li title="Delivery Contents" style={{borderBottom: '2px solid #bec5cf'}} onClick={() => { navigate('/hotLists') }}>
<label htmlFor="tab2" role="button" style={{cursor: 'default',color: '#bec5cf'}}>
<svg viewBox="0 0 24 24" style={{fill: '#bec5cf'}}>
<path d="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" />
</svg>
<span></span>
</label>
</li>
</ul>
</div>
<List>
{formData.map((item: Lists, index: number) => (
<Link
style={{ textDecoration:'none'}}
key={index}
to={`/newsInfo/${item.news_id}`}
state={{
id: item.news_id,
likes: item.likes,
collections: item.collections,
cate: item.cate,
read_num:item.read_num,
}}>
<List.Item key={index}>
<p>
<span className="cate">{item.cate}</span>
<span className='title'>{ item.title }</span>
</p>
<p className="discribe">
<span className="ctime">{ item.ctime} </span>
<span className="read_num">{item.read_num}</span>
<span className="likes">:{item.likes}</span>
<span className="collections">:{item.collections}</span>
</p>
</List.Item>
</Link>
))}
</List>
<InfiniteScroll loadMore={loadMore} hasMore={hasMore} />
<BottomBar />
</>
)
}

View File

@@ -0,0 +1,172 @@
@charset "UTF-8";
.login-nav {
margin: 0 0 6em 1rem;
}
/* container */
.login-container {
height: 100vh;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
line-height: 1;
letter-spacing: 1px;
display: block;
position: relative;
z-index: 0;
background-image: url(https://img0.baidu.com/it/u=3564438015,1736378667&fm=26&fmt=auto);
background-repeat: no-repeat;
background-attachment: fixed;
background-size: 100% 100%;
background-color: black;
padding: 4rem 4rem 4rem 4rem;
overflow: hidden;
height: 100vh;
box-sizing: border-box;
}
/* 登录和注册的切换 */
.login-nav {
position: relative;
padding: 0;
}
.login-nav__item {
list-style: none;
display: inline-block;
}
.login-nav__item + .login-nav__item {
margin-left: 2.25rem;
}
.login-nav__item a {
position: relative;
color: rgba(255, 255, 255, 0.5);
text-decoration: none;
text-transform: uppercase;
font-weight: 500;
font-size: 1.5rem;
padding-bottom: .5rem;
transition: .20s all ease;
}
.login-nav__item.active a,
.login-nav__item a:hover {
color: #ffffff;
transition: .15s all ease;
}
.login-nav__item a:after {
content: '';
display: inline-block;
height: 10px;
background-color: white;
position: absolute;
right: 100%;
bottom: -1px;
left: 0;
border-radius: 50%;
transition: .15s all ease;
}
.login-nav__item a:hover:after,
.login-nav__item.active a:after {
background-color: #1161ed;
height: 2px;
right: 0;
bottom: 2px;
border-radius: 0;
transition: .20s all ease;
}
/* label标签 */
.login__label {
display: block;
padding-left: 1rem;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
font-size: .75rem;
margin-bottom: 1rem;
}
.login__label {
margin-top: 1.5rem;
}
/* 提交按钮 */
.login__submit {
color: #ffffff;
font-size: 1rem;
font-family: 'Montserrat', sans-serif;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 2rem;
padding: .75rem;
border-radius: 2rem;
display: block;
width: 100%;
background-color: rgba(17, 97, 237, 0.75);
border: none;
cursor: pointer;
}
.login__submit:hover {
background-color: #1161ed;
}
.login__label {
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
font-size: .75rem;
margin-bottom: 1rem;
}
.login__label--checkbox {
font-size: .75rem;
margin-bottom: 1rem;
display: flex;
position: relative;
margin-top: 2rem;
}
.login__input--checkbox {
position: absolute;
top: .1rem;
left: 0;
margin: 0;
}
.login__forgot {
display: block;
margin-top: 3rem;
text-align: center;
color: rgba(255, 255, 255, 0.75);
font-size: .75rem;
text-decoration: none;
position: relative;
z-index: 1;
}
.login__forgot:hover {
color: #1161ed;
}
.adm-list-body {
background-color: transparent;
}
.adm-form {
--border-inner: none;
--border-top: none;
--border-bottom: none;
}
.adm-input {
--color: rgba(255, 255, 255, 0.5);
--placeholder-color: rgba(255, 255, 255, 0.5);
--font-size: 1.225rem;
}
.adm-checkbox-content {
color: rgba(255, 255, 255, 0.5);
}

View File

@@ -0,0 +1 @@
.login-nav{margin:0 0 6em 1rem}.login-container{height:100vh;font-family:'Montserrat', sans-serif;font-size:16px;line-height:1;letter-spacing:1px;display:block;position:relative;z-index:0;background-image:url(https://img0.baidu.com/it/u=3564438015,1736378667&fm=26&fmt=auto);background-repeat:no-repeat;background-attachment:fixed;background-size:100% 100%;background-color:black;padding:4rem 4rem 4rem 4rem;overflow:hidden;height:100vh;box-sizing:border-box}.login-nav{position:relative;padding:0}.login-nav__item{list-style:none;display:inline-block}.login-nav__item+.login-nav__item{margin-left:2.25rem}.login-nav__item a{position:relative;color:rgba(255,255,255,0.5);text-decoration:none;text-transform:uppercase;font-weight:500;font-size:1.5rem;padding-bottom:.5rem;transition:.20s all ease}.login-nav__item.active a,.login-nav__item a:hover{color:#ffffff;transition:.15s all ease}.login-nav__item a:after{content:'';display:inline-block;height:10px;background-color:#fff;position:absolute;right:100%;bottom:-1px;left:0;border-radius:50%;transition:.15s all ease}.login-nav__item a:hover:after,.login-nav__item.active a:after{background-color:#1161ed;height:2px;right:0;bottom:2px;border-radius:0;transition:.20s all ease}.login__label{display:block;padding-left:1rem;color:rgba(255,255,255,0.5);text-transform:uppercase;font-size:.75rem;margin-bottom:1rem}.login__label{margin-top:1.5rem}.login__submit{color:#ffffff;font-size:1rem;font-family:'Montserrat', sans-serif;text-transform:uppercase;letter-spacing:1px;margin-top:2rem;padding:.75rem;border-radius:2rem;display:block;width:100%;background-color:rgba(17,97,237,0.75);border:none;cursor:pointer}.login__submit:hover{background-color:#1161ed}.login__label{color:rgba(255,255,255,0.5);text-transform:uppercase;font-size:.75rem;margin-bottom:1rem}.login__label--checkbox{font-size:.75rem;margin-bottom:1rem;display:flex;position:relative;margin-top:2rem}.login__input--checkbox{position:absolute;top:.1rem;left:0;margin:0}.login__forgot{display:block;margin-top:3rem;text-align:center;color:rgba(255,255,255,0.75);font-size:.75rem;text-decoration:none;position:relative;z-index:1}.login__forgot:hover{color:#1161ed}.adm-list-body{background-color:transparent}.adm-form{--border-inner:none;--border-top:none;--border-bottom:none}.adm-input{--color:rgba(255,255,255,0.5);--placeholder-color:rgba(255,255,255,0.5);--font-size:1.225rem}.adm-checkbox-content{color:rgba(255,255,255,0.5)}

View File

@@ -0,0 +1,172 @@
.login-nav {
margin: 0 0 6em 1rem;
}
/* container */
.login-container {
height: 100vh;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
line-height: 1;
letter-spacing: 1px;
display: block;
position: relative;
z-index: 0;
background-image: url(https://img0.baidu.com/it/u=3564438015,1736378667&fm=26&fmt=auto);
background-repeat: no-repeat;
background-attachment: fixed;
background-size: 100% 100%;
background-color: black;
padding: 4rem 4rem 4rem 4rem;
overflow: hidden;
height: 100vh;
box-sizing: border-box;
}
/* 登录和注册的切换 */
.login-nav {
position: relative;
padding: 0;
}
.login-nav__item {
list-style: none;
display: inline-block;
}
.login-nav__item+.login-nav__item {
margin-left: 2.25rem;
}
.login-nav__item a {
position: relative;
color: rgba(255, 255, 255, 0.5);
text-decoration: none;
text-transform: uppercase;
font-weight: 500;
font-size: 1.5rem;
padding-bottom: .5rem;
transition: .20s all ease;
}
.login-nav__item.active a,
.login-nav__item a:hover {
color: #ffffff;
transition: .15s all ease;
}
.login-nav__item a:after {
content: '';
display: inline-block;
height: 10px;
background-color: rgb(255, 255, 255);
position: absolute;
right: 100%;
bottom: -1px;
left: 0;
border-radius: 50%;
transition: .15s all ease;
}
.login-nav__item a:hover:after,
.login-nav__item.active a:after {
background-color: rgb(17, 97, 237);
height: 2px;
right: 0;
bottom: 2px;
border-radius: 0;
transition: .20s all ease;
}
/* label标签 */
.login__label {
display: block;
padding-left: 1rem;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
font-size: .75rem;
margin-bottom: 1rem;
}
.login__label {
margin-top: 1.5rem;
}
/* 提交按钮 */
.login__submit {
color: #ffffff;
font-size: 1rem;
font-family: 'Montserrat', sans-serif;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 2rem;
padding: .75rem;
border-radius: 2rem;
display: block;
width: 100%;
background-color: rgba(17, 97, 237, .75);
border: none;
cursor: pointer;
}
.login__submit:hover {
background-color: rgba(17, 97, 237, 1);
}
.login__label{
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
font-size: .75rem;
margin-bottom: 1rem;
}
.login__label--checkbox {
font-size: .75rem;
margin-bottom: 1rem;
display: flex;
position: relative;
margin-top: 2rem;
}
.login__input--checkbox {
position: absolute;
top: .1rem;
left: 0;
margin: 0;
}
.login__forgot {
display: block;
margin-top: 3rem;
text-align: center;
color: rgba(255, 255, 255, 0.75);
font-size: .75rem;
text-decoration: none;
position: relative;
z-index: 1;
}
.login__forgot:hover {
color: rgb(17, 97, 237);
}
// 去除antd默认表单背景色
.adm-list-body{
background-color: transparent;
}
// 去除antd默认表单边框
.adm-form{
--border-inner: none;
--border-top: none;
--border-bottom: none;
}
// antd input框字体
.adm-input{
--color:rgba(255, 255, 255, 0.5);
--placeholder-color:rgba(255, 255, 255, 0.5);
--font-size: 1.225rem;
}
// antd 单选框字体
.adm-checkbox-content {
color:rgba(255, 255, 255, 0.5);
}

View File

@@ -0,0 +1,133 @@
import { NavLink, useNavigate } from 'react-router-dom'
import {useDispatch} from 'react-redux';
import { Form, Input, Checkbox, Toast } from 'antd-mobile';
import './index.scss'
//引入action
import { userLog } from '../../redux/actions'
import { SIGNIN } from '../../redux/types/constant';
import encrypt from '../../assets/js/encrypt'
import axios from 'axios';
import {setCookie} from '../../assets/js/cookie'
import { UserSignIn } from '../../assets/type';
export const SignIn = () => {
const dispatch = useDispatch();
const navigate = useNavigate()
async function onFinish(values: UserSignIn) {
//密码解密
let res = { username: values.name, passwd: encrypt.Decrypt(values.passwd) };
let successData = await axios.post("/login", res).then((res) => {
return res.data
})
if (successData.code === 200) {
let loginInfo = {
LoginName: res.username,
openId: "asfafsfsfsdfsdfsdfdsf"
}
// checke:true--选中记住我 checke:false--未选中记住我
if (values.remember) {
// 调用setCookie方法同时传递需要存储的数据保存天数
setCookie(loginInfo, 7)
} else {
setCookie(loginInfo, 1)
}
Toast.show({
icon: 'success',
content: '登录成功',
})
let second = 1;
// 延迟一秒执行
const timer = setInterval(() => {
second--;
if (!second) {
clearInterval(timer);
// 手动清除 Toast
Toast.clear();
dispatch(userLog(SIGNIN, res))
navigate('/recLists', { replace: true })
}
}, 1000);
} else if (successData.code === 500) {
Toast.show({
icon: 'fail',
content: '登陆失败',
})
} else if (successData.code === 501) {
Toast.show({
icon: 'fail',
content: '密码输入错误',
})
} else if (successData.code === 502) {
Toast.show({
icon: 'fail',
content: '用户名不存在',
})
}
}
return (
<div className="login-container">
<ul className="login-nav">
<li className="login-nav__item active">
<NavLink to="/signIn"></NavLink>
</li>
<li className="login-nav__item">
<NavLink to='/signUp'></NavLink>
</li>
</ul>
<Form
onFinish={onFinish}
initialValues={{remember:true}}
footer={<button className="login__submit"></button>}
>
<label className="login__label"></label>
<Form.Item name='name'
rules={[
{ required: true, message: '请输入用户名' },
]}
>
<Input placeholder='请输入用户名' />
</Form.Item>
<label className="login__label"></label>
<Form.Item
name='passwd'
rules={[
{ required: true, message: '请输入密码' },
]}
>
<Input placeholder='请输入密码' type='password' />
</Form.Item>
<Form.Item name='remember'>
<Checkbox className="login__label--checkbox" defaultChecked
style={{
'--icon-size': '18px',
'--font-size': '14px',
'--gap': '6px',
}}></Checkbox>
</Form.Item>
</Form>
{/*<a href="#" className="login__forgot">忘记密码?</a>*/}
</div>
)
}
export default SignIn

View File

@@ -0,0 +1,150 @@
@charset "UTF-8";
.errorMessage {
color: red;
font-size: .8rem;
display: block;
padding: 1rem 0 0 1rem;
}
/* container */
.login-container {
font-family: 'Montserrat', sans-serif;
font-size: 16px;
line-height: 1;
letter-spacing: 1px;
display: block;
position: relative;
z-index: 0;
background-image: url(https://img0.baidu.com/it/u=3564438015,1736378667&fm=26&fmt=auto);
background-repeat: no-repeat;
background-attachment: fixed;
background-size: 100% 100%;
background-color: black;
padding: 4rem 4rem 4rem 4rem;
overflow: hidden;
height: 100vh;
box-sizing: border-box;
}
/* 登录和注册的切换 */
.login-nav {
position: relative;
padding: 0;
margin: 0 0 3em 1rem;
}
.login-nav__item {
list-style: none;
display: inline-block;
}
.login-nav__item + .login-nav__item {
margin-left: 2.25rem;
}
.login-nav__item a {
position: relative;
color: rgba(255, 255, 255, 0.5);
text-decoration: none;
text-transform: uppercase;
font-weight: 500;
font-size: 1.5rem;
padding-bottom: .5rem;
transition: .20s all ease;
}
.login-nav__item.active a,
.login-nav__item a:hover {
color: #ffffff;
transition: .15s all ease;
}
.login-nav__item a:after {
content: '';
display: inline-block;
height: 10px;
background-color: white;
position: absolute;
right: 100%;
bottom: -1px;
left: 0;
border-radius: 50%;
transition: .15s all ease;
}
.login-nav__item a:hover:after,
.login-nav__item.active a:after {
background-color: #1161ed;
height: 2px;
right: 0;
bottom: 2px;
border-radius: 0;
transition: .20s all ease;
}
/* label标签 */
.login__label {
display: block;
padding-left: 1rem;
color: #cccccc;
text-transform: uppercase;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.login__label {
margin-top: 1.5rem;
}
/* 提交按钮 */
.login__submit {
color: #ffffff;
font-size: 1rem;
font-family: 'Montserrat', sans-serif;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 2rem;
padding: .75rem;
border-radius: 2rem;
display: block;
width: 100%;
background-color: rgba(17, 97, 237, 0.75);
border: none;
cursor: pointer;
}
.login__submit:hover {
background-color: #1161ed;
}
.adm-list-body {
background-color: transparent;
}
.adm-form {
--border-inner: none;
--border-top: none;
--border-bottom: none;
}
.adm-input {
--color: rgba(255, 255, 255, 0.5);
--placeholder-color: rgba(255, 255, 255, 0.5);
--font-size: 1.225rem;
}
.adm-list {
--active-background-color: transparent;
}
.adm-plain-anchor {
color: rgba(255, 255, 255, 0.5);
}
.adm-radio-content {
color: rgba(255, 255, 255, 0.5);
}
.adm-radio {
padding: 10px;
}

View File

@@ -0,0 +1 @@
.errorMessage{color:red;font-size:.8rem;display:block;padding:1rem 0 0 1rem}.login-container{font-family:'Montserrat', sans-serif;font-size:16px;line-height:1;letter-spacing:1px;display:block;position:relative;z-index:0;background-image:url(https://img0.baidu.com/it/u=3564438015,1736378667&fm=26&fmt=auto);background-repeat:no-repeat;background-attachment:fixed;background-size:100% 100%;background-color:black;padding:4rem 4rem 4rem 4rem;overflow:hidden;height:100vh;box-sizing:border-box}.login-nav{position:relative;padding:0;margin:0 0 3em 1rem}.login-nav__item{list-style:none;display:inline-block}.login-nav__item+.login-nav__item{margin-left:2.25rem}.login-nav__item a{position:relative;color:rgba(255,255,255,0.5);text-decoration:none;text-transform:uppercase;font-weight:500;font-size:1.5rem;padding-bottom:.5rem;transition:.20s all ease}.login-nav__item.active a,.login-nav__item a:hover{color:#ffffff;transition:.15s all ease}.login-nav__item a:after{content:'';display:inline-block;height:10px;background-color:#fff;position:absolute;right:100%;bottom:-1px;left:0;border-radius:50%;transition:.15s all ease}.login-nav__item a:hover:after,.login-nav__item.active a:after{background-color:#1161ed;height:2px;right:0;bottom:2px;border-radius:0;transition:.20s all ease}.login__label{display:block;padding-left:1rem;color:#cccccc;text-transform:uppercase;font-size:1.25rem;margin-bottom:1rem}.login__label{margin-top:1.5rem}.login__submit{color:#ffffff;font-size:1rem;font-family:'Montserrat', sans-serif;text-transform:uppercase;letter-spacing:1px;margin-top:2rem;padding:.75rem;border-radius:2rem;display:block;width:100%;background-color:rgba(17,97,237,0.75);border:none;cursor:pointer}.login__submit:hover{background-color:#1161ed}.adm-list-body{background-color:transparent}.adm-form{--border-inner:none;--border-top:none;--border-bottom:none}.adm-input{--color:rgba(255,255,255,0.5);--placeholder-color:rgba(255,255,255,0.5);--font-size:1.225rem}.adm-list{--active-background-color:transparent}.adm-plain-anchor{color:rgba(255,255,255,0.5)}.adm-radio-content{color:rgba(255,255,255,0.5)}.adm-radio{padding:10px}

View File

@@ -0,0 +1,153 @@
.errorMessage {
color: red;
font-size: .8rem;
display: block;
padding: 1rem 0 0 1rem;
}
/* container */
.login-container {
font-family: 'Montserrat', sans-serif;
font-size: 16px;
line-height: 1;
letter-spacing: 1px;
display: block;
position: relative;
z-index: 0;
background-image: url(https://img0.baidu.com/it/u=3564438015,1736378667&fm=26&fmt=auto);
background-repeat: no-repeat;
background-attachment: fixed;
background-size: 100% 100%;
background-color: black;
padding: 4rem 4rem 4rem 4rem;
overflow: hidden;
height: 100vh;
box-sizing: border-box;
}
/* 登录和注册的切换 */
.login-nav {
position: relative;
padding: 0;
margin: 0 0 3em 1rem;
}
.login-nav__item {
list-style: none;
display: inline-block;
}
.login-nav__item+.login-nav__item {
margin-left: 2.25rem;
}
.login-nav__item a {
position: relative;
color: rgba(255, 255, 255, 0.5);
text-decoration: none;
text-transform: uppercase;
font-weight: 500;
font-size: 1.5rem;
padding-bottom: .5rem;
transition: .20s all ease;
}
.login-nav__item.active a,
.login-nav__item a:hover {
color: #ffffff;
transition: .15s all ease;
}
.login-nav__item a:after {
content: '';
display: inline-block;
height: 10px;
background-color: rgb(255, 255, 255);
position: absolute;
right: 100%;
bottom: -1px;
left: 0;
border-radius: 50%;
transition: .15s all ease;
}
.login-nav__item a:hover:after,
.login-nav__item.active a:after {
background-color: rgb(17, 97, 237);
height: 2px;
right: 0;
bottom: 2px;
border-radius: 0;
transition: .20s all ease;
}
/* label标签 */
.login__label {
display: block;
padding-left: 1rem;
color: #cccccc;
text-transform: uppercase;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.login__label {
margin-top: 1.5rem;
}
/* 提交按钮 */
.login__submit {
color: #ffffff;
font-size: 1rem;
font-family: 'Montserrat', sans-serif;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 2rem;
padding: .75rem;
border-radius: 2rem;
display: block;
width: 100%;
background-color: rgba(17, 97, 237, .75);
border: none;
cursor: pointer;
}
.login__submit:hover {
background-color: rgba(17, 97, 237, 1);
}
// 去除antd默认表单背景色
.adm-list-body{
background-color: transparent;
}
// 去除antd默认表单边框
.adm-form{
--border-inner: none;
--border-top: none;
--border-bottom: none;
}
// antd input框字体
.adm-input{
--color:rgba(255, 255, 255, 0.5);
--placeholder-color:rgba(255, 255, 255, 0.5);
--font-size: 1.225rem;
}
// 去除antd级联选择背景颜色
.adm-list{
--active-background-color:transparent
}
// antd级联选择字体颜色
.adm-plain-anchor{
color:rgba(255, 255, 255, 0.5)
}
// antd单选框字体颜色
.adm-radio-content{
color:rgba(255, 255, 255, 0.5)
}
// antd单选框边距
.adm-radio{
padding: 10px;
}

View File

@@ -0,0 +1,211 @@
import {useState} from 'react'
import { NavLink, useNavigate } from 'react-router-dom'
import { Form, Input, Radio, CascadePicker, Toast } from 'antd-mobile'
import city from '../../assets/data/city'
import './index.scss'
import { useDispatch } from 'react-redux'
import { userLog } from '../../redux/actions'
import { SIGNUP } from '../../redux/types/constant'
import encrypt from '../../assets/js/encrypt'
import axios from 'axios'
import { setCookie } from '../../assets/js/cookie'
import { UserSignUp } from '../../assets/type'
let pinyin = require('js-pinyin')
export default function SignUp(){
// 控制级联选择是否显示
const [visible, setVisible] = useState(false)
// 获取选择的城市数据
const [cityValue, setCityValue] = useState<any>('')
const dispatch = useDispatch();
const navigate = useNavigate()
async function onFinish(values: UserSignUp){
values.area = cityValue
//密码加密
values.passwd = encrypt.Encrypt(values.passwd)
let successData = await axios.post("/register", values).then((res)=>{
return res.data
})
if (successData.code === 200) {
let loginInfo = {
LoginName: values.name,
openId: "asdasdadasdasdadad"
}
// 调用setCookie方法同时传递需要存储的数据保存天数
setCookie(loginInfo, 7)
Toast.show({
icon: 'success',
content: '注册成功',
})
let second = 1;
// 延迟一秒执行
const timer = setInterval(() => {
second--;
if (!second) {
clearInterval(timer);
// 手动清除 Toast
Toast.clear();
dispatch(userLog(SIGNUP, values));
navigate('/recLists', { replace: true })
}
}, 1000);
}else if(successData.code === 500) {
Toast.show({
icon: 'fail',
content: '用户名已存在',
})
}else {
Toast.show({
icon: 'fail',
content: '请完整填写注册信息',
})
}
}
const checkName = (_: any, value: string) => {
let nameReg = /^[A-Za-z0-9]+$/
if (!nameReg.test(value) && value) {
return Promise.reject(new Error('用户名格式为字母和数字'))
} else {
return Promise.resolve()
}
}
const checkPasswd = (_: any, value: string) => {
let passwdReg = /^[A-Za-z0-9]{6,}$/
if (!passwdReg.test(value) && value) {
return Promise.reject(new Error('密码长度至少6位仅包括为字母和数字'))
} else {
return Promise.resolve()
}
}
const checkAge = (_: any, value: string) => {
var ageReg = /^([1-9][0-9]{0,1}|100)$/
if (!ageReg.test(value) && value) {
return Promise.reject(new Error('请输入1-100的整数'))
} else {
return Promise.resolve()
}
}
return (
<div className="login-container">
<ul className="login-nav">
<li className="login-nav__item">
<NavLink to="/signIn"></NavLink>
</li>
<li className="login-nav__item active">
<NavLink to='/signUp'></NavLink>
</li>
</ul>
<Form
onFinish={onFinish}
initialValues={{gender:'male'}}
footer={<button className="login__submit"></button>}
>
<label className="login__label"></label>
<Form.Item name='name'
rules={[
{ required: true, message: '请输入用户名' },
{ validator: checkName}
]}
>
<Input placeholder='请输入用户名' />
</Form.Item>
<label className="login__label"></label>
<Form.Item
name='passwd'
rules={[
{ required: true, message: '请输入密码' },
{ validator: checkPasswd }]
}
>
<Input placeholder='请输入密码' type='password' />
</Form.Item>
<label className="login__label"></label>
<Form.Item
name='passwd2'
dependencies={['passwd']}
rules={[
{ required: true, message: '请再次输入密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (getFieldValue('passwd') === value && value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input placeholder='再次输入密码' type='password' />
</Form.Item>
<label className="login__label"></label>
<Form.Item name='age'
rules={[
{ required: true, message: '请输入年龄' },
{ validator: checkAge }
]}
>
<Input placeholder='请输入年龄' type='number'/>
</Form.Item>
<label className="login__label"></label>
<Form.Item
name='area'
trigger='onConfirm'
onClick={() => { setVisible(true) }}
rules={[
{ required: true, message: '请选择城市' }
]}
>
<CascadePicker
title='选择城市'
options={city}
visible={visible}
onClose={() => { setVisible(false) }}
onConfirm={(val, _) => {
setCityValue(pinyin.getFullChars(val[0]))
}}
>
{items => {
if (items.every(item => item === null)) {
return '请选择城市'
} else {
return items.map(item => item?.label ?? '').join('-')
}
}}
</CascadePicker>
</Form.Item>
<label className="login__label"></label>
<Form.Item name='gender'>
<Radio.Group >
<Radio value='male' style={{
'--icon-size': '18px',
'--font-size': '14px',
}}></Radio>
<Radio value='female' style={{
'--icon-size': '18px',
'--font-size': '14px',
}}></Radio>
</Radio.Group>
</Form.Item>
</Form>
</div>
)
}

View File

@@ -0,0 +1,26 @@
import { REC_LIST, HOT_LIST, READ, LIKE_DECREASE,LIKE_INCREASE,COLLECT_DECREASE,COLLECT_INCREASE, CLEAR_INFO} from './types/constant'
// 用户登录(注册)时传入信息
export const userLog = (type: string, data: object) => ({ type, data })
// 给列表赋值
export const pushRecLists = (data: any) => ({ type:REC_LIST, data })
export const pushHotLists = (data: any) => ({ type: HOT_LIST, data })
// 清空列表 在刷新时调用 重新给列表赋值
export const clearRecLists = () => ({ type: REC_LIST })
export const clearHotLists = () => ({ type: HOT_LIST })
// 退出登录时清空用户相关数据
export const clearUser = (data: Array<object>) => ({ type: CLEAR_INFO, data })
//点进新闻详情页时触发,让阅读次数增加
export const readChange = (data:{lists:Array<object>,id:number}) => ({ type: READ, data })
//点击喜欢或者收藏时触发,让相应次数增加或者减少
export const likesDecrease = (data: { lists: Array<object>, id: number }) => ({ type: LIKE_DECREASE, data })
export const likesIncrease = (data: { lists: Array<object>, id: number }) => ({ type: LIKE_INCREASE, data })
export const collectionsDecrease = (data: { lists: Array<object>, id: number }) => ({ type: COLLECT_DECREASE, data })
export const collectionsIncrease = (data: { lists: Array<object>, id: number }) => ({ type: COLLECT_INCREASE, data })

View File

@@ -0,0 +1,13 @@
//引入combineReducers用于汇总多个reducer
import { combineReducers } from 'redux'
import {userLog} from './reducers/userLog'
import { pushRecLists, pushHotLists } from './reducers/pushLists'
import { readChange, likesChange, collectionsChange } from './reducers/numChange'
//汇总所有的reducer变为一个总的reducer
export default combineReducers({
userLog,
pushRecLists, pushHotLists,
readChange, likesChange, collectionsChange
})

View File

@@ -0,0 +1,86 @@
import { Lists } from '../../assets/type'
import { READ, LIKE_DECREASE, LIKE_INCREASE, COLLECT_DECREASE, COLLECT_INCREASE } from '../types/constant'
const readDefault = 0
export function readChange(preState = readDefault, action: { type: string, data: {lists:Array<Lists>,id:number} }) {
switch (action.type){
case READ:
return action.data.lists.map((item) => {
if (item.news_id !== action.data.id) {
return action.data.lists
}
item.read_num++
return {
...item,
...action.data.lists
}
})
default:
return preState
}
}
const likesDefault = 0
export function likesChange(preState = likesDefault, action: { type: string; data: { lists: Array<Lists>, id: number } }) {
switch (action.type) {
case LIKE_DECREASE:
return action.data.lists.map((item) => {
if (item.news_id !== action.data.id) {
return action.data.lists
} else {
item.likes--
return {
...item,
...action.data.lists
}
}
})
case LIKE_INCREASE:
return action.data.lists.map((item) => {
if (item.news_id !== action.data.id) {
return action.data.lists
} else {
item.likes ++
return {
...item,
...action.data.lists
}
}
})
default:
return preState
}
}
const colllectionsDefault = 0
export function collectionsChange(preState = colllectionsDefault, action: { type: string; data: { lists: Array<Lists>, id: number } }) {
switch (action.type) {
case COLLECT_DECREASE:
return action.data.lists.map((item) => {
if (item.news_id !== action.data.id) {
return action.data.lists
} else {
item.collections--
return {
...item,
...action.data.lists
}
}
})
case COLLECT_INCREASE:
return action.data.lists.map((item) => {
if (item.news_id !== action.data.id) {
return action.data.lists
} else {
item.collections++
return {
...item,
...action.data.lists
}
}
})
default:
return preState
}
}

View File

@@ -0,0 +1,19 @@
import { HOT_LIST, REC_LIST } from '../types/constant'
export function pushRecLists(preState = [], action: { type: string; data: any }) {
switch (action.type){
case REC_LIST:
return action.data
default:
return preState
}
}
export function pushHotLists(preState = [], action: { type: string; data: any }) {
switch (action.type){
case HOT_LIST:
return action.data
default:
return preState
}
}

View File

@@ -0,0 +1,23 @@
import { userLogState } from '../../assets/type'
import { CLEAR_INFO, SIGNIN, SIGNUP } from '../types/constant'
const userLogDefault: userLogState = {
type: SIGNIN,
data: {
username: '',
passwd: '',
}
}
export function userLog(preState = userLogDefault, action: userLogState) {
switch(action.type) {
case SIGNIN:
return ({ type: action.type, data: action.data })
case SIGNUP:
return ({ type: action.type, data: action.data })
case CLEAR_INFO:
return ({ type: action.type, data: action.data })
default:
return preState
}
}

View File

@@ -0,0 +1,26 @@
//引入createStore专门用于创建redux中最为核心的store对象
import {createStore,applyMiddleware} from 'redux'
//引入汇总之后的reducer
import rootReducer from './reducers'
//引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'
//引入redux-devtools-extension
import { composeWithDevTools } from 'redux-devtools-extension'
// 引入redux-persist插件使用sessionstorage持久化数据解决页面刷新数据丢失的问题
import { persistStore, persistReducer } from 'redux-persist';
// 存储机制,当前使用sessionStorage
import storageSession from 'redux-persist/lib/storage/session'
const storageConfig = {
key: 'root', // 必须
storage: storageSession, // 缓存机制
}
const myPersistReducer = persistReducer(storageConfig, rootReducer);
const store = createStore(myPersistReducer, composeWithDevTools(applyMiddleware(thunk)));
// 使用ts的条件类型 ReturnType<T>T:函数类型。 获取函数返回值的类型
export type RootState = ReturnType<typeof store.getState>
export const persistor = persistStore(store)
export default store;

View File

@@ -0,0 +1,24 @@
/*
定义action对象中type类型的常量值
*/
export const SIGNIN = 'signIn'
export const SIGNUP = 'signUp'
export const REC_LIST = 'recList'
export const HOT_LIST = 'hotList'
export const SEARCH_ID = 'searchId'
export const READ = 'read'
export const LIKE_DECREASE = 'likeDecrease'
export const LIKE_INCREASE = 'likeIncrease'
export const COLLECT_DECREASE = 'collectDecrease'
export const COLLECT_INCREASE = 'collectIncrease'
export const LIKES = 'likes'
export const COLLECTIONS = 'collections'
export const CLEAR_INFO = 'clearInfo'

View File

@@ -0,0 +1,7 @@
// 使用TypeUseSelectorHook这个interface 来重新定义 useSelector这个hook
import { useSelector as useReduxSelector, TypedUseSelectorHook } from 'react-redux'
import { RootState } from '../store'
export const useSelector:TypedUseSelectorHook<RootState> = useReduxSelector

View File

@@ -0,0 +1,51 @@
import { lazy } from 'react'
import { Router } from '../assets/type'
import SignIn from '../pages/SignIn'
const router: Array<Router> = [
{
path: '/',
component: SignIn,
auth: false,
},
{
path: '/signIn',
auth: false,
component: lazy(() => import('../pages/SignIn'))
},
{
path: '/signUp',
auth: false,
component: lazy(() => import('../pages/SignUp'))
},
{
path: '/recLists',
auth: true,
component: lazy(() => import('../pages/RecLists'))
},
{
path: '/hotLists',
auth: true,
component: lazy(() => import('../pages/HotLists'))
},
{
path: "/newsInfo/:id",
name: "newsInfo",
auth: true,
component: lazy(() => import('../pages/NewsInfo'))
},
{
path: '/mySelf',
auth: true,
component: lazy(() => import('../pages/MySelf'))
},
{
path: '*',
auth: false,
component: lazy(() => import('../pages/404'))
},
]
export default router

View File

@@ -0,0 +1,37 @@
{
// 编译选项
"compilerOptions": {
// 生成代码的语言版本
"target": "es5",
// 指定要包含在编译中的 library
"lib": ["dom", "dom.iterable", "esnext"],
// 允许 ts 编译器编译 js 文件
"allowJs": true,
// 跳过声明文件的类型检查
"skipLibCheck": true,
// es 模块 互操作,屏蔽 ESModule 和 CommonJS 之间的差异
"esModuleInterop": true,
// 允许通过 import x from 'y' 即使模块没有显式指定 default 导出
"allowSyntheticDefaultImports": true,
// 开启严格模式
"strict": true,
// 对文件名称强制区分大小写
"forceConsistentCasingInFileNames": true,
// 为 switch 语句启用错误报告
"noFallthroughCasesInSwitch": true,
// 生成代码的模块化标准
"module": "esnext",
// 模块解析(查找)策略
"moduleResolution": "node",
// 允许导入扩展名为.json的模块
"resolveJsonModule": true,
// 是否将没有 import/export 的文件视为旧(全局而非模块化)脚本文件。
"isolatedModules": true,
// 编译时不生成任何文件(只进行类型检查)
"noEmit": true,
// 指定将 JSX 编译成什么形式
"jsx": "react-jsx"
},
// 指定允许 ts 处理的目录
"include": ["src"]
}

View File

@@ -52,7 +52,7 @@ http://theneverlemon.gitee.io/vue2-fun-rec-project/#/
测试用户名`user` 密码`pass`
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/登录.jpg" width = "30%" height = "30%" alt="登录"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%99%BB%E5%BD%95.jpg" width = "30%" height = "30%" alt="登录"/>
</div>
@@ -67,7 +67,7 @@ http://theneverlemon.gitee.io/vue2-fun-rec-project/#/
- 注册成功后将跳转至主页面
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/注册.jpg" width = "30%" height = "30%" alt="注册"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%B3%A8%E5%86%8C.jpg" width = "30%" height = "30%" alt="注册"/>
</div>
@@ -77,9 +77,9 @@ http://theneverlemon.gitee.io/vue2-fun-rec-project/#/
- 点进新闻详情页后阅读次数会实时增加
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/推荐.jpg" width = "30%" height = "30%" alt="推荐"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/热门.jpg" width = "30%" height = "30%" alt="热门"/>
<div align="center">
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%8E%A8%E8%8D%90.jpg" width = "30%" height = "30%" alt="推荐"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%83%AD%E9%97%A8.jpg" width = "30%" height = "30%" alt="热门"/>
</div>
@@ -90,8 +90,8 @@ http://theneverlemon.gitee.io/vue2-fun-rec-project/#/
- 底部点击`喜欢`或者`收藏`可以记录将当前用户行为,并在列表页相应增加
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/新闻详情.jpg" width = "30%" height = "30%" alt="新闻详情"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/新闻详情2.jpg" width = "30%" height = "30%" alt="新闻详情2"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%85.jpg" width = "30%" height = "30%" alt="新闻详情"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%852.jpg" width = "30%" height = "30%" alt="新闻详情2"/>
</div>
- [X] **个人中心** —— 记录用户的头像和用户名
@@ -101,8 +101,8 @@ http://theneverlemon.gitee.io/vue2-fun-rec-project/#/
- 显示DataWhale相关介绍
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/个人中心1.jpg" width = "30%" height = "30%" alt="个人中心1"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/个人中心2.jpg" width = "30%" height = "30%" alt="个人中心2"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%831.jpg" width = "30%" height = "30%" alt="个人中心1"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%832.jpg" width = "30%" height = "30%" alt="个人中心2"/>
</div>

View File

@@ -52,7 +52,7 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
测试用户名`user` 密码`pass`
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/登录.jpg" width = "30%" height = "30%" alt="登录"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%99%BB%E5%BD%95.jpg" width = "30%" height = "30%" alt="登录"/>
</div>
@@ -67,7 +67,7 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
- 注册成功后将跳转至主页面
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/注册.jpg" width = "30%" height = "30%" alt="注册"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%B3%A8%E5%86%8C.jpg" width = "30%" height = "30%" alt="注册"/>
</div>
@@ -77,9 +77,9 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
- 点进新闻详情页后阅读次数会实时增加
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/推荐.jpg" width = "30%" height = "30%" alt="推荐"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/热门.jpg" width = "30%" height = "30%" alt="热门"/>
<div align="center">
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%8E%A8%E8%8D%90.jpg" width = "30%" height = "30%" alt="推荐"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E7%83%AD%E9%97%A8.jpg" width = "30%" height = "30%" alt="热门"/>
</div>
@@ -90,8 +90,8 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
- 底部点击`喜欢`或者`收藏`可以记录将当前用户行为,并在列表页相应增加
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/新闻详情.jpg" width = "30%" height = "30%" alt="新闻详情"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/新闻详情2.jpg" width = "30%" height = "30%" alt="新闻详情2"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%85.jpg" width = "30%" height = "30%" alt="新闻详情"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E6%96%B0%E9%97%BB%E8%AF%A6%E6%83%852.jpg" width = "30%" height = "30%" alt="新闻详情2"/>
</div>
- [X] **个人中心** —— 记录用户的头像和用户名
@@ -101,8 +101,8 @@ http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
- 显示DataWhale相关介绍
<div align="center">
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/个人中心1.jpg" width = "30%" height = "30%" alt="个人中心1"/>
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/个人中心2.jpg" width = "30%" height = "30%" alt="个人中心2"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%831.jpg" width = "30%" height = "30%" alt="个人中心1"/>
<img src="https://raw.githubusercontent.com/kenken-xr/image/main/fun-rec-img%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%832.jpg" width = "30%" height = "30%" alt="个人中心2"/>
</div>

View File

@@ -1,5 +1,5 @@
{
"name": "vue3-fun-rec2",
"name": "vue3-fun-rec",
"version": "0.1.0",
"private": true,
"scripts": {

View File

@@ -1,11 +1,29 @@
<template>
<router-view v-slot="{ Component }">
<!-- 在路由节点内配置name属性且保证为唯一标识 -->
<keep-alive>
<component :is="Component" />
<component :is="Component" v-if="$route.meta.keepAlive" :key="$route.name"/>
</keep-alive>
<component :is="Component" v-if="!$route.meta.keepAlive" :key="$route.name"/>
</router-view>
</template>
<script setup>
import { useStore } from "vuex";
const store = useStore();
// 在页面加载时读取sessionStorage里的状态信息
if (sessionStorage.getItem('store')) {
// 存储状态
store.replaceState(Object.assign({}, store.state, JSON.parse(sessionStorage.getItem('store'))))
}
// 在页面刷新时将vuex里的信息保存到sessionStorage里
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('store', JSON.stringify(store.state))
})
</script>
<style lang="less">
* {
padding: 0;

View File

@@ -1,23 +1,23 @@
// const CryptoJS = require('crypto-js'); //引用AES源码js
import CryptoJS from 'crypto-js'
// 解析
const key = CryptoJS.enc.Utf8.parse("1234123412ABCDEF"); //十六位十六进制数作为密钥
const iv = CryptoJS.enc.Utf8.parse('ABCDEF1234123412'); //十六位十六进制数作为密钥偏移量
//解密方法
function Decrypt(word) {
let encryptedHexStr = CryptoJS.enc.Hex.parse(word);
let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
let encryptedHexStr = CryptoJS.enc.Hex.parse(word); // 16进制
let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr); // 解密的密文必须是base64
let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8); // 以utf8方法转换
return decryptedStr.toString();
}
//加密方法
function Encrypt(word) {
let srcs = CryptoJS.enc.Utf8.parse(word);
let encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return encrypted.ciphertext.toString().toUpperCase();
let encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // 偏移向量 加密模式 填充方式
return encrypted.ciphertext.toString().toUpperCase(); // 加密后的是对象 需要转到文本
}
export default {

View File

@@ -0,0 +1,21 @@
export const sessionStorage = {
//存储
set(key, value) {
window.sessionStorage.setItem(key, JSON.stringify(value))
},
//取出数据
get(key) {
const value = window.sessionStorage.getItem(key)
if (value && value != "undefined" && value != "null") {
return JSON.parse(value)
}
return null
},
// 删除数据
remove(key) {
window.sessionStorage.removeItem(key)
}
}

View File

@@ -26,6 +26,7 @@ axios.post('http://47.108.56.188:3000/recsys/login?username=11&passwd=111111').t
store.state.flag = true
}).catch((e)=>{
store.state.flag = false
console.log(e)
})
!store.state.flag && require("./mock/index.js")

View File

@@ -54,18 +54,20 @@ for(let i = 0; i < 10; i ++){
})
}
Mock.mock('/recList', 'get', () => {
return {
code:200,
data:Data
}
})
console.log('recListMock',Data)
return {
code:200,
data:Data
}
})
Mock.mock('/hotList', 'get', () => {
return {
code:200,
data:Data
}
console.log('hotListMock',Data)
return {
code:200,
data:Data
}
})
Mock.mock(RegExp("/newsInfo" + ".*"), 'get', (option) => {

View File

@@ -25,11 +25,11 @@
</ul>
</div>
<van-pull-refresh v-model="data.isLoading" @refresh="data.onRefresh">
<van-pull-refresh v-model="data.isLoading" @refresh="onRefresh">
<div class="lists">
<van-list v-model="data.vanListLoading" :finished="data.finished" :finished-text="data.finishedText" @load="onLoad" :offset=300>
<!-- 循环store.state.hotList内的每一个item并显示 -->
<van-cell v-for="(item,i) in store.state.hotList" :key="i">
<van-cell v-for="(item) in store.state.hotList" :key="item.news_id">
<!-- 路由地址传参,需要前面加表示这个参数不是字符串 -->
<router-link :to="{name:'NewsInfo' ,params:{id:item.news_id,likes:item.likes,collections:item.collections,cate:item.cate}}">
<div>
@@ -51,8 +51,6 @@
</div>
</van-pull-refresh>
<!-- 底部导航栏多个组件都会用到需要时直接引入 -->
<bottomBar></bottomBar>
@@ -61,10 +59,10 @@
<script setup>
import bottomBar from "@/components/bottomBar.vue";
import { reactive, onMounted, onActivated, getCurrentInstance } from "vue";
import { reactive, onActivated, getCurrentInstance } from "vue";
import { useStore } from "vuex";
const store = useStore();
import { useRouter, onBeforeRouteLeave } from "vue-router";
const store = useStore();
const router = useRouter();
const { proxy } = getCurrentInstance();
@@ -123,7 +121,6 @@
document.documentElement.scrollTop = data.scrollTop
})
//在离开该组件时执行,执行完后跳转
// to:要去到的组件 from:离开的组件(本组件) next():执行的函数,下一步
onBeforeRouteLeave((to, from, next) => {
@@ -181,4 +178,7 @@
.hotPath {
fill: #428bff;
}
/* .van-pull-refresh {
overflow: auto;
} */
</style>

View File

@@ -31,11 +31,11 @@
<script setup>
import bottomBarVue from "@/components/bottomBar.vue"
import { routerKey, useRouter } from "vue-router";
import { Toast } from 'vant'
import { reactive, getCurrentInstance } from "vue";
import { useStore } from "vuex";
const store = useStore();
import { routerKey, useRouter } from "vue-router";
const router = useRouter();
const { proxy } = getCurrentInstance();

View File

@@ -36,10 +36,10 @@
<script setup>
import { Toast } from 'vant'
import { reactive, getCurrentInstance } from "vue";
import { reactive, getCurrentInstance, onMounted } from "vue";
import { useStore } from "vuex";
const store = useStore();
import { useRouter, useRoute } from "vue-router";
const store = useStore();
const router = useRouter();
const route = useRoute();
const { proxy } = getCurrentInstance();
@@ -77,6 +77,8 @@
})
}
console.log(successData);
if (successData.status === 200) {
data.news_content = successData.data.data
data.content = data.news_content.content.split(reg)[0]
@@ -185,9 +187,9 @@
}
// 创建页面时调用函数
getNewsInfo()
sendInfo()
// 创建页面时调用函数
getNewsInfo()
sendInfo()
</script>

View File

@@ -1,10 +1,10 @@
<template>
<div ref="content">
<div class="tabs" ref="tabs">
<div>
<div class="tabs">
<input type="radio" id="tab1" name="tab-control" checked>
<input type="radio" id="tab2" name="tab-control">
<ul>
<li title="Features" class="rec" @click="toRec">
<li title="Features" class="rec">
<label for="tab1" role="button" class="recCont">
<svg viewBox="0 0 24 24" class="recPath">
<path d="M14,2A8,8 0 0,0 6,10A8,8 0 0,0 14,18A8,8 0 0,0 22,10H20C20,13.32 17.32,16 14,16A6,6 0 0,1 8,10A6,6 0 0,1 14,4C14.43,4 14.86,4.05 15.27,4.14L16.88,2.54C15.96,2.18 15,2 14,2M20.59,3.58L14,10.17L11.62,7.79L10.21,9.21L14,13L22,5M4.93,5.82C3.08,7.34 2,9.61 2,12A8,8 0 0,0 10,20C10.64,20 11.27,19.92 11.88,19.77C10.12,19.38 8.5,18.5 7.17,17.29C5.22,16.25 4,14.21 4,12C4,11.7 4.03,11.41 4.07,11.11C4.03,10.74 4,10.37 4,10C4,8.56 4.32,7.13 4.93,5.82Z" />
@@ -26,10 +26,10 @@
</div>
<van-pull-refresh v-model="data.isLoading" @refresh="onRefresh">
<div class="lists" ref="lists">
<van-list v-model="data.vanListLoading" :finished="data.finished" :finished-text="data.finishedText" @load="onLoad" :offset=300>
<!-- 循环$store.state.hotList内的每一个item并显示 -->
<van-cell v-for="(item,i) in store.state.recList" :key="i">
<div class="lists">
<van-list v-model:loading="data.vanListLoading" :finished="data.finished" :finished-text="data.finishedText" @load="onLoad" :offset=300>
<!-- 循环store.state.recList内的每一个item并显示 -->
<van-cell v-for="(item) in store.state.recList" :key="item.news_id">
<!-- 路由地址传参,需要前面加表示这个参数不是字符串 -->
<router-link :to="{name:'NewsInfo' ,params:{id:item.news_id,likes:item.likes,collections:item.collections,cate:item.cate}}">
<div>
@@ -61,14 +61,13 @@
import bottomBar from "@/components/bottomBar.vue";
import { reactive, onActivated, getCurrentInstance } from "vue";
import { useStore } from "vuex";
const store = useStore();
import { useRouter, onBeforeRouteLeave } from "vue-router";
const store = useStore();
const router = useRouter();
const { proxy } = getCurrentInstance();
const data = reactive({
isActive: true,
vanListLoading: false, // 加载状态
finished: false, // 是否加载
finishedText: '', // 加载完成后的提示文案
@@ -77,15 +76,22 @@
});
function onRefresh() {
setTimeout(() => {
// data.isLoading = false;
// store.commit('clearList', "recList")
// getList()
// data.isLoading = true;
// data.finished = false;
// data.isLoading = true;
// getList()
setTimeout(() => {
data.isLoading = false;
store.commit('clearList', "hotList")
this.getList()
getList()
}, 1000);
}
async function getList() {
var url;
let url;
if(store.state.type == 'signIn'){
url = '/recsys/rec_list?' + 'user_id=' + store.state.user.username
}else if(store.state.type == 'signUp'){
@@ -98,7 +104,8 @@
return res
})
}else {
successData = await proxy.axios.get("/recList").then((res)=>{
successData = await proxy.axios.get("/recList").then(res => {
console.log(store.state.recList,'recList')
return res
})
}
@@ -184,4 +191,8 @@
.recPath {
fill: #428bff;
}
/* .van-pull-refresh {
overflow: auto;
} */
</style>

View File

@@ -93,7 +93,7 @@ async function login() {
const timer = setInterval(() => {
second--;
if (!second) {
clearInterval(timer);
clearInterval(timer);
// 手动清除 Toast
Toast.clear();
router.push({name:'recLists' ,params:{type:'signIn',username:data.model.username}})