diff --git a/CHANGELOG.md b/CHANGELOG.md index 8171e6a..0a82483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +## v 1.2.13 + +- 路由缓存配置相关 +- 升级依赖 + ## v 1.2.12 - WebSocket简单包装类 diff --git a/README.md b/README.md index 6993c94..0687de9 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ vue + vuex + vue router + TypeScript(支持 JavaScript) 模板 ## 环境要求 -- `Node.js`: v12 ([fibers](https://github.com/laverdet/node-fibers#supported-platforms) v4.0.2 不支持 Node v13) +- `Node.js`: >= v12 - `yarn`: 最新 ### 建议开发环境 @@ -171,6 +171,7 @@ yarn vue-cli-service help # [命令] : 比如 yarn vue-cli-service help test:e2e │ │── api # http通信 │ │── assets # 静态资源文件目录, 使用到的会被解析处理(比如图片等) │ │── components # 从views/pages提取的复用组件(建文件夹分类, 未分类的基本就是基础组件了) +│ │── functions # 从views/pages提取的复用逻辑(建文件夹分类, 未分类的基本就是公用逻辑了) │ │── config # 配置目录 │ │── enum # 枚举目录 │ │── lang # 多语言目录 @@ -181,7 +182,7 @@ yarn vue-cli-service help # [命令] : 比如 yarn vue-cli-service help test:e2e │ │── store # 状态管理 │ │ └── modules # 各模块状态管理 │ │── types # ts 接口/申明文件 -│ │── utils # 工具集(一般为幂等函数/单例对象/Class) +│ │── utils # 工具集(业务无关, 一般为幂等函数/单例对象/Class...) │ │── views # 视图 │ │── pages # 【可选】多页时页面的存储目录 │ │── html模板名 # 【可选】存放页面代码目录 @@ -827,6 +828,5 @@ http { ### 其他 - 期待 [vue3.0](https://github.com/vuejs/vue/projects/6) & [webpack 5.0](https://github.com/webpack/webpack/projects/5) [正式版](https://github.com/webpack/changelog-v5/blob/master/README.md) -- [fibers](https://github.com/laverdet/node-fibers#supported-platforms) v4.0.2 **不支持 Node v13** - `crypto-js` v4 **不支持 IE10** - `TypeScript`(3.8.2) `const enum` 编译为内联代码(`inline code`)的支持有限, 尽量使用常量成员, 然后等[更新](https://github.com/microsoft/TypeScript)吧 \ No newline at end of file diff --git a/build/production.config.js b/build/production.config.js index 4ee1f1a..ffe8ebb 100644 --- a/build/production.config.js +++ b/build/production.config.js @@ -240,7 +240,7 @@ module.exports = function(config, ENV, pages) { chunks: 'all', priority: 66, reuseExistingChunk: true, - test: /[\\/]node_modules[\\/]echarts[\\/]/, + test: /[\\/]node_modules[\\/]echarts(?:-.+)?[\\/]/, }, // d3.js d3: { diff --git a/package.json b/package.json index fb6384f..0d5e8eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-tpl", - "version": "1.2.12", + "version": "1.2.13", "private": false, "description": "vue + vuex + vue router + TypeScript(支持 JavaScript) 模板", "author": "毛瑞 ", @@ -17,23 +17,24 @@ "crypto-js": "3.3.0", "d3": "^5.16.0", "echarts": "^4.7.0", + "echarts-liquidfill": "^2.0.5", "element-ui": "^2.13.1", "jsencrypt": "^3.0.0-rc.1", "luma.gl": "^7.3.2", "normalize.css": "^8.0.1", "nprogress": "^0.2.0", - "pixi.js": "^5.2.2", - "pixi.js-legacy": "^5.2.2", + "pixi.js": "^5.2.3", + "pixi.js-legacy": "^5.2.3", "register-service-worker": "^1.7.1", "three": "^0.115.0", "vue": "^2.6.11", "vue-class-component": "^7.2.3", - "vue-i18n": "^8.17.3", - "vue-property-decorator": "^8.4.1", + "vue-i18n": "^8.17.4", + "vue-property-decorator": "^8.4.2", "vue-router": "^3.1.6", - "vuex": "^3.2.0", + "vuex": "^3.3.0", "vuex-class": "^0.3.2", - "vuex-module-decorators": "^0.16.1", + "vuex-module-decorators": "^0.17.0", "zdog": "^1.1.2", "zrender": "^4.3.0" }, @@ -42,12 +43,12 @@ "@babel/plugin-proposal-export-namespace-from": "^7.8.3", "@babel/plugin-proposal-function-sent": "^7.8.3", "@babel/plugin-proposal-private-methods": "^7.8.3", - "@types/crypto-js": "3.1.44", + "@types/crypto-js": "3.1.45", "@types/d3": "^5.7.2", - "@types/echarts": "^4.4.6", + "@types/echarts": "^4.6.0", "@types/jest": "^25.2.1", - "@typescript-eslint/eslint-plugin": "^2.29.0", - "@typescript-eslint/parser": "^2.29.0", + "@typescript-eslint/eslint-plugin": "^2.30.0", + "@typescript-eslint/parser": "^2.30.0", "@vue/cli-plugin-babel": "~4.3.1", "@vue/cli-plugin-e2e-cypress": "~4.3.1", "@vue/cli-plugin-eslint": "~4.3.1", @@ -68,14 +69,14 @@ "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", "eslint-plugin-vue": "^6.2.2", - "fibers": "^4.0.2", + "fibers": "^5.0.0", "hard-source-webpack-plugin": "^0.13.1", - "lint-staged": "^10.1.7", + "lint-staged": "^10.2.0", "postcss-preset-env": "^6.7.0", "regenerate": "^1.4.0", "regjsgen": "^0.5.1", "regjsparser": "^0.6.4", - "sass": "^1.26.3", + "sass": "^1.26.5", "sass-loader": "^8.0.2", "stylelint": "^13.3.3", "stylelint-config-scss-maorey": "^1.1.1", diff --git a/src/api/user.ts b/src/api/user.ts index f990b7d..c1d4bc3 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -6,7 +6,7 @@ import { setHEAD, get, post } from '@/utils/ajax' import { local } from '@/utils/storage' import CONFIG from '@/config' -import API from '@/config/api/user' +import { user as API } from '@/enum/api' // 加密算法(token + RSA 加密) import Jsencrypt from 'jsencrypt' diff --git a/src/components/ChooserAsync.tsx b/src/components/ChooserAsync.tsx index d6d4bb8..3fe1226 100644 --- a/src/components/ChooserAsync.tsx +++ b/src/components/ChooserAsync.tsx @@ -3,7 +3,7 @@ * @Author: 毛瑞 * @Date: 2020-01-02 16:13:36 */ -import { CreateElement, Component as Comp, VNode } from 'vue' +import { CreateElement, VNode } from 'vue' // see: https://github.com/kaorun343/vue-property-decorator import { Component, Vue, Prop, Watch } from 'vue-property-decorator' @@ -12,156 +12,179 @@ import { Component, Vue, Prop, Watch } from 'vue-property-decorator' // import STYLE from './index.module.scss' import Info from './Info' import Loading from './Loading' +import { status, component, filter, SLOTS } from './ChooserAsyncFunctional' -import { isFn } from '@/utils' +import { isDef, isFn, isEqual } from '@/utils' /// 常量(UPPER_CASE),单例/变量(camelCase),函数(无副作用,camelCase) /// // const ModuleOne: any = getAsync(() => // import(/* webpackChunkName: "ihOne" */ './ModuleOne') // ) -export const enum status { - none = 1, - loading = 2, - error = 3, - empty = 4, - success = 5, -} -type component = status | string | Comp -type filter = (data: any) => { data: any; comp: component } | void +export { status, component, filter } -const DIC_SLOT = { - [status.none]: 'none', - [status.loading]: 'loading', - [status.error]: 'error', - [status.empty]: 'empty', -} /** 异步选择器组件, 最终渲染组件将得到一个prop: data, 即异步结果 * - * props: 见: Prop 【注意】: get/error 变化时会重新请求 - * - * events: 见: const enum status 键值 + * props: 见: Prop 【注意】: 响应任意prop变化 * * slots: 见: const enum status 键值, 支持对应作用域插槽/插槽【二选一】作用域插槽优先(二者都有时无法确定顺序, 故) * + * events: 见: const enum status 键值 + * * 示例: * * * ( import 咋没得文档呢, 因为tsx么... ┐(: ´ ゞ`)┌ ) */ -@Component +@Component({ + beforeRouteUpdate(this: any, to, from, next) { + this.isOut = 0 + setTimeout(next) + }, + beforeRouteLeave(this: any, to: any, from: any, next: any) { + this.isOut = to.matched.length && 1 + setTimeout(next) + }, +}) export default class extends Vue { /// [model] (@Model('change') readonly attr!: string) /// /// [props] (@Prop() readonly attr!: string) /// - /** 未匹配到任何组件但有数据时使用的组件[默认‘div’] 若字典存在, tag为字符串, 则优先从字典取 - */ + /** 查询函数 diff:fetch,args ? fetch.apply(store, args) */ + @Prop() readonly fetch?: (...args: any[]) => Promise + /** 查询函数参数列表 */ + @Prop() readonly args?: any[] + /** 查询函数 get.call(store), 同 (fetch + args) */ + @Prop() readonly get?: () => Promise + /** 查询错误处理 store.error(err) */ + @Prop() readonly error?: status | ((err?: Error) => component) + /** 未匹配到任何组件但有数据时使用的组件[默认div] */ @Prop({ default: 'div' }) readonly tag!: component - /** 查询函数 - */ - @Prop() readonly get?: () => Promise - /** 选择组件函数 若字典存在, 返回的comp属性为字符串, 则优先从字典取 - */ + /** 数据&组件筛选 store.filter(data) */ @Prop() readonly filter?: filter - /** 自定义处理查询错误时的展示(接受参数为错误对象) - */ - @Prop() readonly error?: status | ((err: Error) => component) - /** 组件字典 当filter返回string时即从字典取对应组件 - */ - @Prop() readonly components?: IObject + /** 组件字典[优先使用] */ + @Prop() readonly components?: { [name: string]: component } /// [data] (attr: string = '响应式属性' // 除了 undefined) /// private is: component = status.loading - private isSleep = false // 是否失活/休眠 + private isOut = 0 // 是否失活/离开 /// 非响应式属性 (attr?: string // undefined) /// private $_response?: any // 原始响应数据 private $_data?: any // 绑定数据 private $_vnode?: VNode + private $_isGet?: false | 1 | 2 + private $_onError?: (err?: Error) => void /// [computed] (get attr() {} set attr(){}) /// /// [LifeCycle] (private beforeCreate(){}/.../destroyed(){}) /// private created() { - this.i() + this.g() } private activated() { - this.isSleep = false + this.isOut = 0 } private deactivated() { - this.isSleep = true + this.isOut = 1 } /// [watch] (@Watch('attr') onAttrChange(val, oldVal) {}) /// /// [methods] (method(){}) /// - @Watch('get') - private i() { - this.is = status.loading - // eslint-disable-next-line prefer-promise-reject-errors - ;(this.get ? this.get() : Promise.reject()) - .then(data => { - this.$_response = data - this.w() - }) - .catch(err => { - this.is = - (isFn(this.error) ? (this.error as any)(err) : this.error) || - status.error - }) - } - @Watch('tag') @Watch('filter') - @Watch('components') + @Watch('components', { deep: true }) private w() { - const data = this.$_response - if (data) { + let data = this.$_response + if (isDef(data)) { const DIC = this.components - const tag = this.tag - if (!this.filter) { - this.$_data = { props: { data } } - this.is = (DIC && DIC[tag as string]) || tag - return + if (isFn(this.filter)) { + if (isDef((data = this.filter(data)))) { + this.$_data = { data: data.data || data } + this.is = (DIC && DIC[data.comp]) || data.comp || this.tag + } else { + this.is = status.empty + } + } else { + this.$_data = { data } + this.is = (DIC && DIC[this.tag as string]) || this.tag } + } else { + this.is = status.empty + } + } - const result = this.filter(data) - if (result) { - this.$_data = { props: { data: result.data || result } } - this.is = (DIC && DIC[result.comp as string]) || result.comp || tag - return - } + @Watch('error') + private e() { + const onError = + this.$_onError || + (this.$_onError = (err?: Error) => { + this.is = + (isFn(this.error) ? this.error(err) : this.error) || status.error + }) + if (this.$_isGet) { + this.is = status.loading + ;(this.$_isGet > 1 + ? (this.fetch as any).apply(this, this.args) + : (this as any).get() + ) + .then((data: any) => { + this.$_response = data + this.w() + }) + .catch(onError) + } else if (this.error) { + onError() + } else { + this.$_response = 1 + this.w() } + } + + @Watch('get') + private g() { + this.$_isGet = isFn(this.get) ? 1 : isFn(this.fetch) && 2 + this.e() + } + + @Watch('fetch') + private f() { + this.$_isGet = isFn(this.fetch) ? 2 : isFn(this.get) && 1 + this.e() + } - this.is = status.empty + @Watch('args', { deep: true }) + private a(now?: any, old?: any) { + isEqual(now, old) || this.f() } // see: https://github.com/vuejs/jsx#installation // eslint-disable-next-line @typescript-eslint/no-unused-vars private render(h: CreateElement) { - if (this.isSleep) { + if (this.isOut) { return this.$_vnode } let Comp: any = this.is // for 依赖收集 let slot - if ((slot = DIC_SLOT[Comp as keyof typeof DIC_SLOT])) { + if ((slot = (SLOTS as any)[Comp])) { this.$emit(slot) slot = this.$scopedSlots[slot] ? (this.$scopedSlots[slot] as any)(this.$_data) : this.$slots[slot] if (slot && slot.length) { Comp = this.tag - return (this.$_vnode = {slot}) + return (this.$_vnode = {slot}) } } switch (Comp) { case status.none: - return (this.$_vnode = undefined) + return (this.$_vnode = ) case status.loading: return (this.$_vnode = ) case status.empty: @@ -169,13 +192,14 @@ export default class extends Vue { )) case status.error: - return (this.$_vnode = ) + return (this.$_vnode = ) default: this.$emit('success') slot = this.$scopedSlots slot = slot.success || slot.default + Object.assign(this.$data.props || (this.$data.props = {}), this.$_data) return (this.$_vnode = ( - + {slot ? slot(this.$_data) : (slot = this.$slots).success || slot.default} diff --git a/src/components/ChooserAsyncFunctional.tsx b/src/components/ChooserAsyncFunctional.tsx index 1f8393d..ffe9334 100644 --- a/src/components/ChooserAsyncFunctional.tsx +++ b/src/components/ChooserAsyncFunctional.tsx @@ -11,14 +11,14 @@ import Vue, { Component, RenderContext, VNode } from 'vue' import Info from './Info' import Loading from './Loading' -import { hasOwnProperty, isFn, setHook } from '@/utils' +import { hasOwnProperty, isDef, isFn, isEqual, setHook } from '@/utils' +import clone from '@/utils/clone' /// 常量(UPPER_CASE),单例/变量(camelCase),函数(无副作用,camelCase) /// // const ModuleOne: any = getAsync(() => // import(/* webpackChunkName: "ihOne" */ './ModuleOne') // ) -/** 加载状态, 同时键名也是支持的事件 - */ +/** 加载状态, 同时键名也是支持的事件/slot */ export const enum status { none = 1, loading = 2, @@ -27,26 +27,28 @@ export const enum status { success = 5, } export type component = status | string | Component -export type filter = (data: any) => { data: any; comp: component } | void -interface IState { +export type filter = + | ((data: any) => { data: any; comp: component }) + | ((data: any) => any) +type hook = (context: RenderContext) => any +type hooks = hook | hook[] +export interface IStore { /// props /// - /** 未匹配到任何组件但有数据时使用的组件[默认‘div’] 若字典存在, tag为字符串, 则优先从字典取 - */ - tag?: component - /** 查询函数 - */ + /** 查询函数 diff:fetch,args ? fetch.apply(store, args) */ + fetch?: (...args: any[]) => Promise + /** 查询函数参数列表 */ + args?: any[] + /** 查询函数 get.call(store), 同 (fetch + args) */ get?: () => Promise - /** 选择组件函数 若字典存在, 返回的comp属性为字符串, 则优先从字典取 - */ + /** 查询错误处理 store.error(err) */ + error?: status | ((err?: Error) => component) + /** 未匹配到任何组件但有数据时使用的组件[默认div] */ + tag?: component + /** 数据&组件筛选 store.filter(data) */ filter?: filter - /** 自定义处理查询错误时的展示(接受参数为错误对象) - */ - error?: status | ((err: Error) => component) - /** 组件字典 当filter返回string时即从字典取对应组件 - */ - components?: { [name: string]: Component } - /** 类似 v-once, 默认 false - */ + /** 组件字典[优先使用] */ + components?: { [name: string]: component } + /** 类似 v-once, true:上述props未发生变化则[不重新渲染],此时slot将不响应外部状态改变 */ once?: boolean /// 私有状态 /// @@ -54,171 +56,191 @@ interface IState { i: { /** 当前组件 */ i: component - /** 父组件是否未激活 */ + /** 父/此组件是否失活/离开 */ d: 0 | 1 } - /** 触发的事件: status 枚举 */ - f: { [event in status]?: Function | Function[] } /** hooks */ h: { /** 重新加载 */ $: () => void - /** 父/返回组件 activated */ + /** 父/此组件 activated */ a: () => void - /** 父/返回组件 deactivated */ + /** 父/此组件 deactivated */ d: () => void // /** 父组件 destroyed */ // e: () => void } + /** 触发的事件: status 枚举 */ + f?: { [event in status]?: hooks } + /** 1:get 2:(fetch + args) */ + g?: false | 1 | 2 /** 原始响应 */ o?: any - /** 绑定 props.data */ + /** 绑定的 props.data */ d?: any - /** 当前组件VNode */ - n?: VNode /** 上一次组件(once时比较) */ c?: component + /** 当前组件VNode */ + n?: VNode } -function watch(state: IState) { - const data = state.o - const DEFAULT_TAG = 'div' - if (data) { - const DIC = state.components - const tag = state.tag - if (!state.filter) { - state.d = { data } - state.i.i = (DIC && DIC[tag as string]) || tag || DEFAULT_TAG - return - } - - const result = state.filter(data) - if (result) { - state.d = { data: result.data || result } - state.i.i = - (DIC && DIC[result.comp as string]) || result.comp || tag || DEFAULT_TAG - return +function updateState(store: IStore) { + let data = store.o + if (isDef(data)) { + const DIC = store.components + const DEFAULT_TAG = 'div' + if (isFn(store.filter)) { + if (isDef((data = store.filter(data)))) { + store.d = { data: data.data || data } + store.i.i = + (DIC && DIC[data.comp]) || data.comp || store.tag || DEFAULT_TAG + } else { + store.i.i = status.empty + } + } else { + store.d = { data } + store.i.i = (DIC && DIC[store.tag as string]) || store.tag || DEFAULT_TAG } + } else { + store.i.i = status.empty } - - state.i.i = status.empty -} -function get(state: IState) { - state.i.i = status.loading - // eslint-disable-next-line prefer-promise-reject-errors - ;(state.get ? state.get() : Promise.reject()) - .then(data => { - state.o = data - watch(state) - }) - .catch(err => { - state.i.i = - (isFn(state.error) ? (state.error as any)(err) : state.error) || - status.error - }) } - -// get和error改变走get否则watch -const DIC_PROPS = { - get: 1, - error: 1, - filter: 1, - once: 1, - tag: 1, - components: 1, -} -const DIC_EVENTS = { - none: status.none, - loading: status.loading, - error: status.error, - empty: status.empty, - success: status.success, -} -function init(state: IState, context: RenderContext) { - const { - props, - data: { attrs = {}, on = {} }, - } = context - - let fun - let isSame = true // 是否全部未变化 - - let prop - let target - /// props/attrs /// - for (prop in DIC_PROPS) { - if ((target = props[prop] || attrs[prop]) !== (state as any)[prop]) { - isSame = false - fun || ((prop === 'get' || prop === 'error') && (fun = get)) - } - ;(state as any)[prop] = target - attrs[prop] && (attrs[prop] = null) // 防止(特别是function)toString到dom属性 - } - - /// on /// - for (prop in DIC_EVENTS) { - state.f[DIC_EVENTS[prop as keyof typeof DIC_EVENTS] as status] = on[prop] +function fetchData(store: IStore) { + function onError(err?: Error) { + store.i.i = + (isFn(store.error) ? store.error(err) : store.error) || status.error } - - if (isSame) { - if (state.get || state.error) { - return state.once - } - state.o = 1 // for use slot + if (store.g) { + store.i.i = status.loading + ;(store.g > 1 + ? (store as any).fetch.apply(store, store.args as any[]) + : (store as any).get() + ) + .then((data: any) => { + store.o = data + updateState(store) + }) + .catch(onError) + } else if (store.error) { + onError() + } else { + store.o = 1 + updateState(store) } - - return (fun || watch)(state) } - const activated = 'hook:activated' const deactivated = 'hook:deactivated' const destroyed = 'hook:destroyed' -function getState(vm: Vue, key: any) { +function getStore(vm: Vue, key: any) { const CACHE = (vm as any)._$c || ((vm as any)._$c = {}) - let state: IState = CACHE[key] - if (!state) { - state = CACHE[key] = { - f: {}, + let store: IStore = CACHE[key] + if (!store) { + store = CACHE[key] = { i: Vue.observable({ i: status.loading, d: 0 as 0 | 1 }), h: { $: () => { - get(state) + fetchData(store) }, a: () => { - state.i.d = 0 + store.i.d = 0 }, d: () => { - state.i.d = 1 + store.i.d = 1 }, }, } // LifeCycle hooks for parent - vm.$on(activated, state.h.a) - vm.$on(deactivated, state.h.d) + vm.$on(activated, store.h.a) + vm.$on(deactivated, store.h.d) vm.$on(destroyed, () => { - delete CACHE[key] + delete (vm as any)._$c }) } - return state + return store +} +// [注意下面每项的顺序]get/(fetch + args)和error改变走fetchData否则updateState +const PROPS = [ + /* 0 */ 'once', // 无需响应 + /* 1 */ 'components', + /* 2 */ 'filter', + /* 3 */ 'tag', + /* 4 */ 'args', // diff + /* 5 */ 'error', + /* 6 */ 'fetch', + /* 7 */ 'get', +] +const EVENTS = { + none: status.none, + loading: status.loading, + error: status.error, + empty: status.empty, + success: status.success, } -function call(hooks: IState['f'][status], context: RenderContext) { - if (!hooks) { - return +function diff(store: IStore, context: RenderContext) { + const props = context.props + let attrs = context.data.attrs + let maxChangedIndex = 0 // 有变化prop的最大索引 + + let prop + let target + let index = PROPS.length + while (index--) { + prop = PROPS[index] + target = props[prop] + if (attrs && attrs[prop]) { + target || (target = attrs[prop]) + attrs[prop] = null // 防止(特别是function)toString到dom属性 + } + + if (target !== (store as any)[prop]) { + if (index === 4 /** args */) { + if (store.fetch && !isEqual((store as any)[prop], target)) { + ;(store as any)[prop] = clone(target) + maxChangedIndex = 6 + } + } else { + ;(store as any)[prop] = target + maxChangedIndex < index && (maxChangedIndex = index) + } + } } + if ((target = context.data.on)) { + attrs = store.f || (store.f = {}) + for (prop in EVENTS) { + attrs[(EVENTS as any)[prop]] = target[prop] + } + } + + switch (maxChangedIndex) { + case 0: // 无改变 + return store.c === store.i.i ? store.once : updateState(store) + case 6: // fetch + args + store.g = isFn(store.fetch) ? 2 : isFn(store.get) && 1 + return fetchData(store) + case 7: // get + store.g = isFn(store.get) ? 1 : isFn(store.fetch) && 2 + return fetchData(store) + case 5: // error + return fetchData(store) + default: + return updateState(store) + } +} +function trigger(hooks: hooks | undefined, context: RenderContext) { if (Array.isArray(hooks)) { for (const fn of hooks) { fn(context) } - } else { + } else if (hooks) { hooks(context) } } const DEFAULT_KEY = String.fromCharCode(0) -const DIC_SLOT = { +export const SLOTS = { [status.none]: 'none', [status.loading]: 'loading', [status.error]: 'error', @@ -227,82 +249,73 @@ const DIC_SLOT = { /** 异步选择器组件(functional), 最终渲染组件将得到一个prop: data, 即异步结果 * 【相同父组件(functional当然不算)存在多个选择器时, 必须提供key作为唯一标识】 * - * props: 见: interface state 注释 【注意】: get/error 变化时会重新请求 - * - * events: 见: const enum status 键值 + * props: 见: interface IStore 注释 【注意】: 响应任意prop变化 * * slots: 见: const enum status 键值, 支持对应作用域插槽/插槽【二选一】作用域插槽优先(二者都有时无法确定顺序, 故) * + * events: 见: const enum status 键值 + * * 示例: * * * ( import 了咋没得文档呢, 因为tsx么... ┐(: ´ ゞ`)┌ ) */ export default (context: RenderContext) => { - // identify/mark this functional component by/to parent._$c use key - let temp // 工具人 - let data // 工具人 + let temp // 大工具人 + let data // 小工具人 temp = context.parent data = context.data - const state = getState( + const store = getStore( temp, hasOwnProperty(data, 'key') ? data.key : (data.key = DEFAULT_KEY) ) - // situations to use VNode cache - if (state.i.d || (temp.$el && !temp.$el.parentNode)) { - return state.n // 父/此组件失活等 + if (store.i.d || (temp.$el && !temp.$el.parentNode) || diff(store, context)) { + return store.n // 父/此组件失活/离开,自身未变化&once等 } - // init state with diff - temp = init(state, context) - let Comp: any = state.i.i - if (temp && Comp === state.c) { - return state.n // once & 自身未变化 - } - - // LifeCycle hooks for Comp & emit events - state.c = Comp temp = data.on || (data.on = {}) - temp.$ = state.h.$ // reload - setHook(temp, activated, state.h.a) - setHook(temp, deactivated, state.h.d) - call(state.f[Comp as status] || state.f[status.success], context) + temp.$ = store.h.$ // $emit('$')刷新 + setHook(temp, activated, store.h.a) + setHook(temp, deactivated, store.h.d) + + let Comp: any = (store.c = store.i.i) + store.f && + trigger(store.f[Comp as status] || store.f[status.success], context) - // save & return VNode let slot - if ((slot = DIC_SLOT[Comp as keyof typeof DIC_SLOT])) { - // scopedSlots first + if ((slot = (SLOTS as any)[Comp])) { slot = context.scopedSlots[slot] - ? context.scopedSlots[slot](state.d) + ? context.scopedSlots[slot](store.d) : context.slots()[slot] if (slot && slot.length) { data = data.key + Comp - Comp = state.tag - return (state.n = ( + Comp = store.tag + return (store.n = ( {slot} )) } } + switch (Comp) { case status.none: - return (state.n = ( + return (store.n = ( )) case status.loading: - return (state.n = ) + return (store.n = ) case status.empty: - return (state.n = ( + return (store.n = ( { /> )) case status.error: - return (state.n = ) + return (store.n = ) default: - // add props: data data.props = context.props - data.props.data = state.d.data + store.d && (data.props.data = store.d.data) slot = context.scopedSlots slot = slot.success || slot.default - return (state.n = ( + return (store.n = ( {slot - ? slot(state.d) + ? slot(store.d) : (slot = context.slots()).success || slot.default} )) diff --git a/src/components/File.tsx b/src/components/File.tsx index 795a388..de6c235 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -12,7 +12,9 @@ import { Component, Vue, Prop } from 'vue-property-decorator' // import { getAsync } from '@/utils/highOrder' // import STYLE from './index.module.scss' import ElLink from 'element-ui/lib/link' + import storeFile, { STATE, ITask } from '@/store/file' +import { isEqual } from '@/utils' /// 常量(UPPER_CASE),单例/变量(camelCase),函数(无副作用,camelCase) /// // const ModuleOne: any = getAsync(() => @@ -34,11 +36,11 @@ export default class extends Vue { @Prop() readonly text?: string /** 是否禁用 */ @Prop() readonly disabled?: boolean - /** 图标(TODO: 默认取文件名后缀) */ - @Prop({ default: 'el-icon-document' }) readonly icon!: string + /** 图标(默认取文件名后缀) */ + @Prop() readonly icon?: string /// [data] (attr: string = '响应式属性' // 除了 undefined) /// private isSleep = false // 是否失活/休眠 - private isDel = 0 + private task = { state: STATE.pause } as ITask // 当前下载任务信息 /// 非响应式属性 (attr?: string // undefined) /// private $_vnode?: VNode /// [computed] (get attr() {} set attr(){}) /// @@ -47,22 +49,28 @@ export default class extends Vue { throw new Error('File: 必须重写store以提供文件管理数据仓库!') } - protected get task() { - let task: any = this.isDel - this.store.ADD_TASK({ - task: { - task: { - url: this.href, - query: this.query, - name: this.fileName, - }, - state: STATE.pause, - }, - callback(t) { - task = t - }, - }) - return task as ITask + protected get $_icon() { + const icon = this.icon + if (icon) { + return icon + } + + return 'el-icon-document' + + // TODO: 文件类型图标 + // !(icon = this.task.type) && + // (icon = this.fileName) && + // (icon = icon.substring(icon.lastIndexOf('.') + 1 || icon.length)) + + // switch (icon) { + // case 'doc': + // case 'docx': + // return 'i-doc' + // case 'pdf': + // return 'i-pdf' + // default: + // return 'el-icon-document' + // } } /// [LifeCycle] (private beforeCreate(){}/.../destroyed(){}) /// @@ -77,18 +85,30 @@ export default class extends Vue { /// [watch] (@Watch('attr') onAttrChange(val, oldVal) {}) /// /// [methods] (method(){}) /// protected load() { - this.store.SET_STATE({ - task: this.task as ITask, - state: STATE.loading, + this.store.ADD_TASK({ + task: { + url: this.href, + query: this.query, + name: this.fileName, + }, + callback: task => { + if (task === this.task) { + this.store.SET_STATE({ task, state: STATE.loading }) + } else { + this.task = task + } + }, }) } protected save() { - this.store.SAVE(this.task as ITask) - } - - protected reLoad() { - this.isDel++ + const task = this.task + // 不允许重命名文件 this.fileName === task.name + if (this.href === task.url && isEqual(this.query, task.query)) { + this.store.SAVE(task) + } else { + this.load() + } } // see: https://github.com/vuejs/jsx#installation @@ -99,16 +119,14 @@ export default class extends Vue { } const text = this.text || this.$slots.default - const icon = this.icon // TODO: 未指定图标时根据文件名后缀设置图标 - switch (this.task.state) { case STATE.del: return (this.$_vnode = ( {text} @@ -129,7 +147,7 @@ export default class extends Vue { return (this.$_vnode = ( @@ -151,7 +169,7 @@ export default class extends Vue { // eslint-disable-next-line no-fallthrough default: return (this.$_vnode = ( - + {text} )) diff --git a/src/components/Image.tsx b/src/components/Image.tsx index b904979..b52aa92 100644 --- a/src/components/Image.tsx +++ b/src/components/Image.tsx @@ -5,7 +5,7 @@ */ import { CreateElement, VNode } from 'vue' // see: https://github.com/kaorun343/vue-property-decorator -import { Component, Vue, Prop } from 'vue-property-decorator' +import { Component, Vue, Prop, Watch } from 'vue-property-decorator' /// [import] vue组件,其他,CSS Module /// // import { getAsync } from '@/utils/highOrder' @@ -32,11 +32,11 @@ export default class extends Vue { @Prop() readonly query?: IObject /** 同 */ @Prop() readonly alt?: string - /** 滚动容器选择器(document.querySelector), 若设置则懒加载 */ + /** 滚动容器选择器(document.querySelector), 若设置则懒加载【不响应prop变化】 */ @Prop() readonly el?: string /// [data] (attr: string = '响应式属性' // 除了 undefined) /// private isSleep = false // 是否失活/休眠 - private isDel = 0 + private task = { state: STATE.wait } as ITask // 当前下载任务信息 /// 非响应式属性 (attr?: string // undefined) /// private $_vnode?: VNode /// [computed] (get attr() {} set attr(){}) /// @@ -45,25 +45,13 @@ export default class extends Vue { throw new Error('Image: 必须重写store以提供图片管理数据仓库!') } - protected get task() { - let task - this.isDel && - this.store.LOAD({ - task: { url: this.src, query: this.query }, - callback(t) { - task = t - }, - }) - return (task as any) as ITask - } - /// [LifeCycle] (private beforeCreate(){}/.../destroyed(){}) /// private created() { if (this.el) { const el = document.querySelector(this.el) if (el) { const onScroll = debounce(() => { - if (this.isDel) { + if (this.task.id) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return removeListener() } @@ -79,7 +67,7 @@ export default class extends Vue { const x = dom.offsetLeft // + (dom.offsetWidth >> 1) if (y > top && y < bottom && x > left && x < right) { - this.isDel++ + this.load() } }, 99) const removeListener = () => { @@ -90,7 +78,7 @@ export default class extends Vue { return this.$nextTick(onScroll) } } - this.isDel++ + this.load() } private activated() { @@ -103,8 +91,15 @@ export default class extends Vue { /// [watch] (@Watch('attr') onAttrChange(val, oldVal) {}) /// /// [methods] (method(){}) /// - protected reLoad() { - this.isDel++ + @Watch('src') + @Watch('query', { deep: true }) + protected load() { + this.store.LOAD({ + task: { url: this.src, query: this.query }, + callback: task => { + this.task = task + }, + }) } // see: https://github.com/vuejs/jsx#installation @@ -112,7 +107,7 @@ export default class extends Vue { private render(h: CreateElement) { const task = this.task if (this.isSleep || !task) { - return this.$_vnode || (this.$_vnode = ) + return this.$_vnode } switch (task.state) { @@ -123,7 +118,7 @@ export default class extends Vue { type="info" msg={this.alt} retry="重新加载图片" - on={{ $: this.reLoad }} + on={{ $: this.load }} /> )) case STATE.wait: @@ -149,7 +144,7 @@ export default class extends Vue { )) default: diff --git a/src/components/RouterViewTransparent.ts b/src/components/RouterViewTransparent.ts index 5a0b669..7642122 100644 --- a/src/components/RouterViewTransparent.ts +++ b/src/components/RouterViewTransparent.ts @@ -8,12 +8,14 @@ import { Component } from 'vue' /// [import] vue组件,其他,CSS Module /// // import { getAsync } from '@/utils/highOrder' // import STYLE from './index.module.scss' +import CONFIG from '@/config' import getKey from '@/utils/getKey' /// 常量(UPPER_CASE),单例/变量(camelCase),函数(无副作用,camelCase) /// // const ModuleOne: any = getAsync(() => // import(/* webpackChunkName: "ihOne" */ './ModuleOne') // ) +const max = CONFIG.subPage > 1 ? CONFIG.subPage : 1 /** 透明分发路由(支持嵌套) * 可以给个key防止复用: @@ -26,6 +28,7 @@ export default { data() { return { d: 0 } // 是否失活/离开 }, + props: ['route'], beforeRouteUpdate(this: any, to, from, next) { this.d = 0 setTimeout(next) @@ -46,12 +49,12 @@ export default { } const exclude = this.$router.$.e - const meta = this.$route.meta + const meta = (this.route || this.$route).meta - return (this.c = h('KeepAlive', { props: { max: 5, exclude } }, [ + return (this.c = h('KeepAlive', { props: { max, exclude } }, [ h( 'RouterView', - { key: meta.k || (meta.k = getKey()) }, + { key: meta.k || (meta.k = getKey('v')) }, this.$slots.default ), ])) diff --git a/src/config/api/user.ts b/src/config/api/user.ts deleted file mode 100644 index 29608cf..0000000 --- a/src/config/api/user.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * @Description: 用户接口字典 - * @Author: 毛瑞 - * @Date: 2019-06-28 16:11:40 - */ -export default { - /*! 【用户接口】 */ - - /*! 获取登陆验证码 */ - /** 获取登陆验证码 - */ - verify: '', - - /*! 用户登陆 */ - /** 用户登陆 - */ - login: '', - - /*! 用户注销登陆 */ - /** 用户注销登陆 - */ - logout: '', -} diff --git a/src/config/index.ts b/src/config/index.ts index b6a9b50..0f04f91 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -7,37 +7,42 @@ export default { /*! 【全局配置】 */ /*! 请求接口基础路径(hash路由建议相对路径, 比如'api') */ - /** 请求接口基础路径(hash路由建议相对路径, 比如'api') - */ + /** 请求接口基础路径(hash路由建议相对路径, 比如'api') */ baseUrl: process.env.BASE_PATH, /*! 接口请求超时 0表示不限制 */ - /** 接口请求超时 0表示不限制 - */ + /** 接口请求超时 0表示不限制 */ timeout: 30 * 1000, /*! 全局接口响应缓存最大数量 */ - /** 全局接口响应缓存最大数量 - */ + /** 全局接口响应缓存最大数量 */ apiMaxCache: 66, /*! 全局接口响应缓存最大存活时间 */ - /** 全局接口响应缓存最大存活时间 - */ + /** 全局接口响应缓存最大存活时间 */ apiCacheAlive: 30 * 1000, /*! token header 字段 */ - /** token header 字段 - */ + /** token header 字段 */ token: 'token', /*! token 有效期(小时 服务端响应?) */ - /** token 有效期(小时 服务端响应?) - */ + /** token 有效期(小时 服务端响应?) */ tokenAlive: 24 * 7, /*! 本地偏好存储键 */ - /** 本地偏好存储键 - */ + /** 本地偏好存储键 */ prefer: 'prefer', + + /*! 最大页面缓存数 */ + /** 最大页面缓存数 */ + page: 9, + + /*! 最大子页面缓存数 */ + /** 最大子页面缓存数 */ + subPage: 5, + + /*! 最大页面缓存时间 */ + /** 最大页面缓存时间 */ + pageAlive: 30 * 1000, } diff --git a/src/enum/api.ts b/src/enum/api.ts new file mode 100644 index 0000000..4ef8cca --- /dev/null +++ b/src/enum/api.ts @@ -0,0 +1,9 @@ +/** 用户登录相关接口 */ +export const enum user { + /** 获取登陆验证码 */ + verify = 'verify', + /** 用户登陆 */ + login = 'login', + /** 用户注销登陆 */ + logout = 'logout', +} diff --git a/src/pages/index/App.vue b/src/pages/index/App.vue index d468a78..d425096 100644 --- a/src/pages/index/App.vue +++ b/src/pages/index/App.vue @@ -23,7 +23,7 @@ mode="out-in" > @@ -62,6 +62,7 @@ import { Component, Vue } from 'vue-property-decorator' import { RouteConfig } from 'vue-router' +import CONFIG from '@/config' import statePrefer from '@index/store/modules/prefer' @Component @@ -70,7 +71,7 @@ export default class extends Vue { private showNav = false // computed - get LINK() { + private get LINK() { const LINK = [] const ROUTE = (this.$router as any).options.routes as RouteConfig[] @@ -85,17 +86,21 @@ export default class extends Vue { return LINK } - get SKIN() { + private get SKIN() { return ['light', 'dark'] } - get skin() { + private get skin() { return statePrefer.skin } - set skin(skin: string) { + private set skin(skin: string) { statePrefer.SET_SKIN(skin) } + + private get max() { + return CONFIG.page > 1 ? CONFIG.page : 1 + } } diff --git a/src/pages/index/api/charts.ts b/src/pages/index/api/charts.ts index 13f05eb..86fc5b5 100644 --- a/src/pages/index/api/charts.ts +++ b/src/pages/index/api/charts.ts @@ -1,5 +1,5 @@ import { get as xhrGet } from '@/utils/ajax' -import API from '@index/config/api/charts' +import { charts as API } from '@index/enum/api' /** 获取饼图数据 * @param {boolean} noCache 禁用缓存 diff --git a/src/pages/index/config/api/charts.ts b/src/pages/index/config/api/charts.ts deleted file mode 100644 index 5c1a348..0000000 --- a/src/pages/index/config/api/charts.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default { - pie: '', - line: '', -} diff --git a/src/pages/index/config/route/index.ts b/src/pages/index/config/route/index.ts index b7a578e..b0e29aa 100644 --- a/src/pages/index/config/route/index.ts +++ b/src/pages/index/config/route/index.ts @@ -10,7 +10,15 @@ import IMG_HOME from '@index/assets/home.png' import IMG_ABOUT from '@index/assets/about.png' export default { - /*! 【index页路由配置(https://router.vuejs.org/zh/api/#router-构建选项)】 */ + /*! 【index页路由配置(https://router.vuejs.org/zh/api/#router-构建选项)】 + * meta: { + * name 对应标题/位置导航 + * thumb 缩略图 + * noCache 是否不缓存 + * refresh 是否需要刷新 + * pageAlive 最大缓存时间 + * } + */ mode: 'hash', meta: { diff --git a/src/pages/index/enum/api.ts b/src/pages/index/enum/api.ts new file mode 100644 index 0000000..af9b09d --- /dev/null +++ b/src/pages/index/enum/api.ts @@ -0,0 +1,5 @@ +/** 查询图表数据 */ +export const enum charts { + pie = 'pie', + line = 'line', +} diff --git a/src/pages/index/main.ts b/src/pages/index/main.ts index c255319..69565d5 100644 --- a/src/pages/index/main.ts +++ b/src/pages/index/main.ts @@ -9,6 +9,7 @@ import Vue from 'vue' import App from './App' import { dev } from '@/utils' +import { on, off, once, emit } from '@/utils/eventBus' // import { throttle } from '@/utils/performance' import './registerServiceWorker' @@ -18,26 +19,33 @@ import '@/scss/transitions.scss?skin=' /* ---------------------- 我是一条分割线 (灬°ω°灬) ---------------------- */ -/// 错误日志采集 /// +let temp + +/// eventBus 注入 /// +temp = Vue.prototype +temp.on = on +temp.off = off +temp.once = once +temp.emit = emit + +/// 埋点 /// // if (process.env.NODE_ENV === 'production') { // Vue.config.errorHandler = function(err, vm, info) { // // 采集并上传错误日志 // } +// const data: IObject[] = [] +// window.addEventListener( +// 'mousemove', +// throttle((e: MouseEvent) => { +// // 页面地址&鼠标位置 可用于统计(比如热力图)用户关注的页面及功能 +// data.push({ url: location.href, x: e.pageX, y: e.pageY }) +// }, 3000) +// ) +// window.addEventListener('beforeunload', () => { +// submit(data) // 上传数据 +// }) // } -/// 埋点 /// -// const data: IObject[] = [] -// window.addEventListener( -// 'mousemove', -// throttle((e: MouseEvent) => { -// // 页面地址&鼠标位置 可用于统计(比如热力图)用户关注的页面及功能 -// data.push({ url: location.href, x: e.pageX, y: e.pageY }) -// }, 3000) -// ) -// window.addEventListener('beforeunload', () => { -// submit(data) // 上传数据 -// }) - dev(Vue) // 防阻塞页面(defer的脚本已缓存时不会非阻塞执行bug:chromium#717979) setTimeout(() => { @@ -47,8 +55,8 @@ setTimeout(() => { // render: (h: CreateElement): VNode => h(App), // }).$mount('#app') // hack: 省root组件 - const options = App.options || App - options.store = store - options.router = router + temp = App.options || App + temp.store = store + temp.router = router new Vue(App).$mount('#app') }) diff --git a/src/pages/index/router.ts b/src/pages/index/router.ts index b9a161b..604a494 100644 --- a/src/pages/index/router.ts +++ b/src/pages/index/router.ts @@ -4,8 +4,9 @@ * @Date: 2019-06-18 15:58:46 */ import Vue from 'vue' -import Router, { RouterOptions, RouteConfig, Route, Location } from 'vue-router' +import Router, { RouteConfig, Route, Location } from 'vue-router' +import CONFIG from '@/config' import configRoute from '@iRoute' // 使用别名 import getKey from '@/utils/getKey' import { cancel } from '@/utils/ajax' @@ -13,6 +14,34 @@ import { cancel } from '@/utils/ajax' import NProgress from 'nprogress' import 'nprogress/nprogress.css' +export interface IMeta { + /** 标题/面包屑 */ + name: string + /** 缩略图 */ + thumb: string + /** 是否需要刷新 */ + refresh?: boolean + /** 是否不缓存 */ + noCache?: boolean + /** 页面最大缓存时间 */ + pageAlive?: number + /** 滚动位置 */ + y?: number + x?: number + /** 是否有权访问 */ + _: boolean + /** pageAlive setTimeout id */ + t?: number + /** 嵌套路由自动加key用以标识 */ + k?: number +} +declare global { + /** 路由对象 */ + interface IRoute extends Route { + meta: IMeta + } +} + const META = configRoute.meta /// 路由元数据 /// ;(function hack(list?: RouteConfig[]) { @@ -28,11 +57,11 @@ const META = configRoute.meta meta._ = true // 有权访问 或者移除无权的 hack(route.children) } -})(configRoute.routes as RouteConfig[]) +})(configRoute.routes) // scrollBehavior 不能处理指定元素的滚动 -const router = new Router(configRoute as RouterOptions) -;(router as any).$ = Vue.observable({ e: null }) // hack 刷新路由 +const router = new Router(configRoute) +;(router as any).$ = Vue.observable({ e: 0 }) // hack 刷新路由 /// 路由刷新 /// /** @@ -57,7 +86,7 @@ function restoreName(this: any) { (temp = temp.Ctor.options) && (temp.name = this._$a) } -function refreshRoute(route: Route) { +function refreshRoute(route: Route, noTop?: 1) { if (route.matched) { route = route.matched as any // 最后一个match一定是当前路由 route = (route as any)[(route as any).length - 1] @@ -65,10 +94,11 @@ function refreshRoute(route: Route) { let temp: any let instance: any + const instances = (route as any).instances const HOOK = 'hook:beforeDestroy' - for (temp in (route as any).instances) { + for (temp in instances) { if ( - (instance = (route as any).instances[temp]) && + (instance = instances[temp]) && (temp = (temp = instance.$vnode.componentOptions) && temp.Ctor.options) ) { if (!instance._$a) { @@ -79,11 +109,11 @@ function refreshRoute(route: Route) { } } - // 没实例 刷她爸爸/整个网页 + // 没实例(已经渲染了,劫持render也没用) 刷她爸爸/整个网页 instance || - ((route as any).parent - ? refreshRoute((route as any).parent) - : location.reload()) + ((temp = (route as any).parent) // eslint-disable-line no-cond-assign + ? refreshRoute(temp, noTop) + : noTop || location.reload()) } /// 导航守卫 /// @@ -91,20 +121,17 @@ const REG_REDIRECT = /\/r\// router.beforeEach((to, from, next) => { let temp if (!to.matched.length) { - // 没有匹配的路由 if (!from.matched.length) { - next(META.home) - return + return next(META.home) } if (REG_REDIRECT.test((temp = to.redirectedFrom || to.fullPath))) { temp = temp.replace(REG_REDIRECT, '/') if (temp === (from.redirectedFrom || from.fullPath)) { - refreshRoute(from) - return + return refreshRoute(from) } // 重定向并刷新 if ((to = router.resolve(temp).route).matched.length) { - refreshRoute(to) + refreshRoute(to, 1) next(to as Location) // 还是要再进一次beforeEach, 虽然都给解析出来了┐(: ´ ゞ`)┌ } } @@ -115,8 +142,27 @@ router.beforeEach((to, from, next) => { from.matched.length || next(META.home) return } + NProgress.start() // 开始进度条 cancel('导航: 取消未完成请求') + // 缓存控制 + if ((temp = to.meta).noCache) { + refreshRoute(to, 1) + } else { + if (temp.refresh) { + refreshRoute(to, 1) + temp.refresh = 0 + } + if (temp.t) { + clearTimeout(temp.t) + temp.t = 0 + } + temp = from.meta.pageAlive || CONFIG.pageAlive + temp && + (from.meta.t = setTimeout(() => { + from.meta.refresh = 1 + }, temp)) + } // 关闭所有提示 // temp = router.app // temp.$message.closeAll() @@ -124,8 +170,8 @@ router.beforeEach((to, from, next) => { // try { // temp.$msgbox.close() // } catch (error) {} + // 记录离开前的滚动位置 // if ((temp = temp.$el) && (temp = temp.querySelector('.el-main'))) { - // // 记录离开前的滚动位置 // from.meta.x = temp.scrollLeft // from.meta.y = temp.scrollTop // } @@ -142,7 +188,7 @@ router.beforeEach((to, from, next) => { // function restoreScrollPosition(this: Vue) { // const container = this.$root.$el.querySelector('.el-main') // if (container) { -// const meta = this.$route.meta +// const meta = ((this as any).route || this.$route).meta // container.scrollLeft = meta.x // container.scrollTop = meta.y // } diff --git a/src/pages/index/views/Home/index.tsx b/src/pages/index/views/Home/index.tsx index 21aa8eb..de609a3 100644 --- a/src/pages/index/views/Home/index.tsx +++ b/src/pages/index/views/Home/index.tsx @@ -3,7 +3,6 @@ * @Author: 毛瑞 * @Date: 2019-07-09 16:08:07 */ -// import { on } from '@/utils/eventBus' // 全局消息总线 import { getAsync } from '@/utils/highOrder' // 高阶组件工具 import PageHeader from '@indexCom/PageHeader' // 页面标题 diff --git a/src/pages/other/App.vue b/src/pages/other/App.vue index 1abac15..2d901bb 100644 --- a/src/pages/other/App.vue +++ b/src/pages/other/App.vue @@ -29,7 +29,7 @@ mode="out-in" > @@ -173,24 +173,29 @@ diff --git a/src/pages/other/config/route/index.ts b/src/pages/other/config/route/index.ts index d96e75f..799ace5 100644 --- a/src/pages/other/config/route/index.ts +++ b/src/pages/other/config/route/index.ts @@ -7,28 +7,26 @@ import { getAsync } from '@/utils/highOrder' // 高阶组件工具 // import RouterViewTransparent from '@com/RouterViewTransparent' export default { - /*! 【other页路由配置(https://router.vuejs.org/zh/api/#router-构建选项)】 */ + /*! 【other页路由配置(https://router.vuejs.org/zh/api/#router-构建选项)】 + * meta: { + * name 对应标题/位置导航 + * noCache 是否不缓存 + * refresh 是否需要刷新 + * pageAlive 最大缓存时间 + * } + */ mode: 'hash', meta: { - /*! 默认页 */ - /** 默认页 - */ - home: '/home', - - /*! 标题 */ - /** 标题 - */ - name: 'vue-tpl', + /*! 默认页 */ home: '/home', + /*! 标题 */ name: 'vue-tpl', }, routes: [ { /*! 首页 */ path: '/home', - meta: { - name: '首页', // 标题 - }, + meta: { name: '首页' }, component: getAsync(() => import(/* webpackChunkName: "oHome" */ '@other/views/Home') ), @@ -64,9 +62,7 @@ export default { /*! 关于 */ path: '/about', - meta: { - name: '关于', - }, + meta: { name: '关于' }, component: getAsync(() => import(/* webpackChunkName: "oAbout" */ '@other/views/About') ), diff --git a/src/pages/other/main.ts b/src/pages/other/main.ts index 5bbaeb4..3027e79 100644 --- a/src/pages/other/main.ts +++ b/src/pages/other/main.ts @@ -3,12 +3,14 @@ * @Author: 毛瑞 * @Date: 2019-06-18 15:58:46 */ +// import './checkLogin' // 未登录尽快跳转 import Vue from 'vue' import router from './router' import store from './store' import App from './App' import { dev } from '@/utils' +import { on, off, once, emit } from '@/utils/eventBus' // import { throttle } from '@/utils/performance' import './registerServiceWorker' @@ -35,6 +37,8 @@ import Autocomplete from 'element-ui/lib/autocomplete' import Select from 'element-ui/lib/select' import Option from 'element-ui/lib/option' import OptionGroup from 'element-ui/lib/option-group' +import Checkbox from 'element-ui/lib/checkbox' +import CheckboxGroup from 'element-ui/lib/checkbox-group' // 提示 import Tooltip from 'element-ui/lib/tooltip' import Popover from 'element-ui/lib/popover' @@ -52,17 +56,17 @@ import '@/scss/base.scss?skin=' // 基础样式 import './scss/main.scss' // 全局皮肤样式 // hack: 不出现滚动条时不显示 -let options = Scrollbar.options || Scrollbar -const created = options.components.Bar.created -options.components.Bar.created = function() { +let temp = (Scrollbar.options || Scrollbar).components.Bar +const created = temp.created +temp.created = function() { created && created.apply(this, arguments) this.$watch('size', function(this: any, size: string) { this.$el.style.display = size && size !== '0' ? '' : 'none' }) } // hack: 表单重设初始值(initialValue) -options = FormItem.options || FormItem -options.mounted = function() { +temp = FormItem.options || FormItem +temp.mounted = function() { if (this.prop) { this.dispatch('ElForm', 'el.form.addField', [this]) Array.isArray((this.initialValue = this.fieldValue)) && @@ -70,7 +74,7 @@ options.mounted = function() { this.addValidateEvents() } } -options.methods.setIni = function(model: IObject) { +temp.methods.setIni = function(model: IObject) { for (const field of this.fields) { field.initialValue = model[field.prop] } @@ -79,6 +83,8 @@ options.methods.setIni = function(model: IObject) { // hack: ElDialog props dragable 默认(true)允许拖拽 elDialogDragable(Dialog) +temp = Vue.prototype + // 布局 Vue.use(Row) Vue.use(Col) @@ -90,7 +96,7 @@ Vue.use(Button) Vue.use(ButtonGroup) Vue.use(Link) Vue.use(Loading.directive) -Vue.prototype.$loading = Loading.service +temp.$loading = Loading.service Vue.use(Divider) Vue.use(Card) // 弹窗&表单 @@ -102,40 +108,46 @@ Vue.use(Autocomplete) Vue.use(Select) Vue.use(Option) Vue.use(OptionGroup) +Vue.use(Checkbox) +Vue.use(CheckboxGroup) // 提示 Vue.use(Tooltip) Vue.use(Popover) -Vue.prototype.$msgbox = MessageBox -Vue.prototype.$alert = MessageBox.alert -Vue.prototype.$confirm = MessageBox.confirm -Vue.prototype.$prompt = MessageBox.prompt -Vue.prototype.$notify = Notification -Vue.prototype.$message = Message +temp.$msgbox = MessageBox +temp.$alert = MessageBox.alert +temp.$confirm = MessageBox.confirm +temp.$prompt = MessageBox.prompt +temp.$notify = Notification +temp.$message = Message // 滚动面板 Vue.use(Scrollbar) /* ---------------------- 我是一条分割线 (灬°ω°灬) ---------------------- */ -/// 错误日志采集 /// +/// eventBus 注入 /// +temp.on = on +temp.off = off +temp.once = once +temp.emit = emit + +/// 埋点 /// // if (process.env.NODE_ENV === 'production') { // Vue.config.errorHandler = function(err, vm, info) { // // 采集并上传错误日志 // } +// const data: IObject[] = [] +// window.addEventListener( +// 'mousemove', +// throttle((e: MouseEvent) => { +// // 页面地址&鼠标位置 可用于统计(比如热力图)用户关注的页面及功能 +// data.push({ url: location.href, x: e.pageX, y: e.pageY }) +// }, 3000) +// ) +// window.addEventListener('beforeunload', () => { +// submit(data) // 上传数据 +// }) // } -/// 埋点 /// -// const data: IObject[] = [] -// window.addEventListener( -// 'mousemove', -// throttle((e: MouseEvent) => { -// // 页面地址&鼠标位置 可用于统计(比如热力图)用户关注的页面及功能 -// data.push({ url: location.href, x: e.pageX, y: e.pageY }) -// }, 3000) -// ) -// window.addEventListener('beforeunload', () => { -// submit(data) // 上传数据 -// }) - dev(Vue) // 防阻塞页面(defer的脚本已缓存时不会非阻塞执行bug:chromium#717979) setTimeout(() => { @@ -145,8 +157,8 @@ setTimeout(() => { // render: (h: CreateElement) => h(App), // }).$mount('#app') // hack: 省root组件 - options = App.options || App - options.store = store - options.router = router + temp = App.options || App + temp.store = store + temp.router = router new Vue(App).$mount('#app') }) diff --git a/src/pages/other/router.ts b/src/pages/other/router.ts index b7eb793..0eb18a6 100644 --- a/src/pages/other/router.ts +++ b/src/pages/other/router.ts @@ -4,8 +4,9 @@ * @Date: 2019-06-18 15:58:46 */ import Vue from 'vue' -import Router, { RouterOptions, RouteConfig, Route, Location } from 'vue-router' +import Router, { RouteConfig, Route, Location } from 'vue-router' +import CONFIG from '@/config' import configRoute from '@oRoute' // 使用别名 import getKey from '@/utils/getKey' import { cancel } from '@/utils/ajax' @@ -13,6 +14,30 @@ import { cancel } from '@/utils/ajax' import NProgress from 'nprogress' import 'nprogress/nprogress.css' +export interface IMeta { + /** 标题/面包屑 */ + name: string + /** 是否需要刷新 */ + refresh?: boolean + /** 是否不缓存 */ + noCache?: boolean + /** 滚动位置 */ + y?: number + x?: number + /** 是否有权访问 */ + _: boolean + /** pageAlive setTimeout id */ + t?: number + /** 嵌套路由自动加key用以标识 */ + k?: number +} +declare global { + /** 路由对象 */ + interface IORoute extends Route { + meta: IMeta + } +} + const META = configRoute.meta /// 路由元数据 /// ;(function hack(list?: RouteConfig[]) { @@ -28,11 +53,11 @@ const META = configRoute.meta meta._ = true // 有权访问 或者移除无权的 hack(route.children) } -})(configRoute.routes as RouteConfig[]) +})(configRoute.routes) // scrollBehavior 不能处理指定元素的滚动 -const router = new Router(configRoute as RouterOptions) -;(router as any).$ = Vue.observable({ e: null }) // hack 刷新路由 +const router = new Router(configRoute) +;(router as any).$ = Vue.observable({ e: 0 }) // hack 刷新路由 /// 路由刷新 /// /** @@ -57,7 +82,7 @@ function restoreName(this: any) { (temp = temp.Ctor.options) && (temp.name = this._$a) } -function refreshRoute(route: Route) { +function refreshRoute(route: Route, noTop?: 1) { if (route.matched) { route = route.matched as any // 最后一个match一定是当前路由 route = (route as any)[(route as any).length - 1] @@ -65,10 +90,11 @@ function refreshRoute(route: Route) { let temp: any let instance: any + const instances = (route as any).instances const HOOK = 'hook:beforeDestroy' - for (temp in (route as any).instances) { + for (temp in instances) { if ( - (instance = (route as any).instances[temp]) && + (instance = instances[temp]) && (temp = (temp = instance.$vnode.componentOptions) && temp.Ctor.options) ) { if (!instance._$a) { @@ -79,11 +105,11 @@ function refreshRoute(route: Route) { } } - // 没实例 刷她爸爸/整个网页 + // 没实例(已经渲染了,劫持render也没用) 刷她爸爸/整个网页 instance || - ((route as any).parent - ? refreshRoute((route as any).parent) - : location.reload()) + ((temp = (route as any).parent) // eslint-disable-line no-cond-assign + ? refreshRoute(temp, noTop) + : noTop || location.reload()) } /// 导航守卫 /// @@ -91,20 +117,17 @@ const REG_REDIRECT = /\/r\// router.beforeEach((to, from, next) => { let temp if (!to.matched.length) { - // 没有匹配的路由 if (!from.matched.length) { - next(META.home) - return + return next(META.home) } if (REG_REDIRECT.test((temp = to.redirectedFrom || to.fullPath))) { temp = temp.replace(REG_REDIRECT, '/') if (temp === (from.redirectedFrom || from.fullPath)) { - refreshRoute(from) - return + return refreshRoute(from) } // 重定向并刷新 if ((to = router.resolve(temp).route).matched.length) { - refreshRoute(to) + refreshRoute(to, 1) next(to as Location) // 还是要再进一次beforeEach, 虽然都给解析出来了┐(: ´ ゞ`)┌ } } @@ -115,8 +138,27 @@ router.beforeEach((to, from, next) => { from.matched.length || next(META.home) return } + NProgress.start() // 开始进度条 cancel('导航: 取消未完成请求') + // 缓存控制 + if ((temp = to.meta).noCache) { + refreshRoute(to, 1) + } else { + if (temp.refresh) { + refreshRoute(to, 1) + temp.refresh = 0 + } + if (temp.t) { + clearTimeout(temp.t) + temp.t = 0 + } + temp = from.meta.pageAlive || CONFIG.pageAlive + temp && + (from.meta.t = setTimeout(() => { + from.meta.refresh = 1 + }, temp)) + } // 关闭所有提示 temp = router.app temp.$message.closeAll() @@ -124,8 +166,8 @@ router.beforeEach((to, from, next) => { try { temp.$msgbox.close() } catch (error) {} + // 记录离开前的滚动位置 // if ((temp = temp.$el) && (temp = temp.querySelector('.el-main'))) { - // // 记录离开前的滚动位置 // from.meta.x = temp.scrollLeft // from.meta.y = temp.scrollTop // } @@ -142,7 +184,7 @@ router.beforeEach((to, from, next) => { // function restoreScrollPosition(this: Vue) { // const container = this.$root.$el.querySelector('.el-main') // if (container) { -// const meta = this.$route.meta +// const meta = ((this as any).route || this.$route).meta // container.scrollLeft = meta.x // container.scrollTop = meta.y // } diff --git a/src/pages/other/scss/main.scss b/src/pages/other/scss/main.scss index 30c34fe..d9d46bd 100644 --- a/src/pages/other/scss/main.scss +++ b/src/pages/other/scss/main.scss @@ -26,6 +26,8 @@ @import '~element-ui/packages/theme-chalk/src/select'; @import '~element-ui/packages/theme-chalk/src/option'; @import '~element-ui/packages/theme-chalk/src/option-group'; +@import '~element-ui/packages/theme-chalk/src/checkbox'; +@import '~element-ui/packages/theme-chalk/src/checkbox-group'; // 提示 @import '~element-ui/packages/theme-chalk/src/tooltip'; @import '~element-ui/packages/theme-chalk/src/popover'; diff --git a/src/shims-modules.d.ts b/src/shims-modules.d.ts index 3c6245c..1cf5ee3 100644 --- a/src/shims-modules.d.ts +++ b/src/shims-modules.d.ts @@ -1,8 +1,4 @@ -/* - * @Description: 模块申明 - * @Author: 毛瑞 - * @Date: 2019-07-09 16:19:51 - */ +/* 模块申明 */ declare module '*.module.scss' { /** css 模块 @@ -92,6 +88,13 @@ declare module '*.json' { // declare module '@luma.gl/addons' declare module '*' +declare type Falsy = false | 0 | 0n | '' | null | undefined // | NaN +/** 任意对象 */ +declare interface IObject { + [key: string]: T + [key: number]: T +} + // hack ECharts for switch skin declare namespace echarts { // eslint-disable-next-line @typescript-eslint/interface-name-prefix @@ -107,12 +110,3 @@ declare namespace echarts { ): void } } - -/** 任意对象 - */ -declare interface IObject { - [key: string]: T - [key: number]: T -} - -declare type Falsy = false | 0 | 0n | '' | null | undefined // | NaN diff --git a/src/shims-tsx.d.ts b/src/shims-tsx.d.ts index ac04abb..0c7d170 100644 --- a/src/shims-tsx.d.ts +++ b/src/shims-tsx.d.ts @@ -1,8 +1,4 @@ -/* - * @Description: tsx 支持 - * @Author: 毛瑞 - * @Date: 2019-06-18 15:58:46 - */ +/* tsx 申明 */ // see: https://github.com/wonderful-panda/vue-tsx-support import 'vue-tsx-support/enable-check' diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts index 477174d..0f82ae0 100644 --- a/src/shims-vue.d.ts +++ b/src/shims-vue.d.ts @@ -1,9 +1,7 @@ -/* - * @Description: vue 扩展 - * @Author: 毛瑞 - * @Date: 2019-07-09 17:15:16 - */ -import Vue, { VNode } from 'vue' +/* vue 扩展申明 */ + +import { VNode } from 'vue' +import { Handler } from '@/utils/eventBus' type type = 'success' | 'warning' | 'info' | 'error' type action = 'confirm' | 'cancel' | 'close' @@ -110,11 +108,63 @@ interface IMessage extends Message { declare module 'vue/types/vue' { // eslint-disable-next-line @typescript-eslint/interface-name-prefix interface Vue { - /** 默认绑定