Commit 74afb124 authored by Nature's avatar Nature

Initial commit

parents
{
"presets": [
["env", {
"modules": false
}],
"stage-2"
],
"plugins": ["transform-runtime"]
}
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
build/
config/
static/
test/
dist/
src/assets/
src/libs/
!src/data/config/
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
// 指定环境
env: {
browser: true,
node: true,
jest: true,
es6: true,
},
// 全局变量
"globals": {
navigator: false,
cordova: false,
HandBridge: false,
process: true,
$config: true,
JMessagePlugin:true,
vum:true,
hlsPopup:true,
hlsHttp:true,
hlsUtil:true
},
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
extends: ['plugin:vue/recommended', 'standard'],
// required to lint *.vue files
plugins: ['vue', 'import'],
// check if imports actually resolve
settings: {
'import/resolver': {
webpack: {
config: 'build/webpack.base.conf.js'
}
}
},
// add your custom rules here
// "off" 或 0 - 关闭规则
// "warn" 或 1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出)
// "error" 或 2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)
rules: {
// 以下为自定义的 javascript 规则:
// 箭头函数当只有一个参数时允许省略圆括号
'arrow-parens': 0,
// 允许生成器
'generator-star-spacing': 0,
// 对象和数组结尾强制分号
"comma-dangle": [2, {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "ignore",
}],
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
"no-array-constructor": 0,
"no-irregular-whitespace": 0,
"no-new-wrappers": 0,
"no-alert": 0,
"no-eq-null": 2,
// 以下为自定义的 plugin-vue 规则:
// 单行中允许多个属性
"vue/max-attributes-per-line": ['error', {
"singleline": 4,
"multiline": {
"max": 4,
"allowFirstLine": false
}
}],
// 关闭属性名必须是 '-' 连接
"vue/attribute-hyphenation": 0,
"vue/require-v-for-key": 0,
// 以下为自定义的 plugin-import 规则:
// https://github.com/benmosher/eslint-plugin-import
// don't require .vue extension when importing
'import/extensions': ['error', 'always', {
js: 'never',
vue: 'never'
}],
// allow optionalDependencies
'import/no-extraneous-dependencies': ['error', {
optionalDependencies: ['test/unit/index.js']
}],
// 关闭必须默认输出
'import/prefer-default-export': ['off'],
}
}
.DS_Store
node_modules/
platforms/
www/
.idea/
/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*~
*.sw[mnpcod]
*.log
*.tmp
*.tmp.*
log.txt
*.sublime-project
*.sublime-workspace
Thumbs.db
UserInterfaceState.xcuserstate
# OS X
.DS_Store
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
// to edit target browsers: use "browserslist" field in package.json
"postcss-import": {},
"autoprefixer": {}
}
}
*.min.css
iconfont.css
// https://stylelint.io/user-guide/configuration
module.exports = {
defaultSeverity: 'warning',
extends: 'stylelint-config-standard',
rules: {
'rule-empty-line-before': null,
'selector-list-comma-newline-after': 'always-multi-line',
},
};
# hls-car-vue
> A Vue.js project
## Build Setup
``` bash
# install dependencies
yarn install
# serve with hot reload at localhost:8080
npm run dev
# build for uat with minification
npm run build:uat
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
```
For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
### 文件命名规范
1. 文件夹全部采用驼峰命名法,即首字母小写后面每个单词首字母大写
2. 文件名全部采用驼峰命名法,如 **userInfo.vue,userInfoDetail.vue**,
3. 路由的注册 `import` 语句后的单词采用 Pascal命名法,所有单词的首字母大写,其余字母小写,单词与单词之间不使用任何符号风格。如
```javascript
import HomeManager from '@/pages/homeManager/homeManager'
import UserInfo from '@/pages/userInfo/userInfo'
import UserInfoDetail from '@/pages/userInfo/userInfoDetail'
```
4. 实际路由注册需安照如下写法,`path``/tab/文件名`,`/tab`是否保留视实际情况而定。`component`后接的单词需和`import`的单词保持一致,`name`后接的单词也需和`import`的单词保持一致
```javascript
{path: "/tab/homeManager", component: HomeManager, name: 'HomeManager', meta: {keepAlive: true}},
{path: '/tab/userInfo', component: UserInfo, name: 'UserInfo', meta: {keepAlive: true}},
```
# keyStore签名信息
keystore文件 hlscar.keystore
别名 HLSkey
密码 com.hls.easy.car
# 签名
jarsigner -verbose -keystore hls.keystore -signedjar 车租易.apk hls.apk HLSkey
# 打包冲突解决
各项目如果安装了 com.hls.plugins.barcode 扫码插件与cordova-plugin-open-camera 媒体插件
两个插件之间存在一些冲突 请注释掉媒体插件 plugin.xml 第83行 <uses-feature android:name="android.hardware.camera" />
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
require('./check-versions')()
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
var app = express()
var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {}
})
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())
// serve webpack bundle output
app.use(devMiddleware)
// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)
// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port
var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
})
console.log('> Starting dev server...')
devMiddleware.waitUntilValid(() => {
console.log('> Listening at ' + uri + '\n')
// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
_resolve()
})
var server = app.listen(port)
module.exports = {
ready: readyPromise,
close: () => {
server.close()
}
}
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const pkg = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const px2remLoader = {
loader: 'px2rem-loader',
options: {
remUnit: options.remUnit,
},
};
var postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
//const loaders = options.usePostCSS ? [cssLoader, postcssLoader,px2remLoader] : [cssLoader]
let loaders = options.useFlexible ? [cssLoader, px2remLoader]: [cssLoader]
loaders = options.usePostCSS ? [...loaders, postcssLoader] : loaders
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader',
publicPath:'../../'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
exports.createNotifierCallback = function () {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') {
return
}
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: pkg.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
useFlexible: config.base.useFlexible,
remUnit: config.base.remUnit,
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: 'src',
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
const vuxLoader = require('vux-loader')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
let webpackConfig = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
}
module.exports = vuxLoader.merge(webpackConfig, {
plugins: [
'vux-ui',
'progress-bar',
{
name: 'duplicate-style',
options: {
cssProcessorOptions : {
safe: true,
zindex: false,
autoprefixer: {
add: true,
browsers: [
'iOS >= 7',
'Android >= 4.1'
]
}
}
}
},
{name: 'less-theme', path: 'src/styles/variables.less'}
]
})
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: true,
hot: true,
compress: true,
host: process.env.HOST || config.dev.host,
port: process.env.PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay ? {
warnings: false,
errors: true,
} : false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env'),
$config: require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${config.dev.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
//console.log(config)
const env = require(`../config/${config.ENV}.env`)
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: false
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env,
$config: env,
}),
// UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
drop_debugger: true,
drop_console: true
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// set the following option to `true` if you want to extract CSS from
// codesplit chunks into this main css file as well.
// This will result in *all* of your app's CSS being loaded upfront.
allChunks: false,
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vender modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
CONFIG_ENV: JSON.stringify(process.env.CONFIG_ENV),
debug: true,
isMobilePlatform: false,
appCode:'"HLS_APP"',
clearTable: true,
loginPath: '"http://hlsapp.hand-china.com/core/oauth/token?client_id=hQGCtxTItRa34PUOgxaD0r7oSPeuEaIB&client_secret=7ee8338c-4a06-44a1-87cc-afa63f8e1bc3&grant_type=password&username=app&password=" ',
basePath: '"http://hlsapp.hand-china.com/core/r/api?sysName=HLS_APP&apiName="',
rootPath: '"http://hlsapp.hand-china.com/core/r/api"',
riskPath: '"http://hlsapp.hand-china.com/core/r/api/app/riskQuery?sysName=HLS_RISK&apiName="',
riskKey: '"pIGBbSRyThA4Xg"',
pushKey: '"45ace0e521f01edb2f1c753a"',
file_url: '"http://hlsapp.hand-china.com/file/"',
appId: '"com.hls.easy.car"',
currentVersion: '"1.0.0"',
dbName: '"Function.db"',
dbLocation: 0,
});
'use strict'
// Template version: 1.2.4
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
const CONFIG_ENV = process.env.CONFIG_ENV
module.exports = {
ENV:CONFIG_ENV,
base: {
// Use Flexible?
useFlexible: true,
remUnit: 50, // 启用flexible时的根字体大小(px)
// Use PostCSS?
usePostCSS: false,
},
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false,
ENV:CONFIG_ENV
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../www/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../www'),
assetsSubDirectory: 'static',
assetsPublicPath: './',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report,
ENV:CONFIG_ENV
}
}
'use strict'
module.exports = {
NODE_ENV: '"production"',
CONFIG_ENV: JSON.stringify(process.env.CONFIG_ENV),
debug: false,
isMobilePlatform: true,
appCode:'"HLS_APP"',
clearTable: true,
loginPath: '"http://hlsapp.hand-china.com/core/oauth/token?client_id=hQGCtxTItRa34PUOgxaD0r7oSPeuEaIB&client_secret=7ee8338c-4a06-44a1-87cc-afa63f8e1bc3&grant_type=password&username=app&password=" ',
basePath: '"http://hlsapp.hand-china.com/core/r/api?sysName=HLS_APP&apiName="',
rootPath: '"http://hlsapp.hand-china.com/core/r/api"',
riskPath: '"http://hlsapp.hand-china.com/core/r/api/app/riskQuery?sysName=HLS_RISK&apiName="',
riskKey: '"pIGBbSRyThA4Xg"',
pushKey: '"45ace0e521f01edb2f1c753a"',
file_url: '"http://hlsapp.hand-china.com/file/"',
appId: '"com.hls.easy.car"',
currentVersion: '"1.0.0"',
dbName: '"Function.db"',
dbLocation: 0,
}
'use strict'
module.exports = {
NODE_ENV: '"production"',
CONFIG_ENV: JSON.stringify(process.env.CONFIG_ENV),
debug: false,
isMobilePlatform: true,
appCode:'"HLS_APP"',
clearTable: true,
loginPath: '"http://hlsapp.hand-china.com/core/oauth/token?client_id=hQGCtxTItRa34PUOgxaD0r7oSPeuEaIB&client_secret=7ee8338c-4a06-44a1-87cc-afa63f8e1bc3&grant_type=password&username=app&password=" ',
basePath: '"http://hlsapp.hand-china.com/core/r/api?sysName=HLS_APP&apiName="',
rootPath: '"http://hlsapp.hand-china.com/core/r/api"',
riskPath: '"http://hlsapp.hand-china.com/core/r/api/app/riskQuery?sysName=HLS_RISK&apiName="',
riskKey: '"pIGBbSRyThA4Xg"',
pushKey: '"45ace0e521f01edb2f1c753a"',
file_url: '"http://hlsapp.hand-china.com/file/"',
appId: '"com.hls.easy.car"',
currentVersion: '"1.0.0"',
dbName: '"Function.db"',
dbLocation: 0,
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, width=device-width, viewport-fit=cover">
<meta name="format-detection" content="telephone=no">
<meta name="format-detection" content="email=no">
<!-- safari私有meta标签 允许全屏模式浏览 指定safari顶部状态栏样式(黑色) -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<script type="text/javascript" src="../../cordova.js"></script>
<title>车租易</title>
</head>
<body>
<div id="app-box"></div>
<!-- built files will be auto injected -->
</body>
</html>
{
"name": "hls-vue-app",
"version": "1.0.0",
"description": "A Vue.js project",
"author": "JingChao <jingchao.wu@hand-china.com>",
"private": true,
"scripts": {
"dev": "cross-env CONFIG_ENV=dev webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"build:uat": "cross-env CONFIG_ENV=uat node build/build.js",
"build": "cross-env CONFIG_ENV=prod node build/build.js"
},
"dependencies": {
"autosize": "^3.0.20",
"better-scroll": "^1.10.3",
"crypto-js": "^3.1.9-1",
"fastclick": "https://github.com/fiso/fastclick.git",
"vue": "^2.5.2",
"vue-router": "^3.0.1",
"vuex": "^2.1.1",
"vuex-i18n": "^1.3.1",
"vux": "^2.9.2",
"hls-easy-ui": "https://hel.hand-china.com/easyUI/hls-easy-ui.git"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^8.2.1",
"babel-loader": "^7.1.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^2.0.1",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"cross-env": "^5.2.0",
"css-loader": "^0.28.0",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-import-resolver-webpack": "^0.8.3",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.5.0",
"eventsource-polyfill": "^0.9.6",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"prelude-ls": "^1.1.2",
"px2rem-loader": "^0.1.9",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"stylelint": "^8.0.0",
"stylelint-config-standard": "^18.2.0",
"stylelint-webpack-plugin": "^0.10.4",
"style-loader": "^0.21.0",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"url-loader": "^0.5.8",
"vconsole": "^3.2.0",
"vue-infinite-scroll": "^2.0.2",
"vue-loader": "^13.3.0",
"vue-loading-template": "^1.3.0",
"vue-slim-better-scroll": "^1.4.1",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"vux-loader": "^1.0.56",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-middleware": "^1.10.0",
"webpack-dev-server": "^2.9.1",
"webpack-hot-middleware": "^2.16.1",
"webpack-merge": "^4.1.0",
"yaml-loader": "^0.4.0"
},
"cordovaPlugins": [
"cordova-plugin-console",
"cordova-plugin-device",
"cordova-plugin-statusbar",
"cordova-plugin-splashscreen",
"cordova-plugin-whitelist",
"ionic-plugin-keyboard",
"cordova-plugin-inappbrowser"
],
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"iOS >= 7",
"Android >= 4.1"
],
"cordova": {
"platforms": [
"android"
],
"plugins": {
"cordova-plugin-camera": {},
"cordova-plugin-device": {},
"cordova-plugin-whitelist": {},
"cordova-plugin-crosswalk-webview": {
"XWALK_VERSION": "22+",
"XWALK_LITEVERSION": "xwalk_core_library_canary:17+",
"XWALK_COMMANDLINE": "--disable-pull-to-refresh-effect",
"XWALK_MODE": "embedded",
"XWALK_MULTIPLEAPK": "true"
}
}
}
}
<template>
<div id="app">
<transition :name="transitionName">
<keep-alive>
<router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
</transition>
<transition :name="transitionName">
<router-view v-if="!$route.meta.keepAlive"/>
</transition>
</div>
</template>
<script>
export default {
data () {
return {
pathList: [],
transitionName: 'slide-left',
}
},
watch: { // 监听路由变化
$route (to, from) {
if (this.pathList.includes(to.path)) {
const index = (this.pathList.findIndex(() => {
return from.path
}))
this.pathList.splice(index, 1)
this.$router.isBack = true
} else {
this.pathList.push(to.path)
this.$router.isBack = false
}
if (to.path === '/home') {
this.$router.isBack = true
this.pathList = []
}
let isBack = this.$router.isBack
if (isBack) {
this.transitionName = 'router-slide-right'
} else {
this.transitionName = 'router-slide-left'
}
this.$router.isBack = false
},
},
mounted () {
this.getAccessToken()
},
methods: {
getAccessToken () {
let vm = this
let url = process.env.loginPath + 'appadmin'
let param = {}
vm.hlsHttp.post(url, param).then(function (res) {
window.localStorage.setItem('access_token', res.access_token)
})
},
// 右滑返回上一页
onSwipeRight () {
if (this.$route.name === 'Home') return
this.$router.go(-1)
},
},
}
</script>
<style lang="less">
html, body, #app {
height: 100%;
width: 100%;
overflow-x: hidden;
//overflow: auto;
//-webkit-overflow-scrolling: touch;
//overflow-scrolling: touch;
}
</style>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="rolling" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve">
<path fill="#ccc" d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="1s"
from="0 50 50"
to="360 50 50"
repeatCount="indefinite"/>
</path>
</svg>
### 此文件夹存放第三方js
/**
* 基础 mixins
*/
import { case2Param } from '../utils'
const preName = 'hls'
export default {
methods: {
/**
* 生成 css class helper
* c() // 'hls-componentName'
* c('wrap') // 'hls-componentName-wrap'
* c('wrap--active') // 'hls-componentName-wrap--active'
* @param {String} className 类名
* @returns String
*/
c (className) {
const { name } = this.$options // 组件名
return className ? `${preName}-${case2Param(name)}-${className}` : `${preName}-${case2Param(name)}`
},
},
}
import base from './base'
import prefix from './prefix'
import touch from './touch'
export {
base,
prefix,
touch,
}
const preName = 'hls'
export default {
methods: {
// 生成 css class
b (className) {
return className ? `${preName}-${className}` : ''
},
},
}
export default {
methods: {
onTouchStart (event) {
this.direction = ''
this.deltaX = 0
this.deltaY = 0
this.offsetX = 0
this.offsetY = 0
this.startX = event.touches[0].clientX
this.startY = event.touches[0].clientY
},
onTouchMove (event) {
const touch = event.touches[0]
this.deltaX = touch.clientX - this.startX
this.deltaY = touch.clientY - this.startY
this.offsetX = Math.abs(this.deltaX)
this.offsetY = Math.abs(this.deltaY)
this.direction = this.offsetX > this.offsetY ? 'horizontal' : this.offsetX < this.offsetY ? 'vertical' : ''
},
},
}
/**
* 一些帮助函数
*/
/**
* setTimeout 的 promise 封装
* @param {Number} time
* @returns
*/
export function timeout (time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
/**
* 断言
* @param {any} condition 条件
* @param {any} msg 信息
*/
export function assert (condition, msg) {
if (!condition) throw new Error(`[hips] ${msg}`)
}
/**
* 改变 case 格式为 param
* @param {String} str 字符串
*/
export function case2Param (str) {
assert(typeof str === 'string', 'case2Param 传入数据类型错误:应为 String')
str = str.replace(/^[A-Z]/g, $0 => $0.toLowerCase())
return str.replace(/[A-Z]/g, $0 => `-${$0.toLowerCase()}`)
}
/**
* 判断平台
* @return {String} 平台
*/
export function detectOS () {
const ua = navigator.userAgent.toLowerCase()
if (/MicroMessenger/i.test(ua)) {
return 'weixin'
} else if (/iPhone|iPad|iPod|iOS/i.test(ua)) {
return 'ios'
} else if (/Android/i.test(ua)) {
return 'android'
} else {
return 'other'
}
}
/* eslint-disable */
/**
* YDUI 可伸缩布局方案
* rem计算方式:设计图尺寸px / 50 = 实际rem 例: 100px = 2rem
*/
!(function(window) {
/* 设计图文档宽度 */
var docWidth = 750;
var doc = window.document,
docEl = doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
var recalc = (function refreshRem() {
var clientWidth = docEl.getBoundingClientRect().width;
/* 8.55:小于320px不再缩小,11.2:大于420px不再放大 */
docEl.style.fontSize = Math.max(Math.min(20 * (clientWidth / docWidth), 11.2), 8.55) * 5 + 'px';
return refreshRem;
})();
/* 添加倍屏标识,安卓倍屏为1 */
docEl.setAttribute('data-dpr', window.navigator.appVersion.match(/iphone/gi) ? window.devicePixelRatio : 1);
if (/iP(hone|od|ad)/.test(window.navigator.userAgent)) {
/* 添加IOS标识 */
doc.documentElement.classList.add('ios');
/* IOS8以上给html添加hairline样式,以便特殊处理 */
if (parseInt(window.navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/)[1], 10) >= 8) doc.documentElement.classList.add('hairline');
}
if (!doc.addEventListener) return;
window.addEventListener(resizeEvt, recalc, false);
doc.addEventListener('DOMContentLoaded', recalc, false);
})(window);
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import FastClick from 'fastclick'
import App from './App'
import Vuex from 'vuex'
import vuexI18n from 'vuex-i18n'
import router from './router/index'
import flexible from './common/ydui.flexible'
import {componentInstall, appStyle} from 'hls-easy-ui'
/**
* 指令
*/
import directives from './scripts/directives'
import filter from './scripts/filter'
/**
* 组件
*/
import {
ViewBox,
Tabbar,
TabbarItem,
XHeader,
Picker,
PopupHeader,
} from 'vux'
import './scripts/prototype'
import './scripts/vuePlatform'
/**
* 弹框组件
*/
import hlsPopup from './scripts/hlsPopup'
/**
* http
*/
import { post, get } from './scripts/hlsHttp'
/** 全局函数hlsUtil**/
import hlsUtil from './scripts/hlsUtil'
Vue.use(componentInstall)
Vue.use(appStyle)
/** end**/
if (process.env.CONFIG_ENV === 'uat') {
const VConsole = require('vconsole')
new VConsole() // eslint-disable-line
}
Vue.use(flexible)
Vue.use(directives)
Vue.use(filter)
Vue.use(Vuex)
// Vue.directive('transfer-dom', TransferDom);
Vue.component('x-header', XHeader)
Vue.component('view-box', ViewBox)
Vue.component('tabbar', Tabbar)
Vue.component('tabbar-item', TabbarItem)
Vue.component('PopupHeader', PopupHeader)
Vue.component('picker', Picker)
/** i18n **/
let store = new Vuex.Store({
modules: {
i18n: vuexI18n.store,
},
})
Vue.use(vuexI18n.plugin, store)
Vue.prototype.hlsPopup = window.hlsPopup = hlsPopup
Vue.prototype.$devicePixelRatio = 2
Vue.prototype.$post = post
Vue.prototype.$get = get
let hlsHttp = {
get: get,
post: post,
}
Vue.prototype.hlsHttp = window.hlsHttp = hlsHttp
Vue.prototype.hlsUtil = window.hlsUtil = hlsUtil
/**
* 全局返回上一页面
* @param index
*/
let routeGo = function (index) {
if (!index) {
index = -1
}
this.$router.go(index)
}
Vue.prototype.$routeGo = routeGo
let hlsExit = function () {
if (vum.Platform.isIOS()) {
cordova.exec(null, null, 'BridgePlugin', 'closeWebView', [])
} else if (vum.Platform.isIOS()) {
var dict = {
'className': 'WebBridge',
'function': 'close',
'successCallBack': 'sCallBack',
'failureCallBack': 'eCallBack',
}
HandBridge.postMessage(JSON.stringify(dict))
}
}
Vue.prototype.$hlsExit = hlsExit
FastClick.attach(document.body)
Vue.config.productionTip = false
vum.$vumPlatform.ready(function () {
if ((vum.Platform.isAndroid()) || (vum.Platform.isIOS())) {
}
})
vum.$vumPlatform.registerBackButtonAction(function (e) {
}, 101)
/* eslint-disable no-new */
new Vue({
data () {
return {
pathList: [],
transitionName: null,
}
},
router,
watch: { // 监听路由变化
$route (to, from) {
document.body.scrollTop = 0
document.documentElement.scrollTop = 0
},
},
render: h => h(App),
}).$mount('#app-box')
This diff is collapsed.
This diff is collapsed.
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/pages/home'
// test工具类
import HlsPopup from '@/pages/hlsPopup'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
redirect: '/home',
},
{path: '/home', component: Home, name: 'Home', meta: {keepAlive: true}},
// test工具类
{path: '/hls-popup', component: HlsPopup, name: 'HlsPopup', meta: {keepAlive: false}},
],
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash,
}
}
},
})
This diff is collapsed.
export default{
createElement: function (marker, tag) {
let el = document.createElement(tag || 'div')
el.setAttribute(marker, '')
document.body.appendChild(el)
},
removeElement: function (marker) {
let el = document.querySelector(marker) || document.querySelector(`[${marker}]`)
if (el) { document.body.removeChild(el) }
},
timeout: function (duration = 0) {
return new Promise((resolve, reject) => {
setTimeout(resolve, duration)
})
},
}
export default (Vue) => {
Vue.filter('currency', function (val) {
if (!val) return '0.00'
var intPart = Number(val).toFixed(0) // 获取整数部分
var intPartFormat = intPart.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') // 将整数部分逢三一断
var floatPart = '.00' // 预定义小数部分
var value2Array = (val + '').split('.')
// =2表示数据有小数位
if (value2Array.length === 2) {
floatPart = value2Array[1].toString() // 拿到小数部分
if (floatPart.length === 1) { // 补0,实际上用不着
return intPartFormat + '.' + floatPart + '0'
} else {
return intPartFormat + '.' + floatPart
}
} else {
return intPartFormat + floatPart
}
})
Vue.filter('datetime', timestamp => {
function format (number) {
return number.toString().padStart(2, '0')
}
const date = new Date(Number.parseInt(timestamp, 10))
const YYYY = date.getFullYear()
const MM = date.getMonth() + 1
const DD = date.getDate()
const hh = date.getHours()
const mm = date.getMinutes()
const ss = date.getSeconds()
return `${YYYY}-${format(MM)}-${format(DD)} ${format(hh)}:${format(mm)}:${format(ss)}`
})
}
// 引入axios
import axios from 'axios'
import hlsPopup from './hlsPopup'
let promiseArr = {}
let cancel = {}
const CancelToken = axios.CancelToken
// 请求拦截器
axios.interceptors.request.use(config => {
// 发起请求时,取消掉当前正在进行的相同请求
if (promiseArr[config.url]) {
promiseArr[config.url]('操作取消')
promiseArr[config.url] = cancel
} else {
promiseArr[config.url] = cancel
}
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截器即异常处理
axios.interceptors.response.use(response => {
if ($config.debug) {
let postName = 'post'
console.log(postName + ' success')
console.log(postName + ' response ' + JSON.stringify(response.data, '', 2))
console.log(postName + ' End!')
}
if (response.data.result === 'E' || response.data.code === 'E') {
hlsPopup.hideLoading()
const err = {}
err.message = response.data.message
hlsPopup.showError(err.message)
return Promise.resolve(err)
} else {
return response.data
}
}, err => {
if (err && err.response) {
switch (err.response.status) {
case 400:
err.message = '错误请求'
break
case 401:
err.message = '登录已失效,请重新登录'
break
case 403:
err.message = '拒绝访问'
break
case 404:
err.message = '请求错误,未找到该资源'
break
case 405:
err.message = '不支持的请求类型'
break
case 408:
err.message = '请求超时'
break
case 500:
err.message = '服务器端出错'
break
case 501:
err.message = '网络未实现'
break
case 502:
err.message = '网络错误'
break
case 503:
err.message = '服务不可用'
break
case 504:
err.message = '网络超时'
break
case 505:
err.message = 'http版本不支持该请求'
break
default:
err.message = `连接错误${err.response.status}`
}
} else {
err.message = '连接到服务器失败'
}
if (err.response && err.response.status === 401) {
hlsPopup.hideLoading()
hlsPopup.showPopup({
title: '登录失效,重新登录',
onConfirm: () => {
// router.push({name: 'Login'})
if (vum.Platform.isIOS()) {
cordova.exec(null, null, 'BridgePlugin', 'closeWebView', [])
} else if (vum.Platform.isIOS()) {
var dict = {
'className': 'WebBridge',
'function': 'close',
'successCallBack': 'sCallBack',
'failureCallBack': 'eCallBack',
}
HandBridge.postMessage(JSON.stringify(dict))
}
},
})
} else {
hlsPopup.hideLoading()
hlsPopup.showError(err.message)
}
return Promise.resolve(err)
})
axios.defaults.baseURL = ''
axios.defaults.timeout = 10000
// get请求
export function get (url) {
let param = {}
let headers = {}
if (window.localStorage.access_token) {
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + window.localStorage.access_token,
}
} else {
headers = {
'Content-Type': 'application/json',
}
}
return new Promise((resolve, reject) => {
axios({
method: 'get',
url,
headers: headers,
params: param,
cancelToken: new CancelToken(c => {
cancel = c
}),
}).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
// post请求
export function post (url, param) {
param.user_id = window.localStorage.user_id
param.access_token = window.localStorage.access_token
let headers = {}
if (window.localStorage.access_token) {
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + window.localStorage.access_token,
}
} else {
headers = {
'Content-Type': 'application/json',
}
}
if (process.env.debug) {
let postName = 'POST'
console.log(postName + ' Start!')
console.log(postName + ' url ' + url)
console.log(postName + ' parameter ' + JSON.stringify(param, '', 2))
}
return new Promise((resolve, reject) => {
axios({
method: 'post',
headers: headers,
url,
data: param,
cancelToken: new CancelToken(c => {
cancel = c
}),
}).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment