Merge pull request #46 from kenken-xr/master

vue3
This commit is contained in:
若如意
2022-02-18 10:24:07 +08:00
committed by GitHub
36 changed files with 31309 additions and 1 deletions

2
.gitignore vendored
View File

@@ -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

View File

@@ -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数据模拟远程服务器获取不到数据时使用没有推荐功能)
---

View File

@@ -1,7 +1,8 @@
**演示链接:**
http://theneverlemon.gitee.io/vue2-fun-rec-project/#/
测试用户名: `user` 测试密码: `pass`
- 测试用户名: `11` 测试密码: `111111` (连接远程服务器,具有推荐功能,优先使用这个)
- 测试用户名: `user` 测试密码: `pass` (mock数据模拟远程服务器获取不到数据时使用没有推荐功能)
---

View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View 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'
}
}

View 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?

View 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')
}
```

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -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>

View 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>

View File

@@ -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);
}

View File

@@ -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;
}
}

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,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
}

View File

@@ -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
}

View File

@@ -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>

View 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");

View 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,
}
})

View 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

View File

@@ -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
}
}
}
}
},
});

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View File

@@ -0,0 +1,4 @@
module.exports = {
publicPath: "./",
lintOnSave: false,
};