作者 REN·WANG

init

> 1%
last 2 versions
not dead
... ...
NODE_ENV = 'development'
VUE_APP_NAME = 'dev'
... ...
NODE_ENV = 'development'
VUE_APP_NAME = 'devOnLine'
... ...
NODE_ENV = 'production'
VUE_APP_NAME = 'pro'
... ...
NODE_ENV = 'production'
VUE_APP_NAME = 'uat'
... ...
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
... ...
{
"endOfLine": "lf",
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"useTabs": false,
"tabWidth": 2,
"printWidth": 180,
"proseWrap": "preserve",
"arrowParens": "avoid",
"bracketSpacing": true,
"quoteProps": "as-needed"
}
... ...
# kraken-vue-app
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
... ...
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
... ...
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
... ...
{
"name": "kraken-vue-app",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"dev": "vue-cli-service serve --mode dev --open",
"dev:online": "vue-cli-service serve --mode devOnLine --open",
"uat": "vue-cli-service build --mode uat",
"pro": "vue-cli-service build --mode pro"
},
"dependencies": {
"axios": "^1.4.0",
"core-js": "^3.8.3",
"vant": "^2.12.54",
"vue": "^2.6.14",
"vue-router": "^3.5.1",
"vuex": "^3.6.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"sass": "^1.32.7",
"sass-loader": "^12.0.0",
"vue-template-compiler": "^2.6.14"
}
}
... ...
此 diff 太大无法显示。
{
"name": "kraken-vue-app",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"dev": "vue-cli-service serve --mode dev --open",
"dev:online": "vue-cli-service serve --mode devOnLine --open",
"uat": "vue-cli-service build --mode uat --no-source-map",
"pro": "vue-cli-service build --mode pro --no-source-map"
},
"dependencies": {
"axios": "^1.4.0",
"vant": "^2.12.54",
"vue": "^2.6.14",
"vue-drag-resize": "^1.5.4",
"vue-router": "^3.5.1",
"vuex": "^3.6.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"sass": "^1.32.7",
"sass-loader": "^12.0.0",
"vue-template-compiler": "^2.6.14"
}
}
... ...
不能预览此文件类型
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
\ No newline at end of file
... ...
<template>
<div class="app" id="app" :style="{ paddingTop: systemInfo.statusHeight + 'px' }">
<MyNavBar
:offset-top="systemInfo.statusHeight"
:title="$route.meta.title"
:show="getIsApp && $route.meta.isTabBarMenu && $route.meta.showNavBar"
:right-text="rightBtnText"
@right-callback="othersBtnFun"
/>
<MyNavBar
:offset-top="systemInfo.statusHeight"
:title="$route.meta.title"
:show="getIsApp && !$route.meta.isTabBarMenu && $route.meta.showNavBar"
:left-arrow="true"
:left-arrow-style="{ color: '#f00' }"
:right-text="rightBtnText"
left-text="返回"
@left-callback="backPrePage"
@right-callback="othersBtnFun"
/>
<div class="router-page-view" :style="{ marginTop: getAppNavBarShow ? '46px' : '' }" :class="{ 'is-tab-bar-menu-page': $route.meta.isTabBarMenu }">
<!-- <keep-alive :include="keepAlivePageNameList"> -->
<router-view />
<!-- </keep-alive> -->
</div>
<template v-if="$route.meta.isTabBarMenu">
<MyTabBar v-model="currentTabBarName" :tab-bar-list="tabBarList" @change="tabBarChange" />
</template>
<MyDialog
:show-popup="dialogOpen"
:title="myDialogConfig.title"
:description="myDialogConfig.description"
:descriptionType="myDialogConfig.descriptionType"
:show-cancel-btn="true"
:cancel-btn-text="myDialogConfig.cancelBtnText"
:ok-close="true"
@ok="dialogOk"
@cancel="dialogCancel"
/>
<MyToast loading-icon="icon-loading-1" ref="myToast" />
<!-- <MyConsole ref="MyConsole" theme="dark" /> -->
</div>
</template>
<script>
import { versionFormatting } from '@/libs/tools'
import { getVersionApi } from '@/apis/commApis'
import MyNavBar from '@/components/MyNavBar/MyNavBar.vue'
import MyTabBar from '@/components/MyTabBar/MyTabBar.vue'
import MyDialog from '@/components/MyDialog/MyDialog.vue'
import MyToast from '@/components/MyToast/MyToast.vue'
import MyConsole from '@/components/MyConsole/MyConsole.vue'
const myDialogConfig = {
title: '提示',
description: '',
descriptionType: 'html',
cancelBtnText: '取消'
}
export default {
name: 'myApp',
components: {
MyNavBar,
MyTabBar,
MyDialog,
MyToast,
MyConsole
},
data() {
return {
currentTabBarName: '',
dialogOpen: false,
isOpenToast: false,
systemInfo: {
version: '0.0.0',
statusHeight: 0
},
newVersion: '',
myDialogConfig: myDialogConfig,
rightBtnText: '',
loadingToast: null
}
},
computed: {
keepAlivePageNameList() {
return this.$router.options.routes
.filter(item => {
return item.meta.keepAlive
})
.map(item => {
return item.name
})
},
tabBarList() {
return this.$router.options.routes
.filter(item => {
return item.meta.isTabBarMenu
})
.map(item => {
return {
name: item.name,
icon: item.meta.icon,
label: item.meta.title
}
})
}
},
watch: {
$route(newVal) {
if (this.getIsApp) {
this.$store.commit('setAppNavBarShow', newVal.meta.showNavBar)
}
}
},
created() {
if (this.getIsApp) {
this.$store.commit('setAppNavBarShow', this.$route.meta.showNavBar)
// 监听App端传过来的系统信息
webf.methodChannel.addMethodCallHandler('postSystemInfo', event => {
console.log('event:', event)
this.systemInfo = event
this.$store.commit('setSystemInfo', event)
})
// 监听下载进度
webf.methodChannel.addMethodCallHandler('updateVersionProgress', event => {
this.loadingToast.ylTotasHintText = `下载中 ${event.toFixed(2)}%`
if (event === 100) {
this.loadingToast.ylTotasOpen = false
this.dialogOk = this.updateVerOkRestart
this.myDialogConfig.title = '下载完成'
this.myDialogConfig.description = '重启App即可更新'
this.myDialogConfig.cancelBtnText = '稍后重启'
this.dialogOpen = true
}
})
}
},
async mounted() {
setTimeout(() => {
this.currentTabBarName = this.$route.name
}, 60)
if (this.getIsApp) {
// 如果是APP,需要检查版本号判断有没有更新
try {
let res = await getVersionApi()
let oldVersion = versionFormatting(this.systemInfo.version)
let newVersion = versionFormatting(res.currentVersion)
this.newVersion = res.currentVersion
if (newVersion - oldVersion > 0) {
this.myDialogConfig.title = '更新提示'
this.myDialogConfig.description = res.description
this.dialogOpen = true
}
} catch (error) {
console.log('error:', error)
}
}
},
methods: {
// tabBar切换
tabBarChange(event) {
this.$router.replace({
name: event.name
})
},
backPrePage() {
console.log('返回:')
this.$router.go(-1)
},
othersBtnFun() {
console.log('this.getAppNavBarShow:', this.getAppNavBarShow)
},
dialogCancel() {
this.rightBtnText = '立即更新'
},
dialogOk() {
webf.methodChannel.invokeMethod('updateVersionDownloadConfirm', {
msg: '确定更新',
newVersion: this.newVersion
})
this.dialogOpen = false
this.loadingToast = this.$refs.myToast.loading({
hintText: '下载中',
duration: 0,
isOpen: true
})
},
// 确定重启更新-触发flutter中webf监听的事件
updateVerOkRestart() {
webf.methodChannel.invokeMethod('updateVersionRestart', {
msg: '确定更新重启'
})
}
}
}
</script>
<style lang="scss">
@import '@/assets/style/iconfont/iconfont.css';
@import '@/assets/style/vant-icon/vant-icon.css';
// @import '@/assets/style/common.scss';
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.router-page-view {
overflow-x: hidden;
&.is-tab-bar-menu-page {
padding-bottom: 54px;
}
}
#app {
overflow-x: hidden;
.my-tabbar-view {
position: fixed;
left: 0;
bottom: 0;
z-index: 999;
width: 100%;
}
}
</style>
... ...
import axios from '@/libs/api.request'
//获取版本信息
export const getVersionApi = params => {
return axios.request({
url: 'api/getVersion',
params,
method: 'get'
})
}
... ...
/* 多行行文本溢出出现省略号 */
.clamp {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}
.clamp.clamp-a {
-webkit-line-clamp: 1;
}
.clamp.clamp-b {
-webkit-line-clamp: 2;
}
.clamp.clamp-c {
-webkit-line-clamp: 3;
}
/* 开启硬件加速 */
.threeD {
transform: translateZ(0);
}
.anim {
transition: all 0.4s;
}
@keyframes turn {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(90deg);
}
50% {
transform: rotate(180deg);
}
75% {
transform: rotate(270deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes flicker {
0% {
transform: scale(1); /*开始为原始大小*/
}
25% {
transform: scale(1.1); /*放大1.1倍*/
}
50% {
transform: scale(1);
}
75% {
transform: scale(1.1);
}
}
.flicker {
animation: flicker 2s infinite;
}
@keyframes spin {
to {
transform: rotate(0deg);
}
from {
transform: rotate(-360deg);
}
}
.spin {
animation: spin 1s infinite;
animation-fill-mode: forwards;
}
.turn {
animation: turn 1s infinite;
animation-fill-mode: forwards;
}
.transform90 {
transform: rotate(90deg);
}
.pos-rel {
position: relative;
}
.pos-abs {
position: absolute;
}
.text-l {
text-align: left !important;
}
.text-r {
text-align: right !important;
}
.text-c {
text-align: center !important;
}
.left {
float: left;
}
.right {
float: right;
}
.clear:after {
display: block;
content: '.';
clear: both;
visibility: hidden;
overflow: hidden;
width: 100%;
height: 0;
}
.clear {
clear: both;
}
.flex {
display: flex;
}
.al-i-c {
align-items: center;
}
.ju-c-sb {
justify-content: space-between;
}
.ju-c-sa {
justify-content: space-around;
}
.ju-c-c {
justify-content: center;
}
.ju-c-end {
justify-content: flex-end;
}
@for $i from 1 through 50 {
.p-#{$i} {
padding: (1px * $i) !important;
}
.p-t-#{$i} {
padding-top: (1px * $i) !important;
}
.p-r-#{$i} {
padding-right: (1px * $i) !important;
}
.p-b-#{$i} {
padding-bottom: (1px * $i) !important;
}
.p-l-#{$i} {
padding-left: (1px * $i) !important;
}
.m-#{$i} {
margin: (1px * $i) !important;
}
.m-t-#{$i} {
margin-top: (1px * $i) !important;
}
.m-r-#{$i} {
margin-right: (1px * $i) !important;
}
.m-b-#{$i} {
margin-bottom: (1px * $i) !important;
}
.m-l-#{$i} {
margin-left: (1px * $i) !important;
}
.fz-#{$i} {
font-size: (1px * $i) !important;
}
}
.text-line-thr {
text-decoration: line-through !important;
}
... ...
@font-face {
font-family: 'iconfont';
src: url('iconfont.ttf?t=1687638242320') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-kaobei:before {
content: '\e601';
}
.icon-shezhi:before {
content: '\e827';
}
.icon-kefu:before {
content: '\e642';
}
.icon-loading-1:before {
content: '\e61d';
}
.icon-loading-2:before {
content: '\e93d';
}
.icon-loading-3:before {
content: '\e67b';
}
... ...
不能预览此文件类型
@font-face {
font-family: 'vant-icon';
src: url('vant-icon.ttf') format('truetype');
}
.van-icon {
font-family: 'vant-icon' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
... ...
不能预览此文件类型
<template>
<div class="collapse-com" :style="{ backgroundColor: backgroundColor }">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Collapse',
props: {
value: [String, Array, Number],
accordion: {
type: Boolean,
default: () => {
return false
}
},
backgroundColor: {
type: String,
default: '#f5f5f5'
}
},
data() {
return {
currentValue: this.value
}
},
mounted() {
this.setActive()
},
methods: {
setActive() {
// 为它下面的子元素都设置一个index值
// console.log("setActive");
const activeKey = this.getActiveKey()
this.$children.forEach((child, index) => {
const name = child.name || index.toString() // toString 1=>"1"整数转换成为字符串
child.isActive = activeKey.indexOf(name) > -1 // 给选中的元素赋值活跃状态
// console.log(child);
child.index = index
})
},
toggle(data) {
// console.log("toggle");
const name = data.name.toString() // 强行转换成为字符串
let newActivekey = []
if (this.accordion) {
// 如果是手风琴模式
if (!data.isActive) {
newActivekey.push(name)
}
} else {
let activeKey = this.getActiveKey()
const nameIndex = activeKey.indexOf(name)
if (data.isActive) {
// 如果当前是展开状态
if (nameIndex > -1) {
activeKey.splice(nameIndex, 1)
}
} else {
if (nameIndex < 0) {
activeKey.push(name)
}
}
newActivekey = activeKey
}
this.currentValue = newActivekey
// console.log(data);
this.$emit('input', newActivekey)
this.$emit('on-change', newActivekey)
},
getActiveKey() {
// 获取当前展开的元素,并且做成数组的形式 1 => ["1"]
let activeKey = this.currentValue || []
const accordion = this.accordion
if (!Array.isArray(activeKey)) {
// 判断 activeKey 是不是数组
activeKey = [activeKey] // 不是数组则让它变成数组
}
if (accordion && activeKey.length > 1) {
// 如果是手风琴模式,必定是只会有一个元素
activeKey = [activeKey[0]]
}
for (let i = 0; i < activeKey.length; i++) {
activeKey[i] = activeKey[i].toString()
}
return activeKey
}
},
watch: {
value(val) {
this.currentValue = val
},
currentValue() {
this.setActive()
}
}
}
</script>
<style lang="scss" scoped></style>
... ...
<template>
<div class="collapse-item-com" :style="{ marginBottom: marginBottom + 'px' }">
<div class="title-view" @click="toggle">
<div class="title-str">
<slot v-if="$slots.leftIcon" name="leftIcon"></slot>
<span v-else class="iconfont left-icon" :class="leftIcon"></span>
<slot v-if="$slots.title" name="title"></slot>
<span v-else class="title-text">{{ title }}</span>
</div>
<slot v-if="$slots.rightIcon" name="rightIcon"></slot>
<span v-else class="iconfont right-icon" :class="_rightIcon"></span>
</div>
<div class="collapse-extra" :style="{ height: isActive ? height + 'px' : '0' }" ref="collapseExtra">
<slot v-if="$slots.collapseExtra" name="collapseExtra"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'CollapseItem',
props: {
name: {
type: String
},
title: {
type: String,
default: null
},
leftIcon: {
type: String,
default: null
},
rightIcon: {
type: String,
default: null
},
marginBottom: {
type: [String, Number]
}
},
data() {
return {
index: 0,
height: 0,
isActive: false
}
},
computed: {
_rightIcon() {
if (this.rightIcon) {
return this.rightIcon
} else {
if (this.isActive) {
return 'icon-xiangshang2'
} else {
return 'icon-xiangxia2'
}
}
}
},
methods: {
toggle() {
this.$parent.toggle({
name: this.name || this.index,
isActive: this.isActive
})
}
},
mounted() {
this.$nextTick(() => {
this.height = this.$refs.collapseExtra.childNodes[0].offsetHeight
})
}
}
</script>
<style lang="scss" scoped>
.collapse-item-com {
background-color: #fff;
.title-view {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid #f5f5f5;
.title-str {
.left-icon {
color: #666;
font-size: 12px;
}
.title-text {
font-size: 16px;
color: #333;
}
}
.right-icon {
color: #666;
font-size: 12px;
}
}
.collapse-extra {
height: 0;
will-change: 'height';
overflow: hidden;
transition: all 0.2s ease 0s;
}
}
</style>
... ...
<template>
<div class="my-console-view" :class="[myPopupOpen ? 'show' : '']">
<!-- <vue-drag-resize
v-if="!getIsApp"
:w="81"
:h="32"
:x="230"
:y="400"
:isResizable="false"
:isActive="dragActive"
:parentLimitation="true"
@dragging="onDragging"
@activated="onClicked"
@deactivated="onDeactivated"
@dragstop="onDragStop"
>
<van-button class="open-console-btn" size="small" type="primary" @click.stop="_opeConsole">MyConsole</van-button>
</vue-drag-resize> -->
<van-button v-if="!getIsApp" class="open-console-btn not-app" size="small" type="primary" @click.stop="_opeConsole">MyConsole</van-button>
<MyPopup v-model="myPopupOpen" position="bottom" height="60" :theme="theme" content-bg-color="#1E1E20" :round="false" :show-close-icon="true" popup-title="控制台">
<div class="console-content">
<div class="tab-list" :class="`theme-${theme}`">
<div class="tab-item" v-for="(item, index) in tabList" :key="index" :class="{ 'current-item': current === item.value }" @click="changeTab(item.value)">
{{ item.name }}
</div>
</div>
<div class="all-cell" ref="allCell">
<template v-if="current !== 'network'">
<div class="cell-item" :class="`theme-${theme}`" v-for="(item, index) in consoleList" :key="index">
<template v-for="(oItem, oIndex) in item.data">
<div class="log-item" :class="oItem.logType === 'log' ? 'log' : oItem.logType === 'error' ? 'error' : ''" :key="oIndex">{{ oItem.log }}</div>
</template>
</div>
</template>
<template v-else-if="current === 'network'">
<div class="cell-item network-cell-item network-cell-header" :class="`theme-${theme}`">
<div class="network-item">Name</div>
<div class="network-item">Type</div>
<div class="network-item">Status</div>
<div class="network-item">Size</div>
<div class="network-item">Time</div>
</div>
<div style="padding-top: 34px; height: 100%; overflow-y: auto">
<div v-for="(item, index) in consoleList" class="cell-item network-cell-item" :class="[`theme-${theme}`]" :key="index" @click="lookNetworkDetail(item)">
<div class="network-item">{{ item.name }}</div>
<div class="network-item">{{ item.type }}</div>
<div class="network-item">{{ item.status }}</div>
<div class="network-item">{{ item.size }}</div>
<div class="network-item">{{ item.time }} ms</div>
</div>
</div>
</template>
</div>
<MyPopup
v-model="openApiDetailPopup"
position="right"
height="60"
width="70"
bottom="0"
content-bg-color="#1E1E20"
:theme="theme"
:round="false"
:show-close-icon="true"
popup-title="接口详情"
>
<div class="console-content" v-if="currentApiDetail">
<div class="all-cell detail-cell">
<van-cell title="名称" :label="currentApiDetail.name" />
<van-cell title="类型" :label="currentApiDetail.type" />
<van-cell class="pos-rel" title="地址" :label="currentApiDetail.responseURL">
<template #right-icon>
<span class="pos-abs iconfont icon-kaobei"></span>
</template>
</van-cell>
<van-cell title="状态" :label="currentApiDetail.status" />
<van-cell class="pos-rel" title="返回结果" style="white-space: pre" :label="currentApiDetail.response | prettyPrint">
<template #right-icon>
<span class="pos-abs iconfont icon-kaobei"></span>
</template>
</van-cell>
</div>
</div>
</MyPopup>
</div>
</MyPopup>
</div>
</template>
<script>
import { abilitySort } from '@/libs/tools'
import MyPopup from '@/components/MyPopup/MyPopup.vue'
export default {
name: 'MyConsole',
components: { MyPopup },
props: {
/**
* 主题色
* light | dark
*/
theme: {
type: String,
default: 'light'
}
},
data() {
return {
myPopupOpen: false,
consoleList: [],
dragActive: false,
top: 0,
left: 0,
sT: null,
draggNum: 0,
tabList: [
{ name: 'All', value: 'all' },
{ name: 'Log', value: 'log' },
{ name: 'Error', value: 'error' },
{ name: 'Network', value: 'network' }
],
current: 'all',
currentApiDetail: null,
openApiDetailPopup: false,
global: this.$global
}
},
watch: {
getLoggerData() {
this.changeTab(this.current)
},
getNetworkLoggerData() {
this.changeTab(this.current)
},
myPopupOpen(newVal) {
if (!newVal) {
this.consoleList = []
}
},
draggNum(newVal) {
if (newVal > 1) {
if (this.sT) {
clearTimeout(this.sT)
this.sT = null
}
}
}
},
computed: {
getLoggerData() {
return this.$store.getters.getLoggerData
},
getNetworkLoggerData() {
return this.$store.getters.getNetworkLoggerData
}
},
created() {
if (this.getIsApp) {
/**
* @description: 监听打开调试窗口
* @return {*}
*/
webf.methodChannel.addMethodCallHandler('openMyConsole', event => {
this._opeConsole()
})
/**
* @description: 监听请求拦截器新增请求
* @return {*}
*/
webf.methodChannel.addMethodCallHandler('addHttpInterceptors', event => {
console.log('addHttpInterceptors:', event)
console.netwokrLog({
uid: JSON.parse(event.headers).uid,
status: event.status || 0,
statusText: event.statusText || '',
name: event.url.split('/').pop().split('?')[0],
type: event.type,
requestHeaders: event.headers,
method: event.method,
responseURL: event.url,
response: null,
size: event.size || 0,
time: 0
})
})
// 监听请求拦截器更新请求
webf.methodChannel.addMethodCallHandler('updateHttpInterceptors', event => {
console.log('updateHttpInterceptors:', event)
let updateItem = this.$global.cNetworkData.filter(item => {
return item.uid === event.uid
})[0]
if (updateItem) {
Object.assign(updateItem, event)
// 触发获取返回结果
if (updateItem.type === 'Fetch/XHR') {
webf.methodChannel.invokeMethod('getResponseBody', event.uid)
}
}
})
// 监听请求拦截器更新返回结果事件
webf.methodChannel.addMethodCallHandler('httpInterceptorsSetResponseBody', event => {
console.log('httpInterceptorsSetResponseBody:', event)
let updateItem = this.$global.cNetworkData.filter(item => {
return item.uid === event.uid
})[0]
if (updateItem) {
updateItem.response = event.response
}
})
}
},
methods: {
_opeConsole() {
this.changeTab(this.current)
this.myPopupOpen = true
},
onDragging(newRect) {
this.draggNum += 1
this.top = newRect.top
this.left = newRect.left
},
onClicked() {
this.sT = setTimeout(() => {
this._opeConsole()
}, 200)
this.dragActive = true
},
onDeactivated() {
this.draggNum = 0
this.dragActive = false
},
onDragStop() {
this.dragActive = false
},
changeTab(value) {
this.current = value
let temp = []
if (value === 'all') {
temp = JSON.parse(JSON.stringify(this.getLoggerData))
} else if (value === 'log') {
temp = JSON.parse(JSON.stringify(this.getLoggerData)).filter(item => {
return item.logType === 'log'
})
} else if (value === 'error') {
temp = JSON.parse(JSON.stringify(this.getLoggerData)).filter(item => {
return item.logType === 'error'
})
} else if (value === 'network') {
temp = JSON.parse(JSON.stringify(this.getNetworkLoggerData))
this.consoleList = temp
return
}
this.consoleList = abilitySort(temp, 'logId')
this.$nextTick(() => {
this.scrollToBottom()
})
},
// 滚动到最底部
scrollToBottom() {
try {
let allCell = this.$refs['allCell']
allCell.scrollTop = allCell.scrollHeight
} catch (error) {}
},
// 查看接口详情
lookNetworkDetail(detail) {
this.currentApiDetail = detail
this.openApiDetailPopup = true
}
}
}
</script>
<style lang="scss" scoped>
.my-console-view {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
// pointer-events: none;
.console-content {
position: relative;
height: 100%;
.all-cell {
padding-top: 34px;
height: 100%;
overflow-y: auto;
position: relative;
&.detail-cell {
padding-top: 0;
.iconfont {
right: 20px;
}
}
}
.van-cell__title {
width: 100%;
.van-cell__label {
width: 100%;
overflow-x: auto;
}
}
}
.tab-list {
width: 100%;
height: 34px;
display: flex;
align-items: center;
justify-content: start;
position: absolute;
left: 0;
top: 0;
z-index: 100;
.tab-item {
height: 100%;
display: flex;
align-items: center;
padding: 0 10px;
}
&.theme-light {
color: #333;
border: 1px solid #eee;
background-color: #fff;
.tab-item {
border-right: 1px solid #eee;
}
}
&.theme-dark {
color: #a9b0b9;
border: 1px solid #333;
border-top: 0;
background-color: #252528;
.tab-item {
border-right: 1px solid #333;
&.current-item {
background-color: #000;
color: #fff;
}
}
}
}
.cell-item {
min-height: 44px;
display: flex;
flex-direction: column;
align-items: start;
justify-content: center;
font-size: 14px;
&:last-child {
border-bottom: 0;
}
&.network-cell-item {
flex-direction: row;
align-items: center;
justify-content: start;
&.network-cell-header {
position: absolute;
top: 34px;
left: 0;
min-height: unset;
height: 34px;
width: 100%;
.network-item {
height: 34px;
line-height: 34px;
}
}
.network-item {
height: 44px;
line-height: 44px;
flex: 1;
padding-left: 6px;
overflow: hidden;
}
}
.log-item {
padding: 4px 6px;
width: 100%;
&.error {
background-color: #210700;
color: #f00;
border-bottom: 1px solid #491100;
}
}
&.theme-light {
color: #333;
border-bottom: 1px solid #eee;
background-color: #fff;
&.network-cell-header {
background-color: #fff;
}
&.network-cell-item {
.network-item {
border-right: 1px solid #eee;
}
}
}
&.theme-dark {
color: #a9b0b9;
border-bottom: 1px solid #333;
&.network-cell-header {
background-color: #252528;
}
&.network-cell-item {
.network-item {
border-right: 1px solid #333;
}
}
}
}
.open-console-btn {
// pointer-events: auto;
z-index: 1000;
border-radius: 6px;
&.not-app {
position: fixed;
bottom: 20%;
right: 50px;
}
}
.close-drag-active {
position: fixed;
top: -100%;
opacity: 0;
}
}
</style>
... ...
<template>
<div class="my-popup" v-if="isShow">
<div class="mask-view"></div>
<div class="popup-content">
<div class="popup-title">{{ title }}</div>
<div class="popup-description" v-if="descriptionType === ''">{{ description }}</div>
<div class="popup-description" v-if="descriptionType === 'html'" v-html="description"></div>
<div class="popup-btn-list">
<div class="btn" v-if="showCancelBtn" @click="_cancel">{{ cancelBtnText }}</div>
<div class="btn" v-if="showOkBtn" @click="_ok">{{ okBtnText }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'MyPopup',
props: {
showPopup: {
type: Boolean,
default: () => {
return false
}
},
title: {
type: String,
default: '提示'
},
description: {
type: String,
default: '内容'
},
descriptionType: {
type: String,
default: ''
},
showOkBtn: {
type: Boolean,
default: () => {
return true
}
},
showCancelBtn: {
type: Boolean,
default: () => {
return false
}
},
okBtnText: {
type: String,
default: '确定'
},
cancelBtnText: {
type: String,
default: '取消'
},
okClose: {
type: Boolean,
default: () => {
return false
}
}
},
watch: {
showPopup(newVal) {
this.isShow = newVal
}
},
created() {
this.isShow = this.showPopup
},
data() {
return {
isShow: false
}
},
methods: {
_ok() {
if (this.okClose) {
this.isShow = false
}
this.$emit('ok')
},
_cancel() {
this.isShow = false
// this.$emit('cancel')
}
}
}
</script>
<style lang="scss" scoped>
.my-popup {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 999;
.mask-view {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: #000;
opacity: 0.5;
z-index: 10;
}
.popup-content {
position: fixed;
left: 20px;
right: 20px;
top: 50%;
transform: translateY(-50%);
background-color: #fff;
border-radius: 14px;
z-index: 11;
.popup-title {
font-size: 16px;
font-weight: bold;
height: 44px;
line-height: 44px;
text-align: center;
border-bottom: 1px solid #ededed;
}
.popup-description {
padding: 15px;
}
.popup-btn-list {
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid #ededed;
.btn {
flex: 1;
text-align: center;
height: 44px;
line-height: 44px;
font-size: 14px;
}
}
}
}
</style>
... ...
<template>
<div class="app-nav-bar" v-if="show" :style="{ height: 46 + offsetTop + 'px', paddingTop: offsetTop + 'px', backgroundColor: background }">
<slot v-if="$slots.leftArrow" name="leftArrow"></slot>
<div v-else-if="leftArrow" class="left-arrow" @click="leftClick" :style="leftArrowStyle">
<van-icon name="arrow-left" />
<span class="l-text">{{ leftText }}</span>
</div>
<div class="nav-title" :style="navTitleStyle">{{ title }}</div>
<slot v-if="$slots.rightArrow" name="rightArrow"></slot>
<div v-else-if="rightArrow" class="right-arrow" @click="rightClick" :style="rightArrowStyle">
<span class="r-text">{{ rightText }}</span>
</div>
</div>
</template>
<script>
export default {
name: 'MyNavBar',
props: {
show: {
type: Boolean,
default: () => {
return false
}
},
title: {
type: String,
default: ''
},
navTitleStyle: {
type: Boolean,
default: () => {
return false
}
},
offsetTop: {
type: Number,
default: 0
},
background: {
type: String,
default: '#fff'
},
leftArrow: {
type: Boolean,
default: () => {
return false
}
},
leftText: {
type: String,
default: ''
},
leftArrowStyle: {
type: Object,
default: () => {
return {}
}
},
rightArrow: {
type: Boolean,
default: () => {
return false
}
},
rightText: {
type: String,
default: ''
},
rightArrowStyle: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {}
},
methods: {
leftClick() {
this.$emit('left-callback')
},
rightClick() {
this.$emit('right-click')
}
}
}
</script>
<style lang="scss" scoped>
.app-nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: 46px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #ebedf0;
.nav-title {
font-size: 16px;
font-weight: bold;
color: #323233;
}
.left-arrow {
position: absolute;
z-index: 10;
top: 0;
left: 10px;
min-width: 46px;
height: 46px;
display: flex;
align-items: center;
color: #333;
font-size: 14px;
.l-text {
margin-left: 4px;
}
}
.right-arrow {
position: absolute;
z-index: 10;
top: 0;
right: 10px;
min-width: 46px;
height: 46px;
display: flex;
align-items: center;
color: #333;
font-size: 14px;
}
}
</style>
... ...
<template>
<div class="my-popup-view" :class="`theme-${theme}`" v-if="open">
<div v-if="overlay" class="popup-mask-view" @click="closeOnClickOverlay ? _close() : ''"></div>
<div class="popup-content-view" :class="`position-${position} ${transition ? `position-${position}-enter-to` : `position-${position}-leave-to`}`" :style="contentStyle">
<div
v-if="popupTitle !== '' || showCloseIcon"
class="popup-title"
:style="[popupTitleTextStyle, theme !== 'light' && theme !== 'dark' ? { backgroundColor: contentBgColor } : {}]"
>
<div v-if="showCloseIcon" class="close-btn van-icon van-icon-cross" @click="_close"></div>
<span>{{ popupTitle }}</span>
</div>
<div class="popup-slot">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'MyPopup',
props: {
theme: {
type: String,
default: 'light'
},
// 是否显示
value: {
type: Boolean,
required: true
},
// 遮罩
overlay: {
type: Boolean,
default: () => {
return true
}
},
closeOnClickOverlay: {
type: Boolean,
default: () => {
return true
}
},
popupTitle: {
type: String,
default: ''
},
popupTitleTextStyle: {
type: Object,
default: () => {
return {}
}
},
contentBgColor: {
type: String,
default: '#fff'
},
showCloseIcon: {
type: Boolean,
default: () => {
return false
}
},
// 弹出位置
// position.options: top bottom right left
position: {
type: String,
default: 'center'
},
// 圆角
round: {
type: Boolean,
default: () => {
return false
}
},
width: {
type: [String, Number],
default: '80'
},
height: {
type: [String, Number],
default: '300'
},
bottom: {
type: String,
default: null
}
},
watch: {
isOpen(newVal) {
if (typeof newVal === 'boolean') {
this.open = newVal
} else {
newVal.then(res => (this.open = res))
}
}
},
computed: {
isOpen: {
get() {
if (this.value) {
setTimeout(() => {
this.transition = true
}, 100)
return this.value
} else {
this.transition = false
return new Promise(resolve => {
setTimeout(() => {
resolve(false)
}, 300)
})
}
},
set(show) {
this.$emit('input', show)
}
},
contentStyle() {
let bc = {}
if (this.theme !== 'light' && this.theme !== 'dark') {
bc = {
backgroundColor: this.contentBgColor
}
}
if (this.position === 'center') {
return Object.assign(bc, {
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)'
})
} else if (this.position === 'top') {
return Object.assign(bc, {
left: 0,
top: '-100%',
maxHeight: '100%',
width: '100%',
height:
typeof this.height === 'string' && this.height.indexOf('%') !== -1
? this.height
: typeof this.height === 'string' && this.height.indexOf('%') === -1 && Number(this.height) < 100
? this.height + '%'
: typeof this.height === 'string' && this.height.indexOf('%') === -1 && Number(this.height) > 100
? this.height + 'px'
: typeof this.height === 'number'
? this.height + 'px'
: '300px',
'border-bottom-left-radius': this.round ? '12px' : '0',
'border-bottom-right-radius': this.round ? '12px' : '0'
})
} else if (this.position === 'bottom') {
return Object.assign(bc, {
left: 0,
bottom: '-100%',
maxHeight: '100%',
width: '100%',
height:
typeof this.height === 'string' && this.height.indexOf('%') !== -1
? this.height
: typeof this.height === 'string' && this.height.indexOf('%') === -1 && Number(this.height) < 100
? this.height + '%'
: typeof this.height === 'string' && this.height.indexOf('%') === -1 && Number(this.height) > 100
? this.height + 'px'
: typeof this.height === 'number'
? this.height + 'px'
: '300px',
'border-top-left-radius': this.round ? '12px' : '0',
'border-top-right-radius': this.round ? '12px' : '0'
})
} else if (this.position === 'left') {
return Object.assign(bc, {
left: '-100%',
top: this.bottom ? 'unset' : '0',
bottom: this.bottom ? this.bottom : 'unset',
maxWidth: '100%',
width:
typeof this.width === 'string' && this.width.indexOf('%') !== -1
? this.width
: typeof this.width === 'string' && this.width.indexOf('%') === -1 && Number(this.width) < 100
? this.width + '%'
: typeof this.width === 'string' && this.width.indexOf('%') === -1 && Number(this.width) > 100
? this.width + 'px'
: typeof this.width === 'number'
? this.width + 'px'
: '240px',
height:
typeof this.height === 'string' && this.height.indexOf('%') !== -1
? this.height
: typeof this.height === 'string' && this.height.indexOf('%') === -1 && Number(this.height) < 100
? this.height + '%'
: typeof this.height === 'string' && this.height.indexOf('%') === -1 && Number(this.height) > 100
? this.height + 'px'
: typeof this.height === 'number'
? this.height + 'px'
: '100%',
'border-top-right-radius': this.round ? '12px' : '0',
'border-bottom-right-radius': this.round ? '12px' : '0'
})
} else if (this.position === 'right') {
return Object.assign(bc, {
right: '-100%',
top: this.bottom ? 'unset' : '0',
bottom: this.bottom ? this.bottom : 'unset',
maxWidth: '100%',
width:
typeof this.width === 'string' && this.width.indexOf('%') !== -1
? this.width
: typeof this.width === 'string' && this.width.indexOf('%') === -1 && Number(this.width) < 100
? this.width + '%'
: typeof this.width === 'string' && this.width.indexOf('%') === -1 && Number(this.width) > 100
? this.width + 'px'
: typeof this.width === 'number'
? this.width + 'px'
: '240px',
height:
typeof this.height === 'string' && this.height.indexOf('%') !== -1
? this.height
: typeof this.height === 'string' && this.height.indexOf('%') === -1 && Number(this.height) < 100
? this.height + '%'
: typeof this.height === 'string' && this.height.indexOf('%') === -1 && Number(this.height) > 100
? this.height + 'px'
: typeof this.height === 'number'
? this.height + 'px'
: '100%',
'border-top-left-radius': this.round ? '12px' : '0',
'border-bottom-left-radius': this.round ? '12px' : '0'
})
}
}
},
data() {
return {
open: false,
transition: false
}
},
mounted() {
this.isOpen.then(value => {
this.open = value
})
},
methods: {
_close() {
this.isOpen = false
}
}
}
</script>
<style lang="scss" scoped>
.my-popup-view {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 999999;
pointer-events: auto;
.popup-mask-view {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 10;
background-color: #000;
opacity: 0.5;
}
.popup-content-view {
position: absolute;
z-index: 11;
overflow-y: auto;
transition: all 0.3s;
&.position-top-leave-to {
top: -100% !important;
}
&.position-top-enter-to {
top: 0 !important;
}
&.position-bottom-leave-to {
bottom: -100% !important;
}
&.position-bottom-enter-to {
bottom: 0 !important;
}
&.position-left-leave-to {
left: -100% !important;
}
&.position-left-enter-to {
left: 0 !important;
}
&.position-right-leave-to {
right: -100% !important;
}
&.position-right-enter-to {
right: 0 !important;
}
.popup-title {
padding: 0 10px;
font-size: 16px;
font-weight: bold;
text-align: center;
height: 44px;
line-height: 44px;
position: relative;
}
.close-btn {
width: 36px;
height: 36px;
line-height: 36px;
position: absolute;
top: 4px;
right: 5px;
text-align: center;
font-size: 16px;
color: #888;
}
.popup-slot {
height: calc(100% - 44px);
}
}
&.theme-light {
.popup-content-view {
background-color: #fff;
.popup-title {
background-color: #fff;
border-bottom: 1px solid #eee;
color: #333;
}
}
}
&.theme-dark {
.popup-content-view {
background-color: #1e1e20;
.popup-title {
background-color: #252528;
border-bottom: 1px solid #414346;
color: #797c82;
}
}
}
}
</style>
... ...
<template>
<div class="tab-bar-view">
<div class="tab-bar-item" v-for="(item, index) in tabBarList" :key="index" :class="{ activity: activityTabBarIndex === index }" @click="itemClick(item, index)">
<div v-if="item.icon" class="tab-bar-icon van-icon" :class="`van-icon-${item.icon}`" @click.stop="itemClick(item, index)"></div>
<div class="tab-bar-name">{{ item.label }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'MyTabBar',
props: {
value: {
type: [String, Number],
required: true
},
tabBarList: {
type: Array,
default: () => {
return []
}
}
},
computed: {
activityTabBarIndex: {
get() {
let currentIndex = 0
if (typeof this.value === 'string') {
this.tabBarList.forEach((item, index) => {
if (item.name === this.value) {
currentIndex = index
}
})
} else if (typeof this.value === 'number') {
currentIndex = this.value
}
return currentIndex
},
set(index) {
this.$emit('input', index)
}
}
},
data() {
return {}
},
methods: {
itemClick(item, index) {
console.log('tabbar-itemClick-index', index)
this.activityTabBarIndex = index
this.$emit('change', item)
}
}
}
</script>
<style lang="scss" scoped>
.tab-bar-view {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 51px;
display: flex;
align-items: center;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
background-color: #fff;
box-shadow: 0 0 10px #d9d9d9;
border-top: 1px solid #e3e3e3;
.tab-bar-item {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
font-size: 12px;
line-height: 1;
color: #7d7e80;
cursor: pointer;
.tab-bar-icon {
margin-bottom: 4px;
font-size: 22px;
}
&.activity {
color: #1989fa;
}
}
}
</style>
... ...
<template>
<div class="dyson-toast" v-if="ylTotasOpen">
<div v-if="ylToastType === 'hint'" class="toast-view hint-toast">
<div class="totas-text">{{ ylTotasHintText }}</div>
</div>
<div v-if="ylToastType === 'loading'" class="toast-view loading-toast">
<div class="loading-img" v-if="isShowIcon">
<span class="iconfont" :class="ylTotasLoadingIcon"></span>
</div>
<div class="totas-text">{{ ylTotasHintText }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'MyToast',
props: {
isOpen: {
type: Boolean,
default: () => {
return false
}
},
isShowIcon: {
type: Boolean,
default: () => {
return true
}
},
toastType: {
type: String,
default: 'hint'
},
hintText: {
type: String,
default: ''
},
duration: {
type: Number,
default: 3000
},
loadingIcon: {
type: String,
default: 'icon-loading-3'
}
},
data() {
return {
ylTotasOpen: false,
ylToastType: '',
ylTotasHintText: '',
ylTotasDuration: 0,
ylTotasLoadingIcon: '',
st: null
}
},
watch: {
ylTotasOpen(newVal) {
if (newVal && this.ylToastType !== 'loading') {
if (this.ylTotasDuration !== 0) {
this.st = setTimeout(() => {
this.ylTotasOpen = false
}, this.ylTotasDuration)
}
}
},
isOpen(newVal) {
this.ylTotasOpen = newVal
},
hintText(newVal) {
this.ylTotasHintText = newVal
}
},
created() {
this.ylTotasOpen = this.isOpen
this.ylTotasHintText = this.hintText
this.ylToastType = this.toastType
this.ylTotasDuration = this.duration
this.ylTotasLoadingIcon = this.loadingIcon
},
methods: {
loading(config) {
if (this.st) {
clearTimeout(this.st)
this.st = null
}
this.ylToastType = 'loading'
this.ylTotasHintText = config.hintText
this.ylTotasDuration = config.duration
this.ylTotasOpen = config.isOpen
if (config.loadingIcon) {
this.ylTotasLoadingIcon = config.loadingIcon
}
return this
}
}
}
</script>
<style lang="scss" scoped>
.dyson-toast {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
.toast-view {
min-width: 130px;
padding: 8px 30px;
border-radius: 6px;
background-color: #333;
.totas-text {
font-size: 12px;
color: #fff;
text-align: center;
}
}
.loading-toast {
.loading-img {
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
animation: rotate 1s linear infinite;
.iconfont {
color: #fff;
font-size: 18px;
}
}
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(90deg);
}
50% {
transform: rotate(180deg);
}
75% {
transform: rotate(270deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
... ...
export default {
/**
* @description 配置显示在浏览器标签的title
*/
title: 'Admin',
/**
* @description token在Cookie中存储的天数,默认1天
*/
cookieExpires: 1,
/**
* @description 是否使用国际化,默认为false
* 如果不使用,则需要在路由中给需要在菜单中展示的路由设置meta: {title: 'xxx'}
* 用来在菜单中显示文字
*/
useI18n: true,
/**
* @description api请求基础路径
*/
baseUrl: {
dev: 'https://mock.apifox.cn/m1/2852848-0-default/',
devOnLine: 'https://mock.apifox.cn/m1/2852848-0-default/',
uat: 'https://mock.apifox.cn/m1/2852848-0-default/',
pro: 'https://mock.apifox.cn/m1/2852848-0-default/'
},
imgToCos: {
dev: false,
uat: true,
pro: true
},
//配置图片请求地址
imgBaseUrl: {
dev: 'http://localhost:3001',
// dev: 'http://localhost:3001/admin/cosGetObject?key=',
devOnLine: 'http://localhost:3001/admin/cosGetObject?key=',
uat: 'http://localhost:3001/admin/cosGetObject?key=',
pro: 'http://localhost:3001/admin/cosGetObject?key='
},
/**
* @description 默认打开的首页的路由name值,默认为home
*/
homeName: 'home',
/**
* @description 需要加载的插件
*/
plugin: {
'error-store': {
showInHeader: false, // 设为false后不会在顶部显示错误日志徽标
developmentOff: true // 设为true后在开发环境不会收集错误信息,方便开发中排查错误
}
}
}
... ...
export function prettyPrint(value) {
if (value === null) {
return '-'
}
return JSON.stringify(JSON.parse(value), null, '\t')
}
... ...
// import HttpRequest from '@/libs/axios'
// import config from '@/config'
// const baseUrl = config.baseUrl[process.env.VUE_APP_NAME]
// const axios = new HttpRequest(baseUrl)
// export default axios
import HttpRequest from '@/libs/fetch'
import config from '@/config'
const baseUrl = config.baseUrl[process.env.VUE_APP_NAME]
const axios = new HttpRequest(baseUrl)
export default axios
... ...
import axios from 'axios'
import router from '@/router'
import store from '@/store'
import { Notify } from 'vant'
import { setToken, getToken } from '@/libs/util'
const addErrorLog = errorInfo => {
const { statusText, status, config } = errorInfo
let info = {
type: 'ajax',
code: status,
// osInfo: getOs(),
// browserInfo: getBrowser().browser + '/' + getBrowser().version,
msg: errorInfo.data.message ? errorInfo.data.message : statusText,
url: config.url,
dataInfo: config.data ? JSON.parse(config.data) : {},
headers: config.headers
}
if (!config.url.includes('save_error_logger')) {
store.dispatch('addErrorLog', info)
}
}
class HttpRequest {
path = ''
curPath = ''
constructor(baseUrl = baseURL) {
this.baseUrl = baseUrl
this.queue = {}
}
getInsideConfig() {
const config = {
baseURL: this.baseUrl,
headers: {}
}
if (getToken()) {
config.headers.Authorization = 'Bearer ' + getToken()
}
return config
}
destroy(url) {
delete this.queue[url]
if (!Object.keys(this.queue).length) {
// Spin.hide()
}
}
interceptors(instance, url) {
// 请求拦截
instance.interceptors.request.use(
config => {
// 添加全局的loading...
// if (!Object.keys(this.queue).length) {
// Spin.show() // 不建议开启,因为界面不友好
// }
this.queue[url] = true
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截
instance.interceptors.response.use(
response => {
this.destroy(url)
// console.log(response)
if (response.data.code == 200) {
return response.data
} else {
Notify({ type: 'warning', message: response.data.message })
return Promise.reject(response.data.message)
}
},
error => {
this.destroy(url)
let errorInfo = error.response
console.log('errorInfo:', errorInfo)
if (!errorInfo) {
const {
request: { statusText, status },
config
} = JSON.parse(JSON.stringify(error))
console.log('config:', config)
errorInfo = {
statusText,
status,
config
}
}
addErrorLog(errorInfo)
if (errorInfo.status === 401) {
setToken('')
router.replace({
name: 'login'
})
}
return Promise.reject(error)
}
)
}
setPath(...paths) {
this.curPath = `${this.path}/${paths.join('/')}`
return this
}
replace(...params) {
let count = 0
this.curPath = this.curPath.replace(/\{.*?\}/g, _match => params[count++])
return this
}
request(options) {
const instance = axios.create()
options = Object.assign(this.getInsideConfig(), options)
if (this.curPath !== '') {
options.url = this.curPath
this.curPath = ''
this.path = ''
}
this.interceptors(instance, options.url)
return instance(options)
}
}
export default HttpRequest
... ...
import router from '@/router'
// import store from '@/store'
// import { Toast } from 'vant'
import { setToken, getToken } from '@/libs/util'
const addErrorLog = errorInfo => {
const { statusText, status, url, headers, data } = errorInfo
let info = {
type: 'ajax',
code: status,
// osInfo: getOs(),
// browserInfo: getBrowser().browser + '/' + getBrowser().version,
msg: errorInfo.message ? errorInfo.message : statusText,
url: url,
dataInfo: data,
headers: headers
}
// if (!url.includes('save_error_logger')) {
// store.dispatch('addErrorLog', info)
// }
}
class HttpRequest {
path = ''
curPath = ''
constructor(baseURL) {
this.baseURL = baseURL
}
setPath(...paths) {
this.curPath = `${this.path}/${paths.join('/')}`
return this
}
replace(...params) {
let count = 0
this.curPath = this.curPath.replace(/\{.*?\}/g, _match => params[count++])
return this
}
request(parames) {
let url = parames.url || ''
let method = parames.method || 'GET'
let data = parames.data || parames.params
let requestUrl = url.indexOf('http://') === -1 && url.indexOf('https://') === -1 ? this.baseURL + url : url
if (this.curPath !== '') {
requestUrl = this.curPath
this.curPath = ''
this.path = ''
}
let options = {
method: method.toUpperCase(),
headers: {
'Content-Type': 'application/json; charset=UTF-8'
},
body: JSON.stringify(data)
}
// 设置token
if (getToken()) {
options.headers.token = getToken()
}
// get请求转换参数
if (method == 'GET') {
delete options.body
let qs = '?'
for (const key in parames.data) {
qs += key + '=' + parames.data[key] + '&'
}
qs = qs.substring(0, qs.length - 1)
qs.length > 1 ? (requestUrl += qs) : ''
}
return new Promise((resolve, reject) => {
fetch(requestUrl, options)
.then(function (response) {
if (response.ok) {
return response.json()
} else {
const errorInfo = {
statusText: response.statusText,
status: response.status,
url: response.url,
headers: JSON.stringify(response.headers),
data: response.data
}
addErrorLog(errorInfo)
if (response.status === 404) {
error('服务器开小差了,请联系客服~')
}
reject(response)
}
})
.then(res => {
successCallBack(res, resolve, reject)
})
.catch(err => {
error()
})
})
}
}
function error(msg = '请检查您的网络') {
// Toast({
// message: msg,
// icon: 'none',
// duration: 3500
// })
}
function successCallBack(res, resolve, reject) {
switch (res.code) {
case 200:
resolve(res.data)
break
case 401:
error('请重新登录!')
setToken('')
router.replace({
name: 'login'
})
break
default:
businessError(res, reject)
break
}
}
function businessError(err, reject) {
// Toast({
// message: err.message,
// duration: 2000
// })
reject(err)
}
export default HttpRequest
... ...
var storage = {
get: (name) => {
return localStorage.getItem(name)
? JSON.parse(localStorage.getItem(name))
: null
},
set: (name, val) => {
localStorage.setItem(name, JSON.stringify(val))
},
remove: (name) => {
localStorage.removeItem(name)
}
}
export default storage
... ...
export const forEach = (arr, fn) => {
if (!arr.length || !fn) return
let i = -1
let len = arr.length
while (++i < len) {
let item = arr[i]
fn(item, i, arr)
}
}
/**
* @param {Array} arr1
* @param {Array} arr2
* @description 得到两个数组的交集, 两个数组的元素为数值或字符串
*/
export const getIntersection = (arr1, arr2) => {
let len = Math.min(arr1.length, arr2.length)
let i = -1
let res = []
while (++i < len) {
const item = arr2[i]
if (arr1.indexOf(item) > -1) res.push(item)
}
return res
}
/**
* @param {Array} arr1
* @param {Array} arr2
* @description 得到两个数组的并集, 两个数组的元素为数值或字符串
*/
export const getUnion = (arr1, arr2) => {
return Array.from(new Set([...arr1, ...arr2]))
}
/**
* @param {Array} target 目标数组
* @param {Array} arr 需要查询的数组
* @description 判断要查询的数组是否至少有一个元素包含在目标数组中
*/
export const hasOneOf = (targetarr, arr) => {
return targetarr.some(_ => arr.indexOf(_) > -1)
}
/** 根据传入的字段进行分组
* @param arr 需要分组的数组
* @param property 分组的字段
* @returns {*[]} 已分好组的数组
*/
export const abilitySort = (arr, property) => {
let map = {}
for (let i = 0; i < arr.length; i++) {
const ai = arr[i]
if (!map[ai[property]]) map[ai[property]] = [ai]
else map[ai[property]].push(ai)
}
let res = []
Object.keys(map).forEach(key => {
res.push({ [property]: key, data: map[key] })
})
return res
}
/**
* @param {String|Number} value 要验证的字符串或数值
* @param {*} validList 用来验证的列表
*/
export function oneOf(value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) {
return true
}
}
return false
}
/**
* @param {Number} timeStamp 判断时间戳格式是否是毫秒
* @returns {Boolean}
*/
const isMillisecond = timeStamp => {
const timeStr = String(timeStamp)
return timeStr.length > 10
}
/**
* @param {Number} timeStamp 传入的时间戳
* @param {Number} currentTime 当前时间时间戳
* @returns {Boolean} 传入的时间戳是否早于当前时间戳
*/
const isEarly = (timeStamp, currentTime) => {
return timeStamp < currentTime
}
/**
* @param {Number} num 数值
* @returns {String} 处理后的字符串
* @description 如果传入的数值小于10,即位数只有1位,则在前面补充0
*/
const getHandledValue = num => {
return num < 10 ? '0' + num : num
}
/**
* @param {Number} timeStamp 传入的时间戳
* @param {Number} startType 要返回的时间字符串的格式类型,传入'year'则返回年开头的完整时间
*/
export const getDate = (timeStamp, startType) => {
const d = new Date(timeStamp * 1000)
const year = d.getFullYear()
const month = getHandledValue(d.getMonth() + 1)
const date = getHandledValue(d.getDate())
const hours = getHandledValue(d.getHours())
const minutes = getHandledValue(d.getMinutes())
const second = getHandledValue(d.getSeconds())
let resStr = ''
if (startType === 'year') {
resStr = year + '-' + month + '-' + date + ' ' + hours + ':' + minutes + ':' + second
} else resStr = month + '-' + date + ' ' + hours + ':' + minutes
return resStr
}
/**
* @param {String|Number} timeStamp 时间戳
* @returns {String} 相对时间字符串
*/
export const getRelativeTime = timeStamp => {
// 判断当前传入的时间戳是秒格式还是毫秒
const IS_MILLISECOND = isMillisecond(timeStamp)
// 如果是毫秒格式则转为秒格式
if (IS_MILLISECOND) Math.floor((timeStamp /= 1000))
// 传入的时间戳可以是数值或字符串类型,这里统一转为数值类型
timeStamp = Number(timeStamp)
// 获取当前时间时间戳
const currentTime = Math.floor(Date.parse(new Date()) / 1000)
// 判断传入时间戳是否早于当前时间戳
const IS_EARLY = isEarly(timeStamp, currentTime)
// 获取两个时间戳差值
let diff = currentTime - timeStamp
// 如果IS_EARLY为false则差值取反
if (!IS_EARLY) diff = -diff
let resStr = ''
const dirStr = IS_EARLY ? '前' : '后'
// 少于等于59秒
if (diff <= 59) resStr = diff + '秒' + dirStr
// 多于59秒,少于等于59分钟59秒
else if (diff > 59 && diff <= 3599) {
resStr = Math.floor(diff / 60) + '分钟' + dirStr
}
// 多于59分钟59秒,少于等于23小时59分钟59秒
else if (diff > 3599 && diff <= 86399) {
resStr = Math.floor(diff / 3600) + '小时' + dirStr
}
// 多于23小时59分钟59秒,少于等于29天59分钟59秒
else if (diff > 86399 && diff <= 2623859) {
resStr = Math.floor(diff / 86400) + '天' + dirStr
}
// 多于29天59分钟59秒,少于364天23小时59分钟59秒,且传入的时间戳早于当前
else if (diff > 2623859 && diff <= 31567859 && IS_EARLY) {
resStr = getDate(timeStamp)
} else resStr = getDate(timeStamp, 'year')
return resStr
}
/**
* @returns {String} 当前浏览器名称
*/
export const getExplorer = () => {
const ua = window.navigator.userAgent
const isExplorer = exp => {
return ua.indexOf(exp) > -1
}
if (isExplorer('MSIE')) return 'IE'
else if (isExplorer('Firefox')) return 'Firefox'
else if (isExplorer('Chrome')) return 'Chrome'
else if (isExplorer('Opera')) return 'Opera'
else if (isExplorer('Safari')) return 'Safari'
}
/**
* @description 绑定事件 on(element, event, handler)
*/
export const on = (function () {
if (document.addEventListener) {
return function (element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false)
}
}
} else {
return function (element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler)
}
}
}
})()
/**
* @description 解绑事件 off(element, event, handler)
*/
export const off = (function () {
if (document.removeEventListener) {
return function (element, event, handler) {
if (element && event) {
element.removeEventListener(event, handler, false)
}
}
} else {
return function (element, event, handler) {
if (element && event) {
element.detachEvent('on' + event, handler)
}
}
}
})()
/**
* 判断一个对象是否存在key,如果传入第二个参数key,则是判断这个obj对象是否存在key这个属性
* 如果没有传入key这个参数,则判断obj对象是否有键值对
*/
export const hasKey = (obj, key) => {
if (key) return key in obj
else {
let keysArr = Object.keys(obj)
return keysArr.length
}
}
/**
* @param {*} obj1 对象
* @param {*} obj2 对象
* @description 判断两个对象是否相等,这两个对象的值只能是数字或字符串
*/
export const objEqual = (obj1, obj2) => {
const keysArr1 = Object.keys(obj1)
const keysArr2 = Object.keys(obj2)
if (keysArr1.length !== keysArr2.length) return false
else if (keysArr1.length === 0 && keysArr2.length === 0) return true
/* eslint-disable-next-line */ else {
return !keysArr1.some(key => obj1[key] != obj2[key])
}
}
// 检测json格式
export const isJSON = str => {
if (typeof str === 'string') {
try {
var obj = JSON.parse(str)
if (typeof obj === 'object' && obj) {
return true
} else {
return false
}
} catch (e) {
return false
}
} else if (typeof str === 'object' && str) {
return true
}
}
// 美化json字符串
export const beautifyJSONFormat = str => {
if (!str) return
return JSON.stringify(JSON.parse(str), null, '\t')
}
export const handleFullscreen = type => {
let main = document.body
if (!type) {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
}
} else {
if (main.requestFullscreen) {
main.requestFullscreen()
} else if (main.mozRequestFullScreen) {
main.mozRequestFullScreen()
} else if (main.webkitRequestFullScreen) {
main.webkitRequestFullScreen()
} else if (main.msRequestFullscreen) {
main.msRequestFullscreen()
}
}
}
//深拷贝对象
export const deepCopyObj = obj => {
return JSON.parse(JSON.stringify(obj))
}
// 正则去除字符串空格
export const strTrim = str => {
return str.replace(/\s*/g, '')
}
// 版本号处理
export const versionFormatting = version => {
return Number(version.split('.').join(''))
}
// 递归打印对象
export const logProperties = obj => {
if (typeof obj !== 'object' || obj === null) {
console.log(obj)
return
}
var properties = Object.getOwnPropertyNames(obj)
properties.forEach(function (prop) {
console.log(prop + ':', obj[prop])
logProperties(obj[prop])
})
}
... ...
import config from '@/config'
import storage from '@/libs/storageUtil'
import { forEach, hasOneOf, objEqual } from '@/libs/tools'
const { title, useI18n } = config
export const TOKEN_KEY = 'token'
export const setToken = token => {
storage.set(TOKEN_KEY, token)
}
export const getToken = () => {
const token = storage.get(TOKEN_KEY)
if (token) return token
else return false
}
export const hasChild = item => {
return item.children && item.children.length !== 0
}
const showThisMenuEle = (item, access) => {
if (item.meta && item.meta.access && item.meta.access.length) {
if (hasOneOf(item.meta.access, access)) return true
else return false
} else return true
}
/**
* @param {Array} list 通过路由列表得到菜单列表
* @returns {Array}
*/
export const getMenuByRouter = (list, access) => {
let res = []
forEach(list, item => {
if (!item.meta || (item.meta && !item.meta.hideInMenu)) {
let obj = {
icon: (item.meta && item.meta.icon) || '',
name: item.name,
meta: item.meta
}
if ((hasChild(item) || (item.meta && item.meta.showAlways)) && showThisMenuEle(item, access)) {
obj.children = getMenuByRouter(item.children, access)
}
if (item.meta && item.meta.href) obj.href = item.meta.href
if (showThisMenuEle(item, access)) res.push(obj)
}
})
return res
}
/**
* @param {Array} routeMetched 当前路由metched
* @returns {Array}
*/
export const getBreadCrumbList = (route, homeRoute) => {
let homeItem = { ...homeRoute, icon: homeRoute.meta.icon }
let routeMetched = route.matched
if (routeMetched.some(item => item.name === homeRoute.name)) return [homeItem]
let res = routeMetched
.filter(item => {
return item.meta === undefined || !item.meta.hideInBread
})
.map(item => {
let meta = { ...item.meta }
if (meta.title && typeof meta.title === 'function') {
meta.__titleIsFunction__ = true
meta.title = meta.title(route)
}
let obj = {
icon: (item.meta && item.meta.icon) || '',
name: item.name,
meta: meta
}
return obj
})
res = res.filter(item => {
return !item.meta.hideInMenu
})
return [{ ...homeItem, to: homeRoute.path }, ...res]
}
export const getRouteTitleHandled = route => {
let router = { ...route }
let meta = { ...route.meta }
let title = ''
if (meta.title) {
if (typeof meta.title === 'function') {
meta.__titleIsFunction__ = true
title = meta.title(router)
} else title = meta.title
}
meta.title = title
router.meta = meta
return router
}
export const showTitle = (item, vm) => {
let { title, __titleIsFunction__ } = item.meta
if (!title) return
if (useI18n) {
if (title.includes('{{') && title.includes('}}') && useI18n) title = title.replace(/({{[\s\S]+?}})/, (m, str) => str.replace(/{{([\s\S]*)}}/, (m, _) => vm.$t(_.trim())))
else if (__titleIsFunction__) title = item.meta.title
else title = vm.$t(item.name)
} else title = (item.meta && item.meta.title) || item.name
return title
}
/**
* @description 本地存储和获取标签导航列表
*/
export const setTagNavListInLocalstorage = list => {
localStorage.tagNaveList = JSON.stringify(list)
}
/**
* @returns {Array} 其中的每个元素只包含路由原信息中的name, path, meta三项
*/
export const getTagNavListFromLocalstorage = () => {
const list = localStorage.tagNaveList
return list ? JSON.parse(list) : []
}
/**
* @param {Array} routers 路由列表数组
* @description 用于找到路由列表中name为home的对象
*/
export const getHomeRoute = (routers, homeName = 'home') => {
let i = -1
let len = routers.length
let homeRoute = {}
while (++i < len) {
let item = routers[i]
if (item.children && item.children.length) {
let res = getHomeRoute(item.children, homeName)
if (res.name) return res
} else {
if (item.name === homeName) homeRoute = item
}
}
return homeRoute
}
/**
* @param {*} list 现有标签导航列表
* @param {*} newRoute 新添加的路由原信息对象
* @description 如果该newRoute已经存在则不再添加
*/
export const getNewTagList = (list, newRoute) => {
const { name, path, meta } = newRoute
let newList = [...list]
if (newList.findIndex(item => item.name === name) >= 0) return newList
else newList.push({ name, path, meta })
return newList
}
/**
* @param {*} access 用户权限数组,如 ['super_admin', 'admin']
* @param {*} route 路由列表
*/
const hasAccess = (access, route) => {
if (route.meta && route.meta.access) return hasOneOf(access, route.meta.access)
else return true
}
/**
* 权鉴
* @param {*} name 即将跳转的路由name
* @param {*} access 用户权限数组
* @param {*} routes 路由列表
* @description 用户是否可跳转到该页
*/
export const canTurnTo = (name, access, routes) => {
const routePermissionJudge = list => {
return list.some(item => {
if (item.children && item.children.length) {
return routePermissionJudge(item.children)
} else if (item.name === name) {
return hasAccess(access, item)
}
})
}
return routePermissionJudge(routes)
}
/**
* @param {String} url
* @description 从URL中解析参数
*/
export const getParams = url => {
const keyValueArr = url.split('?')[1].split('&')
let paramObj = {}
keyValueArr.forEach(item => {
const keyValue = item.split('=')
paramObj[keyValue[0]] = keyValue[1]
})
return paramObj
}
/**
* @param {Array} list 标签列表
* @param {String} name 当前关闭的标签的name
*/
export const getNextRoute = (list, route) => {
let res = {}
if (list.length === 2) {
res = getHomeRoute(list)
} else {
const index = list.findIndex(item => routeEqual(item, route))
if (index === list.length - 1) res = list[list.length - 2]
else res = list[index + 1]
}
return res
}
/**
* @param {Number} times 回调函数需要执行的次数
* @param {Function} callback 回调函数
*/
export const doCustomTimes = (times, callback) => {
let i = -1
while (++i < times) {
callback(i)
}
}
/**
* @param {Object} file 从上传组件得到的文件对象
* @returns {Promise} resolve参数是解析后的二维数组
* @description 从Csv文件中解析出表格,解析成二维数组
*/
export const getArrayFromFile = file => {
let nameSplit = file.name.split('.')
let format = nameSplit[nameSplit.length - 1]
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.readAsText(file) // 以文本格式读取
let arr = []
reader.onload = function (evt) {
let data = evt.target.result // 读到的数据
let pasteData = data.trim()
arr = pasteData
.split(/[\n\u0085\u2028\u2029]|\r\n?/g)
.map(row => {
return row.split('\t')
})
.map(item => {
return item[0].split(',')
})
if (format === 'csv') resolve(arr)
else reject(new Error('[Format Error]:你上传的不是Csv文件'))
}
})
}
/**
* @param {Array} array 表格数据二维数组
* @returns {Object} { columns, tableData }
* @description 从二维数组中获取表头和表格数据,将第一行作为表头,用于在iView的表格中展示数据
*/
export const getTableDataFromArray = array => {
let columns = []
let tableData = []
if (array.length > 1) {
let titles = array.shift()
columns = titles.map(item => {
return {
title: item,
key: item
}
})
tableData = array.map(item => {
let res = {}
item.forEach((col, i) => {
res[titles[i]] = col
})
return res
})
}
return {
columns,
tableData
}
}
export const findNodeUpper = (ele, tag) => {
if (ele.parentNode) {
if (ele.parentNode.tagName === tag.toUpperCase()) {
return ele.parentNode
} else {
return findNodeUpper(ele.parentNode, tag)
}
}
}
export const findNodeUpperByClasses = (ele, classes) => {
let parentNode = ele.parentNode
if (parentNode) {
let classList = parentNode.classList
if (classList && classes.every(className => classList.contains(className))) {
return parentNode
} else {
return findNodeUpperByClasses(parentNode, classes)
}
}
}
export const findNodeDownward = (ele, tag) => {
const tagName = tag.toUpperCase()
if (ele.childNodes.length) {
let i = -1
let len = ele.childNodes.length
while (++i < len) {
let child = ele.childNodes[i]
if (child.tagName === tagName) return child
else return findNodeDownward(child, tag)
}
}
}
export const showByAccess = (access, canViewAccess) => {
return hasOneOf(canViewAccess, access)
}
/**
* @description 根据name/params/query判断两个路由对象是否相等
* @param {*} route1 路由对象
* @param {*} route2 路由对象
*/
export const routeEqual = (route1, route2) => {
const params1 = route1.params || {}
const params2 = route2.params || {}
const query1 = route1.query || {}
const query2 = route2.query || {}
return route1.name === route2.name && objEqual(params1, params2) && objEqual(query1, query2)
}
/**
* 判断打开的标签列表里是否已存在这个新添加的路由对象
*/
export const routeHasExist = (tagNavList, routeItem) => {
let len = tagNavList.length
let res = false
doCustomTimes(len, index => {
if (routeEqual(tagNavList[index], routeItem)) res = true
})
return res
}
export const localSave = (key, value) => {
localStorage.setItem(key, value)
}
export const localRead = key => {
return localStorage.getItem(key) || ''
}
// scrollTop animation
export const scrollTop = (el, from = 0, to, duration = 500, endCallback) => {
if (!window.requestAnimationFrame) {
window.requestAnimationFrame =
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
return window.setTimeout(callback, 1000 / 60)
}
}
const difference = Math.abs(from - to)
const step = Math.ceil((difference / duration) * 50)
const scroll = (start, end, step) => {
if (start === end) {
endCallback && endCallback()
return
}
let d = start + step > end ? end : start + step
if (start > end) {
d = start - step < end ? end : start - step
}
if (el === window) {
window.scrollTo(d, d)
} else {
el.scrollTop = d
}
window.requestAnimationFrame(() => scroll(d, end, step))
}
scroll(from, to, step)
}
/**
* @description 根据当前跳转的路由设置显示在浏览器标签的title
* @param {Object} routeItem 路由对象
* @param {Object} vm Vue实例
*/
export const setTitle = (routeItem, vm) => {
const handledRoute = getRouteTitleHandled(routeItem)
const pageTitle = showTitle(handledRoute, vm)
const resTitle = pageTitle ? `${title} - ${pageTitle}` : title
window.document.title = resTitle
}
... ...
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Vant from 'vant'
import { Lazyload } from 'vant'
import commMixin from '@/mixin/comm'
import 'vant/lib/index.css'
import { prettyPrint } from './filters'
import loggerPlugin from './plugin/loggerPlugin'
// import VueDragResize from 'vue-drag-resize'
// Vue.component('vue-drag-resize', VueDragResize)
Vue.use(Vant)
Vue.use(Lazyload)
Vue.use(loggerPlugin, store)
Vue.filter('prettyPrint', prettyPrint)
Vue.config.productionTip = false
Vue.config.errorHandler = function (error, vm, info) {
console.error('Error message:' + error.message, 'Error stack:' + error.stack)
}
// function getObjToArrayBufferDoLength(obj) {
// // 将对象序列化为 JSON 字符串
// const json = JSON.stringify(obj)
// // 创建一个新的 TextEncoder 对象
// const encoder = new TextEncoder()
// // 将 JSON 字符串编码为 Uint8Array
// const data = encoder.encode(json)
// // 创建一个新的 ArrayBuffer,大小为数据长度
// const buffer = new ArrayBuffer(data.length)
// // 获取 ArrayBuffer 的视图(Uint8Array)
// const view = new Uint8Array(buffer)
// // 将数据复制到 ArrayBuffer 中
// view.set(data)
// return view.byteLength
// }
Vue.mixin(commMixin)
const app = new Vue({
router,
store,
created() {
this.setIsApp()
setTimeout(() => {
if (!this.getIsApp) {
rewriteXHR()
}
}, 0)
},
methods: {
setIsApp() {
let isApp = false
try {
isApp = webf ? true : false
} catch (error) {
isApp = false
}
this.$store.commit('setIsApp', isApp)
}
},
render: h => h(App)
}).$mount('#app')
function rewriteXHR() {
// 保存原始的 XMLHttpRequest 构造函数
let OriginalXHR = window.XMLHttpRequest
// 创建新的 XMLHttpRequest 构造函数,扩展了原始的构造函数
function CustomXHR(app) {
let xhr = new OriginalXHR()
let sendDate = 0
xhr.addEventListener('loadstart', function (event) {
sendDate = Date.now()
})
xhr.addEventListener('load', function (event) {
const responseURL = xhr.response.url || xhr.responseURL
const response = xhr.response.body || xhr.response
try {
console.netwokrLog({
name: responseURL.split('/').pop(),
status: xhr.status,
statusText: xhr.statusText,
response: response,
responseURL: responseURL,
size: 0,
time: Date.now() - sendDate
})
} catch (error) {
console.log('error:', error)
}
})
xhr.addEventListener('error', function (event) {
console.log('error:', event)
})
return xhr
}
// 重写全局的 XMLHttpRequest 构造函数
window.XMLHttpRequest = CustomXHR
}
// new Vue({
// router,
// store,
// render: h => h(App)
// }).$mount(document.body)
... ...
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['getIsApp', 'getAppNavBarShow', 'getSystemInfo', 'getTopOffsetHeight'])
}
}
... ...
// loggerPlugin.js
export default {
install(Vue, options) {
// 创建一个 Vue 实例作为事件总线
// const bus = new Vue()
const cLog = console.log
const cError = console.error
options.commit('setStoreLog', cLog)
console.netwokrLog = function (data) {
options.commit('addNetworkLoggerData', {
sendTime: Date.now(),
...data
})
}
console.log = function (data) {
cLog(...arguments)
if (arguments[0]) {
let length = arguments.length
let logTime = Date.now()
for (let index = 0; index < length; index++) {
const item = arguments[index]
options.commit('addLoggerData', {
logType: 'log',
logId: logTime,
type: typeof item,
log: item
})
}
}
}
console.error = function () {
cError(...arguments)
let length = arguments.length
let logTime = Date.now()
for (let index = 0; index < length; index++) {
const item = arguments[index]
options.commit('addLoggerData', {
logType: 'error',
logId: logTime,
type: typeof item,
log: typeof item === 'object' ? JSON.stringify(item) : item
})
}
}
Vue.config.errorHandler = function (error, vm, info) {
console.error('Error message:' + error.message, 'Error stack:' + error.stack)
}
}
}
... ...
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '@/views/HomeView.vue'
Vue.use(VueRouter)
// 获取原型对象push函数
const originalPush = VueRouter.prototype.push
// 获取原型对象replace函数
const originalReplace = VueRouter.prototype.replace
// 修改原型对象中的push函数
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
// 修改原型对象中的replace函数
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch(err => err)
}
const routes = [
{
path: '/',
name: 'HomeView',
meta: {
title: '首页',
icon: 'home-o',
isTabBarMenu: true,
keepAlive: true,
showNavBar: true
},
component: HomeView
},
{
path: '/caricature',
name: 'CaricatureView',
meta: {
title: '漫画',
icon: 'home-o',
isTabBarMenu: true,
keepAlive: true,
showNavBar: true
},
component: () => import('../views/CaricatureView.vue')
},
{
path: '/active',
name: 'ActiveView',
meta: {
title: '活动',
icon: 'home-o',
isTabBarMenu: true,
keepAlive: true,
showNavBar: true
},
component: () => import('../views/ActiveView.vue')
},
{
path: '/live',
name: 'LiveView',
meta: {
title: '直播',
icon: 'home-o',
isTabBarMenu: true,
keepAlive: true,
showNavBar: true
},
component: () => import('../views/LiveView.vue')
},
{
path: '/mine',
name: 'MineView',
meta: {
title: '我的',
icon: 'home-o',
isTabBarMenu: true,
keepAlive: true,
showNavBar: true
},
component: () => import('../views/MineView.vue')
},
{
path: '/detail',
name: 'DetailView',
meta: {
title: '详情',
isTabBarMenu: false,
showNavBar: true
},
component: () => import('../views/DetailView.vue')
}
]
const router = new VueRouter({
// mode: 'history',
// base: process.env.BASE_URL,
routes
})
export default router
... ...
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
isApp: false,
appNavBarShow: false,
systemInfo: {
statusHeight: 0
},
loggerData: [],
networkLoggerData: [],
/**
* @description: 如果需要在addLoggerData/addNetworkLoggerData打印log
* @description: 请使用:storeLog(log)的方式避免死循环
* @return {*}
*/
storeLog: null
},
getters: {
getIsApp: state => state.isApp,
getAppNavBarShow: state => state.appNavBarShow,
getSystemInfo: state => state.systemInfo,
getTopOffsetHeight: state => {
if (state.isApp) {
if (state.appNavBarShow) {
return 46 + state.systemInfo.statusHeight
} else {
return 46
}
} else {
return 0
}
},
getLoggerData: state => state.loggerData,
getNetworkLoggerData: state => state.networkLoggerData
},
mutations: {
setIsApp(state, isApp) {
state.isApp = isApp
},
setAppNavBarShow(state, isShow) {
state.appNavBarShow = isShow
},
setSystemInfo(state, systemInfo) {
state.systemInfo = systemInfo
},
setStoreLog(state, log) {
state.storeLog = log
},
addLoggerData(state, log) {
state.loggerData.push(log)
},
addNetworkLoggerData(state, networkLog) {
state.networkLoggerData.push(networkLog)
}
},
actions: {},
modules: {}
})
... ...
<template>
<div>活动</div>
</template>
<script>
export default {
name: 'ActiveView'
}
</script>
<style lang="scss" scoped></style>
... ...
<template>
<div class="about">
<h1>漫画</h1>
<van-button type="primary" @click="jump">测试跳转</van-button>
</div>
</template>
<script>
export default {
name: 'CaricatureView',
data() {
return {}
},
methods: {
jump() {
this.$router.push({
name: 'DetailView'
})
}
}
}
</script>
... ...
<template>
<div>
<div>详情页</div>
</div>
</template>
<script>
export default {
name: 'DetailView',
data() {
return {}
},
methods: {}
}
</script>
<style lang="scss" scoped></style>
... ...
<template>
<div class="home-page">
<div class="header-nav-bar" :style="{ top: getTopOffsetHeight + 'px' }">
<div class="tabs-view">
<div class="tab-item" :class="{ active: activeIndex === index }" v-for="(item, index) in tabList" :key="item.value" @click="changeTabIndex(index)">
{{ item.label }}
<div class="active-line" :class="{ show: activeIndex === index }" />
</div>
</div>
<div class="icon-list">
<span class="iconfont icon-shezhi"></span>
<span class="iconfont icon-kefu"></span>
</div>
</div>
<div class="search-view" :style="{ top: getTopOffsetHeight + 44 + 'px' }">
<van-search shape="round" v-model="searchKeyWord" placeholder="请输入搜索关键词" />
</div>
<div v-if="activeIndex === 0">
<div class="banner-view">
<van-swipe class="my-swipe" indicator-color="white">
<van-swipe-item v-for="(item, index) in images" :key="index">
<img class="swipe-img" :src="item" />
</van-swipe-item>
</van-swipe>
</div>
<div class="notice-view">
<van-notice-bar left-icon="volume-o" background="#f8f8fb" text="在代码阅读过程中人们说脏话的频率是衡量代码质量的唯一标准。">
<template #right-icon>
<van-button class="hot-live-btn" size="mini" icon="fire-o" type="primary" @click="showGlobalLog">热门直播</van-button>
</template>
</van-notice-bar>
</div>
<div class="list-view">
<collapse v-model="competitionActiveNames">
<collapse-item v-for="(item, index) in competitionList" :key="index" :title="`标题${index + 1}`" :margin-bottom="4">
<template #collapseExtra>
<div class="competition-content">
<div>{{ item.a }}</div>
<div>{{ item.b }}</div>
</div>
</template>
</collapse-item>
</collapse>
</div>
</div>
<div v-if="activeIndex === 1">娱乐123</div>
<div v-if="activeIndex === 2">小说</div>
<div v-if="activeIndex === 3">GPT</div>
<MyPopup v-model="myPopupOpen" position="right" width="80%" :round="false" :show-close-icon="true" popup-title="弹出框标题" :popup-title-text-style="{ textAlign: 'left' }">
内容
</MyPopup>
</div>
</template>
<script>
import Collapse from '@/components/Collapse/Collapse.vue'
import CollapseItem from '@/components/Collapse/CollapseItem.vue'
import MyPopup from '@/components/MyPopup/MyPopup.vue'
export default {
name: 'HomeView',
components: { Collapse, CollapseItem, MyPopup },
data() {
return {
myPopupOpen: false,
searchKeyWord: '',
activeIndex: 0,
isBottom: false,
tabList: [
{
label: '新闻',
value: 'news'
},
{
label: '娱乐',
value: 'recreation'
},
{
label: '小说',
value: 'fiction'
},
{
label: 'GPT',
value: 'gpt'
}
],
images: ['https://img01.yzcdn.cn/vant/apple-1.jpg?time=' + Date.now(), 'https://img01.yzcdn.cn/vant/apple-2.jpg'],
competitionActiveNames: [],
competitionList: [
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' },
{ title: '名称', a: 'A', b: 'B' }
]
}
},
mounted() {
console.log('mounted:HomePage')
console.log('mounted:Object', { a: 123 })
this.$nextTick(() => {
window.addEventListener('scroll', this.pageScroll)
})
},
activated() {
console.log('HomePage page is activated')
// 执行页面被缓存时的逻辑
},
deactivated() {
console.log('HomePage page is deactivated')
// 执行页面离开缓存时的逻辑
},
methods: {
testRequest() {
// 创建一个 XMLHttpRequest 实例
var xhr = new XMLHttpRequest()
// 发送请求
xhr.open('GET', 'https://mock.apifox.cn/m1/2852848-0-default/api/getVersion')
xhr.send()
},
showGlobalLog() {
this.testRequest()
// this.myPopupOpen = !this.myPopupOpen
console.log(asdfasf)
// this.myPopupOpen = true
},
changeTabIndex(index) {
this.activeIndex = index
},
emitWebF() {
console.log('getIsApp:', this.getIsApp)
if (this.getIsApp) {
webf.methodChannel.invokeMethod('somemethod-webf', {
isBottom: this.isBottom,
getIsApp: this.getIsApp
})
}
},
pageScroll(e) {
// console.log('pageScroll:', e)
//scrollTop是滚动条滚动时,距离顶部的距离
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop
//windowHeight是可视区的高度
var windowHeight = document.documentElement.clientHeight || document.body.clientHeight
//scrollHeight是滚动条的总高度
var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
//滚动条到底部的条件
if (scrollTop + windowHeight == scrollHeight) {
//到了这个就可以进行业务逻辑加载后台数据了
// console.log('到了底部')
// console.log('getIsApp:', this.getIsApp)
if (this.getIsApp) {
// 有webf则表示是app
// webf.methodChannel.invokeMethod('somemethod-webf', {
// isBottom: true,
// msg: '到底啦'
// })
}
} else if (scrollTop === 0) {
console.log('到了顶部')
}
},
onRefresh() {
setTimeout(() => {
this.isLoading = false
}, 1000)
}
}
}
</script>
<style lang="scss" scoped>
.home-page {
width: 100%;
overflow-x: hidden;
padding-top: 98px;
.header-nav-bar {
position: fixed;
width: 100%;
height: 44px;
z-index: 12;
display: flex;
align-items: center;
background-color: #fff;
box-shadow: 2px 2px 20px #ededed;
.tabs-view {
height: 44px;
flex: 4;
margin-right: 10px;
display: flex;
align-items: center;
padding: 0 10px;
.tab-item {
flex: 1;
text-align: center;
font-size: 14px;
color: #666;
position: relative;
transition: all 0.1s ease-in 0s;
height: 44px;
line-height: 44px;
&.active {
color: #f00;
}
.active-line {
position: absolute;
left: 50%;
bottom: 2px;
width: 0;
height: 2px;
background-color: #f00;
transition: all 0.2s ease-in 0s;
&.show {
width: 70%;
left: 15%;
}
}
}
}
.icon-list {
flex: 1;
height: 44px;
display: flex;
align-items: center;
justify-content: space-around;
}
}
.icon-img {
width: 50px;
height: 50px;
margin-right: 10px;
}
.search-view {
position: fixed;
width: 100%;
height: 54px;
z-index: 11;
background-color: #fff;
}
.notice-view {
position: sticky;
top: 98px;
z-index: 11;
background-color: #fff;
.hot-live-btn {
margin-left: 5px;
}
}
.list-view {
margin-top: 5px;
.competition-content {
padding: 15px;
}
}
}
::v-deep .my-swipe {
.van-swipe-item {
color: #fff;
height: 150px;
text-align: center;
padding: 10px;
box-sizing: border-box;
.swipe-img {
width: 100%;
height: 100%;
border-radius: 10px;
}
}
}
</style>
... ...
<template>
<div>直播</div>
</template>
<script>
export default {
name: 'LivewView'
}
</script>
<style lang="scss" scoped></style>
... ...
<template><div>我的</div></template>
<script>
export default {
name: 'MineView',
data() {
return {}
}
}
</script>
<style lang="scss" scoped></style>
... ...
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack: config => {
config.mode('development')
config.optimization.delete('splitChunks')
},
filenameHashing: false,
productionSourceMap: false,
configureWebpack(config) {
config.devtool = config.mode === 'production' ? false : 'source-map'
},
css: {
extract: false
},
publicPath: './'
})
... ...