Authored by 张文文

社区详情页面 review by lea.guo

... ... @@ -24,12 +24,12 @@
</template>
<script>
import { get } from "lodash";
import videojs from "video.js";
import { getArticleImageSize, processImage } from "utils/image-handler";
import { get } from 'lodash';
import videojs from 'video.js';
import { getArticleImageSize, processImage } from 'utils/image-handler';
export default {
name: "VideoPlayer",
name: 'VideoPlayer',
props: {
source: String,
cover: String,
... ... @@ -41,7 +41,7 @@ export default {
return {
muted: true,
controls: true,
aspectRatio: "1:1"
aspectRatio: '1:1'
};
}
}
... ... @@ -68,45 +68,45 @@ export default {
2,
imgSize.width,
imgSize.height,
get(this.yoho, "window.supportWebp")
get(this.yoho, 'window.supportWebp')
);
} else {
return this.cover.split("?")[0];
return this.cover.split('?')[0];
}
} else {
return "";
return '';
}
},
sourceType() {
let type = "video/mp4";
let type = 'video/mp4';
if (this.source) {
let source = this.source.split("?")[0];
let source = this.source.split('?')[0];
source = source.split(".");
source = source.split('.');
switch (source[source.length - 1]) {
case "opus":
case "ogv":
type = "video/ogg";
case 'opus':
case 'ogv':
type = 'video/ogg';
break;
case "mkv":
type = "video/x-matroska";
case 'mkv':
type = 'video/x-matroska';
break;
case "m3u8":
type = "application/x-mpegURL";
case 'm3u8':
type = 'application/x-mpegURL';
break;
case "m4a":
type = "audio/mp4";
case 'm4a':
type = 'audio/mp4';
break;
case "mp3":
type = "audio/mpeg";
case 'mp3':
type = 'audio/mpeg';
break;
case "aac":
type = "audio/aac";
case 'aac':
type = 'audio/aac';
break;
case "oga":
type = "audio/ogg";
case 'oga':
type = 'audio/ogg';
break;
default:
break;
... ... @@ -131,7 +131,6 @@ export default {
},
methods: {
async parentHandleclick() {
// await this.delay(1000);
this.player.play();
const timeId = setTimeout(() => {
... ... @@ -155,37 +154,37 @@ export default {
});
},
initPlayer() {
const noVioceClass = "vjs-yoho-novoice";
const noVioceClass = 'vjs-yoho-novoice';
this.player = videojs(this.$refs.videoPlayer, this.options);
this.voiceBtn = this.player.addChild("button");
this.voiceBtn.addClass("vjs-yoho-voice");
this.voiceBtn = this.player.addChild('button');
this.voiceBtn.addClass('vjs-yoho-voice');
this.voiceBtn.addClass(noVioceClass);
this.backBtn = this.player.addChild("button");
this.backBtn.addClass("vjs-yoho-back");
this.voiceBtn.on("touchend", () => {
this.backBtn = this.player.addChild('button');
this.backBtn.addClass('vjs-yoho-back');
this.voiceBtn.on('touchend', () => {
this.player.muted(!this.voiceBtn.hasClass(noVioceClass));
});
this.backBtn.on("touchend", () => {
this.backBtn.on('touchend', () => {
this.player.exitFullscreen();
});
this.player.on("play", () => {
this.player.on('play', () => {
this.player._yohoPlayTime = this.getTime();
});
this.player.on("pause", () => {
this.player.on('pause', () => {
this.player._yohoPauseTime = this.getTime();
});
this.player.on("ended", () => {
this.player.on('ended', () => {
this.player._yohoEndedTime = this.getTime();
});
this.player.on("fullscreenchange", () => {
this.player.on('fullscreenchange', () => {
this.player._yohoPlayTime = this.getTime();
});
this.player.on("volumechange", () => {
this.player.on('volumechange', () => {
const soundOff = this.player.muted() || this.player.volume() === 0;
if (soundOff) {
... ... @@ -205,7 +204,7 @@ export default {
<style lang="scss" scoped>
.video-js {
width: 100%;
height: auto;
height: 750px;
video {
width: 100%;
... ...
<template>
<div class="avatar-wrap">
<ImageFormat class="img-avatar" :lazy="true" :src="imageSrc" :width="width" :height="height" @error="errorHandle"></ImageFormat>
</div>
</template>
<script>
export default {
name: 'WidgetAvatar',
props: {
src: {
type: String,
default: ''
},
width: {
type: Number,
default: 35
},
height: {
type: Number,
default: 35
},
lazy: {
type: Boolean,
default: false
},
small: {
type: Boolean,
default: false
},
group: [Number, String]
},
data() {
return {
imgSrc: this.src,
defaultSrc: 'https://img12.static.yhbimg.com/article/2019/02/26/16/02456ade977d8dfdbc4ca548b196c1d62b.png?imageView/2/w/{width}/h/{height}'
};
},
computed: {
imageSrc() {
return this.imgSrc || this.defaultSrc;
}
},
watch: {
src(val) {
this.imgSrc = val;
}
},
methods: {
errorHandle() {
this.imgSrc = this.defaultSrc;
}
}
};
</script>
<style lang="css" scoped>
.avatar-wrap {
display: inline-block;
position: relative;
> img {
width: 100%;
height: 100%;
}
}
.img-avatar {
border-radius: 50%;
overflow: hidden;
}
</style>
... ...
<template>
<LayoutApp :show-back="true" title="社区详情">
<div class="detail-container">
<div class="author-container">
<ArticleAuthor :userHeadIco="userHeadIco" :userName="userName"></ArticleAuthor>
</div>
<div class="source-container">
<ArticleVideo v-if="showVideo" :videoUrl="videoUrl" :coverUrl="coverUrl"></ArticleVideo>
<ArticleImage v-else :imageList="imageList"></ArticleImage>
</div>
<div class="slide-container">
<HorizontalSlide :value="imageList"></HorizontalSlide>
</div>
<div class="note-container">
<div class="note-header">Nike 旗下大热鞋款 Air Max 95</div>
<div class="note-content">Nike 旗下大热鞋款 Air Max 95 一直以来在街头造型当中的能见度都算高,凭藉其舒适脚感与百搭外型 @NIKE官方 Max 95 也轻松成为许多鞋迷的心头好紧接「Triple White」之后,备受期待的 Nike 全新鞋款 SF-AF1 Mid 又有一双鞋的「Tiger Camo」配色率先现身网络。</div>
<div class="note-date">2019-01-02</div>
<div class="praise-wrapper" ></div>
</div>
<div class="recommend-container">
<div class="recommend-text">推荐阅读</div>
<div class="recommend-list">list</div>
</div>
</div>
</LayoutApp>
</template>
<script>
import ArticleAuthor from './components/article-author';
import ArticleVideo from './components/article-video';
import ArticleImage from './components/article-image';
import HorizontalSlide from './components/horizontalSlide';
export default {
name: 'articleDetail',
components: {
ArticleAuthor,
ArticleVideo,
ArticleImage,
HorizontalSlide
},
data() {
return {
imageList: [
{image_url: 'http://img11.static.yhbimg.com/goodsimg/2019/07/02/16/0191aa70fd1f46d75e5ff974dfe0cdac1c.jpg?imageMogr2/thumbnail/{width}x{height}/background/d2hpdGU=/position/center/quality/80'},
{image_url: 'http://img11.static.yhbimg.com/goodsimg/2018/11/06/14/01bbabe3eb9597d5a36f51a0a29c23e441.jpg?imageMogr2/thumbnail/{width}x{height}/background/d2hpdGU=/position/center/quality/80'},
{image_url: 'http://img11.static.yhbimg.com/goodsimg/2018/11/06/14/016c59b642d25a92049003c318ea7b97ff.jpg?imageMogr2/thumbnail/{width}x{height}/background/d2hpdGU=/position/center/quality/80'},
{image_url: 'http://img11.static.yhbimg.com/goodsimg/2018/11/06/14/012df38fe6117ea9467a01919a4b4c2f3c.jpg?imageMogr2/thumbnail/{width}x{height}/background/d2hpdGU=/position/center/quality/80'}
],
showVideo: false,
videoUrl: 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4',
coverUrl: 'http://file02.16sucai.com/d/file/2015/0408/779334da99e40adb587d0ba715eca102.jpg',
userHeadIco: 'https://img12.static.yhbimg.com/article/2019/02/26/16/02456ade977d8dfdbc4ca548b196c1d62b.png?imageView/2/w/{width}/h/{height}',
userName: 'MOMO草'
};
},
};
</script>
<style lang="scss" scoped>
.detail-container {
background: #fff;
}
.author-container {
width: 100%;
height: 100px;
}
.source-container {
width: 100%;
height: 750px;
}
.slide-container {
padding: 40px 0 36px 24px;
}
.note-container {
padding: 0 24px 100px;
background: #fff;
}
.note-header {
font-size: 36px;
color: #222;
letter-spacing: 0.34px;
line-height: 56px;
font-weight: bold;
}
.note-content {
font-size: 30px;
color: #000;
letter-spacing: 0.28px;
line-height: 50px;
}
.note-date {
margin-top: 20px;
font-size: 30px;
color: #999;
letter-spacing: 0.28px;
}
.praise-wrapper {
margin-top: 60px;
margin-left: calc((100%-160px)/2);
height: 160px;
width: 160px;
background: url("~statics/image/coupon/used@3x.png");
background-size: contain;
background-repeat: no-repeat;
}
.recommend-container {
background: #F2F2F2;
padding: 36px 24px 0;
}
.recommend-text {
font-size: 32px;
color: #222222;
font-weight: bold;
text-align: left;
}
.recommend-list {
margin-top: 30px;
}
</style>
... ...
<template>
<div class="container">
<WidgetAvatar :lazy="true" class="widget-avatar" :src="userHeadIco" :width="70" :height="70"></WidgetAvatar>
<span>{{userName}}</span>
</div>
</template>
<script>
import WidgetAvatar from '../../../components/widget-avatar';
export default {
name: 'articleAuthor',
components: {
WidgetAvatar
},
props: {
userHeadIco: String,
userName: String
}
};
</script>
<style lang="scss" scoped>
.container {
height: 100px;
padding-left: 24px;
display: flex;
flex-direction: row;
align-items: center;
.widget-avatar {
width: 70px;
height: 70px;
}
span {
margin-left: 20px;
font-size: 24px;
color: #222;
letter-spacing: 0.05px;
}
}
</style>
... ...
<template>
<div class="cover-slide">
<cube-slide ref="slide" :loop="false" :auto-play="false" :data="imageList">
<cube-slide-item v-for="(item, index) in imageList" :key="index" @click.native="clickHandler(item, index)">
<a click="javascript:void 0" class="square-img-container">
<SquareImg :src="item.image_url" :width="600" :height="600"/>
</a>
</cube-slide-item>
<template slot="dots" slot-scope="props">
<div class="dot-wrap">
{{props.current + 1}}/{{imageList.length}}
</div>
</template>
</cube-slide>
</div>
</template>
<script>
import { Slide } from 'cube-ui';
import SquareImg from '../../product/components/square-img';
export default {
name: 'articleImage',
components: {
'cube-slide': Slide,
'cube-slide-item': Slide.Item,
SquareImg
},
props: {
imageList: Array
},
data() {
return {
};
},
methods: {
clickHandler(item, index) {
console.log(item, index);
}
}
};
</script>
<style lang="scss" scoped>
.cover-slide {
flex: 1;
.square-img-container {
display: block;
width: 100%;
height: 750px;
margin: 0 auto;
}
.dot-wrap {
margin-left: 24px;
margin-bottom: 24px;
background: #000;
opacity: 0.5;
width: 80px;
height: 48px;
border-radius: 30px;
color: white;
font-size: 24px;
letter-spacing: 1.96px;
line-height: 48px;
text-align: center;
}
}
.cube-slide-item {
img {
margin: 0 auto;
}
}
</style>
... ...
<template>
<div class="video-container">
<VideoPlayer
ref="videoPlayer"
class="videoWrapper"
:source="videoUrl"
:cover="coverUrl"
></VideoPlayer>
</div>
</template>
<script>
import VideoPlayer from '@/components/video-player';
export default {
name: 'article-video',
props: {
videoUrl: String,
coverUrl: String
},
data() {
return {
};
},
components: {
VideoPlayer
}
};
</script>
<style scoped>
.video-container {
flex: 1;
}
.videoWrapper {
display: block;
width: 100%;
height: 750px;
}
</style>
... ...
... ... @@ -27,7 +27,6 @@ export default {
data() {
return {};
},
methods: {
imageformatError() {
console.log(6666);
... ... @@ -35,8 +34,9 @@ export default {
},
computed: {},
components: {}
}
};
</script>
<style lang="scss" scoped>
.horizontalSlide {
overflow: hidden;
... ... @@ -45,17 +45,20 @@ export default {
-webkit-overscroll-behavior-x: contain;
scroll-behavior: smooth;
}
.list-warp {
overflow-x: scroll;
width: max-content;
font-size: 0;
}
.list-item {
display: inline-flex;
width: 600px;
height: 180px;
margin-right: 24px;
border-radius: 8px;
.image {
min-width: 136px;
min-height: 180px;
... ... @@ -64,6 +67,7 @@ export default {
border-radius: 8px 0 0 8px;
filter: contrast(0.9);
}
.title {
font-size: 24px;
color: #999;
... ... @@ -75,11 +79,13 @@ export default {
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.bottom {
display: flex;
align-items: center;
justify-content: space-between;
}
.info {
padding: 20px;
display: flex;
... ... @@ -88,6 +94,7 @@ export default {
border: 1px solid #F0F0F0;
border-radius: 0 8px 8px 0;
}
.price {
margin-left: 16px;
font-size: 28px;
... ... @@ -95,6 +102,7 @@ export default {
vertical-align: middle;
flex: 1 1 auto;
}
.icon-goumai {
float: right;
}
... ...
... ... @@ -7,11 +7,6 @@ export default [
{
name: 'ArticleDetail',
path: '/xianyu/article/detail',
// component: () => import()
},
{
name: 'waterfall',
path: '/xianyu/waterfallTest',
component: () => import('./waterfall')
},
component: () => import(/* webpackChunkName: "articleDetail" */ './articleDetail')
}
];
... ...
<!--
* @description:
* @fileName: waterfall.vue
* @author: huzhiming
* @date: 2019-12-18 15:23:55
* @后台人员:
* @version: v1.0.0
* @path: 页面访问路径及参数说明
!-->
<template>
<div class="article-waterfall-wrap">
<horizontalSlide v-model="items" :style="{'margin': '20px 0 0 12px'}"></horizontalSlide>
<!-- <div class="water">
<div class="piping" ref="piping0"></div>
<div class="piping" ref="piping1"></div>
<div class="piping" ref="piping2"></div>
<div class="piping" ref="piping3"></div>
</div> -->
</div>
</template>
<script>
import axios from 'axios';
import horizontalSlide from './components/horizontalSlide'
export default {
name: 'Article',
mixins: [],
props: {},
// 服务端渲染函数
async asyncData ({ isDev, route, store, env, params, query, req, res, redirect, error }) {
return {}
},
data() {
return {
items: [...Array(20).keys()].map(item => item),
moments: [],
available: 1,
height1: 0,
height2: 0,
height3: 0,
page: 1
}
},
created() {
// 获取第一页数据
// this.fetchMoments();
},
mounted() {
// 用来监听滚轮
window.addEventListener("scroll", this.handleScroll);
},
activated() {},
deactivated() {},
// beforeRouteEnter (to, from, next) {},
// beforeRouteUpdate(to, from, next) {},
// beforeRouteLeave(to, from, next) {},
destroyed() {},
methods: {
fetchMoments() {
// 请求接口方法
// const result = await this.$api.post('https://m.yohobuy.com/grass/api/grass/topicRelatedArticles', {
// page,
// limit: limit || 10,
// type: 6,
// topicId: 2713
// });
axios.get('https://m.yohobuy.com/grass/api/grass/topicRelatedArticles')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// https://m.yohobuy.com/grass/api/grass/topicRelatedArticles
},
// sort()函数是递归的,因为要确保每个卡片的图片加载完成后再获取管道的高度,但是图片加载完成的函数是个异步函数,
// 如果放在for循环中会打乱顺序,因此要使异步函数同步执行,for循环改为递归。
sort(j) {
if (j < this.moments.length) {
let that = this;
// 创建Image类
var newImg = new Image();
// 获取要加载的图片地址
newImg.src =
"http://lanyue.ink:8123/images/" +
(Math.floor(Math.random() * 15) + 1) +
".png";
// 图片加载完成后(异步)
newImg.onload = () => {
// 四个管道的高度
var arr = [
that.$refs.piping0.offsetHeight,
that.$refs.piping1.offsetHeight,
that.$refs.piping2.offsetHeight,
that.$refs.piping3.offsetHeight
];
//获取管道最小高度
var min = arr.indexOf(Math.min.apply(Math, arr));
// 添加卡片的模板
var html =
`<div class="card">
<img src=` + newImg.src + `>
<div>
<img src="http://lanyue.ink:8123/images/avatar.jpg" alt="">
<div>` + this.moments[j].id + " " + this.moments[j].content + `</div>
</div>
</div>`;
//给最小的管道添加卡片
if (min == 0) {
that.$refs.piping0.innerHTML += html;
} else if (min == 1) {
that.$refs.piping1.innerHTML += html;
} else if (min == 2) {
that.$refs.piping2.innerHTML += html;
} else if (min == 3) {
that.$refs.piping3.innerHTML += html;
}
that.sort(j + 1);
};
}
},
handleScroll() {
// 获取滚轮位置
var scrollTop =
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop;
this.height1 = scrollTop;
// 文档高度
this.height2 = document.body.scrollHeight;
// 可视区域
this.height3 =
document.compatMode == "CSS1Compat"
? document.documentElement.clientHeight
: document.body.clientHeight;
// 如果滚动到最低(这里设置离最底还有100距离才触发函数)
// available条件是为了防止触底时一直不断地请求。因此,请求一次后available设为0,直到滚动到离底部超过100距离(即数据加载玩后)才设为1
if (this.height3 + this.height1 >= this.height2 - 100 && this.available) {
//请求下一页
this.page++;
this.available = 0;
let that = this;
fetch("api/moments?page=" + this.page)
.then(res => res.json())
.then(res => {
that.moments = res.data;
if (that.moments[0]) {
that.sort(0);
} else {
that.page--;
}
});
} else if (this.height3 + this.height1 < this.height2 - 100) {
this.available = 1;
}
}
},
computed: {},
watch: {},
components: {
horizontalSlide
}
};
</script>
/* 定义局部样式,添加外围容器,scss嵌套尽量不要超过三层,会影响查找器性能 */
<style rel='stylesheet/scss' lang='scss' scoped>
.waterfall-wrap {}
//@import "./style.scss";
</style>
/* 定义全局样式,添加外围容器,避免覆盖全局样式, 若不需要,请删除 */
<style rel='stylesheet/scss' lang='scss'>
.waterfall-wrap {}
</style>