2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
/codes/news_recsys/news_rec_web/Vue-newsinfo/node_modules/
|
||||
/codes/news_recsys/news_rec_web/vue2-fun-rec/node_modules/
|
||||
/codes/news_recsys/news_rec_web/vue2-fun-rec/node_modules/
|
||||
*.pyc
|
||||
*.log
|
||||
@@ -37,6 +37,19 @@ http://theneverlemon.gitee.io/vue2-fun-rec-project/#/
|
||||
- 测试用户名: `11` 测试密码: `111111` (连接远程服务器,具有推荐功能,优先使用这个)
|
||||
- 测试用户名: `user` 测试密码: `pass` (mock数据模拟,远程服务器获取不到数据时使用,没有推荐功能)
|
||||
|
||||
<br>
|
||||
|
||||
#### vue3
|
||||
|
||||
**源代码:**
|
||||
[GitHub](https://github.com/datawhalechina/fun-rec/tree/master/codes/news_recsys/news_rec_web/vue3-fun-rec) [Gitee](https://gitee.com/theNeverLemon/vue3-fun-rec)
|
||||
|
||||
**演示链接:**
|
||||
http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
|
||||
|
||||
- 测试用户名: `11` 测试密码: `111111` (连接远程服务器,具有推荐功能,优先使用这个)
|
||||
- 测试用户名: `user` 测试密码: `pass` (mock数据模拟,远程服务器获取不到数据时使用,没有推荐功能)
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
**演示链接:**
|
||||
http://theneverlemon.gitee.io/vue2-fun-rec-project/#/
|
||||
|
||||
测试用户名: `user` 测试密码: `pass`
|
||||
- 测试用户名: `11` 测试密码: `111111` (连接远程服务器,具有推荐功能,优先使用这个)
|
||||
- 测试用户名: `user` 测试密码: `pass` (mock数据模拟,远程服务器获取不到数据时使用,没有推荐功能)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
18
codes/news_recsys/news_rec_web/vue3-fun-rec/.eslintrc.js
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-unused-vars':'off'
|
||||
}
|
||||
}
|
||||
22
codes/news_recsys/news_rec_web/vue3-fun-rec/.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
546
codes/news_recsys/news_rec_web/vue3-fun-rec/README.md
Normal file
@@ -0,0 +1,546 @@
|
||||
**演示链接:**
|
||||
http://theneverlemon.gitee.io/vue3-fun-rec-project/#/
|
||||
|
||||
- 测试用户名: `11` 测试密码: `111111` (连接远程服务器,具有推荐功能,优先使用这个)
|
||||
- 测试用户名: `user` 测试密码: `pass` (mock数据模拟,远程服务器获取不到数据时使用,没有推荐功能)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### 新闻推荐系统
|
||||
|
||||
+ 基于vue3的框架Vant UI的基本使用。
|
||||
|
||||
+ 基于nodejs的npm包管理工具、打包工具webpack和与之相对应的插件。
|
||||
|
||||
+ vue路由的相关知识,路由的配置和渲染以及路由守卫的使用。
|
||||
|
||||
+ vue-axios的使用,项目里调用接口都是用的这个异步ajax插件。
|
||||
|
||||
+ 全局组件的使用。
|
||||
|
||||
---
|
||||
|
||||
|
||||
### 运行
|
||||
|
||||
1. 跳转到前端项目文件目录:`cd vue3-fun-rec`
|
||||
|
||||
2. 本地安装node环境,在项目根目录命令行输入命令`npm install`安装依赖包
|
||||
|
||||
如果因为版本或者网络问题下载失败请执行`npm install -g cnpm -registry=https://registry.npm.taobao.org/
|
||||
`和`cnpm install`
|
||||
|
||||
1. 启动前端服务:`npm run server`
|
||||
|
||||
2. 本机访问地址`http://localhost:8080/#/`
|
||||
|
||||
3. 点击`F12`或者右键选择`检查`打开`开发者模式`,选中移动端浏览(点击左上角箭头右边的手机按钮)开始体验
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
### 目标功能
|
||||
- [X] **用户登录** —— 老用户登录
|
||||
|
||||
- `记住我`可以保存将登录信息保存7天
|
||||
|
||||
- `忘记密码` 暂时没有写逻辑不能使用
|
||||
|
||||
如果获取不到远程数据库 将使用mock进行数据模拟
|
||||
测试用户名`user` 密码`pass`
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/登录.jpg" width = "30%" height = "30%" alt="登录"/>
|
||||
</div>
|
||||
|
||||
|
||||
- [X] **用户注册** —— 新用户注册信息
|
||||
|
||||
- 用户名可以为英文和数字
|
||||
|
||||
- 密码是大于6位的英文和数字
|
||||
|
||||
- 年龄是1-100的整数
|
||||
|
||||
- 注册成功后将跳转至主页面
|
||||
|
||||
<div align="center">
|
||||
<img src="https://gitee.com/theNeverLemon/news-img/raw/master/img/注册.jpg" width = "30%" height = "30%" alt="注册"/>
|
||||
</div>
|
||||
|
||||
|
||||
- [X] **推荐页及热门页内容显示** —— 根据不同用户个性化显示不同新闻内容
|
||||
|
||||
- 推荐页和热门页之间的切换(首次切换时会刷新,正在修复这个bug)
|
||||
|
||||
- 点进新闻详情页后阅读次数会实时增加
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
- [x] **新闻详情** —— 显示当前新闻的详细信息
|
||||
|
||||
- 显示标题、内容等信息
|
||||
|
||||
- 底部点击`喜欢`或者`收藏`可以记录将当前用户行为,并在列表页相应增加
|
||||
|
||||
<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"/>
|
||||
</div>
|
||||
|
||||
- [X] **个人中心** —— 记录用户的头像和用户名
|
||||
|
||||
- 显示头像和登录名(头像暂时统一为DataWhle图标)
|
||||
|
||||
- 显示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"/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
### 项目目录
|
||||
```markdown
|
||||
├─ .browserslistrc 配置兼容浏览器
|
||||
├─ .eslintrc.js eslintrc配置文件
|
||||
├─ babel.config.js 兼容js语法
|
||||
├─ package.json 项目配置文件
|
||||
├─ public
|
||||
│ ├─ favicon.ico 浏览器小图标
|
||||
│ └─ index.html 首页入口文件
|
||||
├─ README.md 项目介绍
|
||||
├─ src
|
||||
│ ├─ App.vue 根组件
|
||||
│ ├─ assets 资源目录
|
||||
│ │ ├─ css 样式文件
|
||||
│ │ │ ├─ sign.css 登录注册页的样式
|
||||
│ │ │ └─ test.css 顶部导航样式
|
||||
│ │ ├─ images 静态图片
|
||||
│ │ │ ├─ collects.png 未选中收藏
|
||||
│ │ │ ├─ collects1.png 选中收藏
|
||||
│ │ │ ├─ datawhale.png DataWhale头像
|
||||
│ │ │ ├─ dw.png DataWhale二维码
|
||||
│ │ │ ├─ favicon.ico 浏览器小图标
|
||||
│ │ │ ├─ likes.png 未选中喜欢
|
||||
│ │ │ └─ likes1.png 选中喜欢
|
||||
│ │ └─ js 功能文件
|
||||
│ │ ├─ cookie.js 定义cookie的相关操作
|
||||
│ │ └─ encrypt.js 密码加密
|
||||
│ ├─ components 组件
|
||||
│ │ └─ bottomBar.vue 底部导航
|
||||
│ ├─ main.js 入口js文件
|
||||
│ ├─ mock
|
||||
│ │ └─ index.js 数据模拟
|
||||
│ ├─ router
|
||||
│ │ └─ index.js 配置路由控制页面跳转
|
||||
│ ├─ store
|
||||
│ │ └─ index.js 状态管理
|
||||
│ └─ views 视图
|
||||
│ ├── hotLists.vue 热门页
|
||||
│ ├── Myself.vue 个人中心
|
||||
│ ├── NewsInfo.vue 新闻详情页
|
||||
│ ├── recLists.vue 推荐页
|
||||
│ ├── signIn.vue 登录
|
||||
│ └── signUp.vue 注册
|
||||
└─ vue.config.js vue项目的配置文件,专用于vue项目
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 数据获取
|
||||
在远程服务器偶尔获取不到的情况下通过`mockjs`进行数据的模拟
|
||||
|
||||
#### 定义flag
|
||||
```javascript
|
||||
// store/index.js
|
||||
state: {
|
||||
flag:false,
|
||||
},
|
||||
```
|
||||
|
||||
#### 判断服务器是否正常运行
|
||||
```javascript
|
||||
// main.js
|
||||
|
||||
// 如果数据可以获取到 将flag赋值为true 否则赋值为false
|
||||
axios.post('http://47.108.56.188:3000/recsys/login?username=11&passwd=111111').then(() => {
|
||||
store.state.flag = true
|
||||
}).catch(()=>{
|
||||
store.state.flag = false
|
||||
})
|
||||
|
||||
// 如果为false 引入mockjs进行数据模拟
|
||||
!store.state.flag && require("./mock/index.js")
|
||||
```
|
||||
|
||||
#### 通过flag请求不同的数据链接
|
||||
```javascript
|
||||
// hotList.vue
|
||||
|
||||
async getList() {
|
||||
let url = '/recsys/hot_list?' + 'user_id=' + this.$store.state.user.username
|
||||
|
||||
let successData
|
||||
// 通过flag判断url地址
|
||||
if(this.$store.state.flag){
|
||||
successData = await this.axios.get(url).then(res => {
|
||||
return res
|
||||
})
|
||||
}else {
|
||||
successData = await this.axios.get("/hotList").then(res => {
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (successData.data.code === 200) {
|
||||
this.$store.state.hotList.push(...successData.data.data)
|
||||
this.vanListLoading = false
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
### 主要文件说明
|
||||
|
||||
#### 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.js
|
||||
|
||||
定义路由相关配置,控制页面跳转
|
||||
|
||||
``` javascript
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
alias: '/signIn', // 设置别名 当访问'/signIn'时,显示'/'的页面
|
||||
component: SignIn, // 同步加载组件,加载完成后进入首页
|
||||
name: 'signIn',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/signUp',
|
||||
component: () => import('../views/SignUp.vue'), //异步加载组件,进入组件时再加载提高进入首页时的加载速度
|
||||
name: 'signUp',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
```
|
||||
|
||||
路由守卫,用户未登录时通过外部链接进入页面会跳转到首页
|
||||
|
||||
``` javascript
|
||||
/*
|
||||
* beforeEach:从一个页面跳转到另外一个页面时触发
|
||||
* to:要跳转的页面
|
||||
* from:从哪个页面出来
|
||||
* next:决定是否通过
|
||||
*/
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (cookie.getCookie("openId")) {
|
||||
next()
|
||||
} else {
|
||||
if (to.path == "/" || to.path == '/signUp') {
|
||||
next()
|
||||
} else {
|
||||
Toast({
|
||||
message: '暂未登录,请先登录',
|
||||
});
|
||||
let second = 1;
|
||||
// 延迟一秒执行
|
||||
const timer = setInterval(() => {
|
||||
second--;
|
||||
if (!second) {
|
||||
clearInterval(timer);
|
||||
// 手动清除 Toast
|
||||
Toast.clear();
|
||||
}
|
||||
}, 1000);
|
||||
next('/')
|
||||
}
|
||||
}
|
||||
if(to.path == '/myself'){
|
||||
document.documentElement.scrollTop = 0
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
#### store/index.js
|
||||
|
||||
管理用户的各种状态
|
||||
|
||||
|
||||
``` javascript
|
||||
//创建一个 store
|
||||
export default createStore({
|
||||
// 添加 state 状态
|
||||
state: {
|
||||
|
||||
},
|
||||
|
||||
// 更改 store 中的 state 状态
|
||||
mutations: {
|
||||
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**state:**
|
||||
|
||||
在store中存储状态,在组件中通过 `this.$store.state.type` 调用
|
||||
|
||||
``` javascript
|
||||
state: {
|
||||
flag:false, // 判断是否引入mock
|
||||
type: '', //signIn,signUp 区分获取接口时的url
|
||||
user: {
|
||||
username: '',
|
||||
age: '',
|
||||
gender: ''
|
||||
}, //存储用户信息
|
||||
recList: [], //推荐页的新闻列表
|
||||
hotList: [], //热门页的新闻列表
|
||||
},
|
||||
```
|
||||
|
||||
**mutations:**
|
||||
|
||||
更改 store 中的状态,在组件中通过 `store.commit('FunctionName')`调用
|
||||
|
||||
``` javascript
|
||||
mutations: {
|
||||
//点进新闻详情页时触发,让阅读次数增加
|
||||
numChange(state, payload) {
|
||||
let reg = /NewsInfo\//
|
||||
if(payload.item == 'recList'){
|
||||
for (let i = 0; i < state.recList.length; i++) {
|
||||
if (state.recList[i].news_id == payload.path.split(reg)[1]) {
|
||||
state.recList[i].read_num++
|
||||
}
|
||||
}
|
||||
}else if(payload.item == 'hotList'){
|
||||
for (let i = 0; i < state.hotList.length; i++) {
|
||||
if (state.hotList[i].news_id == payload.path.split(reg)[1]) {
|
||||
state.hotList[i].read_num++
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//点击喜欢或者收藏时触发,让相应次数增加或者减少
|
||||
actionChange(state, payload){
|
||||
if(payload.type == 'likes'){
|
||||
for(let i = 0; i<state.recList.length; i++){
|
||||
if(state.recList[i].news_id == payload.id){
|
||||
state.recList[i].likes = state.recList[i].likes + payload.num
|
||||
}
|
||||
}
|
||||
for(let i = 0; i<state.hotList.length; i++){
|
||||
if(state.hotList[i].news_id == payload.id){
|
||||
state.hotList[i].likes = state.hotList[i].likes + payload.num
|
||||
}
|
||||
}
|
||||
}else if(payload.type == 'collections'){
|
||||
for(let i = 0; i<state.recList.length; i++){
|
||||
if(state.recList[i].news_id == payload.id){
|
||||
state.recList[i].collections = state.recList[i].collections + payload.num
|
||||
}
|
||||
}
|
||||
for(let i = 0; i<state.hotList.length; i++){
|
||||
if(state.hotList[i].news_id == payload.id){
|
||||
state.hotList[i].collections = state.hotList[i].collections + payload.num
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### APP.vue
|
||||
|
||||
定义了组件的缓存
|
||||
|
||||
从新闻列表跳到详情页,然后返回详情页的时列表不需要刷新,并且滚动条保持在之前的位置,使用keep-alive组件进行状态缓存
|
||||
|
||||
被keep-alive包裹住的组件在重新进入时不会刷新,通过设置router中的meta.keepAlive属性值选择需要被缓存的组件
|
||||
|
||||
``` html
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
```
|
||||
|
||||
|
||||
#### signIn.vue/signUp.vue
|
||||
|
||||
登录注册时将信息存入store
|
||||
|
||||
``` javascript
|
||||
store.state.type = 'signIn'
|
||||
store.state.user.username = res.username
|
||||
```
|
||||
|
||||
存入cookie值
|
||||
|
||||
``` javascript
|
||||
// checke:true--选中记住我 checke:false--未选中记住我
|
||||
if(data.checked){
|
||||
// 调用setCookie方法,同时传递需要存储的数据,保存天数
|
||||
data.cookie.setCookie(loginInfo, 7)
|
||||
}else{
|
||||
data.cookie.setCookie(loginInfo, 1)
|
||||
}
|
||||
```
|
||||
|
||||
#### views/recLists.vue(hotLists.vue)
|
||||
|
||||
|
||||
再次进入页面时定位在退出时的位置
|
||||
|
||||
``` javascript
|
||||
// 当组件在 <keep-alive> 内被切换,activated 会被对应执行
|
||||
// 每次进入该组件时会执行,设置滚动条的位置
|
||||
onActivated(()=>{
|
||||
document.documentElement.scrollTop = data.scrollTop
|
||||
})
|
||||
|
||||
//在离开该组件时执行,执行完后跳转
|
||||
// to:要去到的组件 from:离开的组件(本组件) next():执行的函数,下一步
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
// 如果下一个去到的组件是新闻详情页,触发store中的numChange函数,使阅读次数+1
|
||||
if(to.name == 'NewsInfo' ){
|
||||
store.commit('numChange', {item:'recList',path:to.path})
|
||||
}
|
||||
// 存储离开时的滚动条位置
|
||||
data.scrollTop = document.documentElement.scrollTop
|
||||
// next()必须要写,不写不会发生跳转
|
||||
next();
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
#### views/NewsInfo.vue
|
||||
|
||||
发送action请求
|
||||
|
||||
``` javascript
|
||||
sendInfo() {
|
||||
// 阅读
|
||||
var val = {
|
||||
user_name: store.state.user.username,
|
||||
news_id: data.id,
|
||||
action_time: Date.now(),
|
||||
action_type: 'read',
|
||||
}
|
||||
|
||||
// 喜欢
|
||||
var val = {
|
||||
user_name: store.state.user.username,
|
||||
news_id: data.id,
|
||||
action_time: Date.now(),
|
||||
action_type: `likes:${data.islike}`,
|
||||
}
|
||||
|
||||
// 收藏
|
||||
var val = {
|
||||
user_name: store.state.user.username,
|
||||
news_id: data.id,
|
||||
action_time: Date.now(),
|
||||
action_type: `collections:${data.iscollection}`,
|
||||
}
|
||||
|
||||
// 发送对应请求
|
||||
proxy.axios.post("/recsys/action", val).then(resource => {
|
||||
if (resource.status !== 200) {
|
||||
Toast('加载数据失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
```
|
||||
|
||||
#### views/Myself.vue
|
||||
|
||||
退出登录时删除该用户相关信息
|
||||
|
||||
``` javascript
|
||||
quit() {
|
||||
// 清空该用户的新闻列表
|
||||
store.commit('clearUser');
|
||||
|
||||
/*删除cookie*/
|
||||
proxy.cookie.clearCookie('LoginName')
|
||||
proxy.cookie.clearCookie('openId')
|
||||
|
||||
// 跳转到登录页
|
||||
router.push('/signIn')
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
28305
codes/news_recsys/news_rec_web/vue3-fun-rec/package-lock.json
generated
Normal file
37
codes/news_recsys/news_rec_web/vue3-fun-rec/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "vue3-fun-rec2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vant/area-data": "^1.2.2",
|
||||
"amfe-flexible": "^2.2.1",
|
||||
"axios": "^0.25.0",
|
||||
"core-js": "^3.6.5",
|
||||
"crypto-js": "^4.1.1",
|
||||
"js-pinyin": "^0.1.9",
|
||||
"mockjs": "^1.1.0",
|
||||
"vant": "^3.4.4",
|
||||
"vue": "^3.0.0",
|
||||
"vue-axios": "^3.4.1",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vuex": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0",
|
||||
"less": "^3.13.1",
|
||||
"less-loader": "^5.0.0"
|
||||
}
|
||||
}
|
||||
BIN
codes/news_recsys/news_rec_web/vue3-fun-rec/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
40
codes/news_recsys/news_rec_web/vue3-fun-rec/src/App.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
|
||||
/* 去掉获取焦点时的边框 */
|
||||
input {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: black;
|
||||
}
|
||||
|
||||
/* 登录注册页面输入框 */
|
||||
:deep(.van-cell__value:hover),
|
||||
:deep(.van-cell__value:focus) {
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
/* container */
|
||||
.login-container {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 1px;
|
||||
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
padding: 3rem 4rem 0 4rem;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/283591/login-background.jpg) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
/* 紫色的遮罩 */
|
||||
.login-container:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-image: radial-gradient(ellipse at left bottom, rgba(22, 24, 47, 1) 0%, rgba(38, 20, 72, .9) 59%, rgba(17, 27, 75, .9) 100%);
|
||||
box-shadow: 0 -20px 150px -20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.form-login {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-bottom: 4.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* 登录和注册的切换 */
|
||||
.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.25rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.login__label {
|
||||
display: block;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.login__label,
|
||||
.login__label--checkbox {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
font-size: .75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login__label--checkbox {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding-left: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
margin-left: 1rem;
|
||||
color: #ffffff;
|
||||
font-size: .75rem;
|
||||
text-transform: inherit;
|
||||
}
|
||||
|
||||
.login__input {
|
||||
color: white;
|
||||
/* font-size: 1.15rem; */
|
||||
width: 100%;
|
||||
/* padding: .5rem 1rem; */
|
||||
/* border: 2px solid transparent; */
|
||||
outline: none;
|
||||
border-radius: 1.5rem;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
letter-spacing: 1px;
|
||||
min-height: 1rem;
|
||||
}
|
||||
|
||||
.login__input:hover,
|
||||
.login__input:focus {
|
||||
color: white;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.login__input+.login__label {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.login__input--checkbox {
|
||||
position: absolute;
|
||||
top: .1rem;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login__submit {
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 1rem;
|
||||
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__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);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/* colors */
|
||||
/* tab setting */
|
||||
/* breakpoints */
|
||||
/* selectors relative to radio inputs */
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #428bff;
|
||||
font-weight: 300;
|
||||
padding: 40px 0 20px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: white;
|
||||
/* padding: 50px; */
|
||||
/* padding-bottom: 80px; */
|
||||
width: 70%;
|
||||
/* height: 20px; */
|
||||
/* box-shadow: 0 14px 28px rgb(0 0 0 / 25%), 0 10px 10px rgb(0 0 0 / 22%); */
|
||||
border-radius: 5px;
|
||||
/* min-width: 240px; */
|
||||
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;
|
||||
color: #428bff;
|
||||
}
|
||||
|
||||
.tabs ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 10px;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tabs ul li {
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
width: 25%;
|
||||
padding: 0 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tabs ul li label {
|
||||
transition: all 0.3s ease-in-out;
|
||||
color: #929daf;
|
||||
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 br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabs ul li label svg {
|
||||
fill: #929daf;
|
||||
height: 2em;
|
||||
vertical-align: bottom;
|
||||
margin-right: 0.2em;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.tabs ul li label:hover,
|
||||
.tabs ul li label:focus,
|
||||
.tabs ul li label:active {
|
||||
outline: 0;
|
||||
color: #bec5cf;
|
||||
}
|
||||
|
||||
.tabs ul li label:hover svg,
|
||||
.tabs ul li label:focus svg,
|
||||
.tabs ul li label:active svg {
|
||||
fill: #bec5cf;
|
||||
}
|
||||
|
||||
@-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;
|
||||
}
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 323 B |
|
After Width: | Height: | Size: 433 B |
|
After Width: | Height: | Size: 681 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 313 B |
|
After Width: | Height: | Size: 393 B |
@@ -0,0 +1,41 @@
|
||||
|
||||
// 保存cookie
|
||||
// json 需要存储cookie的对象
|
||||
// days 默认存储多少天
|
||||
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
|
||||
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
|
||||
function clearCookie(name) {
|
||||
let json = {};
|
||||
json[name] = '';
|
||||
setCookie(json, -1)
|
||||
}
|
||||
|
||||
export default {
|
||||
setCookie,
|
||||
getCookie,
|
||||
clearCookie
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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();
|
||||
}
|
||||
|
||||
//加密方法
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="bottomBar">
|
||||
<router-view />
|
||||
<van-tabbar route>
|
||||
<van-tabbar-item icon="home-o" replace to="/recLists">首页</van-tabbar-item>
|
||||
<van-tabbar-item icon="friends-o" replace to="/myself">我的</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bottomBar {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
45
codes/news_recsys/news_rec_web/vue3-fun-rec/src/main.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
// 创建根实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 导入路由
|
||||
import router from './router/index';
|
||||
app.use(router);
|
||||
// 导入状态管理
|
||||
import store from "./store/index";
|
||||
app.use(store);
|
||||
|
||||
// 导入axios,axios不是一个插件所以不能Vue.use使用,vue-axios是个插件。
|
||||
import axios from 'axios'
|
||||
import VueAxios from 'vue-axios'
|
||||
app.use(VueAxios, axios);
|
||||
// axios公共基路径,以后所有的请求都会在前面加上这个路径
|
||||
// axios.defaults.baseURL = "http://47.108.56.188:3000";
|
||||
// 设置表单提交方式,默认是 json
|
||||
axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
|
||||
// 数据模拟
|
||||
// 如果服务器可以正常获取到数据则不引入mock
|
||||
// 否则引入mock模拟数据
|
||||
axios.post('http://47.108.56.188:3000/recsys/login?username=11&passwd=111111').then(() => {
|
||||
store.state.flag = true
|
||||
}).catch((e)=>{
|
||||
store.state.flag = false
|
||||
})
|
||||
!store.state.flag && require("./mock/index.js")
|
||||
|
||||
app.config.globalProperties.axios = axios;
|
||||
|
||||
//将cookie绑定在vue的原型上,在各个组件通过 this.coookie调用
|
||||
import cookie from './assets/js/cookie'
|
||||
app.config.globalProperties.cookie = cookie;
|
||||
|
||||
// 导入vant
|
||||
import Vant from "vant";
|
||||
import "vant/lib/index.css";
|
||||
import "amfe-flexible";
|
||||
app.use(Vant);
|
||||
|
||||
// 挂载根实例
|
||||
app.mount("#app");
|
||||
100
codes/news_recsys/news_rec_web/vue3-fun-rec/src/mock/index.js
Normal file
@@ -0,0 +1,100 @@
|
||||
var Mock = require('mockjs')
|
||||
import store from "../store/index";
|
||||
|
||||
// 登录
|
||||
Mock.mock('/login', 'post', (req) => {
|
||||
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) => {
|
||||
const { username } = JSON.parse(req.body)
|
||||
if (username === 'user') {
|
||||
return {
|
||||
code:500
|
||||
}
|
||||
} else if(username !== ''){
|
||||
return {
|
||||
code:200
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
let Data = []
|
||||
for(let i = 0; i < 10; i ++){
|
||||
Data.push({
|
||||
cate:Mock.Random.cword(2),
|
||||
news_id:Mock.Random.string('number', 9),
|
||||
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) => {
|
||||
var res,
|
||||
id = 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
|
||||
// username = option.url.split(/news_id?=/)[1].split(/&user_name=/)[1]
|
||||
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,
|
||||
}
|
||||
})
|
||||
107
codes/news_recsys/news_rec_web/vue3-fun-rec/src/router/index.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
//导入cookie
|
||||
import cookie from '../assets/js/cookie'
|
||||
import { Toast} from 'vant'
|
||||
import SignIn from '../views/SignIn.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
alias: '/signIn', // 设置别名 当访问'/signIn'时,显示'/'的页面
|
||||
component: SignIn, // 同步加载组件,加载完成后进入首页
|
||||
name: 'signIn',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/signUp',
|
||||
component: () => import('../views/SignUp.vue'), //异步加载组件,进入组件时再加载提高进入首页时的加载速度
|
||||
name: 'signUp',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
path: '/recLists',
|
||||
component: () => import('../views/RecLists.vue'),
|
||||
name: 'recLists',
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/hotLists',
|
||||
component: () => import('../views/HotLists.vue'),
|
||||
name: 'hotLists',
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/myself',
|
||||
component: () => import('../views/Myself.vue'),
|
||||
name: 'myself',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
path: '/NewsInfo/:id',
|
||||
name: 'NewsInfo',
|
||||
component: () => import('../views/NewsInfo.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
/*
|
||||
beforeEach:从一个页面跳转到另外一个页面时触发
|
||||
to:要跳转的页面
|
||||
from:从哪个页面出来
|
||||
next:决定是否通过
|
||||
*/
|
||||
|
||||
/* 设置了一个全局守卫,只要发生页面跳转,会执行里面的代码,
|
||||
调用cookie.getCookie()方法读取用户信息,
|
||||
如果不存在代表没有登录,用next('/')进入登录页面进行登录,
|
||||
如果读取到了用户信息,不做拦截直接放行。 */
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (cookie.getCookie("openId")) {
|
||||
next()
|
||||
} else {
|
||||
if (to.path == "/" || to.path == '/signUp') {
|
||||
next()
|
||||
} else {
|
||||
Toast({
|
||||
message: '暂未登录,请先登录',
|
||||
});
|
||||
let second = 1;
|
||||
// 延迟一秒执行
|
||||
const timer = setInterval(() => {
|
||||
second--;
|
||||
if (!second) {
|
||||
clearInterval(timer);
|
||||
// 手动清除 Toast
|
||||
Toast.clear();
|
||||
}
|
||||
}, 1000);
|
||||
next('/')
|
||||
}
|
||||
}
|
||||
if(to.path == '/myself'){
|
||||
document.documentElement.scrollTop = 0
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,81 @@
|
||||
import { createStore } from "vuex";
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
flag:false, // 判断是否引入mock
|
||||
type: '', //signIn,signUp 区分获取接口时的url
|
||||
user: {
|
||||
username: '',
|
||||
age: '',
|
||||
gender: ''
|
||||
}, //存储用户信息
|
||||
recList: [], //推荐页的新闻列表
|
||||
hotList: [], //热门页的新闻列表
|
||||
},
|
||||
mutations: {
|
||||
// 清空列表 在刷新时调用 重新给列表赋值
|
||||
clearList(state, payload){
|
||||
if(payload == "recList"){
|
||||
state.recList = []
|
||||
}else if(payload == "hotList"){
|
||||
state.hotList = []
|
||||
}
|
||||
},
|
||||
|
||||
clearUser(state, payload){
|
||||
state.type = '',
|
||||
state.user = {
|
||||
username: '',
|
||||
age: '',
|
||||
gender: ''}
|
||||
state.recList = []
|
||||
state.hotList = []
|
||||
},
|
||||
|
||||
//点进新闻详情页时触发,让阅读次数增加
|
||||
numChange(state, payload) {
|
||||
let reg = /NewsInfo\//
|
||||
if(payload.item == 'recList'){
|
||||
for (let i = 0; i < state.recList.length; i++) {
|
||||
if (state.recList[i].news_id == payload.path.split(reg)[1]) {
|
||||
state.recList[i].read_num++
|
||||
}
|
||||
}
|
||||
}else if(payload.item == 'hotList'){
|
||||
for (let i = 0; i < state.hotList.length; i++) {
|
||||
if (state.hotList[i].news_id == payload.path.split(reg)[1]) {
|
||||
state.hotList[i].read_num++
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//点击喜欢或者收藏时触发,让相应次数增加或者减少
|
||||
actionChange(state, payload){
|
||||
if(payload.type == 'likes'){
|
||||
for(let i = 0; i<state.recList.length; i++){
|
||||
if(state.recList[i].news_id == payload.id){
|
||||
state.recList[i].likes += payload.num
|
||||
}
|
||||
}
|
||||
for(let i = 0; i<state.hotList.length; i++){
|
||||
if(state.hotList[i].news_id == payload.id){
|
||||
state.hotList[i].likes += payload.num
|
||||
}
|
||||
}
|
||||
}else if(payload.type == 'collections'){
|
||||
for(let i = 0; i<state.recList.length; i++){
|
||||
if(state.recList[i].news_id == payload.id){
|
||||
state.recList[i].collections += payload.num
|
||||
}
|
||||
}
|
||||
for(let i = 0; i<state.hotList.length; i++){
|
||||
if(state.hotList[i].news_id == payload.id){
|
||||
state.hotList[i].collections += payload.num
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="tabs">
|
||||
<input type="radio" id="tab1" name="tab-control">
|
||||
<input type="radio" id="tab2" name="tab-control" checked>
|
||||
<ul>
|
||||
<li title="Features" @click="toRec">
|
||||
<label for="tab1" role="button">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<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>
|
||||
<br>
|
||||
<span>推荐</span>
|
||||
</label>
|
||||
</li>
|
||||
<li title="Delivery Contents" class="hot">
|
||||
<label for="tab2" role="button" class="hotCont">
|
||||
<svg viewBox="0 0 24 24" class="hotPath">
|
||||
<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>
|
||||
<br>
|
||||
<span>热门</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<van-pull-refresh v-model="data.isLoading" @refresh="data.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">
|
||||
<!-- 路由地址传参,需要前面加:号,表示这个参数不是字符串 -->
|
||||
<router-link :to="{name:'NewsInfo' ,params:{id:item.news_id,likes:item.likes,collections:item.collections,cate:item.cate}}">
|
||||
<div>
|
||||
<p>
|
||||
<span class="cate">{{item.cate}}</span>
|
||||
<span class='title'>{{ item.title }}</span>
|
||||
</p>
|
||||
|
||||
<p class="discribe">
|
||||
<span class="ctime">{{ item.ctime}} </span>
|
||||
<span class="read_num">阅读:{{item.read_num}}</span>
|
||||
<span class="likes">喜欢:{{item.likes}}</span>
|
||||
<span class="collections">收藏:{{item.collections}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</router-link>
|
||||
</van-cell>
|
||||
</van-list>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
|
||||
|
||||
|
||||
<!-- 底部导航栏,多个组件都会用到,需要时直接引入 -->
|
||||
<bottomBar></bottomBar>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import bottomBar from "@/components/bottomBar.vue";
|
||||
import { reactive, onMounted, onActivated, getCurrentInstance } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
const store = useStore();
|
||||
import { useRouter, onBeforeRouteLeave } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const data = reactive({
|
||||
vanListLoading: false, // 加载状态
|
||||
finished: false, // 是否加载
|
||||
finishedText: '', // 加载完成后的提示文案
|
||||
scrollTop:0,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
function onRefresh() {
|
||||
setTimeout(() => {
|
||||
data.isLoading = false;
|
||||
store.commit('clearList', "hotList")
|
||||
getList()
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
let url = '/recsys/hot_list?' + 'user_id=' + store.state.user.username
|
||||
|
||||
let successData
|
||||
if(store.state.flag){
|
||||
successData = await proxy.axios.get(url).then(res => {
|
||||
return res
|
||||
})
|
||||
}else {
|
||||
successData = await proxy.axios.get("/hotList").then(res => {
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (successData.data.code === 200) {
|
||||
store.state.hotList.push(...successData.data.data)
|
||||
data.vanListLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// vantUi内部函数,当组件滚动到一定位置时触发 load 事件并将 loading 设置成 true
|
||||
// offset设置当滚动条距离页面底部300px时会触发 load
|
||||
function onLoad() {
|
||||
getList();
|
||||
}
|
||||
function toRec() {
|
||||
router.push('/recLists')
|
||||
}
|
||||
function toHot() {
|
||||
router.push('/hotLists')
|
||||
}
|
||||
|
||||
// 当组件在 <keep-alive> 内被切换,activated 会被对应执行
|
||||
// 每次进入该组件时会执行,设置滚动条的位置
|
||||
onActivated(()=>{
|
||||
document.documentElement.scrollTop = data.scrollTop
|
||||
})
|
||||
|
||||
|
||||
//在离开该组件时执行,执行完后跳转
|
||||
// to:要去到的组件 from:离开的组件(本组件) next():执行的函数,下一步
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
// 如果下一个去到的组件是新闻详情页,触发store中的numChange函数,使阅读次数+1
|
||||
if(to.name == 'NewsInfo' ){
|
||||
store.commit('numChange', {item:'hotList',path:to.path})
|
||||
}
|
||||
// 存储离开时的滚动条位置
|
||||
data.scrollTop = document.documentElement.scrollTop
|
||||
// next()必须要写,不写不会发生跳转
|
||||
next();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url('../assets/css/test.css');
|
||||
|
||||
.lists {
|
||||
position: relative;
|
||||
top: 70px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: .4rem;
|
||||
color: black;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.cate {
|
||||
font-size: .3rem;
|
||||
color: #00F;
|
||||
margin-right: .25rem;
|
||||
padding: .04rem;
|
||||
border: 1px solid #00F
|
||||
}
|
||||
|
||||
.discribe {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: .3rem;
|
||||
color: #918d8d;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.hot {
|
||||
border-bottom: 2px solid #428bff
|
||||
}
|
||||
|
||||
.hotCont {
|
||||
cursor: default;
|
||||
color: #428bff;
|
||||
}
|
||||
|
||||
.hotPath {
|
||||
fill: #428bff;
|
||||
}
|
||||
</style>
|
||||
193
codes/news_recsys/news_rec_web/vue3-fun-rec/src/views/Myself.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="my-info">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" />
|
||||
|
||||
<div class="profile">
|
||||
<div class="profile-pic">
|
||||
<div class="header-color"></div>
|
||||
<img class="tx" src="../assets/images/datawhale.png" alt="头像">
|
||||
</div>
|
||||
<div class="title">
|
||||
<h1>{{data.username}}</h1>
|
||||
</div>
|
||||
<button class="follow" @click="quit">退出登录</button>
|
||||
<div class="description">
|
||||
<h4 class="about">DataWhale 新闻推荐开源项目</h4>
|
||||
<p class="datawhale">Datawhale是一个专注于数据科学与AI领域的开源组织,汇集了众多领域院校和知名企业的优秀学习者,
|
||||
聚合了一群有开源精神和探索精神的团队成员。Datawhale 以“for the learner,和学习者一起成长”为愿景,
|
||||
鼓励真实地展现自我、开放包容、互信互助、敢于试错和勇于担当。同时 Datawhale 用开源的理念去探
|
||||
索开源内容、开源学习和开源方案,赋能人才培养,助力人才成长,建立起人与人,人与知识,人与企业和
|
||||
人与未来的联结。</p>
|
||||
<img src="../assets/images/dw.png" alt="二维码" class="dwimg">
|
||||
</div>
|
||||
|
||||
<!-- 底部导航栏,多个组件都会用到,需要时直接引入 -->
|
||||
<bottomBarVue></bottomBarVue>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import bottomBarVue from "@/components/bottomBar.vue"
|
||||
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();
|
||||
|
||||
const data = reactive({
|
||||
username: 'user'
|
||||
});
|
||||
|
||||
function quit() {
|
||||
// 退出登录时清空该用户信息
|
||||
store.commit('clearUser');
|
||||
|
||||
/*删除cookie*/
|
||||
proxy.cookie.clearCookie('LoginName')
|
||||
proxy.cookie.clearCookie('openId')
|
||||
|
||||
// 跳转到登录页
|
||||
Toast({
|
||||
message: '退出成功',
|
||||
icon:'passed'
|
||||
});
|
||||
|
||||
let second = 1;
|
||||
// 延迟一秒执行
|
||||
const timer = setInterval(() => {
|
||||
second--;
|
||||
if (!second) {
|
||||
clearInterval(timer);
|
||||
// 手动清除 Toast
|
||||
Toast.clear();
|
||||
router.push('/signIn')
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 在该组件被创建时触发,将store中的name值赋给username并显示
|
||||
data.username = store.state.user.username
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap");
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
background: #ede1e7;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
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: #4f759be0;
|
||||
}
|
||||
|
||||
.about {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: .5rem;
|
||||
}
|
||||
|
||||
.datawhale {
|
||||
font-size: .4rem;
|
||||
padding: 0 20px 20px 20px;
|
||||
text-indent: 2em;
|
||||
line-height: .8rem;
|
||||
}
|
||||
|
||||
.dwimg {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
display: block;
|
||||
margin: 15px auto 100px auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div>
|
||||
<van-nav-bar left-text="返回" left-arrow @click-left="onClickLeft" :fixed='data.isFixed' />
|
||||
<div class="newsinfo-continer">
|
||||
<div class="newsTitle">
|
||||
<!--大标题-->
|
||||
<h1>{{ data.news_content.title }}</h1>
|
||||
<!--子标题-->
|
||||
<p>
|
||||
<span>发布时间:{{ data.news_content.ctime}}</span>
|
||||
<span>标签:{{data.cate}}</span>
|
||||
</p>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<!--内容区域-->
|
||||
<div class="content" v-html="data.content"></div>
|
||||
<div class="editor" v-html="data.editor"></div>
|
||||
|
||||
<div id="action">
|
||||
<span>喜欢:
|
||||
<img src="../assets/images/likes.png" alt="" v-show="!data.islike" @click="iflike">
|
||||
<img src="../assets/images/likes1.png" alt="" v-show="data.islike" @click="iflike">
|
||||
</span>
|
||||
<span>收藏:
|
||||
<img src="../assets/images/collects.png" alt="" v-show="!data.iscollection" @click="ifcollection">
|
||||
<img src="../assets/images/collects1.png" alt="" v-show="data.iscollection" @click="ifcollection">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="blank"></div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Toast } from 'vant'
|
||||
import { reactive, getCurrentInstance } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
const store = useStore();
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const data = reactive({
|
||||
// 获取路由地址的id,根据这个id来展示不同的数据。
|
||||
id: route.params.id,
|
||||
news_content: [],
|
||||
content: '',
|
||||
editor: '',
|
||||
islike: false,
|
||||
iscollection: false,
|
||||
isFixed: true,
|
||||
cate: route.params.cate
|
||||
});
|
||||
|
||||
// 返回上一页
|
||||
function onClickLeft() {
|
||||
router.go(-1);
|
||||
}
|
||||
|
||||
// 获取新闻详情
|
||||
async function getNewsInfo() {
|
||||
let reg = /责任编辑/;
|
||||
let user_name = store.state.user.username;
|
||||
|
||||
let successData
|
||||
if(store.state.flag){
|
||||
successData = await proxy.axios.get("/recsys/news_detail?news_id=" + data.id + '&user_name=' + user_name).then(res => {
|
||||
return res
|
||||
})
|
||||
}else {
|
||||
successData = await proxy.axios.get("/newsInfo?news_id=" + data.id + '&user_name=' + user_name).then(res => {
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (successData.status === 200) {
|
||||
data.news_content = successData.data.data
|
||||
data.content = data.news_content.content.split(reg)[0]
|
||||
data.editor = '责任编辑:' + data.news_content.content.split(reg)[1]
|
||||
if (successData.data.data.likes == true) {
|
||||
data.islike = true
|
||||
} else {
|
||||
data.islike = false
|
||||
}
|
||||
if (successData.data.data.collections == true) {
|
||||
data.iscollection = true
|
||||
} else {
|
||||
data.iscollection = false
|
||||
}
|
||||
} else {
|
||||
Toast('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 发送action为read的请求
|
||||
async function sendInfo() {
|
||||
var val = {
|
||||
user_name: store.state.user.username,
|
||||
news_id: data.id,
|
||||
action_time: Date.now(),
|
||||
action_type: 'read',
|
||||
}
|
||||
|
||||
let successData
|
||||
if(store.state.flag){
|
||||
successData = await proxy.axios.post("/recsys/action", val).then(res => {
|
||||
return res
|
||||
})
|
||||
}else {
|
||||
successData = await proxy.axios.post("/action", val).then(res => {
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (successData.status !== 200) {
|
||||
Toast('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 点击喜欢时发送请求
|
||||
async function iflike() {
|
||||
data.islike = !data.islike
|
||||
// 调用store中的actionChange函数控制次数的变化
|
||||
if(data.islike == true){
|
||||
store.commit('actionChange', {type:'likes',id:data.id,num:1})
|
||||
}else{
|
||||
store.commit('actionChange', {type:'likes',id:data.id,num:-1})
|
||||
}
|
||||
var val = {
|
||||
user_name: store.state.user.username,
|
||||
news_id: data.id,
|
||||
action_time: Date.now(),
|
||||
action_type: `likes:${data.islike}`,
|
||||
}
|
||||
|
||||
let successData
|
||||
if(store.state.flag){
|
||||
successData = await proxy.axios.post("/recsys/action", val).then(res => {
|
||||
return res
|
||||
})
|
||||
}else {
|
||||
successData = await proxy.axios.post("/action", val).then(res => {
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (successData.status !== 200) {
|
||||
Toast('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 点击收藏时发送请求
|
||||
async function ifcollection() {
|
||||
data.iscollection = !data.iscollection
|
||||
if(data.iscollection == true){
|
||||
store.commit('actionChange', {type:'collections',id:data.id,num:1})
|
||||
}else{
|
||||
store.commit('actionChange', {type:'collections',id:data.id,num:-1})
|
||||
}
|
||||
var val = {
|
||||
user_name: store.state.user.username,
|
||||
news_id: data.id,
|
||||
action_time: Date.now(),
|
||||
action_type: `collections:${data.iscollection}`,
|
||||
}
|
||||
|
||||
let successData
|
||||
if(store.state.flag){
|
||||
successData = await proxy.axios.post("/recsys/action", val).then(res => {
|
||||
return res
|
||||
})
|
||||
}else {
|
||||
successData = await proxy.axios.post("/action", val).then(res => {
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (successData.status !== 200) {
|
||||
Toast('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 创建页面时调用函数
|
||||
getNewsInfo()
|
||||
sendInfo()
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 标题 */
|
||||
.newsTitle {
|
||||
padding: 1.5rem .5rem 0 .5rem;
|
||||
}
|
||||
|
||||
/* 大标题 */
|
||||
.newsTitle h1 {
|
||||
text-align: center;
|
||||
color: rgb(77,79,83);;
|
||||
font-weight: 600;
|
||||
padding: 20px 0 10px 0;
|
||||
margin: 0;
|
||||
font-size: .6rem;
|
||||
}
|
||||
|
||||
/* 副标题 */
|
||||
.newsTitle p {
|
||||
font-size: .3rem;
|
||||
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: .35rem;
|
||||
padding: 0 20px 20px 20px;
|
||||
text-indent: 2em;
|
||||
line-height: .7rem;
|
||||
/* 首行文本缩进 */
|
||||
color: rgb(28 27 29);
|
||||
}
|
||||
|
||||
/* 责任编辑 */
|
||||
.editor {
|
||||
font-size: .3rem;
|
||||
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: .3rem;
|
||||
}
|
||||
|
||||
.blank {
|
||||
width: 100vw;
|
||||
height: 50px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div ref="content">
|
||||
<div class="tabs" ref="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">
|
||||
<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" />
|
||||
</svg>
|
||||
<br>
|
||||
<span>推荐</span>
|
||||
</label>
|
||||
</li>
|
||||
<li title="Delivery Contents" @click="toHot">
|
||||
<label for="tab2" role="button">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<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>
|
||||
<br>
|
||||
<span>热门</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</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">
|
||||
<!-- 路由地址传参,需要前面加:号,表示这个参数不是字符串 -->
|
||||
<router-link :to="{name:'NewsInfo' ,params:{id:item.news_id,likes:item.likes,collections:item.collections,cate:item.cate}}">
|
||||
<div>
|
||||
<p>
|
||||
<span class="cate">{{item.cate}}</span>
|
||||
<span class='title'>{{ item.title }}</span>
|
||||
</p>
|
||||
|
||||
<p class="discribe">
|
||||
<span class="ctime">{{ item.ctime}} </span>
|
||||
<span class="read_num">阅读:{{item.read_num}}</span>
|
||||
<span class="likes">喜欢:{{item.likes}}</span>
|
||||
<span class="collections">收藏:{{item.collections}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</router-link>
|
||||
</van-cell>
|
||||
</van-list>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 底部导航栏,多个组件都会用到,需要时直接引入 -->
|
||||
<bottomBar></bottomBar>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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 router = useRouter();
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const data = reactive({
|
||||
isActive: true,
|
||||
vanListLoading: false, // 加载状态
|
||||
finished: false, // 是否加载
|
||||
finishedText: '', // 加载完成后的提示文案
|
||||
scrollTop:0,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
function onRefresh() {
|
||||
setTimeout(() => {
|
||||
data.isLoading = false;
|
||||
store.commit('clearList', "hotList")
|
||||
this.getList()
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
var url;
|
||||
if(store.state.type == 'signIn'){
|
||||
url = '/recsys/rec_list?' + 'user_id=' + store.state.user.username
|
||||
}else if(store.state.type == 'signUp'){
|
||||
url = '/recsys/rec_list?' + 'user_id=' + store.state.user.username + '&age=' + store.state.user.age + '&gender=' + store.state.user.gender
|
||||
}
|
||||
|
||||
let successData
|
||||
if(store.state.flag){
|
||||
successData = await proxy.axios.get(url).then(res => {
|
||||
return res
|
||||
})
|
||||
}else {
|
||||
successData = await proxy.axios.get("/recList").then((res)=>{
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (successData.data.code === 200) {
|
||||
store.state.recList.push(...successData.data.data)
|
||||
data.vanListLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// vantUi内部函数,当组件滚动到一定位置时触发 load 事件并将 loading 设置成 true
|
||||
// offset设置当滚动条距离页面底部300px时会触发 load
|
||||
function onLoad() {
|
||||
getList();
|
||||
}
|
||||
function toRec() {
|
||||
router.push('/recLists')
|
||||
}
|
||||
function toHot() {
|
||||
router.push('/hotLists')
|
||||
}
|
||||
|
||||
// 当组件在 <keep-alive> 内被切换,activated 会被对应执行
|
||||
// 每次进入该组件时会执行,设置滚动条的位置
|
||||
onActivated(()=>{
|
||||
document.documentElement.scrollTop = data.scrollTop
|
||||
})
|
||||
|
||||
//在离开该组件时执行,执行完后跳转
|
||||
// to:要去到的组件 from:离开的组件(本组件) next():执行的函数,下一步
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
// 如果下一个去到的组件是新闻详情页,触发store中的numChange函数,使阅读次数+1
|
||||
if(to.name == 'NewsInfo' ){
|
||||
store.commit('numChange', {item:'recList',path:to.path})
|
||||
}
|
||||
// 存储离开时的滚动条位置
|
||||
data.scrollTop = document.documentElement.scrollTop
|
||||
// next()必须要写,不写不会发生跳转
|
||||
next();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url('../assets/css/test.css');
|
||||
|
||||
.lists {
|
||||
position: relative;
|
||||
top: 70px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: .4rem;
|
||||
color: black;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.cate {
|
||||
font-size: .3rem;
|
||||
color: #00F;
|
||||
margin-right: .25rem;
|
||||
padding: .04rem;
|
||||
border: 1px solid #00F
|
||||
}
|
||||
|
||||
.discribe {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: .3rem;
|
||||
color: #918d8d;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.rec {
|
||||
border-bottom: 2px solid #428bff
|
||||
}
|
||||
|
||||
.recCont {
|
||||
cursor: default;
|
||||
color: #428bff;
|
||||
}
|
||||
|
||||
.recPath {
|
||||
fill: #428bff;
|
||||
}
|
||||
</style>
|
||||
296
codes/news_recsys/news_rec_web/vue3-fun-rec/src/views/SignIn.vue
Normal file
@@ -0,0 +1,296 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<ul class="login-nav">
|
||||
<li class="login-nav__item active">
|
||||
<a @click="to('/')">登录</a>
|
||||
</li>
|
||||
<li class="login-nav__item">
|
||||
<a @click="to('/signUp')">注册</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<label for="login-input-user" class="login__label">
|
||||
用户名
|
||||
</label>
|
||||
<van-field v-model="data.model.username" placeholder="请输入用户名" />
|
||||
|
||||
<label for="login-input-password" class="login__label">
|
||||
密码
|
||||
</label>
|
||||
<van-field v-model="data.model.passwd" placeholder="请输入密码" type="password" />
|
||||
|
||||
<van-checkbox v-model="data.checked" shape="square" icon-size="15px" checked-color="#26a2ff" class="login__label--checkbox">记住我</van-checkbox>
|
||||
|
||||
<button class="login__submit" @click="login">登录</button>
|
||||
|
||||
<a href="#" class="login__forgot">忘记密码?</a>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Toast } from 'vant'
|
||||
import encrypt from '../assets/js/encrypt'
|
||||
|
||||
import { reactive, getCurrentInstance } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const data = reactive({
|
||||
model: {
|
||||
username: '',
|
||||
passwd: '',
|
||||
},
|
||||
val: 'login',
|
||||
checked: false,
|
||||
});
|
||||
|
||||
async function login() {
|
||||
let url = '/recsys/login';
|
||||
//密码解密
|
||||
let res = {username: data.model.username, passwd: encrypt.Decrypt(data.model.passwd)};
|
||||
let successData
|
||||
|
||||
if(store.state.flag){
|
||||
successData = await proxy.axios.post(url, res).then(res => {
|
||||
return res
|
||||
})
|
||||
}else {
|
||||
successData = await proxy.axios.post("/login", res).then((res)=>{
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (successData.data.code === 200) {
|
||||
let loginInfo = {
|
||||
LoginName: res.username,
|
||||
openId: "asfafsfsfsdfsdfsdfdsf"
|
||||
}
|
||||
|
||||
// checke:true--选中记住我 checke:false--未选中记住我
|
||||
if(data.checked){
|
||||
// 调用setCookie方法,同时传递需要存储的数据,保存天数
|
||||
proxy.cookie.setCookie(loginInfo, 7)
|
||||
}else{
|
||||
proxy.cookie.setCookie(loginInfo, 1)
|
||||
}
|
||||
|
||||
store.state.type = 'signIn'
|
||||
store.state.user.username = res.username
|
||||
|
||||
Toast({
|
||||
message: '登陆成功',
|
||||
icon:'success'
|
||||
});
|
||||
|
||||
let second = 1;
|
||||
// 延迟一秒执行
|
||||
const timer = setInterval(() => {
|
||||
second--;
|
||||
if (!second) {
|
||||
clearInterval(timer);
|
||||
// 手动清除 Toast
|
||||
Toast.clear();
|
||||
router.push({name:'recLists' ,params:{type:'signIn',username:data.model.username}})
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
}if(successData.data.code === 500){
|
||||
Toast('登陆失败')
|
||||
}if(successData.data.code === 501){
|
||||
Toast('密码输入错误')
|
||||
}if(successData.data.code === 502){
|
||||
Toast('用户名不存在')
|
||||
}
|
||||
}
|
||||
|
||||
// 页面跳转
|
||||
function to(path){
|
||||
router.push(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* container */
|
||||
.login-container {
|
||||
height: 100vh;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 1rem;
|
||||
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: 1rem;
|
||||
height: 100vh;
|
||||
|
||||
/* 登录和注册的切换 */
|
||||
.login-nav {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0 0 3em .5rem;
|
||||
|
||||
.login-nav__item {
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
a {
|
||||
position: relative;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
font-size: .5rem;
|
||||
padding-bottom: .25rem;
|
||||
transition: .20s all ease;
|
||||
&:hover{
|
||||
color: #ffffff;
|
||||
transition: .15s all ease;
|
||||
}
|
||||
&: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 {
|
||||
margin-left: 1.12rem;
|
||||
}
|
||||
}
|
||||
.active {
|
||||
a {
|
||||
color: #ffffff;
|
||||
transition: .15s all ease;
|
||||
&:after {
|
||||
background-color: rgb(17, 97, 237);
|
||||
height: 2px;
|
||||
right: 0;
|
||||
bottom: 2px;
|
||||
border-radius: 0;
|
||||
transition: .20s all ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// :deep(.van-cell__value){
|
||||
// height: .5rem;
|
||||
// }
|
||||
}
|
||||
|
||||
.login-nav__item a:hover:after{
|
||||
background-color: rgb(17, 97, 237);
|
||||
height: 2px;
|
||||
right: 0;
|
||||
bottom: 2px;
|
||||
border-radius: 0;
|
||||
transition: .20s all ease;
|
||||
}
|
||||
|
||||
:deep(.van-cell__value) {
|
||||
margin-top: 0;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
min-height: 1rem;
|
||||
border-radius: .75rem;
|
||||
padding: .35rem .5rem .15rem .5rem;
|
||||
}
|
||||
|
||||
:deep(.van-cell ){
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
background: 0;
|
||||
}
|
||||
:deep(.van-cell:after) {
|
||||
border-bottom: none;
|
||||
}
|
||||
:deep(.van-field__control) {
|
||||
color: #ffffff;
|
||||
}
|
||||
/* label标签 */
|
||||
.login__label {
|
||||
display: block;
|
||||
padding-left: .5rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
font-size: .36rem;
|
||||
margin-bottom: .5rem;
|
||||
margin-top: .75rem;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.login__submit {
|
||||
color: #ffffff;
|
||||
font-size: .35rem;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 1rem;
|
||||
padding: .36rem;
|
||||
border-radius: 1rem;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: rgba(17, 97, 237, .75);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(17, 97, 237, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.login__label{
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
font-size: .36rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.login__label--checkbox {
|
||||
font-size: .36rem;
|
||||
margin-bottom: .5rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.login__input--checkbox {
|
||||
position: absolute;
|
||||
top: .05rem;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.van-checkbox__label) {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.login__forgot {
|
||||
display: block;
|
||||
margin-top: 3rem;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: .36rem;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
color: rgb(17, 97, 237);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
425
codes/news_recsys/news_rec_web/vue3-fun-rec/src/views/SignUp.vue
Normal file
@@ -0,0 +1,425 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<ul class="login-nav">
|
||||
<li class="login-nav__item">
|
||||
<a @click="to('/')">登录</a>
|
||||
</li>
|
||||
<li class="login-nav__item active">
|
||||
<a @click="to('/signUp')">注册</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<label for="login-input-user" class="login__label">
|
||||
用户名
|
||||
</label>
|
||||
<van-field v-model="data.model.username" placeholder="请输入用户名" required @blur='ruleName(data.model.username)' />
|
||||
<span class="errorMessage">{{data.message.username}}</span>
|
||||
|
||||
<label for="login-input-password" class="login__label">
|
||||
密码
|
||||
</label>
|
||||
<van-field v-model="data.model.passwd" placeholder="请输入密码" type="password" required @blur='rulePasswd(data.model.passwd)' />
|
||||
<span class="errorMessage">{{data.message.passwd}}</span>
|
||||
|
||||
<label for="login-input-user" class="login__label">
|
||||
验证密码
|
||||
</label>
|
||||
<van-field v-model="data.model.passwd2" placeholder="再次输入密码" type="password" required @blur='rulePasswd2(data.model.passwd2)' />
|
||||
<span class="errorMessage">{{data.message.passwd2}}</span>
|
||||
|
||||
<label for="login-input-user" class="login__label">
|
||||
年龄
|
||||
</label>
|
||||
<van-field v-model="data.model.age" placeholder="请输入年龄" required @blur='ruleAge(data.model.age)' />
|
||||
<span class="errorMessage">{{data.message.age}}</span>
|
||||
|
||||
<label for="login-input-user" class="login__label">
|
||||
性别
|
||||
</label>
|
||||
<van-radio-group class="login__label_male" v-model="data.model.gender" direction="horizontal" icon-size='16px'>
|
||||
<van-radio name="male">男</van-radio>
|
||||
<van-radio name="female">女</van-radio>
|
||||
</van-radio-group>
|
||||
|
||||
<label for="login-input-user" class="login__label">
|
||||
城市
|
||||
</label>
|
||||
<van-field v-model="data.model.city" readonly is-link name="area" placeholder="点击选择省市" @click="data.showArea = true" />
|
||||
<van-popup v-model:show="data.showArea" position="bottom">
|
||||
<van-area :area-list="data.areaList" @confirm="onConfirm" @cancel="data.showArea = false" :columns-num="2" />
|
||||
</van-popup>
|
||||
|
||||
<button class="login__submit" @click="login">注册</button>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
areaList
|
||||
} from '@vant/area-data';
|
||||
import { Toast } from 'vant'
|
||||
|
||||
import encrypt from '../assets/js/encrypt'
|
||||
import { reactive, getCurrentInstance } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const data = reactive({
|
||||
model: {
|
||||
username: '',
|
||||
passwd: '',
|
||||
passwd2: '',
|
||||
city: '',
|
||||
age: '',
|
||||
gender: 'male',
|
||||
},
|
||||
state: '',
|
||||
message: {
|
||||
username: '',
|
||||
passwd: '',
|
||||
passwd2: '',
|
||||
age: ''
|
||||
},
|
||||
val: 'register',
|
||||
isLogin: true,
|
||||
showArea: false,
|
||||
areaList: areaList
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
async function login() {
|
||||
let url = '/recsys/register'
|
||||
let res = JSON.parse(JSON.stringify(data.model))
|
||||
|
||||
//密码加密
|
||||
res.passwd = encrypt.Encrypt(res.passwd)
|
||||
|
||||
if(data.state && data.model.city){
|
||||
let successData
|
||||
if(store.state.flag){
|
||||
successData = await proxy.axios.post(url, res).then(res => {
|
||||
return res
|
||||
})
|
||||
}else {
|
||||
successData = await proxy.axios.post("/register", res).then((res)=>{
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
if (successData.data.code === 200) {
|
||||
let loginInfo = {
|
||||
LoginName: res.username,
|
||||
openId: "asdasdadasdasdadad"
|
||||
}
|
||||
|
||||
// 调用setCookie方法,同时传递需要存储的数据,保存天数
|
||||
proxy.cookie.setCookie(loginInfo, 7)
|
||||
|
||||
//将信息存入store 全部组件都可以使用
|
||||
store.state.type = 'signUp'
|
||||
store.state.user.username = res.username
|
||||
store.state.user.age = data.model.age
|
||||
store.state.user.gender = data.model.gender
|
||||
|
||||
Toast({
|
||||
message: '注册成功',
|
||||
icon:'success'
|
||||
});
|
||||
|
||||
let second = 1;
|
||||
// 延迟一秒执行
|
||||
const timer = setInterval(() => {
|
||||
second--;
|
||||
if (!second) {
|
||||
clearInterval(timer);
|
||||
// 手动清除 Toast
|
||||
Toast.clear();
|
||||
router.push('/recLists')
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
} if(successData.data.code === 500) {
|
||||
Toast('用户名已存在')
|
||||
}
|
||||
} else {
|
||||
Toast('请完整填写注册信息')
|
||||
}
|
||||
}
|
||||
|
||||
// 页面跳转
|
||||
function to(path){
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
//处理选择的城市
|
||||
function getCity(city) {
|
||||
data.model.city = city.join('-')
|
||||
}
|
||||
|
||||
// 验证用户名
|
||||
function ruleName(val) {
|
||||
var nameReg = /^[A-Za-z0-9]+$/
|
||||
if (val == '') {
|
||||
data.message.username = '请输入用户名'
|
||||
data.state = false
|
||||
return false
|
||||
} else
|
||||
if (!nameReg.test(val)) {
|
||||
data.message.username = '用户名格式为字母和数字'
|
||||
data.state = false
|
||||
return false
|
||||
} else {
|
||||
data.message.username = ''
|
||||
data.state = true
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
function rulePasswd(val) {
|
||||
var reg = /^[A-Za-z0-9]{6,}$/
|
||||
if (val == '') {
|
||||
data.message.passwd = '请输入密码'
|
||||
data.state = false
|
||||
return false
|
||||
} else
|
||||
if (!reg.test(val)) {
|
||||
data.message.passwd = '密码长度至少6位,仅包括为字母和数字'
|
||||
data.state = false
|
||||
return false
|
||||
} else {
|
||||
data.message.passwd = ''
|
||||
data.state = true
|
||||
}
|
||||
}
|
||||
|
||||
//验证密码是否一致
|
||||
function rulePasswd2(val) {
|
||||
if (val == '') {
|
||||
data.message.passwd2 = '请再次输入密码'
|
||||
data.state = false
|
||||
return false
|
||||
} else
|
||||
if (val !== data.model.passwd) {
|
||||
data.message.passwd2 = '两次输入的密码不正确'
|
||||
data.state = false
|
||||
return false
|
||||
} else {
|
||||
data.message.passwd2 = ''
|
||||
data.state = true
|
||||
}
|
||||
}
|
||||
|
||||
//验证年龄
|
||||
function ruleAge(val) {
|
||||
var ageReg = /^([1-9][0-9]{0,1}|100)$/
|
||||
if (val == '') {
|
||||
data.message.age = '请输入年龄'
|
||||
data.state = false
|
||||
return false
|
||||
} else
|
||||
if (!ageReg.test(val)) {
|
||||
data.message.age = '请输入1-100的整数'
|
||||
data.state = false
|
||||
return false
|
||||
} else {
|
||||
data.message.age = ''
|
||||
data.state = true
|
||||
}
|
||||
}
|
||||
|
||||
// 选择城市
|
||||
function onConfirm(values) {
|
||||
let pinyin = require('js-pinyin');
|
||||
data.model.city = values[1].name.slice(0, values[1].name.length - 1) //过滤到市级,删除‘市’
|
||||
data.model.city = pinyin.getFullChars(data.model.city) //将汉字转换为拼音
|
||||
data.showArea = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* container */
|
||||
.login-container {
|
||||
height: 100vh;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 1rem;
|
||||
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: 1rem;
|
||||
height: 100%;
|
||||
|
||||
/* 登录和注册的切换 */
|
||||
.login-nav {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0 0 1em .5rem;
|
||||
|
||||
.login-nav__item {
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
a {
|
||||
position: relative;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
font-size: .5rem;
|
||||
padding-bottom: .25rem;
|
||||
transition: .20s all ease;
|
||||
&:hover{
|
||||
color: #ffffff;
|
||||
transition: .15s all ease;
|
||||
}
|
||||
&: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 {
|
||||
margin-left: 1.12rem;
|
||||
}
|
||||
}
|
||||
.active {
|
||||
a {
|
||||
color: #ffffff;
|
||||
transition: .15s all ease;
|
||||
&:after {
|
||||
background-color: rgb(17, 97, 237);
|
||||
height: 2px;
|
||||
right: 0;
|
||||
bottom: 2px;
|
||||
border-radius: 0;
|
||||
transition: .20s all ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-nav__item a:hover:after{
|
||||
background-color: rgb(17, 97, 237);
|
||||
height: 2px;
|
||||
right: 0;
|
||||
bottom: 2px;
|
||||
border-radius: 0;
|
||||
transition: .20s all ease;
|
||||
}
|
||||
|
||||
:deep(.van-cell__value) {
|
||||
margin-top: 0;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
min-height: 1rem;
|
||||
border-radius: .75rem;
|
||||
padding: .35rem .5rem .15rem .5rem;
|
||||
}
|
||||
|
||||
:deep(.van-cell ){
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
background: 0;
|
||||
}
|
||||
:deep(.van-cell:after) {
|
||||
border-bottom: none;
|
||||
}
|
||||
:deep(.van-field__control) {
|
||||
color: #ffffff;
|
||||
}
|
||||
/* label标签 */
|
||||
.login__label {
|
||||
display: block;
|
||||
padding-left: .5rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
font-size: .36rem;
|
||||
margin-bottom: .5rem;
|
||||
margin-top: .75rem;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.login__submit {
|
||||
color: #ffffff;
|
||||
font-size: .35rem;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 1rem;
|
||||
padding: .36rem;
|
||||
border-radius: 1rem;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: rgba(17, 97, 237, .75);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(17, 97, 237, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.login__label{
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
font-size: .36rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.login__label--checkbox {
|
||||
font-size: .36rem;
|
||||
margin-bottom: .5rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.login__input--checkbox {
|
||||
position: absolute;
|
||||
top: .05rem;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* radio */
|
||||
:deep(.van-radio-group--horizontal) {
|
||||
justify-content: space-around
|
||||
}
|
||||
|
||||
/* radio的label */
|
||||
:deep(.van-radio__label) {
|
||||
color: #ffffff;
|
||||
font-size: .35rem;
|
||||
}
|
||||
|
||||
/* 复选框label的颜色 */
|
||||
:deep(.van-checkbox__label) {
|
||||
color: white;
|
||||
font-size: .35rem;
|
||||
}
|
||||
|
||||
/* 错误信息提示 */
|
||||
.errorMessage {
|
||||
color: red;
|
||||
font-size: .3rem;
|
||||
display: block;
|
||||
padding: .3rem .2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
publicPath: "./",
|
||||
lintOnSave: false,
|
||||
};
|
||||