Commit c7027cf3 authored by 郝聪敏's avatar 郝聪敏

feature: 优化

parent 67a5e0c2
import http from '../service/http.service'; import http from '../service/http.service';
import config from '../config'; import config from '../config';
import axios from 'axios';
export default { export default {
getPageById(params) { getPageById(params) {
return http.get(`editor/get/${params.pageId}`); return http.get(`${config.apiHost}editor/get/${params.pageId}`);
} }
}; };
\ No newline at end of file
import http from '../service/http.service';
import config from '../config';
export default {
// 商品列表-查询
skuInfo(params) {
return http.post(`${config.opapiHost}/kdspOp/api/kdsp/activity/activity-goods/sku-info/list`, params, {
accessToken: true
});
},
// 获取商品类目
categoryQuery() {
return http.get(`${config.opapiHost}/kdspOp/api/kdsp/op/rear-category/query/all`, {
accessToken: true
});
},
// 优惠券搜索列表
couponList(params) {
return http.post(`${config.opapiHost}/kdspOp/api/kdsp/op/coupon/list`, params, {
accessToken: true
});
},
// 商品管理-商品专题列表_分页
specialPage(params) {
return http.post(`${config.opapiHost}/kdspOp/api/kdsp/activity/activity-goods/special/list-page`, params, {
accessToken: true
});
},
// todo: 对外接口需提供x-auth-token
// 购物车-添加商品
addShopCart(params) {
return http.post(`${config.kdspHost}/api/kdsp/shop-cart/add-update`, params);
},
// 商品组或专题查询
getGoods(params) {
// return http.post(`${config.kdspHost}/api/kdsp/activity/activity-goods-special/skus`, params);
return {
skus: [
{
skuNo: '100014565800',
skuName: '【自营】【自营】小米手机 陶瓷黑 8GB+128GB 官方标配',
skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
salePrice: 2.85,
marketPrice: 293
},
{
skuNo: '100014565820',
skuName: '【自营】[自营][自营]0',
skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
salePrice: 264,
marketPrice: 300
},
{
skuNo: '100014565820',
skuName: '【自营】[自营][自营]0',
skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
salePrice: 264,
marketPrice: 300
},
{
skuNo: '100014565800',
skuName: '【自营】【自营】小米手机 陶瓷黑 8GB+128GB 官方标配',
skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
salePrice: 2.85,
marketPrice: 293
},
{
skuNo: '100014565820',
skuName: '【自营】[自营][自营]0',
skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
salePrice: 264,
marketPrice: 300
},
{
skuNo: '100014565820',
skuName: '【自营】[自营][自营]0',
skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
salePrice: 264,
marketPrice: 300
}
]
};
},
// 根据id查询优惠券列表
getCoupons(params) {
// return http.post(`${config.kdspHost}/api/kdsp/coupon/list`, params, {
// hideToken: true,
// headers: {
// 'x-auth-token': '97466ed6-ec03-452e-be8f-763a8ffafefe'
// }
// });
return {
coupons: [
{
id: 1,
pickupId: 1,
name: '3C数码会场',
startDate: '2021-02-22',
endDate: '2021-02-22',
faceValue: 100,
limitAmount: 300,
limitDesc: '满300减100',
couponCategory: 1,
iconUrl: 'in esse',
description: 'officia do',
status: 19667180,
navUrl: 'exercitation est',
pickupAble: false,
publishCountFinished: false
},
{
id: 2,
pickupId: 1,
name: '3C数码会场',
startDate: '2021-02-22',
endDate: '2021-02-22',
faceValue: 100,
limitAmount: 300,
limitDesc: '满300减100',
couponCategory: 1,
iconUrl: 'oc',
description: 'minim dolore tempor',
status: 63205995,
navUrl: 'cupidat',
pickupAble: true,
publishCountFinished: false
},
{
id: 3,
pickupId: 1,
name: '3C数码会场',
startDate: '2021-02-22',
endDate: '2021-02-22',
faceValue: 100,
limitAmount: 300,
limitDesc: '满300减100',
couponCategory: 1,
iconUrl: 'eu fugiat commodo voluptate exercitation',
description: 'nisi',
status: 43999314,
navUrl: 'commodo in reprehenderit',
pickupAble: false,
publishCountFinished: true
},
{
id: 4,
pickupId: 1,
name: '3C数码会场',
startDate: '2021-02-22',
endDate: '2021-02-22',
faceValue: 100,
limitAmount: 300,
limitDesc: '满300减100',
couponCategory: 1,
iconUrl: 'eu fugiat commodo voluptate exercitation',
description: 'nisi',
status: 43999314,
navUrl: 'commodo in reprehenderit',
pickupAble: false,
publishCountFinished: true
}
]
};
},
// 领取优惠券
pickupCoupon(params) {
return http.post(`${config.kdspHost}/api/kdsp/coupon/pickup`, params, {
// todo: header里二者参数需特殊处理
headers: {
'x-user-terminal': 'H5',
'vccChannel': '',
}
});
},
};
\ No newline at end of file
...@@ -23,6 +23,7 @@ export default class Layout extends Vue { ...@@ -23,6 +23,7 @@ export default class Layout extends Vue {
@Prop({ type: String, default: 'Vue,TypeScript,Isomorphic' }) keywords?: string; @Prop({ type: String, default: 'Vue,TypeScript,Isomorphic' }) keywords?: string;
isNode: boolean = EASY_ENV_IS_NODE; isNode: boolean = EASY_ENV_IS_NODE;
scriptSrc: string = `<script src="https://activitystatic.lkbang.net/sa-sdk-javascript/1.15.16/sensorsdata.min.js"></script>`;
created() { created() {
console.log('>>EASY_ENV_IS_NODE create', EASY_ENV_IS_NODE); console.log('>>EASY_ENV_IS_NODE create', EASY_ENV_IS_NODE);
......
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
<link rel="stylesheet" href="/public/asset/css/reset.css"> <link rel="stylesheet" href="/public/asset/css/reset.css">
</head> </head>
<body> <body>
<div id="app"><slot></slot></div> <div id="app"><slot></slot></div>
<template v-html="scriptSrc"></template>
</body> </body>
</html> </html>
<div v-else-if="!isNode" id="app"><slot></slot></div> <div v-else-if="!isNode" id="app"><slot></slot></div>
......
const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'http'; const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'http';
export default { export default {
apiHost: `http://localhost:7002/`, // apiHost: `http://localhost:7002/`,
// apiHost: `http://192.168.28.199:7001/`, // apiHost: `http://192.168.28.199:7001/`,
// apiHost: 'https://quantum-vcc2.liangkebang.net/', apiHost: 'https://quantum-blocks-vcc2.liangkebang.net/',
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=default`, shenceUrl: `${protocol}//bn.xyqb.com/sa?project=default`,
opapiHost: `https://opapi-vcc2.liangkebang.net`, opapiHost: `https://opapi-vcc2.liangkebang.net`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
kdspHost: 'https://kdsp-api-vcc2.liangkebang.net', kdspHost: 'https://kdsp-api-vcc2.liangkebang.net',
test: true
}; };
// tslint:disable
import { isAndroid, isIOS, isWechat } from '@/service/utils.service';
import localStorage from '@/service/localStorage.service';
import Toast from '@qg/cherry-ui/src/toast';
function getURLSearchParams(json) {
if (!json) return '';
const dataArray = Object.keys(json).map(key => {
if (json[key] === undefined) return '';
return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]);
});
return dataArray.filter(item => item !== undefined && item !== null).join('&');
}
function getVccChannel() {
return isWechat
? localStorage.get('vccChannel') || ''
: (isAndroid ? '159905' : isIOS ? '159904' : localStorage.get('vccChannel')) || '';
}
export default {
service: {
response(res) {
const { data, config } = res;
// api需要返回原始响应
const returnRawData = config.rawData;
if (returnRawData) return [data, null];
const success =
(data.code === 0 && data.business_code === 0) ||
(data.code === '0000' && data.businessCode === '0000');
// 业务完成,返回有效载荷
if (success) {
return [data.data, null];
}
// 接下来处理异常业务
const msg = data.msg || '服务异常';
if (!config.hideToast || !data.noAlert) {
Toast(msg);
}
// 给用户提示信息,明确发生错误
// 请求参数和响应数据都可以控制不提示
const error = new Error(msg);
error.response = data;
console.error(error);
// 业务代码可以根据response进行再处理
return [null, error];
},
request(cfg) {
const { method, hideVccChannel } = cfg;
cfg.headers['X-Auth-Token'] = localStorage.get('vccToken') || '';
if (cfg.customHeader) {
Object.assign(cfg.headers, cfg.customHeader);
}
if (cfg.creditToken) {
cfg.headers['X-Auth-Token'] = localStorage.get('creditToken');
}
if (!hideVccChannel) {
cfg.headers['vccChannel'] = getVccChannel();
}
if (method === 'post' && cfg.emulateJSON) {
cfg.headers['Content-Type'] = 'application/x-www-form-urlencoded';
cfg.data = getURLSearchParams(cfg.data);
}
return cfg;
}
}
// unique: {
// response(res) {
// const { data, config } = res;
// if (data.code === 0) {
// return [data.data, null];
// }
// if (!config.hideToast) {
// Toast(data.msg);
// }
// let error = new Error(data.msg);
// error.response = data;
// console.error(error);
// return [null, error];
// },
// request(cfg) {
// const { method } = cfg;
// cfg.headers['X-Auth-Token'] = localStorage.get('vccToken') || '';
// if (cfg.creditToken) {
// cfg.headers['X-Auth-Token'] = localStorage.get('creditToken');
// }
// cfg.headers['vccChannel'] = getVccChannel();
// if (method === 'post' && cfg.emulateJSON) {
// cfg.headers['Content-Type'] = 'application/x-www-form-urlencoded';
// cfg.data = getURLSearchParams(cfg.data);
// }
// return cfg;
// }
// }
};
...@@ -2,8 +2,7 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https'; ...@@ -2,8 +2,7 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
export default { export default {
apiHost: `https://quantum-blocks.q-gp.com/`, apiHost: `https://quantum-blocks.q-gp.com/`,
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`, shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`,
opapiHost: `${protocol}//opapi.q-gp.com`, opapiHost: `${protocol}//opapi.q-gp.com`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`, test: false
}; };
...@@ -3,7 +3,6 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https'; ...@@ -3,7 +3,6 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
export default { export default {
apiHost: `https://quantum-blocks-vcc2.liangkebang.net/`, apiHost: `https://quantum-blocks-vcc2.liangkebang.net/`,
opApiHost: 'https://opapi-vcc2.liangkebang.net', opApiHost: 'https://opapi-vcc2.liangkebang.net',
qiniuHost: `https://appsync.lkbang.net/`, shenceUrl: `${protocol}//bn.xyqb.com/sa?project=default`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`, test: true
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
}; };
import Vue from 'vue'; import Vue from 'vue';
import { sync } from 'vuex-router-sync'; import { sync } from 'vuex-router-sync';
import initService from '@/service/init.service';
export default class App { export default class App {
config: any; config: any;
...@@ -21,6 +22,11 @@ export default class App { ...@@ -21,6 +22,11 @@ export default class App {
const store = createStore(initState); const store = createStore(initState);
const router = createRouter(); const router = createRouter();
sync(store, router); sync(store, router);
if (EASY_ENV_IS_BROWSER) {
const { initSa } = require('@/service/sa.service');
initService.init(router);
initSa(router);
}
return { return {
router, router,
store, store,
......
<template>
<div :class="['ad', { 'ad_one': column === 1, 'ad_two': column === 2, 'ad_three': column === 3 }]">
<a :href="item.link" v-for="(item, index) in list" :key="index">
<cr-image :src="item.img" height="2.61rem" width="100%"></cr-image>
</a>
</div>
</template>
<script>
export default {
props: {
column: {
type: Number,
default: 2
},
list: {
type: Array,
default: () => ([])
}
}
}
</script>
<style lang="less" scoped>
.image-width(@width) {
a {
width: @width;
}
}
.ad {
display: flex;
justify-content: space-between;
margin: 0 12px;
&_one {
.image-width(100%);
}
&_two {
.image-width(49%);
}
&_three {
.image-width(32%);
}
}
</style>
\ No newline at end of file
<template>
<div class="coupon" v-if="column === 1">
<div class="coupon-item_one" :style="style" :key="coupon.id" v-for="coupon in couponList">
<div class="Gi_one-left">
<p>¥<span>{{ coupon.faceValue || '-' }}</span></p>
<p>{{coupon.limitDesc}}</p>
</div>
<div class="Gi_one-middle">
<p>{{ coupon.name }}</p>
<p>{{`${coupon.startDate} - ${coupon.endDate}`}}</p>
</div>
<cr-button class="Gi_one-right" type="primary" @click="getCoupon(coupon)">{{btnText(coupon.pickupAble)}}</cr-button>
<div class="coupon-item-mask" v-if="!coupon.pickupAble && coupon.publishCountFinished">
<p>已抢空</p>
</div>
</div>
</div>
<div v-else :class="['coupon', {'coupon_two': column === 2, 'coupon_multiple': column === 3}]">
<div class="coupon-item" :style="style" :key="coupon.id" v-for="coupon in couponList">
<p class="coupon-item-title">{{ coupon.name }}</p>
<p class="coupon-item-amount">¥<span>{{ coupon.faceValue || '-' }}</span></p>
<p class="coupon-item-amount">{{coupon.limitDesc}}</p>
<cr-button shape="circle" type="primary" @click="getCoupon(coupon)">
{{btnText(coupon.pickupAble)}}
</cr-button>
<div class="coupon-item-mask" v-if="!coupon.pickupAble && coupon.publishCountFinished">
<p>已抢空</p>
</div>
</div>
</div>
</template>
<script>
import operationApi from '@api/operation.api';
export default {
inject: ['editor'],
props: {
couponsList: Array,
couponsNumber: Number,
column: {
type: Number,
default: 3
},
bgImage: String,
bgColor: {
type: String,
default: '#fff'
}
},
data() {
return {
list: []
}
},
computed: {
couponList() {
if (this.editor) this.$nextTick(() =>this.editor.adjustHeight());
return this.list.slice(0, this.couponsNumber);
},
style() {
return {
background: this.bgImage ? `url(${this.bgImage}) no-repeat 0 0 / cover` : '',
backgroundColor: this.bgColor
}
}
},
watch: {
couponsList: {
handler: async function (newVal) {
const { coupons } = await operationApi.getCoupons({ couponIds: newVal.join(',') });
if(coupons && coupons.length) this.list = coupons;
// console.log('coupons', this.list);
},
immediate: true
}
},
methods: {
async getCoupon(coupon) {
if (!coupon.pickupAble) {
await operationApi.pickupCoupon({ couponId: coupon.couponId });
this.$toast.success('领取成功');
} else {
window.location.href = coupon.navUrl;
}
},
btnText(pickupAble) {
return pickupAble ? '去使用' : '立即领取';
}
}
}
</script>
<style lang="less" scoped>
::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.coupon {
width: 100%;
padding: 0 12px;
box-sizing: border-box;
font-size: 0;
&-item {
position: relative;
padding: 4px 8px;
display: inline-flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
height: 88px;
text-align: center;
background: url('./images/coupon-bg@2x.png') no-repeat 0 0 / cover;
font-size: 12px;
line-height: 18px;
&:last-child {
margin-right: 0;
}
&-title {
color: #EC1500;
}
&-amount {
color: #EC1500;
span {
font-size: 20px;
line-height: 26px;
font-weight: 600;
}
}
&-condition {
color: #999999;
}
&-mask {
position: absolute;
width: 100%;
height: 100%;
background: #fff;
opacity: 0.7;
p {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 66px;
height: 66px;
line-height: 66px;
text-align: center;
border-radius: 50%;
background-color: #000;
color: #fff;
font-weight: 600;
}
}
}
&_multiple {
overflow-x: auto;
white-space: nowrap;
padding: 0 4px;
.coupon-item {
width: 114px;
margin-right: 4px;
}
}
&_two {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.coupon-item {
margin-bottom: 8px;
width: 170px;
&_empty {
padding: 0;
margin: 0;
height: 0;
}
}
}
&-item_one {
position: relative;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
.Gi_one-left {
position: relative;
padding: 8px 0;
display: flex;
flex-direction: column;
justify-content: space-around;
width: 90px;
&:after {
content: "";
position: absolute;
right: 0;
height: 100%;
border-style: dashed;
border-width: 0 1px 0 0;
border-color: #EC3333;
transform:scaleX(.5);
}
p {
line-height: 18px;
color: #EC3333;
span {
font-size: 20px;
line-height: 26px;
font-weight: 600;
}
}
}
.Gi_one-middle {
flex: 1;
padding: 8px 0;
margin-left: 8px;
text-align: left;
p {
&:first-child {
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
&:last-child {
line-height: 24px;
}
}
}
.Gi_one-right {
width: 56px;
height: 80px !important;
}
}
// 组件库替换变量处理
/deep/ .cr-button--primary {
height: 20px;
line-height: 20px;
border: 0;
font-size: 12px;
background: linear-gradient(269deg, #ff5d00 12%, #ff1900 86%);
}
}
</style>
\ No newline at end of file
<template>
<div class="top-bar">
<img class="top-bar-left" :src="leftImg" />
<div class="top-bar-right">
<a :href="href">
<button class="top-bar-button">打开APP</button>
</a>
<img class="top-bar-close" @click="close" src="./images/close@2x.png" />
</div>
</div>
</template>
<script>
export default {
name: "DownloadGuide",
props: {
href: String,
leftImg: String
},
methods: {
close() {
this.$emit('delete');
}
}
}
</script>
<style lang="less" scoped>
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
background-color: #d7e6ff;
padding: 0 20px 0 15px;
.top-bar-left {
width: 92px;
height: 24px;
}
.top-bar-right {
display: flex;
align-items: center;
a {
margin-right: 15px;
font-size: 0;
.top-bar-button {
width: 70px;
height: 24px;
line-height: 24px;
border-radius: 24px !important;
background-color: #5573eb !important;
font-size: 13px;
}
}
.top-bar-close {
width: 20px;
height: 20px;
}
}
}
</style>
\ No newline at end of file
import Http from '@/service/http.service.ts';
import Cookies from '@/service/cookieStorage.service.ts';
import localStorage from '@/service/localStorage.service.ts';
import qs from 'qs';
// const ApiBaseUrl = 'http://passportapi.q-gp.com';
const ApiBaseUrl = 'https://passportapi-fe.liangkebang.net';
export default {
getCaptcha() {
return Http.post(ApiBaseUrl + '/api/captcha');
},
sendVcode(phone, captcha, captchaId) {
const params = {
phoneNo: phone,
captchaId,
captchaValue: captcha,
};
return Http.post(ApiBaseUrl + '/api/sms/send_login_code_new_forH5', qs.stringify(params));
},
sendGtVcode(gt) {
return Http.post(ApiBaseUrl + '/api/sms/send_login_code_new_forH5', qs.stringify(gt));
},
fastLogin(phone, vcode, clickId, uniqueKey, registerFrom = 1) {
const headers = {
Authorization: 'Verification ' + btoa(phone + ':' + vcode),
};
const params = {
channelId: 1,
createdFrom: registerFrom,
key: 'xyqb',
btRegisterChannelId: '',
dimension: '',
click_id: clickId,
uniqueKey,
};
return Http.post(ApiBaseUrl + '/user/login/fastV1', qs.stringify(params), {
headers,
});
},
// 判断极验还是qg
verifyMode(clientType, phoneNo) {
return Http.post(ApiBaseUrl + '/api/captcha/new', { clientType, phoneNo });
},
goNext(params) {
return Http.post(ApiBaseUrl + '/platform/api/page/next', qs.stringify(params));
}
};
<template>
<cr-form
class="login-form"
ref="loginForm"
show-error
validate-trigger="onBlur"
@submit="login"
@failed="onFormFailed"
>
<cr-field
v-model="formData.phoneNo"
name="phoneNo"
type="tel"
label=""
placeholder="请填写您的手机号"
:rules="validOptions[0]"
/>
<cr-field
v-if="captchaShow"
class="send-sms-field"
v-model="formData.captcha"
name="captcha"
type="digit"
label=""
placeholder="请填写图片验证码"
:rules="validOptions[1]"
>
<template slot="button">
<img :src="captchaUrl" @click="getCaptcha()" class="captchaUrl" />
</template>
</cr-field>
<cr-field
class="send-sms-field"
v-model="formData.verifyCode"
name="verifyCode"
type="tel"
label=""
placeholder="请填写验证码"
:rules="validOptions[1]"
>
<template slot="button">
<a
href="javascript:;"
:style="vcStyles"
:class="['login-form-send', { 'not-allow': noAllowSend || showCount }]"
ref="sendVcode"
>
{{ captchaBtnText }}
</a>
</template>
</cr-field>
<cr-button :color="btnColor" :style="btnStyles" shape='circle' block native-type="submit" class="login-form-submit">
{{ btnTxt }}
</cr-button>
</cr-form>
</template>
<script>
import Vue from 'vue';
import Storage from '@/service/localStorage.service.ts';
import Cookies from '@/service/cookieStorage.service.ts';
import { isPhone } from '@/service/utils.service.ts';
import Api from './api/api.ts';
import config from '@/config/index';
const validOptions = [
[
{ required: true, message: "请填写您的手机号" },
{ validator: isPhone, message: "请填写正确的手机号码" }
],
[{ required: true, message: "请填写验证码" }]
];
let timer = null;
let sa = null;
export default {
name: "KALoginForm",
props: {
btnTxt: String,
btnColor: {
type: String,
default: '#000'
},
btnImage: String,
vcBgColor: String,
registerFrom: String,
},
data() {
return {
showCount: false,
count: null,
captchaUrl: "",
captchaShow: false,
gtShow: false,
captchaId: "",
gt: {
clientType: "h5",
verifyType: "gt",
geetest_challenge: "",
geetest_validate: "",
geetest_seccode: "",
phoneNo: ""
},
uniqueKey: "",
formData: {
phoneNo: "",
verifyCode: "",
captcha: ""
},
validOptions
};
},
computed: {
noAllowSend() {
return !isPhone(this.formData.phoneNo);
},
captchaBtnText() {
return this.showCount ? `剩余${this.count}秒` : '获取验证码';
},
btnStyles() {
return this.btnImage ? {
background: `url(${this.btnImage}) no-repeat 0 0 / 100%`,
} : {};
},
vcStyles() {
return { backgroundColor: this.vcBgColor };
}
},
mounted() {
require('./jy/jy.ts');
this.initSa();
this.verifyMode();
},
methods: {
initSa() {
sa = require('sa-sdk-javascript');
//神策数据埋点
sa.init({
server_url: config.shenceUrl,
heatmap: {
//是否开启点击图,默认 default 表示开启,自动采集 $WebClick 事件,可以设置 'not_collect' 表示关闭
clickmap:'default',
//是否开启触达注意力图,默认 default 表示开启,自动采集 $WebStay 事件,可以设置 'not_collect' 表示关闭
scroll_notice_map:'default',
//判断外层是否有 App 的 SDK ,如果有的话,会往 App 的 SDK 发数据。如果没有,就正常发送数据。
// use_app_track: true,
},
show_log: process.env.NODE_ENV !== 'production'
});
//添加公共属性
sa.registerPage({platformType:'H5'});
this.$nextTick(() => {
sa.quick("autoTrackSinglePage");
});
},
async verifyMode() {
const res = await Api.verifyMode(this.gt.clientType, this.formData.phoneNo);
this.uniqueKey = res?.uniqueKey;
if (res?.verifyType === 'gt') {
this.getJyParam(res);
} else {
this.getCaptcha();
}
},
getJyParam({ gt, challenge, success }) {
var vm = this;
initGeetest(
{
width: "100%",
gt: gt,
challenge: challenge,
offline: !success,
product: "bind"
},
function(captchaObj) {
vm.$refs.sendVcode.addEventListener('click', function() {
if (!isPhone(vm.formData.phoneNo)) {
vm.$notify({ type: "warning", message: "请填写正确的手机号码" });
return;
}
if (vm.showCount) return;
captchaObj.verify();
});
captchaObj.onSuccess(function() {
var result = captchaObj.getValidate();
vm.gt.geetest_challenge = result.geetest_challenge;
vm.gt.geetest_validate = result.geetest_validate;
vm.gt.geetest_seccode = result.geetest_seccode;
vm.getCode(captchaObj);
});
captchaObj.onError(function() {
vm.$toast("尝试过多,请点击重试");
});
}
);
},
async getCaptcha() {
this.captchaShow = true;
const res = await Api.getCaptcha();
({ image: this.captchaUrl, imageId: this.captchaId } = res);
},
async getCode(captchaObj) {
if (!isPhone(this.formData.phoneNo)) {
this.$notify({ type: "warning", message: "请填写正确的手机号码" });
return;
}
if (timer) return;
try {
await this.sendCode();
this.smsCount();
this.$notify({ type: "success", message: "短信验证码已发送,请注意查收" });
} finally {
this.resetJyState(captchaObj);
}
},
async sendCode() {
const { phoneNo, captcha } = this.formData;
if (this.captchaShow) {
await Api.sendVcode(phoneNo, captcha, this.captchaId);
} else {
await Api.sendGtVcode({ ...this.gt, phoneNo: phoneNo });
}
},
smsCount() {
this.showCount = true;
this.count = 60;
timer = setInterval(() => {
if (this.count > 0 && this.count <= 60) {
this.count--;
} else {
this.showCount = false;
clearInterval(timer);
timer = null;
}
}, 1000);
},
resetJyState(captchaObj) {
if(captchaObj) captchaObj.reset();
},
async login() {
let le_is_login_success = false;
try {
const { phoneNo, verifyCode } = this.formData;
const user = await Api.fastLogin(phoneNo, verifyCode, null, this.uniqueKey, this.registerFrom);
this.saveInfo(user);
le_is_login_success = true;
this.goNext();
} finally {
sa.track('LoginEvent', {
le_login_action: '注册/登录',
le_login_type: '验证码登录',
le_is_login_success,
le_channel_code: this.registerFrom,
});
}
},
async goNext() {
const extData = { redirectURL: null, productId: 1 };
const params = {
key: 'xyqb',
target: 'cashTarget4',
currentPage: 'login'
};
const { transition } = await Api.goNext(params);
window.location.replace(
transition + '?phoneNo=' + Storage.get('phoneNo') + '&protocol=' + window.location.protocol + '&token=' + Storage.get('token') + (extData ? '&extData=' + btoa(JSON.stringify(extData)) : '')
);
},
onFormFailed(errorInfo) {
const { errors } = errorInfo;
this.$notify({ type: "warning", message: errors[0].message });
},
saveInfo({ token, phoneNo, uuid }) {
Storage.set('token', token);
Cookies.set('phoneNo', phoneNo);
if (uuid) {
Cookies.set('userid', uuid);
sa.login(uuid);
}
}
},
};
</script>
<style lang="less">
@import './jy/jy.less';
.login-form {
padding: 0 20px;
width: 100%;
.cr-field--error-message {
display: none;
}
.cr-cell {
&__value {
overflow: initial;
}
&-group {
&::after {
display: none;
}
}
background-color: #fff;
border-radius: 8px;
margin-bottom: 12px;
align-items: center;
padding: 12.5px 16px;
&::after {
display: none;
}
&__title {
color: #333333;
display: flex;
align-items: center;
}
}
&-btn {
width: 90px;
position: relative;
}
&-send {
padding: 5px 10px 4px;
border-radius: 8px;
background-color: #97d5ff;
color: #fff;
&.not-allow {
background-color: #eaeaea !important;
color: #999;
}
}
&-submit {
height: 50px !important;
border-radius: 8px !important;
border: 0 !important;
}
}
.send-sms-field {
padding: 8px 6px 8px 15px !important;
}
</style>
//覆盖弹出框 滑动模块形式
.geetest_wind.geetest_panel .geetest_panel_box.geetest_panelshowslide{
width: 278px !important;
height: 285px !important;
}
.geetest_holder.geetest_mobile.geetest_ant.geetest_popup .geetest_popup_box {
width: 278px !important;
height: 285px !important;
max-width:none !important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_slider .geetest_slider_track .geetest_slider_tip {
font-size: 14px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_panel .geetest_copyright .geetest_copyright_tip {
font-size: 11px!important;
}
.geetest_holder.geetest_wind .geetest_radar_btn, .geetest_holder.geetest_wind .geetest_success_btn {
height: 44px!important;
}
.geetest_holder.geetest_wind .geetest_radar_tip, .geetest_holder.geetest_wind .geetest_success_radar_tip {
height: 44px!important;
line-height: 44px!important;
font-size: 14px!important;
// padding: 0 0 0 46px!important;
}
.geetest_holder.geetest_wind {
height: 44px!important;
min-width: 260px!important;
}
.geetest_holder.geetest_wind .geetest_logo, .geetest_holder.geetest_wind .geetest_success_logo {
width: 20px!important;
height: 20px!important;
top: 11px!important;
right: 12px!important;
}
.geetest_holder.geetest_wind .geetest_wait .geetest_wait_dot {
width: 20px!important;
height: 20px!important;
margin: 2px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_widget .geetest_window .geetest_result.geetest_fail {
height: 24px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_widget .geetest_window .geetest_result {
height: 24px!important;
bottom: -25px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_widget .geetest_window .geetest_showTip {
bottom:0px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_widget .geetest_window .geetest_result .geetest_result_content {
font-size: 14px!important;
text-indent: 16px!important;
line-height: 24px!important;
height: 24px!important;
bottom: -25px!important;
}
// 点击形式
.geetest_wind.geetest_panel .geetest_panel_box.geetest_panelshowclick {
width: 320px !important;
height: 410px !important;
}
.geetest_holder.geetest_silver.geetest_popup .geetest_popup_box {
min-width: 210px!important;
max-width: 348px!important;
}
.geetest_holder.geetest_silver .geetest_head .geetest_tips {
font-size: 16px!important;
}
.geetest_holder.geetest_silver .geetest_head .geetest_tips .geetest_tip_content {
font-size: 16px!important;
height: 20px!important;
}
.geetest_holder.geetest_silver .geetest_head .geetest_tips .geetest_tip_img {
width: 116px!important;
height: 40px!important;
right: -116px!important;
top: -10px!important;
}
.geetest_holder.geetest_silver .geetest_panel .geetest_small {
height: 23px!important;
margin-top: -11.5px!important;
}
.geetest_holder.geetest_silver .geetest_panel a.geetest_close, .geetest_holder.geetest_silver .geetest_panel a.geetest_refresh, .geetest_holder.geetest_silver .geetest_panel a.geetest_feedback{
height: 23px!important;
width: 23px!important;
}
.geetest_holder.geetest_silver .geetest_panel .geetest_commit .geetest_commit_tip {
margin-top: -10px!important;
height: 20px!important;
line-height: 20px!important;
font-size: 16px!important;
letter-spacing: 2px!important;
}
.geetest_holder.geetest_silver .geetest_panel .geetest_commit {
border-radius: 2px!important;
}
.geetest_holder.geetest_silver .geetest_table_box .geetest_result_tip {
bottom: -29px!important;
height: 28px!important;
text-indent: 9px!important;
font-size: 16px!important;
line-height: 29px!important;
}
.geetest_holder.geetest_silver .geetest_table_box .geetest_result_tip.geetest_up {
bottom:0px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_panel .geetest_copyright {
display: none!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_panel a.geetest_feedback {
display: none!important;
}
.geetest_holder.geetest_wind.geetest_radar_error .geetest_logo {
display: none!important;
}
.geetest_holder.geetest_silver .geetest_panel a.geetest_feedback {
display: none!important;
}
.geetest_holder.geetest_wind .geetest_logo, .geetest_holder.geetest_wind .geetest_success_logo {
display: none!important;
}
.geetest_holder.geetest_silver .geetest_table_box .geetest_window .geetest_item .geetest_big_mark .geetest_mark_no {
// height: 24px!important;
// line-height: 24px!important;
font-size: 18px!important;
}
.geetest_holder.geetest_wind .geetest_radar_tip, .geetest_holder.geetest_wind .geetest_success_radar_tip {
padding: 0!important;
}
// tslint:disable
/* initGeetest 1.0.0
* 用于加载id对应的验证码库,并支持宕机模式
* 暴露 initGeetest 进行验证码的初始化
* 一般不需要用户进行修改
*/
(function(global, factory) {
'use strict';
if (typeof module === 'object' && typeof module.exports === 'object') {
// CommonJS
module.exports = global.document ?
factory(global, true) :
function(w) {
if (!w.document) {
throw new Error('Geetest requires a window with a document');
}
return factory(w);
};
} else {
factory(global);
}
})(typeof window !== 'undefined' ? window : this, function(window, noGlobal) {
'use strict';
if (typeof window === 'undefined') {
throw new Error('Geetest requires browser environment');
}
const document = window.document;
const Math = window.Math;
const head = document.getElementsByTagName('head')[0];
function _Object(obj) {
this._obj = obj;
}
_Object.prototype = {
_each(process) {
const _obj = this._obj;
for (const k in _obj) {
if (_obj.hasOwnProperty(k)) {
process(k, _obj[k]);
}
}
return this;
}
};
function Config(config) {
const self = this;
new _Object(config)._each(function(key, value) {
self[key] = value;
});
}
Config.prototype = {
api_server: 'api.geetest.com',
protocol: 'http://',
type_path: '/gettype.php',
fallback_config: {
slide: {
static_servers: ['static.geetest.com', 'dn-staticdown.qbox.me'],
type: 'slide',
slide: '/static/js/geetest.0.0.0.js'
},
fullpage: {
static_servers: ['static.geetest.com', 'dn-staticdown.qbox.me'],
type: 'fullpage',
fullpage: '/static/js/fullpage.0.0.0.js'
}
},
_get_fallback_config() {
const self = this;
if (isString(self.type)) {
return self.fallback_config[self.type];
} else if (self.new_captcha) {
return self.fallback_config.fullpage;
} else {
return self.fallback_config.slide;
}
},
_extend(obj) {
const self = this;
new _Object(obj)._each(function(key, value) {
self[key] = value;
});
}
};
const isNumber = function(value) {
return (typeof value === 'number');
};
const isString = function(value) {
return (typeof value === 'string');
};
const isBoolean = function(value) {
return (typeof value === 'boolean');
};
const isObject = function(value) {
return (typeof value === 'object' && value !== null);
};
const isFunction = function(value) {
return (typeof value === 'function');
};
const callbacks = {};
const status = {};
const random = function() {
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
};
const loadScript = function(url, cb) {
const script = document.createElement('script');
script.charset = 'UTF-8';
script.async = true;
script.onerror = function() {
cb(true);
};
let loaded = false;
script.onload = script.onreadystatechange = function() {
if (!loaded &&
(!script.readyState ||
'loaded' === script.readyState ||
'complete' === script.readyState)) {
loaded = true;
setTimeout(function() {
cb(false);
}, 0);
}
};
script.src = url;
head.appendChild(script);
};
const normalizeDomain = function(domain) {
return domain.replace(/^https?:\/\/|\/$/g, '');
};
const normalizePath = function(path) {
path = path.replace(/\/+/g, '/');
if (path.indexOf('/') !== 0) {
path = '/' + path;
}
return path;
};
const normalizeQuery = function(query) {
if (!query) {
return '';
}
let q = '?';
new _Object(query)._each(function(key, value) {
if (isString(value) || isNumber(value) || isBoolean(value)) {
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
}
});
if (q === '?') {
q = '';
}
return q.replace(/&$/, '');
};
const makeURL = function(protocol, domain, path, query) {
domain = normalizeDomain(domain);
let url = normalizePath(path) + normalizeQuery(query);
if (domain) {
url = protocol + domain + url;
}
return url;
};
const load = function(protocol, domains, path, query, cb) {
const tryRequest = function(at) {
const url = makeURL(protocol, domains[at], path, query);
loadScript(url, function(err) {
if (err) {
if (at >= domains.length - 1) {
cb(true);
} else {
tryRequest(at + 1);
}
} else {
cb(false);
}
});
};
tryRequest(0);
};
const jsonp = function(domains, path, config, callback) {
if (isObject(config.getLib)) {
config._extend(config.getLib);
callback(config);
return;
}
if (config.offline) {
callback(config._get_fallback_config());
return;
}
const cb = 'geetest_' + random();
window[cb] = function(data) {
if (data.status === 'success') {
callback(data.data);
} else if (!data.status) {
callback(data);
} else {
callback(config._get_fallback_config());
}
window[cb] = undefined;
try {
delete window[cb];
} catch (e) {
}
};
load(config.protocol, domains, path, {
gt: config.gt,
callback: cb
}, function(err) {
if (err) {
callback(config._get_fallback_config());
}
});
};
const throwError = function(errorType, config) {
const errors = {
networkError: '网络错误'
};
if (typeof config.onError === 'function') {
config.onError(errors[errorType]);
} else {
throw new Error(errors[errorType]);
}
};
const detect = function() {
return !!window.Geetest;
};
if (detect()) {
status.slide = 'loaded';
}
const initGeetest = function(userConfig, callback) {
const config = new Config(userConfig);
if (userConfig.https) {
config.protocol = 'https://';
} else if (!userConfig.protocol) {
config.protocol = window.location.protocol + '//';
}
jsonp([config.api_server || config.apiserver], config.type_path, config, function(newConfig) {
const type = newConfig.type;
const init = function() {
config._extend(newConfig);
callback(new window.Geetest(config));
};
callbacks[type] = callbacks[type] || [];
const s = status[type] || 'init';
if (s === 'init') {
status[type] = 'loading';
callbacks[type].push(init);
load(config.protocol, newConfig.static_servers || newConfig.domains, newConfig[type] || newConfig.path, null, function(err) {
if (err) {
status[type] = 'fail';
throwError('networkError', config);
} else {
status[type] = 'loaded';
const cbs = callbacks[type];
for (let i = 0, len = cbs.length; i < len; i = i + 1) {
const cb = cbs[i];
if (isFunction(cb)) {
cb();
}
}
callbacks[type] = [];
}
});
} else if (s === 'loaded') {
init();
} else if (s === 'fail') {
throwError('networkError', config);
} else if (s === 'loading') {
callbacks[type].push(init);
}
});
};
window.initGeetest = initGeetest;
return initGeetest;
});
<template>
<div class="container">
<cr-image width="100%" height="2.4rem" :src="specialRecommend" @click="go('special')" v-if="showSpecial"></cr-image>
<div class="goods" v-if="column === 1">
<div class="goods-item goods-item_one" @click="go('detail', goods.skuNo)" v-for="goods in goodsList" :key="goods.id">
<cr-image width="100%" height="3.15rem" class="goods-item-img" fit="contain" :src="goods.skuUrl"></cr-image>
<div class="goods-item_one-right">
<p class="goods-item-title">{{goods.skuName}}</p>
<div class="goods-item-bottom">
<div class="Gi-bottom-left">
<p>¥{{goods.salePrice || '-'}}</p>
<p>¥{{goods.marketPrice || '-'}}</p>
</div>
<cr-button
shape="circle"
type="primary"
@click="go('detail', goods.skuNo)"
>
立即抢购
</cr-button>
</div>
</div>
</div>
</div>
<div class="goods" v-else>
<div :class="['goods-item', {'goods-item_three': column === 3, 'goods-item_two': column === 2}]" v-for="goods in goodsList" :key="goods.id" @click="go('detail', goods.skuNo)">
<cr-image width="100%" height="3.15rem" class="goods-item-img" fit="contain" :src="goods.skuUrl"></cr-image>
<p class="goods-item-title">{{goods.skuName}}</p>
<div class="goods-item-bottom">
<div class="Gi-bottom-left">
<p>¥{{goods.salePrice || '-'}}</p>
<p>¥{{goods.marketPrice || '-'}}</p>
</div>
<div class="Gi-bottom-right" @click="addShopCart(goods)">
<cr-image width="0.45rem" height="0.45rem" :src="shoppingCart"></cr-image>
</div>
</div>
</div>
<div class="goods-item goods-item_three goods-item_empty" v-if="showEmpty"></div>
</div>
</div>
</template>
<script>
import shoppingCart from './images/shopping-cart@2x.png';
import specialRecommend from './images/special-recommend.png';
import operationApi from '@api/operation.api';
export default {
inject: ['editor'],
props: {
goods: {
type: Object,
default: () => ({
type: 'goodsGroup',
ids: []
})
},
goodsNumber: Number,
column: {
type: Number,
default: 2
},
showSpecial: {
type: Boolean,
default: true
},
specialUrl: String,
specialLink: String,
},
data() {
return {
list: [],
shoppingCart,
specialRecommend
}
},
computed: {
goodsList() {
if (this.editor) this.$nextTick(() =>this.editor.adjustHeight());
return this.list.slice(0, this.goodsNumber);
},
showEmpty() {
return this.column === 3 && this.goodsNumber % 3 === 2;
}
},
watch: {
goods: {
handler: async function (newVal) {
if (newVal.ids.length) {
let records = [];
if(newVal.type === 'goodsGroup') {
({ skus: records } = await operationApi.getGoods({ specialId: newVal.ids[0] }));
} else if (newVal.type === 'goods') {
({ skus: records } = await operationApi.getGoods({ skus: newVal.ids }));
}
if(records.length) this.list = records;
}
// console.log('watch', this.list);
},
immediate: true
}
},
methods: {
go(type, params) {
if (type === 'special' && this.specialLink) {
window.location.href = this.specialLink;
} else if (type === 'goodsDetail') {
window.location.href = `xyqb://homepage/goodsdetail?skuNo=${params}`;
}
},
async addShopCart(goods) {
const { skuId, skuNum, skuSource } = goods;
const params = { skuId, skuNum, skuSource, type: 1 }
await operationApi.addShopCart(params);
this.$toast.success('添加成功');
}
}
}
</script>
<style lang="less" scoped>
.container {
margin: 0 12px;
.goods {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 4px;
font-size: 0;
&-item {
border-radius: 8px;
background: #fff;
margin-bottom: 4px;
overflow: hidden;
&-title {
height: 36px;
font-size: 12px;
line-height: 18px;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-align: left;
}
&-bottom {
display: flex;
justify-content: space-between;
align-items: center;
.Gi-bottom-left {
display: flex;
flex-direction: column;
align-items: center;
p {
max-width: 55px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: left;
&:first-child {
font-size: 15px;
color: #EC3333;
}
&:last-child {
font-size: 13px;
color: #999999;
text-decoration:line-through;
}
}
}
.Gi-bottom-right {
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
background: #FFF3F3;
border-radius: 50%;
/deep/ .cr-image {
width: 18px !important;
height: 18px !important;
}
}
}
&_one {
display: flex;
width: 100%;
.goods-item-img {
width: 112px !important;
height: 109px !important;
}
&-right {
padding: 6px 8px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.Gi-bottom-left {
margin-right: 10px;
flex: 1;
display: flex;
justify-content: space-around;
flex-direction: row;
align-items: flex-end;
p {
max-width: 55px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
// 组件库替换变量处理
/deep/ .cr-button--primary {
height: 24px;
line-height: 24px;
border: 0;
background: linear-gradient(269deg, #ff5d00 12%, #ff1900 86%);
}
}
}
&_two {
width: 49%;
.goods-item-img {
width: 100% !important;
height: 180px !important;
}
.goods-item-title {
margin: 8px 8px;
}
.goods-item-bottom {
margin: 0 8px 6px;
}
}
&_three {
width: 32%;
.goods-item-img {
width: 100% !important;
height: 117px !important;
}
.goods-item-title {
margin: 8px 4px;
}
.goods-item-bottom {
margin: 0 4px 6px;
.goods-item-bottom {
}
}
}
&_empty {
height: 0;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="tabs">
<cr-tabs sticky ref="GoodsTabs" @change="tabsChange" animated :color="underlineColor" :title-active-color="activeColor" :title-inactive-color="inactiveColor">
<cr-tab v-for="(tab, index) in tabs" :key="index" :name="index" :title="tab.tabTitle">
<component :is="tab.name" v-bind="tab.props" :style="transformStyle(tab.commonStyle)"></component>
</cr-tab>
</cr-tabs>
</div>
</template>
<script>
import Goods from '../Goods/index.vue';
import Coupon from '../Coupon/index.vue';
export default {
name: 'goods-tabs',
components: {
Goods,
Coupon
},
props: {
list: {
type: Array,
default: () => []
},
childItem: {
type: Object,
default: () => ({ child: [] })
},
activeColor: {
type: String,
default: '#323233'
},
inactiveColor: {
type: String,
default: '#646566'
},
underlineColor: {
type: String,
default: '#1989fa'
}
},
data() {
return {
}
},
computed: {
tabs() {
const list = this.list.map(tab => {
const rs = { ...tab };
this.childItem?.child.forEach(ele => {
if (tab.componentId === ele.id) Object.assign(rs, tab, ele);
})
return rs;
});
// console.log('tabs', this.childItem.child, list);
return list;
}
},
mounted() {
setTimeout(() => {
this.$refs.GoodsTabs.resize();
})
},
methods: {
tabsChange(name) {
// console.log('change', name);
},
transformStyle(styleObj = {}) {
// console.log('transformStyle', styleObj);
const style = {};
for (const key of Object.keys(styleObj)) {
style[key] = styleObj[key];
if (key === 'backgroundImage' && style.backgroundImage) {
style.background = `url(${style.backgroundImage}) no-repeat 0 0 / cover`;
}
}
return style;
}
}
}
</script>
<style lang="less" scoped>
/deep/ .cr-tabs__nav {
background-color: transparent;
}
/deep/ .cr-swipe-item {
&:nth-child(odd) {
background-color: #66c6f2;
}
&:nth-child(2n) {
background-color: #39a9ed;
}
height: 500px;
}
</style>
\ No newline at end of file
<template>
<div class="guide-cube">
<div v-if="showSwiper" class="guide-cube-swiper" ref="mySwiper" v-swiper:mySwiper="swiperOptions">
<div class="swiper-wrapper">
<div :style="style" :class="{'swiper-slide_two': slidesPerColumn === 2, 'swiper-slide_one': slidesPerColumn === 1, 'swiper-slide': true }" :key="index" v-for="(item, index) in list" @click="go(item.link)">
<cr-image width="" height="" :src="item.img" fit="cover" :show-loading="false"></cr-image>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
import 'swiper/dist/css/swiper.css';
if (EASY_ENV_IS_BROWSER) {
const VueAwesomeSwiper = require('vue-awesome-swiper/dist/ssr')
Vue.use(VueAwesomeSwiper)
}
export default {
inject: ['editor'],
props: {
list: {
type: Array,
default: () => ([])
},
slidesPerColumn: {
type: Number,
default: 1
},
loop: Boolean,
autoplay: Boolean,
animation: Boolean
},
data() {
const vm = this;
let isEven = true;
let lastEvenProgress = 0;
let lastProgress = 0;
return {
showSwiper: false,
// style: {},
swiperOptions: {
loop: this.loop,
slidesPerView: 4,
slidesPerColumn: this.slidesPerColumn,
spaceBetween: 8,
observer:true,
observeParents:true,
watchSlidesProgress: true,
autoplay: this.autoplay,
on: {
progress: function() {
const slidesLength = this.slides.length;
if (!vm.animation) return;
let ratio = Math.abs(this.slides[0].progress % 1).toFixed(10) * 0.2;
if ([0, 1].includes(Math.abs(this.slides[0].progress % 2))) {
isEven = Math.abs(this.slides[0].progress % 2) === 0 ? true : false;
}
if (Math.abs(this.slides[0].progress - lastEvenProgress) > 1) {
lastEvenProgress = Math.floor(this.slides[0].progress);
isEven = !isEven;
}
const isLeft = lastProgress < this.slides[0].progress;
lastProgress = this.slides[0].progress;
for (let i = 0; i < this.slides.length; i++) {
const slide = this.slides.eq(i);
let scale = 1;
if (Math.abs(this.slides[0].progress % 2) === 0) {
scale = vm.memorys[i] ? 0.8 : 1;
} else if(Math.abs(this.slides[0].progress % 2) === 1) {
scale = vm.memorys[i] ? 1 : 0.8;
} else if (isEven) {
if (isLeft) {
if (vm.memorys[i]) {
scale = 0.8 + ratio;
} else {
scale = 1 - ratio;
}
} else {
if (vm.memorys[i]) {
scale = 1 - ratio;
} else {
scale = 0.8 + ratio;
}
}
} else if (!isEven) {
if (isLeft) {
if (vm.memorys[i]) {
scale = 1 - ratio;
} else {
scale = 0.8 + ratio;
}
} else {
if (vm.memorys[i]) {
scale = 0.8 + ratio;
} else {
scale = 1 - ratio;
}
}
}
slide.transform(`scale3d(${scale}, ${scale}, 1)`);
}
}
},
}
}
},
computed: {
swiper() {
return this.$refs.mySwiper.swiper
},
memorys() {
const length = this.list.length;
const baseNumber = this.slidesPerColumn === 1 ? 2 : 4;
return new Array(length).fill(0).map((v, i) => [0, 3].includes(i % baseNumber) ? 1 : 0);
},
style() {
return {
transition: 'all .2s cubic-bezier(.4, 0, .2, 1)'
};
}
},
watch: {
slidesPerColumn(newVal) {
this.refreshSwiper('slidesPerColumn', newVal);
},
autoplay(newVal) {
this.refreshSwiper('autoplay', newVal);
},
loop(newVal) {
this.refreshSwiper('loop', newVal);
},
},
mounted() {
this.showSwiper = true;
},
methods: {
go(url) {
window.location.href = url;
},
refreshSwiper(key, val) {
this.swiperOptions[key] = val;
this.showSwiper= false;
this.$nextTick(() => this.showSwiper= true);
if (this.editor) setTimeout(() =>this.editor.adjustHeight(), 0);
}
},
}
</script>
<style lang="less" scoped>
.guide-cube {
width: 100%;
padding: 12px;
&-swiper {
width: 100%;
.swiper-slide {
display: flex;
justify-content: center;
align-items: center;
width: 81px;
height: 81px;
/deep/ .cr-image {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="marquee">
<div class="marquee-swiper" v-if='showSwiper' ref="mySwiper" v-swiper:mySwiper="swiperOptions">
<div class="swiper-wrapper">
<div class="swiper-slide" :key="index" v-for="(item, index) in list">
<a class="swiper-slide-a" @click="go(item.link)">
<span :style="styles">{{item.text}}</span>
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
import 'swiper/dist/css/swiper.css';
if (EASY_ENV_IS_BROWSER) {
const VueAwesomeSwiper = require('vue-awesome-swiper/dist/ssr')
Vue.use(VueAwesomeSwiper)
}
export default {
inject: ['editor'],
props: {
list: {
type: Array,
default: () => ([])
},
fontColor: {
type: String,
default: '#666666'
}
},
data() {
return {
showSwiper: false,
swiperOptions: {
loop: true,
observer:true,
observeParents:true,
direction: 'vertical',
allowTouchMove: false,
autoplay: true
}
}
},
computed: {
swiper() {
return this.$refs.mySwiper.swiper
},
styles() {
return {
color: this.fontColor
}
}
},
watch: {
list: {
deep: true,
handler() {
this.refreshSwiper();
}
}
},
mounted() {
this.showSwiper = true;
},
methods: {
go(link) {
window.location.href = link;
},
refreshSwiper() {
console.log('refreshSwiper');
this.showSwiper= false;
this.$nextTick(() => this.showSwiper= true);
}
}
}
</script>
<style lang="less" scoped>
.marquee {
width: 100%;
height: 30px;
border-radius: 8px;
background-color: #D8D8D8;
.swiper-container {
height: 100%;
.swiper-slide {
display: flex;
justify-content: center;
align-items: center;
a {
display: flex;
justify-content: center;
align-items: center;
}
span {
font-size: 14px;
line-height: 20px;
color: #666666;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="placeholder" :style="style"></div>
</template>
<script>
export default {
props: {
height: Number,
},
computed: {
style() {
return {
height: `${this.height}px`
}
}
}
}
</script>
<style lang="less" scoped>
.placeholder {
min-height: 10px;
}
</style>
\ No newline at end of file
This diff is collapsed.
import { Component, Prop, Vue, Mixins } from 'vue-property-decorator'; import { Component, Prop, Vue, Mixins } from 'vue-property-decorator';
import { Action, Mutation, State, Getter } from 'vuex-class'; import { Action, Mutation, State, Getter } from 'vuex-class';
import TransformStyleMixin from '@/page/mixins/transformStyle.mixin'; import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
import CustomMarquee from '@/lib/Marquee/index.vue';
import { resizeDiv } from '@/service/utils.service'; import { resizeDiv } from '@/service/utils.service';
@Component({ components: { CustomMarquee }, name: 'FreedomContainer' }) @Component({ name: 'FreedomContainer' })
export default class FreedomContainer extends Mixins(TransformStyleMixin) { export default class FreedomContainer extends Mixins(TransformStyleMixin) {
@Getter('pageData') pageData; @Getter('pageData') pageData;
@State(state => state.activity.curChildIndex) curChildIndex; @State(state => state.activity.curChildIndex) curChildIndex;
......
import { Vue, Component, Watch, Provide, Mixins } from 'vue-property-decorator'; import { Vue, Component, Watch, Provide, Mixins } from 'vue-property-decorator';
import { Getter, State, Mutation } from 'vuex-class';
import FreedomContainer from '../../component/FreedomContainer/index.vue'; import FreedomContainer from '../../component/FreedomContainer/index.vue';
import GridLayout from '../../component/VueGridLayout/GridLayout.vue'; import GridLayout from '../../component/VueGridLayout/GridLayout.vue';
import GridItem from '../../component/VueGridLayout/GridItem.vue'; import GridItem from '../../component/VueGridLayout/GridItem.vue';
// import LoginForm from '@/lib/Form/index.vue';
import DownloadGuide from '@/lib/DownloadGuide/index.vue';
import { Getter, State, Mutation } from 'vuex-class';
import GuideCube from '@/lib/GuideCube/index.vue';
import GoodsTabs from '@/lib/GoodsTabs/index.vue';
import Coupon from '@/lib/Coupon/index.vue';
import Advertisement from '@/lib/Advertisement/index.vue';
import TransformStyleMixin from '@/page/mixins/transformStyle.mixin'; import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
import { getStyle } from '@/service/utils.service'; import { getStyle } from '@/service/utils.service';
@Component({ components: { FreedomContainer, GridLayout, GridItem, DownloadGuide, GoodsTabs, GuideCube, Advertisement, Coupon }, name: 'Activity'}) @Component({ components: { FreedomContainer, GridLayout, GridItem }, name: 'Activity'})
export default class Activity extends Mixins(TransformStyleMixin) { export default class Activity extends Mixins(TransformStyleMixin) {
@Getter('pageData') pageData; @Getter('pageData') pageData;
@State(state => state.activity.pageInfo.pageName) pageName; @State(state => state.activity.pageInfo.pageName) pageName;
......
...@@ -17,7 +17,7 @@ export default class EditorModule implements Module<EditorState, RootState> { ...@@ -17,7 +17,7 @@ export default class EditorModule implements Module<EditorState, RootState> {
actions: ActionTree<EditorState, RootState> = { actions: ActionTree<EditorState, RootState> = {
async getPageDate({ commit }, condition) { async getPageDate({ commit }, condition) {
const res = await api.getPageById(condition); const [res] = await api.getPageById(condition);
const { page, ...rest } = res as PageInfo; const { page, ...rest } = res as PageInfo;
commit(SET_PAGE_INFO, { ...rest, page: JSON.parse(page as string) }); commit(SET_PAGE_INFO, { ...rest, page: JSON.parse(page as string) });
} }
......
import Vue from 'vue';
export default new Vue();
\ No newline at end of file
// tslint:disable
// import store from '@/store';
import Toast from '@qg/cherry-ui/src/toast';
import axios from 'axios'; import axios from 'axios';
import basicConfig from '../config'; import strategyModes from '@/config/http.config';
import localStorage from './localStorage.service';
// import { Notify } from '@qg/cherry-ui';
import Notify from '@qg/cherry-ui/src/notify';
const ERR_MESSAGE_MAP = { const ERR_MESSAGE_MAP = {
status: { status: {
400: '错误请求', 400: '错误请求',
401: '未授权,请重新登录', 401: '您未登录或登录超时,请重新登录',
403: '拒绝访问', 403: '拒绝访问',
404: '请求错误,未找到该资源', 404: '请求错误,未找到该资源',
405: '请求方法未允许', 405: '请求方法未允许',
...@@ -20,125 +20,102 @@ const ERR_MESSAGE_MAP = { ...@@ -20,125 +20,102 @@ const ERR_MESSAGE_MAP = {
505: 'http版本不支持该请求' 505: 'http版本不支持该请求'
} }
}; };
const defaultStratege = {
default: {
request(cfg) {
return cfg;
},
response(res) {
return res.data;
}
}
};
const config = {
timeout: 15000
};
const CancelToken = axios.CancelToken; const CancelToken = axios.CancelToken;
const instance = axios.create(config);
const pending = {}; const pending = {};
let reqNum = 0; let reqNum = 0;
axios.defaults.baseURL = basicConfig.apiHost; let timeId = null;
axios.defaults.timeout = 30000; strategyModes.default = defaultStratege;
axios.defaults.xsrfHeaderName = 'x-csrf-token';
axios.defaults.xsrfCookieName = 'csrfToken';
const wait = async seconds => new Promise(resolve => setTimeout(resolve, seconds));
// 体验优化暂时下掉loading
function beforeRequest() { function beforeRequest() {
reqNum++; reqNum++;
// store.dispatch("setIsLoading", true); clearTimeout(timeId);
timeId = setTimeout(() => {
// store.dispatch('change_loading', true);
}, 1300);
} }
function afterRequest() { function afterRequest() {
clearRequest();
// setTimeout(clearRequest, 300);
}
function clearRequest() {
reqNum--; reqNum--;
if (reqNum <= 0) { if (reqNum <= 0) {
// store.dispatch("setIsLoading", false); clearRequest();
// store.dispatch('change_loading', false);
} }
} }
const instance = axios.create(); function clearRequest() {
clearTimeout(timeId);
}
// 请求拦截器 // 请求拦截器
instance.interceptors.request.use( instance.interceptors.request.use(
config => { config => {
// op-api识别API使用 !config.hideLoading && beforeRequest(config?.url || '');
config.headers['X-Requested-With'] = 'XMLHttpRequest'; // 发起请求时,取消掉当前正在进行的相同请求
// beforeRequest();
// 发起请求时,取消掉当前正在进行的相同请求 // 发起请求时,取消掉当前正在进行的相同请求
if (pending[config.url as string]) { if (pending[config.url]) {
pending[config.url as string]('取消重复请求'); pending[config.url]('取消重复请求');
} }
config.cancelToken = new CancelToken(c => (pending[(config.url + JSON.stringify(config.data)) as string] = c)); config.cancelToken = new CancelToken(c => (pending[config.url] = c));
// 添加token // 使用默认响应处理策略
const token = localStorage.get('token'); if (!config.strategy) {
if (token && !config.hideToken) { config.strategy = 'service';
config.headers['X-Auth-Token'] = token; }
if (config.accessToken) { config.headers['Access-Token'] = token; } if (strategyModes[config.strategy].request) {
config = strategyModes[config.strategy].request(config);
} }
return config; return config;
}, },
error => { error => {
return Promise.reject(error); return [null, error];
} }
); );
// 响应拦截器即异常处理 // 响应拦截器即异常处理
instance.interceptors.response.use( instance.interceptors.response.use(
response => { response => {
let notifyType = 'danger'; if (response.config?.url) {
// afterRequest(); !config.hideLoading && afterRequest();
delete pending[response.config?.url || ''];
delete pending[response.config.url as string];
const codeArr = ['0000', 2000];
if (codeArr.includes(response.data.code)) { return response.data.data; }
// 后端返回异常信息时提出警告
if (response.data.code && response.data.msg) { notifyType = 'warning'; }
Notify({
type: notifyType,
message: response.data.msg || '后端服务异常',
duration: notifyType === 'warning' ? 6000 : 3000
});
if (response.data.code === '40100') {
localStorage.remove('Token');
}
return Promise.reject(response.data);
},
async err => {
// afterRequest();
// 判断是否取消请求
if (err?.message === '取消重复请求') {
const res = {
status: 409,
data: {}
};
return Promise.resolve(res);
}
// 提示网络错误时尝试重试
if (
err.message === 'Network Error' &&
(!err.config?.retryCount || err.config?.retryCount <= 3)
) {
if (!err.config?.retryCount) { err.config.retryCount = 1; }
err.config.retryCount++;
await wait(3000);
return axios.request(err.config);
} }
if (err.response) { return strategyModes[response.config?.strategy].response(response);
switch (err.response.status) { },
case 401: err => {
window.location.href = `${window.location.origin}/editor/login`; !config.hideLoading && afterRequest();
return;
}
}
// 错误提示
let message = ''; let message = '';
if (err.response) { let showToast = true;
message = ERR_MESSAGE_MAP.status[err.response.status] || `连接错误${err.response.status}`;
} else { if (err.message === 'Network Error' && !err.response) {
message = '连接到服务器失败'; // 网络异常: 错误域名,
message = '服务不可用';
} else if (err.response && err.response?.status) {
message =
ERR_MESSAGE_MAP.status[err.response?.status] || `未知异常码: ${err.response?.status}`;
} else if (err.message === '取消重复请求') {
message = '取消重复请求';
showToast = false;
} }
Notify({ type: 'danger', message }); showToast && Toast(message || '服务异常,请稍后重试');
return Promise.reject(err); const error = new Error(message);
console.error(error);
return [null, error];
} }
); );
export default instance; export default instance;
// import store from '../store';
import { isWechat, isApp } from './utils.service';
import Cookies from './cookieStorage.service';
import localStorage from './localStorage.service';
// formXcxPage:标识是从小程序跳转过来的;
const localStorageParams = ['creditToken', 'vccToken', 'vccChannel', 'formXcxPage'];
const cookiesParams = ['h'];
export default {
// token校验,整个流程都是登陆后的
init: router => {
router.beforeEach((to, from, next) => {
// 所有自定义路由字段在此处理
const { meta } = to;
// meta?.has?.header && store.commit('CHANGE_HEADER', meta.has.header); // 改变header
localStorageParams.forEach(item => {
to.query[item] && localStorage.set(item, to.query[item]);
});
cookiesParams.forEach(item => {
to.query[item] && Cookies.set(item, to.query[item]);
});
// (isWechat || isApp || Cookies.get('h') === '0') && store.commit('CHANGE_HEADER', false); // 改变header
// document.body.className = store.state.pay.header ? 'has-header' : '';
// store.commit('CHANGE_TITLE', meta?.title); // 改变title
next();
});
}
};
...@@ -38,6 +38,7 @@ import Tabs from '@qg/cherry-ui/src/tabs'; ...@@ -38,6 +38,7 @@ import Tabs from '@qg/cherry-ui/src/tabs';
import BackTop from '@qg/cherry-ui/src/back-top'; import BackTop from '@qg/cherry-ui/src/back-top';
import Notify from '@qg/cherry-ui/src/notify'; import Notify from '@qg/cherry-ui/src/notify';
import Toast from '@qg/cherry-ui/src/toast'; import Toast from '@qg/cherry-ui/src/toast';
import citrusUi from '@qg/citrus-ui';
Vue.use(Button); Vue.use(Button);
Vue.use(Image); Vue.use(Image);
...@@ -66,7 +67,7 @@ Vue.use(Tabs); ...@@ -66,7 +67,7 @@ Vue.use(Tabs);
// Vue.use(Swipe); // Vue.use(Swipe);
Vue.use(BackTop); Vue.use(BackTop);
// Vue.use(KaLoginForm); Vue.use(citrusUi);
Vue.prototype.$notify = Notify; Vue.prototype.$notify = Notify;
Vue.prototype.$toast = Toast; Vue.prototype.$toast = Toast;
This diff is collapsed.
import Vue from 'vue';
import sa from 'sa-sdk-javascript';
import config from '@/config';
import Storage from '@/service/localStorage.service';
export function initSa(router) {
console.log('initSa', EASY_ENV_IS_BROWSER, config);
sa.init({
server_url: config.shenceUrl,
heatmap: {
// 是否开启点击图,默认 default 表示开启,自动采集 $WebClick 事件,可以设置 'not_collect' 表示关闭
clickmap: 'default',
// 是否开启触达注意力图,默认 default 表示开启,自动采集 $WebStay 事件,可以设置 'not_collect' 表示关闭
scroll_notice_map: 'default',
},
// 判断外层是否有 App 的 SDK ,如果有的话,会往 App 的 SDK 发数据。如果没有,就正常发送数据。
use_app_track: true,
show_log: config.test,
app_js_bridge: true,
});
// 添加公共属性
sa.registerPage({
platformType: 'H5',
uuid: '',
qg_device_id: '',
channel_code: '',
splitStream: '',
});
router.afterEach(() => {
Vue.nextTick(() => {
sa.quick('autoTrackSinglePage');
});
});
}
export function registeredEvents(eventName, eventData) {
sa.track(eventName, eventData);
}
// 用户登录神策埋点
export function loginSa(uuid = Storage.get('uuid')) {
if (!uuid) { return; }
Storage.set('uuid', uuid);
sa.login(uuid);
}
// tslint:disable
export const chunk = function(array, size) { export const chunk = function(array, size) {
const result = []; const result = [];
let [start, end] = [null, null]; let [start, end] = [null, null];
...@@ -118,3 +119,19 @@ export const transformStyle = function(styleObj = {}) { ...@@ -118,3 +119,19 @@ export const transformStyle = function(styleObj = {}) {
} }
return style; return style;
}; };
const ua = !EASY_ENV_IS_BROWSER ? { match() {} } : window.navigator.userAgent.toLowerCase();
// 判断微信环境
export const isWechat = ua.match(/MicroMessenger/i) == "micromessenger";
// 判断羊小咩(信用钱包)环境
export const isApp = ua.match(/xyqb/i) == "xyqb";
// 判断真享生活
export const isVcc = ua.match(/VCC/i) == "vcc";
// 判断IOS环境
export const isIOS = /iphone|ipad|ipod/.test(ua);
// 判读Android环境
export const isAndroid = /android/.test(ua);
...@@ -19,8 +19,8 @@ ...@@ -19,8 +19,8 @@
}, },
"dependencies": { "dependencies": {
"@better-scroll/core": "^2.0.5", "@better-scroll/core": "^2.0.5",
"@qg/cherry-ui": "^2.18.66", "@qg/cherry-ui": "^2.18.75",
"@qg/citrus-ui": "0.0.1-alpha.2", "@qg/citrus-ui": "0.0.14",
"axios": "^0.21.1", "axios": "^0.21.1",
"babel-plugin-transform-vue-jsx": "^4.0.1", "babel-plugin-transform-vue-jsx": "^4.0.1",
"egg": "^2.3.0", "egg": "^2.3.0",
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
"egg-webpack": "^4.4.7", "egg-webpack": "^4.4.7",
"egg-webpack-vue": "^2.0.0", "egg-webpack-vue": "^2.0.0",
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"npm": "^6.14.10",
"qs": "^6.9.4", "qs": "^6.9.4",
"raven-js": "^3.27.2", "raven-js": "^3.27.2",
"sa-sdk-javascript": "^1.15.27", "sa-sdk-javascript": "^1.15.27",
...@@ -42,8 +43,7 @@ ...@@ -42,8 +43,7 @@
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"vuex-class": "^0.3.1", "vuex-class": "^0.3.1",
"vuex-router-sync": "^5.0.0", "vuex-router-sync": "^5.0.0"
"npm": "^6.14.10"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^3.9.2", "typescript": "^3.9.2",
......
...@@ -181,6 +181,9 @@ module.exports = { ...@@ -181,6 +181,9 @@ module.exports = {
} }
} }
}, },
externals: {
'sa-sdk-javascript': 'sensorsDataAnalytic201505'
},
// mode: 'development', // mode: 'development',
devtool:'source-map', devtool:'source-map',
customize(webpackConfig){ customize(webpackConfig){
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment