commit 59c644956bc775bcfeee3efbdca35c0836a806c2 Author: mk <2403457699@qq.com> Date: Wed May 15 20:11:09 2024 +0800 init diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..d6471a3 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,2 @@ +> 1% +last 2 versions diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ea6e20f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..54e3512 --- /dev/null +++ b/.env.development @@ -0,0 +1,6 @@ +NODE_ENV='development' +# must start with VUE_APP_ +VUE_APP_ENV = 'development' +outputDir = 'epmet-work-h5-dev' + + diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..8deecc0 --- /dev/null +++ b/.env.production @@ -0,0 +1,5 @@ +NODE_ENV='production' +# must start with VUE_APP_ +VUE_APP_ENV = 'production' +outputDir = 'epmet-work-h5-prod' + \ No newline at end of file diff --git a/.env.staging b/.env.staging new file mode 100644 index 0000000..92749e3 --- /dev/null +++ b/.env.staging @@ -0,0 +1,4 @@ +NODE_ENV='production' +# must start with VUE_APP_ +VUE_APP_ENV = 'staging' + diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e6529fc --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +build/*.js +src/assets +public +dist diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..610b9aa --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,54 @@ +module.exports = { + root: true, + env: { + browser: true, + node: true, + es6: true, + 'vue/setup-compiler-macros': true + }, + globals: { + defineProps: 'readonly', + defineEmits: 'readonly' + }, + extends: ['plugin:vue/essential', 'eslint:recommended', 'plugin:prettier/recommended'], + parserOptions: { + parser: '@babel/eslint-parser' + }, + rules: { + 'no-console': 'warn', + 'no-debugger': 'warn', + 'vue/script-setup-uses-vars': 'error', + 'vue/custom-event-name-casing': 'off', + 'no-use-before-define': 'off', + 'no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_' + } + ], + 'space-before-function-paren': 'off', + 'vue/attributes-order': 'off', + 'vue/one-component-per-file': 'off', + 'vue/html-closing-bracket-newline': 'off', + 'vue/max-attributes-per-line': 'off', + 'vue/multiline-html-element-content-newline': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/attribute-hyphenation': 'off', + 'vue/require-default-prop': 'off', + 'vue/html-self-closing': [ + 'error', + { + html: { + void: 'always', + normal: 'never', + component: 'always' + }, + svg: 'always', + math: 'always' + } + ], + 'vue/v-on-event-hyphenation': 'off', + 'vue/multi-word-component-names': 'off' + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fdc3bc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +.DS_Store +node_modules +/dist +/docs +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +package-lock.json +yarn.lock \ No newline at end of file diff --git a/.postcssrc.js b/.postcssrc.js new file mode 100644 index 0000000..1b534ef --- /dev/null +++ b/.postcssrc.js @@ -0,0 +1,13 @@ +// https://github.com/michael-ciniawsky/postcss-load-config +module.exports = { + plugins: { + autoprefixer: { + overrideBrowserslist: ['Android 4.1', 'iOS 7.1', 'Chrome > 31', 'ff > 31', 'ie >= 8'] + }, + 'postcss-pxtorem': { + rootValue: 37.5, + propList: ['*'], + //selectorBlackList: ['van-'] + } + } +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..42061c0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..26e9376 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,24 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "none", + "semi": false, + "wrap_line_length": 120, + "wrap_attributes": "auto", + "proseWrap": "always", + "arrowParens": "avoid", + "bracketSpacing": true, + "jsxBracketSameLine": true, + "useTabs": false, + "eslintIntegration":true, + "overrides": [ + { + "files": ".prettierrc", + "options": { + "parser": "json" + } + } + ], + "endOfLine": "auto" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..880838d --- /dev/null +++ b/README.md @@ -0,0 +1,1212 @@ +# vue-h5-template + +基于 vue-cli4.0 + webpack 4 + vant ui + sass+ rem 适配方案+axios 封装 + jssdk配置 + vconsole移动端调试,构建手机端模板脚手架 + +掘金: [vue-cli4 vant rem 移动端框架方案](https://juejin.im/post/5cfefc73f265da1bba58f9f7) + +[查看 demo](https://solui.cn/vue-h5-template/#/) 建议手机端查看 + +

+ +

