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

Initial commit

parents
Pipeline #818 canceled with stages
*.js linguist-language=javascript
*.css linguist-language=javascript
*.html linguist-language=javascript
\ No newline at end of file
.DS_Store
.happypack/
node_modules/
npm-debug.log
.idea/
dist
static
public
private
run
*.iml
artifacts.json
*tmp
_site
logs
app/**/*.js
config/plugin.local.js
config/plugin.js
config/config.*.js
index.js
config/manifest.json
app/view/*
!app/view/layout.html
!app/view/README.md
!app/view/.gitkeep
package-lock.json
yarn.lock
*.log
coverage
\ No newline at end of file
{
"version": "1.0.0",
"configurations": [
{
"name": "Launch Egg Vue",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "npm",
"windows": { "runtimeExecutable": "npm.cmd" },
"runtimeArgs": [ "run", "debug" ],
"console": "integratedTerminal",
"protocol": "auto",
"restart": true,
"port": 9229,
"autoAttachChildProcesses": true
}
]
}
\ No newline at end of file
{
"files.exclude": {
"USE_GITIGNORE": true,
"**/*.js": {
"when": "$(basename).ts"
}
},
"path-intellisense.mappings": {
"lib": "${workspaceRoot}/app/web/lib",
"asset": "${workspaceRoot}/app/web/asset",
"component": "${workspaceRoot}/app/web/component",
"page": "${workspaceRoot}/app/web/page",
"store": "${workspaceRoot}/app/web/store",
},
"typescript.tsdk": "node_modules/typescript/lib"
}
\ No newline at end of file
# egg-vue-typescript-boilerplate
基于 Egg + Vue + Webpack SSR 服务端渲染和 CSR 前端渲染工程骨架项目。
Single Page Application Isomorphic Example for Egg + Vue, Front-End and Node of The Application are Written in TypeScript.
## Document
- https://www.yuque.com/easy-team/egg-vue
- https://www.yuque.com/easy-team/easywebpack
- https://easyjs.cn
## QuickStart
- Development
```bash
$ npm install -g easywebpack-cli
$ easy init
$ npm install
$ npm run dev
$ open http://localhost:7001
```
- Publish
```bash
npm run tsc
npm run build
npm start
```
## Features
- ✔︎ Single Page Application, Support Vue Server Side Render and Client Side Render Modes, Rendering Cache, Automatic Downgrade
- ✔︎ Front-End and Node of The Application are Written in TypeScript, Use `vue-property-decorator` and `vuex-class`
- ✔︎ Build with Webpack + TypeScript, Auto Building, Hot Reload, Code Splitting, High Speed, Performance Optimization
## Rendering
- Front-End TypeScript
![Front-End TypeScript](https://github.com/easy-team/egg-vue-typescript-boilerplate/blob/master/docs/images/vue-front-end.png?raw=true)
- Node TypeScript
![Node TypeScript](https://github.com/easy-team/egg-vue-typescript-boilerplate/blob/master/docs/images/vue-node.png?raw=true)
- UI ScreenShot
![UI ScreenShot](https://github.com/easy-team/egg-vue-typescript-boilerplate/blob/master/docs/images/vue-admin-ui.png?raw=true)
## TypeScript
- https://github.com/kaorun343/vue-property-decorator
- https://github.com/ktsn/vuex-class
## License
[MIT](LICENSE)
import { Controller, Context } from 'egg';
export default class ActivityController extends Controller {
public async home(ctx: Context) {
await ctx.render('activity.js', { url: ctx.url });
}
}
\ No newline at end of file
export function ok(data) {
if (typeof data === 'string') {
return {
code: '0001',
businessCode: '0000',
message: data,
};
}
return {
code: '0000',
businessCode: '0000',
data,
};
}
export function fail({ message = '服务异常', code = '0001', businessCode = '0001' }) {
return {
code,
businessCode,
message,
};
}
import * as path from 'path';
import * as util from 'util';
import { Context } from 'egg';
export default () => {
const skipExt = [ '.png', '.jpeg', '.jpg', '.ico', '.gif' ];
return async function access(ctx: Context, next: any) {
const start = new Date().getTime();
await next();
const rs: number = Math.ceil(new Date().getTime() - start);
ctx.set('X-Response-Time', String(rs));
const ext = path.extname(ctx.url).toLocaleLowerCase();
const isSkip = skipExt.indexOf(ext) !== -1 && ctx.status < 400;
if (!isSkip) {
const ip = ctx.get('X-Real-IP') || ctx.ip;
const port = ctx.get('X-Real-Port');
const protocol = ctx.protocol.toUpperCase();
const method = ctx.method;
const url = ctx.url;
const status = ctx.status;
const length = ctx.length || '-';
const referrer = ctx.get('referrer') || '-';
const ua = ctx.get('user-agent') || '-';
const serverTime = ctx.response.get('X-Server-Response-Time') || '-';
const message = util.format('[access] %s:%s - %s %s %s/%s %s %s %s %s %s',
ip, port, method, url, protocol, status, length, referrer, rs, serverTime, ua);
ctx.logger.info(message);
}
};
};
import { Application } from 'egg';
export default (application: Application) => {
const { router, controller } = application;
const healthMessage = { message: 'service is running...' };
router.get('/tech/health/check', ctx => {
ctx.body = healthMessage;
});
router.get('/activity/:id', controller.activity.home);
};
\ No newline at end of file
## egg规范view目录, 保证view文件夹存在, 否则app.config.view.root为空, 编译服务器文件会存放到该目录.
\ No newline at end of file
module.exports = {
// Sentry options are required
organization: 'sentry',
project: 'quantum-blocks',
apiKey: '2e7e0678d81d4542b09cdbc35812f1cbb07a4993cd13435ba49d9511be11ec95',
// Release version name/hash is required
release: 'quantum-blocks@0.0.1',
baseSentryURL: 'http://sentry.quantgroups.com/api/0',
deleteAfterCompile: true,
include: /js\//, // 只上传js和map文件
ignore: ['node_modules'],
suppressErrors: true,
filenameTransform(filename) {
return '~/' + filename;
},
};
import http from '../service/http.service';
import config from '../config';
import axios from 'axios';
export default {
getPageList(params) {
return http.get('editor/get/list', { params });
},
getPageById(params) {
return http.get(`editor/get/${params.pageId}`);
},
delPageById(pageId) {
return http.delete(`editor/${pageId}`);
},
updatePage(params) {
return http.post(`editor/update`, params);
},
savePage(params) {
return http.post(`editor/save`, params);
},
getTemplateList() {
return http.get('editor/get/template');
},
getUpToken() {
return axios.get(`${config.opapiHost}/upload/getToken`);
},
uploadBase64(base64Str, name, token) {
return axios.post(`${config.qiniuUpHost}/putb64/-1/key/${name}`, base64Str, {
headers: {
'content-type': 'application/octet-stream',
'Authorization': token,
}
});
}
};
\ No newline at end of file
import http from '../service/http.service';
import config from '../config';
export default {
getPageList(params) {
return http.get('editor/get/list', { params });
},
getPageById(params) {
return http.get(`editor/get/${params.pageId}`);
},
delPageById(pageId) {
return http.delete(`editor/${pageId}`);
},
updatePage(params) {
return http.post(`editor/update`, params);
},
savePage(params) {
return http.post(`editor/save`, params);
},
getTemplateList() {
return http.get('editor/get/template');
},
// 商品列表-查询
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
import qs from 'qs';
import http from '../service/http.service';
import config from '../config';
const { opapiHost } = config;
export function getCaptcha() {
return `${opapiHost}/user/captcha`;
}
export function login(params) {
return http.post(`${opapiHost}/backStms/prologue/login`, qs.stringify(params), {
accessToken: true
});
}
export function getUserInfo() {
return http.get(`${opapiHost}/backStms/oauth/currentuserinfo`, {
accessToken: true
});
}
\ No newline at end of file
html,
body {
padding: 0;
margin: 0;
height: 100%;
}
* {
outline: 0;
margin: 0;
padding: 0;
border: none;
box-sizing: border-box;
}
html {
background-color: #f9f9f9;
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
textarea,
input[type="email"],
input[type="number"],
input[type="search"],
input[type="tel"],
input[type="text"],
input[type="password"],
input[type="url"] {
-webkit-appearance: none;
}
a {
text-decoration: none;
}
li {
list-style: none;
}
\ No newline at end of file
import { Vue, Component, Prop } from 'vue-property-decorator';
import Raven from 'raven-js';
import RavenVue from 'raven-js/plugins/vue';
import { release } from '@/.sentryclirc';
import '@/service/qg.service';
// 初始化sentry
if (process.env.SENTRY_ENV !== 'test' && process.env.NODE_ENV === 'production') {
Raven.config('http://0785298052fd46128e201f30ca649102@sentry.q-gp.com/64', {
release,
environment: 'production'
})
.addPlugin(RavenVue, Vue)
.install();
}
@Component({
name: 'Layout'
})
export default class Layout extends Vue {
@Prop({ type: String, default: 'egg' }) title?: string;
@Prop({ type: String, default: 'Vue TypeScript Framework, Server Side Render' }) description?: string;
@Prop({ type: String, default: 'Vue,TypeScript,Isomorphic' }) keywords?: string;
isNode: boolean = EASY_ENV_IS_NODE;
created() {
console.log('>>EASY_ENV_IS_NODE create', EASY_ENV_IS_NODE);
}
}
\ No newline at end of file
<template>
<html v-if="isNode">
<head>
<title>{{title}}</title>
<meta name="keywords" :content="keywords">
<meta name="description" :content="description">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/public/asset/css/reset.css">
</head>
<body>
<div id="app"><slot></slot></div>
</body>
</html>
<div v-else-if="!isNode" id="app"><slot></slot></div>
</template>
<style>
html {
font-size: 10vw !important;
}
@media screen and (min-width: 768Px) {
html {
font-size: 37.5Px !important;
}
body {
max-width: 375Px;
max-height: 667Px;
margin: 0 auto !important;
}
}
</style>
<script lang="ts" src="./index.ts"></script>
\ No newline at end of file
const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'http';
export default {
apiHost: `http://localhost:7001/`,
// apiHost: `http://192.168.28.199:7001/`,
// apiHost: 'https://quantum-vcc2.liangkebang.net/',
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=default`,
opapiHost: `https://opapi-vcc2.liangkebang.net`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
kdspHost: 'https://kdsp-api-vcc2.liangkebang.net',
};
import prod from './prod.config';
import dev from './dev.config';
import test from './test.config';
// export default process.env.NODE_ENV === 'test' ? test : process.env.NODE_ENV === 'production' ? prod : dev;
export default process.env.SENTRY_ENV ? test : process.env.NODE_ENV === 'production' ? prod : dev;
const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
export default {
apiHost: `https://quantum-blocks.q-gp.com/`,
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`,
opapiHost: `${protocol}//opapi.q-gp.com`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
};
const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
export default {
apiHost: `https://quantum-vcc2.liangkebang.net/`,
opApiHost: 'https://opapi-vcc2.liangkebang.net',
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
};
import Vue from 'vue';
import { sync } from 'vuex-router-sync';
export default class App {
config: any;
constructor(config) {
this.config = config;
}
bootstrap() {
console.log('EASY_ENV_IS_NODE', EASY_ENV_IS_NODE, EASY_ENV_IS_BROWSER);
if (EASY_ENV_IS_NODE) {
return this.server();
}
return this.client();
}
create(initState) {
const { entry, createStore, createRouter } = this.config;
console.log('initState', initState);
const store = createStore(initState);
const router = createRouter();
sync(store, router);
return {
router,
store,
render: h => { // not use ...entry, why ?
return h(entry);
},
};
}
fetch(vm): Promise<any> {
const { store, router } = vm;
const matchedComponents = router.getMatchedComponents();
if (!matchedComponents) {
return Promise.reject('No Match Component');
}
return Promise.all(
matchedComponents.map((component: any) => {
const options = component.options;
if (options && options.methods && options.methods.fetchApi) {
return options.methods.fetchApi.call(component, { store, router, route: router.currentRoute });
}
return null;
})
);
}
client() {
Vue.prototype.$http = require('axios');
const vm = this.create(window.__INITIAL_STATE__);
vm.router.afterEach(() => {
this.fetch(vm);
});
const app = new Vue(vm);
const root = document.getElementById('app');
const hydrate = root.childNodes.length > 0;
app.$mount('#app', hydrate);
return app;
}
server() {
return context => {
// console.log('server', context.state);
const vm = this.create(context.state);
const { store, router } = vm;
router.push(context.state.url);
return new Promise((resolve, reject) => {
router.onReady(() => {
this.fetch(vm).then(() => {
context.state = store.state;
return resolve(new Vue(vm));
});
});
});
};
}
}
\ No newline at end of file
declare var EASY_ENV_IS_NODE: boolean;
declare var EASY_ENV_IS_BROWSER: boolean;
type PlainObject<T = any> = { [key: string]: T };
\ No newline at end of file
<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: true,
// 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);
},
},
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: true,
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();
}
}
},
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 { Action, Mutation, State, Getter } from 'vuex-class';
import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
import CustomMarquee from '@/lib/Marquee/index.vue';
import { resizeDiv } from '@/service/utils.service';
@Component({ components: { CustomMarquee }, name: 'FreedomContainer' })
export default class FreedomContainer extends Mixins(TransformStyleMixin) {
@Getter('pageData') pageData;
@State(state => state.editor.curChildIndex) curChildIndex;
@Mutation('UPDATE_PAGE_INFO') updatePageInfo;
@Prop({type: Object, default: () => ({ child: [] })}) childItem;
@Prop(String) backgroundImage;
mounted() {
// 根据背景图设置元素高度
const index = this.pageData?.elements?.findIndex(v => v.point?.responsive);
const { props: { backgroundImage }, point } = this.pageData?.elements[index] || { props: {}};
if (backgroundImage) {
resizeDiv(backgroundImage, null, null, (height) => {
this.updatePageInfo({ containerIndex: index, data: { ...this.pageData?.elements[index], point: { ...point, h: height ?? point.h } } });
});
}
}
}
\ No newline at end of file
<template>
<div class="freedom">
<div class="freedom-body">
<component :class="['freedom-body-item', { 'Fb-item_selected': curChildIndex === index }]" v-for="(item, index) in childItem.child" :style="transformStyle(item.commonStyle, 'container')" :is="item.name" :key="index" v-bind="item.props"></component>
</div>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.freedom {
height: 100%;
width: 100%;
header {
width: 100%;
height: 48px;
line-height: 48px;
text-align: center;
border-bottom: 1px solid #f0f0f0;
}
&-body {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
&_full {
height: calc(100% - 48px);
}
}
}
</style>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
let currentDir = 'auto';
// let currentDir = "auto";
function hasDocument() {
return (typeof document !== 'undefined');
}
function hasWindow() {
return (typeof window !== 'undefined');
}
export function getDocumentDir() {
if (!hasDocument()) {
return currentDir;
}
const direction = (typeof document.dir !== 'undefined') ?
document.dir :
document.getElementsByTagName('html')[0].getAttribute('dir');
return direction;
}
export function setDocumentDir(dir) {
// export function setDocumentDir(dir){
if (!hasDocument) {
currentDir = dir;
return;
}
const html = document.getElementsByTagName('html')[0];
html.setAttribute('dir', dir);
}
export function addWindowEventListener(event, callback) {
if (!hasWindow) {
callback();
return;
}
window.addEventListener(event, callback);
}
export function removeWindowEventListener(event, callback) {
if (!hasWindow) {
return;
}
window.removeEventListener(event, callback);
}
// Get {x, y} positions from event.
export function getControlPosition(e) {
return offsetXYFromParentOf(e);
}
// Get from offsetParent
export function offsetXYFromParentOf(evt) {
const offsetParent = evt.target.offsetParent || document.body;
const offsetParentRect = evt.offsetParent === document.body ? {left: 0, top: 0} : offsetParent.getBoundingClientRect();
const x = evt.clientX + offsetParent.scrollLeft - offsetParentRect.left;
const y = evt.clientY + offsetParent.scrollTop - offsetParentRect.top;
/*const x = Math.round(evt.clientX + offsetParent.scrollLeft - offsetParentRect.left);
const y = Math.round(evt.clientY + offsetParent.scrollTop - offsetParentRect.top);*/
return {x, y};
}
// Create an data object exposed by <DraggableCore>'s events
export function createCoreData(lastX, lastY, x, y) {
// State changes are often (but not always!) async. We want the latest value.
const isStart = !isNum(lastX);
if (isStart) {
// If this is our first move, use the x and y as last coords.
return {
deltaX: 0, deltaY: 0,
lastX: x, lastY: y,
x, y
};
} else {
// Otherwise calculate proper values.
return {
deltaX: x - lastX, deltaY: y - lastY,
lastX, lastY,
x, y
};
}
}
function isNum(num) {
return typeof num === 'number' && !isNaN(num);
}
// @flow
import {cloneLayout, compact, correctBounds} from './utils';
// import type {Layout} from './utils';
// export type ResponsiveLayout = {lg, md, sm, xs, xxs};
// type Breakpoint = string;
// type Breakpoints = {lg, md, sm, xs, xxs};
/**
* Given a width, find the highest breakpoint that matches is valid for it (width > breakpoint).
*
* @param {Object} breakpoints Breakpoints object (e.g. {lg: 1200, md: 960, ...})
* @param {Number} width Screen width.
* @return {String} Highest breakpoint that is less than width.
*/
export function getBreakpointFromWidth(breakpoints, width) {
const sorted = sortBreakpoints(breakpoints);
let matching = sorted[0];
for (let i = 1, len = sorted.length; i < len; i++) {
const breakpointName = sorted[i];
if (width > breakpoints[breakpointName]) { matching = breakpointName; }
}
return matching;
}
/**
* Given a breakpoint, get the # of cols set for it.
* @param {String} breakpoint Breakpoint name.
* @param {Object} cols Map of breakpoints to cols.
* @return {Number} Number of cols.
*/
export function getColsFromBreakpoint(breakpoint, cols) {
if (!cols[breakpoint]) {
throw new Error('ResponsiveGridLayout: `cols` entry for breakpoint ' + breakpoint + ' is missing!');
}
return cols[breakpoint];
}
/**
* Given existing layouts and a new breakpoint, find or generate a new layout.
*
* This finds the layout above the new one and generates from it, if it exists.
*
* @param {Array} orgLayout Original layout.
* @param {Object} layouts Existing layouts.
* @param {Array} breakpoints All breakpoints.
* @param {String} breakpoint New breakpoint.
* @param {String} breakpoint Last breakpoint (for fallback).
* @param {Number} cols Column count at new breakpoint.
* @param {Boolean} verticalCompact Whether or not to compact the layout
* vertically.
* @return {Array} New layout.
*/
export function findOrGenerateResponsiveLayout(orgLayout, layouts, breakpoints,
breakpoint, lastBreakpoint,
cols, verticalCompact) {
// If it already exists, just return it.
if (layouts[breakpoint]) { return cloneLayout(layouts[breakpoint]); }
// Find or generate the next layout
let layout = orgLayout;
const breakpointsSorted = sortBreakpoints(breakpoints);
const breakpointsAbove = breakpointsSorted.slice(breakpointsSorted.indexOf(breakpoint));
for (let i = 0, len = breakpointsAbove.length; i < len; i++) {
const b = breakpointsAbove[i];
if (layouts[b]) {
layout = layouts[b];
break;
}
}
layout = cloneLayout(layout || []); // clone layout so we don't modify existing items
return compact(correctBounds(layout, {cols}), verticalCompact);
}
export function generateResponsiveLayout(layout, breakpoints,
breakpoint, lastBreakpoint,
cols, verticalCompact) {
// If it already exists, just return it.
/*if (layouts[breakpoint]) return cloneLayout(layouts[breakpoint]);
// Find or generate the next layout
let layout = layouts[lastBreakpoint];*/
/*const breakpointsSorted = sortBreakpoints(breakpoints);
const breakpointsAbove = breakpointsSorted.slice(breakpointsSorted.indexOf(breakpoint));
for (let i = 0, len = breakpointsAbove.length; i < len; i++) {
const b = breakpointsAbove[i];
if (layouts[b]) {
layout = layouts[b];
break;
}
}*/
layout = cloneLayout(layout || []); // clone layout so we don't modify existing items
return compact(correctBounds(layout, {cols}), verticalCompact);
}
/**
* Given breakpoints, return an array of breakpoints sorted by width. This is usually
* e.g. ['xxs', 'xs', 'sm', ...]
*
* @param {Object} breakpoints Key/value pair of breakpoint names to widths.
* @return {Array} Sorted breakpoints.
*/
export function sortBreakpoints(breakpoints) {
const keys = Object.keys(breakpoints);
return keys.sort(function(a, b) {
return breakpoints[a] - breakpoints[b];
});
}
This diff is collapsed.
'use strict';
import App from '../../framework/app';
import createStore from '../store/index';
import createRouter from './router/index';
import entry from './view/home/index.vue';
export default new App({ entry, createStore, createRouter }).bootstrap();
\ No newline at end of file
import Vue from 'vue';
import VueRouter from 'vue-router';
import Activity from '../view/activity/index.vue';
Vue.use(VueRouter);
export default function createRouter() {
return new VueRouter({
mode: 'history',
routes: [
{
path: '/activity/:pageId',
component: Activity
}
]
});
}
import { Vue, Component, Watch, Provide, Mixins } from 'vue-property-decorator';
import FreedomContainer from '../../component/FreedomContainer/index.vue';
import GridLayout from '../../component/VueGridLayout/GridLayout.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 { getStyle } from '@/service/utils.service';
@Component({ components: { FreedomContainer, GridLayout, GridItem, DownloadGuide, GoodsTabs, GuideCube, Advertisement, Coupon }, name: 'Activity'})
export default class Activity extends Mixins(TransformStyleMixin) {
@Getter('pageData') pageData;
@State(state => state.editor.pageInfo.pageName) pageName;
@State(state => state.editor.gridLayout.rowHeight) rowHeight;
@Provide('editor');
isLayoutComReady = false;
showBackTop = false;
targetEle: HTMLElement | null = null;
get layout() {
return this.pageData && this.pageData.elements.map(v => v.point) || [];
}
@Watch('pageName', { immediate: true })
onPageNameChange(newVal) {
if (EASY_ENV_IS_BROWSER && newVal) {
document.title = newVal;
// 如果是 iOS 设备,则使用如下 hack 的写法实现页面标题的更新
if (navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) {
const hackIframe = document.createElement('iframe');
hackIframe.style.display = 'none';
hackIframe.src = '/public/fixIosTitle.html?r=' + Math.random();
document.body.appendChild(hackIframe);
setTimeout(() => {
document.body.removeChild(hackIframe);
}, 300);
}
}
}
mounted() {
this.targetEle = document.querySelector('body');
this.showBackTop = true;
const timer = setTimeout(() => {
this.modfiTabsStyle();
}, 500);
this.$once('hook:beforeDestroy', () => {
clearTimeout(timer);
});
}
fetchApi(options) {
const { store, route } = options;
const { pageId } = route.params;
console.log('fetchApi', route);
return store.dispatch('getPageDate', { pageId });
}
createStyle({h}) {
return EASY_ENV_IS_NODE ? {
height: `${h * this.rowHeight}px`,
} : {};
}
modfiTabsStyle() {
const tabsEle = document.querySelector('.tabs');
if (tabsEle) {
const gridItemEle = tabsEle?.parentNode;
if (gridItemEle?.classList.contains('vue-grid-item')) {
// 处理transform
const transform = getStyle(gridItemEle, 'transform');
const transformY = transform.split('(')[1].split(')')[0].split(',')[5];
gridItemEle.style.transform = 'none';
gridItemEle.style.top = `${transformY}px`;
// 处理backgroundColor
const backgroundColor = getStyle(tabsEle, 'backgroundColor');
const crTabs = tabsEle.childNodes[0];
crTabs.style.backgroundColor = backgroundColor;
const stickyEle = crTabs?.childNodes[0];
if (stickyEle?.classList.contains('cr-sticky') && stickyEle?.childNodes) {
stickyEle.childNodes[0]?.style.backgroundColor = backgroundColor;
}
}
}
}
}
\ No newline at end of file
<template>
<div class="activity" :style="transformStyle(pageData.commonStyle)">
<grid-layout
:layout.sync="layout"
:isDraggable="false"
:isResizable="false"
:col-num="12"
:row-height="rowHeight"
:margin="[0, 0]"
:is-draggable="true"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:use-css-transforms="true"
>
<grid-item :style="createStyle(item.point)" v-for="(item, index) in pageData.elements"
:x="item.point.x"
:y="item.point.y"
:w="item.point.w"
:h="item.point.h"
:i="item.point.i"
:key="item.point.i">
<component :style="transformStyle(item.commonStyle)" :data-index="index" :containerIndex="index" :childItem="item" :is="item.name" :key="index" v-bind="item.props"></component>
</grid-item>
</grid-layout>
<cr-back-top v-if="showBackTop && pageData.props.showBackTop" />
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
html,
body,
#app {
height: 100%;
/deep/ .cr-popup {
.cr-notify {
font-size: 14px;
}
}
}
.activity {
width: 100%;
height: 100%;
min-height: 100%;
background-color: rgb(244, 244, 244);
box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2);
/deep/ .vue-grid-layout {
min-height: 667px;
// transform: translateY(-10px);
transition-property: none;
.vue-grid-item {
transition-property: none;
display: flex;
justify-content: center;
align-items: center;
&>*:first-child {
height: 100%;
}
}
}
}
</style>
import { Vue, Component, Emit } from 'vue-property-decorator';
import Layout from 'component/layout/activity/index.vue';
@Component({
components: {
Layout
}
})
export default class Home extends Vue {}
\ No newline at end of file
<template>
<Layout>
<router-view></router-view>
</Layout>
</template>
<script lang="ts" src="./index.ts"></script>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
'use strict';
export const SET_PAGE_INFO = 'SET_PAGE_INFO';
export const SET_DRAGABLE = 'SET_DRAGABLE';
export const COPY_OR_DELETE_PAGE_INFO = 'COPY_OR_DELETE_PAGE_INFO';
export const UPDATE_PAGE_INFO = 'UPDATE_PAGE_INFO';
export const ADD_ELEMENTS = 'ADD_ELEMENTS';
export const SET_CUR_ELE_INDEX = 'SET_CUR_ELE_INDEX';
export const SET_CUR_CHILD_INDEX = 'SET_CUR_CHILD_INDEX';
export const RESET_PAGE_DATA = 'RESET_PAGE_DATA';
export const SET_TEMPLATE_LIST = 'SET_TEMPLATE_LIST';
export const SET_PAGE_DATA = 'SET_PAGE_DATA';
export const UPDATE_COMMON_STYLE = 'UPDATE_COMMON_STYLE';
export const UPDATE_PAGE_STYLE = 'UPDATE_PAGE_STYLE';
export const UPDATE_PAGE_PROPS = 'UPDATE_PAGE_PROPS';
export default interface RootState {
origin: string;
csrf: string;
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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