Authored by 陈峰

commit

Showing 81 changed files with 4723 additions and 0 deletions

Too many changes to show.

To preserve performance only 81 of 81+ files are displayed.

root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
... ...
bundle
dist
coverage
... ...
{
"env": {
"es6": true
},
"extends": [
"plugin:vue/base",
"yoho"
],
"parserOptions": {
"parser": "babel-eslint",
"sourceType": "module"
},
"plugins": [
"html"
],
"rules": {
"camelcase": "off",
"max-len": "off"
}
}
... ...
# Created by https://www.gitignore.io/api/node,webstorm,netbeans,sublimetext,vim
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
### WebStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### WebStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
### NetBeans ###
nbproject/private/
public/build/bundle
nbbuild/
dist/
nbdist/
nbactions.xml
.nb-gradle/
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
### Vim ###
# swap
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags
### YOHO ###
dist
.eslintcache
*.log.*
nbproject/*
.DS_Store
.devhost
.happypack/
bundle/
\ No newline at end of file
... ...
phantomjs_cdnurl=http://npm.taobao.org/mirrors/phantomjs
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
\ No newline at end of file
... ...
10.3.0
\ No newline at end of file
... ...
**/css/**/*.css
**/dist/**/*.css
... ...
{
"extends": "stylelint-config-yoho"
}
... ...
############################################################
# Dockerfile to build hystrix + turbin Installed Containers
# Based on centos 6.7
# How to build new image: docker build -t yoho-hystrix-qcloud .
# the hystrix alert need to post events to influxdb.yohoops.org.
# nginx version: 1.12.0
############################################################
#base image : ccr.ccs.tencentyun.com/yoho-base/node
FROM ccr.ccs.tencentyun.com/yoho-base/node:10.4.1-alpine
MAINTAINER feng.chen <feng.chen@yoho.cn>
ENV NODE_ENV=production \
NODE_HOME=/home
RUN cd /home && \
mkdir -p /home/ufo-app-web
COPY . /home/ufo-app-web
WORKDIR /home/ufo-app-web
#expose port
EXPOSE 6001
CMD ["node","/home/ufo-app-web/app.js"]
... ...
const fs = require('fs');
const path = require('path');
const cluster = require('cluster');
const express = require('express');
const chokidar = require('chokidar');
const {createApp} = require('./create-app.js');
const {createBundleRenderer} = require('vue-server-renderer');
const {devServer, publish} = require('./build/dev-server.js');
const watcher = {
paths: ['./create-app.js', './doraemon', './config', './utils'],
options: {}
};
let renderer;
let realyPromise;
let template = fs.readFileSync(path.join(__dirname, './apps/index.html'), 'utf-8');
if (cluster.isMaster) {
const masterApp = express();
realyPromise = devServer(masterApp, params => {
renderer = createBundleRenderer(params.bundle, Object.assign(params.options, {
runInNewContext: false,
template
}));
});
let childWorker = cluster.fork();
cluster.on('message', (worker, msg) => {
if (msg.action === 'ssr_request') {
realyPromise.then(() => {
renderer.renderToString(msg.context, (err, html) => {
if (err) {
console.error(err);
}
worker.send({action: 'ssr_request', html, err: err && JSON.stringify(err)});
});
});
}
});
chokidar.watch(watcher.paths, watcher.options).on('change', pathStr => {
console.log(`${pathStr} changed`);
childWorker && childWorker.kill();
childWorker = cluster.fork().on('listening', address => {
console.log(`worker is restarted at ${address.port}`);
publish({action: 'reload'});
console.log('client is refresh');
});
});
masterApp.listen(6005, () => {
console.log('master is started');
});
} else {
const app = express();
createApp(app);
}
... ...
const express = require('express');
const {createApp} = require('./create-app.js');
const app = express();
createApp(app);
... ...
import Vue from 'vue';
import App from './app.vue';
import {createRouter} from './router';
import {createStore} from './store';
import 'filters';
import 'directives';
import titleMixin from './mixins/title';
import pluginCore from './plugins/core';
import lazyload from 'vue-lazyload';
import reportError from 'report-error';
Vue.use(lazyload, {
preLoad: 2
});
Vue.use(pluginCore);
Vue.mixin(titleMixin);
export function createApp(context) {
const router = createRouter();
const store = createStore(context);
const app = new Vue({
router,
store,
errorCaptured(error) {
reportError(context, 'server')(error);
return false;
},
render: h => h(App)
});
return {app, router, store};
}
... ...
<template>
<div id="app">
<transition
:name="`route-view-${yoho.direction}`">
<router-view></router-view>
</transition>
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
name: 'App',
computed: {
...mapState(['yoho'])
}
};
</script>
<style lang="scss">
.route-view-forword-enter-active,
.route-view-forword-leave-active,
.route-view-back-enter-active,
.route-view-back-leave-active {
will-change: true;
width: 100%;
height: 100%;
position: absolute;
backface-visibility: hidden;
perspective: 1000;
}
.route-view-forword-leave-active,
.route-view-back-leave-active {
transition: all 200ms;
}
.route-view-forword-enter-active,
.route-view-back-enter-active {
transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
}
.route-view-forword-enter {
transform: translate3d(100%, 0, 0);
}
.route-view-forword-leave-active {
transform: translate3d(-30%, 0, 0);
}
.route-view-back-enter {
z-index: 1;
transform: translate3d(-30%, 0, 0);
}
.route-view-back-leave-active {
transform: translate3d(100%, 0, 0);
z-index: 2;
}
</style>
... ...
import axios from 'axios';
import config from 'config';
axios.defaults.baseURL = config.axiosBaseUrl;
axios.defaults.responseType = config.axiosResponseType;
axios.defaults.headers = {
'X-Requested-With': 'XMLHttpRequest'
};
const errHandle = (error) => {
console.log(error);
return Promise.reject({
code: 500,
message: '服务器开小差了~'
});
};
const request = (options) => {
return axios(options).then(res => res.data, errHandle);
};
export const createApi = () => {
return {
get(url, params, options) {
return request(Object.assign({
url,
params,
method: 'get',
}), options);
},
post(url, data, options) {
return request(Object.assign({
url,
data,
method: 'post',
}, options));
}
};
};
... ...
import checkParams from '../../utils/check-params';
import apiMaps from '../../config/api-map';
import createReport from 'report-error';
const yohoApi = global.yoho.API;
const ufoAPI = global.yoho.UfoAPI;
const serviceApi = global.yoho.ServiceAPI;
const checkApiMap = url => {
return apiMaps[url] ? apiMaps[url] : void 0;
};
const request = async({url, method, reqParams = {}, context}) => {
const apiInfo = checkApiMap(url);
const {env, user} = context;
if (!apiInfo) {
return Promise.reject(new Error(`未找到对应的接口:${url}`));
}
if (!apiInfo.service) {
Object.assign(reqParams, {
uid: (user && user.uid) ? {
toString: () => {
return user.uid;
},
sessionKey: user.sessionKey,
appSessionType: user.appSessionType
} : 1,
method: apiInfo.api,
});
}
const params = checkParams.getParams(reqParams, apiInfo);
const cache = method.toLowerCase() !== 'get' ? false : apiInfo.cache;
const headers = {
'X-YOHO-IP': env.clientIp,
'X-Forwarded-For': env.clientIp,
'User-Agent': 'yoho/nodejs'
};
try {
if (apiInfo.service) {
return await serviceApi.get(`${apiInfo.api}${apiInfo.path}`, params, {
cache: cache,
headers
});
} else if (apiInfo.ufo) {
return await ufoAPI[method](`${apiInfo.path || ''}`, params, {
cache: cache,
headers
});
} else {
return await yohoApi[method](`${apiInfo.path || ''}`, params, {
cache: cache,
headers
});
}
} catch (error) {
return Promise.reject({
code: error.code || 500,
message: error.message || '服务器错误'
});
}
};
const catchError = (context, reqParams) => {
return result => {
if (result && result.code === 500) {
createReport(context, 'api')(Object.assign({
api: reqParams.method,
}, result));
}
return result;
};
};
export const createApi = context => {
return {
get(url, reqParams) {
return request({url, method: 'get', reqParams, context}).then(catchError(context, reqParams));
},
post(url, reqParams) {
return request({url, method: 'post', reqParams, context}).then(catchError(context, reqParams));
}
};
};
... ...
import config from 'config';
const stringify = function(list) {
let data = [];
for (let i = 0; i < list.length; i++) {
let obj = list[i];
let params = [];
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
params.push(prop + '::' + obj[prop]);
}
}
data.push(params.join('$$'));
}
return data.join('**');
};
const report = function(data) {
const str = stringify([data]);
if (!str) {
return;
}
const imgElem = new Image();
imgElem.src = config.reportUrl + '?s=ufo-app-web&l=' + str + '&t=' + (new Date()).getTime();
};
export default context => {
return (err) => {
if (process.env.NODE_ENV === 'production') {
setTimeout(() => {
try {
report({
tp: 'err',
msg: err.message,
sc: 'cdn.yoho.cn',
ln: 0,
cn: 0,
pt: encodeURIComponent(location.href),
u: 0,
ud: 0,
rid: 0,
st: JSON.stringify(err && err.stack),
r: context.route
});
} catch (error) {
console.log(error);
}
}, 0);
} else {
console.log(err);
}
};
};
... ...
import {get} from 'lodash';
const sender = global.yoho.apmSender;
const logger = global.yoho.logger;
export default (context, type = 'server') => {
return (err, vm, info) => {
logger.error(err, vm, info);
if (process.env.NODE_ENV === 'production') {
setImmediate(() => {
const reportData = {
measurement: 'error-report',
tags: {
app: 'ufo-app-web', // 应用名称
hostname: context.hostname,
type: type,
route: context.route, // 请求路由
uid: get(context, 'user.uid', 0),
udid: context.udid,
api: err.api,
code: err.code || 500,
path: context.path,
url: encodeURIComponent(context.url),
ip: context.env.clientIp
},
fields: {
useragent: context.ua,
message: err.message,
stack: err.stack
}
};
try {
sender.addMessage(reportData);
} catch (error) {
logger.error(error);
}
});
}
};
};
... ...
export const getImgUrl = function(src, width = 300, height = 300, mode = 2) {
return src ? src.replace(/(\{width}|\{height}|\{mode})/g, function($0) {
const dict = {
'{width}': width,
'{height}': height,
'{mode}': mode || 2
};
return dict[$0];
}).replace(/https?:/, '') + '/interlace/1' : '';
};
export const replaceHttp = function(src) {
return src.replace(/https?:/, '');
};
export const debounce = (idle, action) => { // 函数去抖动,超过一定时间才会执行,如果周期内触发,充值计时器
let last;
return function() {
let args = arguments;
if (last) {
clearTimeout(last);
}
last = setTimeout(() => {
action.apply(this, args);
}, idle);
};
};
export const throttle = (delay, action) => { // 函数节流器,定义函数执行间隔,按频率触发函数
let last = 0;
return function() {
let args = arguments;
let curr = +new Date();
if (curr - last > delay) {
action.apply(this, args);
last = curr;
}
};
};
... ...
/**
* YOHO-SDK
*
* 与原生 APP 交互的代码
* 所有函数要做降级处理
* 假如不是 YOHO App,在浏览器实现对应的功能
* 浏览器不支持的功能,给出提示,控制台不能报错,不影响后续代码执行
*
* 希望能与 微信 JS-SDK 一样方便
*/
import cookie from 'yoho-cookie';
/* 空方法 */
const nullFun = () => {};
let isYohoBuy = /YohoBuy/i.test(navigator.userAgent || '');
let $appLink = document.querySelector('#yoho-app-link');
if (isYohoBuy && !$appLink) {
let body = document.querySelector('body');
$appLink = document.createElement('a');
$appLink.id = 'yoho-app-link';
$appLink.href = 'javascript:;';
$appLink.style.display = 'none';
$appLink.className = 'no-intercept';
body.appendChild($appLink);
$appLink.onclick = (evt) => {
evt.stopPropagation();
};
}
const yoho = {
/**
* 判断是否是 APP
*/
isApp: /YohoBuy/i.test(navigator.userAgent || '') || /YohoBuy/i.test(navigator.userAgent || ''),
isiOS: /\(i[^;]+;( U;)? CPU.+Mac OS X/i.test(navigator.userAgent || ''),
isAndroid: /Android/i.test(navigator.userAgent || ''),
isYohoBuy: isYohoBuy,
/**
* JS 与 APP 共享的对象
*/
data: window.yohoInterfaceData,
/**
* 判断是否是 登录
*/
isLogin() {
return cookie.get('_YOHOUID');
},
ready(callback) {
if (this.isApp || this.isYohoBuy) {
document.addEventListener('deviceready', callback);
} else {
return callback();
}
},
/**
* 跳转至指定index的tab(从0开始)
* @param args 传递给 APP 的参数 {"index":tab_index}
* @param success 调用成功的回调方法
* @param fail 调用失败的回调方法
*/
goTab(args, success, fail) {
if (this.isApp && window.yohoInterface) {
args.showScrollbar = 'no';
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.tab',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 跳转至登录页面
* @param args 传递给 APP 的参数 {""}
* @param success 调用成功的回调方法
* @param fail 调用失败的回调方法
*/
goLogin(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.login',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 获取app版本号
* @param args {""}
* @param success
* @param fail
*/
getAppVersion(args, success, fail) {
if (window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'get.appversion',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 退出登录,清除本地用户数据
* @param args {""}
* @param success
* @param fail
*/
goLogout(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.loginout',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 设置shoppingkey
* @param args 传递给 APP 的参数 {"shoppingkey":""}
* @param success 调用成功的回调方法
* @param fail 调用失败的回调方法
*/
goShopingKey(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.shoppingkey',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 跳转至购物车页面
* @param args 传递给 APP 的参数 {""}
* @param success 调用成功的回调方法
* @param fail 调用失败的回调方法
*/
goShopingCart(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.shopingCart',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 跳转地址页面 1:地址选择页面 2:地址管理页面
* @param args 传递给 APP 的参数 {"type":"1"}
* @param success 调用成功的回调方法
* @param fail 调用失败的回调方法
*/
goAddress(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.address',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 跳转至图片浏览页面;images:浏览图片的url index:点击的图片序号
* @param args 传递给 APP 的参数 {"images":[imgUrl1,imgUrl2...],"index":"1"}
* @param success 调用成功的回调方法
* @param fail 调用失败的回调方法
*/
goImageBrowser(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.imageBrowser',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 跳转至新页面(页面内容为html)
* @param args 传递给 APP 的参数 {"url":""}
* @param success 调用成功的回调方法
* @param fail 调用失败的回调方法
*/
goNewPage(args) {
if (this.isYohoBuy) {
let url = args.url;
if (url.indexOf('openby:') < 0) {
delete args.url;
if (args.header && args.header.headerid !== void 0) {
args.headerid = args.header.headerid;
delete args.header;
}
url += (url.indexOf('?') >= 0 ? '&' : '?') + 'openby:yohobuy=' + JSON.stringify({
action: 'go.h5',
params: {
islogin: 'N',
type: 0,
updateflag: Date.now() + '',
url: url,
param: args
}
});
}
if (window.parent && window.parent !== window) {
window.parent.blkDocument.goNewPage({url});
} else {
$appLink.href = url;
$appLink.click();
if (this.isiOS) {
$appLink.click();
}
}
} else {
if (args.url) {
window.open(args.url);
}
}
},
/**
* 跳转至支付页面
* @param args 传递给 APP 的参数 {"orderid":"098768"}
* @param success 调用成功的回调方法
* @param fail 调用失败的回调方法
*/
goPay(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.pay',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 返回上一级页面
* @param args {""}
* @param success
* @param fail
*/
goBack(args, success, fail) {
if ((this.isApp || this.isYohoBuy) && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.back',
arguments: args
});
} else {
history.go(-1);
}
},
/**
* 新的返回上一级页面
* @param args {""}
* @param success
* @param fail
*/
goNewBack(args, success, fail) {
if (this.isYohoBuy && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.newback',
arguments: args
});
} else {
history.go(-1);
}
},
/**
* 联系电话
* @param args {""}
* @param success
* @param fail
*/
goTel(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.tel',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 获取频道
* @param args {""}
* @param success
* @param fail
*/
getChannel(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'get.channel',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 设置频道
* @param args {""}
* @param success
* @param fail
*/
setChannel(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'set.channel',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 频道跳转
* @param args {""}
* @param success
* @param fail
*/
goChannel(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.channel',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 分享
* @param args {"title":"标题","des":"描述","img":"icon地址","url":"网页地址"}
* @param success
* @param fail
*/
goShare(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.share',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 跳转到搜索页面
* @param args {""}
* @param success
* @param fail
*/
goSearch(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.search',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 跳转到设置页面
* @param args {""}
* @param success
* @param fail
*/
goSetting(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.setting',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 跳转到设置头像
* @param args {""}
* @param success
* @param fail
*/
goSetAvatar(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.setAvatar',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 跳转到收藏管理
* @param args {""}
* @param success
* @param fail
*/
goPageView(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.pageView',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 更新头部信息
* @param args {""}
* @param success
* @param fail
*/
updateNavigationBar(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'update.navigationBarStyle',
arguments: args
});
} else {
// tip(tipInfo);
}
},
/**
* 显示 loading
* @param args Boolen
* @param success
* @param fail
*/
showLoading(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.loading',
arguments: {
show: args ? 'yes' : 'no'
}
});
} else {
// tip(tipInfo);
}
},
/**
* 显示返回滑块
* @param args Boolean
* @param success
* @param fail
*/
blkBackStatus(args, success, fail) {
if (this.isYohoBuy && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.blkBackStatus',
arguments: {
isHidden: args ? 'Y' : 'N'
}
});
} else {
// tip(tipInfo);
}
},
/**
* 原生调用 JS 方法
* @param name 方法名
* @param callback 回调
*/
addNativeMethod(name, callback) {
// 延迟 500ms 注入
setTimeout(function() {
if (window.yohoInterface) {
window.yohoInterface[name] = callback;
}
}, 500);
},
/**
* 跳转到 app 促销商品列表
* @param args
* @param success
* @param fail
*/
goCouponProductList(args, success, fail) {
if (this.isApp && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.couponProductList',
arguments: args
});
} else {
// tip(tipInfo);
}
},
goPage: function(action, params) {
var url = window.location.protocol + '//m.yohobuy.com/';
if (this.isYohoBuy && window.yohoInterface) {
url = url + '?openby:yohobuy=' + JSON.stringify({
action,
params
});
}
if ($appLink) {
$appLink.href = url;
$appLink.click();
}
},
finishPage(args, success, fail) {
if (this.isYohoBuy && window.yohoInterface) {
window.yohoInterface.triggerEvent(success || nullFun, fail || nullFun, {
method: 'go.finish',
arguments: args
});
} else {
// tip(tipInfo);
}
}
};
export default yoho;
... ...
<template>
<button class="btn" :class="classes" type="button" @touchstart="onTouchstart" @touchend.prevent.stop="onTouchend">
<span>
<slot></slot>
</span>
</button>
</template>
<script>
export default {
name: 'Button',
data() {
return {
classes: {}
};
},
methods: {
onTouchstart() {
this.classes = {
active: true
};
},
onTouchend() {
this.clearActive();
this.$emit('click');
},
clearActive() {
this.classes = {};
}
}
};
</script>
<style lang="scss">
.btn {
display: inline-block;
margin-bottom: 0;
font-weight: normal;
text-align: center;
vertical-align: middle;
touch-action: none;
background: none;
white-space: nowrap;
line-height: 1.5;
user-select: none;
font-size: 30px;
outline: 0;
border: none;
color: #000;
transition: opacity 0.1s linear, background-color 0.1s linear, border 0.1s linear, box-shadow 0.1s linear;
background-color: #eee;
height: 60px;
padding-left: 40px;
padding-right: 40px;
&:active,
&.active {
opacity: 0.5;
// color: #eee !important;
}
}
</style>
... ...
<template>
<img v-lazy="currentSrc" :alt="alt" v-if="!refresh">
</template>
<script>
export default {
name: 'ImgSize',
props: {
src: String,
width: Number,
height: Number,
alt: String,
},
data() {
return {
refresh: false
};
},
watch: {
src() {
this.refresh = true;
this.$nextTick(() => {
this.refresh = false;
});
}
},
computed: {
currentSrc() {
return (this.src || '')
.replace('{width}', this.width)
.replace('{height}', this.height);
}
}
};
</script>
<style>
</style>
... ...
const config = {
development: {
axiosBaseUrl: '',
axiosResponseType: 'json',
reportUrl: '//badjs.yoho.cn/apm/yas2.gif'
},
production: {
axiosBaseUrl: '',
axiosResponseType: 'json',
reportUrl: '//badjs.yoho.cn/apm/yas2.gif'
}
};
export default config[process.env.NODE_ENV];
... ...
export default {
inserted(el) {
el.focus();
}
};
... ...
import Vue from 'vue';
import focus from './focus';
import TransferDom from './transfer-dom';
import Tap from './tap';
Vue.directive('focus', focus);
Vue.directive('TransferDom', TransferDom);
Vue.directive('Tap', Tap);
... ...
const bingFn = (fn) => {
return function(evt) {
fn(evt);
evt.preventDefault();
evt.stopPropagation();
};
};
export default {
bind(el, binding) {
el.addEventListener('e-click', bingFn(binding.value));
},
unbind(el, binding) {
el.removeEventListener('e-click', bingFn(binding.value));
}
};
... ...
// Thanks to: https://github.com/airyland/vux/blob/v2/src/directives/transfer-dom/index.js
// Thanks to: https://github.com/calebroseland/vue-dom-portal
/**
* Get target DOM Node
* @param {(Node|string|Boolean)} [node=document.body] DOM Node, CSS selector, or Boolean
* @return {Node} The target that the el will be appended to
*/
function getTarget(node) {
if (node === void 0) {
node = document.body
}
if (node === true) { return document.body }
return node instanceof window.Node ? node : document.querySelector(node)
}
const directive = {
inserted(el, { value }, vnode) {
if (el.dataset && el.dataset.transfer !== 'true') return false;
el.className = el.className ? el.className + ' v-transfer-dom' : 'v-transfer-dom';
const parentNode = el.parentNode;
if (!parentNode) return;
const home = document.createComment('');
let hasMovedOut = false;
if (value !== false) {
parentNode.replaceChild(home, el); // moving out, el is no longer in the document
getTarget(value).appendChild(el); // moving into new place
hasMovedOut = true
}
if (!el.__transferDomData) {
el.__transferDomData = {
parentNode: parentNode,
home: home,
target: getTarget(value),
hasMovedOut: hasMovedOut
}
}
},
componentUpdated(el, { value }) {
if (el.dataset && el.dataset.transfer !== 'true') return false;
// need to make sure children are done updating (vs. `update`)
const ref$1 = el.__transferDomData;
if (!ref$1) return;
// homes.get(el)
const parentNode = ref$1.parentNode;
const home = ref$1.home;
const hasMovedOut = ref$1.hasMovedOut; // recall where home is
if (!hasMovedOut && value) {
// remove from document and leave placeholder
parentNode.replaceChild(home, el);
// append to target
getTarget(value).appendChild(el);
el.__transferDomData = Object.assign({}, el.__transferDomData, { hasMovedOut: true, target: getTarget(value) });
} else if (hasMovedOut && value === false) {
// previously moved, coming back home
parentNode.replaceChild(el, home);
el.__transferDomData = Object.assign({}, el.__transferDomData, { hasMovedOut: false, target: getTarget(value) });
} else if (value) {
// already moved, going somewhere else
getTarget(value).appendChild(el);
}
},
unbind(el) {
if (el.dataset && el.dataset.transfer !== 'true') return false;
el.className = el.className.replace('v-transfer-dom', '');
const ref$1 = el.__transferDomData;
if (!ref$1) return;
if (el.__transferDomData.hasMovedOut === true) {
el.__transferDomData.parentNode && el.__transferDomData.parentNode.appendChild(el)
}
el.__transferDomData = null
}
};
export default directive;
\ No newline at end of file
... ...
import Vue from 'vue';
import {
ROUTE_CHANGE,
} from 'store/yoho/types';
import {createApp} from './app';
import {createApi} from 'create-api';
import {Style, Toast, Dialog} from 'cube-ui'; //eslint-disable-line
import {get} from 'lodash';
import Lazy from 'vue-lazyload';
import yoho from 'common/yoho';
import 'statics/scss/common.scss';
import 'statics/font/iconfont.css';
import 'statics/font/ufofont.css';
const $app = document.getElementById('app');
const isDegrade = Boolean(!($app && $app.attributes['data-server-rendered']));
const context = get(window, '__INITIAL_STATE__.yoho.context');
const {app, router, store} = createApp(context);
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
window._router = get(store, 'state.yoho.context.route');
Vue.prop('yoho', yoho);
Vue.use(Toast);
Vue.use(Dialog);
Vue.prop('api', createApi());
Vue.use(Lazy, {error: ''});
const fetchAsycData = (matched, r) => {
const asyncDataPromises = matched
.map(({asyncData}) => asyncData && asyncData({store, router: r})).
filter(p => p);
return Promise.all(asyncDataPromises);
};
const trackPage = (path) => {
if (window._hmt) {
try {
window._hmt.push(['_trackPageview', path]);
} catch (error) {
console.error(error);
}
}
};
router.onReady(() => {
store.dispatch('reportYas', {
params: {
appop: 'YB_H5_PAGE_OPEN_L',
param: {
F_URL: `${location.origin}${router.currentRoute.fullPath}`,
PAGE_URL: '',
PAGE_NAME: router.currentRoute.name
}
}
});
if (isDegrade) {
fetchAsycData(router.getMatchedComponents(), router.currentRoute);
}
router.beforeResolve((to, from, next) => {
try {
trackPage(to.fullPath);
const matched = router.getMatchedComponents(to);
store.commit(ROUTE_CHANGE, {to, from});
store.dispatch('reportYas', {
params: {
appop: 'YB_H5_PAGE_OPEN_L',
param: {
F_URL: `${location.origin}${to.fullPath}`,
PAGE_URL: `${location.origin}${from.fullPath}`,
PAGE_NAME: from.name
}
}
});
fetchAsycData(matched, to)
.then(next)
.catch(e => {
store.dispatch('reportError', {error: e});
console.error(e);
return next();
});
} catch (e) {
store.dispatch('reportError', {error: e});
return next();
}
});
app.$mount(isDegrade ? '#degrade-app' : '#app');
});
router.onError(e => {
store.dispatch('reportError', {error: e});
router.push({name: 'error.500'});
});
... ...
import {createApp} from './app';
import createReport from 'report-error';
import {
SET_ENV,
} from 'store/yoho/types';
export default context => {
const reportError = createReport(context);
return new Promise((resolve, reject) => {
const {app, router, store} = createApp(context);
const {url} = context;
store.commit(SET_ENV, {context});
router.push(url);
router.onReady(() => {
const matched = router.getMatchedComponents();
if (matched.some(m => !m)) {
reportError(new Error('导航组件为空'));
router.push({name: 'error.500'});
return resolve(app);
}
if (!matched.length) {
return reject({code: 404, message: ''});
}
const asyncDataPromises = matched.map(({asyncData}) => {
try {
return asyncData && asyncData({store, router: router.currentRoute});
} catch (error) {
return Promise.reject(error);
}
}).filter(p => p);
Promise.all(asyncDataPromises)
.then(() => {
context.state = store.state;
return resolve(app);
}).catch(e => {
reportError(e);
return resolve(app);
});
});
router.onError(e => {
reportError(e);
router.push({name: 'error.500'});
return resolve(app);
});
});
};
... ...
export const genderFilter = (value) => {
switch (value) {
case 1:
return '男款';
case 2:
return '女款';
default:
return '通用';
}
};
... ...
import Vue from 'vue';
import {genderFilter} from './gender';
Vue.filter('gender', genderFilter);
... ...
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<meta name="keywords" content="">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta content="yes" name="apple-mobile-web-app-capable">
<meta content="telephone=no" name="format-detection">
<meta content="email=no" name="format-detection">
<script type="text/javascript">
(function(d,c){var e=d.documentElement,a="orientationchange" in window?"orientationchange":"resize",b=function(){var f=e.clientWidth;if(!f){return}if(f>=750){e.style.fontSize="40px"}else{e.style.fontSize=40*(f/750)+"px"}};if(!d.addEventListener){return}b();c.addEventListener(a,b,false);d.addEventListener("DOMContentLoaded",b,false)})(document,window);
</script>
</head>
<body>
<!--vue-ssr-outlet-->
<div id="degrade-app"></div>
<div id="main-wrap">
<div id="no-download"></div>
</div>
<script>
(function(w, d, s, j, f) {
var a = d.createElement(s);
var m = d.getElementsByTagName(s)[0];
w.YohoAcquisitionObject = f;
w[f] = function() {
w[f].p = arguments;
};
a.async = 1;
a.src = j;
m.parentNode.insertBefore(a, m);
}(window, document, 'script', (document.location.protocol === 'https:' ? 'https:' : 'http:') + '//cdn.yoho.cn/yas-jssdk/2.4.18/yas.js', '_yas'));
var _hmt = _hmt || [];
(function() {
function getUid() {
var uid,
name = 'app_uid',
cookies = (document.cookie && document.cookie.split(';')) || [];
for (var i = 0; i < cookies.length; i++) {
if (cookies[i].indexOf(name) > -1) {
uid = decodeURIComponent(cookies[i].replace(name + '=', '').trim());
break;
}
}
if (!uid) return 0;
uid = uid.split('::');
if (!uid || uid.length < 4) {
return 0;
}
return uid[1];
}
function queryString() {
var vars = {},
hash,
i;
var hashes = window.location.search.slice(1).split('&');
for (i = 0; i < hashes.length; i++) {
hash = hashes[i].split('=');
vars[hash[0]] = hash[1];
}
return vars;
}
var uid = getUid() || queryString().uid;
uid = uid === 0 ? '' : uid;
window._ozuid = uid; // 暴露ozuid
if (window._yas) {
window._yas(1 * new Date(), '2.4.16', 'yohoappweb', uid, '', '');
}
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?65dd99e0435a55177ffda862198ce841";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
}());
</script>
</body>
</html>
... ...
import {
SET_TITLE
} from 'store/yoho/types';
const getTitle = vue => {
const {title} = vue.$options;
if (title) {
return typeof title === 'function' ? title.call(vue) : title;
}
};
const serverTitleMixin = {
created() {
const title = getTitle(this);
if (title) {
this.$ssrContext.title = title;
this.$store.commit(SET_TITLE, {title});
}
}
};
const clientTitleMixin = {
mounted() {
const title = getTitle(this);
if (title) {
document.title = title;
}
}
};
export default process.env.VUE_ENV === 'server' ?
serverTitleMixin :
clientTitleMixin;
... ...
<template>
<div class="err-404">
404
</div>
</template>
<script>
export default {
name: 'ErrorNotFound'
}
</script>
<style>
</style>
... ...
<template>
<div class="err-500">
500
<NotFound></NotFound>
</div>
</template>
<script>
export default {
name: 'Error',
created() {
console.log('400 created')
},
mounted() {
console.log('400 mounted')
},
components: {NotFound: () => import('./404')}
}
</script>
<style>
</style>
... ...
export default [{
path: '/mapp/error/404',
name: 'error.404',
component: () => import(/* webpackChunkName: "error" */ './404')
}, {
path: '/mapp/error/500',
name: 'error.500',
component: () => import(/* webpackChunkName: "error" */ './500')
}];
... ...
import ErrorPages from './error';
export default [...ErrorPages];
... ...
import Single from './single';
import Common from './common';
export default [...Single, ...Common];
... ...
<template>
<div class="div">
<ul>
<li v-for="i in 10000" :key="i" @click="onClick(i, $event)">
{{i}}
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Channel',
methods: {
onClick(i, evt) {
evt.preventDefault();
console.log(i);
}
}
};
</script>
<style lang="scss" scoped>
body {
width: 100%;
}
.div {
width: 100%;
pointer-events: none;
li {
border: 1px solid #000;
height: 50px;
margin: 5px;
}
}
</style>
... ...
export default [{
path: '/channel',
name: 'channel',
component: () => import(/* webpackChunkName: "channel" */ './channel')
}];
... ...
import Channel from './channel';
export default [...Channel];
... ...
import Layout from './layout';
export default [...Layout];
... ...
<template>
<div class="header">
<div class="back" @touchend="onBack"></div>
</div>
</template>
<script>
export default {
name: 'HeaderUfo',
methods: {
onBack() {
this.$yoho.finishPage({});
}
}
};
</script>
<style lang="scss" scoped>
.header {
width: 100%;
height: 45PX;
display: flex;
padding-left: 20PX;
padding-right: 20PX;
align-items: center;
background-color: #fff;
.back {
width: 24PX;
height: 24PX;
background: url(~statics/image/order/back@3x.png) no-repeat;
background-size: cover;
}
}
</style>
... ...
import LayoutApp from './layout-app';
export default [LayoutApp];
... ...
<template>
<div class="layout">
<LayoutHeader class="layout-header"></LayoutHeader>
<div class="layout-context">
<slot></slot>
</div>
</div>
</template>
<script>
import LayoutHeader from './header-ufo';
export default {
name: 'LayoutApp',
components: {
LayoutHeader
}
};
</script>
<style lang="scss">
.layout {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
font-size: 24px;
display: flex;
flex-direction: column;
.layout-context {
flex: 1;
overflow: hidden;
position: relative;
}
}
</style>
... ...
<template>
<div ref="wrapper" class="list-wrapper">
<div class="scroll-content">
<div ref="listWrapper">
<slot></slot>
</div>
<slot name="pullup"
:pullUpLoad="pullUpLoad"
:isPullUpLoad="isPullUpLoad"
>
<div class="pullup-wrapper" v-if="pullUpLoad">
<div class="before-trigger" v-if="!isPullUpLoad">
<span>{{pullUpTxt}}</span>
</div>
<div class="after-trigger" v-else>
<span>加载中</span>
</div>
</div>
</slot>
</div>
<slot name="pulldown"
:pullDownRefresh="pullDownRefresh"
:pullDownStyle="pullDownStyle"
:beforePullDown="beforePullDown"
:bubbleY="bubbleY"
>
<div ref="pulldown" class="pulldown-wrapper" :style="pullDownStyle" v-if="pullDownRefresh">
<div class="before-trigger" v-show="beforePullDown">
<div id="beforePullDown"></div>
</div>
<div class="after-trigger" v-show="!beforePullDown">
<div class="loading">
<div id="afterPullDown"></div>
</div>
</div>
</div>
</slot>
</div>
</template>
<script>
import {BetterScroll} from 'cube-ui';
function getRect(el) {
if (el instanceof window.SVGElement) {
let rect = el.getBoundingClientRect();
return {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height
};
} else {
return {
top: el.offsetTop,
left: el.offsetLeft,
width: el.offsetWidth,
height: el.offsetHeight
};
}
}
const DIRECTION_H = 'horizontal';
const DIRECTION_V = 'vertical';
export default {
name: 'ScrollUfo',
props: {
data: {
type: Array,
default: function() {
return [];
}
},
probeType: {
type: Number,
default: 1
},
click: {
type: Boolean,
default: true
},
listenScroll: {
type: Boolean,
default: false
},
listenBeforeScroll: {
type: Boolean,
default: false
},
listenScrollEnd: {
type: Boolean,
default: false
},
direction: {
type: String,
default: DIRECTION_V
},
scrollbar: {
type: null,
default: false
},
pullDownRefresh: {
type: null,
default() {
return {
threshold: 70,
stop: 90
};
}
},
pullUpLoad: {
type: null,
default: false
},
startY: {
type: Number,
default: 0
},
refreshDelay: {
type: Number,
default: 20
},
freeScroll: {
type: Boolean,
default: false
},
mouseWheel: {
type: Boolean,
default: false
},
bounce: {
default: true
},
zoom: {
default: false
},
observeDom: {
type: Boolean,
default: true
}
},
data() {
return {
beforePullDown: true,
isRebounding: false,
isPullUpLoad: false,
isPullingDown: false,
pullUpDirty: true,
pullDownStyle: '',
bubbleY: 0
};
},
computed: {
pullUpTxt() {
const moreTxt = (this.pullUpLoad && this.pullUpLoad.txt && this.pullUpLoad.txt.more) || '下拉加载';
const noMoreTxt = (this.pullUpLoad && this.pullUpLoad.txt && this.pullUpLoad.txt.noMore) || '没有更多了';
return this.pullUpDirty ? moreTxt : noMoreTxt;
},
refreshTxt() {
return (this.pullDownRefresh && this.pullDownRefresh.txt) || '加载完成';
}
},
created() {
this.pullDownInitTop = -90;
},
mounted() {
setTimeout(() => {
this.initScroll();
}, 20);
},
methods: {
initScroll() {
if (!this.$refs.wrapper) {
return;
}
if (this.$refs.listWrapper && (this.pullDownRefresh || this.pullUpLoad)) {
this.$refs.listWrapper.style.minHeight = `${getRect(this.$refs.wrapper).height + 1}px`;
}
let options = {
probeType: this.probeType,
click: this.click,
scrollY: this.freeScroll || this.direction === DIRECTION_V,
scrollX: this.freeScroll || this.direction === DIRECTION_H,
scrollbar: this.scrollbar,
pullDownRefresh: this.pullDownRefresh,
pullUpLoad: this.pullUpLoad,
startY: this.startY,
freeScroll: this.freeScroll,
mouseWheel: this.mouseWheel,
bounce: this.bounce,
zoom: this.zoom,
observeDOM: this.observeDom,
};
this.scroll = new BetterScroll(this.$refs.wrapper, options);
if (this.listenScrollEnd) {
this.scroll.on('scrollEnd', (pos) => {
this.$emit('scroll-end', pos);
});
}
if (this.listenBeforeScroll) {
this.scroll.on('beforeScrollStart', () => {
this.$emit('beforeScrollStart');
});
this.scroll.on('scrollStart', () => {
this.$emit('scroll-start');
});
}
if (this.pullDownRefresh) {
this._initPullDownRefresh();
}
if (this.pullUpLoad) {
this._initPullUpLoad();
}
this._initLottie();
},
disable() {
this.scroll && this.scroll.disable();
},
enable() {
this.scroll && this.scroll.enable();
},
refresh() {
this.scroll && this.scroll.refresh();
},
scrollTo() {
this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments);
},
scrollToElement() {
this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments);
},
destroy() {
this.scroll.destroy();
},
forceUpdate(dirty) {
if (this.pullDownRefresh && this.isPullingDown) {
this.isPullingDown = false;
this._reboundPullDown().then(() => {
this._afterPullDown();
});
} else if (this.pullUpLoad && this.isPullUpLoad) {
this.isPullUpLoad = false;
this.scroll.finishPullUp();
this.pullUpDirty = dirty;
this.refresh();
} else {
this.refresh();
}
},
_initLottie() {
setTimeout(() => {
import(/* webpackChunkName: "lottie-web" */ 'lottie-web').then(lottie => {
this.lottieBefore = lottie.loadAnimation({
container: document.getElementById('beforePullDown'), // the dom element that will contain the animation
renderer: 'svg',
loop: false,
autoplay: true,
path: 'https://cdn.yoho.cn/mapp/lottie/ufo-pull-1227.json' // the path to the animation json
});
this.lottieAfter = lottie.loadAnimation({
container: document.getElementById('afterPullDown'), // the dom element that will contain the animation
renderer: 'svg',
loop: true,
autoplay: true,
path: 'https://cdn.yoho.cn/mapp/lottie/ufo-refresh-1227.json' // the path to the animation json
});
this.$nextTick(() => {
this.pullDownInitTop = 0 - this.$refs.pulldown.clientHeight;
});
});
}, 200);
},
_initPullDownRefresh() {
this.scroll.on('pullingDown', () => {
this.beforePullDown = false;
this.isPullingDown = true;
this.$emit('pullingDown');
});
this.scroll.on('scroll', (pos) => {
if (!this.pullDownRefresh) {
return;
}
if (this.beforePullDown) {
if (pos.y > 43) {
this.lottieBefore.goToAndStop(Math.abs(pos.y - 43) * 15);
} else {
this.lottieBefore.goToAndStop(0);
}
}
if (pos.y >= this.pullDownInitTop) {
this.pullDownStyle = `top:${Math.min(pos.y + this.pullDownInitTop, 0)}px`;
if (this.isRebounding) {
this.pullDownStyle = `top:${0 - (this.pullDownRefresh.stop || 40 - pos.y)}px`;
}
}
});
},
_initPullUpLoad() {
this.scroll.on('pullingUp', () => {
this.isPullUpLoad = true;
this.$emit('pullingUp');
});
},
_reboundPullDown() {
const {stopTime = 600} = this.pullDownRefresh;
return new Promise((resolve) => {
setTimeout(() => {
this.isRebounding = true;
this.scroll.finishPullDown();
resolve();
}, stopTime);
});
},
_afterPullDown() {
setTimeout(() => {
this.pullDownStyle = `top:${this.pullDownInitTop}px`;
this.beforePullDown = true;
this.isRebounding = false;
this.refresh();
}, this.scroll.options.bounceTime);
}
},
watch: {
data() {
setTimeout(() => {
this.refresh();
}, this.refreshDelay);
}
}
};
</script>
<style lang="scss" scoped>
.list-wrapper {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
overflow: hidden;
background: #fff;
.scroll-content {
position: relative;
z-index: 1;
}
.list-content {
position: relative;
z-index: 10;
background: #fff;
.list-item {
height: 60px;
line-height: 60px;
font-size: 18px;
padding-left: 20px;
border-bottom: 1px solid #e5e5e5;
}
}
.pulldown-wrapper {
position: absolute;
left: 60px;
right: 60px;
display: flex;
justify-content: center;
align-items: center;
transition: all;
width: 630px;
height: 168px;
}
.pullup-wrapper {
width: 100%;
justify-content: center;
align-items: center;
display: flex;
padding: 16px 0;
}
}
</style>
... ...
# 有货App优惠券列表
包括:
* 有货优惠券列表
* UFO优惠券列表
\ No newline at end of file
... ...
<template>
<div class="coupon-section">
<div :class="classes">
<div :class="contentClass">
<p class="value"><span>{{item.coupon_value_str}}</span></p>
<p class="threshold" v-if="item.use_rule">{{item.use_rule}}</p>
</div>
<div class="coupon-right">
<p class="title">
<span :class="typeClass"> [{{item.catalog_name}}] </span>
{{item.coupon_name}}
</p>
<p class="time">{{item.coupon_validity}}</p>
<div class="use-intro" v-if="item.notes" @click="onClickTip">
<span class="show-intro-btn">使用说明</span>
<span :class="introClass"></span>
</div>
<span class="tip"></span>
</div>
<template v-if="item.usedOvertimeOrInValid">
<div class="stamp overtime-stamp" v-if="item.is_overtime === 'Y'"></div>
<div class="stamp invalid-stamp" v-if="item.is_invalid === 'Y'"></div>
<div class="stamp used-stamp" v-if="item.is_used"></div>
</template>
<template v-else>
<div v-if="item.useNowLink" class="use-now" @click="onUseClick">立即使用</div>
<span class="top-tip" v-if="item.is_overdue_soon === 'Y'"></span>
</template>
</div>
<ul class="coupon-intro" v-if="show">
<li v-for="(it, index) in item.notes" :key="index">{{it}}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'CouponItem',
props: {
item: {
type: Object,
default() {
return {};
}
}
},
mounted() {
},
data() {
return {
show: false
};
},
methods: {
onClickTip() {
this.show = !this.show;
},
onUseClick() {
this.$yoho.goPage('go.couponProductList', {
coupon_code: this.item.coupon_code,
coupon_id: this.item.coupon_id,
title: '优惠活动商品',
coupon_title: `以下商品可使用【${this.item.coupon_name}】优惠券`
});
}
},
computed: {
classes() {
return ['coupon', {
'coupon-overtime': this.item.is_overtime === 'Y',
'coupon-invalid': this.item.is_invalid === 'Y',
'coupon-used': this.item.is_used
}];
},
contentClass() {
if (this.item.usedOvertimeOrInValid) {
return ['coupon-left'];
} else {
return ['coupon-left', {
'coupon-left-shop': this.item.catalog === 100,
'coupon-left-activity': this.item.catalog === 200,
'coupon-left-freight': this.item.catalog === 300,
'coupon-left-ufo': this.item.catalog === 'UFO',
}];
}
},
typeClass() {
return [{
'type-shop': this.item.catalog === 100,
'type-activity': this.item.catalog === 200,
'type-freight': this.item.catalog === 300,
'type-ufo': this.item.catalog === 'UFO',
}];
},
introClass() {
if (this.show) {
return ['iconfont', 'icon-up'];
} else {
return ['iconfont', 'icon-downn'];
}
}
},
watch: {},
};
</script>
<style lang="scss" scoped>
.coupon-section {
margin-top: 20px;
margin-bottom: 20px;
}
.coupon {
position: relative;
width: 710px;
height: 200px;
margin: 0 auto;
.coupon-left {
width: 200px;
height: 200px;
background-image: url("~statics/image/coupon/overtime.png");
background-size: 100% 100%;
float: left;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.coupon-left > p {
color: #fc5960;
font-size: 24px;
> span {
font-size: 60px;
font-weight: 600;
}
}
.coupon-left-activity {
background-image: url("~statics/image/coupon/activity.png");
}
.coupon-left-freight {
background-image: url("~statics/image/coupon/freight.png");
> p {
color: #000;
}
> p > span {
font-size: 48px;
font-weight: 600;
}
}
.coupon-left-shop {
background-image: url("~statics/image/coupon/shop.png");
> p {
color: #ffa72e;
}
}
.coupon-left-ufo {
background-image: url("~statics/image/coupon/ufo.png");
> p {
color: #002b47;
}
}
.coupon-right {
width: 510px;
height: 200px;
background-color: #fff;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
float: left;
padding: 20px;
font-size: 22px;
color: #b0b0b0;
position: relative;
white-space: initial;
.type-shop,
.type-activity,
.type-freight {
font-weight: 500;
}
.type-shop {
color: #efaf46;
}
.type-activity {
color: #fc5960;
}
.type-freight {
color: #222;
}
.type-ufo {
color: #002b47;
}
.title {
width: 370px;
font-size: 24px;
color: #444;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.time {
position: absolute;
font-size: 22px;
top: 50%;
transform: translateY(-40%);
}
.use-intro {
font-size: 22px;
position: absolute;
bottom: 20px;
}
.tip {
position: absolute;
top: 0;
right: 0;
}
}
.use-now {
font-size: 20px;
position: absolute;
width: 130px;
height: 50px;
line-height: 50px;
text-align: center;
border: 1px solid #444;
border-radius: 25px;
background-color: #fff;
color: #444;
bottom: 20px;
right: 20px;
}
.stamp {
position: absolute;
width: 126px;
height: 114px;
right: 20px;
top: 50%;
transform: translateY(-50%);
background-size: 100% 100%;
}
.overtime-stamp {
background-image: url("~statics/image/coupon/timeout.png");
}
.used-stamp {
background-image: url("~statics/image/coupon/used.png");
}
.invalid-stamp {
background-image: url("~statics/image/coupon/invalid.png");
}
.top-tip {
width: 84px;
height: 84px;
background-image: url("~statics/image/coupon/top-tip.png");
background-size: 100% 100%;
position: absolute;
top: 0;
right: 0;
}
}
.freight-coupon {
> .coupon-left {
font-size: 48px;
}
}
.coupon-invalid,
.coupon-used,
.coupon-overtime {
> .coupon-left > p {
color: #b0b0b0;
}
> .coupon-right > .title {
color: #b0b0b0;
& > .type-activity {
color: #b0b0b0;
}
& > .type-freight {
color: #b0b0b0;
}
& > .type-shop {
color: #b0b0b0;
}
}
}
.coupon-intro {
background: rgba(255, 255, 255, 0.7);
margin: -12px auto 0;
padding: 36px 22px 22px;
width: 710px;
li {
font-size: 22px;
color: #444;
}
}
.no-conpon-now {
text-align: center;
padding-top: 262px;
color: #b0b0b0;
.icon-not {
width: 208px;
height: 130px;
background-image: url("~statics/image/coupon/not.png");
background-size: 100% 100%;
margin: auto auto 30px;
}
p {
font-size: 28px;
}
}
.used-tip {
display: flex;
span {
color: #b0b0b0;
font-size: 24px;
}
hr {
width: 160px;
border: none;
border-bottom: 1px solid #e0e0e0;
}
}
.hide {
display: none;
}
</style>
... ...
import CouponItem from './coupon-item';
export default CouponItem;
... ...
<template>
<div class="no-conpon-now">
<div class="icon-not"></div>
<p>暂无优惠券</p>
</div>
</template>
<script>
export default {
name: 'EmptyComp'
};
</script>
<style lang="scss" scoped>
.no-conpon-now {
text-align: center;
padding-top: 262px;
color: #b0b0b0;
.icon-not {
width: 208px;
height: 130px;
background-image: url("~statics/image/coupon/not.png");
background-size: 100% 100%;
margin: auto auto 30px;
}
p {
font-size: 28px;
}
}
</style>
... ...
import Empty from './empty';
export default Empty;
... ...
<template>
<div class="exchange-box">
<div class="input-wrapper">
<input type="text" name="couponCodeInput" placeholder="请输入优惠券码" v-model="code">
</div>
<button :class="btnCls" @click="onClick">兑换</button>
</div>
</template>
<script>
export default {
name: 'ExchangeBox',
data() {
return {
code: ''
};
},
methods: {
onClick() {
if (this.code) {
this.$emit('click');
}
}
},
computed: {
btnCls() {
return ['exchange-coupon-btn', {
active: this.code !== ''
}];
}
},
watch: {
code() {
this.$emit('input', this.code);
},
value(val) {
this.code = val;
}
}
};
</script>
<style lang="scss" scoped>
.exchange-box {
height: 90px;
padding: 16px 20px;
background-color: #fff;
top: 0;
left: 0;
z-index: 2;
.input-wrapper {
display: inline-block;
position: relative;
}
input {
width: 570px;
height: 60px;
margin-right: 12px;
padding: 0 20px;
background-color: #f0f0f0;
border-radius: 4px;
border: none;
}
.exchange-coupon-btn {
width: 120px;
height: 60px;
border-radius: 4px;
font-size: 28px;
color: #fff;
background-color: #b0b0b0;
border: none;
}
.active {
background-color: #444;
}
}
</style>
... ...
import ExchangeBox from './exchage-box';
export default ExchangeBox;
... ...
<template>
<div class="filter-item">
<template v-for="(item, index) in list">
<button :class="btnCls(item)" @click="onClick(item)" :key="item.filter_id">{{item.filter_name}}</button>
</template>
</div>
</template>
<script>
export default {
name: 'FilterBar',
props: {
list: {
type: Array,
default() {
return [];
}
},
value: {
type: Number,
default() {
return 0;
}
}
},
data() {
return {
clickFilterId: this.value
};
},
methods: {
onClick(item) {
this.clickFilterId = item.filter_id;
this.$emit('input', item.filter_id);
},
btnCls(item) {
if (item.filter_id === this.clickFilterId) {
return ['active'];
}
return [];
}
},
}
</script>
<style lang="scss" scoped>
.filter-item {
display: flex;
width: 100%;
height: 130px;
justify-content: space-around;
align-items: center;
background-color: #fff;
position: fixed;
left: 0;
z-index: 3;
button {
width: 150px;
height: 70px;
background-color: #fff;
color: #444;
font-size: 28px;
border: 1px solid #e0e0e0;
border-radius: 4px;
&.active {
background-color: #444;
color: #fff;
border: 1px solid transparent;
}
}
}
</style>
... ...
import FilterBar from './filter-bar';
export default FilterBar;
... ...
<template>
<LayoutApp>
<div :class="classes">
<div :class="[prefixCls + '-bar']">
<span class="back" @click="onBackClick">
<i class="iconfont fontcls">&#xe763;</i>
</span>
<span class="help" @click="onHelpClick">
<i class="iconfont fontcls">&#xe630;</i>
</span>
<div>
<router-link class="yoho-tab" :to="{name: 'couponYoho'}" active-class="yoho-tab-active">有货优惠券</router-link><router-link class="yoho-tab" :to="{name: 'couponUfo'}" active-class="yoho-tab-active" >UFO优惠券</router-link>
</div>
</div>
<router-view></router-view>
</div>
</LayoutApp>
</template>
<script>
export default {
name: 'Headers',
props: {
type: {
type: [String, Number]
},
navList: []
},
data() {
return {
prefixCls: 'yoho-tabs',
activeKey: this.type
};
},
computed: {
classes() {
return [`${this.prefixCls}`];
},
},
methods: {
updateNav() {
this.navList.forEach((pane, index) => {
this.navList.push({
label: pane.label,
name: pane.currentName || index,
disabled: pane.disabled
});
if (!pane.currentName) {
pane.currentName = index;
}
if (index === 0) {
if (!this.activeKey) {
this.activeKey = pane.currentName || index;
}
}
});
this.updateStatus();
},
updateStatus() {
const tabs = this.getTabs();
tabs.forEach(tab => (tab.show = tab.currentName === this.activeKey));
},
handleChange(index) {
const nav = this.navList[index];
this.activeKey = nav.name;
this.$emit('input', nav.name);
},
tabCls(item) {
return [
'yoho-tab',
{
['yoho-tab-active']: item.name === this.activeKey
}
];
},
onBackClick() {
this.$yoho.finishPage({});
},
onHelpClick() {
this.$yoho.goNewPage({
url: window.location.protocol + '//m.yohobuy.com/service/qaDetail?keyword=%E4%BC%98%E6%83%A0%E5%88%B8&sonId=181'
});
}
},
watch: {
activeKey() {
this.updateStatus();
},
value(val) {
this.activeKey = val;
}
}
};
</script>
<style lang="scss" scoped>
$yoho-tab: yoho-tab;
.#{$yoho-tab}s {
width: 100%;
height: 100%;
}
.#{$yoho-tab} {
display: inline-block;
width: 200px;
height: 56px;
font-size: 24px;
color: white;
border: 1px solid white;
box-sizing: border-box;
background: #3a3a3a;
text-align: center;
line-height: 56px;
}
.#{$yoho-tab} + .#{$yoho-tab} {
border-left: none;
}
.#{$yoho-tab}s-bar {
width: 100%;
height: 90px;
display: flex;
background: #3a3a3a;
justify-content: center;
align-items: center;
z-index: 4;
}
.#{$yoho-tab}-active {
color: #444;
background-color: white;
}
.#{$yoho-tab}s-pane {
width: 100%;
height: 100%;
}
.fontcls {
color: white;
font-size: 45px;
}
.back {
display: inline-block;
width: 90px;
height: 90px;
line-height: 90px;
position: absolute;
left: 0;
text-align: center;
}
.help {
display: inline-block;
width: 90px;
height: 90px;
line-height: 90px;
position: absolute;
right: 0;
text-align: center;
}
</style>
... ...
import Header from './header';
export default Header;
... ...
import Layout from './layout';
export default Layout;
... ...
<template>
<div class="layout">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Layout',
};
</script>
<style lang="scss">
.layout {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
font-size: 24px;
}
</style>
... ...
export default {
name: 'RenderCell',
functional: true,
props: {
render: Function
},
render: (h, ctx) => {
return ctx.props.render(h);
}
};
... ...
import ScrollView from './scroll-view';
export default ScrollView;
... ...
<template>
<div class="list-wrapper">
<Scroll ref="scroll" :options="options" @pulling-up="reachBottom" @pulling-down="pullRefresh" :data="data">
<slot></slot>
</Scroll>
</div>
</template>
<script>
import {Scroll} from 'cube-ui';
export default {
name: 'yoho-scroll-view',
props: {
options: {
type: Object,
default() {
return {};
}
},
pullRefresh: {
type: Function,
default() {
return {};
}
},
reachBottom: {
type: Function,
default() {
return {};
}
},
data: {
type: Array,
default() {
return [];
}
}
},
methods: {
forceUpdate() {
this.$refs.scroll.forceUpdate();
},
scrollTo(x, y) {
this.$refs.scroll.scrollTo(x, y, 1000);
}
},
components: {
Scroll
}
};
</script>
<style lang="scss" scoped>
.list-wrapper {
position: relative;
height: 100%;
overflow: hidden;
background-color: #f0f0f0;
}
</style>
... ...
<template>
<div>
<div :class="classes">
<div class="yoho-coupon-filter-bar">
<div
:class="tabCls(item)"
v-for="(item, index) in navList"
@click="handleChange(index)"
:key="index"
>{{item.label}}
<span v-if="getNum[index]">({{getNum[index]}})</span>
<span :class="filterClass" v-if="item.filter"></span>
</div>
</div>
</div>
<div class="filter-wrapper" v-if="showFilter">
<div class="filter-mask" @click="onMaskClick"></div>
<FilterBar :list="filterList" v-model="selectFilterId" class="filter"></FilterBar>
</div>
</div>
</template>
<script>
import {createNamespacedHelpers} from 'vuex';
import FilterBar from '../filter-bar';
const {mapGetters, mapState} = createNamespacedHelpers('coupon/yoho');
export default {
name: 'ClassicTabs',
props: {
value: {
type: [String, Number]
},
data: {
type: Array,
default() {
return [];
}
},
filterId: {
type: [String, Number],
default() {
return 0;
}
}
},
data() {
return {
prefixCls: 'yoho-tabs',
navList: this.data,
activeKey: this.value,
showFilter: false,
selectFilterId: this.filterId,
};
},
computed: {
classes() {
return [`${this.prefixCls}`];
},
...mapState(['filterList']),
filterClass() {
if (this.showFilter) {
return ['iconfont', 'icon-up'];
} else {
return ['iconfont', 'icon-downn'];
}
},
...mapGetters(['getNum'])
},
methods: {
handleChange(index) {
const nav = this.navList[index];
// 第二次点击
if (this.activeKey === nav.label && index === 0) {
this.showFilter = !this.showFilter;
this.$emit('on-filter-change', this.showFilter);
return;
}
this.activeKey = nav.label;
this.$emit('input', nav.label);
},
tabCls(item) {
return [
'filter-btn-box',
{
active: item.label === this.activeKey
}
];
},
onMaskClick() {
this.showFilter = false;
},
},
watch: {
value(val) {
this.activeKey = val;
},
selectFilterId(newVal) {
this.$emit('update:filterId', newVal);
this.onMaskClick();
}
},
components: {
FilterBar
}
};
</script>
<style lang="scss" scoped>
$yoho-tab: yoho-tab;
.#{$yoho-tab}s {
width: 100%;
position: relative;
}
.yoho-coupon-filter-bar {
width: 100%;
height: 90px;
display: flex;
padding: 14px 0;
background-color: #fff;
left: 0;
z-index: 3;
border-bottom: 1px solid #e0e0e0;
.filter-btn-box {
flex: 1;
line-height: 60px;
text-align: center;
font-size: 28px;
color: #b0b0b0;
border-right: 1px solid #e0e0e0;
}
.filter-btn-box:last-child {
border-right: none;
}
.show-filter-btn {
color: #b0b0b0;
}
.active {
color: #444;
}
}
.filter-wrapper {
width: 100%;
height: 100%;
position: absolute;
top: 180px;
z-index: 4;
}
.filter {
position: absolute;
top: 0;
}
.filter-mask {
position: absolute;
width: 100%;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}
</style>
... ...
import Tabs from './tabs';
import Pane from './pane';
import ClassicTabs from './classic-tabs';
Tabs.Pane = Pane;
Tabs.ClassicTabs = ClassicTabs;
export default Tabs;
... ...
<template>
<div :class="prefixCls" v-if="show">
<slot></slot>
</div>
</template>
<script>
const prefixCls = 'yoho-tabs-tabpane';
export default {
name: 'TabPane',
props: {
label: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
prefixCls: prefixCls,
show: this.disabled
};
},
methods: {
updateNav() {
this.$parent.updateNav();
}
},
created() {
this.updateNav();
}
};
</script>
<style lang="scss" scoped>
.yoho-tabs-tabpane {
background: #f0f0f0;
width: 100%;
height: 100%;
}
</style>
... ...
<template>
<div :class="classes">
<slot name="tabs" v-bind:navList="navList">
<div :class="[prefixCls + '-bar']">
<span class="back" @click="onBackClick">
<i class="iconfont fontcls">&#xe763;</i>
</span>
<span class="help" @click="onHelpClick">
<i class="iconfont fontcls">&#xe630;</i>
</span>
<div>
<div
:class="tabCls(item)"
v-for="(item, index) in navList"
@click="handleChange(index)"
:key="index"
>{{item.label}}</div>
</div>
</div>
</slot>
<div :class="contentClasses" :style="contentStyle" ref="panes">
<slot name="panes"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'Tabs',
props: {
value: {
type: [String, Number]
}
},
data() {
return {
prefixCls: 'yoho-tabs',
navList: [],
activeKey: this.value
};
},
computed: {
classes() {
return [`${this.prefixCls}`];
},
contentClasses() {
return ['yoho-tabs-pane'];
},
contentStyle() {
return [];
}
},
methods: {
updateNav() {
this.navList = [];
this.getTabs().forEach((pane, index) => {
this.navList.push({
label: pane.label,
name: pane.currentName || index,
disabled: pane.disabled
});
if (!pane.currentName) {
pane.currentName = index;
}
if (index === 0) {
if (!this.activeKey) {
this.activeKey = pane.currentName || index;
}
}
});
this.updateStatus();
},
getTabs() {
return this.$children.filter(item => item.$options.name === 'TabPane');
},
updateStatus() {
const tabs = this.getTabs();
tabs.forEach(tab => (tab.show = tab.currentName === this.activeKey));
},
handleChange(index) {
const nav = this.navList[index];
this.activeKey = nav.name;
this.$emit('input', nav.name);
},
tabCls(item) {
return [
'yoho-tab',
{
['yoho-tab-active']: item.name === this.activeKey
}
];
},
onBackClick() {
this.$yoho.finishPage({});
},
onHelpClick() {
this.$yoho.goNewPage({
url: window.location.protocol + '//m.yohobuy.com/service/qaDetail?keyword=%E4%BC%98%E6%83%A0%E5%88%B8&sonId=181'
});
}
},
watch: {
activeKey() {
this.updateStatus();
},
value(val) {
this.activeKey = val;
}
},
created() {
this.updateNav();
}
};
</script>
<style lang="scss" scoped>
$yoho-tab: yoho-tab;
.#{$yoho-tab}s {
width: 100%;
height: 100%;
}
.#{$yoho-tab} {
display: inline-block;
width: 200px;
height: 56px;
font-size: 24px;
color: white;
border: 1px solid white;
box-sizing: border-box;
background: #3a3a3a;
text-align: center;
line-height: 56px;
}
.#{$yoho-tab} + .#{$yoho-tab} {
border-left: none;
}
.#{$yoho-tab}s-bar {
width: 100%;
height: 90px;
display: flex;
background: #3a3a3a;
justify-content: center;
align-items: center;
z-index: 4;
}
.#{$yoho-tab}-active {
color: #444;
background-color: white;
}
.#{$yoho-tab}s-pane {
width: 100%;
height: 100%;
}
.fontcls {
color: white;
font-size: 45px;
}
.back {
display: inline-block;
width: 90px;
height: 90px;
line-height: 90px;
position: absolute;
left: 0;
text-align: center;
}
.help {
display: inline-block;
width: 90px;
height: 90px;
line-height: 90px;
position: absolute;
right: 0;
text-align: center;
}
</style>
... ...
import YohoPage from './list';
export default [{
name: 'coupon',
path: '/mapp/coupon/yoho.html',
component: YohoPage,
}];
... ...
<template>
<LayoutApp>
<Tabs>
<template slot="panes">
<TabPane label="有货优惠券" :disable="true">
<YohoPage></YohoPage>
</TabPane>
<TabPane label="UFO优惠券" :disable="true">
<UfoPage></UfoPage>
</TabPane>
</template>
</Tabs>
</LayoutApp>
</template>
<script>
import Tabs from './components/tabs';
import Layout from './components/layout';
import YohoPage from './yoho';
import UfoPage from './ufo';
const TYPE = {notuse: 'notuse', use: 'use', overtime: 'overtime'};
export default {
name: 'ListPage',
asyncData({store}) {
return Promise.all([
store.dispatch('coupon/yoho/fetchYohoList', {
type: TYPE.notuse,
refresh: true
})
]);
},
components: {
Tabs,
TabPane: Tabs.Pane,
YohoPage,
UfoPage,
LayoutApp: Layout
}
};
</script>
<style lang="scss" scoped>
</style>
... ...
// export default {
// path: 'ufo.html',
// name: 'couponUfo',
// component: () => import(/* webpackChunkName: "coupon" */ './list')
// };
import UfoList from './list';
export default UfoList;
... ...
<template>
<div style="height: 100%; background-color: #f0f0f0;">
<ScrollView class="scroll-view" :data="ufoList" :options="scrollOptions" v-if="ufoList.length">
<CouponItem :item="item" v-for="(item, index) in ufoList" :key ="index"></CouponItem>
</ScrollView>
<Empty v-else></Empty>
</div>
</template>
<script>
import {createNamespacedHelpers} from 'vuex';
const {mapState} = createNamespacedHelpers('coupon/yoho');
import CouponItem from '../components/coupon-item';
import Empty from '../components/empty';
import ScrollView from '../components/scroll-view';
export default {
name: 'UfoCouponListPage',
computed: {
...mapState(['ufoList']),
},
data() {
return {
scrollOptions: {
directionLockThreshold: 0,
bounce: true
}
};
},
created() {
},
mounted() {
this.getList();
},
methods: {
getList() {
return this.$store.dispatch('coupon/yoho/fetchUfoList', {
limit: 100,
page: 1
});
}
},
components: {
ScrollView,
CouponItem,
Empty
}
};
</script>
<style lang="scss" scoped>
.scroll-view {
height: calc(100% - 90px);
}
</style>
... ...
// export default {
// path: 'yoho.html',
// name: 'couponYoho',
// component: () => import(/* webpackChunkName: "coupon" */ './list')
// };
import YohoList from './list';
export default YohoList;
... ...
<template>
<div style="height: 100%;">
<ClassicTabs ref="tabs" v-model="selectLabel" :data="tabLabels" :filterId.sync="selectFilterId"></ClassicTabs>
<div class="tab-slide-container">
<Slide
ref="slide"
:loop="false"
:auto-play="false"
:show-dots="false"
:initial-index="initialIndex"
:options="slideOptions"
@scroll="scroll"
@change="changePage"
>
<SlideItem style="background-color: #f0f0f0;">
<ExchangeBox v-model="inputCouponCode" @click="onSubmitCode"></ExchangeBox>
<ScrollView class="scroll-view1" ref="notuse" :data="getNotUseList" :options="scrollOptions" v-if="getNotUseList.length" :reach-bottom="reachBottom">
<CouponItem :item="item" v-for="(item, index) in getNotUseList" :key="index + '' + item.coupon_id"></CouponItem>
</ScrollView>
<Empty v-else></Empty>
</SlideItem>
<SlideItem style="background-color: #f0f0f0;">
<ScrollView class="scroll-view2" ref="use" :data="getUseList" :options="scrollOptions" v-if="getUseList.length" :reach-bottom="reachBottom">
<CouponItem :item="item" v-for="(item, index) in getUseList" :key="index + '' + item.coupon_id"></CouponItem>
</ScrollView>
<Empty v-else></Empty>
</SlideItem>
<SlideItem style="background-color: #f0f0f0;">
<ScrollView class="scroll-view2" ref="overtime" :data="getOvertimeList" :options="scrollOptions" v-if="getOvertimeList.length" :reach-bottom="reachBottom">
<CouponItem :item="item" v-for="(item, index) in getOvertimeList" :key="index + '' + item.coupon_id"></CouponItem>
</ScrollView>
<Empty v-else></Empty>
</SlideItem>
</Slide>
</div>
</div>
</template>
<script>
import {createNamespacedHelpers} from 'vuex';
const {mapState, mapActions, mapGetters} = createNamespacedHelpers('coupon/yoho');
import Tabs from '../components/tabs';
import FilterBar from '../components/filter-bar';
import ExchangeBox from '../components/exchange-box';
import CouponItem from '../components/coupon-item';
import Empty from '../components/empty';
import ScrollView from '../components/scroll-view';
import {Slide} from 'cube-ui';
const TYPE = {notuse: 'notuse', use: 'use', overtime: 'overtime'};
export default {
name: 'YohoCouponListPage',
data() {
return {
selectIndex: 0,
selectLabel: '未使用',
selectType: TYPE.notuse,
selectFilterId: 0,
inputCouponCode: '',
tabLabels: [
{
label: '未使用',
type: TYPE.notuse,
filter: true,
selectFilterId: 0,
}, {
label: '已使用',
type: TYPE.use,
filter: false,
selectFilterId: 0,
}, {
label: '已失效',
type: TYPE.overtime,
filter: false,
selectFilterId: 0,
}
],
scrollOptions: {
directionLockThreshold: 0,
pullUpLoad: {
threshold: 0,
txt: {
more: '加载更多',
noMore: '以上是最新的内容'
}
},
bounce: true
},
slideOptions: {
listenScroll: true,
probeType: 3,
click: false,
directionLockThreshold: 0
},
};
},
computed: {
...mapState(['filterList']),
...mapGetters(['getNotUseList', 'getUseList', 'getOvertimeList']),
initialIndex() {
let index = 0;
index = this.tabLabels.findIndex(i => i.label === this.selectLabel);
return index;
}
},
mounted() {
this.fetchYohoNum();
this.$store.dispatch('coupon/yoho/fetchYohoList', {
type: TYPE.use,
refresh: true
});
this.$store.dispatch('coupon/yoho/fetchYohoList', {
type: TYPE.overtime,
refresh: true
});
},
methods: {
...mapActions(['getCouponCode', 'fetchYohoList', 'fetchYohoNum']),
changePage(current) {
this.selectIndex = current;
this.selectLabel = this.tabLabels[current].label;
this.selectType = this.tabLabels[current].type;
},
scroll() {
},
onSubmitCode() {
this.getCouponCode({code: this.inputCouponCode}).then(result => {
if (result.code === 200) {
this.$createToast({
txt: result.message,
type: 'txt',
mask: true,
time: 1000
}).show();
this.reachBottom(true);
} else {
this.$createToast({
txt: result.message,
type: 'txt',
mask: true,
time: 1000
}).show();
}
});
},
reachBottom(refresh = false) {
this.fetchYohoList({
type: this.selectType,
filter: this.tabLabels[this.selectIndex].selectFilterId,
refresh
}).then((result) => {
if (!result) {
this.$refs[this.selectType] && this.$refs[this.selectType].forceUpdate();
}
});
}
},
watch: {
selectFilterId(val) {
this.tabLabels[0].selectFilterId = val;
this.$refs.notuse && this.$refs.notuse.scrollTo(0, 0);
this.reachBottom(true);
}
},
components: {
FilterBar,
ClassicTabs: Tabs.ClassicTabs,
ExchangeBox,
Slide,
SlideItem: Slide.Item,
CouponItem,
Empty,
ScrollView
}
};
</script>
<style lang="scss" scoped>
.page-wrapper {
height: calc(100% - 90px);
}
.tab-slide-container {
height: 100%;
}
.scroll-view1 {
height: calc(100% - 270px);
}
.scroll-view2 {
height: calc(100% - 180px);
}
</style>
... ...
import Order from './order';
export default [...Order];
... ...
<template>
<CubeInput v-bind="$attrs" v-bind:value="value" v-on="inputListeners" :maxlength="8" class="input-number">
<span slot="prepend">
<slot name="prepend"></slot>
</span>
<span slot="append">
<slot name="append"></slot>
</span>
</CubeInput>
</template>
<script>
import {Input} from 'cube-ui';
export default {
name: 'InputUfo',
props: ['value'],
computed: {
inputListeners() {
return Object.assign({},
this.$listeners,
{
input: (value) => {
this.$emit('input', value);
}
}
);
}
},
components: {CubeInput: Input}
};
</script>
<style lang="scss">
.input-number {
margin-bottom: 15px;
background-color: #f5f5f5;
border-radius: 10px;
font-size: 40px;
&:after {
border-radius: 20px;
border-color: #f5f5f5;
}
/deep/ .cube-input-field {
font-weight: bold;
color: #000;
}
}
</style>
... ...
<template>
<div class="modal-box" v-show="value" v-transfer-dom :data-transfer="transfer">
<div class="modal-mask"></div>
<div class="modal-wrap" @touchmove.prevent.stop="onTouchmove">
<div class="modal modal-content">
<div class="modal-body">
<slot>
<div class="text">{{title}}</div>
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
<Button class="btn" type="button" @click="onCancel">{{cancelText}}</Button>
<Button class="btn" :class="{active: loading}" type="button" @click="onSure">{{sureText}}</Button>
</slot>
</div>
</div>
</div>
</div>
</template>
<script>
import Button from 'components/button.vue';
export default {
name: 'Modal',
props: {
transfer: [Boolean],
title: String,
value: Boolean,
loading: Boolean,
sureText: {
type: String,
default: '确认'
},
cancelText: {
type: String,
default: '取消'
}
},
methods: {
onTouchmove() {},
onCancel() {
this.$emit('on-cancel');
this.$emit('input', false);
},
onSure() {
if (!this.loading) {
this.$emit('on-sure');
}
}
},
components: {Button}
};
</script>
<style lang="scss">
.modal-box {
font-size: 24px;
}
.modal-mask {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
height: 100%;
z-index: 99;
&-hidden {
display: none;
}
}
.modal {
width: auto;
margin: 0 auto;
position: relative;
outline: none;
&-hidden {
display: none !important;
}
&-wrap {
position: fixed;
overflow: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 99;
-webkit-overflow-scrolling: touch;
outline: 0;
}
&-content {
position: relative;
background-color: #fff;
top: 314px;
border: 0;
width: 600px;
background-clip: padding-box;
& .text {
text-align: center;
padding-top: 30px;
padding-bottom: 30px;
}
}
&-body {
padding: 38px;
}
&-footer {
width: 100%;
border-top: 1px solid #eee;
display: flex;
button {
width: 50%;
overflow: hidden;
height: 100px;
background: none;
&:first-child {
color: #999;
}
}
button + button {
margin-left: 8px;
margin-bottom: 0;
border-left: 1px solid #eee;
}
}
}
</style>
... ...
import UfoOrder from './ufo';
export default [...UfoOrder];
... ...
<template>
<Modal
v-model="visiable"
sure-text="调整售价"
cancel-text="取消"
:loading="fetchingChangePrice"
:transfer="true"
@on-sure="onSure">
<div class="change-price-modal">
<p class="modal-title">当前{{skc.sizeName}}码最低售价:¥{{skc.leastPrice}}</p>
<InputUfo type="number" :maxlength="8" class="input-number" v-model="chgPrice">
<span class="prepend" slot="prepend">¥</span>
</InputUfo>
<p class="tips" v-show="errorTip">{{errorTip}}</p>
<p class="price-line" v-for="(price, inx) in prices" :key="inx" :class="{total: price.total}">
<span class="title">{{price.label}}</span>
<span class="price">{{price.money}}</span>
</p>
</div>
</Modal>
</template>
<script>
import {debounce, get} from 'lodash';
import InputUfo from '../../components/input-ufo';
import Modal from '../../components/modal.vue';
import {createNamespacedHelpers} from 'vuex';
const {mapState, mapActions} = createNamespacedHelpers('ufo/order');
export default {
name: 'ModalPrice',
data() {
return {
visiable: false,
skc: {},
prices: [],
errorTip: '',
chgPrice: '',
};
},
mounted() {
this.inputChange = debounce(this.onChange.bind(this), 500);
},
watch: {
chgPrice(newVal) {
this.inputChange(newVal);
}
},
computed: {
...mapState(['fetchingChangePrice'])
},
methods: {
...mapActions(['postCalcPrice']),
show({skc, product}) {
this.chgPrice = '';
this.errorTip = '';
this.prices = [];
this.skc = skc;
this.product = product;
this.visiable = true;
},
hide() {
this.skc = {};
this.product = {};
this.visiable = false;
},
onChange(price) {
if (this.checkPrice(price)) {
this.calcPrice(price);
}
},
async calcPrice(price) {
const result = await this.postCalcPrice({
product_id: this.product.productId,
storage_id: this.skc.storageId,
new_price: price,
old_price: this.skc.price,
num: this.skc.storageNum
});
if (result && result.code === 200) {
this.prices = [{
label: '平台费用:',
money: get(result, 'data.platformFee.amount', '')
}, {
label: '银行转账费用:',
money: get(result, 'data.bankTransferFee', '')
}, {
label: '实际收入:',
money: get(result, 'data.income', ''),
total: true
}];
} else {
if (result.message) {
this.errorTip = result.message;
}
this.$createToast({
txt: result.message,
type: 'warn',
}).show();
}
},
checkPrice(price) {
let valid = false;
if (!price) {
this.errorTip = '没有价格';
return false;
} else if (!/^\d+$/.test(price)) {
this.errorTip = '价格只能为正整数';
} else if (!/9$/.test(price)) {
this.errorTip = '出售价格必须以9结尾';
} else if (this.skc.minPrice && price < this.skc.minPrice) {
this.errorTip = '您的出价过低';
} else if (this.skc.maxPrice && price > this.skc.maxPrice) {
this.errorTip = '您的出价格过高';
} else if (price === this.skc.price) {
this.errorTip = '前后价格没有变化';
} else if (this.skc.suggestMaxPrice && price > this.skc.suggestMaxPrice) {
this.errorTip = '超出建议价将被限制展示,建议下调至合理价格区间';
valid = true;
} else {
this.errorTip = '';
valid = true;
}
return valid;
},
onSure() {
if (!this.checkPrice(this.chgPrice)) {
this.$createToast({
txt: this.errorTip,
type: 'warn',
}).show();
} else {
this.$emit('on-change-price', {skc: this.skc, price: this.chgPrice});
}
},
onInput(val) {
this.$emit('input', val);
}
},
components: {Modal, InputUfo}
};
</script>
<style lang="scss" scoped>
.change-price-modal {
.tips {
color: #d0021b;
font-size: 24px;
margin-bottom: 20px;
}
.price-line {
margin-bottom: 20px;
color: #999;
font-size: 24px;
display: flex;
.title {
width: 50%;
}
.price {
width: 50%;
text-align: right;
}
&.total {
color: #000;
}
}
.input-number {
/deep/ .prepend {
width: 40px;
margin-left: 20px;
text-align: left;
}
}
.modal-title {
line-height: 100px;
text-align: center;
}
}
</style>
... ...
<template>
<Modal
v-model="visiable"
:loading="fetchingNoSale"
:transfer="true"
@on-sure="onSure">
<div class="change-price-modal">
<p class="modal-title">选择你要下架的数量</p>
<InputUfo v-model="unStockNum" :maxlength="8" :readonly="true" class="inp-unstock">
<i slot="prepend" class="iconfont icon-plus-minus" @touchend="onChangeNum(-1)"></i>
<i slot="append" class="iconfont icon-i-add" @touchend="onChangeNum(1)"></i>
</InputUfo>
<p class="stock-txt">
目前还有 {{storageNum}} 个库存
</p>
<p class="tips">
下架商品的保证金将会被释放
</p>
</div>
</Modal>
</template>
<script>
import InputUfo from '../../components/input-ufo';
import Modal from '../../components/modal.vue';
import {createNamespacedHelpers} from 'vuex';
const {mapState} = createNamespacedHelpers('ufo/order');
export default {
name: 'ModalPrice',
data() {
return {
visiable: false,
skc: {},
unStockNum: 1,
storageNum: 1,
};
},
computed: {
...mapState(['fetchingNoSale'])
},
methods: {
show({skc}) {
this.skc = skc;
this.unStockNum = 1;
this.storageNum = skc.storageNum;
this.visiable = true;
},
hide() {
this.skc = {};
this.storageNum = 1;
this.unStockNum = 1;
this.visiable = false;
},
onSure() {
this.$emit('on-no-sale', {skc: this.skc, num: this.unStockNum});
},
onInput(val) {
this.$emit('input', val);
},
onChangeNum(num) {
const value = this.unStockNum + num;
if (value <= 0 || value > this.storageNum) {
return;
}
this.unStockNum = value;
}
},
components: {Modal, InputUfo}
};
</script>
<style lang="scss" scoped>
.change-price-modal {
.stock-txt {
margin-bottom: 10px;
}
.tips {
margin-bottom: 10px;
color: #999;
}
.inp-unstock {
margin-bottom: 10px;
/deep/ .iconfont {
padding: 34px 40px;
width: 112px;
font-size: 32px;
color: #999;
font-weight: bold;
}
/deep/ .cube-input-field {
text-align: center;
}
}
.modal-title {
line-height: 100px;
text-align: center;
}
}
</style>
... ...
<template>
<div class="product-item" :class="{['has-tip']: value.tip}">
<div class="item-content" :style="itemStyle" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
<div class="tip" v-if="showTip">超出建议售价将被限制展示,建议下调至合理价格区间</div>
<div class="info">
<div class="left">
<span class="size ufo-font">{{value.goodsInfo.sizeName}}</span>
<span class="l-size"></span>
<span class="unit">码</span>
</div>
<div class="middle">
<p class="size-store">¥{{value.goodsInfo.price}},{{value.goodsInfo.storageNum}}个库存</p>
<p class="low-price">当前最低价¥{{value.goodsInfo.leastPrice}}</p>
</div>
<div class="right">
<Button class="chg-price" @click="onChgPrice">调 价</Button>
</div>
</div>
</div>
<div ref="options" class="item-options">
<Button class="btn-no-sale" @click="onNoSale">不卖了</Button>
</div>
</div>
</template>
<script>
import {Button} from 'cube-ui';
export default {
name: 'ProductItem',
props: {
value: Object,
slideValue: Object
},
data() {
return {
distance: 0,
startX: 0,
startY: 0,
move: false,
transition: true
};
},
computed: {
showTip() {
return this.value.goodsInfo.price > this.value.goodsInfo.suggestMaxPrice;
},
itemStyle() {
return {
transition: this.transition ? void 0 : 'none 0s ease 0s',
transform: this.move ? `translate3d(${this.distance}px, 0px, 0px)` : void 0
};
}
},
mounted() {
},
watch: {
slideValue(val) {
if (this.distance !== 0 && this.value !== val) {
this.distance = 0;
this.timeout = setTimeout(() => {
this.move = false;
}, 500);
}
}
},
methods: {
onNoSale() {
this.$emit('on-no-sale', this.value.goodsInfo);
},
onChgPrice() {
this.$emit('on-change-price', this.value.goodsInfo);
},
onTouchStart(evt) {
const {clientX, clientY} = evt.touches[0];
this.optionsWidth = this.$refs.options.clientWidth;
this.startX = clientX - this.distance;
this.startY = clientY;
if (this.timeout) {
clearTimeout(this.timeout);
}
},
onTouchMove(evt) {
this.transition = false;
const {clientX, clientY} = evt.touches[0];
let distance = clientX - this.startX;
if (Math.abs(distance) > this.optionsWidth) {
distance = (0 - this.optionsWidth) + (distance + this.optionsWidth) * 0.1;
}
if (Math.abs(clientY - this.startY) > 20 && this.distance === 0) {
this.startX = 0;
return;
}
if (0 - distance > 20) {
this.$emit('on-slide', this.value);
this.move = true;
}
if (this.distance + distance > 0) {
return;
}
if (this.move) {
this.distance = distance;
}
},
onTouchEnd() {
if (0 - this.distance > this.optionsWidth) {
this.transition = true;
this.distance = 0 - this.optionsWidth;
} else if (0 - this.distance < this.optionsWidth) {
this.transition = true;
this.distance = 0;
}
if (this.distance === 0) {
this.timeout = setTimeout(() => {
this.move = false;
}, 500);
}
}
},
components: {Button}
};
</script>
<style lang="scss" scoped>
.product-item {
width: 100%;
border-bottom: 1px solid #eee;
position: relative;
}
.item-content {
padding-left: 40px;
padding-right: 40px;
position: relative;
z-index: 2;
background-color: #fff;
padding-top: 20px;
padding-bottom: 40px;
transition: transform 0.5s cubic-bezier(0.36, 0.66, 0.04, 1);
}
.item-options {
position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 1;
.btn-no-sale {
height: 100%;
line-height: 1;
background-color: #eee;
width: 160px;
font-size: 28px;
color: #000;
&:active {
opacity: 0.7;
}
}
}
.tip {
color: #d0021b;
}
.info {
width: 100%;
display: flex;
padding-top: 20px;
}
.left {
width: 160px;
display: flex;
height: 56px;
align-items: flex-end;
.size {
font-size: 56px;
line-height: 56px;
margin-right: 6px;
}
.l-size {
align-self: flex-end;
margin-right: 6px;
}
.unit {
align-self: flex-start;
margin-right: 6px;
}
}
.middle {
flex: 1;
.size-store {
font-size: 28px;
}
.low-price {
color: #999;
margin-top: 10px;
}
}
.right {
width: 130px;
text-align: right;
display: flex;
align-items: center;
.chg-price {
width: 130px;
height: 60px;
line-height: 60px;
padding-top: 0;
padding-bottom: 0;
background-color: #08314d;
font-size: 28px;
border-radius: 0;
&:active {
opacity: 0.7;
}
}
}
</style>
... ...
<template>
<div class="product-group">
<ProductItem
v-for="(skc, i) in skcs"
:key="i"
:value="skc"
:slideValue="slideSkc"
@on-change-price="onChangePrice"
@on-no-sale="onNoSale"
@on-slide="onSlide"></ProductItem>
</div>
</template>
<script>
import ProductItem from './product-item';
import {createNamespacedHelpers} from 'vuex';
const {mapActions} = createNamespacedHelpers('ufo/order');
export default {
name: 'ProductList',
props: {
skcs: Array,
},
data() {
return {
slideSkc: {}
};
},
methods: {
...mapActions(['postNoSale']),
onChangePrice(params) {
this.$emit('on-change-price', params);
},
onNoSale(params) {
this.$emit('on-no-sale', params);
},
onSlide(val) {
this.slideSkc = val;
},
reset() {
this.slideSkc = {};
}
},
components: {ProductItem}
};
</script>
... ...
export default [{
path: '/mapp/order/ufo/:orderId(\\d+).html',
name: 'order',
component: () => import(/* webpackChunkName: "order" */ './order')
}];
... ...