+ +### Node 版本要求 + +`Vue CLI` 需要 Node.js 8.9 或更高版本 (推荐 8.11.0+)。你可以使用 [nvm](https://github.com/nvm-sh/nvm) 或 +[nvm-windows](https://github.com/coreybutler/nvm-windows) 在同一台电脑中管理多个 Node 版本。 + +本示例 Node.js 12.14.1 + +### 项目结构 +>vue-h5-template -- UI主目录 +├── public -- 静态资源 +├ ├── favicon.ico -- 图标 +├ └── index.html -- 首页 +├── src -- 源码目录 +├ ├── api -- 后端交互的接口 +├ ├── assets -- 静态资源目录 +├ ├── css +├ ├── index.scss -- 全局通用样式 +├ ├── mixin.scss -- 全局mixin +├ └── variables.scss -- 全局变量 +├ ├── components -- 封装的组件 +├ ├── config -- 环境配置 +├ ├── const -- 放vue页面的配置常量 +├ ├── filters -- 过滤器 +├ ├── plugins -- 插件 +├ └── route -- VUE路由 +├ ├── index -- 路由入口 +├ └── router.config.js -- 路由表 +├ ├── store -- VUEX +├ └── util -- 工具包 +├ ├── request.js -- axios封装 +├ ├── vconsole.js -- 移动端调试插件 +├ ├── jsApiList.js -- 微信JS接口列表 +├ ├── wechatPlugin.js -- jssdk插件配置 +├ ├── storage.js -- 本地存储封装 +├ └── util -- 工具包 +├ └── views -- 业务上的vue页面 +├ ├── layouts -- 路由布局页面(是否缓存页面) +├ └── home -- 公众号 +├ ├── App.vue -- 根组件 +├ └── main.js -- 入口js +├── .env.development -- 开发环境 +├── .env.production -- 生产环境 +├── .env.staging -- 测试环境 +├── .editorconfig -- ESLint配置 +├── .gitignore -- git忽略 +├── .postcssrc.js -- CSS预处理配置(rem适配) +├── babel.config.js -- barbel配置入口 +├── jsconfig.json -- vscode路径引入配置 +├── package.json -- 依赖管理 +└── vue.config.js -- vue cli3的webpack配置 + +### 启动项目 + +```bash + +git clone https://github.com/sunniejs/vue-h5-template.git + +cd vue-h5-template + +npm install + +npm run serve +``` +目录 + +- √ Vue-cli4 +- [√ 配置多环境变量](#env) +- [√ rem 适配方案](#rem) +- [√ VantUI 组件按需加载](#vant) +- [√ Sass 全局样式](#sass) +- [√ Vuex 状态管理](#vuex) +- [√ Vue-router](#router) +- [√ Axios 封装及接口管理](#axios) +- [√ Webpack 4 vue.config.js 基础配置](#base) +- [√ 配置 alias 别名](#alias) +- [√ 配置 proxy 跨域](#proxy) +- [√ 配置 打包分析](#bundle) +- [√ 配置 externals 引入 cdn 资源 ](#externals) +- [√ 去掉 console.log ](#console) +- [√ splitChunks 单独打包第三方模块](#chunks) +- [√ 添加 IE 兼容 ](#ie) +- [√ Eslint+Pettier 统一开发规范 ](#pettier) +- [√ vconsole ](#vconsole) +- [√ 动态设置title ](#dyntitle) +- [√ 配置Jssdk ](#jssdk) +- [√ 本地存储storage封装 ](#storage) + +### ✅ 配置多环境变量 + +`package.json` 里的 `scripts` 配置 `serve` `stage` `build`,通过 `--mode xxx` 来执行不同环境 + +- 通过 `npm run serve` 启动本地 , 执行 `development` +- 通过 `npm run stage` 启动测试 , 执行 `development` +- 通过 `npm run prod` 启动开发 , 执行 `development` +- 通过 `npm run stageBuild` 打包测试 , 执行 `staging` +- 通过 `npm run build` 打包正式 , 执行 `production` + +```javascript +"scripts": { + "serve": "vue-cli-service serve --open", + "stage": "cross-env NODE_ENV=dev vue-cli-service serve --mode staging", + "prod": "cross-env NODE_ENV=dev vue-cli-service serve --mode production", + "stageBuild": "vue-cli-service build --mode staging", + "build": "vue-cli-service build", +} +``` + +##### 配置介绍 + +  以 `VUE_APP_` 开头的变量,在代码中可以通过 `process.env.VUE_APP_` 访问。 +  比如,`VUE_APP_ENV = 'development'` 通过`process.env.VUE_APP_ENV` 访问。 +  除了 `VUE_APP_*` 变量之外,在你的应用代码中始终可用的还有两个特殊的变量`NODE_ENV` 和`BASE_URL` + +在项目根目录中新建`.env.*` + +- .env.development 本地开发环境配置 + +```bash +NODE_ENV='development' +# must start with VUE_APP_ +VUE_APP_ENV = 'development' + +``` + +- .env.staging 测试环境配置 + +```bash +NODE_ENV='production' +# must start with VUE_APP_ +VUE_APP_ENV = 'staging' +``` + +- .env.production 正式环境配置 + +```bash + NODE_ENV='production' +# must start with VUE_APP_ +VUE_APP_ENV = 'production' +``` + +这里我们并没有定义很多变量,只定义了基础的 VUE_APP_ENV `development` `staging` `production` +变量我们统一在 `src/config/env.*.js` 里进行管理。 + +这里有个问题,既然这里有了根据不同环境设置变量的文件,为什么还要去 config 下新建三个对应的文件呢? +**修改起来方便,不需要重启项目,符合开发习惯。** + +config/index.js + +```javascript +// 根据环境引入不同配置 process.env.NODE_ENV +const config = require('./env.' + process.env.VUE_APP_ENV) +module.exports = config +``` + +配置对应环境的变量,拿本地环境文件 `env.development.js` 举例,用户可以根据需求修改 + +```javascript +// 本地环境配置 +module.exports = { + title: 'vue-h5-template', + baseUrl: 'http://localhost:9018', // 项目地址 + baseApi: 'https://test.xxx.com/api', // 本地api请求地址 + APPID: 'xxx', + APPSECRET: 'xxx' +} +``` + +根据环境不同,变量就会不同了 + +```javascript +// 根据环境不同引入不同baseApi地址 +import { baseApi } from '@/config' +console.log(baseApi) +``` + +[▲ 回顶部](#top) + +### ✅ rem 适配方案 + +不用担心,项目已经配置好了 `rem` 适配, 下面仅做介绍: + +Vant 中的样式默认使用`px`作为单位,如果需要使用`rem`单位,推荐使用以下两个工具: + +- [postcss-pxtorem](https://github.com/cuth/postcss-pxtorem) 是一款 `postcss` 插件,用于将单位转化为 `rem` +- [lib-flexible](https://github.com/amfe/lib-flexible) 用于设置 `rem` 基准值 + +##### PostCSS 配置 + +下面提供了一份基本的 `postcss` 配置,可以在此配置的基础上根据项目需求进行修改 + +```javascript +// https://github.com/michael-ciniawsky/postcss-load-config +module.exports = { + plugins: { + autoprefixer: { + overrideBrowserslist: ['Android 4.1', 'iOS 7.1', 'Chrome > 31', 'ff > 31', 'ie >= 8'] + }, + 'postcss-pxtorem': { + rootValue: 37.5, + propList: ['*'] + } + } +} +``` + +更多详细信息: [vant](https://youzan.github.io/vant/#/zh-CN/quickstart#jin-jie-yong-fa) + +**新手必看,老鸟跳过** + +很多小伙伴会问我,适配的问题。 + +我们知道 `1rem` 等于`html` 根元素设定的 `font-size` 的 `px` 值。Vant UI 设置 `rootValue: 37.5`,你可以看到在 iPhone 6 下 +看到 (`1rem 等于 37.5px`): + +```html + +``` + +切换不同的机型,根元素可能会有不同的`font-size`。当你写 css px 样式时,会被程序换算成 `rem` 达到适配。 + +因为我们用了 Vant 的组件,需要按照 `rootValue: 37.5` 来写样式。 + +举个例子:设计给了你一张 750px \* 1334px 图片,在 iPhone6 上铺满屏幕,其他机型适配。 + +- 当`rootValue: 70` , 样式 `width: 750px;height: 1334px;` 图片会撑满 iPhone6 屏幕,这个时候切换其他机型,图片也会跟着撑 + 满。 +- 当`rootValue: 37.5` 的时候,样式 `width: 375px;height: 667px;` 图片会撑满 iPhone6 屏幕。 + +也就是 iphone 6 下 375px 宽度写 CSS。其他的你就可以根据你设计图,去写对应的样式就可以了。 + +当然,想要撑满屏幕你可以使用 100%,这里只是举例说明。 + +```html + + + +``` + +[▲ 回顶部](#top) + +### ✅ VantUI 组件按需加载 + +项目采 +用[Vant 自动按需引入组件 (推荐)](https://youzan.github.io/vant/#/zh-CN/quickstart#fang-shi-yi.-zi-dong-an-xu-yin-ru-zu-jian-tui-jian)下 +面安装插件介绍: + +[babel-plugin-import](https://github.com/ant-design/babel-plugin-import) 是一款 `babel` 插件,它会在编译过程中将 +`import` 的写法自动转换为按需引入的方式 + +#### 安装插件 + +```bash +npm i babel-plugin-import -D +``` + +在`babel.config.js` 设置 + +```javascript +// 对于使用 babel7 的用户,可以在 babel.config.js 中配置 +const plugins = [ + [ + 'import', + { + libraryName: 'vant', + libraryDirectory: 'es', + style: true + }, + 'vant' + ] +] +module.exports = { + presets: [['@vue/cli-plugin-babel/preset', { useBuiltIns: 'usage', corejs: 3 }]], + plugins +} +``` + +#### 使用组件 + +项目在 `src/plugins/vant.js` 下统一管理组件,用哪个引入哪个,无需在页面里重复引用 + +```javascript +// 按需全局引入 vant组件 +import Vue from 'vue' +import { Button, List, Cell, Tabbar, TabbarItem } from 'vant' +Vue.use(Button) +Vue.use(Cell) +Vue.use(List) +Vue.use(Tabbar).use(TabbarItem) +``` + +[▲ 回顶部](#top) + +### ✅ Sass 全局样式 + +首先 你可能会遇到 `node-sass` 安装不成功,别放弃多试几次!!! + +每个页面自己对应的样式都写在自己的 .vue 文件之中 `scoped` 它顾名思义给 css 加了一个域的概念。 + +```html + + + +``` + +#### 目录结构 + +vue-h5-template 所有全局样式都在 `@/src/assets/css` 目录下设置 + +```bash +├── assets +│ ├── css +│ │ ├── index.scss # 全局通用样式 +│ │ ├── mixin.scss # 全局mixin +│ │ └── variables.scss # 全局变量 +``` + +#### 自定义 vant-ui 样式 + +现在我们来说说怎么重写 `vant-ui` 样式。由于 `vant-ui` 的样式我们是在全局引入的,所以你想在某个页面里面覆盖它的样式就不能 +加 `scoped`,但你又想只覆盖这个页面的 `vant` 样式,你就可在它的父级加一个 `class`,用命名空间来解决问题。 + +```css +.about-container { + /* 你的命名空间 */ + .van-button { + /* vant-ui 元素*/ + margin-right: 0px; + } +} +``` + +#### 父组件改变子组件样式 深度选择器 + +当你子组件使用了 `scoped` 但在父组件又想修改子组件的样式可以 通过 `>>>` 来实现: + +```css + +``` + +#### 全局变量 + +`vue.config.js` 配置使用 `css.loaderOptions` 选项,注入 `sass` 的 `mixin` `variables` 到全局,不需要手动引入 ,配 +置`$cdn`通过变量形式引入 cdn 地址,这样向所有 Sass/Less 样式传入共享的全局变量: + +```javascript +const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV) +const defaultSettings = require('./src/config/index.js') +module.exports = { + css: { + extract: IS_PROD, + sourceMap: false, + loaderOptions: { + // 给 scss-loader 传递选项 + scss: { + // 注入 `sass` 的 `mixin` `variables` 到全局, $cdn可以配置图片cdn + // 详情: https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders + prependData: ` + @import "assets/css/mixin.scss"; + @import "assets/css/variables.scss"; + $cdn: "${defaultSettings.$cdn}"; + ` + } + } + } +} +``` + +设置 js 中可以访问 `$cdn`,`.vue` 文件中使用`this.$cdn`访问 + +```javascript +// 引入全局样式 +import '@/assets/css/index.scss' + +// 设置 js中可以访问 $cdn +// 引入cdn +import { $cdn } from '@/config' +Vue.prototype.$cdn = $cdn +``` + +在 css 和 js 使用 + +```html + + +``` + +[▲ 回顶部](#top) + +### ✅ Vuex 状态管理 + +目录结构 + +```bash +├── store +│ ├── modules +│ │ └── app.js +│ ├── index.js +│ ├── getters.js +``` + +`main.js` 引入 + +```javascript +import Vue from 'vue' +import App from './App.vue' +import store from './store' +new Vue({ + el: '#app', + router, + store, + render: h => h(App) +}) +``` + +使用 + +```html + +``` + +[▲ 回顶部](#top) + +### ✅ Vue-router + +本案例采用 `hash` 模式,开发者根据需求修改 `mode` `base` + +**注意**:如果你使用了 `history` 模式,`vue.config.js` 中的 `publicPath` 要做对应的**修改** + +前往:[vue.config.js 基础配置](#base) + +```javascript +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) +export const router = [ + { + path: '/', + name: 'index', + component: () => import('@/views/home/index'), // 路由懒加载 + meta: { + title: '首页', // 页面标题 + keepAlive: false // keep-alive 标识 + } + } +] +const createRouter = () => + new Router({ + // mode: 'history', // 如果你是 history模式 需要配置 vue.config.js publicPath + // base: '/app/', + scrollBehavior: () => ({ y: 0 }), + routes: router + }) + +export default createRouter() +``` + +更多:[Vue Router](https://router.vuejs.org/zh/) + +[▲ 回顶部](#top) + +### ✅ Axios 封装及接口管理 + +`utils/request.js` 封装 axios ,开发者需要根据后台接口做修改。 + +- `service.interceptors.request.use` 里可以设置请求头,比如设置 `token` +- `config.hideloading` 是在 api 文件夹下的接口参数里设置,下文会讲 +- `service.interceptors.response.use` 里可以对接口返回数据处理,比如 401 删除本地信息,重新登录 + +```javascript +import axios from 'axios' +import store from '@/store' +import { Toast } from 'vant' +// 根据环境不同引入不同api地址 +import { baseApi } from '@/config' +// create an axios instance +const service = axios.create({ + baseURL: baseApi, // url = base api url + request url + withCredentials: true, // send cookies when cross-domain requests + timeout: 5000 // request timeout +}) + +// request 拦截器 request interceptor +service.interceptors.request.use( + config => { + // 不传递默认开启loading + if (!config.hideloading) { + // loading + Toast.loading({ + forbidClick: true + }) + } + if (store.getters.token) { + config.headers['X-Token'] = '' + } + return config + }, + error => { + // do something with request error + console.log(error) // for debug + return Promise.reject(error) + } +) +// respone拦截器 +service.interceptors.response.use( + response => { + Toast.clear() + const res = response.data + if (res.status && res.status !== 200) { + // 登录超时,重新登录 + if (res.status === 401) { + store.dispatch('FedLogOut').then(() => { + location.reload() + }) + } + return Promise.reject(res || 'error') + } else { + return Promise.resolve(res) + } + }, + error => { + Toast.clear() + console.log('err' + error) // for debug + return Promise.reject(error) + } +) +export default service +``` + +#### 接口管理 + +在`src/api` 文件夹下统一管理接口 + +- 你可以建立多个模块对接接口, 比如 `home.js` 里是首页的接口这里讲解 `user.js` +- `url` 接口地址,请求的时候会拼接上 `config` 下的 `baseApi` +- `method` 请求方法 +- `data` 请求参数 `qs.stringify(params)` 是对数据系列化操作 +- `hideloading` 默认 `false`,设置为 `true` 后,不显示 loading ui 交互中有些接口不需要让用户感知 + +```javascript +import qs from 'qs' +// axios +import request from '@/utils/request' +//user api + +// 用户信息 +export function getUserInfo(params) { + return request({ + url: '/user/userinfo', + method: 'post', + data: qs.stringify(params), + hideloading: true // 隐藏 loading 组件 + }) +} +``` + +#### 如何调用 + +```javascript +// 请求接口 +import { getUserInfo } from '@/api/user.js' + +const params = { user: 'sunnie' } +getUserInfo(params) + .then(() => {}) + .catch(() => {}) +``` + +[▲ 回顶部](#top) + +### ✅ Webpack 4 vue.config.js 基础配置 + +如果你的 `Vue Router` 模式是 hash + +```javascript +publicPath: './', +``` + +如果你的 `Vue Router` 模式是 history 这里的 publicPath 和你的 `Vue Router` `base` **保持一直** + +```javascript +publicPath: '/app/', +``` + +```javascript +const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV) + +module.exports = { + publicPath: './', // 署应用包时的基本 URL。 vue-router hash 模式使用 + // publicPath: '/app/', // 署应用包时的基本 URL。 vue-router history模式使用 + outputDir: 'dist', // 生产环境构建文件的目录 + assetsDir: 'static', // outputDir的静态资源(js、css、img、fonts)目录 + lintOnSave: !IS_PROD, + productionSourceMap: false, // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。 + devServer: { + port: 9020, // 端口号 + open: false, // 启动后打开浏览器 + overlay: { + // 当出现编译器错误或警告时,在浏览器中显示全屏覆盖层 + warnings: false, + errors: true + } + // ... + } +} +``` + +[▲ 回顶部](#top) + +### ✅ 配置 alias 别名 + +```javascript +const path = require('path') +const resolve = dir => path.join(__dirname, dir) +const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV) + +module.exports = { + chainWebpack: config => { + // 添加别名 + config.resolve.alias + .set('@', resolve('src')) + .set('assets', resolve('src/assets')) + .set('api', resolve('src/api')) + .set('views', resolve('src/views')) + .set('components', resolve('src/components')) + } +} +``` + +[▲ 回顶部](#top) + +### ✅ 配置 proxy 跨域 + +如果你的项目需要跨域设置,你需要打来 `vue.config.js` `proxy` 注释 并且配置相应参数 + +**!!!注意:你还需要将 `src/config/env.development.js` 里的 `baseApi` 设置成 '/'** + +```javascript +module.exports = { + devServer: { + // .... + proxy: { + //配置跨域 + '/api': { + target: 'https://test.xxx.com', // 接口的域名 + // ws: true, // 是否启用websockets + changOrigin: true, // 开启代理,在本地创建一个虚拟服务端 + pathRewrite: { + '^/api': '/' + } + } + } + } +} +``` + +使用 例如: `src/api/home.js` + +```javascript +export function getUserInfo(params) { + return request({ + url: '/api/userinfo', + method: 'post', + data: qs.stringify(params) + }) +} +``` + +[▲ 回顶部](#top) + +### ✅ 配置 打包分析 + +```javascript +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin + +module.exports = { + chainWebpack: config => { + // 打包分析 + if (IS_PROD) { + config.plugin('webpack-report').use(BundleAnalyzerPlugin, [ + { + analyzerMode: 'static' + } + ]) + } + } +} +``` + +```bash +npm run build +``` + +[▲ 回顶部](#top) + +### ✅ 配置 externals 引入 cdn 资源 + +这个版本 CDN 不再引入,我测试了一下使用引入 CDN 和不使用,不使用会比使用时间少。网上不少文章测试 CDN 速度块,这个开发者可 +以实际测试一下。 + +另外项目中使用的是公共 CDN 不稳定,域名解析也是需要时间的(如果你要使用请尽量使用同一个域名) + +因为页面每次遇到` + <% } %> +``` + +[▲ 回顶部](#top) + +### ✅ 去掉 console.log + +保留了测试环境和本地环境的 `console.log` + +```bash +npm i -D babel-plugin-transform-remove-console +``` + +在 babel.config.js 中配置 + +```javascript +// 获取 VUE_APP_ENV 非 NODE_ENV,测试环境依然 console +const IS_PROD = ['production', 'prod'].includes(process.env.VUE_APP_ENV) +const plugins = [ + [ + 'import', + { + libraryName: 'vant', + libraryDirectory: 'es', + style: true + }, + 'vant' + ] +] +// 去除 console.log +if (IS_PROD) { + plugins.push('transform-remove-console') +} + +module.exports = { + presets: [['@vue/cli-plugin-babel/preset', { useBuiltIns: 'entry' }]], + plugins +} +``` + +[▲ 回顶部](#top) + +### ✅ splitChunks 单独打包第三方模块 + +```javascript +module.exports = { + chainWebpack: config => { + config.when(IS_PROD, config => { + config + .plugin('ScriptExtHtmlWebpackPlugin') + .after('html') + .use('script-ext-html-webpack-plugin', [ + { + // 将 runtime 作为内联引入不单独存在 + inline: /runtime\..*\.js$/ + } + ]) + .end() + config.optimization.splitChunks({ + chunks: 'all', + cacheGroups: { + // cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块 + commons: { + name: 'chunk-commons', + test: resolve('src/components'), + minChunks: 3, // 被至少用三次以上打包分离 + priority: 5, // 优先级 + reuseExistingChunk: true // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。 + }, + node_vendors: { + name: 'chunk-libs', + chunks: 'initial', // 只打包初始时依赖的第三方 + test: /[\\/]node_modules[\\/]/, + priority: 10 + }, + vantUI: { + name: 'chunk-vantUI', // 单独将 vantUI 拆包 + priority: 20, // 数字大权重到,满足多个 cacheGroups 的条件时候分到权重高的 + test: /[\\/]node_modules[\\/]_?vant(.*)/ + } + } + }) + config.optimization.runtimeChunk('single') + }) + } +} +``` + +[▲ 回顶部](#top) + +### ✅ 添加 IE 兼容 + +之前的方式 会报 `@babel/polyfill` is deprecated. Please, use required parts of `core-js` and +`regenerator-runtime/runtime` separately + +`@babel/polyfill` 废弃,使用 `core-js` 和 `regenerator-runtime` + +```bash +npm i --save core-js regenerator-runtime +``` + +在 `main.js` 中添加 + +```javascript +// 兼容 IE +// https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md#babelpolyfill +import 'core-js/stable' +import 'regenerator-runtime/runtime' +``` + +配置 `babel.config.js` + +```javascript +const plugins = [] + +module.exports = { + presets: [['@vue/cli-plugin-babel/preset', { useBuiltIns: 'usage', corejs: 3 }]], + plugins +} +``` + +[▲ 回顶部](#top) + +### ✅ Eslint + Pettier 统一开发规范 + +VScode 安装 `eslint` `prettier` `vetur` 插件 + +在文件 `.prettierrc` 里写 属于你的 pettier 规则 + +```bash +{ + "printWidth": 120, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "none", + "semi": false, + "wrap_line_length": 120, + "wrap_attributes": "auto", + "proseWrap": "always", + "arrowParens": "avoid", + "bracketSpacing": false, + "jsxBracketSameLine": true, + "useTabs": false, + "overrides": [{ + "files": ".prettierrc", + "options": { + "parser": "json" + } + }] +} +``` + +Vscode setting.json 设置 + +```bash + "[vue]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + // 保存时用eslint格式化 + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + // 两者会在格式化js时冲突,所以需要关闭默认js格式化程序 + "javascript.format.enable": false, + "typescript.format.enable": false, + "vetur.format.defaultFormatter.html": "none", + // js/ts程序用eslint,防止vetur中的prettier与eslint格式化冲突 + "vetur.format.defaultFormatter.js": "none", + "vetur.format.defaultFormatter.ts": "none", +``` + +[▲ 回顶部](#top) + +### ✅ vconsole 移动端调试 +参考地址:https://github.com/AlloyTeam/AlloyLever +参考地址:https://www.cnblogs.com/liyinSakura/p/9883777.html +```js +import Vconsole from 'vconsole' +const vConsole = new Vconsole() +export default vConsole +``` +* app.vue中设置暗门,点击几次显示vconsole + * 在app.vue中通过limit进行设置 + * 开发测试环境点击一次就可显示 + * 生产环境点击10次 +

