问卷系统开发 ---前端
手机端调试组件
在index.html文件内加上代码
<script type="text/javascript" src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>
// 手机端模拟console
eruda.init();
// console.log(navigator.language)
</script>
rem适配
- 在assets目录下加入flexible.js文件。flexible.js为阿里团队开源的一个库。根据屏幕宽度,给根元素确定一个px字号,页面中的制作稿则统一使用rem这个单位来制作。使用flexible.js轻松搞定各种不同的移动端设备兼容自适应问题。
- 在assets/css目录下创建config.scss。在config.scss里编写需要使用的scss方法,混入,变量等等.
// 将px转化为rem。设计稿以iphone7为模版,宽度为375px,所以是 $px / 12.5
@function rem ($px) {
@return $px / 12.5+rem;
}
安装sass-loader ,node-sass。 编译scss样式文件
npm install --save-dev sass-loader npm install --save-dev node-sass
- vue.config.js中配置config.scss。全局配置最好只放入css变量,配置普通css会导致样式重复。
// vue.config -> module.css // css相关配置 css: { // 是否使用css分离插件 ExtractTextPlugin extract: false, // 开启 CSS source maps? sourceMap: false, // css预设器配置项 loaderOptions: { //向所有 Sass/Less 样式传入共享的全局变量 scss:{ // 如果 sass-loader 版本 = 8,这里使用 `prependData` 字段 // 如果 sass-loader 版本 < 8,这里使用 `data` 字段 additionalData:'@import "~@/assets/css/config.scss";' }, }, // 启用 CSS modules for all css / pre-processor files. requireModuleExtension: true, },
打包文件的压缩优化
- 安装压缩插件,压缩图片 插件
npm i compression-webpack-plugin --save-dev npm i image-webpack-loader --save-dev
- 编写vue.config ,使vue项目运行时对文件进行压缩
//vue.config -->module.chainWebpack const CompressionPlugin = require('compression-webpack-plugin');//引入gzip压缩插件 // webpack配置 chainWebpack: config => { //压缩图片 config.module.rule('images') .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) .use('image-webpack-loader') .loader('image-webpack-loader') .options({ bypassOnDebug: true }) //压缩文件 config.plugin('compressionPlugin') .use(new CompressionPlugin({ test: /\.js$|\.html$|\.css/,//匹配文件名 threshold: 10240, // 对超过10k的数据压缩 deleteOriginalAssets: false // 不删除源文件 })) }
滚动行为在组件之间相互影响的解决办法
使用路由提供的钩子函数
router.afterEach((to,from,next) => { window.scrollTo(0,0) });
多个组件中监听vuex中同一个action的回调,如何避免重复执行方法
多个组件同时发出请求,第一个请求赋值给该变量,其他情况直接返回该变量
//store.js 文件 const state={ web3:null }; var flag=false; var fun=null; const action={ myFunction(){ //如果有数据直接返回 if (state.web3) { Promise.resolve(state.web3) return }else{ //如果没有数据且是第一次请求则执行函数 if(!falg){ falg=true; fun=new Promise((resolve,reject)=>{ ajax('*****').then(res=>{ state.web3=res resolve(res) }).catch(err=>{ //报错情况下可以再次执行函数 falg=false; }) }) } //如果没有数据但是不是第一次请求直接返回promise函数 return fun } } }
滚动行为在组件之间相互影响的解决办法
使用路由提供的钩子函数,在路由离开时滚动到最顶部。
//router.js
router.afterEach((to,from,next) => {
window.scrollTo(0,0)
});
微信分享接口配置 参考—微信js-sdk说明文档
echarts图表强制横屏全屏展示
- 使用css3设置旋转中心和旋转角度。编写横屏全屏样式。
//scss
.st_rotate{
transform: rotate(90deg)!important;
transform-origin:0% 0%!important;
width: 100vh!important;/*2.利用 vh 重置 ‘宽度’ */
height: 100vw!important;/* 3.利用 vw 重置 ‘高度’ */
top: 0!important;
left: 100vw!important;/* 4.旋转后页面超出屏幕,重置页面定位位置 */
position: absolute!important;
z-index: 2!important;
// 对echarts容器重新设置宽高
#etfCharts{
width: 100vh!important;
height: 95vw!important;
}
}
- 在要全屏dom结构上加上css名,重新加载echarts图表
import { kBarline } from "@/assets/common/echarsVue.js"; //净值图、k线图
//Vue script
fullScreen(){
document.getElementById('ChartsBox').classList.add('st_rotate'); //
echarts.init(document.getElementById('etfCharts')).dispose();
var myChart = echarts.init(document.getElementById("etfCharts"));
kBarline(myChart,data); //kBarline 封装的创建k线方法
}
//封装的创建k线方法 echarsVue.js
/**
* k线&柱状
*/
export function kBarline(myChart,rawData){
var dates = rawData.map(function (item) {
return formatDateTime(item.TimeStamp);
});
var data2=[],data3=[];
var data = rawData.map(function (item,index) {
var oldVal=index==0?item.EndPrice:rawData[index-1].EndPrice;
data2.push((item.EndPrice-oldVal)>0?(item.EndPrice-oldVal):0);
data3.push((item.EndPrice-oldVal)>0?0:(item.EndPrice-oldVal));
return [+item.BeginPrice, +item.EndPrice, +item.LowestPrice, +item.HighestPrice,Number(+item.EndPrice-oldVal).toFixed(4),((+item.EndPrice-oldVal)*100/oldVal).toFixed(2)];
});
console.log(rawData,data2,data3)
var option={
backgroundColor: '#21202D', //背景颜色
legend: {
data: ['15分', '1小时', '4小时', '1天'],
inactiveColor: '#777',
textStyle: {
color: '#fff',
shadowColor:'none'
}
},
//提示框组件。
tooltip: {
trigger: 'axis',
//坐标轴指示器
axisPointer: {
// animation: false,
type: 'cross',
lineStyle: {
color: '#376df4',
width: 1,
opacity: 1,
type: 'cross',
}
},
formatter: function (params) {
console.log('params',params)
var color = '';
var style = 'float:right;text-align:right';
if(params[4].value == '--') {
color = '#D7DCEF'
} else {
if(params[4]>=0) {
color = '#00AF91'
} else {
color = '#DA435D'
}
}
var htmlStr = '<div style="width:auto">';
htmlStr +='<div>' + '时间' + ':' + '<span style='+style+'>'+params[0].axisValue+'</span></div>';
htmlStr +='<div>' + '开' + ':' + '<span style='+style+'>'+params[0].value[1].toFixed(4)+'</span></div>';
htmlStr +='<div>' + '收' + ':' + '<span style='+style+'>'+params[0].value[2].toFixed(4)+'</span></div>';
htmlStr +='<div>' + '低' + ':' + '<span style='+style+'>'+params[0].value[3].toFixed(4)+'</span></div>';
htmlStr +='<div>' + '高' + ':' + '<span style='+style+'>'+params[0].value[4].toFixed(4)+'</span></div>';
htmlStr +='<div>' + '涨跌值' + ':' + '<span style="float:right;color:'+color+';text-align:right">'+params[0].value[5]+'</span></div>';
htmlStr +='<div>' + '涨跌幅' + ':' + '<span style="float:right;color:'+color+';text-align:right">'+params[0].value[6]+'</span></div>';
htmlStr += '</div>'
return htmlStr;
}
},
// 用于区域缩放,从而能自由关注细节的数据信息,或者概览数据整体,或者去除离群点的影响。
dataZoom: [{
type: 'inside',
xAxisIndex: [0, 1],
start: 40,
end: 70,
top: 30,
height: 20
}],
xAxis: [{
type: 'category',
data: dates,
axisLine: { lineStyle: { color: '#8392A5' } },
scale: true,
boundaryGap: false,
splitNumber: 20,
min: 'dataMin',
max: 'dataMax',
splitLine: {
show: false
},
},{
show:false,
gridIndex:1,
scale: true,
data: dates,
splitLine: {
show: false
},
},
],
yAxis: [
{
scale: true,
splitArea: {
show: false
},
axisLine: { lineStyle: { color: '#8392A5' } },
splitLine: {
show: false
},
},
{
show:false,
splitLine: {
show: false
},
gridIndex:1,
scale: true,
splitArea: {
show: false
}
}
],
animation: false,
// 直角坐标系内绘图网格
grid:[
{
left: '12%',
height: '55%',
top: '10%',
},
{
left: '12%',
top: '70%',
height: '30%'
}
],
series: [
{
name: 'bar',
type: 'bar',
data: data2,
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: '#FD1050',
// colo12: 'red',
// borderColor: '#eee',
// borderColor0: '#0CF49B'
},
},
{
name: 'bar',
type: 'bar',
data: data3,
xAxisIndex: 1,
yAxisIndex: 1,
itemStyle: {
color: '#FD1050',
// colo12: 'red',
// borderColor: '#eee',
// borderColor0: '#0CF49B'
},
},
{
type: 'candlestick',
name: '15分',
data: data,
itemStyle: {
color: '#FD1050',
color0: '#0CF49B',
borderColor: '#FD1050',
borderColor0: '#0CF49B'
},
markLine: {
// show:false,
symbol: ['none', 'none'],
data: [
[
{
showSymbol:false,
name: 'from lowest to highest',
type: 'min',
valueDim: 'lowest',
symbol: "none",
symbolSize: 10,
label: {
show: false
},
emphasis: {
label: {
show: false
}
}
},
{
showSymbol:false,
type: 'max',
valueDim: 'highest',
symbol:"none",
symbolSize: 10,
label: {
show: false
},
emphasis: {
label: {
show: false
}
}
}
],
{
showSymbol:false,
name: 'min line on close',
type: 'min',
valueDim: 'close'
},
{
showSymbol:false,
name: 'max line on close',
type: 'max',
valueDim: 'close'
}
]
},
markPoint: {
label: {
formatter: function (param) {
return param != null ? Math.round(param.value): '';
},
textStyle: {
color: '#fff',
shadowColor:'none',
backgroundColor:"#999",
rich:{
padding:[0 ,100,0,0]
}
},
distance: 0,
position:'top',
},
data: [
{
name: 'XX标点',
coord: ['2013/5/31', 2300],
value: 2300,
itemStyle: {
color: '#999'
}
},
{
name: 'highest value',
type: 'max',
valueDim: 'highest',
symbol:"arrow",
symbolSize: 5,
symbolRotate:180,
symbolOffset:[0,0],
itemStyle:{
color:"#fff"
}
},
{
name: 'lowest value',
type: 'min',
valueDim: 'lowest',
symbol:"arrow",
symbolSize: 5,
symbolRotate:180,
symbolOffset:[0,0],
itemStyle:{
color:"#fff"
}
},
{
name: 'average value on close',
type: 'average',
valueDim: 'close',
symbol:"arrow",
symbolSize: 10,
}
],
},
},
{
name: '1小时',
type: 'line',
data: calculateMA(5, data),
smooth: true,
showSymbol: false,
lineStyle: {
width: 1
}
},
{
name: '4小时',
type: 'line',
data: calculateMA(10, data),
smooth: true,
showSymbol: false,
lineStyle: {
width: 1
}
},
{
name: '1天',
type: 'line',
data: calculateMA(20, data),
smooth: true,
showSymbol: false,
lineStyle: {
width: 1
}
},
{
name: 'MA30',
type: 'line',
data: calculateMA(30, data),
smooth: true,
showSymbol: false,
lineStyle: {
width: 1
}
}
]
}
myChart.setOption(option,true);
}
使用js编写订阅者发布者模型
import {
UpGetFundPrice,
UpGetCurrencyPriceInfo,
} from "@/api/common.js"; //接口文档
import md5 from "js-md5"
/**
* 请求数据结构体
* @param {*请求的页面名称} observeViewName
* @param {*币种名称} requestName
* @param {*请求类型} requesttype
* @param {*回调} callback
*/
class Observe {
constructor() {
this.requestsDict = {}
this.etfnamesarr = []
this.isetfvaluerequestfree = true
}
// 订阅方法
subscribe(etfrequest) {
if (!etfrequest||!etfrequest.requestName ||!etfrequest.requesttype||!etfrequest.observeViewName) {
return
}
var key = etfrequest.requestName
var callback = etfrequest.callback
var requesttype = etfrequest.requesttype
var observeViewName = etfrequest.observeViewName
//没有对应的key时,初始化
if (!this.requestsDict[key]) {
this.requestsDict[key] = {
'requestKey': [],
'timerKey': '',
}
}
//添加请求体到数组里
var requestData = this.requestsDict[key]
var requestList = requestData['requestKey']
var isfound = false
for (let i = 0; i < requestList.length; i++) {
var request = requestList[i]
var key2 = request.requestName
var requesttype2 = request.requesttype
var observeViewName2 = request.observeViewName
if (key2 == key && requesttype2 == requesttype && observeViewName2 == observeViewName) {
isfound = true
break
}
}
if (!isfound) {
requestList.push(etfrequest)
}
//timer没启动时,启动一下
var thetimer = requestData['timerKey']
// ==1 获取净值
if (requesttype == 1) {
UpGetFundPriceWith(etfrequest.requestName)
if (!thetimer || thetimer === '') {
let timer = setInterval(() => {
UpGetFundPriceWith(etfrequest.requestName)
}, 3000)
requestData['timerKey'] = timer
}
// 获取价格
} else if (requesttype == 2) {
UpGetCurrencyPriceInfoWith(etfrequest.requestName)
if (!thetimer || thetimer === '') {
let timer = setInterval(() => {
UpGetCurrencyPriceInfoWith(etfrequest.requestName)
}, 5000)
requestData['timerKey'] = timer
}
}
}
// 发布
notify(key, value) {
var requestData = this.requestsDict[key]
if (requestData) {
var requestList = requestData['requestKey']
if (requestList && requestList.length > 0) {
for (let i = 0; i < requestList.length; i++) {
var request = requestList[i]
var fn = request.callback
fn.call(this, { key, value })
}
}
}
}
//删除订阅者
unsubscribe(etfrequest) {
if (!etfrequest||!etfrequest.requestName||!etfrequest.observeViewName) {
return
}
var key = etfrequest.requestName
for (var k in this.requestsDict) {
if (k === key) {
var requestData = this.requestsDict[key]
var requestList = requestData['requestKey']
if (requestList) {
for (let i = requestList.length - 1; i >= 0; i--) {
var isequal = (requestList[i].observeViewName == etfrequest.observeViewName)
if (isequal) {
requestList.splice(i, 1)
}
}
if (requestList.length == 0) {
var timer = requestData['timerKey']
if (timer && timer !== '') {
clearInterval(timer);
timer = ''
requestData['timerKey'] = ''
}
}
}
break
}
}
}
}
function UpGetFundPriceWith(etfname) {
const dataParams = JSON.stringify({
name: etfname,
});
UpGetFundPrice({
data: dataParams,
syn: md5(`${dataParams}X9dsf_)#&334$R(`),
}).then(
(result) => {
if (result && result.data && result.data.code == 10000) {
let res = result.data.data;
if (res && res.IsSuccess) {
observeManager.notify(etfname, res)
}
}
},
(error) => { }
);
}
/**
* 币种价格
*/
function UpGetCurrencyPriceInfoWith(name) {
const dataParams = JSON.stringify({
InstrumentID: name,
});
UpGetCurrencyPriceInfo({
data: dataParams,
syn: md5(`${dataParams}X9dsf_)#&334$R(`),
}).then(
(res) => {
if (res && res.data && res.data.code == 10000) {
let result = res.data.data;
if (result) {
observeManager.notify(name, result)
}
}
},
(error) => { }
);
}
const observeManager = new Observe()
export default observeManager
axios封装
重新构建axios实例对象,使用interceptor对request和response做请求的拦截和后端响应拦截。request中设置请求头,如token,cookie,和header。response写回调函数,监听http status,对返回状态做不同同理,500跳转500页面,404跳转404页面。然后将后台接口封装在一个js文件,再加入一个js接口数据校验文件。导出方法给页面的时间调用。利于维护。
- 构建axios实例对象
//httpServer.js
//构建axios实例对象
import Vue from 'vue'
import axios from 'axios'
import store from '@/store.js'
//刷新token的请求方法co
const getRefreshToken = () => {
let data = {
refreshToken: Vue.prototype.$getStore('refreshToken')
};
return axios.post('', data);
}
//添加一个请求拦截器
axios.interceptors.request.use(config => {
config.timeout = 120000
config.headers = {
"Content-Type": "application/json",
"ETFLanguage":store.state.lan,
};
return config
}, error => {
Vue.prototype.$message({
showClose: true,
message: error,
type: 'error'
});
return Promise.reject(error)
})
//添加一个返回拦截器
axios.interceptors.response.use(response => {
//对响应数据做些事
if (response.data.code === 1002) {
let config = response.config;
return getRefreshToken()
.then(function(res) {
let data = res.data;
if (data.code === 1000) {
Vue.prototype.$setStore('authToken', data.data.authToken);
Vue.prototype.$setStore('refreshToken', data.data.refreshToken);
//修改原请求的token
let authToken = Vue.prototype.$getStore('authToken');
config.headers.JWTToken = authToken;
/*这边不需要baseURL是因为会重新请求url
*url中已经包含baseURL的部分了
*如果不修改成空字符串,会变成'api/api/xxxx'的情况*/
config.baseURL = '';
//重新请求
return axios(config).then(
(responsedata) => {
console.log("重发上一次请求")
return responsedata
}
).catch(
(response) => {
console.log("未渲染成功")
return response
});
} else {
Vue.prototype.$message({ showClose: true, message: '超时退出', type: 'warning' });Vue.prototype.$removeStore('authToken');
Vue.prototype.$removeStore('refreshToken');
Vue.prototype.$removeStore('loginInfouser');
router.push({path: '/login'});
return { data: { code: '1001' } }
}
});
} else if (response.data.code === 1001) {
Vue.prototype.$message({ showClose: true, message: '超时退出', type: 'warning' });
Vue.prototype.$removeStore('authToken');
Vue.prototype.$removeStore('refreshToken');
Vue.prototype.$removeStore('loginInfouser');
router.push({path: '/login'});
return { data: { code: '1001' } }
} else {
return response
}
}, error => {
return Promise.resolve(error.response)
})
//错误处理
function errorState(response) {
// 如果http状态码正常,则直接返回数据
if (response && (response.status === 200 || response.status === 304 || response.status === 400)) {
return response
// 如果不需要除了data之外的数据,可以直接 return response.data
} else {
// Vue.prototype.$message({
// showClose: true,
// message: '网络异常',
// type: 'error'
// });
}
}
function successState(res) {
//统一判断后端返回的错误码
// if (res.data.code == '9999') {
// Vue.prototype.$message({
// showClose: true,
// message: '9999-服务器内部异常/网络异常',
// type: 'error'
// });
// }
}
const httpServer =async (opts, data) => {
let httpDefaultOpts = { //http默认配置
method: opts.method || 'post',
url: opts.url
}
if (opts.method == 'get') {
httpDefaultOpts.params = data
} else {
httpDefaultOpts.data = data
}
let promise = new Promise(function(resolve, reject) {
axios(httpDefaultOpts).then(
(res) => {
successState(res)
resolve(res)
}
).catch(
(response) => {
errorState(response)
reject(response)
}
)
})
return promise
}
export default httpServer
- 后台接口文件
//@/api/common.js
//后台接口文件
import http from "@/server/httpServer.js";
// 节点接口
export const UpdateFundAssets = data => {
return http({
url: "/UpdateFundAssets",
method: "get"
}, data);
};
- js接口数据校验文件
import { UpdateFundAssets} from "@/api/common.js";
/**
*封装接口数据
*/
export function bindData(obj) {
var dataParams = obj ? JSON.stringify(obj) : '';
var data = {
data: dataParams,
syn: md5(`${dataParams}X9dsf_)#&334$R(`)
}
return data
}
/**
* 更新合约数据
*/
export function VerifyUpdateFundAssets(TokenAddress, AccountAddress) {
if (!(AccountAddress && TokenAddress)) {
return
}
var obj = {
AccountAddress: AccountAddress.toString() || '',
TokenAddress: TokenAddress.toString() || '',
}
var data = bindData(obj)
return new Promise((resolve, reject) => {
UpdateFundAssets(data).then(res => {
if (res.data.code == 10000) {
resolve(res.data.data)
} else {
reject(res)
}
}).catch(err => {
reject(err)
})
})
}
页面弹窗插件化
为了方便弹窗的调用,同时减轻冗余代码,提高代码复用性。在开发过程中将用到的弹窗封装成插件。学习element插件封装方式,使插件既能全部引入,也能局部引入。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。