Authored by 陈峰

commit

... ... @@ -7,6 +7,7 @@ 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
... ... @@ -14,6 +15,7 @@ Vue.use(lazyload, {
Vue.use(pluginCore);
Vue.mixin(titleMixin);
export function createApp(context) {
const router = createRouter();
const store = createStore(context);
... ... @@ -21,8 +23,8 @@ export function createApp(context) {
const app = new Vue({
router,
store,
errorCaptured(e) {
console.log('errorCaptured', e);
errorCaptured(error) {
reportError(context, 'server')(error);
return false;
},
render: h => h(App)
... ...
<template>
<div id="app">
<transition
:name="`route-view-${yoho.direction}`">
<router-view></router-view>
</transition>
</div>
</template>
... ... @@ -11,9 +14,6 @@ export default {
name: 'App',
computed: {
...mapState(['yoho'])
},
errorCaptured(e) {
console.error(e)
}
};
</script>
... ... @@ -24,7 +24,6 @@ export default {
.route-view-back-enter-active,
.route-view-back-leave-active {
will-change: true;
transition: all 300ms;
width: 100%;
height: 100%;
position: absolute;
... ... @@ -32,23 +31,31 @@ export default {
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 {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
.route-view-forword-leave-active {
opacity: 0;
transform: translate3d(-100%, 0, 0);
transform: translate3d(-30%, 0, 0);
}
.route-view-back-enter {
opacity: 0;
transform: translate3d(-100%, 0, 0);
z-index: 1;
transform: translate3d(-30%, 0, 0);
}
.route-view-back-leave-active {
opacity: 0;
transform: translate3d(100%, 0, 0);
z-index: 2;
}
</style>
... ...
... ... @@ -7,10 +7,12 @@ axios.defaults.headers = {
'X-Requested-With': 'XMLHttpRequest'
};
const errHandle = error => {
let msg = error && error.config ? `ssr api:[${error.config.method}] ${error.config.url} ${error.config.params || ''} ${error.response && error.response.data}` : 'axios error';
return Promise.reject(msg);
const errHandle = (error) => {
console.log(error);
return Promise.reject({
code: 500,
message: '服务器开小差了~'
});
};
const request = (options) => {
return axios(options).then(res => res.data, errHandle);
... ...
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 checkParams = require('../../utils/check-params');
const apiMaps = require('../../config/api-map');
const checkApiMap = url => {
return apiMaps[url] ? apiMaps[url] : void 0;
... ... @@ -59,13 +62,24 @@ const request = async({url, method, reqParams = {}, context}) => {
}
};
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});
return request({url, method: 'get', reqParams, context}).then(catchError(context, reqParams));
},
post(url, reqParams) {
return request({url, method: 'post', reqParams, context});
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=yoho-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: 'yoho-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);
}
});
}
};
};
... ...
... ... @@ -2,10 +2,12 @@ const config = {
development: {
axiosBaseUrl: '',
axiosResponseType: 'json',
reportUrl: '//badjs.yoho.cn/apm/yas2.gif'
},
production: {
axiosBaseUrl: '',
axiosResponseType: 'json',
reportUrl: '//badjs.yoho.cn/apm/yas2.gif'
}
};
... ...
import Vue from 'vue';
import {
ROUTE_CHANGE,
REPORT_YAS,
} from 'store/yoho/types';
import {createApp} from './app';
import {createApi} from 'create-api';
... ... @@ -12,12 +11,14 @@ import 'statics/scss/common.scss';
import 'statics/font/iconfont.css';
import 'statics/font/ufofont.css';
const {app, router, store} = createApp();
const {app, router, store} = createApp(window.__INITIAL_STATE__.yoho.context);
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
window._router = store.state.yoho.context.route;
Vue.prop('yoho', yoho);
Vue.use(Toast);
Vue.use(Dialog);
... ... @@ -25,15 +26,27 @@ Vue.prop('api', createApi());
Vue.use(Lazy, {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
}
}
});
router.beforeResolve((to, from, next) => {
try {
const matched = router.getMatchedComponents(to);
store.commit(ROUTE_CHANGE, to);
store.commit(ROUTE_CHANGE, {to, from});
if (window._hmt) {
window._hmt.push(['_trackPageview', to.fullPath]);
}
store.dispatch(REPORT_YAS, {
store.dispatch('reportYas', {
params: {
appop: 'YB_H5_PAGE_OPEN_L',
param: {
... ... @@ -54,15 +67,20 @@ router.onReady(() => {
}
})
.catch(e => {
store.dispatch('reportError', {error: e});
console.error(e);
return next();
});
} catch (e) {
store.dispatch('reportError', {error: e});
return next();
}
});
app.$mount('#app');
});
router.onError(e => {
console.error(e);
store.dispatch('reportError', {error: e});
router.push({name: 'error.500'});
});
... ...
import {createApp} from './app';
import {get} from 'lodash';
import createReport from 'report-error';
import {
SET_ENV,
} from 'store/yoho/types';
const sender = global.yoho.apmSender;
const logger = global.yoho.logger;
const catchError = (err, message, context) => {
logger.error(message, err);
setImmediate(() => {
try {
sender.addMessage({
measurement: 'error-report',
tags: {
app: 'yoho-app-web', // 应用名称
hostname: context.hostname,
type: 'server',
route: context.route, // 请求路由
uid: get(context, 'user.uid', 0),
udid: context.udid,
code: err.code || 500,
path: context.path,
url: encodeURIComponent(context.url),
ip: context.env.clientIp
},
fields: {
message: err.message,
stack: err.stack,
useragent: context.ua
}
});
} catch (error) {
logger.error(error);
}
});
};
export default context => {
const reportError = createReport(context);
return new Promise((resolve, reject) => {
const {app, router, store} = createApp(context);
const {url, env} = context;
const {url} = context;
store.commit(SET_ENV, env);
store.commit(SET_ENV, {context});
router.push(url);
router.onReady(() => {
const matched = router.getMatchedComponents();
if (matched.some(m => !m)) {
catchError(new Error('导航组件为空'), '[router.onReady]', context);
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);
}
});
Promise.all(matched.map(({asyncData}) =>
asyncData && asyncData({store, router: router.currentRoute})))
Promise.all(asyncDataPromises)
.then(() => {
context.state = store.state;
return resolve(app);
}).catch(e => {
catchError(e, '[asyncData]', context);
reportError(e);
return resolve(app);
});
});
router.onError(e => {
catchError(e, 'router.onError', context);
reportError(e);
router.push({name: 'error.500'});
return resolve(app);
});
... ...
... ... @@ -16,5 +16,77 @@
</head>
<body>
<!--vue-ssr-outlet-->
<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>
... ...
export default [{
path: '/error/404',
path: '/mapp/error/404',
name: 'error.404',
component: () => import(/* webpackChunkName: "error" */ './404')
}, {
path: '/error/500',
path: '/mapp/error/500',
name: 'error.500',
component: () => import(/* webpackChunkName: "error" */ './500')
}];
... ...
... ... @@ -2,7 +2,7 @@
<Modal
class="ufo-font"
v-model="visiable"
sure-text="调整价格"
sure-text="调整售价"
cancel-text="取消"
:loading="fetchingChangePrice"
:transfer="true"
... ... @@ -12,9 +12,7 @@
<InputUfo type="number" :maxlength="8" class="input-number" v-model="chgPrice">
<span class="prepend" slot="prepend">¥</span>
</InputUfo>
<transition name="tips">
<p class="tips" v-show="errorTip">{{errorTip}}</p>
</transition>
<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>
... ... @@ -95,8 +93,11 @@ export default {
total: true
}];
} else {
if (result.message) {
this.errorTip = result.message;
}
this.$createToast({
txt: result.message || '计算失败',
txt: result.message,
type: 'warn',
}).show();
}
... ... @@ -184,18 +185,4 @@ export default {
text-align: center;
}
}
.tips-enter-active,
.tips-leave-active {
will-change: true;
transition: opacity 300ms;
}
.tips-enter {
opacity: 0;
}
.tips-leave-active {
opacity: 0;
}
</style>
... ...
... ... @@ -55,6 +55,7 @@ export default {
this.visiable = false;
},
onSure() {
if (this.storageNum <= 1) {
this.$createDialog({
type: 'confirm',
content: '您确定不卖此商品吗?',
... ... @@ -72,6 +73,9 @@ export default {
this.$emit('on-no-sale', {skc: this.skc, num: this.unStockNum});
},
}).show();
} else {
this.$emit('on-no-sale', {skc: this.skc, num: this.unStockNum});
}
},
onInput(val) {
this.$emit('input', val);
... ...
<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="value.tip">超出建议售价将被限制超出建议售价将被限制展示</div>
<div class="tip" v-if="showTip">超出建议售价将被限制超出建议售价将被限制展示</div>
<div class="info">
<div class="left">
<span class="size">{{value.goodsInfo.sizeName}}</span>
... ... @@ -9,7 +9,7 @@
<span class="unit">码</span>
</div>
<div class="middle">
<p class="size-store">¥{{value.goodsInfo.price}},{{value.goodsInfo.storageNum}}个库存</p>
<p class="size-store font">¥{{value.goodsInfo.price}},{{value.goodsInfo.storageNum}}个库存</p>
<p class="low-price">当前最低价¥{{value.goodsInfo.leastPrice}}</p>
</div>
<div class="right">
... ... @@ -28,6 +28,10 @@ import {Button} from 'cube-ui';
export default {
name: 'ProductItem',
props: {
value: Object,
slideValue: Object
},
data() {
return {
distance: 0,
... ... @@ -38,6 +42,9 @@ export default {
};
},
computed: {
showTip() {
return this.value.goodsInfo.price > this.value.goodsInfo.suggestMaxPrice;
},
itemStyle() {
return {
transition: this.transition ? void 0 : 'none 0s ease 0s',
... ... @@ -45,10 +52,6 @@ export default {
};
}
},
props: {
value: Object,
slideValue: Object
},
mounted() {
},
... ... @@ -72,6 +75,7 @@ export default {
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) {
... ... @@ -83,6 +87,10 @@ export default {
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;
... ... @@ -101,12 +109,10 @@ export default {
}
},
onTouchEnd() {
const optionsWidth = this.$refs.options.clientWidth;
if (0 - this.distance > optionsWidth) {
if (0 - this.distance > this.optionsWidth) {
this.transition = true;
this.distance = 0 - optionsWidth;
} else if (0 - this.distance < optionsWidth) {
this.distance = 0 - this.optionsWidth;
} else if (0 - this.distance < this.optionsWidth) {
this.transition = true;
this.distance = 0;
}
... ... @@ -202,7 +208,7 @@ export default {
.low-price {
color: #999;
margin-top: 6px;
margin-top: 10px;
}
}
... ... @@ -220,6 +226,7 @@ export default {
padding-bottom: 0;
background-color: #08314d;
font-size: 28px;
border-radius: 0;
&:active {
opacity: 0.7;
... ...
... ... @@ -50,6 +50,7 @@ const {mapState, mapActions, mapMutations} = createNamespacedHelpers('ufo/order'
export default {
name: 'OrderPage',
title: '订单',
data() {
return {
classes: {},
... ... @@ -158,7 +159,7 @@ export default {
<style lang="scss" scoped>
.order-page {
& > .title {
font-size: 82px;
font-size: 78px;
font-weight: bold;
padding-top: 24px;
padding-left: 40px;
... ... @@ -169,11 +170,11 @@ export default {
width: 100%;
padding-left: 40px;
padding-right: 40px;
height: 192px;
height: 180px;
display: flex;
.pro-img {
width: 192px;
width: 180px;
}
.pro-info {
... ... @@ -211,7 +212,7 @@ export default {
font-size: 24px;
padding-left: 14px;
padding-right: 14px;
background-color: #f0f0f0;
background-color: #f5f5f5;
display: flex;
align-items: center;
... ...
... ... @@ -520,6 +520,10 @@ textarea {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.font {
font-family: "PingFang SC", "HiraginoSansGB-W3", "SanFranciscoText-Regular", Helvetica, Roboto, "Heiti SC", "黑体", Arial;
}
html,
body {
font-size: 24px;
... ...
... ... @@ -21,6 +21,7 @@ export function createStore(context) {
const api = createApi(context);
store.$api = api;
store.$context = context;
return store;
}
... ...
import * as Types from './types';
import cookie from 'yoho-cookie';
export default function() {
return {
state: {
context: {
title: '',
env: {
isApp: true,
... ... @@ -11,39 +13,69 @@ export default function() {
isYohoBuy: false,
channel: 'men',
fs: true,
supportsPassive: false
},
scrolling: false,
route: '',
path: ''
},
historys: [],
visible: true,
pageVisible: false,
touchStatus: '',
scrollTime: 0,
direction: 'forword',
},
mutations: {
[Types.SET_ENV]() {
},
[Types.INIT_ROUTE_CHANGE]() {
},
[Types.ROUTE_CHANGE]() {
[Types.SET_ENV](state, {context}) {
state.context.title = context.title;
state.context.env = context.env;
state.context.route = context.route;
state.context.path = context.path;
},
[Types.REPORT_YAS]() {
[Types.SET_TITLE](state, {title}) {
state.context.title = title;
},
setScroll(state, isScroll) {
state.scrolling = isScroll;
},
SET_SUPPORTS_PASSIVE(state, supportsPassive) {
state.supportsPassive = supportsPassive;
},
SET_TOUCH_STATUS(state, {touchStatus, time}) {
console.log(touchStatus)
if (time) {
state.scrollTime = time;
[Types.ROUTE_CHANGE](state, {to, from}) {
if (!state.historys.length) {
state.historys.push({
name: from.name,
path: from.fullPath
});
}
const routeIndex = state.historys.findIndex(route => route.path === to.fullPath);
if (routeIndex >= 0) {
state.historys = state.historys.slice(0, routeIndex + 1);
state.direction = 'back';
} else {
state.historys.push({
name: to.name,
path: to.fullPath
});
state.direction = 'forword';
}
state.touchStatus = touchStatus;
}
},
actions: {
reportYas(params, {params: {appop, param}, asyncindx = false}) {
document.addEventListener('deviceready', () => {
setTimeout(() => {
if (window._yas && window._yas.sendAppLogs) {
param = param || {};
if (!param.C_ID) {
const channel = {
men: 1,
women: 2
}[cookie.get('_Channel') || 'men'];
param.C_ID = channel;
}
window._yas.sendAppLogs({
appop,
param: param ? JSON.stringify(param) : '{}'
}, asyncindx);
}
}, 300);
});
}
}
};
}
... ...
export const SET_ENV = 'SET_ENV';
export const SET_TITLE = 'SET_TITLE';
export const ROUTE_CHANGE = 'ROUTE_CHANGE';
export const INIT_ROUTE_CHANGE = 'INIT_ROUTE_CHANGE';
export const PAGE_INIT_VISIBLE = 'PAGE_INIT_VISIBLE';
export const YOHO_PAGE_VISIBLE = 'YOHO_PAGE_VISIBLE';
export const REPORT_YAS = 'REPORT_YAS';
... ...
module.exports = {
presets: [
["@babel/preset-env", {
"modules": false,
"useBuiltIns": "usage"
['@babel/preset-env', {
modules: false,
useBuiltIns: 'usage'
}]
],
sourceType: "unambiguous",
sourceType: 'unambiguous',
plugins: [
"transform-vue-jsx",
"@babel/transform-async-to-generator",
"@babel/proposal-object-rest-spread",
"@babel/syntax-dynamic-import"
'transform-vue-jsx',
'@babel/transform-async-to-generator',
'@babel/proposal-object-rest-spread',
'@babel/syntax-dynamic-import'
]
}
\ No newline at end of file
};
... ...
const shelljs = require('shelljs');
const path = require('path');
const distDir = path.join(__dirname, '../dist/node');
const distDir = path.join(__dirname, '../public/dist/node');
shelljs.rm('-rf', distDir);
shelljs.mkdir('-p', distDir);
... ... @@ -19,8 +19,8 @@ const cpPaths = [
'doraemon',
'utils',
'apps/index.html',
'dist/manifest.json',
'dist/manifest.server.json'
'public/dist/manifest.json',
'public/dist/manifest.server.json'
];
new Promise(resolve => { // 加载manifest.json文件
... ...
... ... @@ -6,7 +6,7 @@ var TransformModulesPlugin = require('webpack-transform-modules-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const pkg = require('../package.json');
const isProd = process.env.NODE_ENV === 'production';
const distDir = path.join(__dirname, `../dist/statics/${pkg.name}`);
const distDir = path.join(__dirname, `../public/dist/statics/${pkg.name}`);
function resolve(dir) {
return path.join(__dirname, '..', dir);
... ...
... ... @@ -94,7 +94,8 @@ const webpackConfig = merge(baseConfig, {
},
resolve: {
alias: {
'create-api': 'common/create-api-client.js'
'create-api': 'common/create-api-client.js',
'report-error': 'common/report-error-client.js'
}
},
plugins: [
... ...
... ... @@ -3,7 +3,6 @@ const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
let baseConfig = require('./webpack.base.conf');
const pkg = require('../package.json');
const isProd = process.env.NODE_ENV === 'production';
let webpackConfig = merge(baseConfig, {
... ... @@ -14,7 +13,8 @@ let webpackConfig = merge(baseConfig, {
target: 'node',
resolve: {
alias: {
'create-api': 'common/create-api-server.js'
'create-api': 'common/create-api-server.js',
'report-error': 'common/report-error-server.js'
}
},
module: {
... ...
... ... @@ -33,7 +33,7 @@ if (!isDev) {
const getContext = (req) => {
return {
url: req.url,
title: 'BLK!',
title: 'BLK2!',
user: req.user,
env: {
isApp: req.yoho.isApp,
... ...