+ +

+ +[▲ 回顶部](#top) + +### ✅ 动态设置title +参考地址:https://github.com/deboyblog/vue-wechat-title +参考地址:https://www.cnblogs.com/guiyishanren/p/10666127.html +```js +// main.js +// 引入插件 +Vue.use(require('vue-wechat-title')) +``` +使用 +```js +// app.vue + +``` + +[▲ 回顶部](#top) + +### ✅ 配置Jssdk +安装: +```js +yarn add weixin-js-sdk +``` +引用: +```js +// util +wechatPlugin.js // jssdk插件配置 +jsApiList.js // 微信JS接口列表 +// main.js +// 全局注册微信js-sdk +import WechatPlugin from '@/utils/wechatPlugin' +Vue.use(WechatPlugin) +``` +调用: +```js +created() { + console.log(this.$wx) +}, +``` +[▲ 回顶部](#top) + +### ✅ 本地存储storage封装 +安装: +```js +storage.js +``` +引用: +```js +// 引入本地存储 +import { storage, sessionStorage } from '@/utils/storage' +Vue.prototype.$storage = storage +Vue.prototype.$sessionStorage = sessionStorage +``` +调用: +```js +created() { + this.$storage.set('key','value') + this.$storage.get('key') + this.$sessionStorage.set('key','value') + this.$sessionStorage.get('key') +}, +``` +[▲ 回顶部](#top) + +# 鸣谢 ​ + +[vue-cli4-config](https://github.com/staven630/vue-cli4-config) +[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) + +Path` 要做对应的**修改** + +前往:[vue.config.js 基础配置](#base) + +```javascript +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) +export const router = [ + { + path: '/', + name: 'index', + component: () => import('@/views/home/index'), // 路由懒加载 + meta: { + title: '首页', // 页面标题 + keepAlive: false // keep-alive 标识 + } + } +] +const createRouter = () => + new Router({ + // mode: 'history', // 如果你是 history模式 需要配置 vue.config.js publicPath + // base: '/app/', + scrollBehavior: () => ({ y: 0 }), + routes: router + }) + +export default createRouter() +``` + +更多:[Vue Router](https://router.vuejs.org/zh/) + +[▲ 回顶部](#top) + +### ✅ Axios 封装及接口管理 + +`utils/request.js` 封装 axios ,开发者需要根据后台接口做修改。 + +- `service.interceptors.request.use` 里可以设置请求头,比如设置 `token` +- `config.hideloading` 是在 api 文件夹下的接口参数里设置,下文会讲 +- `service.interceptors.response.use` 里可以对接口返回数据处理,比如 401 删除本地信息,重新登录 + +```javascript +import axios from 'axios' +import store from '@/store' +import { Toast } from 'vant' +// 根据环境不同引入不同api地址 +import { baseApi } from '@/config' +// create an axios instance +const service = axios.create({ + baseURL: baseApi, // url = base api url + request url + withCredentials: true, // send cookies when cross-domain requests + timeout: 5000 // request timeout +}) + +// request 拦截器 request interceptor +service.interceptors.request.use( + config => { + // 不传递默认开启loading + if (!config.hideloading) { + // loading + Toast.loading({ + forbidClick: true + }) + } + if (store.getters.token) { + config.headers['X-Token'] = '' + } + return config + }, + error => { + // do something with request error + console.log(error) // for debug + return Promise.reject(error) + } +) +// respone拦截器 +service.interceptors.response.use( + response => { + Toast.clear() + const res = response.data + if (res.status && res.status !== 200) { + // 登录超时,重新登录 + if (res.status === 401) { + store.dispatch('FedLogOut').then(() => { + location.reload() + }) + } + return Promise.reject(res || 'error') + } else { + return Promise.resolve(res) + } + }, + error => { + Toast.clear() + console.log('err' + error) // for debug + return Promise.reject(error) + } +) +export default service +``` + +#### 接口管理 + +在`src/api` 文件夹下统一管理接口 + +- 你可以建立多个模块对接接口, 比如 `home.js` 里是首页的接口这里讲解 `user.js` +- `url` 接口地址,请求的时候会拼接上 `config` 下的 `baseApi` +- `method` 请求方法 +- `data` 请求参数 `qs.stringify(params)` 是对数据系列化操作 +- `hideloading` 默认 `false`,设置为 `true` 后,不显示 loading ui 交互中有些接口不需要让用户感知 + +```javascript +import qs from 'qs' +// axios +import request from '@/utils/request' +//user api + +// 用户信息 +export function getUserInfo(params) { + return request({ + url: '/user/userinfo diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..0355640 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,22 @@ +// 获取 VUE_APP_ENV 非 NODE_ENV,测试环境依然 console +const IS_PROD = ['production', 'prod'].includes(process.env.VUE_APP_ENV) +const plugins = [ + [ + 'import', + { + libraryName: 'vant', + libraryDirectory: 'es', + style: true + }, + 'vant' + ] +] +// 去除 console.log +if (IS_PROD) { + plugins.push('transform-remove-console') +} + +module.exports = { + presets: [['@vue/cli-plugin-babel/preset', {useBuiltIns: 'usage', corejs: 3}]], + plugins +} diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..5097104 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "allowSyntheticDefaultImports": true, + "baseUrl": "./", + "paths": { + // 路径匹配 + "@/*": ["src/*"] + } + }, + "exclude": [ + // 排除某些文件 + "node_modules" + ], + "include": ["./src/**/*"] //自动引入Vue组件和普通Js模块 +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..64cbe56 --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "epmet-work-h5", + "version": "1.0.0", + "description": "", + "author": "", + "private": true, + "scripts": { + "serve": "vue-cli-service serve --host 0.0.0.0", + "dev": "vue-cli-service serve", + "build": "vue-cli-service build", + "build:dev": "vue-cli-service build --mode development", + "build:prod": "vue-cli-service build --mode production", + "build:stage": "vue-cli-service build --mode staging", + "lint": "vue-cli-service lint", + "deps": "yarn upgrade-interactive --latest" + }, + "dependencies": { + "@vue-office/docx": "^1.0.0", + "@vue-office/excel": "^1.0.0", + "@vue-office/pdf": "^1.0.0", + "amfe-flexible": "^2.2.1", + "axios": "^0.27.2", + "core-js": "^3.23.3", + "dayjs": "^1.11.7", + "eslint": "^8.12.0", + "eslint-plugin-vue": "^8.4.0", + "filemanager-webpack-plugin": "^8.0.0", + "html-webpack-plugin": "^5.5.3", + "less-loader": "^11.1.0", + "postcss-pxtorem": "^6.0.0", + "regenerator-runtime": "^0.13.5", + "vant": "^2.12.48", + "vconsole": "^3.15.0", + "vue": "^2.7.8", + "vue-demi": "^0.14.0", + "vue-router": "^3.5.4", + "vuex": "^3.6.2" + }, + "devDependencies": { + "@babel/core": "^7.18.10", + "@babel/eslint-parser": "^7.18.2", + "@vue/cli-plugin-babel": "~5.0.8", + "@vue/cli-plugin-eslint": "~5.0.8", + "@vue/cli-plugin-router": "~5.0.8", + "@vue/cli-plugin-vuex": "~5.0.8", + "@vue/cli-service": "~5.0.8", + "ajv": "^7.2.4", + "babel-eslint": "^10.1.0", + "babel-plugin-import": "^1.13.5", + "babel-plugin-transform-remove-console": "^6.9.4", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.0.0", + "less": "^4.1.2", + "postcss": "^8.4.12", + "prettier": "2.6.2", + "script-ext-html-webpack-plugin": "2.1.4", + "svg-sprite-loader": "5.1.1", + "webpack-bundle-analyzer": "^4.5.0" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..29f8606 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f34d51c --- /dev/null +++ b/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + + <%= webpackConfig.name %> + + + +
+ + + + + diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..2d238e1 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/api/home.js b/src/api/home.js new file mode 100644 index 0000000..4239f3b --- /dev/null +++ b/src/api/home.js @@ -0,0 +1,6 @@ +// import qs from 'qs' +// axios +// import request from '@/utils/request' +//home api + + \ No newline at end of file diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 0000000..6147745 --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,7 @@ +const api = { + Login: '/user/login', + UserInfo: '/user/userinfo', + UserName: '/user/name' +} + +export default api diff --git a/src/api/user.js b/src/api/user.js new file mode 100644 index 0000000..7a0ebff --- /dev/null +++ b/src/api/user.js @@ -0,0 +1,32 @@ +import api from './index' +// axios +import request from '@/utils/request' + +// 登录 +export function login(data) { + return request({ + url: api.Login, + method: 'post', + data + }) +} + +// 用户信息 post 方法 +export function getUserInfo(data) { + return request({ + url: api.UserInfo, + method: 'post', + data, + hideloading: true + }) +} + +// 用户名称 get 方法 +export function getUserName(params) { + return request({ + url: api.UserName, + method: 'get', + params, + hideloading: true + }) +} diff --git a/src/assets/css/common.less b/src/assets/css/common.less new file mode 100644 index 0000000..92853f5 --- /dev/null +++ b/src/assets/css/common.less @@ -0,0 +1,70 @@ + +html, +body #app { + color: @black; + background-color: @background-color; +} + +#app { + font-size: 16px; + background-size: 100% auto; +} + +.flex { + display: flex; +} + +.mr10 { + margin-right: 10px; +} + +.primary-color { + color: @blue; +} + +.jcsb { + justify-content: space-between; +} + +.aic { + align-items: center; +} +.container { + padding: 10px; +} + + +.card{ + background-color: #fff; + border-radius: 2px; + box-shadow: 0 1px 3px rgba(0,0,0,.3); + box-sizing: border-box; + width: 100%; +} +.flex{ + display: flex; + .flex1{ + flex: 1; + } + &-y{ + flex-direction: column; + } + &-center1{ + justify-content: center; + } + &-center2{ + align-items: center; + } + &-mean{ + justify-content: space-around; + } + &-end{ + justify-content: space-between; + } +} +.m-top12{ + margin-top: 12px; +} + + + diff --git a/src/assets/css/index.less b/src/assets/css/index.less new file mode 100644 index 0000000..6572567 --- /dev/null +++ b/src/assets/css/index.less @@ -0,0 +1,35 @@ + +html, +body #app { + color: @black; + background-color: @background-color; +} + +#app { + font-size: 16px; + background-size: 100% auto; +} + +.flex { + display: flex; +} + +.mr10 { + margin-right: 10px; +} + +.primary-color { + color: @blue; +} + +.jcsb { + justify-content: space-between; +} + +.aic { + align-items: center; +} +.container { + padding: 10px; +} + diff --git a/src/assets/css/mixin.scss b/src/assets/css/mixin.scss new file mode 100644 index 0000000..327b6d4 --- /dev/null +++ b/src/assets/css/mixin.scss @@ -0,0 +1,36 @@ +// mixin +// 清除浮动 +@mixin clearfix { + &:after { + content: ""; + display: table; + clear: both; + } +} + +// 多行隐藏 +@mixin textoverflow($clamp:1) { + display: block; + overflow: hidden; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: $clamp; + /*! autoprefixer: ignore next */ + -webkit-box-orient: vertical; +} + +//flex box +@mixin flexbox($jc:space-between, $ai:center, $fd:row, $fw:nowrap) { + display: flex; + display: -webkit-flex; + flex: 1; + justify-content: $jc; + -webkit-justify-content: $jc; + align-items: $ai; + -webkit-align-items: $ai; + flex-direction: $fd; + -webkit-flex-direction: $fd; + flex-wrap: $fw; + -webkit-flex-wrap: $fw; +} diff --git a/src/assets/css/vant-theme.less b/src/assets/css/vant-theme.less new file mode 100644 index 0000000..25374ca --- /dev/null +++ b/src/assets/css/vant-theme.less @@ -0,0 +1,688 @@ +// Color Palette +@black: #000; +@white: #fff; +@gray-1: #f7f8fa; +@gray-2: #f2f3f5; +@gray-3: #ebedf0; +@gray-4: #dcdee0; +@gray-5: #c8c9cc; +@gray-6: #969799; +@gray-7: #646566; +@gray-8: #323233; +@red: #ee0a24; +@blue: #1989fa; +@orange: #ff976a; +@orange-dark: #ed6a0c; +@orange-light: #fffbe8; +@green: #07c160; + +// Gradient Colors +@gradient-red: linear-gradient(to right, #ff6034, #ee0a24); +@gradient-orange: linear-gradient(to right, #ffd01e, #ff8917); + +// Component Colors +@text-color: @gray-8; +@active-color: @gray-2; +@active-opacity: 0.7; +@disabled-opacity: 0.5; +@background-color: @gray-1; +@background-color-light: #fafafa; +@text-link-color: #576b95; + +// Padding +@padding-base: 4px; +@padding-xs: @padding-base * 2; +@padding-sm: @padding-base * 3; +@padding-md: @padding-base * 4; +@padding-lg: @padding-base * 6; +@padding-xl: @padding-base * 8; + +// Font +@font-size-xs: 10px; +@font-size-sm: 12px; +@font-size-md: 14px; +@font-size-lg: 16px; +@font-weight-bold: 500; +@line-height-xs: 14px; +@line-height-sm: 18px; +@line-height-md: 20px; +@line-height-lg: 22px; +@base-font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', + Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', + 'Microsoft Yahei', sans-serif; +@price-integer-font-family: Avenir-Heavy, PingFang SC, Helvetica Neue, Arial, + sans-serif; + +// Animation +@animation-duration-base: 0.3s; +@animation-duration-fast: 0.2s; + +// Border +@border-color: @gray-3; +@border-width-base: 1px; +@border-radius-sm: 2px; +@border-radius-md: 4px; +@border-radius-lg: 8px; +@border-radius-max: 999px; + +//ActionSheet +@action-sheet-max-height: 90%; +@action-sheet-header-height: 48px; +@action-sheet-header-font-size: @font-size-lg; +@action-sheet-description-color: @gray-6; +@action-sheet-description-font-size: @font-size-md; +@action-sheet-description-line-height: 20px; +@action-sheet-item-background: @white; +@action-sheet-item-font-size: @font-size-lg; +@action-sheet-item-line-height: 22px; +@action-sheet-item-text-color: @text-color; +@action-sheet-item-disabled-text-color: @gray-5; +@action-sheet-subname-color: @gray-6; +@action-sheet-subname-font-size: @font-size-sm; +@action-sheet-subname-line-height: 20px; +@action-sheet-close-icon-size: 22px; +@action-sheet-close-icon-color: @gray-5; +@action-sheet-close-icon-padding: 0 @padding-md; +@action-sheet-cancel-text-color: @gray-7; +@action-sheet-cancel-padding-top: @padding-xs; +@action-sheet-cancel-padding-color: @background-color; + +// Button +@button-mini-height: 22px; +@button-mini-min-width: 50px; +@button-mini-font-size: @font-size-xs; +@button-small-height: 30px; +@button-small-font-size: @font-size-sm; +@button-small-min-width: 60px; +@button-normal-font-size: @font-size-md; +@button-large-height: 50px; +@button-default-color: @text-color; +@button-default-height: 44px; +@button-default-font-size: @font-size-lg; +@button-default-background-color: @white; +@button-default-border-color: @border-color; +@button-primary-color: @white; +@button-primary-background-color: @blue; +@button-primary-border-color: @blue; +@button-info-color: @white; +@button-info-background-color: @blue; +@button-info-border-color: @blue; +@button-danger-color: @white; +@button-danger-background-color: @red; +@button-danger-border-color: @red; +@button-warning-color: @white; +@button-warning-background-color: @orange; +@button-warning-border-color: @orange; +@button-line-height: 20px; +@button-border-width: 1px; +@button-border-radius: @border-radius-sm; +@button-round-border-radius: @border-radius-max; +@button-plain-background-color: @white; +@button-disabled-opacity: @disabled-opacity; + +// Calendar +@calendar-height: 100%; +@calendar-background-color: @white; +@calendar-popup-height: 90%; +@calendar-header-box-shadow: 0 2px 10px rgba(125, 126, 128, 0.16); +@calendar-header-title-height: 44px; +@calendar-header-title-font-size: @font-size-lg; +@calendar-header-subtitle-font-size: @font-size-md; +@calendar-weekdays-height: 30px; +@calendar-weekdays-font-size: @font-size-sm; +@calendar-month-title-font-size: @font-size-md; +@calendar-month-mark-color: fade(@gray-2, 80%); +@calendar-month-mark-font-size: 160px; +@calendar-day-height: 64px; +@calendar-day-font-size: @font-size-lg; +@calendar-range-edge-color: @white; +@calendar-range-edge-background-color: @red; +@calendar-range-middle-color: @red; +@calendar-range-middle-background-opacity: 0.1; +@calendar-selected-day-size: 54px; +@calendar-selected-day-color: @white; +@calendar-info-font-size: @font-size-xs; +@calendar-info-line-height: 14px; +@calendar-selected-day-background-color: @red; +@calendar-day-disabled-color: @gray-5; +@calendar-confirm-button-height: 36px; +@calendar-confirm-button-margin: 7px 0; +@calendar-confirm-button-line-height: 34px; + +// Card +@card-padding: @padding-xs @padding-md; +@card-font-size: @font-size-sm; +@card-text-color: @text-color; +@card-background-color: @background-color-light; +@card-thumb-size: 88px; +@card-title-line-height: 16px; +@card-desc-color: @gray-7; +@card-desc-line-height: 20px; +@card-price-color: @red; +@card-origin-price-color: @gray-7; +@card-origin-price-font-size: @font-size-xs; +@card-price-font-size: @font-size-sm; +@card-price-integer-font-size: @font-size-lg; +@card-price-font-family: @price-integer-font-family; + +// Cell +@cell-font-size: @font-size-md; +@cell-line-height: 24px; +@cell-vertical-padding: 10px; +@cell-horizontal-padding: @padding-md; +@cell-text-color: @text-color; +@cell-background-color: @white; +@cell-border-color: @border-color; +@cell-active-color: @active-color; +@cell-required-color: @red; +@cell-label-color: @gray-6; +@cell-label-font-size: @font-size-sm; +@cell-label-line-height: 18px; +@cell-label-margin-top: 3px; +@cell-value-color: @gray-6; +@cell-icon-size: 16px; +@cell-right-icon-color: @gray-6; +@cell-large-vertical-padding: @padding-sm; +@cell-large-title-font-size: @font-size-lg; +@cell-large-value-font-size: @font-size-lg; +@cell-large-label-font-size: @font-size-md; + +// CellGroup +@cell-group-background-color: @white; +@cell-group-title-color: @gray-6; +@cell-group-title-padding: @padding-md @padding-md @padding-xs; +@cell-group-title-font-size: @font-size-md; +@cell-group-title-line-height: 16px; +@cell-group-inset-padding: 0 @padding-md; +@cell-group-inset-border-radius: @border-radius-lg; +@cell-group-inset-title-padding: @padding-md @padding-md @padding-xs @padding-xl; + +// Checkbox +@checkbox-size: 20px; +@checkbox-border-color: @gray-5; +@checkbox-transition-duration: 0.2s; +@checkbox-label-margin: 10px; +@checkbox-label-color: @text-color; +@checkbox-checked-icon-color: @blue; +@checkbox-disabled-icon-color: @gray-5; +@checkbox-disabled-label-color: @gray-5; +@checkbox-disabled-background-color: @border-color; + +// Circle +@circle-text-color: @text-color; + +// Collapse +@collapse-item-transition-duration: 0.3s; +@collapse-item-content-padding: 15px; +@collapse-item-content-font-size: 13px; +@collapse-item-content-line-height: 1.5; +@collapse-item-content-text-color: @gray-6; +@collapse-item-content-background-color: @white; +@collapse-item-title-disabled-color: @gray-5; + +// CountDown +@count-down-text-color: @text-color; +@count-down-font-size: @font-size-md; +@count-down-line-height: 20px; + +// Dialog +@dialog-width: 320px; +@dialog-small-screen-width: 90%; +@dialog-font-size: @font-size-lg; +@dialog-border-radius: 16px; +@dialog-background-color: @white; +@dialog-header-font-weight: @font-weight-bold; +@dialog-header-line-height: 24px; +@dialog-header-padding-top: @padding-lg; +@dialog-header-isolated-padding: @padding-lg 0; +@dialog-message-padding: @padding-lg; +@dialog-message-font-size: @font-size-md; +@dialog-message-line-height: 20px; +@dialog-message-max-height: 60vh; +@dialog-has-title-message-text-color: @gray-7; +@dialog-has-title-message-padding-top: @padding-xs; + +// Field +@field-label-color: @gray-7; +@field-input-text-color: @text-color; +@field-input-error-text-color: @red; +@field-input-disabled-text-color: @gray-5; +@field-placeholder-text-color: @gray-5; +@field-icon-size: 16px; +@field-clear-icon-size: 16px; +@field-clear-icon-color: @gray-5; +@field-icon-container-color: @gray-6; +@field-error-message-color: @red; +@field-error-message-text-font-size: @font-size-sm; +@field-text-area-min-height: 18px; +@field-word-limit-color: @gray-7; +@field-word-limit-font-size: @font-size-sm; +@field-word-limit-line-height: 16px; +@field-word-num-full-color: @red; +@field-disabled-text-color: @gray-5; + +// GoodsAction +@goods-action-background-color: @white; +@goods-action-height: 50px; +@goods-action-icon-width: 48px; +@goods-action-icon-height: @goods-action-height; +@goods-action-icon-color: @text-color; +@goods-action-icon-size: 18px; +@goods-action-icon-font-size: @font-size-xs; +@goods-action-icon-text-color: @gray-7; +@goods-action-button-height: 40px; +@goods-action-button-line-height: @button-line-height; +@goods-action-button-border-radius: @border-radius-max; +@goods-action-button-warning-color: @gradient-orange; +@goods-action-button-danger-color: @gradient-red; +@goods-action-button-plain-color: @white; + +// Image +@image-placeholder-text-color: @gray-6; +@image-placeholder-font-size: @font-size-md; +@image-placeholder-background-color: @background-color; +@image-loading-icon-size: 32px; +@image-loading-icon-color: @gray-4; +@image-error-icon-size: 32px; +@image-error-icon-color: @gray-4; + +// Info +@info-size: 16px; +@info-color: @white; +@info-padding: 0 3px; +@info-font-size: 12px; +@info-font-weight: 500; +@info-border-width: 1px; +@info-background-color: @red; +@info-dot-color: @red; +@info-dot-size: 8px; +@info-font-family: -apple-system-font, Helvetica Neue, Arial, sans-serif; + +// Loading +@loading-text-color: @gray-6; +@loading-text-font-size: @font-size-md; +@loading-text-line-height: 20px; +@loading-spinner-color: @gray-5; +@loading-spinner-size: 30px; +@loading-spinner-animation-duration: 0.8s; + +// NavBar +@nav-bar-height: 46px; +@nav-bar-background-color: @white; +@nav-bar-arrow-size: 16px; +@nav-bar-icon-color: @blue; +@nav-bar-text-color: @blue; +@nav-bar-title-font-size: @font-size-lg; +@nav-bar-title-text-color: @text-color; + +// NoticeBar +@notice-bar-height: 40px; +@notice-bar-padding: 0 @padding-md; +@notice-bar-wrapable-padding: @padding-xs @padding-md; +@notice-bar-font-size: @font-size-md; +@notice-bar-text-color: @orange-dark; +@notice-bar-line-height: 24px; +@notice-bar-background-color: @orange-light; +@notice-bar-icon-size: 16px; +@notice-bar-icon-min-width: 22px; + +// Notify +@notify-padding: 6px 15px; +@notify-font-size: 14px; +@notify-line-height: 20px; +@notify-primary-background-color: @blue; +@notify-success-background-color: @blue; +@notify-danger-background-color: @red; +@notify-warning-background-color: @orange; + +// Overlay +@overlay-background-color: rgba(0, 0, 0, 0.7); + +// Panel +@panel-background-color: @white; +@panel-header-value-color: @red; +@panel-footer-padding: @padding-xs @padding-md; + +// Picker +@picker-background-color: @white; +@picker-toolbar-height: 44px; +@picker-title-font-size: @font-size-lg; +@picker-action-padding: 0 @padding-md; +@picker-action-font-size: @font-size-md; +@picker-confirm-action-color: @text-link-color; +@picker-cancel-action-color: @gray-6; +@picker-option-font-size: @font-size-lg; +@picker-option-text-color: @black; +@picker-loading-icon-color: @blue; +@picker-loading-mask-color: rgba(255, 255, 255, 0.9); +@picker-option-disabled-opacity: 0.3; +@picker-option-selected-text-color: @text-color; + +// Popup +@popup-background-color: @white; +@popup-round-border-radius: 16px; +@popup-close-icon-size: 18px; +@popup-close-icon-color: @gray-6; +@popup-close-icon-margin: 16px; +@popup-close-icon-z-index: 1; + +// Progress +@progress-height: 4px; +@progress-background-color: @gray-3; +@progress-pivot-padding: 0 5px; +@progress-color: @blue; +@progress-pivot-font-size: @font-size-xs; +@progress-pivot-line-height: 1.6; +@progress-pivot-background-color: @blue; +@progress-pivot-text-color: @white; + +// Radio +@radio-size: 20px; +@radio-border-color: @gray-5; +@radio-transition-duration: 0.2s; +@radio-label-margin: 10px; +@radio-label-color: @text-color; +@radio-checked-icon-color: @blue; +@radio-disabled-icon-color: @gray-5; +@radio-disabled-label-color: @gray-5; +@radio-disabled-background-color: @border-color; + +// Rate +@rate-horizontal-padding: 2px; +@rate-icon-size: 20px; +@rate-icon-void-color: @gray-5; +@rate-icon-full-color: @red; +@rate-icon-disabled-color: @gray-5; +@rate-icon-gutter: @padding-base; + +// Switch +@switch-width: 2em; +@switch-height: 1em; +@switch-node-size: 1em; +@switch-node-z-index: 1; +@switch-node-background-color: @white; +@switch-node-box-shadow: 0 3px 1px 0 rgba(0, 0, 0, 0.05), + 0 2px 2px 0 rgba(0, 0, 0, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.05); +@switch-background-color: @white; +@switch-on-background-color: @blue; +@switch-transition-duration: 0.3s; +@switch-disabled-opacity: 0.4; +@switch-border: 1px solid rgba(0, 0, 0, 0.1); + +// ShareSheet +@share-sheet-header-padding: @padding-sm @padding-md @padding-base; +@share-sheet-title-color: @text-color; +@share-sheet-title-font-size: @font-size-md; +@share-sheet-title-line-height: @line-height-md; +@share-sheet-description-color: @gray-6; +@share-sheet-description-font-size: @font-size-sm; +@share-sheet-description-line-height: 16px; +@share-sheet-icon-size: 48px; +@share-sheet-option-name-color: @gray-7; +@share-sheet-option-name-font-size: @font-size-sm; +@share-sheet-option-description-color: @gray-5; +@share-sheet-option-description-font-size: @font-size-sm; +@share-sheet-cancel-button-font-size: @font-size-lg; +@share-sheet-cancel-button-height: 48px; +@share-sheet-cancel-button-background: @white; + +// Search +@search-background-color: @gray-1; +@search-padding: 10px @padding-sm; +@search-input-height: 34px; +@search-label-padding: 0 5px; +@search-label-color: @text-color; +@search-label-font-size: @font-size-md; +@search-left-icon-color: @gray-6; +@search-action-padding: 0 @padding-xs; +@search-action-text-color: @text-color; +@search-action-font-size: @font-size-md; + +// Sidebar +@sidebar-width: 80px; + +// SidebarItem +@sidebar-font-size: @font-size-md; +@sidebar-line-height: 20px; +@sidebar-text-color: @text-color; +@sidebar-disabled-text-color: @gray-5; +@sidebar-padding: 20px @padding-sm 20px @padding-xs; +@sidebar-active-color: @active-color; +@sidebar-background-color: @background-color; +@sidebar-selected-font-weight: @font-weight-bold; +@sidebar-selected-text-color: @text-color; +@sidebar-selected-border-color: @red; +@sidebar-selected-background-color: @white; + +// Slider +@slider-active-background-color: @blue; +@slider-inactive-background-color: @gray-3; +@slider-disabled-opacity: @disabled-opacity; +@slider-bar-height: 2px; +@slider-button-width: 24px; +@slider-button-height: 24px; +@slider-button-border-radius: 50%; +@slider-button-background-color: @white; +@slider-button-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); + +// Step +@step-text-color: @gray-6; +@step-process-text-color: @text-color; +@step-font-size: @font-size-md; +@step-line-color: @border-color; +@step-finish-line-color: @blue; +@step-finish-text-color: @text-color; +@step-icon-size: 12px; +@step-circle-size: 5px; +@step-circle-color: @gray-6; +@step-horizontal-title-font-size: @font-size-sm; + +// Steps +@steps-background-color: @white; + +// Stepper +@stepper-active-color: #e8e8e8; +@stepper-background-color: @active-color; +@stepper-button-icon-color: @text-color; +@stepper-button-disabled-color: @background-color; +@stepper-button-disabled-icon-color: @gray-5; +@stepper-button-round-theme-color: @red; +@stepper-input-width: 32px; +@stepper-input-height: 28px; +@stepper-input-font-size: @font-size-md; +@stepper-input-line-height: normal; +@stepper-input-text-color: @text-color; +@stepper-input-disabled-text-color: @gray-5; +@stepper-input-disabled-background-color: @active-color; +@stepper-border-radius: @border-radius-md; + +// SubmitBar +@submit-bar-height: 50px; +@submit-bar-z-index: 100; +@submit-bar-background-color: @white; +@submit-bar-button-width: 110px; +@submit-bar-price-color: @red; +@submit-bar-price-font-size: @font-size-sm; +@submit-bar-currency-font-size: @font-size-sm; +@submit-bar-text-color: @text-color; +@submit-bar-text-font-size: 14px; +@submit-bar-tip-padding: 10px; +@submit-bar-tip-font-size: 12px; +@submit-bar-tip-line-height: 1.5; +@submit-bar-tip-color: #f56723; +@submit-bar-tip-background-color: #fff7cc; +@submit-bar-tip-icon-size: 12px; +@submit-bar-button-height: 40px; +@submit-bar-padding: 0 @padding-md; +@submit-bar-price-integer-font-size: 20px; +@submit-bar-price-font-family: @price-integer-font-family; + +// Tabbar +@tabbar-height: 50px; +@tabbar-background-color: @white; + +// TabbarItem +@tabbar-item-font-size: @font-size-sm; +@tabbar-item-text-color: @gray-7; +@tabbar-item-active-color: @blue; +@tabbar-item-line-height: 1; +@tabbar-item-icon-size: 22px; +@tabbar-item-margin-bottom: 4px; + +// Tab +@tab-text-color: @gray-7; +@tab-active-text-color: @text-color; +@tab-disabled-text-color: @gray-5; +@tab-font-size: @font-size-md; + +// Tabs +@tabs-default-color: @red; +@tabs-line-height: 44px; +@tabs-card-height: 30px; +@tabs-nav-background-color: @white; +@tabs-bottom-bar-height: 3px; +@tabs-bottom-bar-color: @tabs-default-color; + +// Tag +@tag-padding: 0 @padding-base; +@tag-text-color: @white; +@tag-font-size: @font-size-sm; +@tag-border-radius: 2px; +@tag-line-height: 16px; +@tag-medium-padding: 2px 6px; +@tag-large-padding: @padding-base @padding-xs; +@tag-large-border-radius: @border-radius-md; +@tag-large-font-size: @font-size-md; +@tag-round-border-radius: @border-radius-max; +@tag-danger-color: @red; +@tag-primary-color: @blue; +@tag-success-color: @blue; +@tag-warning-color: @orange; +@tag-default-color: @gray-6; +@tag-plain-background-color: @white; + +// Toast +@toast-max-width: 70%; +@toast-font-size: 14px; +@toast-text-color: @white; +@toast-line-height: 20px; +@toast-border-radius: @border-radius-lg; +@toast-background-color: fade(@black, 70%); +@toast-icon-size: 36px; +@toast-text-min-width: 96px; +@toast-text-padding: @padding-xs @padding-sm; +@toast-default-padding: @padding-md; +@toast-default-width: 88px; +@toast-default-min-height: 88px; + +// GridItem +@grid-item-content-padding: @padding-md @padding-xs; +@grid-item-content-background-color: @white; +@grid-item-content-active-color: @active-color; +@grid-item-icon-size: 26px; +@grid-item-text-color: @gray-7; +@grid-item-text-font-size: @font-size-sm; + +// Divider +@divider-margin: @padding-md 0; +@divider-text-color: @gray-6; +@divider-font-size: @font-size-md; +@divider-line-height: 24px; +@divider-border-color: @border-color; +@divider-content-padding: @padding-md; +@divider-content-left-width: 10%; +@divider-content-right-width: 10%; + +// Empty +@empty-padding: @padding-xl 0; +@empty-image-size: 160px; +@empty-description-margin-top: @padding-md; +@empty-description-padding: 0 60px; +@empty-description-color: @gray-6; +@empty-description-font-size: 14px; +@empty-description-line-height: 20px; +@empty-bottom-margin-top: 24px; + +// TreeSelect +@tree-select-font-size: @font-size-md; +@tree-select-nav-background-color: @background-color; +@tree-select-content-background-color: @white; +@tree-select-nav-item-padding: @padding-sm @padding-xs @padding-sm @padding-sm; +@tree-select-item-height: 44px; +@tree-select-item-active-color: @red; +@tree-select-item-disabled-color: @gray-5; + +// Uploader +@uploader-size: 80px; +@uploader-icon-size: 24px; +@uploader-icon-color: @gray-4; +@uploader-text-color: @gray-6; +@uploader-text-font-size: @font-size-sm; +@uploader-upload-background-color: @gray-1; +@uploader-upload-active-color: @active-color; +@uploader-delete-color: @white; +@uploader-delete-icon-size: 14px; +@uploader-delete-background-color: rgba(0, 0, 0, 0.7); +@uploader-file-background-color: @background-color; +@uploader-file-icon-size: 20px; +@uploader-file-icon-color: @gray-7; +@uploader-file-name-padding: 0 @padding-base; +@uploader-file-name-margin-top: @padding-xs; +@uploader-file-name-font-size: @font-size-sm; +@uploader-file-name-text-color: @gray-7; +@uploader-mask-background-color: fade(@gray-8, 88%); +@uploader-mask-icon-size: 22px; +@uploader-mask-message-font-size: @font-size-sm; +@uploader-mask-message-line-height: 14px; +@uploader-loading-icon-size: 22px; +@uploader-loading-icon-color: @white; +@uploader-disabled-opacity: @disabled-opacity; + +// DropdownMenu +@dropdown-menu-height: 50px; +@dropdown-menu-background-color: @white; +@dropdown-menu-title-font-size: 15px; +@dropdown-menu-title-text-color: @text-color; +@dropdown-menu-title-active-text-color: @red; +@dropdown-menu-title-disabled-text-color: @gray-6; +@dropdown-menu-title-padding: 0 @padding-lg 0 @padding-xs; +@dropdown-menu-title-line-height: 18px; +@dropdown-menu-option-active-color: @red; +@dropdown-menu-box-shadow: 0 2px 12px fade(@gray-7, 12); + +// IndexAnchor +@index-anchor-padding: 0 @padding-md; +@index-anchor-text-color: @text-color; +@index-anchor-font-weight: 500; +@index-anchor-font-size: @font-size-md; +@index-anchor-line-height: 32px; +@index-anchor-background-color: transparent; +@index-anchor-active-background-color: @white; +@index-anchor-active-text-color: @blue; + +// IndexBar +@index-bar-index-font-size: @font-size-xs; +@index-bar-index-line-height: 14px; + +// skeleton +@skeleton-padding: 0 @padding-md; +@skeleton-row-height: 16px; +@skeleton-row-background-color: @gray-2; +@skeleton-row-margin-top: @padding-sm; +@skeleton-avatar-background-color: @gray-2; +@skeleton-animation-duration: 1.2s; + +// Cascader +@cascader-header-height: 48px; +@cascader-header-padding: 0 16px; +@cascader-title-font-size: 16px; +@cascader-title-line-height: 20px; +@cascader-close-icon-size: 22px; +@cascader-close-icon-color: #c8c9cc; +@cascader-selected-icon-size: 18px; +@cascader-tabs-height: 48px; +@cascader-active-color: @blue; +@cascader-options-height: 384px; +@cascader-option-disabled-color: #c8c9cc; +@cascader-tab-color: #323233; +@cascader-unselected-tab-color: #969799; \ No newline at end of file diff --git a/src/assets/css/variables.scss b/src/assets/css/variables.scss new file mode 100644 index 0000000..ee5a721 --- /dev/null +++ b/src/assets/css/variables.scss @@ -0,0 +1,3 @@ +// variables +$background-color: #f8f8f8; +$theme-color: #07b0b8; diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..f3d2503 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/components/TabBar.vue b/src/components/TabBar.vue new file mode 100644 index 0000000..631e926 --- /dev/null +++ b/src/components/TabBar.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/config/env.development.js b/src/config/env.development.js new file mode 100644 index 0000000..046d75b --- /dev/null +++ b/src/config/env.development.js @@ -0,0 +1,9 @@ +// 本地环境配置 +module.exports = { + env: 'development', + title: 'e联社区', + baseUrl: 'http://192.168.1.144/', // 项目地址 + baseApi: 'http://192.168.1.144/api', // 本地api请求地址,注意:如果你使用了代理,请设置成'/' + APPID: 'xxx', + APPSECRET: 'xxx', +} diff --git a/src/config/env.production.js b/src/config/env.production.js new file mode 100644 index 0000000..fc3e233 --- /dev/null +++ b/src/config/env.production.js @@ -0,0 +1,8 @@ +// 正式 +module.exports = { + env: 'production', + title: 'e联社区', + baseUrl: 'https://epmet-preview.elinkservice.cn/', // 正式项目地址 + baseApi: 'https://epmet-preview.elinkservice.cn/api', // 正式api请求地址 + APPSECRET: 'xxx', +} diff --git a/src/config/env.staging.js b/src/config/env.staging.js new file mode 100644 index 0000000..0f67f68 --- /dev/null +++ b/src/config/env.staging.js @@ -0,0 +1,8 @@ +module.exports = { + env: 'staging', + title: 'e联社区', + baseUrl: 'https://test.xxx.com', // 测试项目地址 + baseApi: 'https://test.xxx.com/api', // 测试api请求地址 + APPID: 'xxx', + APPSECRET: 'xxx', +} diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 0000000..72a4fa1 --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,3 @@ +// 根据环境引入不同配置 process.env.NODE_ENV +const config = require('./env.' + process.env.VUE_APP_ENV) +module.exports = config diff --git a/src/const/index.js b/src/const/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/filters/filter.js b/src/filters/filter.js new file mode 100644 index 0000000..e8bb4c0 --- /dev/null +++ b/src/filters/filter.js @@ -0,0 +1,37 @@ +/** + *格式化时间 + *yyyy-MM-dd hh:mm:ss + */ +export function formatDate(time, fmt) { + if (time === undefined || '') { + return + } + const date = new Date(time) + if (/(y+)/.test(fmt)) { + fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) + } + const o = { + 'M+': date.getMonth() + 1, + 'd+': date.getDate(), + 'h+': date.getHours(), + 'm+': date.getMinutes(), + 's+': date.getSeconds() + } + for (const k in o) { + if (new RegExp(`(${k})`).test(fmt)) { + const str = o[k] + '' + fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? str : padLeftZero(str)) + } + } + return fmt +} + +function padLeftZero(str) { + return ('00' + str).substr(str.length) +} +/* + * 隐藏用户手机号中间四位 + */ +export function hidePhone(phone) { + return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') +} diff --git a/src/filters/index.js b/src/filters/index.js new file mode 100644 index 0000000..a889078 --- /dev/null +++ b/src/filters/index.js @@ -0,0 +1,7 @@ +import Vue from 'vue' +import * as filter from './filter' + +Object.keys(filter).forEach(k => Vue.filter(k, filter[k])) + +Vue.prototype.$formatDate = Vue.filter('formatDate') +Vue.prototype.$hidePhone = Vue.filter('hidePhone') diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..92f038e --- /dev/null +++ b/src/main.js @@ -0,0 +1,39 @@ +import 'core-js/stable' +import 'regenerator-runtime/runtime' + +import Vue from 'vue' +import App from './App.vue' +import router from './router' +import store from './store' + +// 全局引入按需引入UI库 vant +import '@/plugins/vant' + +// 引入全局样式 +import '@/assets/css/common.less' +import '@/assets/css/index.less' +// 移动端适配 +import 'amfe-flexible' + +Vue.config.productionTip = false + +// 日期格式化插件 +import dayjs from 'dayjs' + +Vue.prototype.$dayjs = dayjs +// 提示框封装 +// import { Tips } from '@/utils' + +// Vue.prototype.$tips = Tips +//开发环境使用,生产环境自动取消 +import Vconsole from 'vconsole' + +if (process.env.NODE_ENV !== 'production') { + new Vconsole() +} +new Vue({ + el: '#app', + router, + store, + render: h => h(App) +}) diff --git a/src/plugins/vant.js b/src/plugins/vant.js new file mode 100644 index 0000000..343ce83 --- /dev/null +++ b/src/plugins/vant.js @@ -0,0 +1,53 @@ +// 按需全局引入 @组件 +import Vue from 'vue' +import { + Button, + Field, + Cell, + Uploader, + Image, + Picker, + Popup, + Icon, + Toast, + Progress, + NavBar, + RadioGroup, + Radio, + Dialog, + Tabs, + Tab, + Tag, + Steps, + Step, + Divider, + List, + ActionSheet, + Checkbox, + CellGroup +} from 'vant' + +Vue.use(Button) + .use(Field) + .use(Cell) + .use(Uploader) + .use(Image) + .use(Picker) + .use(Popup) + .use(Icon) + .use(Toast) + .use(Progress) + .use(NavBar) + .use(RadioGroup) + .use(Radio) + .use(Dialog) + .use(Tabs) + .use(Tab) + .use(Tag) + .use(Steps) + .use(Step) + .use(Divider) + .use(List) + .use(ActionSheet) + .use(Checkbox) + .use(CellGroup) diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..5889228 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue' +import Router from 'vue-router' +import { constantRouterMap } from './router.config.js' + +// hack router push callback +const originalPush = Router.prototype.push +Router.prototype.push = function push(location, onResolve, onReject) { + if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject) + return originalPush.call(this, location).catch(err => err) +} + +Vue.use(Router) + +const createRouter = () => + new Router({ + scrollBehavior: () => ({ y: 0 }), + routes: constantRouterMap + }) + +const router = createRouter() + +// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 +export function resetRouter() { + const newRouter = createRouter() + router.matcher = newRouter.matcher // reset router +} + + + +export default router \ No newline at end of file diff --git a/src/router/router.config.js b/src/router/router.config.js new file mode 100644 index 0000000..34b3015 --- /dev/null +++ b/src/router/router.config.js @@ -0,0 +1,11 @@ +/** + * 基础路由 + * @type { *[] } + */ +export const constantRouterMap = [ + { + path: '/', + component: () => import('@/views/login'), + meta: { title: '登录', keepAlive: false } + } +] diff --git a/src/store/getters.js b/src/store/getters.js new file mode 100644 index 0000000..93634df --- /dev/null +++ b/src/store/getters.js @@ -0,0 +1,4 @@ +const getters = { + userName: state => state.app.userName +} +export default getters diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..1ee10b9 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import getters from './getters' +import app from './modules/app' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + modules: { + app + }, + getters +}) + +export default store diff --git a/src/store/modules/app.js b/src/store/modules/app.js new file mode 100644 index 0000000..7f81758 --- /dev/null +++ b/src/store/modules/app.js @@ -0,0 +1,19 @@ +const state = { + userName: '' +} +const mutations = { + SET_USER_NAME(state, name) { + state.userName = name + } +} +const actions = { + // 设置name + setUserName({ commit }, name) { + commit('SET_USER_NAME', name) + } +} +export default { + state, + mutations, + actions +} diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..cd14bd5 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,110 @@ +/** + * Created by PanJiaChen on 16/11/18. + */ + +/** + * Parse the time to string + * @param {(Object|string|number)} time + * @param {string} cFormat + * @returns {string} + */ +export function parseTime(time, cFormat) { + if (arguments.length === 0) { + return null + } + const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { + time = parseInt(time) + } + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} + +/** + * @param {number} time + * @param {string} option + * @returns {string} + */ +export function formatTime(time, option) { + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } + const d = new Date(time) + const now = Date.now() + + const diff = (now - d) / 1000 + + if (diff < 30) { + return '刚刚' + } else if (diff < 3600) { + // less 1 hour + return Math.ceil(diff / 60) + '分钟前' + } else if (diff < 3600 * 24) { + return Math.ceil(diff / 3600) + '小时前' + } else if (diff < 3600 * 24 * 2) { + return '1天前' + } + if (option) { + return parseTime(time, option) + } else { + return ( + d.getMonth() + + 1 + + '月' + + d.getDate() + + '日' + + d.getHours() + + '时' + + d.getMinutes() + + '分' + ) + } +} + +/** + * @param {string} url + * @returns {Object} + */ +export function param2Obj(url) { + const search = url.split('?')[1] + if (!search) { + return {} + } + return JSON.parse( + '{"' + + decodeURIComponent(search) + .replace(/"/g, '\\"') + .replace(/&/g, '","') + .replace(/=/g, '":"') + .replace(/\+/g, ' ') + + '"}' + ) +} diff --git a/src/utils/jsApiList.js b/src/utils/jsApiList.js new file mode 100644 index 0000000..6d3cd63 --- /dev/null +++ b/src/utils/jsApiList.js @@ -0,0 +1,7 @@ +export const jsApiList = [ + 'updateAppMessageShareData', + 'updateTimelineShareData', + 'getLocation', + 'openLocation', + 'chooseWXPay' +] diff --git a/src/utils/request.js b/src/utils/request.js new file mode 100644 index 0000000..6788f91 --- /dev/null +++ b/src/utils/request.js @@ -0,0 +1,58 @@ +import axios from 'axios' +import store from '@/store' +import { Toast } from 'vant' +// 根据环境不同引入不同api地址 +import { baseApi } from '@/config' +// create an axios instance +const service = axios.create({ + baseURL: baseApi, // url = base api url + request url + withCredentials: true, // send cookies when cross-domain requests + timeout: 5000 // request timeout +}) + +// request拦截器 request interceptor +service.interceptors.request.use( + config => { + // 不传递默认开启loading + if (!config.hideloading) { + // loading + Toast.loading({ + forbidClick: true + }) + } + if (store.getters.token) { + config.headers['X-Token'] = '' + } + return config + }, + error => { + // do something with request error + console.log(error) // for debug + return Promise.reject(error) + } +) +// respone拦截器 +service.interceptors.response.use( + response => { + Toast.clear() + const res = response.data + if (res.status && res.status !== 200) { + // 登录超时,重新登录 + if (res.status === 401) { + store.dispatch('FedLogOut').then(() => { + location.reload() + }) + } + return Promise.reject(res || 'error') + } else { + return Promise.resolve(res) + } + }, + error => { + Toast.clear() + console.log('err' + error) // for debug + return Promise.reject(error) + } +) + +export default service diff --git a/src/utils/storage.js b/src/utils/storage.js new file mode 100644 index 0000000..8105661 --- /dev/null +++ b/src/utils/storage.js @@ -0,0 +1,35 @@ +/** + * 封装操作localstorage本地存储的方法 + */ +export const storage = { + //存储 + set(key, value) { + localStorage.setItem(key, JSON.stringify(value)) + }, + //取出数据 + get(key) { + return JSON.parse(localStorage.getItem(key)) + }, + // 删除数据 + remove(key) { + localStorage.removeItem(key) + } +} + +/** + * 封装操作sessionStorage本地存储的方法 + */ +export const sessionStorage = { + //存储 + set(key, value) { + window.sessionStorage.setItem(key, JSON.stringify(value)) + }, + //取出数据 + get(key) { + return JSON.parse(window.sessionStorage.getItem(key)) + }, + // 删除数据 + remove(key) { + window.sessionStorage.removeItem(key) + } +} diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000..e9bd1ba --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,20 @@ +/** + * Created by Sunnie on 19/06/04. + */ + +/** + * @param {string} path + * @returns {Boolean} + */ +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUsername(str) { + const valid_map = ['admin', 'editor'] + return valid_map.indexOf(str.trim()) >= 0 +} diff --git a/src/utils/vconsole.js b/src/utils/vconsole.js new file mode 100644 index 0000000..dd6121a --- /dev/null +++ b/src/utils/vconsole.js @@ -0,0 +1,3 @@ +import Vconsole from 'vconsole' +const vConsole = new Vconsole() +export default vConsole diff --git a/src/utils/wechatPlugin.js b/src/utils/wechatPlugin.js new file mode 100644 index 0000000..90e6ab4 --- /dev/null +++ b/src/utils/wechatPlugin.js @@ -0,0 +1,12 @@ +import wx from 'weixin-js-sdk' + +const plugin = { + install(Vue) { + Vue.prototype.$wx = wx + Vue.wx = wx + }, + $wx: wx +} + +export default plugin +export const install = plugin.install diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 0000000..d25101f --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/static/image/demo.png b/static/image/demo.png new file mode 100644 index 0000000..0cf476f Binary files /dev/null and b/static/image/demo.png differ diff --git a/static/image/secret.png b/static/image/secret.png new file mode 100644 index 0000000..c57b074 Binary files /dev/null and b/static/image/secret.png differ diff --git a/vue.config.js b/vue.config.js new file mode 100644 index 0000000..d63a012 --- /dev/null +++ b/vue.config.js @@ -0,0 +1,171 @@ +const path = require('path') +const defaultSettings = require('./src/config/index.js') +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin +const OUTPUT_FILE_NAME = process.env.outputDir +const FileManagerPlugin = require('filemanager-webpack-plugin') +const resolve = dir => path.join(__dirname, dir) +// page title +const name = defaultSettings.title || 'vue mobile template' +// 生产环境,测试和正式 +const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV) + +const { defineConfig } = require('@vue/cli-service') +module.exports = defineConfig({ + publicPath: process.env.NODE_ENV === 'development' ? '/' : '/epmet-work-h5', // 署应用包时的基本 URL。 vue-router hash 模式使用 + // publicPath: process.env.NODE_ENV === 'development' ? '/' : '/h5', //署应用包时的基本 URL。 vue-router history模式使用 + outputDir: `dist/${OUTPUT_FILE_NAME}`, // 生产环境构建文件的目录 + assetsDir: 'static', // outputDir的静态资源(js、css、img、fonts)目录 + lintOnSave: false, + productionSourceMap: false, // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。 + devServer: { + // host: 'epmet-cloud.elinkservice.cn', + port: 80, // 端口 + open: false, // 启动后打开浏览器 + client: { + overlay: { + // 当出现编译器错误或警告时,在浏览器中显示全屏覆盖层 + warnings: false, + errors: true + } + }, + proxy: { + //配置跨域 + '/api': { + // target: `http://219.146.91.110:30801`, + target: `http://127.0.0.1:8080`, + changeOrigin: true, + ws: false, + pathRewrite: { + '^api': '' + } + } + } + }, + css: { + extract: IS_PROD, + sourceMap: false, + loaderOptions: { + less: { + lessOptions: { + modifyVars: { + hack: `true; @import "assets/css/vant-theme.less";` + } + } + } + } + }, + configureWebpack: config => { + config.name = name + config.plugins.push( + new FileManagerPlugin({ + events: { + onEnd: { + delete: [`./${OUTPUT_FILE_NAME}.zip`], + archive: [ + { + source: `./dist/${OUTPUT_FILE_NAME}`, + destination: `./${OUTPUT_FILE_NAME}.zip` + } + ] + } + } + }) + ) + + // 为生产环境修改配置... + // if (IS_PROD) { + // // externals + // config.externals = externals + // } + }, + + chainWebpack: config => { + config.plugins.delete('preload') // TODO: need test + config.plugins.delete('prefetch') // TODO: need test + + // set svg-sprite-loader + config.module.rule('svg').exclude.add(resolve('src/assets/icons')).end() + config.module + .rule('icons') + .test(/\.svg$/) + .include.add(resolve('src/assets/icons')) + .end() + .use('svg-sprite-loader') + .loader('svg-sprite-loader') + .options({ + symbolId: 'icon-[name]' + }) + .end() + + // 别名 alias + config.resolve.alias + .set('@', resolve('src')) + .set('assets', resolve('src/assets')) + .set('api', resolve('src/api')) + .set('views', resolve('src/views')) + .set('components', resolve('src/components')) + /** + * 设置保留空格 + */ + config.module + .rule('vue') + .use('vue-loader') + .loader('vue-loader') + .tap(options => { + options.compilerOptions.preserveWhitespace = true + return options + }) + .end() + /** + * 打包分析 + */ + if (IS_PROD) { + config.plugin('webpack-report').use(BundleAnalyzerPlugin, [ + { + analyzerMode: 'static' + } + ]) + } + config + // https://webpack.js.org/configuration/devtool/#development + .when(!IS_PROD, config => config.devtool('cheap-source-map')) + + config.when(IS_PROD, config => { + config + .plugin('ScriptExtHtmlWebpackPlugin') + .after('html') + .use('script-ext-html-webpack-plugin', [ + { + // 将 runtime 作为内联引入不单独存在 + inline: /runtime\..*\.js$/ + } + ]) + .end() + config.optimization.splitChunks({ + chunks: 'all', + cacheGroups: { + // cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块 + commons: { + name: 'chunk-commons', + test: resolve('src/components'), + minChunks: 3, // 被至少用三次以上打包分离 + priority: 5, // 优先级 + reuseExistingChunk: true // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。 + }, + node_vendors: { + name: 'chunk-libs', + chunks: 'initial', // 只打包初始时依赖的第三方 + test: /[\\/]node_modules[\\/]/, + priority: 10 + }, + vantUI: { + name: 'chunk-vantUI', // 单独将 vantUI 拆包 + priority: 20, // 数字大权重到,满足多个 cacheGroups 的条件时候分到权重高的 + test: /[\\/]node_modules[\\/]_?vant(.*)/ + } + } + }) + config.optimization.runtimeChunk('single') + }) + } +})