Commit 5dcc3913 authored by shiyunjie's avatar shiyunjie

init

parent ac209d47
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
autoprefixer: {},
"postcss-aspect-ratio-mini": {},
"postcss-write-svg": {
utf8: false
},
"postcss-px-to-viewport": {
viewportWidth: 750,
viewportHeight: 1334,
unitPrecision: 1,
viewportUnit: 'vw',
selectorBlackList: [".ignore", ".hairlines"],
minPixelValue: 1,
mediaQuery: false,
exclude: [/node_modules/]
},
"postcss-viewport-units":{
silence: true
}
}
}
{
"singleQuote": true
}
This project was bootstrapped with [vue-cli](https://github.com/vuejs/vue-cli)
## 介绍
适合想用`vue,ts以及class-component`写法开发手机端h5的项目基于`vue2.6.6`。状态管理`vuex`。路由是`vue-router`加入了`keep-alive`来做缓存。
### class-component写法
这里用的是[vue-property-decorato](https://github.com/kaorun343/vue-property-decorator)。可参考[vue-property-decorato用法](https://www.jianshu.com/p/d8ed3aa76e9b)
### 解决前进刷新后退不刷新问题
利用`keep-alive`解决[多路由,前进刷新,后退不刷新](https://segmentfault.com/a/1190000012083511)
### 手机适配方案
适配上我选择了`viewport`的方案可参考
[如何在Vue项目中使用vw实现移动端适配](https://www.w3cplus.com/mobile/vw-layout-in-vue.html)
[再聊移动端页面的适配](https://www.w3cplus.com/css/vw-for-layout.html)
### ui组建
项目引入的组件库是
[vant](https://youzan.github.io/vant/#/zh-CN/quickstart)已经用 [ts-import-plugin](https://github.com/Brooooooklyn/ts-import-plugin)做了按需加载
### 数据请求
这边对[axios](https://github.com/axios/axios)中的post以及get等方法进行了一些封装。以及拦截器做好了配置,只需加入一些特定的处理。常用的`loading`已经加入。我们项目中还会选择在拦截器中做统一报错处理。
### 目录
```
├── App.vue
├── api // 接口请求
│ └── home.ts
├── assets
│ ├── config // 配置文件(基础和业务)
│ │ ├── commonConfig.ts
│ │ ├── index.ts
│ │ └── persionalInfo.ts
│ ├── fetch.ts
│ ├── iconfont
│ │ ├── iconfont.css
│ │ └── iconfont.ttf
│ └── images
│ └── logo.png
├── components // 基础组件
│ └── HelloWorld.vue
├── main.ts
├── routes // 路由
│ ├── demo.ts
│ └── index.ts
├── shims-tsx.d.ts
├── shims-vue.d.ts
├── store // 状态管理
│ ├── index.ts
│ └── modules
│ ├── num.ts
│ └── user.ts
└── views // 页面
├── Home.vue
└── demo
├── About.vue
└── Home.vue
```
### 前端和原生交互
[tpt-js-sdk](https://johnbian.github.io/TPTJS-SDK/#/)
### 已经封装组件 (demo看view/home)
#### Empty 空列表
#### pickerView
#### UploaderImg 照片选择
# 记录几个问题
## 一、safair浏览器兼容问题
### 1.input 去掉默认样式。
```css
input {
border: 0px;
outline: none;
}
```
### 2.image 显示不出 因为Viewport Units Buggyfill的原因。
```css
img {
content: normal !important;
}
```
## 二、build问题
### 1.ts-loader问题。 解决:看vue-config。
### 2.按需加载 样式打不进去。 解决: parallel: false
### 3.publicPath 生产环境下记得改
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn run serve
```
### Compiles and minifies for production
```
yarn run build
```
### Run your tests
```
yarn run test
```
### Lints and fixes files
```
yarn run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
module.exports = {
presets: [
'@vue/app'
]
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "<%= projectName %>",
"version": "0.1.0",
"private": true,
"description": "<%= description %>",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.18.0",
"postcss-aspect-ratio-mini": "^1.0.1",
"postcss-loader": "^3.0.0",
"postcss-px-to-viewport": "^1.1.0",
"postcss-viewport-units": "^0.1.6",
"postcss-write-svg": "^3.0.1",
"pre-commit": "^1.2.2",
"tpt-js-sdk": "^1.1.4",
"vant": "^2.5.4",
"vue": "^2.6.6",
"vue-class-component": "^6.0.0",
"vue-property-decorator": "^7.0.0",
"vue-router": "^3.0.1",
"vuex": "^3.0.1",
"vuex-persistedstate": "^2.5.4"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.4.0",
"@vue/cli-plugin-typescript": "^3.4.0",
"@vue/cli-service": "^3.4.0",
"node-sass": "^4.9.0",
"sass-loader": "^7.1.0",
"ts-import-plugin": "^1.5.5",
"typescript": "^3.0.0",
"vue-template-compiler": "^2.5.21"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8",
"not ie <= 11",
"Android >= 4.0",
"iOS >= 8"
]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="yes" name="apple-mobile-web-app-capable">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title></title>
</head>
<body>
<noscript>
<strong>We're sorry but insure-shop 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>
<script>
(function(para) {
var p = para.sdk_url, n = para.name, w = window, d = document, s = 'script',x = null,y = null;
if(typeof(w['sensorsDataAnalytic201505']) !== 'undefined') {
return false;
}
w['sensorsDataAnalytic201505'] = n;
w[n] = w[n] || function(a) {return function() {(w[n]._q = w[n]._q || []).push([a, arguments]);}};
var ifs = ['track','quick','register','registerPage','registerOnce','trackSignup', 'trackAbtest', 'setProfile','setOnceProfile','appendProfile', 'incrementProfile', 'deleteProfile', 'unsetProfile', 'identify','login','logout','trackLink','clearAllRegister','getAppStatus'];
for (var i = 0; i < ifs.length; i++) {
w[n][ifs[i]] = w[n].call(null, ifs[i]);
}
if (!w[n]._t) {
x = d.createElement(s), y = d.getElementsByTagName(s)[0];
x.async = 1;
x.src = p;
x.setAttribute('charset','UTF-8');
w[n].para = para;
y.parentNode.insertBefore(x, y);
}
})({
sdk_url: 'https://ecustomer.cntaiping.com/static/npm/sa-sdk-javascript/sensorsdata.min.js',
heatmap_url: 'https://ecustomer.cntaiping.com/static/npm/sa-sdk-javascript/heatmap.min.js',
name: 'sensors',
use_app_track: true,
server_url: 'https://yhxwtz_test.ft.cntaiping.com:8111/sa?project=tpt_test',
// server_url: 'https://cd.life.cntaiping.com:8106/sa?project=chinataiping',
heatmap:{},
is_track_single_page : function (){
return true
}
});
sensors.quick('autoTrack');
sensors.quick('isReady',function(){
sessionStorage.setItem('anonymousId', sensors.quick('getAnonymousID'));
});
</script>
</html>
<template>
<div id="app">
<transition :name="transitionName">
<keep-alive :exclude="excludePage">
<router-view />
</keep-alive>
</transition>
</div>
</template>
<script lang="ts">
import { Component, Vue , Watch} from 'vue-property-decorator';
import { Toast } from 'vant';
import config from '@/assets/config';
Toast.setDefaultOptions({forbidClick: true});
@Component
export default class App extends Vue {
private transitionName: string = 'slide-left';
private docmHeight: number = document.documentElement.clientHeight;
private showHeight: number = document.documentElement.clientHeight;
private loading: any = null;
get loadingNum() {
return this.$store.state.num.num;
}
get excludePage() {
return this.$store.state.num.excludePage;
}
@Watch('loadingNum')
public loadingNumChange(newVal: any, oldVal: any) {
if (newVal > 0 && oldVal === 0) {
this.loading = Toast.loading({
duration: 0,
message: '拼命加载中',
loadingType: 'spinner',
forbidClick: true,
});
} else if (newVal <= 0 && this.loading) {
this.loading.clear();
}
}
@Watch('$route')
public changeRoute(to: any, from: any) {
if (!to.meta.isBack) {
this.transitionName = 'slide-left';
} else {
this.transitionName = 'slide-right';
}
}
@Watch('showHeight')
public changeShowHeight() {
const btn: any = document.getElementsByClassName('van-button--warning')[0];
if (this.docmHeight > this.showHeight && config.isAndroidXZ && btn) {
btn.style.position = 'static';
} else if (this.docmHeight <= this.showHeight && config.isAndroidXZ && btn) {
btn.style.position = 'fixed';
}
const btns: any = document.getElementsByClassName('bottom-btns-hg')[0];
if (this.docmHeight > this.showHeight && config.isAndroidXZ && btns) {
btns.style.position = 'static';
} else if (this.docmHeight <= this.showHeight && config.isAndroidXZ && btns) {
btns.style.position = 'fixed';
}
}
public mounted() {
window.onresize = () => {
this.showHeight = document.documentElement.clientHeight;
};
}
}
</script>
<style>
@import url('../src/assets/iconfont/iconfont.css');
* {
box-sizing: inherit;
}
*::before {
box-sizing: inherit;
}
*::after {
box-sizing: inherit;
}
html {
margin: 0;
padding: 0;
height: 100vh;
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
min-height: 100vh;
background-color: #F4F5F5;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333333;
font-size: 14px;
box-sizing: border-box;
height: 100%;
}
p {
margin: 0;
}
.slide-right-enter-active,
.slide-right-leave-active,
.slide-left-enter-active,
.slide-left-leave-active {
will-change: transform;
transition: all 500ms;
position: absolute;
}
.slide-right-enter {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
.slide-right-leave-active {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
.slide-left-enter {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
.slide-left-leave-active {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
[aspectratio] {
position: relative;
}
[aspectratio]::before {
content: '';
display: block;
width: 1px;
margin-left: -1px;
height: 0;
}
[aspectratio-content] {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}
[flexContainer] {
display: flex;
width: 750px;
}
input {
border: 0px;
outline: none;
}
textarea {
border: 0px;
resize: none;
outline: none;
}
img {
content: normal !important;
}
button.van-button--warning {
position: fixed;
width: 690px;
bottom: 24px;
margin-left: 30px;
margin-top: 20px;
margin-bottom: 20px;
border-radius: 8px;
background-color: #0E6DCF;
border: none;
}
</style>
<style lang="scss">
.need-image-rewrite {
.van-image {
height: 90px;
width: 90px;
}
}
</style>
\ No newline at end of file
import fetch from '@/assets/fetch';
/**
* @author johnbian
* @description 接口处理
*/
class HomeAPI {
/**
* @author johnbian
* @description demo 测试post请求
*/
public async nameSet(): Promise<any> {
const params = {};
const res = await fetch.post('/nameSet', params);
return res;
}
}
const homeAPI = new HomeAPI();
export default homeAPI;
const ENV = 'SIT';
// const ENV = 'UAT'
// const ENV = 'PRO';
const urlList = {
SIT: 'https://ecustomer.tp95589.com/sit',
UAT: 'https://ecustomer.tp95589.com',
// SIT: 'http://localhost:8080/sit',
// UAT: 'http://localhost:8080',
PRO: 'https://ecustomer.cntaiping.com'
};
const fileUrl: any = {
SIT: 'https://ecustomer.tp95589.com',
UAT: 'https://ecustomer.tp95589.com',
PRO: 'https://ecustomer.cntaiping.com'
};
const baseUrl = urlList[ENV];
const responseCode = {
businessError: ['9794', '9894', '9896', '9797', '9796', '9795', '9793'],
networkError: '网络不稳定,请稍后再试',
systemError: '系统繁忙,请稍后再试'
};
const uXZ = navigator.userAgent;
const isAndroidXZ = uXZ.indexOf('Android') > -1 || uXZ.indexOf('Linux') > -1; // android终端或者uc浏览器
const isiOSXZ = !!uXZ.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); // ios终端
const uaXZ = window.navigator.userAgent.toLowerCase();
const isWeixin = uaXZ.indexOf('micromessenger') !== -1;
const isKhtAPP = uXZ.indexOf('kehutong') > -1;
export default {
responseCode,
baseUrl,
ENV,
isAndroidXZ,
isiOSXZ,
isWeixin,
isKhtAPP,
fileUrl
};
import config from './commonConfig';
import persionalInfo from './persionalInfo';
const configMerge = Object.assign(config, persionalInfo, {});
export default configMerge;
/**
* 性别枚举
*/
const genderColumns: any = [
{value: '0', name: '男'}, {value: '1', name: '女'},
];
export default {
genderColumns,
};
import Vue from 'vue';
import * as axios from 'axios';
import commonConfig from '@/assets/config';
import store from '@/store';
import { Toast } from 'vant';
import * as TPTJS from 'tpt-js-sdk';
Vue.use(Toast);
Toast.allowMultiple(true);
/**
* @author john.bian
* @description axios封装
* @
*/
class Fetch {
public cancelToken = axios.default.CancelToken;
public source = this.cancelToken.source();
constructor(public ax: axios.AxiosInstance) { }
/**
* get 请求
* @param url 请求地址
*/
public async get(url: string, baseConfig?: any): Promise<any> {
const config = {
cancelToken: this.source.token,
shouldHandleError: baseConfig ? baseConfig.shouldHandleError : true
};
try {
const ret = await this.ax.get(encodeURI(url), config);
return this.handleResponse(url, ret);
} catch (err) {
return this.commonErrorHandle(err);
}
}
/**
* post 请求
* @param url 请求地址
* @param params 请求参数
*/
public async post(url: any, params: any, baseConfig?: any): Promise<any> {
const config = {
cancelToken: this.source.token,
shouldHandleError: baseConfig ? baseConfig.shouldHandleError : true
};
try {
const ret = await this.ax.post(encodeURI(url), params, config);
return this.handleResponse(url, ret);
} catch (err) {
return this.commonErrorHandle(err);
}
}
/**
* 用户取消请求 比如返回
*/
public cancel(): void {
this.source.cancel('bxsccancel');
}
private handleResponse(url: string, ret: axios.AxiosResponse): any {
// message.destroy();
// if (ret.data.code !== '0000') {
// // message.error(ret.data.message);
// return ret.data;
// }
return ret.data;
}
private commonErrorHandle(err: any): any {
if (err.response && err.response.data) {
return err.response.data;
}
return err;
}
}
const instance = axios.default.create({
baseURL: commonConfig.baseUrl,
timeout: 60 * 1000
});
instance.interceptors.request.use((config): any => {
config.headers = {
'x-ac-mc-type': 'gateway.user',
'x-ac-channel-id': 'KHT',
'x-ac-token-ticket': (store.state as any).user.userInfo.token
};
config.headers['Content-Type'] = 'application/json';
store.dispatch('num/add');
return config;
});
instance.interceptors.response.use(
(response): any => {
const { shouldHandleError }: any = response.config;
store.dispatch('num/del');
if (response.data.code === '0000') {
return response;
} else if (
commonConfig.responseCode.businessError.indexOf(response.data.code) !== -1
) {
TPTJS.khtAppRouteRequest(0, '', 1, 'native', 'LogIn');
return response;
} else if (response.data.code[0] === '9' && shouldHandleError) {
Toast(commonConfig.responseCode.systemError);
return response;
} else if (response.data.code === '8999') {
TPTJS.khtAppRouteRequest(0, '', 1, 'native', 'RealNameAuthentication');
} else if (
Number(response.data.code[0]) >= 0 &&
Number(response.data.code[0]) <= 8 && shouldHandleError
) {
Toast(response.data.desc);
return response;
} else {
return response;
}
},
(err): any => {
store.dispatch('num/del');
if (err.message === 'bxsccancel') {
return Promise.resolve(err.response);
} else if (err.toString().indexOf('Network') !== -1) {
Toast(commonConfig.responseCode.networkError);
return err;
} else {
Toast(commonConfig.responseCode.systemError);
return err.response;
}
}
);
const fetch = new Fetch(instance);
export default fetch;
@font-face {
font-family: 'kht';
src: url('./iconfont.ttf') format('truetype');
}
.kht {
font-family: "kht" !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.kht-weixin:before {
content: "\e63d";
}
.kht-Alipay:before {
content: "\e6ec";
}
.kht-arrow:before {
content: "\e632";
}
.kht-calendar-o:before {
content: "\e633";
}
.kht-WeChat:before {
content: "\e635";
}
.kht-no-checked:before {
content: "\e634";
}
.kht-checked:before {
content: "\e636";
}
.kht-add-o:before {
content: "\e637";
}
.kht-delete-o:before {
content: "\e638";
}
.kht-top-o:before {
content: "\e604";
}
.kht-down-o:before {
content: "\e646";
}
.kht-close-o:before {
content: "\e647";
}
.kht-counter-o:before {
content: "\e648";
}
.kht-question:before {
content: "\e649";
}
.kht-answer:before {
content: "\e64a";
}
.kht-down-origin:before {
content: "\e64b";
}
.kht-camera:before {
content: "\e614";
}
\ No newline at end of file
declare let kht: any;
import config from '@/assets/config';
/**
* @description 时间格式化
* @param date
*/
export function formatter(date: Date | string, format?: string): string {
date = new Date(date as Date);
const year = date.getFullYear();
const month = date.getMonth();
const days = date.getDate();
const hours = date.getHours();
const mins = date.getMinutes();
const seconds = date.getSeconds();
const MM = month > 8 ? month + 1 : `0${month + 1}`;
const dd = days > 9 ? days : `0${days}`;
const HH = hours > 9 ? hours : `0${hours}`;
const mm = mins > 9 ? mins : `0${mins}`;
const ss = seconds > 9 ? seconds : `0${seconds}`;
if (format === 'yyMMddHHmmss') {
return `${year}${MM}${dd}${HH}${mm}${ss}`;
} else {
// yyyy-MM-dd
return `${year}-${MM}-${dd}`;
}
}
/**
* @description 脱敏通用方法
* @param str 需要脱敏的字符串
* @param start 开始位置
* @param end 结束位置 比如 倒数第二位 -2
*/
export function desensitization(str: string, start: number, end: number ): string {
const len = str.length;
const firstStr = str.substr(0, start);
const lastStr = str.substr(end);
const middleStr = str.substring(start, len - Math.abs(end)).replace(/[\s\S]/ig, '*');
const tempStr = firstStr + middleStr + lastStr;
return tempStr;
}
/**
* 身份证验证
* @param value
*/
export function modIDNumberConfirm(value: any): any {
const city: any = {
11: '北京',
12: '天津',
13: '河北',
14: '山西',
15: '内蒙古',
21: '辽宁',
22: '吉林',
23: '黑龙江 ',
31: '上海',
32: '江苏',
33: '浙江',
34: '安徽',
35: '福建',
36: '江西',
37: '山东',
41: '河南',
42: '湖北 ',
43: '湖南',
44: '广东',
45: '广西',
46: '海南',
50: '重庆',
51: '四川',
52: '贵州',
53: '云南',
54: '西藏 ',
61: '陕西',
62: '甘肃',
63: '青海',
64: '宁夏',
65: '新疆',
71: '台湾',
81: '香港',
82: '澳门',
91: '国外 ',
};
const reg = /^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i;
if (!value || !reg.test(value)) {
return '身份证号错误';
} else if (!city[value.substr(0, 2)]) {
return '身份证号错误';
} else {
// 18位身份证需要验证最后一位校验位
if (value.length === 18) {
value = value.split('');
// ∑(ai×Wi)(mod 11)
// 加权因子
const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
// 校验位
const parity = [1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2];
let sum = 0;
let ai = 0;
let wi = 0;
for (let i = 0; i < 17; i++) {
ai = value[i];
wi = factor[i];
sum += ai * wi;
}
const last = parity[sum % 11].toString();
if (last === value[17]) {
return '';
} else {
return '身份证号错误';
}
} else {
return '';
}
}
}
/**
* 手机号验证
* @param value
*/
export function modPhoneConfirm(value: any): any {
// /^(?=\d{11}$)^1(?:3\d|4[57]|5[^4\D]|7[^249\D]|8\d)\d{8}$/
const reg = /^(?=\d{11}$)^1\d{10}$/;
if (!value || !reg.test(value)) {
return '请输入正确的手机号码';
} else {
return '';
}
}
/**
* 邮箱验资
* @param value
*/
export function modEmailConfirm(value: any): any {
const exg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
if (!value || exg.test(value)) {
return '';
}
return '请填写正确的邮箱';
}
export function modCarNoConfirm(value: string): any {
const exg = /^[\u4e00-\u9fa5a-zA-Z0-9_]{7,8}/;
if (!value || exg.test(value)) {
return '';
}
return '请输入正确的车牌号';
}
/**
* 身份证格式化年龄
* @param value
*/
export function IdToBeath(value: string): string {
if (value.length === 18) {
return `${value.substring(6, 10)}-${value.substring(10, 12)}-${value.substring(12, 14)}`;
} else {
return `19${value.substring(6, 8)}-${value.substring(8, 10)}-${value.substring(10, 12)}`;
}
}
/**
* 身份证格式化性别
* @description 1: 男, 2: 女
* @param value
*/
export function IdToGender(value: string): string {
if (value.length === 18) {
if (Number(value.substr(16, 1)) % 2 === 1) {
return '1';
} else {
return '2';
}
} else {
if (Number(value.substr(14, 1)) % 2 === 1) {
return '1';
} else {
return '2';
}
}
}
/**
* 获取数组对应值
* @param arr
* @param value
*/
export function choiceArr(arr: any, value: any, valueName = 'value'): number {
return arr.findIndex((element: any) => (element[valueName] === value));
}
/**
* 分秒倒计时
* @param endString 结束时间
*/
export function countTimeDate(endString: string): string {
const end: Date = new Date(endString);
const now: Date = new Date();
const nowTime = now.getTime();
const endTime = end.getTime();
const leftTime: number = endTime - nowTime;
if (leftTime > 0) {
const mm = Math.floor(leftTime / 1000 / 60 % 60);
const dd = Math.floor(leftTime / 1000 % 60);
return `${mm > 9 ? mm : `0${mm}`}:${dd > 9 ? dd : `0${dd}`}`;
} else {
return '00:00';
}
}
/**
* 分秒倒计时
* @param totalSecond 结束时间
*/
export function countTime(totalSecond: number): string {
if (totalSecond > 0) {
const mm = Math.floor(totalSecond / 1000 / 60 );
const dd = Math.floor((totalSecond / 1000) % 60);
return `${mm > 9 ? mm : `0${mm}`}:${dd > 9 ? dd : `0${dd}`}`;
} else {
return '00:00';
}
}
/**
* 支付方式筛选
* @param payMethod 过滤条件
* @param pays 前端设置所有情况
*/
export function screenPayWay(payMethod: string, pays: any[]): any[] {
const payMethodArr = payMethod.split(',');
const arr: any[] = [];
pays.forEach((element) => {
const index = payMethodArr.findIndex((payWay: string) => payWay === element.value);
if (index >= 0) {
arr.push(element);
}
});
return arr;
}
/**
* 计算年龄是否符合
* @param birthday 生日
*/
export function computeAge(birthday: string): number {
const birth = new Date(birthday);
const birthYear = birth.getFullYear();
const birthMonth = birth.getMonth();
const birthDay = birth.getDate();
const now = new Date();
const nowYear = now.getFullYear();
const nowMonth = now.getMonth();
const nowDay = now.getDate();
const ageDiff: number = nowYear - birthYear;
const monthDiff = nowMonth - birthMonth; // 月之差
const dayDiff = nowDay - birthDay; // 日之差
if (ageDiff === 0) {
return ageDiff;
} else if (ageDiff < 0) {
return -1;
} else if (monthDiff < 0) {
return ageDiff - 1;
} else if (monthDiff > 0) {
return ageDiff;
} else if (dayDiff < 0) {
return ageDiff - 1;
} else {
return ageDiff;
}
}
/**
* 第三方查询格式话
* @description 1:男,2:女
* @param gender 性别 0男1女
*/
export function userInfoToGender(gender: string): string {
return gender === '0' ? '1' : '2';
}
/**
* 获取现在开始前后多少年的时间
* @description -10表示之前10年 10 表示之后10年
* @param year 现在开始后多少年
*/
export function getDateAfterYear(year: number): Date {
const now = new Date();
const nowYear = now.getFullYear();
const nowMonth = now.getMonth();
const nowDate = now.getDate();
return new Date(nowYear + year , nowMonth, nowDate);
}
/**
* 去除首尾空格
* @author johnbian
*/
export function trimStr(str: string): string {
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
/**
* 用于服务申请时 必填项检验
* @param requireList 必填列表
* @param needSaveDate 已填写信息
* @param layout 页面布局字段
* @author johnbian
*/
export function modRequireInfo(requireList: any, needSaveDate: any, layout: any): boolean {
let allPass: boolean = false;
if (requireList.length === 0) {
allPass = true;
} else {
for (const el of requireList) {
if (!needSaveDate[el] && layout.indexOf(el) > -1) {
allPass = false;
break;
} else {
allPass = true;
}
}
}
return allPass;
}
/**
* 用于服务申请时 获取必填项
* @param list 列表
*/
export function getReuireList(list: any): string[] {
const requireList: string[] = [];
Object.keys(list).forEach((item) => {
if (list[item] === '1') {
requireList.push(item);
}
});
return requireList;
}
/**
* 金额输入框进行限定
* 只能输入数字和小数点;小数点只能有1个;第一位不能是小数点;第一位如果输入0,且第二位不是小数点,则去掉第一位的0;小数点后保留2位
* @param num
*/
export function numberCheck(num: string) {
let num1 = num;
num1 = num1.replace(/[^\d.]/g, ''); // 清除"数字"和"."以外的字符
num1 = num1.replace(/^\./g, ''); // 验证第一个字符是数字而不是
const index = num1.indexOf('.');
if (index > -1 && num1.slice(0, index).length > 8) {
num1 = num1.slice(0, index);
}
num1 = num1.replace(/\.{2,}/g, '.'); // 只保留第一个. 清除多余的
num1 = num1.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.');
num1 = num1.replace(/^(\-)*(\d+)\.(\d{2}).*$/, '$1$2.$3'); // 只能输入2个小数
return num1;
}
/**
* Android封装唤起照片选择
* @param capture CAMERA 相机;ALBUM 相册
*/
export function imgUpload(capture?: string) {
if (config.isKhtAPP && config.isAndroidXZ) {
const paiZhaoXC = JSON.stringify({
isShowCamera: true,
isShowAlbum: true,
cameraText: '手机拍照',
albumText: '从手机相册选择',
location: 'BOTTOM',
directOpen: capture,
});
kht.getImgString(paiZhaoXC);
}
}
<style lang="scss" scoped>
.empty {
padding: 320px 104px 0;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
img {
width: 168px;
}
span {
padding-top: 50px;
font-size: 32px;
color: #333;
font-weight: 400;
}
}
</style>
<template>
<div class="empty">
<img src="@/assets/images/icon_feedback_empty.png" alt />
<span>{{ tip }}</span>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class Empty extends Vue {
@Prop({ type: String, default: '产品即将上线,敬请期待' })
private tip?: string;
}
</script>
<style lang="scss" scoped>
.lazy-image {
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}
</style>
<template>
<div class="lazy-image">
<VantImage
lazy-load
:src="src"
>
<template v-slot:loading>
<Loading type="spinner" size="20" />
</template>
</VantImage>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class LazyImage extends Vue {
@Prop() private src?: string;
}
</script>
<template>
<div class="picker-view">
<Field
readonly
:left-icon="leftIcon"
:clickable="clickable"
:required="required"
:label="label"
v-model="value"
:name="name"
:formatter="formatter"
:input-align="align"
:is-link="link"
:arrow-direction="direction"
:placeholder="placeholder"
@click="click"
/>
<Popup v-model="pickerShow" position="bottom">
<DatetimePicker
v-if="type === 'date'"
type="date"
:min-date="minDate"
:max-date="maxDate"
v-model="currentDate"
@confirm="onConfirm"
@cancel="onCancel"
/>
<Picker
v-else
show-toolbar
:title="title"
:confirm-button-text="confirmText"
:cancel-button-text="cancelText"
:columns="columns"
@confirm="onConfirm"
@cancel="onCancel"
:value-key="showName"
/>
</Popup>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { formatter } from '@/assets/utils.ts';
import { DatetimePicker, Picker } from 'vant';
@Component({
components: {
DatetimePicker,
Picker
}
})
export default class PickerView extends Vue {
@Prop(String) public label!: string;
@Prop({ type: Boolean, default: false }) public required?: boolean;
@Prop({ type: Boolean, default: false }) public disabled?: boolean;
@Prop({ type: String, default: '' }) public type?: string;
@Prop(String) public placeholder?: string;
@Prop(String) public name!: string;
@Prop() public defaultValue?: string;
@Prop() public maxDate?: Date;
@Prop() public minDate?: Date;
@Prop({ type: Boolean, default: true }) public clickable?: boolean;
@Prop() public columns?: any[];
@Prop() public showName?: string;
@Prop() public valueName?: string;
@Prop() public align?: string;
@Prop() public link?: boolean;
@Prop() public direction?: string;
@Prop() public title?: string;
@Prop() public confirmText?: string;
@Prop() public cancelText?: string;
@Prop() public leftIcon?: string;
@Prop() public formatter?: any;
public value: string = '';
private currentDate: any = new Date();
private pickerShow: boolean = false;
@Watch('defaultValue', {immediate: true})
private changeDefaultValue(newValue: string, oldValue: string) {
if (newValue !== oldValue) {
this.value = this.formatter ? this.formatter(newValue) : newValue;
}
}
private click() {
if (!this.disabled) {
this.pickerShow = true;
}
}
private onConfirm(value: any, index: any): void {
this.pickerShow = false;
if (this.type === 'date') {
const dateValue = formatter(value);
this.value = dateValue;
} else if (this.valueName) {
this.value = this.formatter ? this.formatter(value[this.valueName]) : value[this.valueName];
this.$emit('onConfirm', value, index);
} else if (this.showName) {
this.value = value[this.showName];
this.$emit('onConfirm', value, index);
} else {
this.value = value;
this.$emit('onConfirm', value, index);
}
}
private onCancel(): void {
this.pickerShow = false;
}
}
</script>
<style lang="scss" scoped>
.picker-view {
}
</style>
<template>
<div class="hello" @click="upLoaderImage">
<Uploader class="ops" :disabled="disabled" v-if="isIOS" :after-read="clickOcr" :capture="capture">
<slot></slot>
</Uploader>
<div v-else>
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Uploader } from 'vant';
import { imgUpload } from '@/assets/utils';
import config from '@/assets/config';
@Component({
components: {
Uploader
},
})
export default class UploaderImg extends Vue {
@Prop({ default: '', type: String }) private capture!: string;
@Prop({ default: false, type: Boolean }) private disabled!: boolean;
private isIOS: boolean = !(config.isKhtAPP && config.isAndroidXZ);
public created() {
// android选择照片回调函数
(window as any).uploadImgkht = (txt: any) => {
const photos: any = {
content: txt,
file: {
lastModified: new Date().getTime(),
name: `${new Date().getTime()}.jpeg`,
size: '',
type: 'image/jpeg',
},
};
this.$emit('choiceImage', photos);
};
}
// android 唤起拍照
private upLoaderImage() {
if (!this.disabled) {
imgUpload(this.capture);
}
}
// ios
private clickOcr(file: any) {
if (file.file.size > 1000000) {
const img = new Image();
const width = 1080; // 图像大小
const quality = 0.8; // 图像质量
const canvas = document.createElement('canvas');
const drawer = canvas.getContext('2d');
img.src = file.content;
this.$store.dispatch('num/add');
img.onload = () => {
canvas.width = width;
canvas.height = width * (img.height / img.width);
(drawer as any).drawImage(img, 0, 0, canvas.width, canvas.height);
file.content = canvas.toDataURL(file.file.type, quality);
this.$store.dispatch('num/del');
this.$emit('choiceImage', file);
};
} else {
this.$emit('choiceImage', file);
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss">
.ops {
.van-uploader__wrapper--disabled {
opacity: 1;
}
}
</style>
/**
* banner图
* @description banner图
* @author johnbian
*/
export interface IBanner {
authorization: number; // 0: 不需要 1: 需要
detail: string;
img: string;
linkInfo: any;
login: number;
name: string;
pageType: string;
tagUrl: string;
type: string;
}
import Vue from 'vue';
import App from './App.vue';
import router from './routes';
import store from './store';
import { Button, Icon, Form, Field, Dialog, Popup, Picker,
Uploader, Lazyload, Image as VantImage, Loading} from 'vant';
Vue.component('Button', Button);
Vue.component('Icon', Icon);
Vue.component('Form', Form);
Vue.component('Field', Field);
Vue.component('Popup', Popup);
Vue.component('Picker', Picker);
Vue.component('Uploader', Uploader);
Vue.component('VantImage', VantImage);
Vue.component('Loading', Loading);
Vue.use(Dialog);
Vue.use(Lazyload);
Vue.config.productionTip = false;
(window as any).khtGetAppCurrentUserInfo = (param: any) => {
const { authToken, userId } = param;
if (authToken) {
store.dispatch('user/setUserInfo', {token: authToken, userId});
} else {
store.dispatch('user/setUserInfo', {token: '', userId: ''});
}
};
const app = new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
export default app;
const demoRoutes: any = [
{
path: '/demo/home',
name: 'demoHome',
meta: {
keepAlive: true,
isBack: false,
index: 1,
},
component: () => import(/* webpackChunkName: "home" */ '../views/demo/Home.vue'),
},
{
path: '/demo/about',
name: 'demoAbout',
meta: {
keepAlive: true,
isBack: false,
index: 2,
},
component: () => import(/* webpackChunkName: "about" */ '../views/demo/About.vue'),
},
];
export default demoRoutes;
import Vue from 'vue';
import Router from 'vue-router';
import demoRoutes from './demo';
Vue.use(Router);
const baseRoutes: any = [
{
path: '/home',
name: 'home',
meta: {
keepAlive: true,
isBack: false,
index: 1
},
component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue'),
},
];
const routes = [
...baseRoutes,
...demoRoutes,
];
const router: Router = new Router({
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { x: 0, y: 0 };
}
}
});
router.beforeEach((to, from, next) => {
if (from.path === '/ipfc/result' || from.path === '/ipfc/vcode') {
to.meta.isBack = true;
}
next();
});
export default router;
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
declare module 'tpt-js-sdk';
import Vue from 'vue';
import Vuex from 'vuex';
import num from './modules/num';
import user from './modules/user';
import createPersistedState from 'vuex-persistedstate';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
num,
user,
},
plugins: [
createPersistedState({
storage: window.sessionStorage,
reducer(val) {
return {
user: val.user
};
}
})
]
});
/**
* 请求数量的加减 用来控制loading
* 路由跳转控制是否缓存
*/
const state = {
num: 0,
excludePage: [],
};
const actions = {
add(context: any) {
context.commit('add');
},
del(context: any) {
context.commit('del');
},
setExcludePageAct({ commit }: any, excludePage: any) {
commit('setExcludePage', excludePage);
},
};
const mutations = {
add(states: any) {
states.num ++;
},
del(states: any) {
states.num --;
},
setExcludePage(states: any, excludePage: any) {
states.excludePage = excludePage;
},
};
export default {
namespaced: true,
state,
actions,
mutations,
};
/**
* user信息
*/
const state = {
userInfo: {
token: '',
userId: ''
}
};
const actions = {
setUserInfo({ commit }: any, userInfo: any) {
commit('setUserInfo', userInfo);
}
};
const mutations = {
setUserInfo(states: any, userInfo: any) {
states.userInfo = userInfo;
}
};
export default {
namespaced: true,
state,
actions,
mutations
};
.row {
display: flex;
flex-direction: row;
}
.centerX {
justify-content: center;
}
.centerY {
align-items: center;
}
.betweenX {
justify-content: space-between;
}
.between {
justify-content: space-between;
}
.font-size-{
&36 {
font-size: 36px;
}
&32 {
font-size: 32px;
}
&28 {
font-size: 28px;
}
&24 {
font-size: 24px;
}
&20 {
font-size: 20px;
}
}
.font-color-{
&20253C {
color: #20253C;
}
&666 {
color: #666;
}
&fff {
color: #fff;
}
&0E6DCF {
color: #0E6DCF;
}
}
\ No newline at end of file
.reload-vant- {
&border {
.van-cell:not(:last-child)::after {
border: none;
}
}
&label {
.van-field__label {
max-width: 200px;
}
}
&padding-left {
.van-cell {
padding-left: 0;
}
}
}
\ No newline at end of file
<template>
<div class="demo">
<Form @submit="onSubmit">
<PickerView
label="时间选择"
name="time"
required
type="date"
placeholder="时间选择"
:minDate="new Date()"
/>
<PickerView
label="一般选择器"
name="city"
:required="false"
placeholder="一般选择"
:columns="columns"
showName="city"
/>
<Button
size="large"
type="warning"
native-type="submit"
>下一步</Button>
</Form>
<UploaderImg @choiceImage="choiceImage">
<Icon class-prefix="kht"
name="camera"
size="24px"
color="#333333"
/>
</UploaderImg>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import PickerView from '@/components/PickerView.vue';
import UploaderImg from '@/components/UploaderImg.vue';
import homeAPI from '@/api/home';
@Component({
name: 'home',
components: {
PickerView,
UploaderImg,
},
})
export default class Home extends Vue {
private columns: any[] = [
{city: '北京', id: '1'},
{city: '上海', id: '2'},
{city: '深圳', id: '3'},
{city: '杭州', id: '4'},
];
public async activated(): Promise<any> {
if (!this.$route.meta.isBack) {
await homeAPI.nameSet();
}
this.$route.meta.isBack = false;
}
private choiceImage(photos: any) {
// todo
}
private onSubmit(values: any) {
// todo
}
}
</script>
<style lang="scss" scoped>
.demo {
width: 100vw;
height: 100%;
}
</style>
\ No newline at end of file
<template>
<div class="about">
<h1>This is an Vuex Test page</h1>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import homeAPI from '@/api/home';
@Component
export default class Home extends Vue {
public async activated(): Promise<any> {
if (!this.$route.meta.isBack) {
await homeAPI.nameSet();
}
this.$route.meta.isBack = false;
}
}
</script>
\ No newline at end of file
<template>
<div class="home">
<img alt="Vue logo" src="@/assets/images/logo.png">
<Icon class-prefix="kht" name="question" size="32px" color="#05C449" />
<Button @click="_add">+</Button>
{{ num }}
<Button @click="_del" :disabled="num<=0">-</Button>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import homeAPI from '@/api/home';
Component.registerHooks([
'beforeRouteEnter',
]);
@Component({
components: {
},
})
export default class Home extends Vue {
private num: number = 0;
public beforeRouteEnter(to: any, from: any, next: any): void {
document.title = '首页';
if (from.name === 'about') {
to.meta.isBack = true;
}
next();
}
public async activated(): Promise<any> {
if (!this.$route.meta.isBack) {
await homeAPI.nameSet();
}
this.$route.meta.isBack = false;
}
private _add(): void {
this.num ++;
}
private _del(): void {
if (this.num > 0) {
this.num --;
}
}
}
</script>
<style scoped>
.home {
width: 100vw;
height: 100%;
}
[w-375-246]{
width: 375px;
}
[w-375-246]{
aspect-ratio:'375:246';
}
[w-188-246]{
width: 188px;
}
[w-188-246]{
aspect-ratio:'188:246';
}
[w-187-246]{
width: 187px;
}
[w-187-246]{
aspect-ratio:'187:246';
}
@svg 1px-border {
height: 2px;
@rect {
fill: var(--color, black);
width: 100%;
height: 50%;
}
}
[w-369]{
width: 369px;
}
[w-324-324]{
width: 324px;
background: #fff;
overflow: hidden;
}
[w-324-324]{
aspect-ratio:'324:324';
}
ul[w-369]:nth-child(2n) {
margin-left: 12px;
}
ul[w-369] li {
background: #fff;
margin-bottom: 12px;
}
[w-369] figcaption {
padding: 20px;
}
[w-369] figcaption div {
font-size: 22px;
color: #999;
}
[w-369] h2 {
margin: 0 0 30px;
font-size: 26px;
color: #333;
font-weight: 400;
}
[w-369] h2 span, [w-369] i {
color: #ff5000;
font-size: 26px;
margin-right: 20px;
}
[w-369] h2 span {
background: #ff5000;
color: #fff;
display: inline-block;
border-radius: 4px;
text-shadow: 0 2px 2px #ff5000;
padding: 2px 5px;
}
[w-369] i {
font-size: 20px;
margin-right: 5px;
font-style: normal;
}
.block-5 li {
background: #fff;
border-bottom: 1px solid transparent;
margin-bottom: 15px;
border-image: svg(1px-border param(--color #333)) 2 2 stretch;
}
[aspectratio-content] img {
width: 100%;
height: 100%;
vertical-align: top;
}
.block-5 figcaption {
flex: 1;
padding: 30px;
}
.block-5 h2 {
font-size: 32px;
font-weight: 400;
margin: 0 0 20px;
}
.block-5 figcaption div {
font-size: 30px;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
</style>
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
{
"defaultSeverity": "warning",
"extends": ["tslint:recommended"],
"linterOptions": {
"exclude": ["node_modules/**", "App.vue", ".babelrc"]
},
"rules": {
"quotemark": [true, "single"],
"trailing-comma": [false],
"indent": [true, "spaces", 2],
"interface-name": false,
"ordered-imports": false,
"no-console": false,
"arrow-parens": ["as-needed"],
"object-literal-sort-keys": false,
"no-consecutive-blank-lines": false
}
}
// vue.config.js
const tsImportPluginFactory = require('ts-import-plugin');
module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? '/static/assure' : '/',
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境...
} else {
// 为开发环境修改配置...
}
config.module.rules.push({
test: /\.(jsx|tsx|js|ts)$/,
use: [
{
loader: 'ts-loader',
options: {
happyPackMode: true, // IMPORTANT! use happyPackMode mode to speed-up compilation and reduce errors reported to webpack
transpileOnly: true,
getCustomTransformers: () => ({
before: [
tsImportPluginFactory({
libraryName: 'vant',
libraryDirectory: 'lib',
style: true
})
]
}),
compilerOptions: {
module: 'es2015'
}
}
}
],
exclude: /node_modules/
});
},
devServer: {
proxy: {
'/sit': {
target: 'https://ecustomer.tp95589.com', //代理接口
changeOrigin: true,
pathRewrite: {
'^/sit': '/sit' //代理的路径
}
},
// http://10.28.136.167:8773/
// '/lifems': {
// target: 'https://58.49.129.4', //代理接口
// changeOrigin: true,
// pathRewrite: {
// '^/lifems': '/lifems' //代理的路径
// }
// },
'/static': {
target: 'https://ecustomer.tp95589.com', //代理接口
changeOrigin: true,
pathRewrite: {
'^/static': '/static' //代理的路径
}
}
}
},
css: {
loaderOptions: {
stylus: {
'resolve url': true,
import: []
}
}
},
parallel: false
};
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment