master
toesbieya 6 years ago
parent d375bf5619
commit 2e835ae853
  1. 85
      vue/element-ui-personal/components/Menu/MenuItem.vue
  2. 488
      vue/element-ui-personal/components/Menu/Submenu.vue
  3. 152
      vue/element-ui-personal/components/Message/main.vue
  4. 1408
      vue/element-ui-personal/components/Select/Select.vue
  5. 74
      vue/element-ui-personal/components/Select/SelectDropdown.vue
  6. 6
      vue/src/App.vue
  7. 34
      vue/src/components/AbstractForm/AbstractFormItem.vue
  8. 36
      vue/src/components/AbstractForm/index.vue
  9. 56
      vue/src/components/AbstractTable/index.vue
  10. 72
      vue/src/components/Charts/PieChart.vue
  11. 54
      vue/src/components/CollapseCard/index.vue
  12. 28
      vue/src/components/ColorCheckBox/group.vue
  13. 64
      vue/src/components/ColorCheckBox/index.vue
  14. 12
      vue/src/components/ContextMenu/ContextMenuItem.vue
  15. 132
      vue/src/components/ContextMenu/index.vue
  16. 84
      vue/src/components/Empty/DefaultEmptyImage.vue
  17. 54
      vue/src/components/Empty/index.vue
  18. 40
      vue/src/components/ExtraArea/index.vue
  19. 96
      vue/src/components/FormDialog/index.vue
  20. 28
      vue/src/components/LinerProgress/index.vue
  21. 32
      vue/src/components/LoadingMask/index.vue
  22. 382
      vue/src/components/RegionSelector/Tab/index.vue
  23. 136
      vue/src/components/RegionSelector/Tree/TreeDialog.vue
  24. 120
      vue/src/components/RegionSelector/Tree/index.vue
  25. 22
      vue/src/components/RegionSelector/index.vue
  26. 36
      vue/src/components/SearchForm/SearchFormItem.vue
  27. 210
      vue/src/components/SearchForm/index.vue
  28. 100
      vue/src/components/Skeleton/PageSkeleton.vue
  29. 78
      vue/src/components/Skeleton/index.vue
  30. 74
      vue/src/components/SvgIcon/index.vue
  31. 182
      vue/src/components/TinymceEditor/index.vue
  32. 70
      vue/src/components/ZoomControl/index.vue
  33. 56
      vue/src/components/biz/CategoryTree/index.vue
  34. 50
      vue/src/components/biz/UserSelector/SimpleMultipleUserSelector.vue
  35. 6
      vue/src/components/biz/doc/DocDetailFooter.vue
  36. 32
      vue/src/components/biz/doc/DocDetailHeader.vue
  37. 62
      vue/src/globalMethods/BottomTip/main.vue
  38. 358
      vue/src/globalMethods/Guide/main.vue
  39. 512
      vue/src/globalMethods/PuzzleVerify/main.vue
  40. 256
      vue/src/globalMethods/SignautreBoard/main.vue
  41. 20
      vue/src/layout/components/Footer.vue
  42. 132
      vue/src/layout/components/Header.vue
  43. 116
      vue/src/layout/components/Main.vue
  44. 44
      vue/src/layout/components/Navbar/components/Bell.vue
  45. 50
      vue/src/layout/components/Navbar/components/Breadcrumb.vue
  46. 38
      vue/src/layout/components/Navbar/components/Hamburger.vue
  47. 228
      vue/src/layout/components/Navbar/components/HeaderSearch.vue
  48. 106
      vue/src/layout/components/Navbar/components/SettingDrawer.vue
  49. 90
      vue/src/layout/components/Navbar/index.vue
  50. 34
      vue/src/layout/components/Sidebar/components/Logo.vue
  51. 106
      vue/src/layout/components/Sidebar/components/SidebarItem.vue
  52. 40
      vue/src/layout/components/Sidebar/components/SidebarItemContent.vue
  53. 254
      vue/src/layout/components/Sidebar/index.vue
  54. 132
      vue/src/layout/components/TagsView/ScrollPane.vue
  55. 286
      vue/src/layout/components/TagsView/index.vue
  56. 94
      vue/src/layout/index.vue
  57. 166
      vue/src/views/app/403.vue
  58. 326
      vue/src/views/app/404.vue
  59. 156
      vue/src/views/app/login/RegisterForm.vue
  60. 84
      vue/src/views/app/login/SetAnimation.vue
  61. 88
      vue/src/views/app/login/index.vue
  62. 24
      vue/src/views/app/redirect.vue
  63. 30
      vue/src/views/example/components/regionSelectorExample.vue
  64. 6
      vue/src/views/example/components/rippleExample.vue
  65. 26
      vue/src/views/example/components/signatureExample.vue
  66. 54
      vue/src/views/example/components/skeletonExample.vue
  67. 42
      vue/src/views/example/components/treeSelectExample.vue
  68. 76
      vue/src/views/example/components/uploadExample.vue
  69. 76
      vue/src/views/example/cool/fluid.vue
  70. 36
      vue/src/views/example/cool/l2d.vue
  71. 12
      vue/src/views/example/detailPage.vue
  72. 76
      vue/src/views/example/developingTest/components/ApiSpeedTest.vue
  73. 76
      vue/src/views/example/developingTest/components/ExportExcelTest.vue
  74. 278
      vue/src/views/example/developingTest/components/ImageCompressTest.vue
  75. 66
      vue/src/views/example/icons/index.vue
  76. 90
      vue/src/views/example/stylePage.vue
  77. 146
      vue/src/views/index/components/DailyFinishOrderStat.vue
  78. 160
      vue/src/views/index/components/DailyProfitStat.vue
  79. 148
      vue/src/views/index/components/PanelGroup/PanelGroupItem.vue
  80. 96
      vue/src/views/index/components/PanelGroup/index.vue
  81. 100
      vue/src/views/index/components/TotalProfitGoods.vue
  82. 16
      vue/src/views/index/index.vue
  83. 324
      vue/src/views/message/manage/EditDialog.vue
  84. 212
      vue/src/views/message/manage/index.vue
  85. 90
      vue/src/views/message/user/AutoHidden.vue
  86. 274
      vue/src/views/message/user/MessageStream.vue
  87. 74
      vue/src/views/message/user/index.vue
  88. 156
      vue/src/views/purchase/inbound/components/OrderSelector.vue
  89. 224
      vue/src/views/purchase/inbound/detail.vue
  90. 132
      vue/src/views/purchase/order/components/CategorySelector.vue
  91. 156
      vue/src/views/purchase/order/components/SupplierSelector.vue
  92. 270
      vue/src/views/purchase/order/detail.vue
  93. 112
      vue/src/views/sell/order/components/CustomerSelector.vue
  94. 110
      vue/src/views/sell/order/components/StockSelector.vue
  95. 256
      vue/src/views/sell/order/detail.vue
  96. 156
      vue/src/views/sell/outbound/components/OrderSelector.vue
  97. 102
      vue/src/views/sell/outbound/components/StockSelector.vue
  98. 174
      vue/src/views/stock/current/DetailDialog.vue
  99. 260
      vue/src/views/stock/current/index.vue
  100. 222
      vue/src/views/system/category/components/Form.vue
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,19 +1,20 @@
<template> <template>
<li <li
class="el-menu-item" class="el-menu-item"
:style="paddingStyle" :style="paddingStyle"
:class="{'is-active': active,'is-disabled': disabled}" :class="{'is-active': active,'is-disabled': disabled}"
@click="handleClick" @click="handleClick"
> >
<el-tooltip <el-tooltip
v-if="parentMenu.$options.componentName === 'ElMenu' && rootMenu.collapse && $slots.title" v-if="parentMenu.$options.componentName === 'ElMenu' && rootMenu.collapse && $slots.title"
effect="dark" effect="dark"
placement="right" placement="right"
> >
<div slot="content"> <div slot="content">
<slot name="title"/> <slot name="title"/>
</div> </div>
<div style="position: absolute;left: 0;top: 0;height: 100%;width: 100%;display: inline-block;box-sizing: border-box;padding: 0 20px;"> <div
style="position: absolute;left: 0;top: 0;height: 100%;width: 100%;display: inline-block;box-sizing: border-box;padding: 0 20px;">
<slot/> <slot/>
</div> </div>
</el-tooltip> </el-tooltip>
@ -26,49 +27,49 @@
</template> </template>
<script> <script>
import MenuMixin from 'element-ui/packages/menu/src/menu-mixin' import MenuMixin from 'element-ui/packages/menu/src/menu-mixin'
import Emitter from 'element-ui/lib/mixins/emitter' import Emitter from 'element-ui/lib/mixins/emitter'
export default { export default {
name: 'ElMenuItem', name: 'ElMenuItem',
componentName: 'ElMenuItem', componentName: 'ElMenuItem',
mixins: [MenuMixin, Emitter], mixins: [MenuMixin, Emitter],
data() { data() {
return {} return {}
}, },
props: { props: {
index: String, index: String,
route: [String, Object], route: [String, Object],
disabled: Boolean disabled: Boolean
}, },
computed: { computed: {
active() { active() {
return this.index === this.rootMenu.activeIndex return this.index === this.rootMenu.activeIndex
} }
}, },
methods: { methods: {
handleClick() { handleClick() {
if (!this.disabled) { if (!this.disabled) {
this.dispatch('ElMenu', 'item-click', this) this.dispatch('ElMenu', 'item-click', this)
this.$emit('click', this) this.$emit('click', this)
}
} }
}, }
},
mounted() { mounted() {
this.parentMenu.addItem(this) this.parentMenu.addItem(this)
this.rootMenu.addItem(this) this.rootMenu.addItem(this)
}, },
beforeDestroy() { beforeDestroy() {
this.parentMenu.removeItem(this) this.parentMenu.removeItem(this)
this.rootMenu.removeItem(this) this.rootMenu.removeItem(this)
}
} }
}
</script> </script>

@ -1,276 +1,276 @@
<script type="text/jsx"> <script type="text/jsx">
import MenuMixin from 'element-ui/packages/menu/src/menu-mixin' import MenuMixin from 'element-ui/packages/menu/src/menu-mixin'
import Emitter from 'element-ui/lib/mixins/emitter' import Emitter from 'element-ui/lib/mixins/emitter'
import Popper from 'element-ui/lib/utils/vue-popper' import Popper from 'element-ui/lib/utils/vue-popper'
const PopperMixins = { const PopperMixins = {
props: { props: {
transformOrigin: {type: [Boolean, String], default: false}, transformOrigin: {type: [Boolean, String], default: false},
offset: Popper.props.offset, offset: Popper.props.offset,
boundariesPadding: Popper.props.boundariesPadding, boundariesPadding: Popper.props.boundariesPadding,
popperOptions: Popper.props.popperOptions popperOptions: Popper.props.popperOptions
}, },
data: Popper.data, data: Popper.data,
methods: Popper.methods, methods: Popper.methods,
beforeDestroy: Popper.beforeDestroy, beforeDestroy: Popper.beforeDestroy,
deactivated: Popper.deactivated deactivated: Popper.deactivated
} }
export default {
name: 'ElSubmenu',
componentName: 'ElSubmenu',
mixins: [MenuMixin, Emitter, PopperMixins],
props: {
index: String,
showTimeout: {type: Number, default: 300},
hideTimeout: {type: Number, default: 300},
popperClass: String,
disabled: Boolean,
popperAppendToBody: {type: Boolean, default: true}
},
data() {
return {
popperJS: null,
timeout: null,
items: {},
submenus: {},
mouseInChild: false
}
},
export default { computed: {
name: 'ElSubmenu', appendToBody() {
return this.popperAppendToBody === undefined
? this.isFirstLevel
: this.popperAppendToBody
},
menuTransitionName() {
return this.rootMenu.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top'
},
opened() {
return this.rootMenu.openedMenus.includes(this.index)
},
active() {
const items = this.items
componentName: 'ElSubmenu', const isActive = Object.keys(items).some(key => items[key].active)
mixins: [MenuMixin, Emitter, PopperMixins], if (!isActive) {
const submenus = this.submenus
return Object.keys(submenus).some(key => submenus[key].active)
}
props: { return isActive
index: String,
showTimeout: {type: Number, default: 300},
hideTimeout: {type: Number, default: 300},
popperClass: String,
disabled: Boolean,
popperAppendToBody: {type: Boolean, default: true}
}, },
mode() {
data() { return this.rootMenu.mode
return {
popperJS: null,
timeout: null,
items: {},
submenus: {},
mouseInChild: false
}
}, },
isMenuPopup() {
computed: { return this.rootMenu.isMenuPopup
appendToBody() { },
return this.popperAppendToBody === undefined isFirstLevel() {
? this.isFirstLevel const arr = ['ElSubmenu', 'ElMenuItemGroup']
: this.popperAppendToBody let isFirstLevel = true
}, let parent = this.$parent
menuTransitionName() {
return this.rootMenu.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top' while (parent && parent !== this.rootMenu) {
}, if (arr.includes(parent.$options.componentName)) {
opened() { isFirstLevel = false
return this.rootMenu.openedMenus.includes(this.index) break
},
active() {
const items = this.items
const isActive = Object.keys(items).some(key => items[key].active)
if (!isActive) {
const submenus = this.submenus
return Object.keys(submenus).some(key => submenus[key].active)
} }
else parent = parent.$parent
}
return isFirstLevel
}
},
return isActive watch: {
}, opened() {
mode() { this.isMenuPopup && this.$nextTick(this.updatePopper)
return this.rootMenu.mode }
}, },
isMenuPopup() {
return this.rootMenu.isMenuPopup methods: {
}, handleCollapseToggle(value) {
isFirstLevel() { if (value) {
const arr = ['ElSubmenu', 'ElMenuItemGroup'] this.initPopper()
let isFirstLevel = true }
let parent = this.$parent else {
this.doDestroy()
while (parent && parent !== this.rootMenu) {
if (arr.includes(parent.$options.componentName)) {
isFirstLevel = false
break
}
else parent = parent.$parent
}
return isFirstLevel
} }
}, },
watch: { addItem(item) {
opened() { this.$set(this.items, item.index, item)
this.isMenuPopup && this.$nextTick(this.updatePopper)
}
}, },
methods: { removeItem(item) {
handleCollapseToggle(value) { delete this.items[item.index]
if (value) { },
this.initPopper()
}
else {
this.doDestroy()
}
},
addItem(item) {
this.$set(this.items, item.index, item)
},
removeItem(item) {
delete this.items[item.index]
},
addSubmenu(item) {
this.$set(this.submenus, item.index, item)
},
removeSubmenu(item) {
delete this.submenus[item.index]
},
handleClick() {
const {rootMenu, disabled} = this
if (
(rootMenu.menuTrigger === 'hover' && rootMenu.mode === 'horizontal') ||
(rootMenu.collapse && rootMenu.mode === 'vertical') ||
disabled
) {
return
}
this.dispatch('ElMenu', 'submenu-click', this)
},
handleMouseenter(event, showTimeout = this.showTimeout) { addSubmenu(item) {
this.$set(this.submenus, item.index, item)
},
if (!('ActiveXObject' in window) && event.type === 'focus' && !event.relatedTarget) { removeSubmenu(item) {
return delete this.submenus[item.index]
} },
const {rootMenu, disabled} = this
if ( handleClick() {
(rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') || const {rootMenu, disabled} = this
(!rootMenu.collapse && rootMenu.mode === 'vertical') || if (
disabled (rootMenu.menuTrigger === 'hover' && rootMenu.mode === 'horizontal') ||
) { (rootMenu.collapse && rootMenu.mode === 'vertical') ||
return disabled
} ) {
this.dispatch('ElSubmenu', 'mouse-enter-child') return
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.rootMenu.openMenu(this.index, this.indexPath)
}, showTimeout)
if (this.appendToBody) {
this.$parent.$el.dispatchEvent(new MouseEvent('mouseenter'))
}
},
handleMouseleave(deepDispatch = false) {
const {rootMenu} = this
if (
(rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') ||
(!rootMenu.collapse && rootMenu.mode === 'vertical')
) {
return
}
this.dispatch('ElSubmenu', 'mouse-leave-child')
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
!this.mouseInChild && this.rootMenu.closeMenu(this.index)
}, this.hideTimeout)
if (this.appendToBody && deepDispatch) {
if (this.$parent.$options.name === 'ElSubmenu') {
this.$parent.handleMouseleave(true)
}
}
},
updatePlacement() {
this.placement = this.mode === 'horizontal' && this.isFirstLevel
? 'bottom-start'
: 'right-start'
},
initPopper() {
this.referenceElm = this.$el
this.popperElm = this.$refs.menu
this.updatePlacement()
} }
this.dispatch('ElMenu', 'submenu-click', this)
}, },
created() { handleMouseenter(event, showTimeout = this.showTimeout) {
this.$on('toggle-collapse', this.handleCollapseToggle)
this.$on('mouse-enter-child', () => { if (!('ActiveXObject' in window) && event.type === 'focus' && !event.relatedTarget) {
this.mouseInChild = true return
clearTimeout(this.timeout) }
}) const {rootMenu, disabled} = this
this.$on('mouse-leave-child', () => { if (
this.mouseInChild = false (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') ||
clearTimeout(this.timeout) (!rootMenu.collapse && rootMenu.mode === 'vertical') ||
}) disabled
) {
return
}
this.dispatch('ElSubmenu', 'mouse-enter-child')
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.rootMenu.openMenu(this.index, this.indexPath)
}, showTimeout)
if (this.appendToBody) {
this.$parent.$el.dispatchEvent(new MouseEvent('mouseenter'))
}
}, },
mounted() { handleMouseleave(deepDispatch = false) {
this.parentMenu.addSubmenu(this) const {rootMenu} = this
this.rootMenu.addSubmenu(this) if (
this.initPopper() (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') ||
(!rootMenu.collapse && rootMenu.mode === 'vertical')
) {
return
}
this.dispatch('ElSubmenu', 'mouse-leave-child')
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
!this.mouseInChild && this.rootMenu.closeMenu(this.index)
}, this.hideTimeout)
if (this.appendToBody && deepDispatch) {
if (this.$parent.$options.name === 'ElSubmenu') {
this.$parent.handleMouseleave(true)
}
}
}, },
beforeDestroy() { updatePlacement() {
this.parentMenu.removeSubmenu(this) this.placement = this.mode === 'horizontal' && this.isFirstLevel
this.rootMenu.removeSubmenu(this) ? 'bottom-start'
: 'right-start'
}, },
render() { initPopper() {
const {rootMenu, $slots} = this this.referenceElm = this.$el
this.popperElm = this.$refs.menu
const popupMenu = ( this.updatePlacement()
<transition name={this.menuTransitionName}> }
<div },
ref="menu"
v-show={this.opened} created() {
class={[`el-menu--${this.mode}`, this.popperClass]} this.$on('toggle-collapse', this.handleCollapseToggle)
on-mouseenter={e => this.handleMouseenter(e, 100)} this.$on('mouse-enter-child', () => {
on-mouseleave={() => this.handleMouseleave(true)} this.mouseInChild = true
on-focus={e => this.handleMouseenter(e, 100)}> clearTimeout(this.timeout)
<ul class={['el-menu el-menu--popup', `el-menu--popup-${this.currentPlacement}`]}> })
{$slots.default} this.$on('mouse-leave-child', () => {
</ul> this.mouseInChild = false
</div> clearTimeout(this.timeout)
</transition> })
) },
const inlineMenu = ( mounted() {
<el-collapse-transition> this.parentMenu.addSubmenu(this)
<ul v-show={this.opened} class="el-menu el-menu--inline"> this.rootMenu.addSubmenu(this)
this.initPopper()
},
beforeDestroy() {
this.parentMenu.removeSubmenu(this)
this.rootMenu.removeSubmenu(this)
},
render() {
const {rootMenu, $slots} = this
const popupMenu = (
<transition name={this.menuTransitionName}>
<div
ref="menu"
v-show={this.opened}
class={[`el-menu--${this.mode}`, this.popperClass]}
on-mouseenter={e => this.handleMouseenter(e, 100)}
on-mouseleave={() => this.handleMouseleave(true)}
on-focus={e => this.handleMouseenter(e, 100)}>
<ul class={['el-menu el-menu--popup', `el-menu--popup-${this.currentPlacement}`]}>
{$slots.default} {$slots.default}
</ul> </ul>
</el-collapse-transition> </div>
) </transition>
)
const submenuTitleIcon = (
rootMenu.mode === 'horizontal' && this.isFirstLevel || const inlineMenu = (
rootMenu.mode === 'vertical' && !rootMenu.collapse <el-collapse-transition>
) ? 'el-icon-arrow-down' : 'el-icon-arrow-right' <ul v-show={this.opened} class="el-menu el-menu--inline">
{$slots.default}
return ( </ul>
<li </el-collapse-transition>
class={{ )
'el-submenu': true,
'is-active': this.active, const submenuTitleIcon = (
'is-opened': this.opened, rootMenu.mode === 'horizontal' && this.isFirstLevel ||
'is-disabled': this.disabled rootMenu.mode === 'vertical' && !rootMenu.collapse
}} ) ? 'el-icon-arrow-down' : 'el-icon-arrow-right'
on-mouseenter={this.handleMouseenter}
on-mouseleave={() => this.handleMouseleave(false)} return (
on-focus={this.handleMouseenter} <li
class={{
'el-submenu': true,
'is-active': this.active,
'is-opened': this.opened,
'is-disabled': this.disabled
}}
on-mouseenter={this.handleMouseenter}
on-mouseleave={() => this.handleMouseleave(false)}
on-focus={this.handleMouseenter}
>
<div
ref="submenu-title"
class="el-submenu__title"
style={this.paddingStyle}
on-click={this.handleClick}
> >
<div {$slots.title}
ref="submenu-title" <i class={['el-submenu__icon-arrow', submenuTitleIcon]}/>
class="el-submenu__title" </div>
style={this.paddingStyle} {this.isMenuPopup ? popupMenu : inlineMenu}
on-click={this.handleClick} </li>
> )
{$slots.title}
<i class={['el-submenu__icon-arrow', submenuTitleIcon]}/>
</div>
{this.isMenuPopup ? popupMenu : inlineMenu}
</li>
)
}
} }
}
</script> </script>
<style> <style>

@ -14,94 +14,94 @@
<p v-else v-html="message" class="el-message__content"/> <p v-else v-html="message" class="el-message__content"/>
</slot> </slot>
<i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close"/> <i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close"/>
<div v-if="badge>1" :key="'badge'+badge" class="el-message__badge">{{badge}}</div> <div v-if="badge>1" :key="'badge'+badge" class="el-message__badge">{{ badge }}</div>
<div v-if="progress&&!closed" :key="'progress'+badge" class="el-message__progress" :style="progressStyle"/> <div v-if="progress&&!closed" :key="'progress'+badge" class="el-message__progress" :style="progressStyle"/>
</div> </div>
</transition> </transition>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return {
visible: false,
badge: 1,//1
progress: true,//
pause: false,//
message: '',
duration: 3000,
startAt: 0,//
remainTime: 0,//
type: '',//js
iconClass: '',
customClass: '',
onClose: null,
showClose: false,
closed: false,
verticalOffset: 20,
timer: null,
dangerouslyUseHTMLString: false,
center: false
}
},
computed: {
messageClass() {
return [
'el-message',
this.type && !this.iconClass ? `el-message--${this.type}` : '',
this.center ? 'is-center' : '',
this.showClose ? 'is-closable' : '',
this.customClass
]
},
typeClass() {
return this.type && !this.iconClass
? `el-message__icon el-icon-${this.type}`
: ''
},
positionStyle() {
return { return {
visible: false, 'top': `${this.verticalOffset}px`
badge: 1,//1
progress: true,//
pause: false,//
message: '',
duration: 3000,
startAt: 0,//
remainTime: 0,//
type: '',//js
iconClass: '',
customClass: '',
onClose: null,
showClose: false,
closed: false,
verticalOffset: 20,
timer: null,
dangerouslyUseHTMLString: false,
center: false
} }
}, },
computed: { progressStyle() {
messageClass() { if (!this.progress || this.closed) return 'transform: scaleX(0)'
return [ if (this.pause) return `transform: scaleX(${this.remainTime / this.duration});animation-play-state:paused`
'el-message', return `animation-duration: ${this.duration}ms;animation-play-state:running`
this.type && !this.iconClass ? `el-message--${this.type}` : '', }
this.center ? 'is-center' : '', },
this.showClose ? 'is-closable' : '', watch: {
this.customClass closed(newVal) {
] if (newVal) this.visible = false
}, }
typeClass() { },
return this.type && !this.iconClass methods: {
? `el-message__icon el-icon-${this.type}` handleAfterLeave() {
: '' this.$destroy(true)
}, this.$el.parentNode.removeChild(this.$el)
positionStyle() {
return {
'top': `${this.verticalOffset}px`
}
},
progressStyle() {
if (!this.progress || this.closed) return 'transform: scaleX(0)'
if (this.pause) return `transform: scaleX(${this.remainTime / this.duration});animation-play-state:paused`
return `animation-duration: ${this.duration}ms;animation-play-state:running`
}
}, },
watch: { close() {
closed(newVal) { this.closed = true
if (newVal) this.visible = false typeof this.onClose === 'function' && this.onClose(this)
}
}, },
methods: { clearTimer(reset) {
handleAfterLeave() { this.pause = true
this.$destroy(true) this.remainTime = reset === true ? this.duration : this.remainTime - (Date.now() - this.startAt)
this.$el.parentNode.removeChild(this.$el) window.clearTimeout(this.timer)
},
close() {
this.closed = true
typeof this.onClose === 'function' && this.onClose(this)
},
clearTimer(reset) {
this.pause = true
this.remainTime = reset === true ? this.duration : this.remainTime - (Date.now() - this.startAt)
window.clearTimeout(this.timer)
},
startTimer() {
if (this.duration > 0 && !this.closed) {
this.pause = false
this.startAt = Date.now()
if (this.remainTime === 0) this.remainTime = this.duration
this.timer = setTimeout(() => {
!this.closed && this.close()
}, this.remainTime)
}
}
}, },
mounted() { startTimer() {
this.startTimer() if (this.duration > 0 && !this.closed) {
this.pause = false
this.startAt = Date.now()
if (this.remainTime === 0) this.remainTime = this.duration
this.timer = setTimeout(() => {
!this.closed && this.close()
}, this.remainTime)
}
} }
},
mounted() {
this.startTimer()
} }
}
</script> </script>

File diff suppressed because it is too large Load Diff

@ -1,48 +1,48 @@
<template> <template>
<div <div
class="el-select-dropdown el-popper" class="el-select-dropdown el-popper"
:class="[{ 'is-multiple': $parent.multiple }, popperClass]" :class="[{ 'is-multiple': $parent.multiple }, popperClass]"
:style="{ minWidth }" :style="{ minWidth }"
> >
<slot/> <slot/>
</div> </div>
</template> </template>
<script type="text/babel"> <script type="text/babel">
import Popper from '@ele/mixins/vue-popper' import Popper from '@ele/mixins/vue-popper'
export default { export default {
name: 'ElSelectDropdown', name: 'ElSelectDropdown',
componentName: 'ElSelectDropdown', componentName: 'ElSelectDropdown',
mixins: [Popper], mixins: [Popper],
data() { data() {
return { return {
minWidth: '' minWidth: ''
} }
}, },
computed: { computed: {
popperClass() { popperClass() {
return this.$parent.popperClass return this.$parent.popperClass
} }
}, },
watch: { watch: {
'$parent.inputWidth'() { '$parent.inputWidth'() {
this.minWidth = this.$parent.$el.getBoundingClientRect().width + 'px' this.minWidth = this.$parent.$el.getBoundingClientRect().width + 'px'
}
},
mounted() {
this.referenceElm = this.$parent.$refs.reference.$el
this.$parent.popperElm = this.popperElm = this.$el
this.$on('updatePopper', () => {
this.$parent.visible && this.updatePopper()
})
this.$on('destroyPopper', this.destroyPopper)
} }
},
mounted() {
this.referenceElm = this.$parent.$refs.reference.$el
this.$parent.popperElm = this.popperElm = this.$el
this.$on('updatePopper', () => {
this.$parent.visible && this.updatePopper()
})
this.$on('destroyPopper', this.destroyPopper)
} }
}
</script> </script>

@ -7,7 +7,7 @@
</template> </template>
<script> <script>
export default { export default {
name: 'App' name: 'App'
} }
</script> </script>

@ -1,25 +1,25 @@
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: "AbstractFormItem", name: "AbstractFormItem",
functional: true, functional: true,
props: {full: Boolean, dense: Boolean, label: String, prop: String}, props: {full: Boolean, dense: Boolean, label: String, prop: String},
render(h, context) { render(h, context) {
const {full, dense, label, prop} = context.props const {full, dense, label, prop} = context.props
let attrs = {xs: 24, sm: 12, lg: 8} let attrs = {xs: 24, sm: 12, lg: 8}
if (full) attrs = {span: 24} if (full) attrs = {span: 24}
else if (dense) attrs.lg = 12 else if (dense) attrs.lg = 12
return ( return (
<el-col {...{attrs}}> <el-col {...{attrs}}>
<el-form-item label={label} prop={prop}> <el-form-item label={label} prop={prop}>
{context.children} {context.children}
</el-form-item> </el-form-item>
</el-col> </el-col>
) )
}
} }
}
</script> </script>

@ -1,23 +1,23 @@
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: "AbstractForm", name: "AbstractForm",
functional: true, functional: true,
render(h, context) { render(h, context) {
const {data, children} = context const {data, children} = context
return ( return (
<el-form <el-form
ref="form" ref="form"
label-position="right" label-position="right"
label-width="100px" label-width="100px"
size="small" size="small"
status-icon status-icon
{...data} {...data}
> >
<el-row>{children}</el-row> <el-row>{children}</el-row>
</el-form> </el-form>
) )
}
} }
}
</script> </script>

@ -1,36 +1,36 @@
<script type="text/jsx"> <script type="text/jsx">
import Empty from "@/components/Empty" import Empty from "@/components/Empty"
export default { export default {
name: "AbstractTable", name: "AbstractTable",
functional: true, functional: true,
render(h, context) { render(h, context) {
const {data, children, scopedSlots} = context const {data, children, scopedSlots} = context
if (!scopedSlots.empty) { if (!scopedSlots.empty) {
scopedSlots.empty = () => <Empty/> scopedSlots.empty = () => <Empty/>
}
return (
<el-table
ref="table"
current-row-key="id"
row-key="id"
highlight-current-row
{...data}
>
{children}
{
Object
.entries(scopedSlots)
.map(([name, slot]) => (
name !== 'default' && <slot slot={name}>{slot()}</slot>
))
}
</el-table>
)
} }
return (
<el-table
ref="table"
current-row-key="id"
row-key="id"
highlight-current-row
{...data}
>
{children}
{
Object
.entries(scopedSlots)
.map(([name, slot]) => (
name !== 'default' && <slot slot={name}>{slot()}</slot>
))
}
</el-table>
)
} }
}
</script> </script>

@ -3,47 +3,47 @@
</template> </template>
<script> <script>
import {logic, resize} from "@/mixins/chart" import {logic, resize} from "@/mixins/chart"
export default { export default {
mixins: [resize, logic], mixins: [resize, logic],
props: { props: {
title: String, title: String,
data: Array data: Array
}, },
watch: { watch: {
data: { data: {
deep: true, deep: true,
handler(val) { handler(val) {
if (!window.echarts) return if (!window.echarts) return
this.init(val) this.init(val)
}
} }
}, }
},
methods: { methods: {
init(data) { init(data) {
this.chart.setOption({ this.chart.setOption({
title: { title: {
text: this.title, text: this.title,
left: 'center', left: 'center',
align: 'right' align: 'right'
}, },
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: '{b} : {c} ({d}%)' formatter: '{b} : {c} ({d}%)'
}, },
series: [ series: [
{ {
type: 'pie', type: 'pie',
center: ['50%', '58%'], center: ['50%', '58%'],
data data
} }
] ]
}) })
}
} }
} }
}
</script> </script>

@ -1,7 +1,7 @@
<template> <template>
<el-card :class="{collapsed}"> <el-card :class="{collapsed}">
<div slot="header" class="clearfix"> <div slot="header" class="clearfix">
<slot name="header">{{header}}</slot> <slot name="header">{{ header }}</slot>
<i class="el-icon-arrow-up collapse-card-icon" @click="collapse"/> <i class="el-icon-arrow-up collapse-card-icon" @click="collapse"/>
</div> </div>
<slot/> <slot/>
@ -9,40 +9,40 @@
</template> </template>
<script> <script>
export default { export default {
name: "CollapseCard", name: "CollapseCard",
props: { props: {
header: String header: String
}, },
data() { data() {
return { return {
collapsed: false collapsed: false
} }
}, },
methods: { methods: {
collapse() { collapse() {
this.collapsed = !this.collapsed this.collapsed = !this.collapsed
}
} }
} }
}
</script> </script>
<style> <style>
.collapse-card-icon { .collapse-card-icon {
float: right; float: right;
font-weight: bold; font-weight: bold;
transition: transform .3s; transition: transform .3s;
cursor: pointer; cursor: pointer;
} }
.collapsed .collapse-card-icon { .collapsed .collapse-card-icon {
transform: rotate(180deg); transform: rotate(180deg);
} }
.el-card.collapsed > .el-card__body { .el-card.collapsed > .el-card__body {
display: none; display: none;
} }
</style> </style>

@ -5,25 +5,25 @@
</template> </template>
<script> <script>
export default { export default {
name: "ColorCheckGroup", name: "ColorCheckGroup",
provide() { provide() {
return { return {
colorCheckGroup: this colorCheckGroup: this
} }
}, },
props: {value: String}, props: {value: String},
data: () => ({children: []}) data: () => ({children: []})
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.color-checkbox-group { .color-checkbox-group {
@include clearfix @include clearfix
} }
</style> </style>

@ -5,44 +5,44 @@
</template> </template>
<script> <script>
export default { export default {
name: "ColorCheckBox", name: "ColorCheckBox",
inject: ['colorCheckGroup'], inject: ['colorCheckGroup'],
props: {color: String}, props: {color: String},
computed: { computed: {
checked() { checked() {
const group = this.colorCheckGroup const group = this.colorCheckGroup
return group && group.value === this.color return group && group.value === this.color
} }
}, },
methods: { methods: {
click() { click() {
if (this.checked) return if (this.checked) return
this.colorCheckGroup.$emit('input', this.color) this.colorCheckGroup.$emit('input', this.color)
} }
}, },
created() { created() {
const group = this.colorCheckGroup const group = this.colorCheckGroup
group && group.children.push(this.color) group && group.children.push(this.color)
}, },
} }
</script> </script>
<style> <style>
.color-checkbox { .color-checkbox {
float: left; float: left;
width: 20px; width: 20px;
height: 20px; height: 20px;
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
margin-right: 8px; margin-right: 8px;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
color: #fff; color: #fff;
} }
</style> </style>

@ -1,11 +1,11 @@
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: "ContextMenuItem", name: "ContextMenuItem",
functional: true, functional: true,
render(h, context) { render(h, context) {
return <li class="contextmenu-item" {...context.data}>{context.children}</li> return <li class="contextmenu-item" {...context.data}>{context.children}</li>
}
} }
}
</script> </script>

@ -5,87 +5,87 @@
</template> </template>
<script> <script>
export default { export default {
name: "ContextMenu", name: "ContextMenu",
props: { props: {
value: Boolean, value: Boolean,
left: Number, left: Number,
top: Number top: Number
}, },
data() { data() {
return { return {
realLeft: '0px', realLeft: '0px',
realTop: '0px' realTop: '0px'
} }
},
watch: {
value(v) {
document.body[v ? 'addEventListener' : 'removeEventListener']('click', this.closeContextMenu)
if (v) this.$nextTick(this.autoAdapt)
},
left(v) {
if (!v) return
this.autoAdaptLeft(v)
}, },
top(v) {
if (!v) return
this.autoAdaptTop(v)
}
},
watch: { methods: {
value(v) { closeContextMenu() {
document.body[v ? 'addEventListener' : 'removeEventListener']('click', this.closeContextMenu) this.$emit('input', false)
if (v) this.$nextTick(this.autoAdapt) },
}, autoAdapt() {
left(v) { this.autoAdaptTop(this.top)
if (!v) return this.autoAdaptLeft(this.left)
this.autoAdaptLeft(v) },
}, autoAdaptTop(v) {
top(v) { if (!this.value) return
if (!v) return if (this.$el.offsetHeight > document.body.clientHeight - v && v > this.$el.offsetHeight) {
this.autoAdaptTop(v) this.realTop = v - this.$el.offsetHeight + 'px'
} }
else this.realTop = v + 'px'
}, },
autoAdaptLeft(v) {
methods: { if (!this.value) return
closeContextMenu() { if (this.$el.offsetWidth > document.body.clientWidth - v) {
this.$emit('input', false) this.realLeft = v - this.$el.offsetWidth + 'px'
},
autoAdapt() {
this.autoAdaptTop(this.top)
this.autoAdaptLeft(this.left)
},
autoAdaptTop(v) {
if (!this.value) return
if (this.$el.offsetHeight > document.body.clientHeight - v && v > this.$el.offsetHeight) {
this.realTop = v - this.$el.offsetHeight + 'px'
}
else this.realTop = v + 'px'
},
autoAdaptLeft(v) {
if (!this.value) return
if (this.$el.offsetWidth > document.body.clientWidth - v) {
this.realLeft = v - this.$el.offsetWidth + 'px'
}
else this.realLeft = v + 'px'
} }
else this.realLeft = v + 'px'
} }
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.contextmenu { .contextmenu {
margin: 0; margin: 0;
background: $--menu-background-color; background: $--menu-background-color;
z-index: 10; z-index: 10;
position: absolute; position: absolute;
list-style-type: none; list-style-type: none;
padding: 5px 0; padding: 5px 0;
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
color: #fff; color: #fff;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
&-item { &-item {
margin: 0; margin: 0;
padding: 7px 16px; padding: 7px 16px;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: $--color-primary; color: $--color-primary;
}
} }
} }
}
</style> </style>

@ -1,51 +1,51 @@
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: 'DefaultEmptyImage', name: 'DefaultEmptyImage',
functional: true, functional: true,
render() { render() {
return ( return (
<svg width="184" height="152" viewBox="0 0 184 152" xmlns="http://www.w3.org/2000/svg"> <svg width="184" height="152" viewBox="0 0 184 152" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd"> <g fill="none" fillRule="evenodd">
<g transform="translate(24 31.67)"> <g transform="translate(24 31.67)">
<ellipse <ellipse
fillOpacity=".8" fillOpacity=".8"
fill="#F5F5F7" fill="#F5F5F7"
cx="67.797" cx="67.797"
cy="106.89" cy="106.89"
rx="67.797" rx="67.797"
ry="12.668" ry="12.668"
/> />
<path <path
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z" d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
fill="#AEB8C2" fill="#AEB8C2"
/> />
<path <path
d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z" d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z"
fill="url(#linearGradient-1)" fill="url(#linearGradient-1)"
transform="translate(13.56)" transform="translate(13.56)"
/> />
<path <path
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z" d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
fill="#F5F5F7" fill="#F5F5F7"
/> />
<path
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
fill="#DCE0E6"
/>
</g>
<path <path
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z" d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
fill="#DCE0E6" fill="#DCE0E6"
/> />
<g transform="translate(149.65 15.383)" fill="#FFF">
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815"/>
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z"/>
</g>
</g> </g>
</svg> <path
) d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
} fill="#DCE0E6"
/>
<g transform="translate(149.65 15.383)" fill="#FFF">
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815"/>
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z"/>
</g>
</g>
</svg>
)
} }
}
</script> </script>

@ -1,38 +1,38 @@
<script type="text/jsx"> <script type="text/jsx">
import DefaultEmptyImage from "./DefaultEmptyImage" import DefaultEmptyImage from "./DefaultEmptyImage"
export default { export default {
name: "AEmpty", name: "AEmpty",
functional: true, functional: true,
props: { props: {
description: String, description: String,
image: [String, Object], image: [String, Object],
imageStyle: [String, Object] imageStyle: [String, Object]
}, },
render(h, context) { render(h, context) {
const {description = '暂无数据', image, imageStyle} = context.props const {description = '暂无数据', image, imageStyle} = context.props
const prefixCls = 'ant-empty' const prefixCls = 'ant-empty'
const cls = {[prefixCls]: true} const cls = {[prefixCls]: true}
let imageNode = null let imageNode = null
if (typeof image === 'string') { if (typeof image === 'string') {
imageNode = <img alt={description} src={image}/> imageNode = <img alt={description} src={image}/>
}
else {
imageNode = <DefaultEmptyImage/>
}
return (
<div class={cls} {...{on: context.listeners}}>
<div class={`${prefixCls}-image`} style={imageStyle}>{imageNode}</div>
{description && <p class={`${prefixCls}-description`}>{description}</p>}
{context.children && <div class={`${prefixCls}-footer`}>{context.children}</div>}
</div>
)
} }
else {
imageNode = <DefaultEmptyImage/>
}
return (
<div class={cls} {...{on: context.listeners}}>
<div class={`${prefixCls}-image`} style={imageStyle}>{imageNode}</div>
{description && <p class={`${prefixCls}-description`}>{description}</p>}
{context.children && <div class={`${prefixCls}-footer`}>{context.children}</div>}
</div>
)
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -13,31 +13,31 @@
</template> </template>
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: "ExtraArea", name: "ExtraArea",
props: { props: {
extra: {type: Number, default: 5} extra: {type: Number, default: 5}
}, },
data() { data() {
return { return {
expanded: false expanded: false
} }
}, },
computed: { computed: {
extraSpan() { extraSpan() {
return this.expanded ? 0 : this.extra return this.expanded ? 0 : this.extra
}, },
defaultSpan() { defaultSpan() {
return 23 - this.extraSpan return 23 - this.extraSpan
}, },
iconClass() { iconClass() {
return `el-icon-arrow-${this.expanded ? 'right' : 'left'}` return `el-icon-arrow-${this.expanded ? 'right' : 'left'}`
}
} }
} }
}
</script> </script>

@ -1,54 +1,54 @@
<script type="text/jsx"> <script type="text/jsx">
import LoadingMask from '@/components/LoadingMask' import LoadingMask from '@/components/LoadingMask'
export default { export default {
name: "FormDialog", name: "FormDialog",
functional: true, functional: true,
props: { props: {
value: Boolean, value: Boolean,
title: String, title: String,
loading: {type: Boolean, default: true}, loading: {type: Boolean, default: true},
top: {type: String, default: '50px'}, top: {type: String, default: '50px'},
width: {type: String, default: '30%'} width: {type: String, default: '30%'}
}, },
render(h, context) { render(h, context) {
const {value, title, loading, top, width} = context.props const {value, title, loading, top, width} = context.props
const l = context.listeners const l = context.listeners
function onClose() { function onClose() {
l.input && l.input(false) l.input && l.input(false)
}
return (
<el-dialog
v-drag-dialog
visible={value}
custom-class="form-dialog"
top={top}
width={width}
on-close={onClose}
{...context.data}
>
<slot slot="title" name="title">{title}</slot>
<div class="form-dialog-container">
<LoadingMask show={loading}/>
<el-scrollbar>
<div class="form-dialog-body">
{context.children}
</div>
<div style="margin-bottom: 17px"/>
</el-scrollbar>
</div>
<slot slot="footer" name="footer"/>
</el-dialog>
)
} }
return (
<el-dialog
v-drag-dialog
visible={value}
custom-class="form-dialog"
top={top}
width={width}
on-close={onClose}
{...context.data}
>
<slot slot="title" name="title">{title}</slot>
<div class="form-dialog-container">
<LoadingMask show={loading}/>
<el-scrollbar>
<div class="form-dialog-body">
{context.children}
</div>
<div style="margin-bottom: 17px"/>
</el-scrollbar>
</div>
<slot slot="footer" name="footer"/>
</el-dialog>
)
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -1,23 +1,23 @@
<script type="text/jsx"> <script type="text/jsx">
//muse-uihttps://muse-ui.org/#/zh-CN/progress //muse-uihttps://muse-ui.org/#/zh-CN/progress
export default { export default {
name: "LinerProgress", name: "LinerProgress",
functional: true, functional: true,
props: {show: Boolean}, props: {show: Boolean},
render(h, context) { render(h, context) {
if (context.props.show) { if (context.props.show) {
return ( return (
<div class="liner-progress"> <div class="liner-progress">
<div class="liner-progress-background"/> <div class="liner-progress-background"/>
<div class="linear-progress-indeterminate"/> <div class="linear-progress-indeterminate"/>
</div> </div>
) )
}
} }
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -1,24 +1,24 @@
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: "LoadingMask", name: "LoadingMask",
functional: true, functional: true,
props: {show: Boolean}, props: {show: Boolean},
render(h, context) { render(h, context) {
if (context.props.show) { if (context.props.show) {
return ( return (
<div class="el-loading-mask"> <div class="el-loading-mask">
<div class="el-loading-spinner"> <div class="el-loading-spinner">
<svg viewBox="25 25 50 50" class="circular"> <svg viewBox="25 25 50 50" class="circular">
<circle cx="50" cy="50" r="20" fill="none" class="path"/> <circle cx="50" cy="50" r="20" fill="none" class="path"/>
</svg> </svg>
<p>加载中...</p> <p>加载中...</p>
</div>
</div> </div>
) </div>
} )
} }
} }
}
</script> </script>

@ -1,13 +1,13 @@
<template> <template>
<el-select <el-select
ref="select" ref="select"
:value="value" :value="value"
:disabled="readonly" :disabled="readonly"
:size="size" :size="size"
popper-append-to-body popper-append-to-body
clearable clearable
@clear="() => $emit('clear')" @clear="() => $emit('clear')"
@visible-change="visibleChange" @visible-change="visibleChange"
> >
<template slot="empty"> <template slot="empty">
<div class="rg-header"> <div class="rg-header">
@ -24,19 +24,19 @@
<div class="rg-search"> <div class="rg-search">
<input <input
ref="searchInput" ref="searchInput"
v-model="searchText" v-model="searchText"
class="rg-input" class="rg-input"
type="text" type="text"
placeholder="输入地区id或名称搜索" placeholder="输入地区id或名称搜索"
@input="search" @input="search"
> >
</div> </div>
<div class="rg-level-tabs"> <div class="rg-level-tabs">
<ul> <ul>
<li v-for="(tab,index) in selected" :key="tab.name" :class="{active:index+1 === currentLevel}"> <li v-for="(tab,index) in selected" :key="tab.name" :class="{active:index+1 === currentLevel}">
<a href="javascript:" @click="()=>clickTab(index,tab)">{{tab.name}}</a> <a href="javascript:" @click="()=>clickTab(index,tab)">{{ tab.name }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -44,12 +44,12 @@
<div v-loading="loading" class="rg-results-container"> <div v-loading="loading" class="rg-results-container">
<ul class="rg-results"> <ul class="rg-results">
<li <li
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
:class="{'rg-item': true, 'active': isItemActive(item.id)}" :class="{'rg-item': true, 'active': isItemActive(item.id)}"
@click="()=>clickItem(item)" @click="()=>clickItem(item)"
> >
{{limit ? item.name + `(${item.value})` : item.name}} {{ limit ? item.name + `(${item.value})` : item.name }}
</li> </li>
<li v-if="items.length === 0" class="rg-message-box">无匹配项目</li> <li v-if="items.length === 0" class="rg-message-box">无匹配项目</li>
</ul> </ul>
@ -59,216 +59,216 @@
</template> </template>
<script> <script>
import {debounce} from "@/utils" import {debounce} from "@/utils"
import {createLimitTree, getNodeId} from "@/utils/tree" import {createLimitTree, getNodeId} from "@/utils/tree"
import {store, init} from '../store' import {store, init} from '../store'
import common from '../mixin' import common from '../mixin'
const PROVINCE_CITIES = ['北京市', '天津市', '上海市', '重庆市'] const PROVINCE_CITIES = ['北京市', '天津市', '上海市', '重庆市']
const DEFAULT_TABS = [{name: '省/直辖市'}, {name: '市'}, {name: '区/县'}, {name: '乡/镇/街道'}] const DEFAULT_TABS = [{name: '省/直辖市'}, {name: '市'}, {name: '区/县'}, {name: '乡/镇/街道'}]
export default { export default {
name: "RegionTabSelector", name: "RegionTabSelector",
mixins: [common], mixins: [common],
props: {
value: [String, Array],
maxLevel: {type: Number, default: 3},
separation: {type: String, default: ','}
},
data() {
return {
loading: false,
searchText: '',
currentLevel: 1,
realMaxLevel: this.maxLevel,
selected: [],
treeData: [],
items: []
}
},
props: { computed: {
value: [String, Array], regionTree() {
maxLevel: {type: Number, default: 3}, return store.data
separation: {type: String, default: ','}
}, },
},
data() { watch: {
return { currentLevel(v) {
loading: false, this.setItems(v)
searchText: '', }
currentLevel: 1, },
realMaxLevel: this.maxLevel,
selected: [], methods: {
treeData: [], //select
items: [] visibleChange(v) {
if (v) {
this.transformValueToSelected()
this.$nextTick(() => this.$refs.searchInput.focus())
} }
else this.removeAll()
}, },
computed: { search() {
regionTree() { const parent = this.selected[this.currentLevel - 2] || {children: this.treeData}
return store.data
},
},
watch: { if (!this.searchText) return this.items = parent.children
currentLevel(v) {
this.setItems(v)
}
},
methods: {
//select
visibleChange(v) {
if (v) {
this.transformValueToSelected()
this.$nextTick(() => this.$refs.searchInput.focus())
}
else this.removeAll()
},
search() { const predicate = item => item.id.startsWith(this.searchText) || item.name.startsWith(this.searchText)
const parent = this.selected[this.currentLevel - 2] || {children: this.treeData}
if (!this.searchText) return this.items = parent.children this.items = parent.children.filter(predicate)
},
const predicate = item => item.id.startsWith(this.searchText) || item.name.startsWith(this.searchText) clickTab(index, tab) {
//tab
if (index + 1 === this.currentLevel) return
this.items = parent.children.filter(predicate) this.currentLevel = index + 1
}, },
clickTab(index, tab) { clickItem(item) {
//tab this.setTabs(item, this.currentLevel)
if (index + 1 === this.currentLevel) return
this.currentLevel = index + 1 this.nextTab()
}, },
clickItem(item) { setTabs(item, level) {
this.setTabs(item, this.currentLevel) const prev = this.selected.slice(0, level - 1)
prev.push(item)
const next = JSON.parse(JSON.stringify(DEFAULT_TABS))
this.nextTab() if (level === 1) {
}, //-1''tab
if (PROVINCE_CITIES.includes(item.name)) {
next.splice(1, 1)
this.realMaxLevel = this.maxLevel - 1
}
else this.realMaxLevel = this.maxLevel
}
setTabs(item, level) { this.selected = prev.concat(next.slice(level, this.realMaxLevel))
const prev = this.selected.slice(0, level - 1) },
prev.push(item)
const next = JSON.parse(JSON.stringify(DEFAULT_TABS))
if (level === 1) { nextTab() {
//-1''tab this.searchText = ''
if (PROVINCE_CITIES.includes(item.name)) { //
next.splice(1, 1) if (this.currentLevel >= this.realMaxLevel) {
this.realMaxLevel = this.maxLevel - 1 this.done()
} }
else this.realMaxLevel = this.maxLevel else this.currentLevel++
} },
this.selected = prev.concat(next.slice(level, this.realMaxLevel)) //
}, isItemActive(id) {
const selected = this.selected[this.currentLevel - 1]
return selected && selected.id === id
},
nextTab() { //tab
this.searchText = '' setItems(level) {
// //
if (this.currentLevel >= this.realMaxLevel) { if (level === 1) this.items = this.treeData
this.done()
}
else this.currentLevel++
},
//
isItemActive(id) {
const selected = this.selected[this.currentLevel - 1]
return selected && selected.id === id
},
//tab
setItems(level) {
//
if (level === 1) this.items = this.treeData
//children
else {
const parent = this.selected[level - 2]
this.items = parent ? parent.children || [] : []
}
},
//value //children
transformValueToSelected() { else {
if (!this.value) return const parent = this.selected[level - 2]
this.items = parent ? parent.children || [] : []
}
},
const result = [], //value
loopArray = Array.isArray(this.value) transformValueToSelected() {
? this.value if (!this.value) return
: this.value.split(this.separation).filter(Boolean)
for (let i = 0; i < loopArray.length; i++) { const result = [],
const str = loopArray[i], loopArray = Array.isArray(this.value)
parent = result[i - 1] || {children: this.treeData} ? this.value
: this.value.split(this.separation).filter(Boolean)
const node = parent.children.find(predicate(str)) for (let i = 0; i < loopArray.length; i++) {
//退 const str = loopArray[i],
if (!node) break parent = result[i - 1] || {children: this.treeData}
result.push(node) const node = parent.children.find(predicate(str))
} //退
if (!node) break
result.forEach((item, index) => this.setTabs(item, index + 1)) result.push(node)
}, }
removeAll() { result.forEach((item, index) => this.setTabs(item, index + 1))
this.realMaxLevel = this.maxLevel },
this.selected = JSON.parse(JSON.stringify(DEFAULT_TABS))
this.currentLevel = 1
this.searchText = ''
this.setItems(this.currentLevel)
},
done() { removeAll() {
const result = this.selected.filter(item => item.id) this.realMaxLevel = this.maxLevel
this.$emit('input', result.map(item => item.name).join(this.separation)) this.selected = JSON.parse(JSON.stringify(DEFAULT_TABS))
this.currentLevel = 1
this.searchText = ''
this.setItems(this.currentLevel)
},
if (result.length === 0) { done() {
this.$emit('select', {}, []) const result = this.selected.filter(item => item.id)
return this.$refs.select.blur() this.$emit('input', result.map(item => item.name).join(this.separation))
}
const node = result[result.length - 1] if (result.length === 0) {
const payload = [node] this.$emit('select', {}, [])
return this.$refs.select.blur()
}
if (this.getChildrenOnSelect) { const node = result[result.length - 1]
const ids = getNodeId(node.children) const payload = [node]
ids.unshift(node.id)
payload.push(ids)
}
this.$emit('select', ...payload) if (this.getChildrenOnSelect) {
const ids = getNodeId(node.children)
this.$refs.select.blur() ids.unshift(node.id)
}, payload.push(ids)
init() {
this.loading = true
const hasInit = this.regionTree.length > 0
const promise = () => hasInit ? Promise.resolve() : init()
return promise()
.then(() => this.limit ? this.limitApi() : Promise.resolve())
.then(data => {
this.treeData = data ? createLimitTree(this.regionTree, data) : this.regionTree
this.setItems(this.currentLevel)
})
.finally(() => this.loading = false)
} }
},
created() { this.$emit('select', ...payload)
this.search = debounce(this.search, 200)
this.selected = JSON.parse(JSON.stringify(DEFAULT_TABS)) this.$refs.select.blur()
}, },
mounted() { init() {
this.init() this.loading = true
const hasInit = this.regionTree.length > 0
const promise = () => hasInit ? Promise.resolve() : init()
return promise()
.then(() => this.limit ? this.limitApi() : Promise.resolve())
.then(data => {
this.treeData = data ? createLimitTree(this.regionTree, data) : this.regionTree
this.setItems(this.currentLevel)
})
.finally(() => this.loading = false)
} }
},
created() {
this.search = debounce(this.search, 200)
this.selected = JSON.parse(JSON.stringify(DEFAULT_TABS))
},
mounted() {
this.init()
} }
}
//node{id:'10',...}{name:'',...}'10''' //node{id:'10',...}{name:'',...}'10'''
function predicate(node) { function predicate(node) {
if (typeof node === 'object') { if (typeof node === 'object') {
const key = node.hasOwnProperty('id') ? 'id' : 'name' const key = node.hasOwnProperty('id') ? 'id' : 'name'
return item => item[key] === node[key] return item => item[key] === node[key]
} }
else { else {
const firstCharCode = node.charCodeAt(0) const firstCharCode = node.charCodeAt(0)
const key = 48 <= firstCharCode && firstCharCode <= 57 ? 'id' : 'name' const key = 48 <= firstCharCode && firstCharCode <= 57 ? 'id' : 'name'
return item => item[key] === node return item => item[key] === node
}
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -1,84 +1,84 @@
<script type="text/jsx"> <script type="text/jsx">
import {store, init} from '../store' import {store, init} from '../store'
import FormDialog from "@/components/FormDialog" import FormDialog from "@/components/FormDialog"
import {createLimitTree, getNodeId} from "@/utils/tree" import {createLimitTree, getNodeId} from "@/utils/tree"
export default { export default {
components: {FormDialog}, components: {FormDialog},
data() { data() {
return { return {
loading: false, loading: false,
visible: false, visible: false,
handler: null, handler: null,
limitTree: [], limitTree: [],
getChildrenOnSelect: false, getChildrenOnSelect: false,
limit: false, limit: false,
limitApi: null limitApi: null
} }
}, },
computed: {
regionTree() {
return store.data
}
},
methods: { computed: {
closeDialog() { regionTree() {
this.visible = false return store.data
}, }
},
nodeClick(obj) { methods: {
const payload = [obj] closeDialog() {
this.visible = false
},
if (this.getChildrenOnSelect) { nodeClick(obj) {
const ids = getNodeId(obj.children) const payload = [obj]
ids.unshift(obj.id)
payload.push(ids)
}
this.handler(payload) if (this.getChildrenOnSelect) {
const ids = getNodeId(obj.children)
ids.unshift(obj.id)
payload.push(ids)
}
this.closeDialog() this.handler(payload)
},
init() { this.closeDialog()
this.loading = true
const hasInit = this.regionTree.length > 0
const promise = () => hasInit ? Promise.resolve() : init()
return promise()
.then(() => this.limit ? this.limitApi() : Promise.resolve())
.then(data => data && (this.limitTree = createLimitTree(this.regionTree, data)))
.finally(() => this.loading = false)
}
}, },
render() { init() {
return ( this.loading = true
<form-dialog const hasInit = this.regionTree.length > 0
v-model={this.visible} const promise = () => hasInit ? Promise.resolve() : init()
directives={[{name: 'drag-dialog'}]} return promise()
class="tree-dialog" .then(() => this.limit ? this.limitApi() : Promise.resolve())
title="选择行政区域" .then(data => data && (this.limitTree = createLimitTree(this.regionTree, data)))
loading={this.loading} .finally(() => this.loading = false)
appendToBody={true} }
v-on:close={this.closeDialog} },
render() {
return (
<form-dialog
v-model={this.visible}
directives={[{name: 'drag-dialog'}]}
class="tree-dialog"
title="选择行政区域"
loading={this.loading}
appendToBody={true}
v-on:close={this.closeDialog}
>
<el-tree
data={this.limit ? this.limitTree : this.regionTree}
expand-on-click-node={false}
node-key="id"
v-on:node-click={this.nodeClick}
> >
<el-tree {({data}) => (
data={this.limit ? this.limitTree : this.regionTree} <span className="el-tree-node__label">
expand-on-click-node={false}
node-key="id"
v-on:node-click={this.nodeClick}
>
{({data}) => (
<span className="el-tree-node__label">
{this.limit ? data.name + `(${data.value})` : data.name} {this.limit ? data.name + `(${data.value})` : data.name}
</span> </span>
)} )}
</el-tree> </el-tree>
</form-dialog> </form-dialog>
) )
}
} }
}
</script> </script>

@ -1,79 +1,79 @@
<script type="text/jsx"> <script type="text/jsx">
import Vue from 'vue' import Vue from 'vue'
import TreeDialog from "./TreeDialog" import TreeDialog from "./TreeDialog"
import common from '../mixin' import common from '../mixin'
const TreeDialogConstructor = Vue.extend(TreeDialog) const TreeDialogConstructor = Vue.extend(TreeDialog)
let limit, full let limit, full
export default { export default {
name: "RegionTreeSelector", name: "RegionTreeSelector",
mixins: [common], mixins: [common],
data() { data() {
return { return {
dialogVisible: false, dialogVisible: false,
limitTree: [] limitTree: []
}
},
methods: {
openDialog() {
if (this.limit) {
this.initLimit()
return limit.visible = true
}
else {
!full && this.initFull()
full.visible = true
} }
}, },
methods: { handler(payload) {
openDialog() { this.$emit('select', ...payload)
if (this.limit) { },
this.initLimit()
return limit.visible = true
}
else {
!full && this.initFull()
full.visible = true
}
},
handler(payload) {
this.$emit('select', ...payload)
},
initLimit() {
if (limit) {
limit.getChildrenOnSelect = this.getChildrenOnSelect
limit.limit = this.limit
limit.limitApi = this.limitApi
return limit.init()
}
const data = {...this.$props, handler: this.handler}
limit = new TreeDialogConstructor({data}).$mount()
document.body.appendChild(limit.$el)
initLimit() {
if (limit) {
limit.getChildrenOnSelect = this.getChildrenOnSelect
limit.limit = this.limit
limit.limitApi = this.limitApi
return limit.init() return limit.init()
}, }
initFull() { const data = {...this.$props, handler: this.handler}
if (full) { limit = new TreeDialogConstructor({data}).$mount()
return full.getChildrenOnSelect = this.getChildrenOnSelect document.body.appendChild(limit.$el)
}
const data = {getChildrenOnSelect: this.getChildrenOnSelect, handler: this.handler} return limit.init()
full = new TreeDialogConstructor({data}).$mount() },
document.body.appendChild(full.$el)
return full.init() initFull() {
if (full) {
return full.getChildrenOnSelect = this.getChildrenOnSelect
} }
},
render() { const data = {getChildrenOnSelect: this.getChildrenOnSelect, handler: this.handler}
return ( full = new TreeDialogConstructor({data}).$mount()
<el-input document.body.appendChild(full.$el)
value={this.value}
readonly={this.readonly} return full.init()
size={this.size}
clearable
v-on:clear={() => this.$emit('clear')}
v-on:focus={this.openDialog}
/>
)
} }
},
render() {
return (
<el-input
value={this.value}
readonly={this.readonly}
size={this.size}
clearable
v-on:clear={() => this.$emit('clear')}
v-on:focus={this.openDialog}
/>
)
} }
}
</script> </script>

@ -1,18 +1,18 @@
<script> <script>
const Tree = () => import('./Tree').then(_ => _.default) const Tree = () => import('./Tree').then(_ => _.default)
const Tab = () => import('./Tab').then(_ => _.default) const Tab = () => import('./Tab').then(_ => _.default)
export default { export default {
name: "RegionSelector", name: "RegionSelector",
functional: true, functional: true,
props: { props: {
type: {type: String, default: 'tab', validator: v => ['tree', 'tab'].includes(v)} type: {type: String, default: 'tab', validator: v => ['tree', 'tab'].includes(v)}
}, },
render(h, context) { render(h, context) {
return h(context.props.type === 'tree' ? Tree : Tab, context.data) return h(context.props.type === 'tree' ? Tree : Tab, context.data)
}
} }
}
</script> </script>

@ -1,24 +1,24 @@
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: "SearchFormItem", name: "SearchFormItem",
inject: { inject: {
searchForm: { searchForm: {
default: () => ({width: 'auto'}) default: () => ({width: 'auto'})
} }
}, },
props: {label: String}, props: {label: String},
render() { render() {
const {width} = this.searchForm const {width} = this.searchForm
return ( return (
<el-col style={{width}} span={100}> <el-col style={{width}} span={100}>
<el-form-item label={this.label}> <el-form-item label={this.label}>
{this.$slots.default} {this.$slots.default}
</el-form-item> </el-form-item>
</el-col> </el-col>
) )
}
} }
}
</script> </script>

@ -1,124 +1,124 @@
<script type="text/jsx"> <script type="text/jsx">
import {getElementInnerWidth} from '@/utils/browser' import {getElementInnerWidth} from '@/utils/browser'
export default { export default {
name: "SearchForm", name: "SearchForm",
provide() { provide() {
return { return {
searchForm: this searchForm: this
} }
}, },
props: { props: {
labelWidth: {type: String, default: '120px'}, labelWidth: {type: String, default: '120px'},
xs: {type: Number, default: 1},// <768px xs: {type: Number, default: 1},// <768px
sm: {type: Number, default: 2},// >=768px sm: {type: Number, default: 2},// >=768px
md: {type: Number, default: 3}, // >=998px md: {type: Number, default: 3}, // >=998px
lg: {type: Number, default: 4} // >=1200px lg: {type: Number, default: 4} // >=1200px
}, },
data() { data() {
return { return {
showCollapse: false, showCollapse: false,
collapse: true, collapse: true,
num: 0, num: 0,
width: 'auto' width: 'auto'
} }
}, },
methods: { methods: {
handleCollapse() { handleCollapse() {
this.collapse = !this.collapse this.collapse = !this.collapse
},
getElementNumInRow() {
const vw = getElementInnerWidth(this.$el.parentNode)
if (vw < 768) return this.xs
else if (vw < 998) return this.sm
else if (vw < 1200) return this.md
return this.lg
},
resize() {
const num = this.getElementNumInRow()
this.num = num
this.width = `${100 / num}%`
this.showCollapse = num < this.$slots.default.length
}
}, },
mounted() { getElementNumInRow() {
this.resize() const vw = getElementInnerWidth(this.$el.parentNode)
window.addEventListener('resize', this.resize) if (vw < 768) return this.xs
else if (vw < 998) return this.sm
this.$once('hook:beforeDestroy', () => { else if (vw < 1200) return this.md
window.removeEventListener('resize', this.resize) return this.lg
})
}, },
render() { resize() {
const slots = this.$slots.default const num = this.getElementNumInRow()
const collapseChildren = [] this.num = num
if (this.showCollapse) { this.width = `${100 / num}%`
//el-row使v-show this.showCollapse = num < this.$slots.default.length
collapseChildren.push( }
<el-collapse-transition> },
<div v-show={!this.collapse}>
<el-row gutter={20}> mounted() {
{slots.slice(this.num)} this.resize()
</el-row>
</div> window.addEventListener('resize', this.resize)
</el-collapse-transition>
) this.$once('hook:beforeDestroy', () => {
const collapseSlot = this.$scopedSlots.collapse window.removeEventListener('resize', this.resize)
})
collapseChildren.push( },
collapseSlot ?
collapseSlot({collapse: this.collapse, handle: this.handleCollapse}) render() {
: const slots = this.$slots.default
<div class="searchForm__collapse"> const collapseChildren = []
<el-button if (this.showCollapse) {
type="text" //el-row使v-show
size="mini" collapseChildren.push(
icon={this.collapse ? 'el-icon-arrow-down' : 'el-icon-arrow-up'} <el-collapse-transition>
on-click={this.handleCollapse} <div v-show={!this.collapse}>
> <el-row gutter={20}>
{this.collapse ? '展开' : '收起'} {slots.slice(this.num)}
</el-button> </el-row>
</div> </div>
) </el-collapse-transition>
} )
const collapseSlot = this.$scopedSlots.collapse
return (
<el-form class="searchForm" label-position="right" label-width={this.labelWidth} size="small"> collapseChildren.push(
<el-row gutter={20}> collapseSlot ?
{this.num > slots.length ? slots : slots.slice(0, this.num)} collapseSlot({collapse: this.collapse, handle: this.handleCollapse})
</el-row> :
{this.showCollapse && collapseChildren} <div class="searchForm__collapse">
</el-form> <el-button
type="text"
size="mini"
icon={this.collapse ? 'el-icon-arrow-down' : 'el-icon-arrow-up'}
on-click={this.handleCollapse}
>
{this.collapse ? '展开' : '收起'}
</el-button>
</div>
) )
} }
return (
<el-form class="searchForm" label-position="right" label-width={this.labelWidth} size="small">
<el-row gutter={20}>
{this.num > slots.length ? slots : slots.slice(0, this.num)}
</el-row>
{this.showCollapse && collapseChildren}
</el-form>
)
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.searchForm { .searchForm {
margin-bottom: -15px; margin-bottom: -15px;
> .el-row { > .el-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
} }
.searchForm__collapse { .searchForm__collapse {
position: relative; position: relative;
bottom: 20px; bottom: 20px;
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
}
} }
}
</style> </style>

@ -1,75 +1,75 @@
<script type="text/jsx"> <script type="text/jsx">
//https://antdv.com/components/skeleton-cn/ //https://antdv.com/components/skeleton-cn/
import QSkeleton from './index' import QSkeleton from './index'
export default { export default {
name: "PageSkeleton", name: "PageSkeleton",
functional: true, functional: true,
render() { render() {
const single = ( const single = (
<div class="page-skeleton"> <div class="page-skeleton">
<div class="page-skeleton__header"> <div class="page-skeleton__header">
<QSkeleton type="circle"/> <QSkeleton type="circle"/>
</div>
<div class="page-skeleton__content">
<QSkeleton tag="h3"/>
<ul class="page-skeleton__paragraph">{new Array(3).fill(<QSkeleton/>)}</ul>
</div>
</div> </div>
) <div class="page-skeleton__content">
<QSkeleton tag="h3"/>
<ul class="page-skeleton__paragraph">{new Array(3).fill(<QSkeleton/>)}</ul>
</div>
</div>
)
return <div class="page-skeleton-wrapper">{new Array(4).fill(single)}</div> return <div class="page-skeleton-wrapper">{new Array(4).fill(single)}</div>
}
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.page-skeleton-wrapper {
margin: $page-view-margin;
}
.page-skeleton-wrapper { .page-skeleton {
margin: $page-view-margin; width: 100%;
display: table;
& + & {
margin-top: 20px;
} }
.page-skeleton { &__header,
width: 100%; &__content {
display: table; display: table-cell;
}
& + & { &__header {
margin-top: 20px; padding-top: 2px;
} padding-right: 16px;
vertical-align: top;
}
&__header, &__content {
&__content { width: 100%;
display: table-cell;
}
&__header { > .q-skeleton {
padding-top: 2px; width: 50%;
padding-right: 16px;
vertical-align: top;
} }
&__content { .page-skeleton__paragraph {
width: 100%; margin-top: 28px;
padding: 0;
> .q-skeleton { .q-skeleton:last-child:not(:first-child):not(:nth-child(2)) {
width: 50%; width: 61%;
} }
.page-skeleton__paragraph { .q-skeleton + .q-skeleton {
margin-top: 28px; margin-top: 16px;
padding: 0;
.q-skeleton:last-child:not(:first-child):not(:nth-child(2)) {
width: 61%;
}
.q-skeleton + .q-skeleton {
margin-top: 16px;
}
} }
} }
} }
}
</style> </style>

@ -1,50 +1,50 @@
<script type="text/jsx"> <script type="text/jsx">
//quasarhttps://quasar.dev/vue-components/skeleton //quasarhttps://quasar.dev/vue-components/skeleton
import {skeletonAnimations, skeletonTypes} from './constant' import {skeletonAnimations, skeletonTypes} from './constant'
export default { export default {
name: 'QSkeleton', name: 'QSkeleton',
functional: true, functional: true,
props: { props: {
type: { type: {
type: String, type: String,
validator: v => skeletonTypes.includes(v), validator: v => skeletonTypes.includes(v),
default: 'text' default: 'text'
},
animation: {
type: String,
validator: v => skeletonAnimations.includes(v),
default: 'wave'
},
className: String,
tag: {type: String, default: 'div'},
dark: Boolean,
square: Boolean,
bordered: Boolean,
size: String,
width: String,
height: String
}, },
animation: {
type: String,
validator: v => skeletonAnimations.includes(v),
default: 'wave'
},
className: String,
tag: {type: String, default: 'div'},
dark: Boolean,
square: Boolean,
bordered: Boolean,
size: String,
width: String,
height: String
},
render(h, context) { render(h, context) {
const {type, animation, className, tag, dark, square, bordered, size, width, height} = context.props const {type, animation, className, tag, dark, square, bordered, size, width, height} = context.props
const style = size !== undefined ? {width: size, height: size} : {width, height} const style = size !== undefined ? {width: size, height: size} : {width, height}
const classes = `q-skeleton--${dark === true ? 'dark' : 'light'} q-skeleton--type-${type}` + const classes = `q-skeleton--${dark === true ? 'dark' : 'light'} q-skeleton--type-${type}` +
(animation !== 'none' ? ` q-skeleton--anim-${animation}` : '') + (animation !== 'none' ? ` q-skeleton--anim-${animation}` : '') +
(square === true ? ' q-skeleton--square' : '') + (square === true ? ' q-skeleton--square' : '') +
(bordered === true ? ' q-skeleton--bordered' : '') + (bordered === true ? ' q-skeleton--bordered' : '') +
(className !== undefined ? ' ' + className : '') (className !== undefined ? ' ' + className : '')
return h(tag, { return h(tag, {
staticClass: 'q-skeleton', staticClass: 'q-skeleton',
class: classes, class: classes,
style, style,
on: context.$listeners on: context.$listeners
}, context.children) }, context.children)
}
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -1,50 +1,50 @@
<script type="text/jsx"> <script type="text/jsx">
import {isExternal} from '@/utils/validate' import {isExternal} from '@/utils/validate'
export default { export default {
name: 'SvgIcon', name: 'SvgIcon',
functional: true, functional: true,
props: { props: {
icon: String, icon: String,
className: String className: String
}, },
render(h, context) { render(h, context) {
const {listeners} = context const {listeners} = context
const {icon, className} = context.props const {icon, className} = context.props
const c = 'svg-icon ' + (className || '') const c = 'svg-icon ' + (className || '')
if (isExternal(icon)) { if (isExternal(icon)) {
const style = { const style = {
mask: `url(${this.icon}) no-repeat 50% 50%`, mask: `url(${this.icon}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.icon}) no-repeat 50% 50%` '-webkit-mask': `url(${this.icon}) no-repeat 50% 50%`
}
return <div {...{on: listeners}} class={'svg-external-icon ' + c} style={style}/>
}
else {
const iconName = `#icon-${icon}`
return (
<svg {...{on: listeners}} class={c} aria-hidden="true">
<use href={iconName}/>
</svg>
)
} }
return <div {...{on: listeners}} class={'svg-external-icon ' + c} style={style}/>
}
else {
const iconName = `#icon-${icon}`
return (
<svg {...{on: listeners}} class={c} aria-hidden="true">
<use href={iconName}/>
</svg>
)
} }
} }
}
</script> </script>
<style> <style>
.svg-icon { .svg-icon {
width: 1em; width: 1em;
height: 1em; height: 1em;
fill: currentColor; fill: currentColor;
overflow: hidden; overflow: hidden;
} }
.svg-external-icon { .svg-external-icon {
background-color: currentColor; background-color: currentColor;
mask-size: cover !important; mask-size: cover !important;
display: inline-block; display: inline-block;
} }
</style> </style>

@ -7,115 +7,115 @@
</template> </template>
<script> <script>
import {waitUntilSuccess} from "@/utils" import {waitUntilSuccess} from "@/utils"
import {elError} from "@/utils/message" import {elError} from "@/utils/message"
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
language: 'zh_CN', language: 'zh_CN',
language_url: `${process.env.BASE_URL}static/tinymce/langs/zh_CN.js`, language_url: `${process.env.BASE_URL}static/tinymce/langs/zh_CN.js`,
object_resizing: false, object_resizing: false,
end_container_on_empty_block: true, end_container_on_empty_block: true,
default_link_target: '_blank', default_link_target: '_blank',
convert_urls: false convert_urls: false
} }
export default { export default {
name: "TinymceEditor", name: "TinymceEditor",
props: { props: {
value: String, value: String,
readonly: Boolean, readonly: Boolean,
height: {type: String, default: '400px'}, height: {type: String, default: '400px'},
width: {type: String, default: '100%'} width: {type: String, default: '100%'}
}, },
data() { data() {
return { return {
id: 'tinymce-' + Date.now(), id: 'tinymce-' + Date.now(),
fullscreen: false, fullscreen: false,
hasInit: false, hasInit: false,
manualChange: false, manualChange: false,
} }
}, },
watch: { watch: {
value(value) { value(value) {
if (!this.hasInit) return if (!this.hasInit) return
if (this.manualChange) this.manualChange = false if (this.manualChange) this.manualChange = false
else this.set(value) else this.set(value)
},
readonly(value) {
this.hasInit && this.getInstance().mode.set(value ? 'readonly' : 'design')
}
}, },
readonly(value) {
this.hasInit && this.getInstance().mode.set(value ? 'readonly' : 'design')
}
},
methods: { methods: {
getInstance() { getInstance() {
return window.tinymce.get(this.id) return window.tinymce.get(this.id)
}, },
get() { get() {
return this.getInstance().getContent() return this.getInstance().getContent()
}, },
set(content) { set(content) {
this.getInstance().setContent(content || '') this.getInstance().setContent(content || '')
}, },
init() { init() {
const ctx = this const ctx = this
waitUntilSuccess(() => window.tinymce) waitUntilSuccess(() => window.tinymce)
.then(() => { .then(() => {
window.tinymce.init({ window.tinymce.init({
...DEFAULT_OPTIONS, ...DEFAULT_OPTIONS,
selector: `#${ctx.id}`, selector: `#${ctx.id}`,
height: ctx.height, height: ctx.height,
init_instance_callback(editor) { init_instance_callback(editor) {
ctx.hasInit = true ctx.hasInit = true
editor.setContent(ctx.value || '') editor.setContent(ctx.value || '')
editor.mode.set(ctx.readonly ? 'readonly' : 'design') editor.mode.set(ctx.readonly ? 'readonly' : 'design')
}, },
setup(editor) { setup(editor) {
editor.on('keyup undo redo', () => { editor.on('keyup undo redo', () => {
ctx.manualChange = true ctx.manualChange = true
ctx.$emit('input', editor.getContent()) ctx.$emit('input', editor.getContent())
}) })
editor.on('FullscreenStateChanged', e => { editor.on('FullscreenStateChanged', e => {
ctx.fullscreen = e.state ctx.fullscreen = e.state
}) })
} }
})
}) })
.catch(() => elError('tinymce初始化失败,请检查js是否引入成功')) })
}, .catch(() => elError('tinymce初始化失败,请检查js是否引入成功'))
destroy() {
const tinymce = this.getInstance()
if (!tinymce) return
this.fullscreen && tinymce.execCommand('mceFullScreen')
tinymce.destroy()
}
}, },
destroy() {
const tinymce = this.getInstance()
if (!tinymce) return
mounted() { this.fullscreen && tinymce.execCommand('mceFullScreen')
this.init()
this.$once('hook:beforeDestroy', this.destroy) tinymce.destroy()
} }
},
mounted() {
this.init()
this.$once('hook:beforeDestroy', this.destroy)
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.tinymce-container { .tinymce-container {
position: relative; position: relative;
.tinymce-wrapper { .tinymce-wrapper {
width: 100%; width: 100%;
.tinymce-textarea { .tinymce-textarea {
visibility: hidden; visibility: hidden;
}
} }
} }
}
.tox-tinymce-aux { .tox-tinymce-aux {
z-index: 10000 !important; z-index: 10000 !important;
} }
</style> </style>

@ -1,47 +1,47 @@
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: "ZoomControl", name: "ZoomControl",
functional: true, functional: true,
props: { props: {
value: {type: Number, default: 100}, value: {type: Number, default: 100},
step: {type: Number, default: 20}, step: {type: Number, default: 20},
min: {type: Number, default: 10}, min: {type: Number, default: 10},
max: {type: Number, default: 200} max: {type: Number, default: 200}
}, },
render(h, context) { render(h, context) {
const {value, step, max, min} = context.props const {value, step, max, min} = context.props
const input = context.listeners.input const input = context.listeners.input
function zoomIn() { function zoomIn() {
const zoom = value + step const zoom = value + step
if (zoom <= max) input(zoom) if (zoom <= max) input(zoom)
} }
function zoomOut() {
const zoom = value - step
if (zoom >= min) input(zoom)
}
return ( function zoomOut() {
<div class="zoom-wrapper"> const zoom = value - step
<el-button circle icon="el-icon-minus" size="small" title="缩小" type="info" on-click={zoomOut}/> if (zoom >= min) input(zoom)
<span class="zoom-number">{value}%</span>
<el-button circle icon="el-icon-plus" size="small" title="放大" type="info" on-click={zoomIn}/>
</div>
)
} }
return (
<div class="zoom-wrapper">
<el-button circle icon="el-icon-minus" size="small" title="缩小" type="info" on-click={zoomOut}/>
<span class="zoom-number">{value}%</span>
<el-button circle icon="el-icon-plus" size="small" title="放大" type="info" on-click={zoomIn}/>
</div>
)
} }
}
</script> </script>
<style> <style>
.zoom-wrapper .zoom-number { .zoom-wrapper .zoom-number {
color: #657180; color: #657180;
padding: 0 10px; padding: 0 10px;
display: inline-block; display: inline-block;
width: 60px; width: 60px;
text-align: center; text-align: center;
} }
</style> </style>

@ -1,41 +1,41 @@
<template> <template>
<el-tree <el-tree
ref="tree" ref="tree"
:data="data" :data="data"
:expand-on-click-node="false" :expand-on-click-node="false"
:props="{label:'name'}" :props="{label:'name'}"
:filter-node-method="filterNodeMethod" :filter-node-method="filterNodeMethod"
highlight-current highlight-current
node-key="id" node-key="id"
v-on="$listeners" v-on="$listeners"
/> />
</template> </template>
<script> <script>
import {getAll} from "@/api/system/category" import {getAll} from "@/api/system/category"
export default { export default {
name: "CategoryTree", name: "CategoryTree",
props: {filterNodeMethod: Function}, props: {filterNodeMethod: Function},
computed: { computed: {
data() { data() {
return this.$store.state.dataCache.categoryTree return this.$store.state.dataCache.categoryTree
} }
}, },
methods: {
filter(v){
this.$refs.tree.filter(v)
}
},
mounted() { methods: {
if (this.data.length > 0) return filter(v) {
getAll() this.$refs.tree.filter(v)
.then(data => this.$store.commit('dataCache/categories', data))
.finally(() => this.loading = false)
} }
},
mounted() {
if (this.data.length > 0) return
getAll()
.then(data => this.$store.commit('dataCache/categories', data))
.finally(() => this.loading = false)
} }
}
</script> </script>

@ -1,40 +1,40 @@
<template> <template>
<el-select :value="value" :disabled="disabled" multiple size="small" @input="emit"> <el-select :value="value" :disabled="disabled" multiple size="small" @input="emit">
<el-option <el-option
v-for="user in data" v-for="user in data"
:key="user.id" :key="user.id"
:label="user.name" :label="user.name"
:value="user.id" :value="user.id"
/> />
</el-select> </el-select>
</template> </template>
<script> <script>
import {getUsers} from '@/api/system/user' import {getUsers} from '@/api/system/user'
export default { export default {
name: "SimpleMultipleUserSelector", name: "SimpleMultipleUserSelector",
props: { props: {
value: Array, value: Array,
disabled: Boolean disabled: Boolean
}, },
data() { data() {
return { return {
data: [] data: []
} }
}, },
methods: {
emit(v) {
this.$emit('input', v)
}
},
mounted() { methods: {
getUsers({page: 1, pageSize: 9999}) emit(v) {
.then(({list}) => this.data = list) this.$emit('input', v)
} }
},
mounted() {
getUsers({page: 1, pageSize: 9999})
.then(({list}) => this.data = list)
} }
}
</script> </script>

@ -10,7 +10,7 @@
</template> </template>
<script> <script>
export default { export default {
name: "DocDetailFooter" name: "DocDetailFooter"
} }
</script> </script>

@ -5,15 +5,15 @@
<el-row> <el-row>
<el-row class="detail-page-header-left"> <el-row class="detail-page-header-left">
<el-col v-for="i in props.description" :key="i.label" :xs="24" :sm="12"> <el-col v-for="i in props.description" :key="i.label" :xs="24" :sm="12">
<span class="detail-page-header-description-label">{{i.label}}</span> <span class="detail-page-header-description-label">{{ i.label }}</span>
<span class="detail-page-header-description-content">{{i.content}}</span> <span class="detail-page-header-description-content">{{ i.content }}</span>
</el-col> </el-col>
</el-row> </el-row>
<div class="detail-page-header-extra"> <div class="detail-page-header-extra">
<div> <div>
<div v-for="i in props.extra" :key="i.title"> <div v-for="i in props.extra" :key="i.title">
<div class="detail-page-header-extra-title">{{i.title}}</div> <div class="detail-page-header-extra-title">{{ i.title }}</div>
<div class="detail-page-header-extra-content">{{i.content}}</div> <div class="detail-page-header-extra-content">{{ i.content }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -22,19 +22,19 @@
</template> </template>
<script> <script>
export default { export default {
name: "DocDetailHeader", name: "DocDetailHeader",
props: { props: {
title: String, title: String,
description: { description: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
extra: { extra: {
type: Array, type: Array,
default: () => [] default: () => []
}
} }
} }
}
</script> </script>

@ -2,48 +2,48 @@
<transition name="el-zoom-in-bottom" @after-leave="handleAfterLeave"> <transition name="el-zoom-in-bottom" @after-leave="handleAfterLeave">
<div v-show="value" class="bottom-tip el-alert--error is-dark"> <div v-show="value" class="bottom-tip el-alert--error is-dark">
<i class="el-icon-warning tip-icon"/> <i class="el-icon-warning tip-icon"/>
<span class="tip-content">{{message}}</span> <span class="tip-content">{{ message }}</span>
</div> </div>
</transition> </transition>
</template> </template>
<script> <script>
export default { export default {
name: "BottomTip", name: "BottomTip",
data() { data() {
return { return {
value: false, value: false,
message: null message: null
}
},
methods: {
handleAfterLeave() {
this.$destroy(true)
this.$el.parentNode.removeChild(this.$el)
},
close() {
this.value = false
},
} }
},
methods: {
handleAfterLeave() {
this.$destroy(true)
this.$el.parentNode.removeChild(this.$el)
},
close() {
this.value = false
},
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.bottom-tip { .bottom-tip {
position: absolute; position: absolute;
width: 100%; width: 100%;
bottom: 0; bottom: 0;
z-index: 100; z-index: 100;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 20px; font-size: 20px;
.tip-content { .tip-content {
font-weight: bold; font-weight: bold;
margin: 10px; margin: 10px;
}
} }
}
</style> </style>

@ -6,19 +6,19 @@
<div class="guide-popover-action"> <div class="guide-popover-action">
<div class="action-close"> <div class="action-close">
<el-button v-if="hasDone" size="small" @click="exit"> <el-button v-if="hasDone" size="small" @click="exit">
{{step.doneBtnText}} {{ step.doneBtnText }}
</el-button> </el-button>
<el-button v-else size="small" @click="exit"> <el-button v-else size="small" @click="exit">
{{step.closeBtnText}} {{ step.closeBtnText }}
</el-button> </el-button>
</div> </div>
<div class="action-step"> <div class="action-step">
<el-button v-if="showPrevBtn" class="prev-btn" size="small" @click="previous"> <el-button v-if="showPrevBtn" class="prev-btn" size="small" @click="previous">
{{step.prevBtnText}} {{ step.prevBtnText }}
</el-button> </el-button>
<el-button v-if="showNextBtn" class="next-btn" size="small" @click="next"> <el-button v-if="showNextBtn" class="next-btn" size="small" @click="next">
{{step.nextBtnText}} {{ step.nextBtnText }}
</el-button> </el-button>
</div> </div>
</div> </div>
@ -33,211 +33,211 @@
</template> </template>
<script> <script>
/** /**
* 由driver.js改造而来 * 由driver.js改造而来
* vue里交互型的导航不太行太多东西做不到换成引导手册吧 * vue里交互型的导航不太行太多东西做不到换成引导手册吧
*/ */
import {debounce, isEmpty} from "@/utils" import {debounce, isEmpty} from "@/utils"
import {addHighlightClasses, getCalculatedPosition, inAppView, jump, removeHighlightClasses} from "./utils" import {addHighlightClasses, getCalculatedPosition, inAppView, jump, removeHighlightClasses} from "./utils"
export default {
name: "Guide",
data() {
return {
steps: [],
options: {},
beforeExit: null,
isActive: false,
showStage: false,
showOverlay: false,
currentStep: 0,
moving: false,
stageStyle: '',
highlightedElement: null,
lastHighlightedElement: null
}
},
export default { computed: {
name: "Guide", showPopover() {
return !this.moving && this.showStage
},
showPrevBtn() {
return this.step.forceShowPrevBtn || this.currentStep > 0 && this.steps[this.currentStep - 1]
},
showNextBtn() {
return this.step.forceShowNextBtn || this.steps[this.currentStep + 1]
},
hasDone() {
return this.currentStep === this.steps.length - 1
},
step() {
if (!this.isActive
|| this.steps.length <= 0
|| !this.steps[this.currentStep]) {
return {}
}
data() {
return { return {
steps: [], ...this.options,
options: {}, ...this.steps[this.currentStep],
beforeExit: null, index: this.currentStep
isActive: false,
showStage: false,
showOverlay: false,
currentStep: 0,
moving: false,
stageStyle: '',
highlightedElement: null,
lastHighlightedElement: null
} }
}
},
watch: {
isActive(v) {
v ? document.body.classList.add('overflow-hidden') : document.body.classList.remove('overflow-hidden')
}
},
methods: {
start(index = 0) {
if (!this.steps || this.steps.length === 0) throw new Error('请传入步骤')
if (index === 'last') index = this.steps.length - 1
else if (index === 'first') index = 0
if (index > this.steps.length - 1 || !this.steps[index]) {
console.error('error index:', index, 'error steps:', JSON.parse(JSON.stringify(this.steps)))
throw new Error('起始步骤有误')
}
this.isActive = true
this.currentStep = index
this.showOverlay = false
this.showStage = false
this.$_highlight()
this.showOverlay = true
this.showStage = true
}, },
computed: { exit() {
showPopover() { if (!this.isActive) return
return !this.moving && this.showStage
}, if (this.beforeExit) {
showPrevBtn() { const result = this.beforeExit(this.hasDone)
return this.step.forceShowPrevBtn || this.currentStep > 0 && this.steps[this.currentStep - 1] if (result && result.then) {
}, result.then(() => this.$_clear())
showNextBtn() {
return this.step.forceShowNextBtn || this.steps[this.currentStep + 1]
},
hasDone() {
return this.currentStep === this.steps.length - 1
},
step() {
if (!this.isActive
|| this.steps.length <= 0
|| !this.steps[this.currentStep]) {
return {}
} }
else result !== false && this.$_clear()
return
}
this.$_clear()
},
previous() {
if (!this.isActive || this.moving) return
this.moving = true
return { if (this.step.onPrevious) {
...this.options, const result = this.step.onPrevious()
...this.steps[this.currentStep], if (result && result.then) {
index: this.currentStep result.then(() => this.$_movePrevious())
} }
else result !== false && this.$_movePrevious()
} }
else this.$_movePrevious()
}, },
watch: { next() {
isActive(v) { if (!this.isActive || this.moving) return
v ? document.body.classList.add('overflow-hidden') : document.body.classList.remove('overflow-hidden') this.moving = true
if (this.step.onNext) {
const result = this.step.onNext()
if (result && result.then) {
result.then(() => this.$_moveNext())
}
else result !== false && this.$_moveNext()
} }
else this.$_moveNext()
}, },
methods: { resize() {
start(index = 0) { if (!this.highlightedElement) return
if (!this.steps || this.steps.length === 0) throw new Error('请传入步骤') this.$_setStageStyle(getCalculatedPosition(this.highlightedElement))
this.$nextTick(this.$refs.popover.updatePopper)
},
if (index === 'last') index = this.steps.length - 1 $_movePrevious() {
else if (index === 'first') index = 0 if (!this.isActive) return
this.currentStep--
this.$_highlight()
},
if (index > this.steps.length - 1 || !this.steps[index]) { $_moveNext() {
console.error('error index:', index, 'error steps:', JSON.parse(JSON.stringify(this.steps))) if (!this.isActive) return
throw new Error('起始步骤有误') this.currentStep++
} this.$_highlight()
},
this.isActive = true $_clear() {
this.currentStep = index if (!this.isActive) return
this.showOverlay = false this.isActive = false
this.showStage = false this.showOverlay = false
this.showStage = false
this.currentStep = 0
this.steps = []
this.beforeExit = null
if (this.highlightedElement) removeHighlightClasses(this.highlightedElement)
this.highlightedElement = null
this.lastHighlightedElement = null
},
this.$_highlight() $_highlight() {
if (isEmpty(this.step.element)) throw new Error(`step${this.currentStep}中element为空`)
this.showOverlay = true const el = document.querySelector(this.step.element)
this.showStage = true
},
exit() { if (!el) throw new Error(`${this.step.element} 没有该元素`)
if (!this.isActive) return
if (this.beforeExit) { if (el === this.highlightedElement) return
const result = this.beforeExit(this.hasDone)
if (result && result.then) {
result.then(() => this.$_clear())
}
else result !== false && this.$_clear()
return
}
this.$_clear() if (this.highlightedElement) removeHighlightClasses(this.highlightedElement)
},
previous() { this.lastHighlightedElement = this.highlightedElement
if (!this.isActive || this.moving) return this.highlightedElement = el
this.moving = true
if (this.step.onPrevious) { if (inAppView(el)) jump(el)
const result = this.step.onPrevious()
if (result && result.then) {
result.then(() => this.$_movePrevious())
}
else result !== false && this.$_movePrevious()
}
else this.$_movePrevious()
},
next() {
if (!this.isActive || this.moving) return
this.moving = true
if (this.step.onNext) {
const result = this.step.onNext()
if (result && result.then) {
result.then(() => this.$_moveNext())
}
else result !== false && this.$_moveNext()
}
else this.$_moveNext()
},
resize() {
if (!this.highlightedElement) return
this.$_setStageStyle(getCalculatedPosition(this.highlightedElement))
this.$nextTick(this.$refs.popover.updatePopper)
},
$_movePrevious() {
if (!this.isActive) return
this.currentStep--
this.$_highlight()
},
$_moveNext() {
if (!this.isActive) return
this.currentStep++
this.$_highlight()
},
$_clear() {
if (!this.isActive) return
this.isActive = false
this.showOverlay = false
this.showStage = false
this.currentStep = 0
this.steps = []
this.beforeExit = null
if (this.highlightedElement) removeHighlightClasses(this.highlightedElement)
this.highlightedElement = null
this.lastHighlightedElement = null
},
$_highlight() {
if (isEmpty(this.step.element)) throw new Error(`step${this.currentStep}中element为空`)
const el = document.querySelector(this.step.element)
if (!el) throw new Error(`${this.step.element} 没有该元素`)
if (el === this.highlightedElement) return
if (this.highlightedElement) removeHighlightClasses(this.highlightedElement)
this.lastHighlightedElement = this.highlightedElement
this.highlightedElement = el
if (inAppView(el)) jump(el)
this.$_setStageStyle(getCalculatedPosition(el))
setTimeout(() => {
this.$refs.popover.updatePopper()
this.moving = false
}, 300)
addHighlightClasses(el)
},
$_setStageStyle({top, left, right, bottom}) {
const width = right - left
const height = bottom - top
this.stageStyle =
`width:${width}px;` +
`height:${height}px;` +
`top:${top}px;` +
`left:${left}px;` +
`backgroundColor:${this.step.stageBackground}`
}
},
mounted() { this.$_setStageStyle(getCalculatedPosition(el))
this.resize = debounce(this.resize)
window.addEventListener('resize', this.resize) setTimeout(() => {
this.$refs.popover.updatePopper()
this.moving = false
}, 300)
this.$once('hook:beforeDestroy', () => { addHighlightClasses(el)
window.removeEventListener('resize', this.resize) },
})
$_setStageStyle({top, left, right, bottom}) {
const width = right - left
const height = bottom - top
this.stageStyle =
`width:${width}px;` +
`height:${height}px;` +
`top:${top}px;` +
`left:${left}px;` +
`backgroundColor:${this.step.stageBackground}`
} }
},
mounted() {
this.resize = debounce(this.resize)
window.addEventListener('resize', this.resize)
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('resize', this.resize)
})
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -1,10 +1,10 @@
<template> <template>
<transition :name="transition" @after-leave="handleAfterLeave"> <transition :name="transition" @after-leave="handleAfterLeave">
<div <div
v-show="visible" v-show="visible"
class="puzzle-container" class="puzzle-container"
:class="{'puzzle-shake':stat==='fail'}" :class="{'puzzle-shake':stat==='fail'}"
:style="`left: ${positionLeft}px;top: ${positionTop}px;`" :style="`left: ${positionLeft}px;top: ${positionTop}px;`"
> >
<div class="puzzle-view" :style="`width:${width}px;height:${height}px;`"> <div class="puzzle-view" :style="`width:${width}px;height:${height}px;`">
<div v-show="!image.loading" style="width: 100%;height: 100%"> <div v-show="!image.loading" style="width: 100%;height: 100%">
@ -26,7 +26,7 @@
</transition> </transition>
<div :class="resultTipClass"> <div :class="resultTipClass">
{{stat==='success'?'验证通过':'请正确拼合图像'}} {{ stat === 'success' ? '验证通过' : '请正确拼合图像' }}
</div> </div>
</div> </div>
@ -53,278 +53,278 @@
</template> </template>
<script> <script>
import BtnStopImage from './image/btn_stop.png' import BtnStopImage from './image/btn_stop.png'
import BtnMoveImage from './image/btn_move.png' import BtnMoveImage from './image/btn_move.png'
/* /*
* https://github.com/Kevin-269581661/vue-puzzle-verification * https://github.com/Kevin-269581661/vue-puzzle-verification
* 网上搬的例子实际开发建议用极验 * 网上搬的例子实际开发建议用极验
* */ * */
export default { export default {
name: "PuzzleVerify", name: "PuzzleVerify",
props: { props: {
//transition.scss //transition.scss
transition: { transition: {
type: String, type: String,
default: 'scale' default: 'scale'
}, },
// //
width: { width: {
type: Number, type: Number,
default: 260 default: 260
}, },
height: { height: {
type: Number, type: Number,
default: 160 default: 160
}, },
// //
blockSize: { blockSize: {
type: Number, type: Number,
default: 40 default: 40
}, },
// //
deviation: { deviation: {
type: Number, type: Number,
default: 4 default: 4
},
//
padding: {
type: Number,
default: 80
}
},
data() {
return {
visible: false,
stat: 'ready',//ready,move,success,fail,error
positionLeft: 0,
positionTop: 0,
resolve: () => ({}),
reject: () => ({}),
moveStartAtX: null,
randomX: null,
randomY: null,
leftNum: 0,
image: {
src: '',
loading: true
}, },
// sliderStyle: {
padding: { left: '0',
type: Number, transition: 'inherit'
default: 80
} }
}
},
computed: {
resultTipClass() {
const showResultTip = ['success', 'fail'].includes(this.stat)
return ['verify-result', `verify-${this.stat}`, showResultTip ? 'show-result' : '']
}, },
data() { sliderBtnStyle() {
return { return {
visible: false, ...this.sliderStyle,
stat: 'ready',//ready,move,success,fail,error 'background-image': `url(${this.stat === 'move' ? BtnMoveImage : BtnStopImage})`
positionLeft: 0,
positionTop: 0,
resolve: () => ({}),
reject: () => ({}),
moveStartAtX: null,
randomX: null,
randomY: null,
leftNum: 0,
image: {
src: '',
loading: true
},
sliderStyle: {
left: '0',
transition: 'inherit'
}
} }
}
},
methods: {
close() {
this.visible = false
this.clear()
this.reject()
}, },
computed: { refresh() {
resultTipClass() { this.initImage().then(() => this.initCanvas())
const showResultTip = ['success', 'fail'].includes(this.stat)
return ['verify-result', `verify-${this.stat}`, showResultTip ? 'show-result' : '']
},
sliderBtnStyle() {
return {
...this.sliderStyle,
'background-image': `url(${this.stat === 'move' ? BtnMoveImage : BtnStopImage})`
}
}
}, },
methods: { initCanvas() {
close() { this.clear()
this.visible = false let MinX = this.padding + this.blockSize
this.clear() let MaxX = this.width - this.padding - 2 * this.blockSize - this.blockSize / 6
this.reject() let MaxY = this.padding
}, let MinY = this.height - this.padding - this.blockSize - this.blockSize / 6
this.randomX = Math.round(Math.random() * MaxX + MinX)
refresh() { this.randomY = Math.round(Math.random() * MaxY + MinY)
this.initImage().then(() => this.initCanvas()) let X = this.randomX
}, let Y = this.randomY
this.leftNum = -X + 10
initCanvas() { let d = this.blockSize / 3
this.clear()
let MinX = this.padding + this.blockSize let c = this.$refs.puzzleBox
let MaxX = this.width - this.padding - 2 * this.blockSize - this.blockSize / 6 let c_l = this.$refs.puzzleLost
let MaxY = this.padding let c_s = this.$refs.puzzleShadow
let MinY = this.height - this.padding - this.blockSize - this.blockSize / 6 let ctx = c.getContext("2d")
this.randomX = Math.round(Math.random() * MaxX + MinX) let ctx_l = c_l.getContext("2d")
this.randomY = Math.round(Math.random() * MaxY + MinY) let ctx_s = c_s.getContext("2d")
let X = this.randomX ctx.globalCompositeOperation = "xor"
let Y = this.randomY ctx.shadowBlur = 10
this.leftNum = -X + 10 ctx.shadowColor = "#fff"
let d = this.blockSize / 3 ctx.shadowOffsetX = 3
ctx.shadowOffsetY = 3
let c = this.$refs.puzzleBox ctx.fillStyle = "rgba(0,0,0,0.7)"
let c_l = this.$refs.puzzleLost ctx.beginPath()
let c_s = this.$refs.puzzleShadow ctx.lineWidth = 1
let ctx = c.getContext("2d") ctx.strokeStyle = "rgba(0,0,0,0)"
let ctx_l = c_l.getContext("2d")
let ctx_s = c_s.getContext("2d") ctx.moveTo(X, Y)
ctx.globalCompositeOperation = "xor" ctx.lineTo(X + d, Y)
ctx.shadowBlur = 10 ctx.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y)
ctx.shadowColor = "#fff" ctx.lineTo(X + 3 * d, Y)
ctx.shadowOffsetX = 3 ctx.lineTo(X + 3 * d, Y + d)
ctx.shadowOffsetY = 3 ctx.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d)
ctx.fillStyle = "rgba(0,0,0,0.7)" ctx.lineTo(X + 3 * d, Y + 3 * d)
ctx.beginPath() ctx.lineTo(X, Y + 3 * d)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0)" ctx.closePath()
ctx.stroke()
ctx.moveTo(X, Y) ctx.fill()
ctx.lineTo(X + d, Y)
ctx.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y) this.drawImage(img => ctx_l.drawImage(img, 0, 0, this.width, this.height))
ctx.lineTo(X + 3 * d, Y)
ctx.lineTo(X + 3 * d, Y + d) ctx_l.beginPath()
ctx.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d) ctx_l.strokeStyle = "rgba(0,0,0,0)"
ctx.lineTo(X + 3 * d, Y + 3 * d)
ctx.lineTo(X, Y + 3 * d) ctx_l.moveTo(X, Y)
ctx_l.lineTo(X + d, Y)
ctx.closePath() ctx_l.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y)
ctx.stroke() ctx_l.lineTo(X + 3 * d, Y)
ctx.fill() ctx_l.lineTo(X + 3 * d, Y + d)
ctx_l.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d)
this.drawImage(img => ctx_l.drawImage(img, 0, 0, this.width, this.height)) ctx_l.lineTo(X + 3 * d, Y + 3 * d)
ctx_l.lineTo(X, Y + 3 * d)
ctx_l.beginPath()
ctx_l.strokeStyle = "rgba(0,0,0,0)" ctx_l.closePath()
ctx_l.stroke()
ctx_l.moveTo(X, Y) ctx_l.shadowBlur = 10
ctx_l.lineTo(X + d, Y) ctx_l.shadowColor = "black"
ctx_l.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y) ctx_l.clip()
ctx_l.lineTo(X + 3 * d, Y) ctx_s.beginPath()
ctx_l.lineTo(X + 3 * d, Y + d) ctx_s.lineWidth = 1
ctx_l.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d) ctx_s.strokeStyle = "rgba(0,0,0,0)"
ctx_l.lineTo(X + 3 * d, Y + 3 * d)
ctx_l.lineTo(X, Y + 3 * d) ctx_s.moveTo(X, Y)
ctx_s.lineTo(X + d, Y)
ctx_l.closePath() ctx_s.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y)
ctx_l.stroke() ctx_s.lineTo(X + 3 * d, Y)
ctx_l.shadowBlur = 10 ctx_s.lineTo(X + 3 * d, Y + d)
ctx_l.shadowColor = "black" ctx_s.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d)
ctx_l.clip() ctx_s.lineTo(X + 3 * d, Y + 3 * d)
ctx_s.beginPath() ctx_s.lineTo(X, Y + 3 * d)
ctx_s.lineWidth = 1
ctx_s.strokeStyle = "rgba(0,0,0,0)" ctx_s.closePath()
ctx_s.stroke()
ctx_s.moveTo(X, Y) ctx_s.shadowBlur = 20
ctx_s.lineTo(X + d, Y) ctx_s.shadowColor = "black"
ctx_s.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y) ctx_s.fill()
ctx_s.lineTo(X + 3 * d, Y) },
ctx_s.lineTo(X + 3 * d, Y + d)
ctx_s.bezierCurveTo(X + 2 * d, Y + d, X + 2 * d, Y + 2 * d, X + 3 * d, Y + 2 * d)
ctx_s.lineTo(X + 3 * d, Y + 3 * d)
ctx_s.lineTo(X, Y + 3 * d)
ctx_s.closePath()
ctx_s.stroke()
ctx_s.shadowBlur = 20
ctx_s.shadowColor = "black"
ctx_s.fill()
},
initImage() { initImage() {
const imageListTemp = [ const imageListTemp = [
"https://dss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=4018557288,1217151095&fm=26&gp=0.jpg", "https://dss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=4018557288,1217151095&fm=26&gp=0.jpg",
"https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3252521864,872614242&fm=26&gp=0.jpg", "https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3252521864,872614242&fm=26&gp=0.jpg",
"https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2534506313,1688529724&fm=26&gp=0.jpg" "https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2534506313,1688529724&fm=26&gp=0.jpg"
] ]
this.image.loading = true this.image.loading = true
let imgRandomIndex = Math.round(Math.random() * (imageListTemp.length - 1)) let imgRandomIndex = Math.round(Math.random() * (imageListTemp.length - 1))
this.image.src = imageListTemp[imgRandomIndex] this.image.src = imageListTemp[imgRandomIndex]
return new Promise(resolve => resolve()) return new Promise(resolve => resolve())
}, },
drawImage(fun) { drawImage(fun) {
let img = new Image() let img = new Image()
img.src = this.image.src img.src = this.image.src
img.onload = () => { img.onload = () => {
fun(img) fun(img)
this.image.loading = false this.image.loading = false
} }
}, },
clear() { clear() {
this.stat = 'ready' this.stat = 'ready'
let c = this.$refs.puzzleBox let c = this.$refs.puzzleBox
let c_l = this.$refs.puzzleLost let c_l = this.$refs.puzzleLost
let c_s = this.$refs.puzzleShadow let c_s = this.$refs.puzzleShadow
c.setAttribute("height", c.getAttribute("height")) c.setAttribute("height", c.getAttribute("height"))
c_l.setAttribute("height", c.getAttribute("height")) c_l.setAttribute("height", c.getAttribute("height"))
c_s.setAttribute("height", c.getAttribute("height")) c_s.setAttribute("height", c.getAttribute("height"))
}, },
moveStart(e) { moveStart(e) {
if (this.stat !== 'ready') return if (this.stat !== 'ready') return
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
this.moveStartAtX = e.type === 'mousedown' ? e.pageX : e.changedTouches[0].pageX this.moveStartAtX = e.type === 'mousedown' ? e.pageX : e.changedTouches[0].pageX
this.addListener() this.addListener()
}, },
moving(e) { moving(e) {
this.stat = 'move' this.stat = 'move'
const pageX = e.type === 'mousemove' ? e.pageX : e.changedTouches[0].pageX const pageX = e.type === 'mousemove' ? e.pageX : e.changedTouches[0].pageX
const distance = pageX - this.moveStartAtX const distance = pageX - this.moveStartAtX
if (this.moveStartAtX === null if (this.moveStartAtX === null
|| distance < 0 || distance < 0
|| distance > this.width - this.blockSize) { || distance > this.width - this.blockSize) {
return return
} }
this.sliderStyle.left = distance + "px" this.sliderStyle.left = distance + "px"
this.sliderStyle.transition = "inherit" this.sliderStyle.transition = "inherit"
}, },
moveEnd(e) { moveEnd(e) {
const pageX = e.type === 'mouseup' ? e.pageX : e.changedTouches[0].pageX const pageX = e.type === 'mouseup' ? e.pageX : e.changedTouches[0].pageX
const distance = pageX - this.moveStartAtX const distance = pageX - this.moveStartAtX
const ver_Num = this.randomX - 10 const ver_Num = this.randomX - 10
const minLeft = ver_Num - this.deviation const minLeft = ver_Num - this.deviation
const maxLeft = ver_Num + this.deviation const maxLeft = ver_Num + this.deviation
if (this.moveStartAtX !== null) { if (this.moveStartAtX !== null) {
const success = maxLeft > distance && distance > minLeft const success = maxLeft > distance && distance > minLeft
this.stat = success ? 'success' : 'fail' this.stat = success ? 'success' : 'fail'
setTimeout(() => {
this.stat = 'ready'
if (!success) return this.initCanvas()
this.resolve()
this.close()
}, success ? 500 : 1000)
}
setTimeout(() => { setTimeout(() => {
this.sliderStyle.left = '0' this.stat = 'ready'
this.sliderStyle.transition = "left 0.5s" if (!success) return this.initCanvas()
}, 1000) this.resolve()
this.moveStartAtX = null this.close()
this.removeListener() }, success ? 500 : 1000)
},
addListener() {
if (this.image.loading) return
document.addEventListener("mousemove", this.moving)
document.addEventListener("mouseup", this.moveEnd)
document.addEventListener("touchmove", this.moving)
document.addEventListener("touchend", this.moveEnd)
},
removeListener() {
document.removeEventListener("mousemove", this.moving)
document.removeEventListener("mouseup", this.moveEnd)
document.addEventListener("touchmove", this.moving)
document.addEventListener("touchend", this.moveEnd)
},
handleAfterLeave() {
this.$destroy(true)
this.$el.parentNode.removeChild(this.$el)
} }
setTimeout(() => {
this.sliderStyle.left = '0'
this.sliderStyle.transition = "left 0.5s"
}, 1000)
this.moveStartAtX = null
this.removeListener()
}, },
mounted() { addListener() {
this.initImage().then(() => this.initCanvas()) if (this.image.loading) return
document.addEventListener("mousemove", this.moving)
document.addEventListener("mouseup", this.moveEnd)
document.addEventListener("touchmove", this.moving)
document.addEventListener("touchend", this.moveEnd)
},
removeListener() {
document.removeEventListener("mousemove", this.moving)
document.removeEventListener("mouseup", this.moveEnd)
document.addEventListener("touchmove", this.moving)
document.addEventListener("touchend", this.moveEnd)
}, },
}
handleAfterLeave() {
this.$destroy(true)
this.$el.parentNode.removeChild(this.$el)
}
},
mounted() {
this.initImage().then(() => this.initCanvas())
},
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -1,13 +1,13 @@
<template> <template>
<div v-show="visible" class="signature-board"> <div v-show="visible" class="signature-board">
<canvas <canvas
ref="canvas" ref="canvas"
@mousedown.prevent="drawStart" @mousedown.prevent="drawStart"
@mousemove.prevent="drawMove" @mousemove.prevent="drawMove"
@mouseup.prevent="drawEnd" @mouseup.prevent="drawEnd"
@touchstart.prevent="drawStart" @touchstart.prevent="drawStart"
@touchmove.prevent="drawMove" @touchmove.prevent="drawMove"
@touchend.prevent="drawEnd" @touchend.prevent="drawEnd"
/> />
<div class="signature-board__footer"> <div class="signature-board__footer">
@ -19,143 +19,143 @@
</template> </template>
<script> <script>
/* /*
* 突然发现没办法做到竖屏横屏都支持坑爹 * 突然发现没办法做到竖屏横屏都支持坑爹
* */ * */
function getEventPoint(e) { function getEventPoint(e) {
let x, y let x, y
if (['mousedown', 'mousemove'].includes(e.type)) { if (['mousedown', 'mousemove'].includes(e.type)) {
x = e.clientX - e.target.offsetLeft x = e.clientX - e.target.offsetLeft
y = e.clientY - e.target.offsetTop y = e.clientY - e.target.offsetTop
} }
else { else {
x = e.changedTouches[0].clientX - e.target.offsetLeft x = e.changedTouches[0].clientX - e.target.offsetLeft
y = e.changedTouches[0].clientY - e.target.offsetTop y = e.changedTouches[0].clientY - e.target.offsetTop
}
return {x, y}
} }
return {x, y}
}
export default {
name: 'SignatureBoard',
props: {
image: String,
lineWidth: Number,
lineColor: String,
onConfirm: Function
},
data() {
return {
visible: false,
drew: false,
drawing: false,
startX: 0,
startY: 0
}
},
methods: {
drawStart(e) {
const {x, y} = getEventPoint(e)
this.drew = true
this.drawing = true
this.startX = x
this.startY = y
this.ctx.strokeStyle = this.lineColor
this.ctx.lineWidth = this.lineWidth
},
export default { drawMove(e) {
name: 'SignatureBoard', if (!this.drawing) return
const {x, y} = getEventPoint(e)
this.ctx.beginPath()
this.ctx.moveTo(this.startX, this.startY)
this.ctx.lineTo(x, y)
this.ctx.stroke()
this.ctx.closePath()
this.startY = y
this.startX = x
},
props: { drawEnd() {
image: String, this.drawing = false
lineWidth: Number,
lineColor: String,
onConfirm: Function
}, },
data() { getBase64() {
return { if (!this.drew) return null
visible: false, return this.$refs.canvas.toDataURL()
drew: false, },
drawing: false,
startX: 0, resize() {
startY: 0 const canvas = this.$refs.canvas
}
canvas.height = window.innerHeight
canvas.width = window.innerWidth
}, },
methods: { init() {
drawStart(e) { this.resize()
const {x, y} = getEventPoint(e)
this.drew = true this.ctx = this.$refs.canvas.getContext('2d')
this.drawing = true this.ctx.lineCap = 'round'
this.startX = x this.ctx.lineJoin = 'round'
this.startY = y
this.ctx.strokeStyle = this.lineColor if (this.image) {
this.ctx.lineWidth = this.lineWidth let img = new Image()
}, img.src = this.image
img.onload = () => {
drawMove(e) { this.ctx.drawImage(img, 0, 0, img.width, img.height)
if (!this.drawing) return this.drew = true
const {x, y} = getEventPoint(e) img = null
this.ctx.beginPath()
this.ctx.moveTo(this.startX, this.startY)
this.ctx.lineTo(x, y)
this.ctx.stroke()
this.ctx.closePath()
this.startY = y
this.startX = x
},
drawEnd() {
this.drawing = false
},
getBase64() {
if (!this.drew) return null
return this.$refs.canvas.toDataURL()
},
resize() {
const canvas = this.$refs.canvas
canvas.height = window.innerHeight
canvas.width = window.innerWidth
},
init() {
this.resize()
this.ctx = this.$refs.canvas.getContext('2d')
this.ctx.lineCap = 'round'
this.ctx.lineJoin = 'round'
if (this.image) {
let img = new Image()
img.src = this.image
img.onload = () => {
this.ctx.drawImage(img, 0, 0, img.width, img.height)
this.drew = true
img = null
}
} }
},
clear() {
this.drew = false
this.drawing = false
this.$refs.canvas && (this.$refs.canvas.height = window.innerHeight)
},
close() {
this.visible = false
this.clear()
},
confirm() {
this.onConfirm && this.onConfirm(this.getBase64())
this.visible = false
} }
}, },
mounted() { clear() {
window.addEventListener('resize', this.resize) this.drew = false
this.drawing = false
this.$refs.canvas && (this.$refs.canvas.height = window.innerHeight)
},
this.$once('hook:beforeDestroy', () => { close() {
this.clear() this.visible = false
window.removeEventListener('resize', this.resize) this.clear()
}) },
confirm() {
this.onConfirm && this.onConfirm(this.getBase64())
this.visible = false
} }
},
mounted() {
window.addEventListener('resize', this.resize)
this.$once('hook:beforeDestroy', () => {
this.clear()
window.removeEventListener('resize', this.resize)
})
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.signature-board { .signature-board {
position: fixed; position: fixed;
background-color: white; background-color: white;
top: 0; top: 0;
right: 0; right: 0;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
z-index: 100; z-index: 100;
overflow: hidden; overflow: hidden;
&__footer { &__footer {
position: absolute; position: absolute;
bottom: 20px; bottom: 20px;
right: 20px; right: 20px;
}
} }
}
</style> </style>

@ -7,19 +7,19 @@
</template> </template>
<script> <script>
export default { export default {
name: "Footer" name: "Footer"
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.page-footer { .page-footer {
padding: 48px 24px 24px 24px; padding: 48px 24px 24px 24px;
text-align: center; text-align: center;
.copyright { .copyright {
color: #808695; color: #808695;
font-size: 14px; font-size: 14px;
}
} }
}
</style> </style>

@ -1,8 +1,8 @@
<template> <template>
<header <header
:class="{'header-container':true,'hide-header':hideHeader}" :class="{'header-container':true,'hide-header':hideHeader}"
@mouseenter="() => this.mouseOutside = false" @mouseenter="() => this.mouseOutside = false"
@mouseleave="() => this.mouseOutside = true" @mouseleave="() => this.mouseOutside = true"
> >
<v-navbar @menu-show="e => this.navbarMenuShow = e"/> <v-navbar @menu-show="e => this.navbarMenuShow = e"/>
@ -11,89 +11,89 @@
</template> </template>
<script> <script>
import {mapState} from 'vuex' import {mapState} from 'vuex'
import VNavbar from './Navbar' import VNavbar from './Navbar'
import TagsView from './TagsView' import TagsView from './TagsView'
export default {
name: "Header",
components: {VNavbar, TagsView},
data() {
return {
mouseOutside: true,
navbarMenuShow: false,
tagsViewMenuShow: false,
appMain: null
}
},
computed: { export default {
...mapState('setting', ['useTagsView', 'headerAutoHidden']), name: "Header",
hideHeader() { components: {VNavbar, TagsView},
return this.mouseOutside
&& this.headerAutoHidden data() {
&& !this.tagsViewMenuShow return {
&& !this.navbarMenuShow mouseOutside: true,
}, navbarMenuShow: false,
}, tagsViewMenuShow: false,
appMain: null
}
},
watch: { computed: {
// ...mapState('setting', ['useTagsView', 'headerAutoHidden']),
useTagsView(v) {
!v && this.$store.dispatch('tagsView/delAllViews')
},
hideHeader(v) { hideHeader() {
this.$store.commit('app/hasHeader', !v) return this.mouseOutside
v ? this.addEvent() : this.removeEvent() && this.headerAutoHidden
} && !this.tagsViewMenuShow
&& !this.navbarMenuShow
}, },
},
methods: { watch: {
moveEvent(e) { //
if (e.clientY <= 15) this.mouseOutside = false useTagsView(v) {
}, !v && this.$store.dispatch('tagsView/delAllViews')
},
addEvent() { hideHeader(v) {
this.appMain.addEventListener('mousemove', this.moveEvent) this.$store.commit('app/hasHeader', !v)
}, v ? this.addEvent() : this.removeEvent()
}
},
removeEvent() { methods: {
this.appMain.removeEventListener('mousemove', this.moveEvent) moveEvent(e) {
} if (e.clientY <= 15) this.mouseOutside = false
}, },
mounted() { addEvent() {
this.appMain = document.querySelector('.app-main') this.appMain.addEventListener('mousemove', this.moveEvent)
if (this.headerAutoHidden) this.addEvent() },
this.$once('hook:beforeDestroy', this.removeEvent) removeEvent() {
this.appMain.removeEventListener('mousemove', this.moveEvent)
} }
},
mounted() {
this.appMain = document.querySelector('.app-main')
if (this.headerAutoHidden) this.addEvent()
this.$once('hook:beforeDestroy', this.removeEvent)
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.has-tags-view .header-container { .has-tags-view .header-container {
height: calc(#{$nav-height} + #{$tags-view-height}); height: calc(#{$nav-height} + #{$tags-view-height});
} }
.header-container { .header-container {
position: relative; position: relative;
height: $nav-height; height: $nav-height;
transition: height .3s ease-in-out; transition: height .3s ease-in-out;
flex-shrink: 0; flex-shrink: 0;
&.hide-header { &.hide-header {
height: 0; height: 0;
> .navbar { > .navbar {
height: 0; height: 0;
}
} }
} }
}
</style> </style>

@ -12,14 +12,14 @@
<!--跨域iframe无法调整高度只能使用原生滚动条--> <!--跨域iframe无法调整高度只能使用原生滚动条-->
<iframe <iframe
v-for="src in iframeList" v-for="src in iframeList"
v-show="showIframe && src === currentIframe" v-show="showIframe && src === currentIframe"
:id="src" :id="src"
:key="src" :key="src"
:src="src" :src="src"
frameborder="0" frameborder="0"
height="100%" height="100%"
width="100%" width="100%"
/> />
<el-backtop v-if="showBackToTop" target=".app-main .el-scrollbar__wrap" :visibility-height="400" :bottom="66"/> <el-backtop v-if="showBackToTop" target=".app-main .el-scrollbar__wrap" :visibility-height="400" :bottom="66"/>
@ -27,75 +27,75 @@
</template> </template>
<script> <script>
import Vue from 'vue' import Vue from 'vue'
import {mapState} from 'vuex' import {mapState} from 'vuex'
import KeepRouterViewAlive from "./KeepAlive" import KeepRouterViewAlive from "./KeepAlive"
import Footer from "./Footer" import Footer from "./Footer"
export default { export default {
name: 'AppMain', name: 'AppMain',
components: {KeepRouterViewAlive}, components: {KeepRouterViewAlive},
computed: { computed: {
...mapState('app', ['scrollTop']), ...mapState('app', ['scrollTop']),
...mapState('setting', ['showBackToTop']), ...mapState('setting', ['showBackToTop']),
...mapState('tagsView', ['cachedViews', 'transitionName']), ...mapState('tagsView', ['cachedViews', 'transitionName']),
...mapState('iframe', { ...mapState('iframe', {
showIframe: state => state.show, showIframe: state => state.show,
currentIframe: state => state.current, currentIframe: state => state.current,
iframeList: state => state.list iframeList: state => state.list
}) })
}, },
watch: { watch: {
scrollTop(v) { scrollTop(v) {
if (v >= 0) this.$refs.scrollbar.$refs.wrap.scrollTop = v if (v >= 0) this.$refs.scrollbar.$refs.wrap.scrollTop = v
}
},
mounted() {
//footer
const FooterConstructor = Vue.extend(Footer)
const footerInstance = new FooterConstructor().$mount()
this.$refs.scrollbar.$refs.wrap.appendChild(footerInstance.$el)
} }
},
mounted() {
//footer
const FooterConstructor = Vue.extend(Footer)
const footerInstance = new FooterConstructor().$mount()
this.$refs.scrollbar.$refs.wrap.appendChild(footerInstance.$el)
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.app-main { .app-main {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background-color: #f5f7f9; background-color: #f5f7f9;
flex: 1; flex: 1;
.scroll-container { .scroll-container {
position: relative; position: relative;
height: 100%; height: 100%;
.page-view { .page-view {
margin: $page-view-margin; margin: $page-view-margin;
} }
> .el-scrollbar__bar.is-horizontal { > .el-scrollbar__bar.is-horizontal {
display: none !important; display: none !important;
} }
> .el-scrollbar__wrap { > .el-scrollbar__wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: hidden; overflow-x: hidden;
.el-scrollbar__view { .el-scrollbar__view {
flex: 1; flex: 1;
}
} }
} }
} }
}
</style> </style>

@ -5,34 +5,34 @@
</template> </template>
<script> <script>
import {baseUrl} from '@/api/message/user' import {baseUrl} from '@/api/message/user'
export default { export default {
name: "Bell", name: "Bell",
computed: { computed: {
hidden() { hidden() {
return this.$store.state.message.unreadCount < 1 return this.$store.state.message.unreadCount < 1
} }
}, },
methods: {
jump() {
let target = baseUrl
if (this.$route.path === target) target = `/redirect${target}`
this.$router.replace(target)
}
},
mounted() { methods: {
this.$store.dispatch('message/refresh') jump() {
let target = baseUrl
if (this.$route.path === target) target = `/redirect${target}`
this.$router.replace(target)
} }
},
mounted() {
this.$store.dispatch('message/refresh')
} }
}
</script> </script>
<style> <style>
.bell-badge .el-badge__content { .bell-badge .el-badge__content {
top: 15px !important; top: 15px !important;
right: 10px !important; right: 10px !important;
} }
</style> </style>

@ -9,39 +9,39 @@
</template> </template>
<script> <script>
export default { export default {
data: () => ({data: []}), data: () => ({data: []}),
watch: { watch: {
$route(route) { $route(route) {
if (route.path.startsWith('/redirect')) return if (route.path.startsWith('/redirect')) return
this.getBreadcrumb()
}
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
this.data = this.$route.matched.filter(item => item.meta.title)
}
},
mounted() {
this.getBreadcrumb() this.getBreadcrumb()
} }
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
this.data = this.$route.matched.filter(item => item.meta.title)
}
},
mounted() {
this.getBreadcrumb()
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.app-breadcrumb { .app-breadcrumb {
margin-left: 8px; margin-left: 8px;
line-height: $nav-height; line-height: $nav-height;
.no-redirect { .no-redirect {
color: $--color-gray; color: $--color-gray;
cursor: text; cursor: text;
}
} }
}
</style> </style>

@ -1,29 +1,29 @@
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: 'Hamburger', name: 'Hamburger',
functional: true, functional: true,
props: {active: Boolean}, props: {active: Boolean},
render(h, context) { render(h, context) {
const {active} = context.props const {active} = context.props
const className = {'navbar-icon': true, 'el-icon-s-unfold': true, 'hamburger': true, 'is-active': active} const className = {'navbar-icon': true, 'el-icon-s-unfold': true, 'hamburger': true, 'is-active': active}
return ( return (
<div {...context.data}> <div {...context.data}>
<i class={className}/> <i class={className}/>
</div> </div>
) )
}
} }
}
</script> </script>
<style> <style>
.hamburger { .hamburger {
font-size: 20px; font-size: 20px;
} }
.hamburger.is-active { .hamburger.is-active {
transform: rotate(180deg); transform: rotate(180deg);
} }
</style> </style>

@ -3,149 +3,149 @@
<i class="el-icon-search navbar-icon" title="快捷搜索" @click.stop="click"/> <i class="el-icon-search navbar-icon" title="快捷搜索" @click.stop="click"/>
<el-select <el-select
ref="headerSearchSelect" ref="headerSearchSelect"
v-model="search" v-model="search"
class="header-search-select" class="header-search-select"
:remote-method="querySearch" :remote-method="querySearch"
default-first-option default-first-option
filterable filterable
placeholder="搜索页面" placeholder="搜索页面"
remote remote
popper-append-to-body popper-append-to-body
@change="change" @change="change"
> >
<el-option <el-option
v-for="item in options" v-for="item in options"
:key="item.path" :key="item.path"
:label="item.title.join(' > ')" :label="item.title.join(' > ')"
:value="item.path" :value="item.path"
/> />
</el-select> </el-select>
</div> </div>
</template> </template>
<script> <script>
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
export default { export default {
name: 'HeaderSearch', name: 'HeaderSearch',
data() { data() {
return { return {
search: '', search: '',
options: [], options: [],
searchPool: [], searchPool: [],
show: false, show: false,
fuse: null fuse: null
} }
},
computed: {
routes() {
return this.$store.state.resource.routes
}
},
watch: {
routes() {
this.searchPool = this.generateRoutes(this.routes)
this.initFuse(this.searchPool)
}, },
computed: { show(value) {
routes() { if (value) document.body.addEventListener('click', this.close)
return this.$store.state.resource.routes else document.body.removeEventListener('click', this.close)
} }
},
methods: {
click() {
this.show = !this.show
if (this.show) this.$refs.headerSearchSelect.focus()
}, },
watch: { close() {
routes() { this.$refs.headerSearchSelect.blur()
this.searchPool = this.generateRoutes(this.routes) this.search = ''
this.initFuse(this.searchPool) this.options = []
}, this.show = false
},
show(value) { change(val) {
if (value) document.body.addEventListener('click', this.close) this.close()
else document.body.removeEventListener('click', this.close) this.$router.push(val).catch(() => ({}))
}
}, },
methods: { initFuse(list) {
click() { this.fuse = new Fuse(list, {
this.show = !this.show shouldSort: true,
if (this.show) this.$refs.headerSearchSelect.focus() threshold: 0.4,
}, location: 0,
distance: 100,
close() { maxPatternLength: 32,
this.$refs.headerSearchSelect.blur() minMatchCharLength: 1,
this.search = '' keys: ['title']
this.options = [] })
this.show = false },
},
generateRoutes(routes, prefixTitle = []) {
change(val) { let res = []
this.close() for (const route of routes) {
this.$router.push(val).catch(() => ({})) const data = {path: route.fullPath, title: [...prefixTitle]}
},
if (route.meta && route.meta.title) {
initFuse(list) { data.title = [...data.title, route.meta.title]
this.fuse = new Fuse(list, { if (route.name) res.push(data)
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ['title']
})
},
generateRoutes(routes, prefixTitle = []) {
let res = []
for (const route of routes) {
const data = {path: route.fullPath, title: [...prefixTitle]}
if (route.meta && route.meta.title) {
data.title = [...data.title, route.meta.title]
if (route.name) res.push(data)
}
if (route.children) {
const tempRoutes = this.generateRoutes(route.children, data.title)
if (tempRoutes.length > 0) res.push(...tempRoutes)
}
} }
return res
},
querySearch(query) { if (route.children) {
this.options = query ? this.fuse.search(query) : [] const tempRoutes = this.generateRoutes(route.children, data.title)
if (tempRoutes.length > 0) res.push(...tempRoutes)
}
} }
return res
}, },
mounted() { querySearch(query) {
this.searchPool = this.generateRoutes(this.routes) this.options = query ? this.fuse.search(query) : []
this.initFuse(this.searchPool)
} }
},
mounted() {
this.searchPool = this.generateRoutes(this.routes)
this.initFuse(this.searchPool)
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.header-search { .header-search {
.header-search-select { .header-search-select {
font-size: 18px; font-size: 18px;
transition: width 0.2s; transition: width 0.2s;
width: 0; width: 0;
overflow: hidden; overflow: hidden;
background: transparent; background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
.el-input__inner {
border-radius: 0; border-radius: 0;
display: inline-block; border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle; vertical-align: middle;
.el-input__inner {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
}
} }
}
&.show { &.show {
.header-search-select { .header-search-select {
width: 210px; width: 210px;
margin-left: 10px; margin-left: 10px;
}
} }
} }
}
</style> </style>

@ -10,7 +10,7 @@
<el-divider>其他设置</el-divider> <el-divider>其他设置</el-divider>
<div v-for="{name,key} in settings" :key="key" class="drawer-item"> <div v-for="{name,key} in settings" :key="key" class="drawer-item">
<span>{{name}}</span> <span>{{ name }}</span>
<el-switch :value="getValue(key)" class="drawer-switch" @input="e => setValue(e,key)"/> <el-switch :value="getValue(key)" class="drawer-switch" @input="e => setValue(e,key)"/>
</div> </div>
</div> </div>
@ -18,70 +18,70 @@
</template> </template>
<script> <script>
import ColorCheckBox from "@/components/ColorCheckBox" import ColorCheckBox from "@/components/ColorCheckBox"
import ColorCheckboxGroup from "@/components/ColorCheckBox/group" import ColorCheckboxGroup from "@/components/ColorCheckBox/group"
import {isDev} from '@/config' import {isDev} from '@/config'
import client from 'webpack-theme-color-replacer/client' import client from 'webpack-theme-color-replacer/client'
import forElementUI from 'webpack-theme-color-replacer/forElementUI' import forElementUI from 'webpack-theme-color-replacer/forElementUI'
const settings = [ const settings = [
{name: '显示logo', key: 'showLogo'}, {name: '显示logo', key: 'showLogo'},
{name: '显示面包屑', key: 'showBreadcrumb'}, {name: '显示面包屑', key: 'showBreadcrumb'},
{name: '使用多页签', key: 'useTagsView'}, {name: '使用多页签', key: 'useTagsView'},
{name: '侧边栏手风琴效果', key: 'sidebarUniqueOpen'}, {name: '侧边栏手风琴效果', key: 'sidebarUniqueOpen'},
{name: '侧边栏折叠', key: 'sidebarCollapse'}, {name: '侧边栏折叠', key: 'sidebarCollapse'},
{name: '折叠菜单显示上级', key: 'sidebarShowParent'}, {name: '折叠菜单显示上级', key: 'sidebarShowParent'},
{name: '侧边栏自动隐藏', key: 'sidebarAutoHidden'}, {name: '侧边栏自动隐藏', key: 'sidebarAutoHidden'},
{name: '头部自动隐藏', key: 'headerAutoHidden'}, {name: '头部自动隐藏', key: 'headerAutoHidden'},
{name: '显示返回顶部按钮', key: 'showBackToTop'} {name: '显示返回顶部按钮', key: 'showBackToTop'}
] ]
const colors = ['#f5222d', '#fa541c', '#fadb14', '#3eaf7c', '#13c2c2', '#1890ff', '#722ed1', '#eb2f96'] const colors = ['#f5222d', '#fa541c', '#fadb14', '#3eaf7c', '#13c2c2', '#1890ff', '#722ed1', '#eb2f96']
export default { export default {
name: "SettingDrawer", name: "SettingDrawer",
components: {ColorCheckBox, ColorCheckboxGroup}, components: {ColorCheckBox, ColorCheckboxGroup},
props: {value: Boolean}, props: {value: Boolean},
data() { data() {
return { return {
settings, settings,
colors, colors,
color: '#1890ff' color: '#1890ff'
} }
}, },
methods: { methods: {
changeColor(color) { changeColor(color) {
this.color = color this.color = color
isDev && client.changer.changeColor({newColors: forElementUI.getElementUISeries(color)}) isDev && client.changer.changeColor({newColors: forElementUI.getElementUISeries(color)})
}, },
getValue(key) { getValue(key) {
return this.$store.state.setting[key] return this.$store.state.setting[key]
}, },
setValue(value, key) { setValue(value, key) {
this.$store.commit(`setting/${key}`, value) this.$store.commit(`setting/${key}`, value)
}
} }
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.drawer-container { .drawer-container {
padding: 24px; padding: 24px;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
word-wrap: break-word; word-wrap: break-word;
.drawer-item { .drawer-item {
color: rgba(0, 0, 0, .65); color: rgba(0, 0, 0, .65);
font-size: 14px; font-size: 14px;
padding: 12px 0; padding: 12px 0;
} }
.drawer-switch { .drawer-switch {
float: right float: right
}
} }
}
</style> </style>

@ -18,13 +18,13 @@
</div> </div>
<el-dropdown <el-dropdown
class="navbar-item" class="navbar-item"
trigger="click" trigger="click"
@visible-change="$emit('menu-show',$event)" @visible-change="$emit('menu-show',$event)"
> >
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<el-avatar :size="30" :src="avatar" icon="el-icon-user-solid"/> <el-avatar :size="30" :src="avatar" icon="el-icon-user-solid"/>
<span class="hidden-xs">{{name}}</span> <span class="hidden-xs">{{ name }}</span>
</div> </div>
<el-dropdown-menu class="user-dropdown" slot="dropdown"> <el-dropdown-menu class="user-dropdown" slot="dropdown">
<router-link to="/user/index"> <router-link to="/user/index">
@ -48,55 +48,55 @@
</template> </template>
<script> <script>
import Hamburger from './components/Hamburger' import Hamburger from './components/Hamburger'
import Breadcrumb from './components/Breadcrumb' import Breadcrumb from './components/Breadcrumb'
import Search from './components/HeaderSearch' import Search from './components/HeaderSearch'
import Bell from "./components/Bell" import Bell from "./components/Bell"
import SettingDrawer from './components/SettingDrawer' import SettingDrawer from './components/SettingDrawer'
import {mapState} from 'vuex' import {mapState} from 'vuex'
import GuideMixin from '@/mixins/guide' import GuideMixin from '@/mixins/guide'
import {auth} from "@/utils/auth" import {auth} from "@/utils/auth"
import {elConfirm} from "@/utils/message" import {elConfirm} from "@/utils/message"
export default { export default {
name: 'navbar', name: 'navbar',
mixins: [GuideMixin.navbar], mixins: [GuideMixin.navbar],
components: {Hamburger, Breadcrumb, Search, Bell, SettingDrawer}, components: {Hamburger, Breadcrumb, Search, Bell, SettingDrawer},
data() { data() {
return { return {
settingDrawer: false settingDrawer: false
} }
}, },
computed: { computed: {
...mapState('user', ['avatar', 'name', 'prepareLogout']), ...mapState('user', ['avatar', 'name', 'prepareLogout']),
...mapState('setting', ['sidebarCollapse', 'showBreadcrumb']), ...mapState('setting', ['sidebarCollapse', 'showBreadcrumb']),
showSystemResource() { showSystemResource() {
return auth('/system/resource') return auth('/system/resource')
} }
}, },
methods: { methods: {
sidebarCtrl() { sidebarCtrl() {
this.$store.commit('setting/sidebarCollapse', !this.sidebarCollapse) this.$store.commit('setting/sidebarCollapse', !this.sidebarCollapse)
}, },
refresh() { refresh() {
this.$router.replace({path: `/redirect${this.$route.fullPath}`}) this.$router.replace({path: `/redirect${this.$route.fullPath}`})
}, },
logout() { logout() {
if (this.prepareLogout) return if (this.prepareLogout) return
elConfirm('确认退出?') elConfirm('确认退出?')
.then(() => this.$store.dispatch('user/logout')) .then(() => this.$store.dispatch('user/logout'))
}
} }
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -1,26 +1,26 @@
<script type="text/jsx"> <script type="text/jsx">
import {sidebarLogoUrl, title} from '@/config' import {sidebarLogoUrl, title} from '@/config'
export default { export default {
name: 'SidebarLogo', name: 'SidebarLogo',
functional: true, functional: true,
props: {collapse: Boolean}, props: {collapse: Boolean},
render(h, context) { render(h, context) {
const {collapse} = context.props const {collapse} = context.props
const logoClass = {'sidebar-logo-container': true, 'collapse': collapse} const logoClass = {'sidebar-logo-container': true, 'collapse': collapse}
return ( return (
<div class={logoClass}> <div class={logoClass}>
<router-link class="sidebar-logo-link" to="/"> <router-link class="sidebar-logo-link" to="/">
{sidebarLogoUrl && <img src={sidebarLogoUrl} class="sidebar-logo"/>} {sidebarLogoUrl && <img src={sidebarLogoUrl} class="sidebar-logo"/>}
{!collapse && <h1 class="sidebar-title">{title}</h1>} {!collapse && <h1 class="sidebar-title">{title}</h1>}
</router-link> </router-link>
</div> </div>
) )
}
} }
}
</script> </script>

@ -1,72 +1,72 @@
<script type="text/jsx"> <script type="text/jsx">
import SidebarItemContent from './SidebarItemContent' import SidebarItemContent from './SidebarItemContent'
function getOnlyChild(item) { function getOnlyChild(item) {
const children = item.children || [] const children = item.children || []
if (children.length === 1) return item.alwaysShow ? null : children[0] if (children.length === 1) return item.alwaysShow ? null : children[0]
if (children.length < 1) return {...item, path: undefined, children: undefined} if (children.length < 1) return {...item, path: undefined, children: undefined}
return null return null
} }
function renderNode(h, {item, showParent, collapse}) { function renderNode(h, {item, showParent, collapse}) {
let onlyOneChild = getOnlyChild(item) let onlyOneChild = getOnlyChild(item)
const showSingle = onlyOneChild && !onlyOneChild.children const showSingle = onlyOneChild && !onlyOneChild.children
if (showSingle) { if (showSingle) {
const {icon, title} = onlyOneChild.meta const {icon, title} = onlyOneChild.meta
return ( return (
<el-menu-item index={onlyOneChild.fullPath}> <el-menu-item index={onlyOneChild.fullPath}>
<SidebarItemContent icon={icon} title={title}/> <SidebarItemContent icon={icon} title={title}/>
</el-menu-item> </el-menu-item>
) )
} }
else { else {
const {icon, title} = item.meta const {icon, title} = item.meta
let children = item.children.map(child => renderNode(h, { let children = item.children.map(child => renderNode(h, {
item: child, item: child,
showParent, showParent,
collapse collapse
})) }))
//mouseleavemouseleave //mouseleavemouseleave
if (collapse) { if (collapse) {
// //
if (showParent) { if (showParent) {
children = [ children = [
<div class="popover-menu__title el-menu-item"> <div class="popover-menu__title el-menu-item">
<SidebarItemContent icon={icon} title={title}/> <SidebarItemContent icon={icon} title={title}/>
</div>, </div>,
<div class="el-menu el-menu--inline">{children}</div> <div class="el-menu el-menu--inline">{children}</div>
] ]
}
} }
return (
<el-submenu index={item.fullPath} popper-append-to-body>
<SidebarItemContent slot="title" icon={icon} title={title}/>
{children}
</el-submenu>
)
} }
return (
<el-submenu index={item.fullPath} popper-append-to-body>
<SidebarItemContent slot="title" icon={icon} title={title}/>
{children}
</el-submenu>
)
} }
}
export default { export default {
functional: true, functional: true,
props: { props: {
item: Object, item: Object,
showParent: Boolean, showParent: Boolean,
collapse: Boolean collapse: Boolean
}, },
render(h, context) { render(h, context) {
return renderNode(h, context.props) return renderNode(h, context.props)
}
} }
}
</script> </script>

@ -1,31 +1,31 @@
<script type="text/jsx"> <script type="text/jsx">
export default { export default {
name: "SidebarItemContent", name: "SidebarItemContent",
functional: true, functional: true,
props: { props: {
icon: String, icon: String,
title: String title: String
}, },
render(h, context) { render(h, context) {
const {icon, title} = context.props const {icon, title} = context.props
const vnodes = [] const vnodes = []
let iconComponent let iconComponent
if (icon) { if (icon) {
const isElIcon = icon.startsWith('el-icon-') const isElIcon = icon.startsWith('el-icon-')
iconComponent = isElIcon ? <i class={`svg-icon ${icon}`}/> : <svg-icon icon={icon}/> iconComponent = isElIcon ? <i class={`svg-icon ${icon}`}/> : <svg-icon icon={icon}/>
} }
else iconComponent = <span class="svg-icon"/> else iconComponent = <span class="svg-icon"/>
vnodes.push(iconComponent) vnodes.push(iconComponent)
if (title) vnodes.push(<span slot="title" class="sidebar-item-content">{title}</span>) if (title) vnodes.push(<span slot="title" class="sidebar-item-content">{title}</span>)
return vnodes return vnodes
}
} }
}
</script> </script>

@ -1,149 +1,149 @@
<script type="text/jsx"> <script type="text/jsx">
import {mapState} from 'vuex' import {mapState} from 'vuex'
import Logo from './components/Logo' import Logo from './components/Logo'
import SidebarItem from './components/SidebarItem' import SidebarItem from './components/SidebarItem'
import {isExternal} from "@/utils/validate" import {isExternal} from "@/utils/validate"
export default { export default {
name: 'sidebar', name: 'sidebar',
components: {SidebarItem, Logo}, components: {SidebarItem, Logo},
data() { data() {
return { return {
mouseOutside: true mouseOutside: true
}
},
computed: {
...mapState('app', ['device']),
...mapState('resource', ['sidebarMenus']),
...mapState('setting', ['showLogo', 'sidebarCollapse', 'sidebarUniqueOpen', 'sidebarShowParent', 'sidebarAutoHidden']),
activeMenu() {
const route = this.$route
const {meta, path} = route
if (path.startsWith('/redirect')) {
return path.replace('/redirect', '')
} }
return meta.activeMenu || path
}, },
computed: { //pc
...mapState('app', ['device']), collapse() {
return this.sidebarCollapse && this.device === 'pc'
...mapState('resource', ['sidebarMenus']),
...mapState('setting', ['showLogo', 'sidebarCollapse', 'sidebarUniqueOpen', 'sidebarShowParent', 'sidebarAutoHidden']),
activeMenu() {
const route = this.$route
const {meta, path} = route
if (path.startsWith('/redirect')) {
return path.replace('/redirect', '')
}
return meta.activeMenu || path
},
//pc
collapse() {
return this.sidebarCollapse && this.device === 'pc'
},
hideSidebar() {
return this.sidebarAutoHidden && this.mouseOutside
|| this.sidebarCollapse && this.device === 'mobile'
},
sidebarClass() {
return {
'sidebar-container': true,
'mobile': this.device === 'mobile',
'collapse-sidebar': this.collapse,
'hide-sidebar': this.hideSidebar
}
}
}, },
watch: { hideSidebar() {
//elMenuinitOpenedMenu()select return this.sidebarAutoHidden && this.mouseOutside
'$route.path'(nv, ov) { || this.sidebarCollapse && this.device === 'mobile'
//redirect
if (nv === `/redirect${ov}`) return
const menu = this.$refs.menu
if (!menu) return
const item = menu.items[this.activeMenu]
if (!item) menu.openedMenus = []
else this.select(item.index, item.indexPath, item, false)
},
hideSidebar(v) {
if (v) document.addEventListener('mousemove', this.moveEvent)
else document.removeEventListener('mousemove', this.moveEvent)
}
}, },
methods: { sidebarClass() {
moveEvent(e) { return {
if (e.clientX <= 15) this.mouseOutside = false 'sidebar-container': true,
}, 'mobile': this.device === 'mobile',
'collapse-sidebar': this.collapse,
select(index, indexPath, item, jump = true) { 'hide-sidebar': this.hideSidebar
//
if (this.sidebarUniqueOpen && indexPath.length === 1) {
const menu = this.$refs.menu
const opened = menu.openedMenus
opened.forEach(i => i !== index && menu.closeMenu(i))
}
//mobile
this.device === 'mobile' && this.$store.commit('setting/sidebarCollapse', true)
if (!jump) return
//
if (isExternal(index)) {
return window.open(index)
}
if (this.$route.path === index) {
this.$store.commit('tagsView/delCachedView', this.$route)
this.$nextTick(() => this.$router.replace({path: '/redirect' + this.$route.fullPath}))
}
else this.$router.push(index)
} }
}
},
watch: {
//elMenuinitOpenedMenu()select
'$route.path'(nv, ov) {
//redirect
if (nv === `/redirect${ov}`) return
const menu = this.$refs.menu
if (!menu) return
const item = menu.items[this.activeMenu]
if (!item) menu.openedMenus = []
else this.select(item.index, item.indexPath, item, false)
}, },
mounted() { hideSidebar(v) {
if (this.sidebarAutoHidden) { if (v) document.addEventListener('mousemove', this.moveEvent)
document.addEventListener('mousemove', this.moveEvent) else document.removeEventListener('mousemove', this.moveEvent)
} }
},
this.$once('hook:beforeDestroy', () => { methods: {
document.removeEventListener('mousemove', this.moveEvent) moveEvent(e) {
}) if (e.clientX <= 15) this.mouseOutside = false
}, },
render() { select(index, indexPath, item, jump = true) {
const menu = ( //
<el-menu if (this.sidebarUniqueOpen && indexPath.length === 1) {
ref="menu" const menu = this.$refs.menu
collapse={this.collapse} const opened = menu.openedMenus
collapse-transition={false} opened.forEach(i => i !== index && menu.closeMenu(i))
default-active={this.activeMenu} }
unique-opened={this.sidebarUniqueOpen}
mode="vertical" //mobile
on-select={this.select} this.device === 'mobile' && this.$store.commit('setting/sidebarCollapse', true)
>
{this.sidebarMenus.map(route => ( if (!jump) return
<sidebar-item
key={route.path} //
show-parent={this.sidebarShowParent} if (isExternal(index)) {
collapse={this.sidebarCollapse} return window.open(index)
item={route} }
/>
))} if (this.$route.path === index) {
</el-menu> this.$store.commit('tagsView/delCachedView', this.$route)
) this.$nextTick(() => this.$router.replace({path: '/redirect' + this.$route.fullPath}))
}
return ( else this.$router.push(index)
<aside
class={this.sidebarClass}
on-mouseenter={() => this.mouseOutside = false}
on-mouseleave={() => this.mouseOutside = true}
>
{this.showLogo && <logo collapse={this.collapse}/>}
{<el-scrollbar noresize native>{menu}</el-scrollbar>}
</aside>
)
} }
},
mounted() {
if (this.sidebarAutoHidden) {
document.addEventListener('mousemove', this.moveEvent)
}
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('mousemove', this.moveEvent)
})
},
render() {
const menu = (
<el-menu
ref="menu"
collapse={this.collapse}
collapse-transition={false}
default-active={this.activeMenu}
unique-opened={this.sidebarUniqueOpen}
mode="vertical"
on-select={this.select}
>
{this.sidebarMenus.map(route => (
<sidebar-item
key={route.path}
show-parent={this.sidebarShowParent}
collapse={this.sidebarCollapse}
item={route}
/>
))}
</el-menu>
)
return (
<aside
class={this.sidebarClass}
on-mouseenter={() => this.mouseOutside = false}
on-mouseleave={() => this.mouseOutside = true}
>
{this.showLogo && <logo collapse={this.collapse}/>}
{<el-scrollbar noresize native>{menu}</el-scrollbar>}
</aside>
)
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -5,89 +5,89 @@
</template> </template>
<script> <script>
const tagAndTagSpacing = 4 const tagAndTagSpacing = 4
export default { export default {
name: 'ScrollPane', name: 'ScrollPane',
data: () => ({rAF: null}), data: () => ({rAF: null}),
computed: { computed: {
scrollWrapper() { scrollWrapper() {
return this.$refs.scrollContainer.$refs.wrap return this.$refs.scrollContainer.$refs.wrap
},
}, },
},
methods: { methods: {
handleScroll(e) { handleScroll(e) {
const eventDelta = e.wheelDelta || -(e.detail || 0) * 40 const eventDelta = e.wheelDelta || -(e.detail || 0) * 40
if (eventDelta > 0 && this.scrollWrapper.scrollLeft === 0) return if (eventDelta > 0 && this.scrollWrapper.scrollLeft === 0) return
if (eventDelta < 0 && this.scrollWrapper.scrollWidth <= this.scrollWrapper.scrollLeft + this.scrollWrapper.clientWidth) return if (eventDelta < 0 && this.scrollWrapper.scrollWidth <= this.scrollWrapper.scrollLeft + this.scrollWrapper.clientWidth) return
this.smoothScroll(-eventDelta * 2) this.smoothScroll(-eventDelta * 2)
}, },
moveToTarget(currentTag) { moveToTarget(currentTag) {
const $container = this.$refs.scrollContainer.$el const $container = this.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.scrollWrapper const $scrollWrapper = this.scrollWrapper
const tagList = this.$parent.$refs.tag const tagList = this.$parent.$refs.tag
let firstTag = null let firstTag = null
let lastTag = null let lastTag = null
// find first tag and last tag // find first tag and last tag
if (tagList.length > 0) { if (tagList.length > 0) {
firstTag = tagList[0] firstTag = tagList[0]
lastTag = tagList[tagList.length - 1] lastTag = tagList[tagList.length - 1]
} }
if (firstTag === currentTag) { if (firstTag === currentTag) {
this.scrollLeft(0) this.scrollLeft(0)
} }
else if (lastTag === currentTag) { else if (lastTag === currentTag) {
this.scrollLeft($scrollWrapper.scrollWidth - $containerWidth) this.scrollLeft($scrollWrapper.scrollWidth - $containerWidth)
} }
else { else {
// find preTag and nextTag // find preTag and nextTag
const currentIndex = tagList.findIndex(item => item === currentTag) const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1] const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1] const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag // the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
// the tag's offsetLeft before of prevTag // the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) { if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
this.scrollLeft(afterNextTagOffsetLeft - $containerWidth) this.scrollLeft(afterNextTagOffsetLeft - $containerWidth)
} }
else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) { else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
this.scrollLeft(beforePrevTagOffsetLeft) this.scrollLeft(beforePrevTagOffsetLeft)
}
} }
}, }
},
scrollLeft(val) { scrollLeft(val) {
this.scrollWrapper.scrollTo({ this.scrollWrapper.scrollTo({
left: val, left: val,
behavior: 'smooth' behavior: 'smooth'
}) })
}, },
smoothScroll(val) { smoothScroll(val) {
if (this.rAF) window.cancelAnimationFrame(this.rAF) if (this.rAF) window.cancelAnimationFrame(this.rAF)
let cost = 300, times = cost / 16, gap = val / times + 1 let cost = 300, times = cost / 16, gap = val / times + 1
const frameFunc = () => { const frameFunc = () => {
if (times > 0) { if (times > 0) {
this.scrollWrapper.scrollLeft += gap this.scrollWrapper.scrollLeft += gap
this.rAF = window.requestAnimationFrame(frameFunc) this.rAF = window.requestAnimationFrame(frameFunc)
times-- times--
}
else window.cancelAnimationFrame(this.rAF)
} }
frameFunc() else window.cancelAnimationFrame(this.rAF)
} }
frameFunc()
} }
} }
}
</script> </script>

@ -2,15 +2,15 @@
<nav class="tags-view-container"> <nav class="tags-view-container">
<scroll-pane class="tags-view-wrapper" ref="scrollPane"> <scroll-pane class="tags-view-wrapper" ref="scrollPane">
<router-link <router-link
ref="tag" ref="tag"
v-for="tag in visitedViews" v-for="tag in visitedViews"
:key="tag.path" :key="tag.path"
:class="{active:isActive(tag)}" :class="{active:isActive(tag)}"
class="tags-view-item" class="tags-view-item"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="div" tag="div"
@contextmenu.prevent.native="e=>openMenu(tag,e)" @contextmenu.prevent.native="e=>openMenu(tag,e)"
@dblclick.prevent.native="()=>closeSelectedTag(tag)" @dblclick.prevent.native="()=>closeSelectedTag(tag)"
> >
{{ tag.title }} {{ tag.title }}
<i v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="()=>closeSelectedTag(tag)"/> <i v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="()=>closeSelectedTag(tag)"/>
@ -29,159 +29,159 @@
</template> </template>
<script> <script>
import ScrollPane from './ScrollPane' import ScrollPane from './ScrollPane'
import ContextMenu from "@/components/ContextMenu" import ContextMenu from "@/components/ContextMenu"
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem" import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
import shortcuts from './mixin/shortcuts' import shortcuts from './mixin/shortcuts'
import decideRouterTransition from './mixin/decideRouterTransition' import decideRouterTransition from './mixin/decideRouterTransition'
export default { export default {
mixins: [shortcuts, decideRouterTransition], mixins: [shortcuts, decideRouterTransition],
components: {ContextMenu, ContextMenuItem, ScrollPane}, components: {ContextMenu, ContextMenuItem, ScrollPane},
data() { data() {
return { return {
contextmenu: { contextmenu: {
show: false, show: false,
top: 0, top: 0,
left: 0, left: 0,
},
selectedTag: {},
affixTags: [],
scroller: null
}
},
computed: {
visitedViews() {
return this.$store.state.tagsView.visitedViews
}, },
selectedTag: {},
affixTags: [],
scroller: null
}
},
routes() { computed: {
return this.$store.state.resource.routes visitedViews() {
} return this.$store.state.tagsView.visitedViews
}, },
watch: { routes() {
$route(to, from) { return this.$store.state.resource.routes
this.decideRouteTransition && this.decideRouteTransition(to, from) }
this.addTags(to).then(() => this.moveToCurrentTag()) },
},
'contextmenu.show'(v) { watch: {
this.$emit('menu-show', v) $route(to, from) {
} this.decideRouteTransition && this.decideRouteTransition(to, from)
this.addTags(to).then(() => this.moveToCurrentTag())
}, },
methods: { 'contextmenu.show'(v) {
isActive(route) { this.$emit('menu-show', v)
return route.path === this.$route.path }
}, },
isAffix(tag) {
return tag.meta && tag.meta.affix
},
filterAffixTags(routes) {
const tags = []
routes.forEach(route => {
if (route.name && route.meta && route.meta.affix) {
tags.push({
//fullPath$route.fullPath
fullPath: route.fullPath,
path: route.fullPath,
name: route.name,
meta: {...route.meta}
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children)
tempTags.length > 0 && tags.push(...tempTags)
}
})
return tags
},
initTags() {
this.affixTags = this.filterAffixTags(this.routes)
for (let tag of this.affixTags) {
this.$store.commit('tagsView/addVisitedView', tag)
}
},
//nametab
addTags(to = this.$route) {
return to.name ? this.$store.dispatch('tagsView/addView', to) : Promise.resolve()
},
//tab methods: {
moveToCurrentTag() { isActive(route) {
this.$nextTick(() => { return route.path === this.$route.path
const tag = this.$refs.tag.find(i => i.to.path === this.$route.path) },
if (!tag) return isAffix(tag) {
this.$refs.scrollPane.moveToTarget(tag) return tag.meta && tag.meta.affix
}) },
},
/* filterAffixTags(routes) {
* 右键菜单选项 const tags = []
* 刷新所选关闭所选关闭其他关闭所有 routes.forEach(route => {
* */ if (route.name && route.meta && route.meta.affix) {
refreshSelectedTag(view) { tags.push({
this.$store.commit('tagsView/delCachedView', view) //fullPath$route.fullPath
this.$nextTick(() => this.$router.replace({path: `/redirect${view.fullPath}`})) fullPath: route.fullPath,
}, path: route.fullPath,
closeSelectedTag(view) { name: route.name,
if (this.isAffix(view)) return meta: {...route.meta}
this.$store
.dispatch('tagsView/delView', view)
.then(() => this.isActive(view) && this.gotoLastView())
},
closeOthersTags() {
this.$store
.dispatch('tagsView/delOthersViews', this.selectedTag)
.then(() => {
if (this.selectedTag.path !== this.$route.path) {
return this.$router.push(this.selectedTag)
}
}) })
}, }
closeAllTags() { if (route.children) {
this.$store const tempTags = this.filterAffixTags(route.children)
.dispatch('tagsView/delAllViews') tempTags.length > 0 && tags.push(...tempTags)
.then(() => this.gotoLastView()) }
}, })
return tags
gotoLastView() { },
if (this.visitedViews.length === 0) return this.$router.push('/') initTags() {
const latest = this.visitedViews[this.visitedViews.length - 1] this.affixTags = this.filterAffixTags(this.routes)
if (this.$route.path !== latest.path) this.$router.push(latest.path) for (let tag of this.affixTags) {
}, this.$store.commit('tagsView/addVisitedView', tag)
}
},
openMenu(tag, e) { //nametab
const menuMinWidth = 105 addTags(to = this.$route) {
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left return to.name ? this.$store.dispatch('tagsView/addView', to) : Promise.resolve()
const offsetWidth = this.$el.offsetWidth // container width },
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
this.contextmenu.left = left > maxLeft ? maxLeft : left //tab
moveToCurrentTag() {
this.$nextTick(() => {
const tag = this.$refs.tag.find(i => i.to.path === this.$route.path)
if (!tag) return
this.$refs.scrollPane.moveToTarget(tag)
})
},
this.contextmenu.top = e.clientY /*
this.contextmenu.show = true * 右键菜单选项
this.selectedTag = tag * 刷新所选关闭所选关闭其他关闭所有
} * */
refreshSelectedTag(view) {
this.$store.commit('tagsView/delCachedView', view)
this.$nextTick(() => this.$router.replace({path: `/redirect${view.fullPath}`}))
},
closeSelectedTag(view) {
if (this.isAffix(view)) return
this.$store
.dispatch('tagsView/delView', view)
.then(() => this.isActive(view) && this.gotoLastView())
},
closeOthersTags() {
this.$store
.dispatch('tagsView/delOthersViews', this.selectedTag)
.then(() => {
if (this.selectedTag.path !== this.$route.path) {
return this.$router.push(this.selectedTag)
}
})
},
closeAllTags() {
this.$store
.dispatch('tagsView/delAllViews')
.then(() => this.gotoLastView())
}, },
beforeDestroy() { gotoLastView() {
//fade if (this.visitedViews.length === 0) return this.$router.push('/')
this.$store.commit('tagsView/transitionName', 'el-fade-in-linear') const latest = this.visitedViews[this.visitedViews.length - 1]
if (this.$route.path !== latest.path) this.$router.push(latest.path)
}, },
mounted() { openMenu(tag, e) {
this.initTags() const menuMinWidth = 105
this.addTags() const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
this.contextmenu.left = left > maxLeft ? maxLeft : left
this.contextmenu.top = e.clientY
this.contextmenu.show = true
this.selectedTag = tag
} }
},
beforeDestroy() {
//fade
this.$store.commit('tagsView/transitionName', 'el-fade-in-linear')
},
mounted() {
this.initTags()
this.addTags()
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -15,68 +15,68 @@
</template> </template>
<script> <script>
import {mapState} from 'vuex' import {mapState} from 'vuex'
import VMain from './components/Main' import VMain from './components/Main'
import VHeader from './components/Header' import VHeader from './components/Header'
import VSidebar from './components/Sidebar' import VSidebar from './components/Sidebar'
import offlineMixin from "@/layout/mixin/offline" import offlineMixin from "@/layout/mixin/offline"
import resizeMixin from './mixin/resize' import resizeMixin from './mixin/resize'
export default { export default {
name: 'Layout', name: 'Layout',
mixins: [offlineMixin, resizeMixin], mixins: [offlineMixin, resizeMixin],
components: {VMain, VSidebar, VHeader}, components: {VMain, VSidebar, VHeader},
computed: { computed: {
...mapState('app', ['device', 'hasHeader']), ...mapState('app', ['device', 'hasHeader']),
...mapState('setting', ['useTagsView', 'sidebarCollapse']), ...mapState('setting', ['useTagsView', 'sidebarCollapse']),
containerClass() { containerClass() {
return { return {
'el-container': true, 'el-container': true,
'main-container': true, 'main-container': true,
'has-header': this.hasHeader, 'has-header': this.hasHeader,
'has-tags-view': this.useTagsView 'has-tags-view': this.useTagsView
}
},
showSidebarMask() {
return !this.sidebarCollapse && this.device === 'mobile'
} }
}, },
methods: { showSidebarMask() {
collapseSidebar() { return !this.sidebarCollapse && this.device === 'mobile'
this.$store.commit('setting/sidebarCollapse', true) }
} },
methods: {
collapseSidebar() {
this.$store.commit('setting/sidebarCollapse', true)
} }
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.app-wrapper { .app-wrapper {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
flex-direction: row; flex-direction: row;
.drawer-bg { .drawer-bg {
background: rgba(0, 0, 0, .5); background: rgba(0, 0, 0, .5);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
width: 100%; width: 100%;
top: 0; top: 0;
height: 100%; height: 100%;
position: fixed; position: fixed;
z-index: 9; z-index: 9;
} }
.main-container { .main-container {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
flex-direction: column; flex-direction: column;
}
} }
}
</style> </style>

@ -19,105 +19,105 @@
</template> </template>
<script> <script>
export default { export default {
name: 'Page403' name: 'Page403'
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.error-container { .error-container {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
position: absolute; position: absolute;
top: 40%; top: 40%;
left: 50%; left: 50%;
> div {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
> div { .pic {
position: relative; position: relative;
width: 1200px; float: left;
padding: 0 50px; width: 600px;
overflow: hidden; overflow: hidden;
}
.pic { .bullshit {
position: relative; position: relative;
float: left; float: left;
width: 600px; width: 300px;
overflow: hidden; padding: 30px 0;
} overflow: hidden;
.bullshit { &__oops {
position: relative; font-size: 32px;
float: left; font-weight: bold;
width: 300px; line-height: 40px;
padding: 30px 0; color: #1482f0;
overflow: hidden; opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__oops { &__headline {
font-size: 32px; font-size: 20px;
font-weight: bold; line-height: 24px;
line-height: 40px; color: #222;
color: #1482f0; font-weight: bold;
opacity: 0; opacity: 0;
margin-bottom: 20px; margin-bottom: 10px;
animation-name: slideUp; animation-name: slideUp;
animation-duration: 0.5s; animation-duration: 0.5s;
animation-fill-mode: forwards; animation-delay: 0.1s;
} animation-fill-mode: forwards;
}
&__headline { &__info {
font-size: 20px; font-size: 13px;
line-height: 24px; line-height: 21px;
color: #222; color: grey;
font-weight: bold; opacity: 0;
opacity: 0; margin-bottom: 30px;
margin-bottom: 10px; animation-name: slideUp;
animation-name: slideUp; animation-duration: 0.5s;
animation-duration: 0.5s; animation-delay: 0.2s;
animation-delay: 0.1s; animation-fill-mode: forwards;
animation-fill-mode: forwards; }
}
&__info { &__return-home {
font-size: 13px; display: block;
line-height: 21px; float: left;
color: grey; width: 110px;
opacity: 0; height: 36px;
margin-bottom: 30px; background: #1482f0;
animation-name: slideUp; border-radius: 100px;
animation-duration: 0.5s; text-align: center;
animation-delay: 0.2s; color: #ffffff;
animation-fill-mode: forwards; opacity: 0;
} font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
&__return-home { @keyframes slideUp {
display: block; 0% {
float: left; transform: translateY(60px);
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0; opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
} }
100% {
@keyframes slideUp { transform: translateY(0);
0% { opacity: 1;
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
} }
} }
} }
} }
}
</style> </style>

@ -22,204 +22,204 @@
</template> </template>
<script> <script>
export default { export default {
name: 'Page404' name: 'Page404'
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.error-container { .error-container {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
position: absolute; position: absolute;
top: 40%; top: 40%;
left: 50%; left: 50%;
> div { > div {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic {
position: relative; position: relative;
width: 1200px; float: left;
padding: 0 50px; width: 600px;
overflow: hidden; overflow: hidden;
.pic { &__parent {
position: relative; width: 100%;
float: left; }
width: 600px;
overflow: hidden; &__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation: cloudLeft 2s linear 1s infinite forwards;
}
&__parent { &.mid {
width: 100%; width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation: cloudMid 2s linear 1.2s infinite forwards;
} }
&__child { &.right {
position: absolute; width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation: cloudRight 2s linear 1s infinite forwards;
}
&.left { @keyframes cloudLeft {
width: 80px; 0% {
top: 17px; top: 17px;
left: 220px; left: 220px;
opacity: 0; opacity: 0;
animation: cloudLeft 2s linear 1s infinite forwards;
} }
20% {
&.mid { top: 33px;
width: 46px; left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px; top: 10px;
left: 420px; left: 420px;
opacity: 0; opacity: 0;
animation: cloudMid 2s linear 1.2s infinite forwards;
} }
20% {
&.right { top: 40px;
width: 62px; left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px; top: 100px;
left: 500px; left: 500px;
opacity: 0; opacity: 0;
animation: cloudRight 2s linear 1s infinite forwards;
} }
20% {
@keyframes cloudLeft { top: 120px;
0% { left: 460px;
top: 17px; opacity: 1;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
} }
@keyframes cloudMid { 80% {
0% { top: 180px;
top: 10px; left: 340px;
left: 420px; opacity: 1;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
} }
@keyframes cloudRight { 100% {
0% { top: 200px;
top: 100px; left: 300px;
left: 500px; opacity: 0;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
} }
} }
} }
}
.bullshit { .bullshit {
position: relative; position: relative;
float: left; float: left;
width: 300px; width: 300px;
padding: 30px 0; padding: 30px 0;
overflow: hidden; overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline { &__oops {
font-size: 20px; font-size: 32px;
line-height: 24px; font-weight: bold;
color: #222; line-height: 40px;
font-weight: bold; color: #1482f0;
opacity: 0; opacity: 0;
margin-bottom: 10px; margin-bottom: 20px;
animation-name: slideUp; animation-name: slideUp;
animation-duration: 0.5s; animation-duration: 0.5s;
animation-delay: 0.1s; animation-fill-mode: forwards;
animation-fill-mode: forwards; }
}
&__info { &__headline {
font-size: 13px; font-size: 20px;
line-height: 21px; line-height: 24px;
color: grey; color: #222;
opacity: 0; font-weight: bold;
margin-bottom: 30px; opacity: 0;
animation-name: slideUp; margin-bottom: 10px;
animation-duration: 0.5s; animation-name: slideUp;
animation-delay: 0.2s; animation-duration: 0.5s;
animation-fill-mode: forwards; animation-delay: 0.1s;
} animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
&__return-home { @keyframes slideUp {
display: block; 0% {
float: left; transform: translateY(60px);
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0; opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
} }
100% {
@keyframes slideUp { transform: translateY(0);
0% { opacity: 1;
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
} }
} }
} }
} }
}
</style> </style>

@ -21,21 +21,21 @@
<svg-icon icon="password"/> <svg-icon icon="password"/>
</span> </span>
<el-input <el-input
v-model="form.repwd" v-model="form.repwd"
placeholder="请确认密码" placeholder="请确认密码"
type="password" type="password"
:maxlength="20" :maxlength="20"
@keyup.enter.native="register" @keyup.enter.native="register"
/> />
</el-form-item> </el-form-item>
<el-button <el-button
:loading="loading" :loading="loading"
class="submit-btn" class="submit-btn"
type="primary" type="primary"
@click="register" @click="register"
> >
{{loading ? '注 册 中...' : '注 册'}} {{ loading ? '注 册 中...' : '注 册' }}
</el-button> </el-button>
<div class="flex" style="margin-top: 20px"> <div class="flex" style="margin-top: 20px">
@ -46,84 +46,84 @@
</template> </template>
<script> <script>
import md5 from "js-md5" import md5 from "js-md5"
import {register, checkName} from "@/api/account" import {register, checkName} from "@/api/account"
import {elSuccess} from "@/utils/message" import {elSuccess} from "@/utils/message"
export default { export default {
name: "RegisterForm", name: "RegisterForm",
data() { data() {
const validateName = (r, v, c) => { const validateName = (r, v, c) => {
checkName(this.form.username) checkName(this.form.username)
.then(({msg}) => msg ? c(msg) : c()) .then(({msg}) => msg ? c(msg) : c())
.catch(e => c(e)) .catch(e => c(e))
} }
const validateRepwd = (r, v, c) => { const validateRepwd = (r, v, c) => {
return v !== this.form.pwd ? c('两次密码输入不一致') : c() return v !== this.form.pwd ? c('两次密码输入不一致') : c()
} }
return { return {
form: { form: {
username: '', username: '',
pwd: '', pwd: '',
repwd: '' repwd: ''
},
rules: {
username: [
{required: true, message: '请输入用户名', trigger: 'change'},
{validator: validateName, trigger: 'change'}
],
pwd: [
{required: true, message: '请输入密码', trigger: 'change'},
{min: 6, max: 32, message: '请输入6-32位的密码', trigger: 'change'}
],
repwd: [
{required: true, message: '请确认密码', trigger: 'change'},
{validator: validateRepwd, trigger: 'change'}
],
},
capsTooltip: false,
loading: false
}
},
methods: {
register() {
if (this.loading) return
this.$refs.form.validate(valid => {
if (!valid) return
this.loading = true
register({username: this.form.username, password: md5(this.form.pwd)})
.then(() => {
elSuccess('注册成功')
this.$router.push('/login')
})
.catch(() => this.loading = false)
})
}, },
rules: {
login() { username: [
!this.loading && this.$router.push('/login') {required: true, message: '请输入用户名', trigger: 'change'},
{validator: validateName, trigger: 'change'}
],
pwd: [
{required: true, message: '请输入密码', trigger: 'change'},
{min: 6, max: 32, message: '请输入6-32位的密码', trigger: 'change'}
],
repwd: [
{required: true, message: '请确认密码', trigger: 'change'},
{validator: validateRepwd, trigger: 'change'}
],
}, },
capsTooltip: false,
loading: false
}
},
capsLockTip({keyCode}) { methods: {
if (keyCode === 20) this.capsTooltip = !this.capsTooltip register() {
}, if (this.loading) return
this.$refs.form.validate(valid => {
if (!valid) return
this.loading = true
register({username: this.form.username, password: md5(this.form.pwd)})
.then(() => {
elSuccess('注册成功')
this.$router.push('/login')
})
.catch(() => this.loading = false)
})
},
addEvent() { login() {
document.addEventListener('keyup', this.capsLockTip) !this.loading && this.$router.push('/login')
}, },
removeEvent() { capsLockTip({keyCode}) {
document.removeEventListener('keyup', this.addCapsLockEvent) if (keyCode === 20) this.capsTooltip = !this.capsTooltip
}
}, },
mounted() { addEvent() {
this.addEvent() document.addEventListener('keyup', this.capsLockTip)
this.$nextTick(() => this.$refs.username.focus()) },
this.$once('hook:beforeDestroy', this.removeEvent) removeEvent() {
document.removeEventListener('keyup', this.addCapsLockEvent)
} }
},
mounted() {
this.addEvent()
this.$nextTick(() => this.$refs.username.focus())
this.$once('hook:beforeDestroy', this.removeEvent)
} }
}
</script> </script>

@ -1,49 +1,49 @@
<script type="text/jsx"> <script type="text/jsx">
const animations = [ const animations = [
{name: '无', value: ''}, {name: '无', value: ''},
{name: '烟花', value: 'firework'}, {name: '烟花', value: 'firework'},
{name: '上帝之光', value: 'godrays'}, {name: '上帝之光', value: 'godrays'},
{name: '简单粒子', value: 'moveFollowMouse'}, {name: '简单粒子', value: 'moveFollowMouse'},
{name: '球', value: 'particleBall'}, {name: '球', value: 'particleBall'},
{name: '网络', value: 'particleNetwork'}, {name: '网络', value: 'particleNetwork'},
{name: '波浪', value: 'particleWave'}, {name: '波浪', value: 'particleWave'},
{name: '雨', value: 'reflectRain'}, {name: '雨', value: 'reflectRain'},
{name: '流星雨', value: 'sparkRain'}, {name: '流星雨', value: 'sparkRain'},
{name: '阳光', value: 'sunlight'}, {name: '阳光', value: 'sunlight'},
] ]
export default { export default {
name: "SetAnimation", name: "SetAnimation",
functional: true, functional: true,
props: { props: {
value: String, value: String,
customClass: String customClass: String
}, },
render(h, context) { render(h, context) {
const {value, customClass} = context.props const {value, customClass} = context.props
return ( return (
<el-dropdown class={customClass} size="medium" trigger="click" onCommand={context.listeners.select}> <el-dropdown class={customClass} size="medium" trigger="click" onCommand={context.listeners.select}>
<el-button type="text" title="选择背景动画"> <el-button type="text" title="选择背景动画">
<svg-icon icon="show"/> <svg-icon icon="show"/>
</el-button> </el-button>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
{ {
animations.map(animation => ( animations.map(animation => (
<el-dropdown-item <el-dropdown-item
key={animation.value} key={animation.value}
disabled={animation.value === value} disabled={animation.value === value}
command={animation.value} command={animation.value}
> >
{animation.name} {animation.name}
</el-dropdown-item> </el-dropdown-item>
)) ))
} }
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
) )
}
} }
}
</script> </script>

@ -4,7 +4,7 @@
<div class="login-container" @click.stop> <div class="login-container" @click.stop>
<div class="title"> <div class="title">
{{title}} {{ title }}
<set-animation :value="loginBackgroundAnimation" custom-class="set-animation" @select="setAnimation"/> <set-animation :value="loginBackgroundAnimation" custom-class="set-animation" @select="setAnimation"/>
</div> </div>
@ -14,61 +14,61 @@
</template> </template>
<script> <script>
import {mapState} from 'vuex' import {mapState} from 'vuex'
import {title} from '@/config' import {title} from '@/config'
import SetAnimation from "./SetAnimation" import SetAnimation from "./SetAnimation"
import LoginForm from "./LoginForm" import LoginForm from "./LoginForm"
import RegisterForm from "./RegisterForm" import RegisterForm from "./RegisterForm"
import {isEmpty} from "@/utils" import {isEmpty} from "@/utils"
export default { export default {
name: 'login', name: 'login',
components: {LoginForm, RegisterForm, SetAnimation}, components: {LoginForm, RegisterForm, SetAnimation},
data() { data() {
return { return {
title, title,
animationInstance: null animationInstance: null
} }
}, },
computed: {
...mapState('app', ['device', 'loginBackgroundAnimation']),
component() { computed: {
const formType = this.$route.path.substring(1) ...mapState('app', ['device', 'loginBackgroundAnimation']),
return `${[...formType].join('')}-form`
}
},
methods: { component() {
clearAnimation() { const formType = this.$route.path.substring(1)
if (this.animationInstance) { return `${[...formType].join('')}-form`
this.animationInstance.stop() }
this.animationInstance = null },
}
},
setAnimation(value) { methods: {
this.clearAnimation() clearAnimation() {
this.$store.commit('app/loginBackgroundAnimation', value) if (this.animationInstance) {
if (isEmpty(value)) return this.animationInstance.stop()
import(`@/plugin/canvasAnimation/${value}`) this.animationInstance = null
.then(_ => this.animationInstance = new _.default(document.getElementById('login-background')))
} }
}, },
mounted() { setAnimation(value) {
//
this.device !== 'mobile' && this.setAnimation(this.loginBackgroundAnimation)
},
beforeDestroy() {
this.clearAnimation() this.clearAnimation()
this.$message.closeAll() this.$store.commit('app/loginBackgroundAnimation', value)
if (isEmpty(value)) return
import(`@/plugin/canvasAnimation/${value}`)
.then(_ => this.animationInstance = new _.default(document.getElementById('login-background')))
} }
},
mounted() {
//
this.device !== 'mobile' && this.setAnimation(this.loginBackgroundAnimation)
},
beforeDestroy() {
this.clearAnimation()
this.$message.closeAll()
} }
}
</script> </script>
<style lang="scss" src="./style.scss"></style> <style lang="scss" src="./style.scss"></style>

@ -1,15 +1,15 @@
<script> <script>
export default { export default {
created() { created() {
const {params, query} = this.$route const {params, query} = this.$route
//使name+params //使name+params
if ('params' in query) { if ('params' in query) {
const {name, params} = JSON.parse(query.params) const {name, params} = JSON.parse(query.params)
this.$router.replace({name, params}) this.$router.replace({name, params})
} }
else this.$router.replace({path: `/${params.path}`, query}) else this.$router.replace({path: `/${params.path}`, query})
}, },
render: h => h() render: h => h()
} }
</script> </script>

@ -19,23 +19,23 @@
</template> </template>
<script> <script>
import RegionSelector from '@/components/RegionSelector' import RegionSelector from '@/components/RegionSelector'
export default { export default {
name: "regionSelectorExample", name: "regionSelectorExample",
components: {RegionSelector}, components: {RegionSelector},
data() { data() {
return { return {
value: '北京市' value: '北京市'
} }
},
methods: {
clear() {
this.value = null
}, },
methods: { select(obj) {
clear() { this.value = obj.fullname
this.value = null
},
select(obj) {
this.value = obj.fullname
}
} }
} }
}
</script> </script>

@ -5,7 +5,7 @@
</template> </template>
<script> <script>
export default { export default {
name: "rippleExample" name: "rippleExample"
} }
</script> </script>

@ -11,19 +11,19 @@
</template> </template>
<script> <script>
export default { export default {
name: 'signatureExample', name: 'signatureExample',
data() { data() {
return { return {
src: null src: null
} }
}, },
methods: { methods: {
signature() { signature() {
this.$signature({ this.$signature({
image: this.src, onConfirm: img => this.src = img image: this.src, onConfirm: img => this.src = img
}) })
}
} }
} }
}
</script> </script>

@ -22,41 +22,41 @@
</template> </template>
<script> <script>
import QSkeleton from '@/components/Skeleton' import QSkeleton from '@/components/Skeleton'
import {skeletonAnimations, skeletonTypes} from '@/components/Skeleton/constant' import {skeletonAnimations, skeletonTypes} from '@/components/Skeleton/constant'
export default { export default {
name: "skeletonExample", name: "skeletonExample",
components: {QSkeleton}, components: {QSkeleton},
data() { data() {
return { return {
skeletonTypes, skeletonTypes,
skeletonAnimations skeletonAnimations
}
} }
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.q-skeleton + .q-skeleton { .q-skeleton + .q-skeleton {
margin-top: 20px; margin-top: 20px;
} }
.bg-primary { .bg-primary {
background: $--color-primary; background: $--color-primary;
} }
.bg-info { .bg-info {
background-color: $--color-teal; background-color: $--color-teal;
} }
.bg-warning { .bg-warning {
background-color: $--color-warning; background-color: $--color-warning;
} }
.bg-error { .bg-error {
background-color: $--color-danger; background-color: $--color-danger;
} }
</style> </style>

@ -9,32 +9,32 @@
</template> </template>
<script> <script>
import TreeSelect from '@/components/TreeSelect' import TreeSelect from '@/components/TreeSelect'
export default { export default {
name: "treeSelectExample", name: "treeSelectExample",
components: {TreeSelect}, components: {TreeSelect},
data() { data() {
return { return {
single: '', single: '',
multiple: [], multiple: [],
data: [ data: [
{id: 1, label: '一级 1', children: [{id: 2, label: '二级 1-1'}, {id: 3, label: '二级 1-2'}]}, {id: 1, label: '一级 1', children: [{id: 2, label: '二级 1-1'}, {id: 3, label: '二级 1-2'}]},
{id: 4, label: '一级 2', children: [{id: 5, label: '二级 2-1'}]}, {id: 4, label: '一级 2', children: [{id: 5, label: '二级 2-1'}]},
], ],
} }
}, },
methods: { methods: {
filter(v, data) { filter(v, data) {
return data.label.includes(v) return data.label.includes(v)
}, },
singleInput(v,data) { singleInput(v, data) {
console.log(data) console.log(data)
}
} }
} }
}
</script> </script>

@ -11,49 +11,49 @@
</template> </template>
<script> <script>
import UploadFile from '@/components/UploadFile' import UploadFile from '@/components/UploadFile'
import request from "@/config/request" import request from "@/config/request"
export default { export default {
name: "uploadExample", name: "uploadExample",
components: {UploadFile}, components: {UploadFile},
data() { data() {
return { return {
fileList: [ fileList: [
{ {
url: 'https://dss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=4018557288,1217151095&fm=26&gp=0.jpg', url: 'https://dss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=4018557288,1217151095&fm=26&gp=0.jpg',
name: '1jpgjpgjpgjpgjpgjp1jpgjpgjpgjpgjpgjpgjpggjpg.jpg' name: '1jpgjpgjpgjpgjpgjp1jpgjpgjpgjpgjpgjpgjpggjpg.jpg'
}, },
{ {
url: 'https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3252521864,872614242&fm=26&gp=0.jpg', url: 'https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3252521864,872614242&fm=26&gp=0.jpg',
name: '2.jpg' name: '2.jpg'
}, },
{ {
url: 'https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2534506313,1688529724&fm=26&gp=0.jpg', url: 'https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2534506313,1688529724&fm=26&gp=0.jpg',
name: '3.jpg' name: '3.jpg'
}, },
] ]
} }
}, },
methods: { methods: {
change(e) { change(e) {
let file = e.target.files[0] let file = e.target.files[0]
let param = new FormData() let param = new FormData()
let selfFilename = '这是自己另外取得文件名' let selfFilename = '这是自己另外取得文件名'
//selfFilename //selfFilename
param.append('file', file, selfFilename) param.append('file', file, selfFilename)
// //
//param.append('file', file) //param.append('file', file)
//RequestParam //RequestParam
param.append('extraParam1', "额外参数1") param.append('extraParam1', "额外参数1")
param.append('extraParam2', "额外参数2") param.append('extraParam2', "额外参数2")
request.post('/test/upload', param, {headers: {"Content-Type": "multipart/form-data"}}) request.post('/test/upload', param, {headers: {"Content-Type": "multipart/form-data"}})
.then(({data}) => alert(data)) .then(({data}) => alert(data))
}
} }
} }
}
</script> </script>

@ -5,51 +5,51 @@
</template> </template>
<script> <script>
import {debounce} from "@/utils" import {debounce} from "@/utils"
export default { export default {
name: "fluid", name: "fluid",
data() { data() {
return { return {
sign: true, sign: true,
width: 1200, width: 1200,
height: 800, height: 800,
$_resizeHandler: null, $_resizeHandler: null,
parentDom: null parentDom: null
} }
}, },
methods: { methods: {
$_initResizeEvent() { $_initResizeEvent() {
window.addEventListener('resize', this.$_resizeHandler) window.addEventListener('resize', this.$_resizeHandler)
}, },
$_destroyResizeEvent() { $_destroyResizeEvent() {
window.removeEventListener('resize', this.$_resizeHandler) window.removeEventListener('resize', this.$_resizeHandler)
}, },
start() { start() {
import('@/plugin/webgl/fluid').then(_ => _.default(this.$el.querySelector('#canvas-fluid'))) import('@/plugin/webgl/fluid').then(_ => _.default(this.$el.querySelector('#canvas-fluid')))
},
close() {
window.location.reload()
}
}, },
close() {
window.location.reload()
}
},
mounted() { mounted() {
this.parentDom = document.querySelector('.app-main') this.parentDom = document.querySelector('.app-main')
this.width = this.parentDom.clientWidth
this.height = this.parentDom.clientHeight
this.$_resizeHandler = debounce(() => {
this.width = this.parentDom.clientWidth this.width = this.parentDom.clientWidth
this.height = this.parentDom.clientHeight this.height = this.parentDom.clientHeight
this.$_resizeHandler = debounce(() => { })
this.width = this.parentDom.clientWidth this.$_initResizeEvent()
this.height = this.parentDom.clientHeight this.$nextTick(() => this.start())
}) },
this.$_initResizeEvent()
this.$nextTick(() => this.start())
},
beforeDestroy() { beforeDestroy() {
this.$_destroyResizeEvent() this.$_destroyResizeEvent()
this.close() this.close()
}
} }
}
</script> </script>

@ -9,28 +9,28 @@
</template> </template>
<script> <script>
import {loadExternalResource} from "@/utils/browser" import {loadExternalResource} from "@/utils/browser"
export default { export default {
name: "l2d", name: "l2d",
data: () => ({l2d: null}), data: () => ({l2d: null}),
mounted() { mounted() {
const path = `${process.env.BASE_URL}static/live2d/` const path = `${process.env.BASE_URL}static/live2d/`
Promise.all([ Promise.all([
import('@/plugin/live2d'), import('@/plugin/live2d'),
loadExternalResource(path + "waifu.css", "css"), loadExternalResource(path + "waifu.css", "css"),
loadExternalResource(path + "live2d.min.js") loadExternalResource(path + "live2d.min.js")
]) ])
.then(([_]) => { .then(([_]) => {
this.l2d = new _.default(`${path}waifu-tips.json`, "https://live2d.fghrsh.net/api") this.l2d = new _.default(`${path}waifu-tips.json`, "https://live2d.fghrsh.net/api")
}) })
}, },
beforeDestroy() { beforeDestroy() {
this.l2d && this.l2d.exit() this.l2d && this.l2d.exit()
}
} }
}
</script> </script>

@ -6,12 +6,12 @@
</template> </template>
<script> <script>
export default { export default {
name: "detailPage", name: "detailPage",
data() { data() {
return { return {
data: '' data: ''
}
} }
} }
}
</script> </script>

@ -1,7 +1,7 @@
<template> <template>
<div class="tip-row"> <div class="tip-row">
测试接口返回速度<br> 测试接口返回速度<br>
当前响应次数{{times}}响应时间{{cur}}毫秒平均响应{{avg}}毫秒最长响应{{max}}毫秒 当前响应次数{{ times }}响应时间{{ cur }}毫秒平均响应{{ avg }}毫秒最长响应{{ max }}毫秒
<el-row> <el-row>
<el-button type="primary" @click="post">点我测试</el-button> <el-button type="primary" @click="post">点我测试</el-button>
<el-button plain @click="reset">重置</el-button> <el-button plain @click="reset">重置</el-button>
@ -10,46 +10,46 @@
</template> </template>
<script> <script>
import request from "@/config/request" import request from "@/config/request"
export default { export default {
name: "ApiSpeedTest", name: "ApiSpeedTest",
data() { data() {
return { return {
loading: false, loading: false,
times: 0, times: 0,
cur: 0, cur: 0,
avg: 0, avg: 0,
max: 0, max: 0,
total: 0 total: 0
} }
},
methods: {
post() {
if (this.loading) return
this.loading = true
const start = performance.now()
request
.post('/test/test')
.finally(() => {
const end = (performance.now() - start)
if (end > this.max) this.max = end
this.cur = end.toFixed(2)
this.total += end
this.times++
this.avg = (this.total / this.times).toFixed(2)
this.loading = false
})
}, },
methods: {
post() {
if (this.loading) return
this.loading = true
const start = performance.now()
request
.post('/test/test')
.finally(() => {
const end = (performance.now() - start)
if (end > this.max) this.max = end
this.cur = end.toFixed(2)
this.total += end
this.times++
this.avg = (this.total / this.times).toFixed(2)
this.loading = false
})
},
reset() { reset() {
if (this.loading) return if (this.loading) return
this.times = 0 this.times = 0
this.cur = 0 this.cur = 0
this.avg = 0 this.avg = 0
this.max = 0 this.max = 0
this.total = 0 this.total = 0
}
} }
} }
}
</script> </script>

@ -1,7 +1,7 @@
<template> <template>
<div class="tip-row"> <div class="tip-row">
测试纯前端导出excel性能超过1w行浏览器将会有卡顿感请勿作死<br> 测试纯前端导出excel性能超过1w行浏览器将会有卡顿感请勿作死<br>
导出耗时{{cost}}毫秒 导出耗时{{ cost }}毫秒
<el-row> <el-row>
<el-input-number v-model="rows" :min="1" :step="1" step-strictly/> <el-input-number v-model="rows" :min="1" :step="1" step-strictly/>
<el-radio v-model="merge" :label="true" style="margin-left: 20px">合并</el-radio> <el-radio v-model="merge" :label="true" style="margin-left: 20px">合并</el-radio>
@ -12,45 +12,45 @@
</template> </template>
<script> <script>
import {exportExcelByJs, json2workbook} from "@/utils/excel" import {exportExcelByJs, json2workbook} from "@/utils/excel"
export default { export default {
name: "ExportExcelTest", name: "ExportExcelTest",
data() { data() {
return { return {
loading: false, loading: false,
column: [ column: [
{header: ['合并表头演示', '序号'], prop: 'no', merge: true}, {header: ['合并表头演示', '序号'], prop: 'no', merge: true},
{header: ['合并表头演示', '名称'], prop: 'name', merge: true}, {header: ['合并表头演示', '名称'], prop: 'name', merge: true},
{header: '日期', prop: 'date'}, {header: '日期', prop: 'date'},
], ],
row: { row: {
no: 1, no: 1,
name: '老王', name: '老王',
date: '20190201' date: '20190201'
}, },
rows: 1000, rows: 1000,
merge: false, merge: false,
cost: 0 cost: 0
} }
}, },
methods: { methods: {
exportExcel() { exportExcel() {
if (this.loading) return if (this.loading) return
const start = Date.now() const start = Date.now()
this.loading = true this.loading = true
const arr = new Array(this.rows).fill(this.row) const arr = new Array(this.rows).fill(this.row)
const workbook = json2workbook(arr, this.column, this.merge ? { const workbook = json2workbook(arr, this.column, this.merge ? {
primaryKey: 'name', primaryKey: 'name',
orderKey: 'no' orderKey: 'no'
} : null) } : null)
exportExcelByJs(workbook, '测试导出.xlsx') exportExcelByJs(workbook, '测试导出.xlsx')
this.loading = false this.loading = false
const end = Date.now() const end = Date.now()
this.cost = end - start this.cost = end - start
},
}, },
} },
}
</script> </script>

@ -9,20 +9,20 @@
<el-button type="primary" @click="() => $refs.jpg.click()">选择图片</el-button> <el-button type="primary" @click="() => $refs.jpg.click()">选择图片</el-button>
</div> </div>
<div v-show="jpg.input.src" class="image-container"> <div v-show="jpg.input.src" class="image-container">
<p>原始图片{{jpg.name}} ({{jpg.input.size|numberFormatter}})</p> <p>原始图片{{ jpg.name }} ({{ jpg.input.size|numberFormatter }})</p>
<img :src="jpg.input.src"> <img :src="jpg.input.src">
</div> </div>
<div v-show="jpg.output.src" class="image-container"> <div v-show="jpg.output.src" class="image-container">
<p> <p>
压缩后大小({{jpg.output.size|numberFormatter}}) 压缩后大小({{ jpg.output.size|numberFormatter }})
压缩率{{jpg.compress}}% 压缩率{{ jpg.compress }}%
总耗时{{jpg.totalCost}}毫秒 总耗时{{ jpg.totalCost }}毫秒
计算耗时{{jpg.calcCost}}毫秒 计算耗时{{ jpg.calcCost }}毫秒
</p> </p>
<img :src="jpg.output.src"> <img :src="jpg.output.src">
</div> </div>
<el-row type="flex"> <el-row type="flex">
<span>压缩图片品质{{jpg.quality}}</span> <span>压缩图片品质{{ jpg.quality }}</span>
<el-slider v-model="jpg.quality" :disabled="jpg.loading" :min="0" :max="100" style="flex: 1"/> <el-slider v-model="jpg.quality" :disabled="jpg.loading" :min="0" :max="100" style="flex: 1"/>
</el-row> </el-row>
<el-row type="flex" justify="center"> <el-row type="flex" justify="center">
@ -41,24 +41,24 @@
<el-button type="primary" @click="() => $refs.png.click()">选择图片</el-button> <el-button type="primary" @click="() => $refs.png.click()">选择图片</el-button>
</div> </div>
<div v-show="png.input.src" class="image-container"> <div v-show="png.input.src" class="image-container">
<p>原始图片{{png.name}} ({{png.input.size|numberFormatter}})</p> <p>原始图片{{ png.name }} ({{ png.input.size|numberFormatter }})</p>
<img :src="png.input.src"> <img :src="png.input.src">
</div> </div>
<div v-show="png.output.src" class="image-container"> <div v-show="png.output.src" class="image-container">
<p> <p>
压缩后大小({{png.output.size|numberFormatter}}) 压缩后大小({{ png.output.size|numberFormatter }})
压缩率{{png.compress}}% 压缩率{{ png.compress }}%
总耗时{{png.totalCost}}毫秒 总耗时{{ png.totalCost }}毫秒
计算耗时{{png.calcCost}}毫秒 计算耗时{{ png.calcCost }}毫秒
</p> </p>
<img :src="png.output.src"> <img :src="png.output.src">
</div> </div>
<el-row type="flex"> <el-row type="flex">
<span>压缩图片品质{{png.quality}}</span> <span>压缩图片品质{{ png.quality }}</span>
<el-slider v-model="png.quality" range :disabled="png.loading" :min="0" :max="100" style="flex: 1"/> <el-slider v-model="png.quality" range :disabled="png.loading" :min="0" :max="100" style="flex: 1"/>
</el-row> </el-row>
<el-row type="flex"> <el-row type="flex">
<span>压缩速度{{png.speed}}</span> <span>压缩速度{{ png.speed }}</span>
<el-slider v-model="png.speed" :disabled="png.loading" :min="0" :max="10" style="flex: 1"/> <el-slider v-model="png.speed" :disabled="png.loading" :min="0" :max="10" style="flex: 1"/>
</el-row> </el-row>
<el-row type="flex" justify="center"> <el-row type="flex" justify="center">
@ -71,151 +71,151 @@
</template> </template>
<script> <script>
import {elError, elSuccess} from "@/utils/message" import {elError, elSuccess} from "@/utils/message"
import {sub, mul, div} from "@/utils/math" import {sub, mul, div} from "@/utils/math"
const commonData = () => ({ const commonData = () => ({
loading: false, loading: false,
name: '', name: '',
compress: 0, compress: 0,
totalCost: 0, totalCost: 0,
calcCost: 0, calcCost: 0,
input: { input: {
size: 0, size: 0,
src: '', src: '',
file: null file: null
}, },
output: { output: {
size: 0, size: 0,
src: '' src: ''
}
})
export default {
name: "ImageCompressTest",
data() {
return {
jpg: {
...commonData(),
quality: 75
},
png: {
...commonData(),
quality: [75, 85],
speed: 4
}
} }
}) },
methods: {
export default { select(e, type) {
name: "ImageCompressTest", const file = e.target.files[0]
data() { const reg = new RegExp(`.(${type})$`, 'i')
return { if (!reg.test(file.name)) {
jpg: { elError(`请选择${type}图片`)
...commonData(), return false
quality: 75
},
png: {
...commonData(),
quality: [75, 85],
speed: 4
}
} }
this[type].name = file.name
this[type].input.file = file
this[type].input.size = file.size
this[type].input.src = window.URL.createObjectURL(file)
}, },
methods: {
select(e, type) {
const file = e.target.files[0]
const reg = new RegExp(`.(${type})$`, 'i')
if (!reg.test(file.name)) {
elError(`请选择${type}图片`)
return false
}
this[type].name = file.name
this[type].input.file = file
this[type].input.size = file.size
this[type].input.src = window.URL.createObjectURL(file)
},
cancel(type) { cancel(type) {
if (this[type].loading) return if (this[type].loading) return
window.URL.revokeObjectURL(this[type].input.src) window.URL.revokeObjectURL(this[type].input.src)
window.URL.revokeObjectURL(this[type].output.src) window.URL.revokeObjectURL(this[type].output.src)
this[type].input.src = '' this[type].input.src = ''
this[type].input.file = null this[type].input.file = null
this[type].output.src = '' this[type].output.src = ''
this[type].output.file = null this[type].output.file = null
this[type].compress = 0 this[type].compress = 0
this[type].totalCost = 0 this[type].totalCost = 0
this[type].calcCost = 0 this[type].calcCost = 0
}, },
compress(type) { compress(type) {
if (this[type].loading) return if (this[type].loading) return
if (!this[type].input.file) return elError(`请选择一张${type}图片`) if (!this[type].input.file) return elError(`请选择一张${type}图片`)
if (this[type].output.src) return elError('请先清除已压缩的图片') if (this[type].output.src) return elError('请先清除已压缩的图片')
const start = window.performance.now() const start = window.performance.now()
this[type].loading = true this[type].loading = true
const reader = new FileReader() const reader = new FileReader()
reader.onload = () => { reader.onload = () => {
const buf = new Uint8Array(reader.result) const buf = new Uint8Array(reader.result)
this['compress' + type.toUpperCase()](buf, start) this['compress' + type.toUpperCase()](buf, start)
} }
reader.readAsArrayBuffer(this[type].input.file) reader.readAsArrayBuffer(this[type].input.file)
}, },
compressJPG(uint8Array, start) { compressJPG(uint8Array, start) {
import('@/plugin/imageCompress/cjpeg') import('@/plugin/imageCompress/cjpeg')
.then(_ => _.default(uint8Array, this.jpg.quality)) .then(_ => _.default(uint8Array, this.jpg.quality))
.then(({data, time}) => this.compressSuccess({type: 'jpg', start, data, time})) .then(({data, time}) => this.compressSuccess({type: 'jpg', start, data, time}))
.finally(() => this.jpg.loading = false) .finally(() => this.jpg.loading = false)
}, },
compressPNG(uint8Array, start) { compressPNG(uint8Array, start) {
import('@/plugin/imageCompress/pngquant') import('@/plugin/imageCompress/pngquant')
.then(_ => _.default(uint8Array, this.png.quality, this.png.speed)) .then(_ => _.default(uint8Array, this.png.quality, this.png.speed))
.then(({data, time}) => this.compressSuccess({type: 'png', start, data, time})) .then(({data, time}) => this.compressSuccess({type: 'png', start, data, time}))
.finally(() => this.png.loading = false) .finally(() => this.png.loading = false)
}, },
compressSuccess({type, start, data, time}) { compressSuccess({type, start, data, time}) {
this[type].totalCost = (window.performance.now() - start).toFixed(2) this[type].totalCost = (window.performance.now() - start).toFixed(2)
this[type].calcCost = time.toFixed(2) this[type].calcCost = time.toFixed(2)
this[type].output.size = data.byteLength this[type].output.size = data.byteLength
this[type].output.src = window.URL.createObjectURL(new Blob([data])) this[type].output.src = window.URL.createObjectURL(new Blob([data]))
this[type].compress = mul(sub(1, div(this[type].output.size, this[type].input.size)), 100).toFixed(2) this[type].compress = mul(sub(1, div(this[type].output.size, this[type].input.size)), 100).toFixed(2)
elSuccess('压缩成功') elSuccess('压缩成功')
}
} }
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.container {
max-width: 1200px;
text-align: center;
.file-select {
width: 400px;
height: 300px;
margin: 40px auto;
border: 3px dashed #ccc;
display: flex;
justify-content: center;
align-items: center;
input {
display: none;
}
}
.container { .image-container {
max-width: 1200px; margin: 40px auto;
text-align: center; text-align: center;
.file-select { p {
width: 400px; color: $--color-gray;
height: 300px; margin-top: 0;
margin: 40px auto; margin-bottom: 1rem;
border: 3px dashed #ccc;
display: flex;
justify-content: center;
align-items: center;
input {
display: none;
}
} }
.image-container { img {
margin: 40px auto; padding: .25rem;
text-align: center; background-color: #fff;
border: 1px solid #dee2e6;
p { border-radius: .25rem;
color: $--color-gray; max-width: 100%;
margin-top: 0; height: auto;
margin-bottom: 1rem;
}
img {
padding: .25rem;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: .25rem;
max-width: 100%;
height: auto;
}
} }
}
.el-slider { .el-slider {
margin-left: 20px; margin-left: 20px;
}
} }
}
</style> </style>

@ -15,7 +15,7 @@
<el-tab-pane label="element-icons"> <el-tab-pane label="element-icons">
<div :key="item" class="icon-item" v-for="item of elementIcons"> <div :key="item" class="icon-item" v-for="item of elementIcons">
<i :class="item"/> <i :class="item"/>
<span>{{ item.replace('el-icon-','') }}</span> <span>{{ item.replace('el-icon-', '') }}</span>
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@ -23,43 +23,43 @@
</template> </template>
<script> <script>
import svgIcons from '@/assets/icons' import svgIcons from '@/assets/icons'
import elementIcons from './element-icons' import elementIcons from './element-icons'
export default { export default {
name: "icons", name: "icons",
data() { data() {
return { return {
svgIcons, svgIcons,
elementIcons elementIcons
}
},
methods: {
generateIconCode(symbol) {
return `<svg-icon icon="${symbol}" />`
},
generateElementIconCode(symbol) {
return `<i class="${symbol}" />`
},
} }
},
methods: {
generateIconCode(symbol) {
return `<svg-icon icon="${symbol}" />`
},
generateElementIconCode(symbol) {
return `<i class="${symbol}" />`
},
} }
}
</script> </script>
<style scoped> <style scoped>
.icon-item { .icon-item {
margin: 20px; margin: 20px;
height: 85px; height: 85px;
text-align: center; text-align: center;
width: 100px; width: 100px;
float: left; float: left;
font-size: 30px; font-size: 30px;
color: #24292e; color: #24292e;
cursor: pointer; cursor: pointer;
} }
span { span {
display: block; display: block;
font-size: 16px; font-size: 16px;
margin-top: 10px; margin-top: 10px;
} }
</style> </style>

@ -53,62 +53,62 @@
</template> </template>
<script> <script>
import AbstractTable from '@/components/AbstractTable' import AbstractTable from '@/components/AbstractTable'
import LinerProgress from '@/components/LinerProgress' import LinerProgress from '@/components/LinerProgress'
import FormDialog from "@/components/FormDialog" import FormDialog from "@/components/FormDialog"
export default { export default {
name: "stylePage", name: "stylePage",
components: {AbstractTable, LinerProgress, FormDialog}, components: {AbstractTable, LinerProgress, FormDialog},
data() { data() {
return { return {
dialog: false, dialog: false,
tableData: [ tableData: [
{name: '老王', address: '中国', tel: '13232423232', age: 18}, {name: '老王', address: '中国', tel: '13232423232', age: 18},
{name: '老王', address: '中国', tel: '13232423232', age: 18}, {name: '老王', address: '中国', tel: '13232423232', age: 18},
{name: '老王', address: '中国', tel: '13232423232', age: 18} {name: '老王', address: '中国', tel: '13232423232', age: 18}
] ]
} }
}, },
mounted() {
const f = (type, time = 0) => {
setTimeout(() => {
this.$notify({
title: type,
message: '提示信息',
type
})
}, time)
}
let i = 0;
['success', 'info', 'warning', 'error'].forEach(t => {
f(t, i)
i += 200
})
this.$once('hook:beforeDestroy', this.$notify.closeAll) mounted() {
const f = (type, time = 0) => {
setTimeout(() => {
this.$notify({
title: type,
message: '提示信息',
type
})
}, time)
} }
let i = 0;
['success', 'info', 'warning', 'error'].forEach(t => {
f(t, i)
i += 200
})
this.$once('hook:beforeDestroy', this.$notify.closeAll)
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.el-message { .el-message {
position: static; position: static;
transform: translateX(0); transform: translateX(0);
& + .el-message { & + .el-message {
margin-left: 20px; margin-left: 20px;
}
} }
}
.el-row + .el-row { .el-row + .el-row {
margin-top: 20px; margin-top: 20px;
} }
.el-tag + .el-tag { .el-tag + .el-tag {
margin-left: 20px; margin-left: 20px;
} }
</style> </style>

@ -3,86 +3,86 @@
</template> </template>
<script> <script>
import {logic, resize} from "@/mixins/chart" import {logic, resize} from "@/mixins/chart"
import {getDailyFinishOrder} from '@/api/statistic' import {getDailyFinishOrder} from '@/api/statistic'
import {timeFormat} from "@/utils" import {timeFormat} from "@/utils"
export default { export default {
name: "DailyFinishOrderStat", name: "DailyFinishOrderStat",
mixins: [resize, logic], mixins: [resize, logic],
methods: { methods: {
init() { init() {
if (this.loading) return if (this.loading) return
this.loading = true this.loading = true
getDailyFinishOrder() getDailyFinishOrder()
.then(data => { .then(data => {
const time = [] const time = []
const purchase = [] const purchase = []
const sell = [] const sell = []
data.forEach(i => { data.forEach(i => {
time.push(timeFormat('MM 月 dd日', new Date(i.time))) time.push(timeFormat('MM 月 dd日', new Date(i.time)))
purchase.push(i.purchase) purchase.push(i.purchase)
sell.push(i.sell) sell.push(i.sell)
})
this.setOptions({time, purchase, sell})
}) })
.finally(() => this.loading = false) this.setOptions({time, purchase, sell})
}, })
.finally(() => this.loading = false)
},
setOptions({time, purchase, sell}) { setOptions({time, purchase, sell}) {
this.chart.setOption({ this.chart.setOption({
title: { title: {
text: '历史七天订单完成情况统计', text: '历史七天订单完成情况统计',
left: 'center', left: 'center',
align: 'right' align: 'right'
}, },
xAxis: { xAxis: {
data: time, data: time,
axisTick: { axisTick: {
show: false show: false
} }
}, },
grid: { grid: {
left: 10, left: 10,
right: 10, right: 10,
bottom: 20, bottom: 20,
top: 30, top: 30,
containLabel: true containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}, },
tooltip: { padding: [5, 10]
trigger: 'axis', },
axisPointer: { yAxis: {
type: 'cross' axisTick: {
}, show: false
padding: [5, 10] }
},
legend: {
data: ['采购订单', '销售订单'],
right: 10
},
series: [
{
name: '采购订单',
smooth: true,
type: 'line',
data: purchase
}, },
yAxis: { {
axisTick: { name: '销售订单',
show: false smooth: true,
} type: 'line',
}, data: sell
legend: { }
data: ['采购订单', '销售订单'], ]
right: 10 })
},
series: [
{
name: '采购订单',
smooth: true,
type: 'line',
data: purchase
},
{
name: '销售订单',
smooth: true,
type: 'line',
data: sell
}
]
})
}
} }
} }
}
</script> </script>

@ -3,94 +3,94 @@
</template> </template>
<script> <script>
import {logic, resize} from "@/mixins/chart" import {logic, resize} from "@/mixins/chart"
import {getDailyProfitStat} from '@/api/statistic' import {getDailyProfitStat} from '@/api/statistic'
import {timeFormat} from "@/utils" import {timeFormat} from "@/utils"
export default { export default {
name: "DailyProfitStat", name: "DailyProfitStat",
mixins: [resize, logic], mixins: [resize, logic],
methods: { methods: {
init() { init() {
if (this.loading) return if (this.loading) return
this.loading = true this.loading = true
getDailyProfitStat() getDailyProfitStat()
.then(data => { .then(data => {
const time = [] const time = []
const purchase = [] const purchase = []
const sell = [] const sell = []
const profit = [] const profit = []
data.forEach(i => { data.forEach(i => {
time.push(timeFormat('MM 月 dd日', new Date(i.time))) time.push(timeFormat('MM 月 dd日', new Date(i.time)))
purchase.push(i.purchase) purchase.push(i.purchase)
sell.push(i.sell) sell.push(i.sell)
profit.push(i.profit) profit.push(i.profit)
})
this.setOptions({time, purchase, sell, profit})
}) })
.finally(() => this.loading = false) this.setOptions({time, purchase, sell, profit})
}, })
.finally(() => this.loading = false)
},
setOptions({time, purchase, sell, profit}) { setOptions({time, purchase, sell, profit}) {
this.chart.setOption({ this.chart.setOption({
title: { title: {
text: '历史七天利润情况统计', text: '历史七天利润情况统计',
left: 'center', left: 'center',
align: 'right' align: 'right'
}, },
xAxis: { xAxis: {
data: time, data: time,
axisTick: { axisTick: {
show: false show: false
} }
}, },
grid: { grid: {
left: 10, left: 10,
right: 10, right: 10,
bottom: 20, bottom: 20,
top: 30, top: 30,
containLabel: true containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}, },
tooltip: { padding: [5, 10]
trigger: 'axis', },
axisPointer: { yAxis: {
type: 'cross' axisTick: {
}, show: false
padding: [5, 10] }
},
legend: {
data: ['采购额', '销售额', '毛利润'],
right: 10
},
series: [
{
name: '采购额',
smooth: true,
type: 'line',
data: purchase
}, },
yAxis: { {
axisTick: { name: '销售额',
show: false smooth: true,
} type: 'line',
data: sell
}, },
legend: { {
data: ['采购额', '销售额', '毛利润'], name: '毛利润',
right: 10 smooth: true,
}, type: 'line',
series: [ data: profit
{ }
name: '采购额', ]
smooth: true, })
type: 'line',
data: purchase
},
{
name: '销售额',
smooth: true,
type: 'line',
data: sell
},
{
name: '毛利润',
smooth: true,
type: 'line',
data: profit
}
]
})
}
} }
} }
}
</script> </script>

@ -1,98 +1,98 @@
<script type="text/jsx"> <script type="text/jsx">
import CountTo from 'vue-count-to' import CountTo from 'vue-count-to'
export default { export default {
name: "PanelGroupItem", name: "PanelGroupItem",
functional: true, functional: true,
props: { props: {
icon: String, icon: String,
color: String, color: String,
text: String, text: String,
value: Number value: Number
}, },
render(h, context) { render(h, context) {
const {icon, color, text, value} = context.props const {icon, color, text, value} = context.props
return ( return (
<el-row class="card-panel" type="flex" justify="space-between" {...context.data}> <el-row class="card-panel" type="flex" justify="space-between" {...context.data}>
<div class="card-panel-icon-wrapper" style={'color:' + color}> <div class="card-panel-icon-wrapper" style={'color:' + color}>
<svg-icon icon={icon} class-name="card-panel-icon"/> <svg-icon icon={icon} class-name="card-panel-icon"/>
</div> </div>
<div class="card-panel-description"> <div class="card-panel-description">
<div class="card-panel-text">{text}</div> <div class="card-panel-text">{text}</div>
<CountTo class="card-panel-num" end-val={value} start-val={0}/> <CountTo class="card-panel-num" end-val={value} start-val={0}/>
</div> </div>
</el-row> </el-row>
) )
}
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.card-panel { .card-panel {
height: 108px; height: 108px;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
color: #666; color: #666;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;
-webkit-box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); -webkit-box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
border-color: rgba(0, 0, 0, .05); border-color: rgba(0, 0, 0, .05);
&:hover .card-panel-icon-wrapper { &:hover .card-panel-icon-wrapper {
color: #fff; color: #fff;
} }
.card-panel-icon-wrapper { .card-panel-icon-wrapper {
margin: 14px 0 14px 14px; margin: 14px 0 14px 14px;
padding: 16px; padding: 16px;
transition: all 0.38s ease-out; transition: all 0.38s ease-out;
border-radius: 6px; border-radius: 6px;
} }
.card-panel-icon { .card-panel-icon {
font-size: 48px; font-size: 48px;
} }
.card-panel-description { .card-panel-description {
font-weight: bold; font-weight: bold;
margin: 26px 20px 0 26px; margin: 26px 20px 0 26px;
.card-panel-text { .card-panel-text {
line-height: 18px; line-height: 18px;
color: rgba(0, 0, 0, 0.45); color: rgba(0, 0, 0, 0.45);
font-size: 16px; font-size: 16px;
margin-bottom: 12px; margin-bottom: 12px;
} }
.card-panel-num { .card-panel-num {
font-size: 20px; font-size: 20px;
}
} }
} }
}
@media (max-width: 550px) { @media (max-width: 550px) {
.card-panel-description { .card-panel-description {
display: none; display: none;
} }
.card-panel-icon-wrapper { .card-panel-icon-wrapper {
float: none !important; float: none !important;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0 !important; margin: 0 !important;
.svg-icon { .svg-icon {
display: block; display: block;
margin: 14px auto !important; margin: 14px auto !important;
float: none !important; float: none !important;
}
} }
} }
}
</style> </style>

@ -7,66 +7,66 @@
</template> </template>
<script> <script>
import PanelGroupItem from './PanelGroupItem' import PanelGroupItem from './PanelGroupItem'
import variables from '@/assets/styles/variables.scss' import variables from '@/assets/styles/variables.scss'
import {getFourBlock} from '@/api/statistic' import {getFourBlock} from '@/api/statistic'
import {isEmpty} from "@/utils" import {isEmpty} from "@/utils"
import {auth} from "@/utils/auth" import {auth} from "@/utils/auth"
export default { export default {
name: 'panelGroup', name: 'panelGroup',
components: {PanelGroupItem}, components: {PanelGroupItem},
data() { data() {
return { return {
loading: false, loading: false,
list: [ list: [
{id: 'online', path: '/system/user', icon: 'user', color: variables.info, value: 0, text: '在线用户'}, {id: 'online', path: '/system/user', icon: 'user', color: variables.info, value: 0, text: '在线用户'},
{ {
id: 'purchase', id: 'purchase',
path: '/purchase/order', path: '/purchase/order',
icon: 'shopping', icon: 'shopping',
color: variables.primary, color: variables.primary,
value: 0, value: 0,
text: '今日采购额' text: '今日采购额'
}, },
{id: 'sell', path: '/sell/order', icon: 'sell', color: variables.warning, value: 0, text: '今日销售额'}, {id: 'sell', path: '/sell/order', icon: 'sell', color: variables.warning, value: 0, text: '今日销售额'},
{id: 'profit', icon: 'money', color: variables.success, value: 0, text: '今日毛利润'}, {id: 'profit', icon: 'money', color: variables.success, value: 0, text: '今日毛利润'},
] ]
} }
}, },
methods: { methods: {
init() { init() {
if (this.loading) return if (this.loading) return
this.loading = true this.loading = true
getFourBlock() getFourBlock()
.then(data => { .then(data => {
this.list.forEach(i => { this.list.forEach(i => {
if (i.id in data) i.value = data[i.id] if (i.id in data) i.value = data[i.id]
})
}) })
.finally(() => this.loading = false) })
}, .finally(() => this.loading = false)
jump({path}) {
if (!isEmpty(path) && auth(path)) this.$router.push(path)
}
}, },
mounted() { jump({path}) {
this.init() if (!isEmpty(path) && auth(path)) this.$router.push(path)
} }
},
mounted() {
this.init()
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.panel-group { .panel-group {
padding-top: 17px; padding-top: 17px;
> .el-col { > .el-col {
margin-bottom: 32px; margin-bottom: 32px;
}
} }
}
</style> </style>

@ -9,64 +9,64 @@
</template> </template>
<script> <script>
import PieChart from "@/components/Charts/PieChart" import PieChart from "@/components/Charts/PieChart"
import {getTotalProfitGoods} from '@/api/statistic' import {getTotalProfitGoods} from '@/api/statistic'
export default { export default {
name: "TotalProfitGoods", name: "TotalProfitGoods",
components: {PieChart}, components: {PieChart},
data() { data() {
return { return {
loading: false, loading: false,
purchase: { purchase: {
title: '各个商品总采购额', title: '各个商品总采购额',
data: [] data: []
},
sell: {
title: '各个商品总销售额',
data: []
},
profit: {
title: '各个商品总毛利润',
data: []
}
}
},
methods: {
getTitle(type) {
return this[type].title
}, },
sell: {
getData(type) { title: '各个商品总销售额',
return this[type].data data: []
}, },
profit: {
init() { title: '各个商品总毛利润',
if (this.loading) return data: []
this.loading = true
getTotalProfitGoods()
.then(data => {
const purchase = []
const sell = []
const profit = []
data.forEach(i => {
if (i.purchase) purchase.push({name: i.cname, value: i.purchase})
if (i.sell) sell.push({name: i.cname, value: i.sell})
if (i.profit) profit.push({name: i.cname, value: i.profit})
})
this.purchase.data = purchase
this.sell.data = sell
this.profit.data = profit
})
.finally(() => this.loading = false)
} }
}
},
methods: {
getTitle(type) {
return this[type].title
},
getData(type) {
return this[type].data
}, },
mounted() { init() {
this.init() if (this.loading) return
this.loading = true
getTotalProfitGoods()
.then(data => {
const purchase = []
const sell = []
const profit = []
data.forEach(i => {
if (i.purchase) purchase.push({name: i.cname, value: i.purchase})
if (i.sell) sell.push({name: i.cname, value: i.sell})
if (i.profit) profit.push({name: i.cname, value: i.profit})
})
this.purchase.data = purchase
this.sell.data = sell
this.profit.data = profit
})
.finally(() => this.loading = false)
} }
},
mounted() {
this.init()
} }
}
</script> </script>

@ -15,14 +15,14 @@
</template> </template>
<script> <script>
import PanelGroup from './components/PanelGroup' import PanelGroup from './components/PanelGroup'
import DailyProfitStat from "./components/DailyProfitStat" import DailyProfitStat from "./components/DailyProfitStat"
import DailyFinishOrderStat from "./components/DailyFinishOrderStat" import DailyFinishOrderStat from "./components/DailyFinishOrderStat"
import TotalProfitGoods from "./components/TotalProfitGoods" import TotalProfitGoods from "./components/TotalProfitGoods"
export default { export default {
name: 'index', name: 'index',
components: {PanelGroup, DailyProfitStat, DailyFinishOrderStat, TotalProfitGoods} components: {PanelGroup, DailyProfitStat, DailyFinishOrderStat, TotalProfitGoods}
} }
</script> </script>

@ -52,188 +52,188 @@
</template> </template>
<script> <script>
import AbstractForm from "@/components/AbstractForm" import AbstractForm from "@/components/AbstractForm"
import AbstractFormItem from "@/components/AbstractForm/AbstractFormItem" import AbstractFormItem from "@/components/AbstractForm/AbstractFormItem"
import FormDialog from '@/components/FormDialog' import FormDialog from '@/components/FormDialog'
import TinymceEditor from "@/components/TinymceEditor" import TinymceEditor from "@/components/TinymceEditor"
import {SimpleMultipleUserSelector as UserSelector} from '@/components/biz/UserSelector' import {SimpleMultipleUserSelector as UserSelector} from '@/components/biz/UserSelector'
import dialogMixin from "@/mixins/dialogMixin" import dialogMixin from "@/mixins/dialogMixin"
import {add, update, publish, withdraw} from "@/api/message/manage" import {add, update, publish, withdraw} from "@/api/message/manage"
import {isEmpty, mergeObj, resetObj} from '@/utils' import {isEmpty, mergeObj, resetObj} from '@/utils'
import {elAlert, elConfirm, elSuccess} from "@/utils/message" import {elAlert, elConfirm, elSuccess} from "@/utils/message"
import {auth} from "@/utils/auth" import {auth} from "@/utils/auth"
export default { export default {
name: "EditDialog", name: "EditDialog",
mixins: [dialogMixin], mixins: [dialogMixin],
components: {AbstractForm, AbstractFormItem, FormDialog, TinymceEditor, UserSelector}, components: {AbstractForm, AbstractFormItem, FormDialog, TinymceEditor, UserSelector},
props: { props: {
value: {type: Boolean, default: false}, value: {type: Boolean, default: false},
type: {type: String, default: 'see'}, type: {type: String, default: 'see'},
data: { data: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
},
baseUrl: String
}, },
baseUrl: String
data() { },
return {
loading: false, data() {
needSearch: false, return {
form: { loading: false,
id: null, needSearch: false,
title: null, form: {
resume: null, id: null,
content: null, title: null,
type: 0, resume: null,
cname: null, content: null,
ctime: null, type: 0,
pname: null, cname: null,
ptime: null, ctime: null,
wname: null, pname: null,
wtime: null, ptime: null,
status: 0, wname: null,
broadcast: 1, wtime: null,
recipient: [] status: 0,
}, broadcast: 1,
rules: { recipient: []
title: [{required: true, message: '请输入标题', trigger: 'change'}], },
type: [{required: true, message: '请选择消息类型', trigger: 'change'}], rules: {
broadcast: [{required: true, message: '请选择通知对象', trigger: 'change'}] title: [{required: true, message: '请输入标题', trigger: 'change'}],
} type: [{required: true, message: '请选择消息类型', trigger: 'change'}],
broadcast: [{required: true, message: '请选择通知对象', trigger: 'change'}]
}
}
},
computed: {
title() {
if (isEmpty(this.type)) return ''
switch (this.type) {
case 'see':
return '查看消息'
case 'add':
return '添加消息'
case 'edit':
return '编辑消息'
} }
}, },
computed: { canSave() {
title() { //addeditstatus=0
if (isEmpty(this.type)) return '' return this.type === 'add' && auth(this.baseUrl + '/add')
switch (this.type) { || this.form.status === 0 && this.type === 'edit' && auth(this.baseUrl + '/update')
case 'see':
return '查看消息'
case 'add':
return '添加消息'
case 'edit':
return '编辑消息'
}
},
canSave() {
//addeditstatus=0
return this.type === 'add' && auth(this.baseUrl + '/add')
|| this.form.status === 0 && this.type === 'edit' && auth(this.baseUrl + '/update')
},
canPublish() {
//addeditstatus=0
return auth(this.baseUrl + '/publish') && (this.type === 'add' || this.type === 'edit' && this.form.status === 0)
},
canWithdraw() {
//editstatus=1
return auth(this.baseUrl + '/withdraw')
&& this.type === 'edit'
&& this.form.status === 1
},
}, },
methods: { canPublish() {
open() { //addeditstatus=0
if (this.type === 'add') return return auth(this.baseUrl + '/publish') && (this.type === 'add' || this.type === 'edit' && this.form.status === 0)
mergeObj(this.form, this.data) },
const recipient = this.data.recipient || ''
this.form.recipient = recipient.split(',').map(i => parseInt(i))
},
clearForm() { canWithdraw() {
resetObj(this.form) //editstatus=1
this.form.broadcast = 1 return auth(this.baseUrl + '/withdraw')
this.needSearch = false && this.type === 'edit'
this.loading = false && this.form.status === 1
this.$nextTick(() => this.$refs.form.clearValidate()) },
}, },
methods: {
open() {
if (this.type === 'add') return
mergeObj(this.form, this.data)
const recipient = this.data.recipient || ''
this.form.recipient = recipient.split(',').map(i => parseInt(i))
},
cancel() { clearForm() {
this.closeDialog() resetObj(this.form)
this.needSearch && this.$emit('search') this.form.broadcast = 1
this.needSearch = false
this.loading = false
this.$nextTick(() => this.$refs.form.clearValidate())
},
setTimeout(() => this.clearForm(), 200) cancel() {
this.loading = false this.closeDialog()
}, this.needSearch && this.$emit('search')
save() { setTimeout(() => this.clearForm(), 200)
if (this.loading) return this.loading = false
this.$refs.form.validate(v => { },
if (!v) return
this.loading = true
const data = this.transformForm()
let promise = this.type === 'add' ? add(data) : update(data)
promise
.then(({data, msg}) => {
this.needSearch = true
elSuccess(msg)
if (this.type === 'add') {
this.form.id = data.id
this.form.cname = data.cname
this.form.ctime = data.ctime
}
this.$emit('update:type', 'edit')
})
.finally(() => this.loading = false)
})
},
publish() {
if (this.loading) return
this.$refs.form.validate(v => {
if (!v) return
if (this.validate) {
let valid = this.validate()
if (!isEmpty(valid)) return elAlert(valid)
}
elConfirm('确认发布?')
.then(() => this.loading = true)
.then(() => publish(this.transformForm()))
.then(({data, msg}) => {
elSuccess(msg)
this.needSearch = true
save() {
if (this.loading) return
this.$refs.form.validate(v => {
if (!v) return
this.loading = true
const data = this.transformForm()
let promise = this.type === 'add' ? add(data) : update(data)
promise
.then(({data, msg}) => {
this.needSearch = true
elSuccess(msg)
if (this.type === 'add') {
this.form.id = data.id this.form.id = data.id
this.form.cname = data.cname this.form.cname = data.cname
this.form.ctime = data.ctime this.form.ctime = data.ctime
this.form.pid = data.pid }
this.form.pname = data.pname this.$emit('update:type', 'edit')
this.form.ptime = data.ptime
this.form.status = data.status
this.$emit('update:type', 'edit')
})
.finally(() => this.loading = false)
})
},
withdraw() {
if (this.loading) return
elConfirm('确认撤回?')
.then(() => {
this.loading = true
return withdraw({id: this.form.id})
}) })
.then(({msg}) => { .finally(() => this.loading = false)
})
},
publish() {
if (this.loading) return
this.$refs.form.validate(v => {
if (!v) return
if (this.validate) {
let valid = this.validate()
if (!isEmpty(valid)) return elAlert(valid)
}
elConfirm('确认发布?')
.then(() => this.loading = true)
.then(() => publish(this.transformForm()))
.then(({data, msg}) => {
elSuccess(msg) elSuccess(msg)
this.needSearch = true this.needSearch = true
this.closeDialog()
this.form.id = data.id
this.form.cname = data.cname
this.form.ctime = data.ctime
this.form.pid = data.pid
this.form.pname = data.pname
this.form.ptime = data.ptime
this.form.status = data.status
this.$emit('update:type', 'edit')
}) })
.finally(() => this.loading = false) .finally(() => this.loading = false)
}, })
},
transformForm() { withdraw() {
if (this.form.broadcast === 1) return this.form if (this.loading) return
else return {...this.form, recipient: this.form.recipient.join(',')} elConfirm('确认撤回?')
} .then(() => {
this.loading = true
return withdraw({id: this.form.id})
})
.then(({msg}) => {
elSuccess(msg)
this.needSearch = true
this.closeDialog()
})
.finally(() => this.loading = false)
},
transformForm() {
if (this.form.broadcast === 1) return this.form
else return {...this.form, recipient: this.form.recipient.join(',')}
} }
} }
}
</script> </script>

@ -32,142 +32,142 @@
<el-table-column align="center" label="#" type="index" width="80"/> <el-table-column align="center" label="#" type="index" width="80"/>
<el-table-column align="center" label="标 题" prop="title" show-overflow-tooltip/> <el-table-column align="center" label="标 题" prop="title" show-overflow-tooltip/>
<el-table-column align="center" label="类 型" show-overflow-tooltip> <el-table-column align="center" label="类 型" show-overflow-tooltip>
<template v-slot="{row}">{{row.type===0?'通知提醒':'系统公告'}}</template> <template v-slot="{row}">{{ row.type === 0 ? '通知提醒' : '系统公告' }}</template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="通知对象" show-overflow-tooltip> <el-table-column align="center" label="通知对象" show-overflow-tooltip>
<template v-slot="{row}">{{row.broadcast===1?'全体用户':'指定用户'}}</template> <template v-slot="{row}">{{ row.broadcast === 1 ? '全体用户' : '指定用户' }}</template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="状 态" show-overflow-tooltip> <el-table-column align="center" label="状 态" show-overflow-tooltip>
<template v-slot="{row}">{{transformStatus(row.status)}}</template> <template v-slot="{row}">{{ transformStatus(row.status) }}</template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="发布人" prop="pname" show-overflow-tooltip/> <el-table-column align="center" label="发布人" prop="pname" show-overflow-tooltip/>
<el-table-column align="center" label="发布时间" width="150" show-overflow-tooltip> <el-table-column align="center" label="发布时间" width="150" show-overflow-tooltip>
<template v-slot="{row}">{{row.ptime | timestamp2Date}}</template> <template v-slot="{row}">{{ row.ptime | timestamp2Date }}</template>
</el-table-column> </el-table-column>
</abstract-table> </abstract-table>
<el-pagination <el-pagination
background background
:current-page="searchForm.page" :current-page="searchForm.page"
:page-size="searchForm.pageSize" :page-size="searchForm.pageSize"
:total="searchForm.total" :total="searchForm.total"
layout="total, prev, pager, next, jumper" layout="total, prev, pager, next, jumper"
@current-change="pageChange" @current-change="pageChange"
/> />
</el-row> </el-row>
<edit-dialog <edit-dialog
v-model="editDialog" v-model="editDialog"
:base-url="baseUrl" :base-url="baseUrl"
:data="row" :data="row"
:type.sync="type" :type.sync="type"
@search="search" @search="search"
/> />
</el-card> </el-card>
</template> </template>
<script> <script>
import SearchForm from "@/components/SearchForm" import SearchForm from "@/components/SearchForm"
import SearchFormItem from "@/components/SearchForm/SearchFormItem" import SearchFormItem from "@/components/SearchForm/SearchFormItem"
import EditDialog from './EditDialog' import EditDialog from './EditDialog'
import {search, del} from "@/api/message/manage" import {search, del} from "@/api/message/manage"
import {isEmpty} from '@/utils' import {isEmpty} from '@/utils'
import {elConfirm, elError, elSuccess} from "@/utils/message" import {elConfirm, elError, elSuccess} from "@/utils/message"
import {auth} from "@/utils/auth" import {auth} from "@/utils/auth"
import tableMixin from '@/mixins/tablePageMixin' import tableMixin from '@/mixins/tablePageMixin'
export default { export default {
name: "messageManagement", name: "messageManagement",
mixins: [tableMixin], mixins: [tableMixin],
components: {SearchForm, SearchFormItem, EditDialog}, components: {SearchForm, SearchFormItem, EditDialog},
data() { data() {
return { return {
baseUrl: '/message/manage', baseUrl: '/message/manage',
searchForm: { searchForm: {
title: null, title: null,
type: null, type: null,
status: null status: null
},
type: 'add',
editDialog: false,
regionDialog: false
}
},
computed: {
canAdd() {
return auth(this.baseUrl + '/add')
}, },
type: 'add',
editDialog: false,
regionDialog: false
}
},
canUpdate() { computed: {
return auth(this.baseUrl + '/update') canAdd() {
}, return auth(this.baseUrl + '/add')
},
canDel() { canUpdate() {
return auth(this.baseUrl + '/del') return auth(this.baseUrl + '/update')
}
}, },
methods: { canDel() {
transformStatus(status) { return auth(this.baseUrl + '/del')
switch (status) { }
case 0: },
return '拟定'
case 1: methods: {
return '已发布' transformStatus(status) {
case 2: switch (status) {
return '已撤回' case 0:
} return '拟定'
}, case 1:
return '已发布'
case 2:
return '已撤回'
}
},
search() { search() {
if (this.config.loading) return if (this.config.loading) return
this.config.loading = true this.config.loading = true
this.row = null this.row = null
this.type = 'see' this.type = 'see'
search(this.searchForm) search(this.searchForm)
.then(({list, total}) => { .then(({list, total}) => {
this.searchForm.total = total this.searchForm.total = total
this.tableData = list this.tableData = list
}) })
.finally(() => this.config.loading = false) .finally(() => this.config.loading = false)
}, },
add() { add() {
this.type = 'add' this.type = 'add'
this.editDialog = true this.editDialog = true
}, },
see() { see() {
if (isEmpty(this.row)) return elError('请选择要查看的消息') if (isEmpty(this.row)) return elError('请选择要查看的消息')
this.type = 'see' this.type = 'see'
this.editDialog = true this.editDialog = true
}, },
edit() { edit() {
if (isEmpty(this.row)) return elError('请选择要编辑的消息') if (isEmpty(this.row)) return elError('请选择要编辑的消息')
this.type = 'edit' this.type = 'edit'
this.editDialog = true this.editDialog = true
}, },
del() { del() {
if (isEmpty(this.row)) return elError('请选择要删除的消息') if (isEmpty(this.row)) return elError('请选择要删除的消息')
if (this.row.status !== 0) return elError('只有状态为【拟定】时才能删除') if (this.row.status !== 0) return elError('只有状态为【拟定】时才能删除')
if (this.config.operating) return if (this.config.operating) return
elConfirm(`确定删除消息【${this.row.title}】?`) elConfirm(`确定删除消息【${this.row.title}】?`)
.then(() => { .then(() => {
this.config.operating = true this.config.operating = true
return del(this.row.id, this.row.title) return del(this.row.id, this.row.title)
}) })
.then(() => { .then(() => {
elSuccess('删除成功') elSuccess('删除成功')
this.search() this.search()
}) })
.finally(() => this.config.operating = false) .finally(() => this.config.operating = false)
}
} }
} }
}
</script> </script>

@ -3,67 +3,67 @@
<div ref="content" v-html="html" class="auto-hidden__content" :style="contentStyle"/> <div ref="content" v-html="html" class="auto-hidden__content" :style="contentStyle"/>
<div v-if="showCollapse" class="auto-hidden__collapse"> <div v-if="showCollapse" class="auto-hidden__collapse">
<el-button <el-button
type="text" type="text"
size="mini" size="mini"
:icon="collapseIcon" :icon="collapseIcon"
@click="handleCollapse" @click="handleCollapse"
> >
{{collapse ? '展开' : '收起'}} {{ collapse ? '展开' : '收起' }}
</el-button> </el-button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {getElementHeight} from "@/utils/browser" import {getElementHeight} from "@/utils/browser"
export default { export default {
name: "AutoHidden", name: "AutoHidden",
props: { props: {
html: String, html: String,
height: {type: String, default: '100px'} height: {type: String, default: '100px'}
}, },
data() { data() {
return { return {
showCollapse: false, showCollapse: false,
collapse: true collapse: true
} }
}, },
computed: { computed: {
contentStyle() { contentStyle() {
if (!this.showCollapse || !this.collapse) return {} if (!this.showCollapse || !this.collapse) return {}
return {'max-height': this.height} return {'max-height': this.height}
},
collapseIcon() {
return this.collapse ? 'el-icon-arrow-down' : 'el-icon-arrow-up'
}
}, },
methods: { collapseIcon() {
handleCollapse() { return this.collapse ? 'el-icon-arrow-down' : 'el-icon-arrow-up'
this.collapse = !this.collapse }
}, },
isOverHeight() { methods: {
const height = getElementHeight(this.$refs['content']) handleCollapse() {
return height > parseFloat(this.height) this.collapse = !this.collapse
}
}, },
mounted() { isOverHeight() {
this.showCollapse = this.isOverHeight() const height = getElementHeight(this.$refs['content'])
return height > parseFloat(this.height)
} }
},
mounted() {
this.showCollapse = this.isOverHeight()
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.auto-hidden { .auto-hidden {
position: relative; position: relative;
&__content { &__content {
overflow: hidden; overflow: hidden;
} }
&__collapse { &__collapse {
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
}
} }
}
</style> </style>

@ -1,11 +1,11 @@
<template> <template>
<div v-loading="config.loading" class="message-stream"> <div v-loading="config.loading" class="message-stream">
<el-button <el-button
v-if="showReadAllBtn>0" v-if="showReadAllBtn>0"
plain plain
size="mini" size="mini"
style="margin-bottom: 20px" style="margin-bottom: 20px"
@click="readAll" @click="readAll"
> >
清除所有未读 清除所有未读
</el-button> </el-button>
@ -13,183 +13,183 @@
<template v-else> <template v-else>
<el-timeline> <el-timeline>
<el-timeline-item <el-timeline-item
v-for="msg in tableData" v-for="msg in tableData"
:key="msg.index" :key="msg.index"
:timestamp="msg.time" :timestamp="msg.time"
placement="top" placement="top"
> >
<el-card <el-card
v-for="child in msg.children" v-for="child in msg.children"
:key="child.id" :key="child.id"
:class="{'message-stream__item':true,viewed:child.__read}" :class="{'message-stream__item':true,viewed:child.__read}"
@click.native="read(child)" @click.native="read(child)"
> >
<div class="message-stream__item__title"> <div class="message-stream__item__title">
<span v-if="!child.__read" class="dot success"/> <span v-if="!child.__read" class="dot success"/>
<b>{{transformType(child.type)}}</b> <b>{{ transformType(child.type) }}</b>
{{child.title}} {{ child.title }}
</div> </div>
<auto-hidden :html="child.content"/> <auto-hidden :html="child.content"/>
<div class="message-stream__item__footer"><b>By:</b> {{child.pname}}</div> <div class="message-stream__item__footer"><b>By:</b> {{ child.pname }}</div>
</el-card> </el-card>
</el-timeline-item> </el-timeline-item>
</el-timeline> </el-timeline>
<el-pagination <el-pagination
background background
:current-page="searchForm.page" :current-page="searchForm.page"
:page-size="searchForm.pageSize" :page-size="searchForm.pageSize"
:total="searchForm.total" :total="searchForm.total"
hide-on-single-page hide-on-single-page
layout="prev, pager, next" layout="prev, pager, next"
@current-change="pageChange" @current-change="pageChange"
/> />
</template> </template>
</div> </div>
</template> </template>
<script> <script>
import tablePageMixin from '@/mixins/tablePageMixin' import tablePageMixin from '@/mixins/tablePageMixin'
import AutoHidden from "./AutoHidden" import AutoHidden from "./AutoHidden"
import Empty from '@/components/Empty' import Empty from '@/components/Empty'
import {search, read, readAll} from "@/api/message/user" import {search, read, readAll} from "@/api/message/user"
export default { export default {
name: "MessageStream", name: "MessageStream",
mixins: [tablePageMixin], mixins: [tablePageMixin],
components: {AutoHidden, Empty}, components: {AutoHidden, Empty},
props: { props: {
mode: String, mode: String,
}, },
data() { data() {
return { return {
searchForm: { searchForm: {
pageSize: 30 pageSize: 30
},
}
},
computed: {
unreadCount() {
return this.$store.state.message.unreadCount
}, },
showReadAllBtn() { }
return this.mode === 'unread' && this.unreadCount > 0 },
}
},
watch: { computed: {
mode() { unreadCount() {
this.search() return this.$store.state.message.unreadCount
}
}, },
showReadAllBtn() {
return this.mode === 'unread' && this.unreadCount > 0
}
},
methods: { watch: {
search() { mode() {
if (this.config.loading) return this.search()
const unread = this.mode === 'unread' }
const currentMode = this.mode },
this.tableData = []
this.config.loading = true methods: {
search({...this.searchForm, unread}) search() {
.then(({list, total}) => { if (this.config.loading) return
if (this.mode !== currentMode) return const unread = this.mode === 'unread'
unread && this.$store.commit('message/unreadCount', total) const currentMode = this.mode
this.searchForm.total = total this.tableData = []
this.tableData = this.transformList(list) this.config.loading = true
}) search({...this.searchForm, unread})
.finally(() => this.config.loading = false) .then(({list, total}) => {
}, if (this.mode !== currentMode) return
unread && this.$store.commit('message/unreadCount', total)
// this.searchForm.total = total
transformList(list) { this.tableData = this.transformList(list)
const thisYear = new Date().getFullYear()
const map = {}
let index = 0
list.forEach(item => {
let time = new Date(item.ptime),
year = time.getFullYear(),
month = time.getMonth() + 1,
day = time.getDate()
//
item.__read = this.mode === 'read'
time = (year === thisYear ? '' : `${year}`) + `${month}${day}`
const already = map[time]
if (!already) {
map[time] = {index: index++, time, children: [item]}
}
else already.children.push(item)
}) })
.finally(() => this.config.loading = false)
},
const result = [] //
transformList(list) {
const thisYear = new Date().getFullYear()
const map = {}
let index = 0
list.forEach(item => {
let time = new Date(item.ptime),
year = time.getFullYear(),
month = time.getMonth() + 1,
day = time.getDate()
//
item.__read = this.mode === 'read'
time = (year === thisYear ? '' : `${year}`) + `${month}${day}`
const already = map[time]
if (!already) {
map[time] = {index: index++, time, children: [item]}
}
else already.children.push(item)
})
Object.keys(map).forEach(key => result[map[key].index] = map[key]) const result = []
return result Object.keys(map).forEach(key => result[map[key].index] = map[key])
},
read(msg) { return result
if (msg.read) return },
msg.read = true
this.$store.commit('message/unreadCount', this.unreadCount - 1)
read(msg.id)
},
readAll() { read(msg) {
readAll().then(() => this.search()) if (msg.read) return
}, msg.read = true
this.$store.commit('message/unreadCount', this.unreadCount - 1)
read(msg.id)
},
transformType(type) { readAll() {
switch (type) { readAll().then(() => this.search())
case 0: },
return '提醒'
case 1: transformType(type) {
return '公告' switch (type) {
} case 0:
}, return '提醒'
} case 1:
return '公告'
}
},
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.message-stream { .message-stream {
&__item { &__item {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
color: #666; color: #666;
background: #FFF9EA; background: #FFF9EA;
& + & { & + & {
margin-top: 20px; margin-top: 20px;
} }
&.viewed { &.viewed {
cursor: auto; cursor: auto;
background: transparent; background: transparent;
&::before { &::before {
visibility: hidden; visibility: hidden;
}
} }
}
&__title { &__title {
text-align: left; text-align: left;
.dot { .dot {
height: 8px; height: 8px;
width: 8px; width: 8px;
margin-right: 0; margin-right: 0;
}
} }
}
&__footer { &__footer {
text-align: right; text-align: right;
}
} }
} }
}
</style> </style>

@ -10,59 +10,59 @@
</template> </template>
<script> <script>
import MessageStream from './MessageStream' import MessageStream from './MessageStream'
export default { export default {
name: "userMessage", name: "userMessage",
components: {MessageStream}, components: {MessageStream},
data() { data() {
return { return {
tabs: [{label: '未读消息', name: 'unread'}, {label: '已读消息', name: 'read'}], tabs: [{label: '未读消息', name: 'unread'}, {label: '已读消息', name: 'read'}],
activeTab: 'unread', activeTab: 'unread',
} }
}, },
computed: { computed: {
tabPosition() { tabPosition() {
return this.$store.state.app.device === 'pc' ? 'left' : 'top' return this.$store.state.app.device === 'pc' ? 'left' : 'top'
}, },
tabContentClass() { tabContentClass() {
return this.tabPosition === 'left' ? 'tab-main-right' : 'tab-main-top' return this.tabPosition === 'left' ? 'tab-main-right' : 'tab-main-top'
}
} }
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.user-message-page { .user-message-page {
.el-tabs--left { .el-tabs--left {
.el-tabs__item.is-left { .el-tabs__item.is-left {
text-align: center; text-align: center;
&.is-active {
background: #f0faff;
}
}
.el-tabs__header.is-left { &.is-active {
min-width: 200px; background: #f0faff;
} }
} }
.el-tabs__item { .el-tabs__header.is-left {
font-size: 16px; min-width: 200px;
} }
}
.tab-main { .el-tabs__item {
&-right { font-size: 16px;
padding: 8px 40px; }
}
&-bottom { .tab-main {
padding: 20px 40px; &-right {
} padding: 8px 40px;
}
&-bottom {
padding: 20px 40px;
} }
} }
}
</style> </style>

@ -1,13 +1,13 @@
<template> <template>
<el-dialog <el-dialog
v-drag-dialog v-drag-dialog
:visible="value" :visible="value"
append-to-body append-to-body
title="选择采购订单" title="选择采购订单"
width="70%" width="70%"
top="50px" top="50px"
@close="cancel" @close="cancel"
@open="search" @open="search"
> >
<el-scrollbar> <el-scrollbar>
<el-row> <el-row>
@ -22,16 +22,16 @@
<liner-progress :show="row._loading"/> <liner-progress :show="row._loading"/>
<div style="text-align: center" v-show="!row._loading"> <div style="text-align: center" v-show="!row._loading">
<el-table <el-table
:data="row.data" :data="row.data"
row-key="id" row-key="id"
border border
@selection-change="row._selection=$event" @selection-change="row._selection=$event"
> >
<el-table-column <el-table-column
:selectable="p=>p.remainNum>0" :selectable="p=>p.remainNum>0"
align="center" align="center"
type="selection" type="selection"
width="60" width="60"
/> />
<el-table-column align="center" label="#" type="index" width="80"/> <el-table-column align="center" label="#" type="index" width="80"/>
<el-table-column align="center" label="商品" prop="cname" show-overflow-tooltip/> <el-table-column align="center" label="商品" prop="cname" show-overflow-tooltip/>
@ -39,10 +39,10 @@
<el-table-column align="center" label="未入库数量" prop="remainNum"/> <el-table-column align="center" label="未入库数量" prop="remainNum"/>
</el-table> </el-table>
<el-button <el-button
size="small" size="small"
style="margin-top: 10px" style="margin-top: 10px"
type="primary" type="primary"
@click="confirm(row)" @click="confirm(row)"
> >
确定 确定
</el-button> </el-button>
@ -53,11 +53,11 @@
<el-table-column align="center" label="单号" prop="id" show-overflow-tooltip/> <el-table-column align="center" label="单号" prop="id" show-overflow-tooltip/>
<el-table-column align="center" label="创建人" prop="cname" show-overflow-tooltip/> <el-table-column align="center" label="创建人" prop="cname" show-overflow-tooltip/>
<el-table-column align="center" label="创建时间" show-overflow-tooltip> <el-table-column align="center" label="创建时间" show-overflow-tooltip>
<template v-slot="{row}">{{row.ctime | timestamp2Date}}</template> <template v-slot="{row}">{{ row.ctime | timestamp2Date }}</template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="审核人" prop="vname" show-overflow-tooltip/> <el-table-column align="center" label="审核人" prop="vname" show-overflow-tooltip/>
<el-table-column align="center" label="审核时间" show-overflow-tooltip> <el-table-column align="center" label="审核时间" show-overflow-tooltip>
<template v-slot="{row}">{{row.vtime | timestamp2Date}}</template> <template v-slot="{row}">{{ row.vtime | timestamp2Date }}</template>
</el-table-column> </el-table-column>
</abstract-table> </abstract-table>
</el-row> </el-row>
@ -66,77 +66,77 @@
</template> </template>
<script> <script>
import LinerProgress from '@/components/LinerProgress' import LinerProgress from '@/components/LinerProgress'
import dialogMixin from "@/mixins/dialogMixin" import dialogMixin from "@/mixins/dialogMixin"
import tableMixin from '@/mixins/tablePageMixin' import tableMixin from '@/mixins/tablePageMixin'
import {getSubById, search} from "@/api/doc/purchase/order" import {getSubById, search} from "@/api/doc/purchase/order"
import {elError} from "@/utils/message" import {elError} from "@/utils/message"
export default { export default {
name: "OrderSelector", name: "OrderSelector",
mixins: [dialogMixin, tableMixin], mixins: [dialogMixin, tableMixin],
components: {LinerProgress}, components: {LinerProgress},
props: {value: Boolean}, props: {value: Boolean},
data() { data() {
return { return {
searchForm: { searchForm: {
status: 2, status: 2,
finish: '0,1' finish: '0,1'
}
} }
}, }
},
methods: { methods: {
search() { search() {
if (!this.value || this.config.loading) return if (!this.value || this.config.loading) return
this.config.loading = true this.config.loading = true
this.row = null this.row = null
search(this.searchForm) search(this.searchForm)
.then(({list, total}) => { .then(({list, total}) => {
list.forEach(i => { list.forEach(i => {
i._loading = false // i._loading = false //
i._loaded = false // i._loaded = false //
i._selection = [] // i._selection = [] //
})
this.searchForm.total = total
this.tableData = list
}) })
.finally(() => this.config.loading = false) this.searchForm.total = total
}, this.tableData = list
})
.finally(() => this.config.loading = false)
},
getSubList(row) { getSubList(row) {
if (row._loaded || row._loading) return if (row._loaded || row._loading) return
row._loading = true row._loading = true
getSubById(row.id) getSubById(row.id)
.then(data => { .then(data => {
row.data = data row.data = data
row._loaded = true row._loaded = true
}) })
.finally(() => row._loading = false) .finally(() => row._loading = false)
}, },
confirm(row) { confirm(row) {
if (row._selection.length <= 0) return elError('请选择要入库的商品') if (row._selection.length <= 0) return elError('请选择要入库的商品')
this.$emit('select', row.id, row._selection) this.$emit('select', row.id, row._selection)
this.cancel() this.cancel()
}, },
cancel() { cancel() {
this.closeDialog() this.closeDialog()
this.tableData = [] this.tableData = []
}
} }
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.el-table { .el-table {
.nets-table-btn { .nets-table-btn {
margin: 0 auto; margin: 0 auto;
}
} }
}
</style> </style>

@ -1,10 +1,10 @@
<template> <template>
<div v-loading="loading" class="detail-page"> <div v-loading="loading" class="detail-page">
<doc-detail-header <doc-detail-header
:title="this.title" :title="this.title"
:description="headerDescription" :description="headerDescription"
:extra="headerExtra" :extra="headerExtra"
@close="close" @close="close"
/> />
<abstract-form :model="form" :rules="rules"> <abstract-form :model="form" :rules="rules">
@ -17,7 +17,7 @@
<el-input v-if="canSave" :value="form.pid" readonly> <el-input v-if="canSave" :value="form.pid" readonly>
<el-button slot="append" @click="parentDialog=true">选择</el-button> <el-button slot="append" @click="parentDialog=true">选择</el-button>
</el-input> </el-input>
<template v-else>{{form.pid}}</template> <template v-else>{{ form.pid }}</template>
</abstract-form-item> </abstract-form-item>
</collapse-card> </collapse-card>
@ -28,14 +28,14 @@
<el-table-column align="center" label="入库数量" width="150"> <el-table-column align="center" label="入库数量" width="150">
<template v-slot="{row}"> <template v-slot="{row}">
<el-input-number <el-input-number
v-if="canSave" v-if="canSave"
v-model="row.num" v-model="row.num"
controls-position="right" controls-position="right"
:min="0" :min="0"
size="small" size="small"
@change="(nv,ov)=>changeInboundNum(nv,ov,row)" @change="(nv,ov)=>changeInboundNum(nv,ov,row)"
/> />
<span v-else>{{row.num}}</span> <span v-else>{{ row.num }}</span>
</template> </template>
</el-table-column> </el-table-column>
</abstract-table> </abstract-table>
@ -43,21 +43,21 @@
<collapse-card header="附件"> <collapse-card header="附件">
<upload-file <upload-file
:file-list="form.imageList" :file-list="form.imageList"
:disabled="!canSave" :disabled="!canSave"
@remove="removeUpload" @remove="removeUpload"
@success="uploadSuccess" @success="uploadSuccess"
/> />
</collapse-card> </collapse-card>
<collapse-card header="备注"> <collapse-card header="备注">
<el-input <el-input
v-model="form.remark" v-model="form.remark"
:rows="4" :rows="4"
:readonly="!canSave" :readonly="!canSave"
maxlength="200" maxlength="200"
show-word-limit show-word-limit
type="textarea" type="textarea"
/> />
</collapse-card> </collapse-card>
</abstract-form> </abstract-form>
@ -78,106 +78,106 @@
</template> </template>
<script> <script>
import docDetailMixin from "@/mixins/docDetailMixin" import docDetailMixin from "@/mixins/docDetailMixin"
import OrderSelector from './components/OrderSelector' import OrderSelector from './components/OrderSelector'
import {baseUrl, add, commit, getById, pass, reject, update, withdraw} from "@/api/doc/purchase/inbound" import {baseUrl, add, commit, getById, pass, reject, update, withdraw} from "@/api/doc/purchase/inbound"
import {getSubById as getParentSubById} from "@/api/doc/purchase/order" import {getSubById as getParentSubById} from "@/api/doc/purchase/order"
import {isEmpty} from "@/utils" import {isEmpty} from "@/utils"
import {elAlert} from "@/utils/message" import {elAlert} from "@/utils/message"
import {isInteger} from "@/utils/validate" import {isInteger} from "@/utils/validate"
export default { export default {
name: "purchaseInboundDetail", name: "purchaseInboundDetail",
mixins: [docDetailMixin], mixins: [docDetailMixin],
components: {OrderSelector}, components: {OrderSelector},
data() { data() {
return { return {
baseUrl, baseUrl,
docName: '采购入库单', docName: '采购入库单',
api: { api: {
getById, add, update, commit, withdraw, pass, reject getById, add, update, commit, withdraw, pass, reject
}, },
form: { form: {
pid: null pid: null
}, },
rules: { rules: {
pid: [{required: true, message: '请选择采购订单', trigger: 'change'}] pid: [{required: true, message: '请选择采购订单', trigger: 'change'}]
}, },
parentDialog: false, parentDialog: false,
parentSubList: [] parentSubList: []
} }
},
computed: {
headerDescription() {
const f = this.$options.filters.timestamp2Date
return [
{label: '创建人:', content: this.form.cname},
{label: '创建时间:', content: f(this.form.ctime)},
{label: '审核人:', content: this.form.vname},
{label: '审核时间:', content: f(this.form.vtime)}
]
}, },
headerExtra() {
return [
{title: '单据状态', content: this.getStatus(this.form.status)}
]
}
},
computed: { methods: {
headerDescription() { afterInit() {
const f = this.$options.filters.timestamp2Date if (this.type !== 'edit') return
return [ return getParentSubById(this.form.pid).then(data => this.parentSubList = data)
{label: '创建人:', content: this.form.cname}, },
{label: '创建时间:', content: f(this.form.ctime)}, selectParent(id, sub) {
{label: '审核人:', content: this.form.vname}, this.parentSubList = sub
{label: '审核时间:', content: f(this.form.vtime)} this.form.pid = id
] this.form.data = sub.map(i => ({
}, pid: this.form.id,
headerExtra() { cid: i.cid,
return [ cname: i.cname,
{title: '单据状态', content: this.getStatus(this.form.status)} num: i.remainNum
] }))
},
changeInboundNum(nv, ov, row) {
let parentSub = this.parentSubList.find(i => i.cid === row.cid)
if (!parentSub || nv > parentSub.remainNum) {
return elAlert(`${row.cname}的入库数量超出采购数量`, () => row.num = ov)
} }
}, },
validate() {
if (this.type === 'edit' && isEmpty(this.form.id)) {
return '属性缺失,请关闭弹窗刷新重试'
}
if (this.form.data.length <= 0) {
return '入库商品不能为空'
}
methods: { let index = 1
afterInit() { for (let sub of this.form.data) {
if (this.type !== 'edit') return if (isEmpty(sub.num)) {
return getParentSubById(this.form.pid).then(data => this.parentSubList = data) return `${index}个商品数量不能为空`
},
selectParent(id, sub) {
this.parentSubList = sub
this.form.pid = id
this.form.data = sub.map(i => ({
pid: this.form.id,
cid: i.cid,
cname: i.cname,
num: i.remainNum
}))
},
changeInboundNum(nv, ov, row) {
let parentSub = this.parentSubList.find(i => i.cid === row.cid)
if (!parentSub || nv > parentSub.remainNum) {
return elAlert(`${row.cname}的入库数量超出采购数量`, () => row.num = ov)
} }
}, if (sub.num < 0 || !isInteger(sub.num)) {
validate() { return `${index}个商品数量有误`
if (this.type === 'edit' && isEmpty(this.form.id)) {
return '属性缺失,请关闭弹窗刷新重试'
}
if (this.form.data.length <= 0) {
return '入库商品不能为空'
} }
let index = 1 let parentSub = this.parentSubList.find(i => i.cid === sub.cid)
for (let sub of this.form.data) {
if (isEmpty(sub.num)) { if (!parentSub) {
return `${index}个商品数量不能为空` return `${index}个商品不在采购订单中`
} }
if (sub.num < 0 || !isInteger(sub.num)) { if (parentSub.remainNum < sub.num) {
return `${index}个商品数量有误` return `${index}个商品的入库数量超出采购数量`
}
let parentSub = this.parentSubList.find(i => i.cid === sub.cid)
if (!parentSub) {
return `${index}个商品不在采购订单中`
}
if (parentSub.remainNum < sub.num) {
return `${index}个商品的入库数量超出采购数量`
}
index++
} }
return null index++
} }
return null
} }
} }
}
</script> </script>

@ -1,88 +1,88 @@
<template> <template>
<tree-select <tree-select
:value="value" :value="value"
:data="tree" :data="tree"
:disabled="disabled" :disabled="disabled"
:props="props" :props="props"
size="small" size="small"
popper-append-to-body popper-append-to-body
@change="change" @change="change"
@input="emit" @input="emit"
/> />
</template> </template>
<script> <script>
import TreeSelect from '@/components/TreeSelect' import TreeSelect from '@/components/TreeSelect'
import {getAll} from "@/api/system/category" import {getAll} from "@/api/system/category"
import {isEmpty} from "@/utils" import {isEmpty} from "@/utils"
import {elError} from "@/utils/message" import {elError} from "@/utils/message"
export default { export default {
name: "CategorySelector", name: "CategorySelector",
components: {TreeSelect}, components: {TreeSelect},
props: { props: {
value: {required: true}, value: {required: true},
disabled: Boolean, disabled: Boolean,
selected: {type: Array, default: () => []} selected: {type: Array, default: () => []}
}, },
data() { data() {
return { return {
props: { props: {
label: 'name', label: 'name',
children: 'children', children: 'children',
disabled: '_disabled' disabled: '_disabled'
}
} }
}, }
},
computed: { computed: {
tree() { tree() {
const arr = this.$store.state.dataCache.categoryTree const arr = this.$store.state.dataCache.categoryTree
this.addDisabledAttr(arr) this.addDisabledAttr(arr)
return arr return arr
},
selectableCategories() {
return this.$store.state.dataCache.categories.filter(i => i.type === 1)
},
}, },
selectableCategories() {
return this.$store.state.dataCache.categories.filter(i => i.type === 1)
},
},
methods: { methods: {
init() { init() {
if (this.tree.length === 0) { if (this.tree.length === 0) {
getAll().then(data => this.$store.commit('dataCache/categories', data)) getAll().then(data => this.$store.commit('dataCache/categories', data))
} }
}, },
addDisabledAttr(arr) {
arr.forEach(i => {
i._disabled = i.type !== 1
i.children && this.addDisabledAttr(i.children)
})
},
change(v) { addDisabledAttr(arr) {
if (isEmpty(v) || this.selected.length <= 0) return arr.forEach(i => {
if (this.selected.includes(v)) { i._disabled = i.type !== 1
elError('该分类已选择') i.children && this.addDisabledAttr(i.children)
this.emit(null) })
} },
},
emit(v) { change(v) {
this.$emit('input', v) if (isEmpty(v) || this.selected.length <= 0) return
if (!isEmpty(v)) { if (this.selected.includes(v)) {
let item = this.selectableCategories.find(i => i.id === v) elError('该分类已选择')
this.$emit('get-name', item ? item.name : null) this.emit(null)
}
else this.$emit('get-name', null)
} }
}, },
mounted() { emit(v) {
this.init() this.$emit('input', v)
if (!isEmpty(v)) {
let item = this.selectableCategories.find(i => i.id === v)
this.$emit('get-name', item ? item.name : null)
}
else this.$emit('get-name', null)
} }
},
mounted() {
this.init()
} }
}
</script> </script>

@ -1,24 +1,24 @@
<template> <template>
<el-dialog <el-dialog
v-drag-dialog v-drag-dialog
:visible="value" :visible="value"
append-to-body append-to-body
title="选择供应商" title="选择供应商"
width="70%" width="70%"
top="50px" top="50px"
@close="cancel" @close="cancel"
@open="search" @open="search"
> >
<el-row> <el-row>
<el-button icon="el-icon-search" size="small" type="success" @click="search"> </el-button> <el-button icon="el-icon-search" size="small" type="success" @click="search"> </el-button>
<el-button size="small" type="primary" @click="confirm"> </el-button> <el-button size="small" type="primary" @click="confirm"> </el-button>
<el-button plain size="small" @click="closeDialog"> </el-button> <el-button plain size="small" @click="closeDialog"> </el-button>
<el-button <el-button
v-if="canGoToSysSupplierPage" v-if="canGoToSysSupplierPage"
plain plain
size="small" size="small"
type="dashed" type="dashed"
@click="goToSysSupplierPage" @click="goToSysSupplierPage"
> >
添加供应商 添加供应商
</el-button> </el-button>
@ -27,13 +27,13 @@
<el-row class="table-container"> <el-row class="table-container">
<liner-progress :show="config.loading"/> <liner-progress :show="config.loading"/>
<el-table <el-table
ref="table" ref="table"
:data="tableData" :data="tableData"
current-row-key="id" current-row-key="id"
highlight-current-row highlight-current-row
row-key="id" row-key="id"
@row-click="rowClick" @row-click="rowClick"
@row-dblclick="dbclick" @row-dblclick="dbclick"
> >
<el-table-column align="center" label="#" type="index" width="80"/> <el-table-column align="center" label="#" type="index" width="80"/>
<el-table-column align="center" label="供应商" prop="name" show-overflow-tooltip/> <el-table-column align="center" label="供应商" prop="name" show-overflow-tooltip/>
@ -42,22 +42,22 @@
<el-table-column align="center" label="联系人" prop="linkman" show-overflow-tooltip/> <el-table-column align="center" label="联系人" prop="linkman" show-overflow-tooltip/>
<el-table-column align="center" label="联系电话" prop="linkphone" show-overflow-tooltip/> <el-table-column align="center" label="联系电话" prop="linkphone" show-overflow-tooltip/>
<el-table-column align="center" label="创建时间" width="150" show-overflow-tooltip> <el-table-column align="center" label="创建时间" width="150" show-overflow-tooltip>
<template v-slot="{row}">{{row.ctime | timestamp2Date}}</template> <template v-slot="{row}">{{ row.ctime | timestamp2Date }}</template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="状 态" width="120"> <el-table-column align="center" label="状 态" width="120">
<template v-slot="{row}"> <template v-slot="{row}">
<span :class="row.status===1?'success':'error'" class="dot"/> <span :class="row.status===1?'success':'error'" class="dot"/>
<span>{{row.status===1?'启用':'禁用'}}</span> <span>{{ row.status === 1 ? '启用' : '禁用' }}</span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination <el-pagination
background background
:current-page="searchForm.page" :current-page="searchForm.page"
:page-size="searchForm.pageSize" :page-size="searchForm.pageSize"
:total="searchForm.total" :total="searchForm.total"
layout="total, prev, pager, next, jumper" layout="total, prev, pager, next, jumper"
@current-change="pageChange" @current-change="pageChange"
/> />
</el-row> </el-row>
</el-scrollbar> </el-scrollbar>
@ -65,60 +65,60 @@
</template> </template>
<script> <script>
import LinerProgress from '@/components/LinerProgress' import LinerProgress from '@/components/LinerProgress'
import dialogMixin from "@/mixins/dialogMixin" import dialogMixin from "@/mixins/dialogMixin"
import tableMixin from '@/mixins/tablePageMixin' import tableMixin from '@/mixins/tablePageMixin'
import {getSuppliers} from "@/api/system/supplier" import {getSuppliers} from "@/api/system/supplier"
import {elError} from "@/utils/message" import {elError} from "@/utils/message"
import {auth} from "@/utils/auth" import {auth} from "@/utils/auth"
export default { export default {
name: "SupplierSelector", name: "SupplierSelector",
mixins: [dialogMixin, tableMixin], mixins: [dialogMixin, tableMixin],
components: {LinerProgress}, components: {LinerProgress},
props: {value: Boolean}, props: {value: Boolean},
data() { data() {
return { return {
searchForm: { searchForm: {
status: 1 status: 1
}
} }
}
},
computed: {
canGoToSysSupplierPage() {
return auth('/system/supplier')
}
},
methods: {
search() {
if (!this.value || this.config.loading) return
this.config.loading = true
this.row = null
getSuppliers(this.searchForm)
.then(({list, total}) => {
this.searchForm.total = total
this.tableData = list
})
.finally(() => this.config.loading = false)
}, },
computed: { dbclick(row) {
canGoToSysSupplierPage() { this.row = row
return auth('/system/supplier') this.confirm()
}
}, },
methods: { confirm() {
search() { if (!this.row) return elError('请选择一个供应商')
if (!this.value || this.config.loading) return this.$emit('select', this.row)
this.config.loading = true this.cancel()
this.row = null },
getSuppliers(this.searchForm) cancel() {
.then(({list, total}) => { this.closeDialog()
this.searchForm.total = total this.tableData = []
this.tableData = list },
}) goToSysSupplierPage() {
.finally(() => this.config.loading = false) this.closeDialog()
}, this.$emit('jump')
dbclick(row) { this.$nextTick(() => this.$router.push('/system/supplier'))
this.row = row
this.confirm()
},
confirm() {
if (!this.row) return elError('请选择一个供应商')
this.$emit('select', this.row)
this.cancel()
},
cancel() {
this.closeDialog()
this.tableData = []
},
goToSysSupplierPage() {
this.closeDialog()
this.$emit('jump')
this.$nextTick(() => this.$router.push('/system/supplier'))
}
} }
} }
}
</script> </script>

@ -1,10 +1,10 @@
<template> <template>
<div v-loading="loading" class="detail-page"> <div v-loading="loading" class="detail-page">
<doc-detail-header <doc-detail-header
:title="this.title" :title="this.title"
:description="headerDescription" :description="headerDescription"
:extra="headerExtra" :extra="headerExtra"
@close="close" @close="close"
/> />
<abstract-form :model="form" :rules="rules"> <abstract-form :model="form" :rules="rules">
@ -17,7 +17,7 @@
<el-input v-if="canSave" :value="form.sname" readonly> <el-input v-if="canSave" :value="form.sname" readonly>
<el-button slot="append" @click="supplierDialog=true">选择</el-button> <el-button slot="append" @click="supplierDialog=true">选择</el-button>
</el-input> </el-input>
<template v-else>{{form.sname}}</template> <template v-else>{{ form.sname }}</template>
</abstract-form-item> </abstract-form-item>
</collapse-card> </collapse-card>
@ -27,43 +27,43 @@
<el-table-column align="center" label="商 品"> <el-table-column align="center" label="商 品">
<template v-slot="{row}"> <template v-slot="{row}">
<category-selector <category-selector
v-if="row._editable" v-if="row._editable"
v-model="row.cid" v-model="row.cid"
:selected="selectedCategories" :selected="selectedCategories"
@get-name="row.cname=$event" @get-name="row.cname=$event"
/> />
<span v-else>{{row.cname}}</span> <span v-else>{{ row.cname }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="采购数量"> <el-table-column align="center" label="采购数量">
<template v-slot="{row}"> <template v-slot="{row}">
<el-input-number <el-input-number
v-if="row._editable" v-if="row._editable"
v-model="row.num" v-model="row.num"
controls-position="right" controls-position="right"
:min="0" :min="0"
size="small" size="small"
/> />
<span v-else>{{row.num}}</span> <span v-else>{{ row.num }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="采购单价"> <el-table-column align="center" label="采购单价">
<template v-slot="{row}"> <template v-slot="{row}">
<el-input-number <el-input-number
v-if="row._editable" v-if="row._editable"
v-model="row.price" v-model="row.price"
controls-position="right" controls-position="right"
:min="0" :min="0"
size="small" size="small"
/> />
<span v-else>{{row.price}}</span> <span v-else>{{ row.price }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column v-if="form.id" align="center" label="剩余未入库" prop="remainNum"/> <el-table-column v-if="form.id" align="center" label="剩余未入库" prop="remainNum"/>
<el-table-column v-if="form.id" align="center" label="入库情况" width="120"> <el-table-column v-if="form.id" align="center" label="入库情况" width="120">
<template v-slot="{row}"> <template v-slot="{row}">
<span :class="{success:row.remainNum===0}" class="dot"/> <span :class="{success:row.remainNum===0}" class="dot"/>
{{row.remainNum===0?'已全部入库':'未全部入库'}} {{ row.remainNum === 0 ? '已全部入库' : '未全部入库' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column v-if="canSave" align="center" label="操作" width="140"> <el-table-column v-if="canSave" align="center" label="操作" width="140">
@ -75,11 +75,11 @@
</el-table-column> </el-table-column>
<div v-if="canSave" slot="append" class="table-add-btn"> <div v-if="canSave" slot="append" class="table-add-btn">
<el-button <el-button
plain plain
type="dashed" type="dashed"
icon="el-icon-plus" icon="el-icon-plus"
:disabled="!canAddSub" :disabled="!canAddSub"
@click="addSub" @click="addSub"
> >
添加采购商品 添加采购商品
</el-button> </el-button>
@ -89,21 +89,21 @@
<collapse-card header="附件"> <collapse-card header="附件">
<upload-file <upload-file
:file-list="form.imageList" :file-list="form.imageList"
:disabled="!canSave" :disabled="!canSave"
@remove="removeUpload" @remove="removeUpload"
@success="uploadSuccess" @success="uploadSuccess"
/> />
</collapse-card> </collapse-card>
<collapse-card header="备注"> <collapse-card header="备注">
<el-input <el-input
v-model="form.remark" v-model="form.remark"
:rows="4" :rows="4"
:readonly="!canSave" :readonly="!canSave"
maxlength="200" maxlength="200"
show-word-limit show-word-limit
type="textarea" type="textarea"
/> />
</collapse-card> </collapse-card>
</abstract-form> </abstract-form>
@ -124,116 +124,116 @@
</template> </template>
<script> <script>
import docDetailMixin from "@/mixins/docDetailMixin" import docDetailMixin from "@/mixins/docDetailMixin"
import CategorySelector from './components/CategorySelector' import CategorySelector from './components/CategorySelector'
import SupplierSelector from './components/SupplierSelector' import SupplierSelector from './components/SupplierSelector'
import {baseUrl, add, commit, getById, pass, reject, update, withdraw} from "@/api/doc/purchase/order" import {baseUrl, add, commit, getById, pass, reject, update, withdraw} from "@/api/doc/purchase/order"
import {isEmpty} from "@/utils" import {isEmpty} from "@/utils"
import {mul, plus} from "@/utils/math" import {mul, plus} from "@/utils/math"
import {isInteger} from "@/utils/validate" import {isInteger} from "@/utils/validate"
export default { export default {
name: "purchaseOrderDetail", name: "purchaseOrderDetail",
mixins: [docDetailMixin], mixins: [docDetailMixin],
components: {CategorySelector, SupplierSelector}, components: {CategorySelector, SupplierSelector},
data() { data() {
return { return {
baseUrl, baseUrl,
docName: '采购订单', docName: '采购订单',
api: { api: {
getById, add, update, commit, withdraw, pass, reject getById, add, update, commit, withdraw, pass, reject
},
form: {
sid: null,
sname: null,
finish: 0,
ftime: null,
total: 0
},
rules: {
sname: [{required: true, message: '供应商不能为空', trigger: 'change'}]
},
supplierDialog: false
}
},
computed: {
headerDescription() {
const f = this.$options.filters.timestamp2Date
return [
{label: '创建人:', content: this.form.cname},
{label: '创建时间:', content: f(this.form.ctime)},
{label: '审核人:', content: this.form.vname},
{label: '审核时间:', content: f(this.form.vtime)},
{label: '完成时间:', content: f(this.form.ftime)}
]
}, },
headerExtra() { form: {
return [ sid: null,
{title: '单据状态', content: this.getStatus(this.form.status)}, sname: null,
{title: '完成情况', content: this.getFinish(this.form.finish)} finish: 0,
] ftime: null,
total: 0
}, },
selectedCategories() { rules: {
return this.form.data.map(i => i.cid) sname: [{required: true, message: '供应商不能为空', trigger: 'change'}]
}, },
canAddSub() { supplierDialog: false
return this.form.data.every(i => !i._editable) }
} },
computed: {
headerDescription() {
const f = this.$options.filters.timestamp2Date
return [
{label: '创建人:', content: this.form.cname},
{label: '创建时间:', content: f(this.form.ctime)},
{label: '审核人:', content: this.form.vname},
{label: '审核时间:', content: f(this.form.vtime)},
{label: '完成时间:', content: f(this.form.ftime)}
]
}, },
headerExtra() {
return [
{title: '单据状态', content: this.getStatus(this.form.status)},
{title: '完成情况', content: this.getFinish(this.form.finish)}
]
},
selectedCategories() {
return this.form.data.map(i => i.cid)
},
canAddSub() {
return this.form.data.every(i => !i._editable)
}
},
methods: { methods: {
selectSupplier(row) { selectSupplier(row) {
this.form.sid = row.id this.form.sid = row.id
this.form.sname = row.name this.form.sname = row.name
}, },
addSub() { addSub() {
this.form.data.push({ this.form.data.push({
cid: null, cid: null,
cname: null, cname: null,
num: 0, num: 0,
price: 0, price: 0,
_editable: true _editable: true
}) })
}, },
delSub(row, index) { delSub(row, index) {
this.form.data.splice(index, 1) this.form.data.splice(index, 1)
}, },
validate() { validate() {
if (this.form.data.length <= 0) { if (this.form.data.length <= 0) {
return '采购商品不能为空' return '采购商品不能为空'
}
if (this.type === 'edit' && isEmpty(this.form.id)) {
return '属性缺失,请关闭弹窗刷新重试'
}
let index = 1
for (let sub of this.form.data) {
if (isEmpty(sub.cid, sub.cname)) {
return `${index}个商品分类不能为空`
} }
if (this.type === 'edit' && isEmpty(this.form.id)) { if (isEmpty(sub.num)) {
return '属性缺失,请关闭弹窗刷新重试' return `${index}个商品数量不能为空`
} }
let index = 1 if (isEmpty(sub.price)) {
for (let sub of this.form.data) { return `${index + 1}个商品单价不能为空`
if (isEmpty(sub.cid, sub.cname)) {
return `${index}个商品分类不能为空`
}
if (isEmpty(sub.num)) {
return `${index}个商品数量不能为空`
}
if (isEmpty(sub.price)) {
return `${index + 1}个商品单价不能为空`
}
if (sub.num < 0 || !isInteger(sub.num)) {
return `${index}个商品数量有误`
}
if (sub.price <= 0 || !isInteger(sub.price)) {
return `${index}个商品单价有误`
}
this.form.total = plus(this.form.total, mul(sub.num, sub.price))
index++
} }
return null if (sub.num < 0 || !isInteger(sub.num)) {
}, return `${index}个商品数量有误`
afterInit() { }
this.form.data.forEach(item => item.__editable = false) if (sub.price <= 0 || !isInteger(sub.price)) {
return `${index}个商品单价有误`
}
this.form.total = plus(this.form.total, mul(sub.num, sub.price))
index++
} }
return null
},
afterInit() {
this.form.data.forEach(item => item.__editable = false)
} }
} }
}
</script> </script>

@ -1,13 +1,13 @@
<template> <template>
<el-dialog <el-dialog
v-drag-dialog v-drag-dialog
:visible="value" :visible="value"
append-to-body append-to-body
title="选择客户" title="选择客户"
width="70%" width="70%"
top="50px" top="50px"
@close="cancel" @close="cancel"
@open="search" @open="search"
> >
<el-row> <el-row>
<el-button icon="el-icon-search" size="small" type="success" @click="search"> </el-button> <el-button icon="el-icon-search" size="small" type="success" @click="search"> </el-button>
@ -27,23 +27,23 @@
<el-table-column align="center" label="联系人" prop="linkman" show-overflow-tooltip/> <el-table-column align="center" label="联系人" prop="linkman" show-overflow-tooltip/>
<el-table-column align="center" label="联系电话" prop="linkphone" show-overflow-tooltip/> <el-table-column align="center" label="联系电话" prop="linkphone" show-overflow-tooltip/>
<el-table-column align="center" label="创建时间" width="150" show-overflow-tooltip> <el-table-column align="center" label="创建时间" width="150" show-overflow-tooltip>
<template v-slot="{row}">{{row.ctime | timestamp2Date}}</template> <template v-slot="{row}">{{ row.ctime | timestamp2Date }}</template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="状 态" width="120"> <el-table-column align="center" label="状 态" width="120">
<template v-slot="{row}"> <template v-slot="{row}">
<span :class="row.status===1?'success':'error'" class="dot"/> <span :class="row.status===1?'success':'error'" class="dot"/>
<span>{{row.status===1?'启用':'禁用'}}</span> <span>{{ row.status === 1 ? '启用' : '禁用' }}</span>
</template> </template>
</el-table-column> </el-table-column>
</abstract-table> </abstract-table>
<el-pagination <el-pagination
background background
:current-page="searchForm.page" :current-page="searchForm.page"
:page-size="searchForm.pageSize" :page-size="searchForm.pageSize"
:total="searchForm.total" :total="searchForm.total"
layout="total, prev, pager, next, jumper" layout="total, prev, pager, next, jumper"
@current-change="pageChange" @current-change="pageChange"
/> />
</el-row> </el-row>
</el-scrollbar> </el-scrollbar>
@ -51,56 +51,56 @@
</template> </template>
<script> <script>
import LinerProgress from '@/components/LinerProgress' import LinerProgress from '@/components/LinerProgress'
import dialogMixin from "@/mixins/dialogMixin" import dialogMixin from "@/mixins/dialogMixin"
import tableMixin from '@/mixins/tablePageMixin' import tableMixin from '@/mixins/tablePageMixin'
import {getCustomers} from "@/api/system/customer" import {getCustomers} from "@/api/system/customer"
import {elError} from "@/utils/message" import {elError} from "@/utils/message"
export default { export default {
name: "CustomerSelector", name: "CustomerSelector",
mixins: [dialogMixin, tableMixin], mixins: [dialogMixin, tableMixin],
components: {LinerProgress}, components: {LinerProgress},
props: {value: Boolean}, props: {value: Boolean},
data() { data() {
return { return {
searchForm: { searchForm: {
status: 1 status: 1
}
} }
}, }
},
methods: { methods: {
search() { search() {
if (!this.value || this.config.loading) return if (!this.value || this.config.loading) return
this.config.loading = true this.config.loading = true
this.row = null this.row = null
getCustomers(this.searchForm) getCustomers(this.searchForm)
.then(({list, total}) => { .then(({list, total}) => {
this.searchForm.total = total this.searchForm.total = total
this.tableData = list this.tableData = list
}) })
.finally(() => this.config.loading = false) .finally(() => this.config.loading = false)
}, },
dbclick(row) { dbclick(row) {
this.row = row this.row = row
this.confirm() this.confirm()
}, },
confirm() { confirm() {
if (!this.row) return elError('请选择一个客户') if (!this.row) return elError('请选择一个客户')
this.$emit('select', this.row) this.$emit('select', this.row)
}, },
cancel() { cancel() {
this.closeDialog() this.closeDialog()
this.tableData = [] this.tableData = []
}
} }
} }
}
</script> </script>

@ -1,13 +1,13 @@
<template> <template>
<el-dialog <el-dialog
v-drag-dialog v-drag-dialog
:visible="value" :visible="value"
append-to-body append-to-body
title="选择库存商品" title="选择库存商品"
width="70%" width="70%"
top="50px" top="50px"
@close="cancel" @close="cancel"
@open="search" @open="search"
> >
<el-scrollbar> <el-scrollbar>
<el-row> <el-row>
@ -20,11 +20,11 @@
<liner-progress :show="config.loading"/> <liner-progress :show="config.loading"/>
<el-table <el-table
ref="table" ref="table"
:data="tableData" :data="tableData"
current-row-key="id" current-row-key="id"
row-key="id" row-key="id"
@selection-change="selection=$event" @selection-change="selection=$event"
> >
<el-table-column align="center" type="selection" reserve-selection width="80"/> <el-table-column align="center" type="selection" reserve-selection width="80"/>
<el-table-column align="center" label="商品分类" prop="cname" show-overflow-tooltip/> <el-table-column align="center" label="商品分类" prop="cname" show-overflow-tooltip/>
@ -32,12 +32,12 @@
</el-table> </el-table>
<el-pagination <el-pagination
background background
:current-page="searchForm.page" :current-page="searchForm.page"
:page-size="searchForm.pageSize" :page-size="searchForm.pageSize"
:total="searchForm.total" :total="searchForm.total"
layout="total, prev, pager, next, jumper" layout="total, prev, pager, next, jumper"
@current-change="pageChange" @current-change="pageChange"
/> />
</el-row> </el-row>
</el-scrollbar> </el-scrollbar>
@ -45,50 +45,50 @@
</template> </template>
<script> <script>
import LinerProgress from '@/components/LinerProgress' import LinerProgress from '@/components/LinerProgress'
import dialogMixin from "@/mixins/dialogMixin" import dialogMixin from "@/mixins/dialogMixin"
import tableMixin from '@/mixins/tablePageMixin' import tableMixin from '@/mixins/tablePageMixin'
import {search} from "@/api/stock/current" import {search} from "@/api/stock/current"
import {elError} from "@/utils/message" import {elError} from "@/utils/message"
export default { export default {
name: "StockSelector", name: "StockSelector",
mixins: [dialogMixin, tableMixin], mixins: [dialogMixin, tableMixin],
components: {LinerProgress}, components: {LinerProgress},
props: {value: Boolean}, props: {value: Boolean},
data() { data() {
return { return {
selection: [] selection: []
} }
}, },
methods: { methods: {
search() { search() {
if (!this.value || this.config.loading) return if (!this.value || this.config.loading) return
this.config.loading = true this.config.loading = true
this.$refs.table && this.$refs.table.clearSelection() this.$refs.table && this.$refs.table.clearSelection()
search(this.searchForm) search(this.searchForm)
.then(({list, total}) => { .then(({list, total}) => {
this.searchForm.total = total this.searchForm.total = total
this.tableData = list this.tableData = list
}) })
.finally(() => this.config.loading = false) .finally(() => this.config.loading = false)
}, },
confirm() { confirm() {
if (this.selection.length <= 0) return elError('请选择商品') if (this.selection.length <= 0) return elError('请选择商品')
this.$emit('select', this.selection) this.$emit('select', this.selection)
}, },
cancel() { cancel() {
this.closeDialog() this.closeDialog()
this.$refs.table.clearSelection() this.$refs.table.clearSelection()
this.tableData = [] this.tableData = []
}
} }
} }
}
</script> </script>

@ -1,10 +1,10 @@
<template> <template>
<div v-loading="loading" class="detail-page"> <div v-loading="loading" class="detail-page">
<doc-detail-header <doc-detail-header
:title="this.title" :title="this.title"
:description="headerDescription" :description="headerDescription"
:extra="headerExtra" :extra="headerExtra"
@close="close" @close="close"
/> />
<abstract-form :model="form" :rules="rules"> <abstract-form :model="form" :rules="rules">
@ -17,7 +17,7 @@
<el-input v-if="canSave" :value="form.customerName" readonly> <el-input v-if="canSave" :value="form.customerName" readonly>
<el-button slot="append" @click="customerDialog=true">选择</el-button> <el-button slot="append" @click="customerDialog=true">选择</el-button>
</el-input> </el-input>
<template v-else>{{form.customerName}}</template> <template v-else>{{ form.customerName }}</template>
</abstract-form-item> </abstract-form-item>
</collapse-card> </collapse-card>
@ -28,41 +28,41 @@
<el-table-column align="center" label="销售数量"> <el-table-column align="center" label="销售数量">
<template v-slot="{row}"> <template v-slot="{row}">
<el-input-number <el-input-number
v-if="canSave" v-if="canSave"
v-model="row.num" v-model="row.num"
controls-position="right" controls-position="right"
:min="0" :min="0"
size="small" size="small"
/> />
<span v-else>{{row.num}}</span> <span v-else>{{ row.num }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="销售单价"> <el-table-column align="center" label="销售单价">
<template v-slot="{row}"> <template v-slot="{row}">
<el-input-number <el-input-number
v-if="canSave" v-if="canSave"
v-model="row.price" v-model="row.price"
controls-position="right" controls-position="right"
:min="0" :min="0"
size="small" size="small"
/> />
<span v-else>{{row.price}}</span> <span v-else>{{ row.price }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="剩余未出库" prop="remainNum"/> <el-table-column align="center" label="剩余未出库" prop="remainNum"/>
<el-table-column v-if="form.id" align="center" label="出库情况" width="120"> <el-table-column v-if="form.id" align="center" label="出库情况" width="120">
<template v-slot="{row}"> <template v-slot="{row}">
<span :class="{success:row.remainNum===0}" class="dot"/> <span :class="{success:row.remainNum===0}" class="dot"/>
{{row.remainNum===0?'已全部出库':'未全部出库'}} {{ row.remainNum === 0 ? '已全部出库' : '未全部出库' }}
</template> </template>
</el-table-column> </el-table-column>
<div v-if="canSave" slot="append" class="table-add-btn"> <div v-if="canSave" slot="append" class="table-add-btn">
<el-button <el-button
plain plain
type="dashed" type="dashed"
icon="el-icon-plus" icon="el-icon-plus"
:disabled="!canAddSub" :disabled="!canAddSub"
@click="stockDialog=true" @click="stockDialog=true"
> >
添加销售商品 添加销售商品
</el-button> </el-button>
@ -72,21 +72,21 @@
<collapse-card header="附件"> <collapse-card header="附件">
<upload-file <upload-file
:file-list="form.imageList" :file-list="form.imageList"
:disabled="!canSave" :disabled="!canSave"
@remove="removeUpload" @remove="removeUpload"
@success="uploadSuccess" @success="uploadSuccess"
/> />
</collapse-card> </collapse-card>
<collapse-card header="备注"> <collapse-card header="备注">
<el-input <el-input
v-model="form.remark" v-model="form.remark"
:rows="4" :rows="4"
:readonly="!canSave" :readonly="!canSave"
maxlength="200" maxlength="200"
show-word-limit show-word-limit
type="textarea" type="textarea"
/> />
</collapse-card> </collapse-card>
</abstract-form> </abstract-form>
@ -109,107 +109,107 @@
</template> </template>
<script> <script>
import docDetailMixin from "@/mixins/docDetailMixin" import docDetailMixin from "@/mixins/docDetailMixin"
import CustomerSelector from './components/CustomerSelector' import CustomerSelector from './components/CustomerSelector'
import StockSelector from './components/StockSelector' import StockSelector from './components/StockSelector'
import {baseUrl, add, commit, getById, pass, reject, update, withdraw} from "@/api/doc/sell/order" import {baseUrl, add, commit, getById, pass, reject, update, withdraw} from "@/api/doc/sell/order"
import {isEmpty} from "@/utils" import {isEmpty} from "@/utils"
import {mul, plus} from "@/utils/math" import {mul, plus} from "@/utils/math"
import {isInteger} from "@/utils/validate" import {isInteger} from "@/utils/validate"
export default { export default {
name: "sellOrderDetail", name: "sellOrderDetail",
mixins: [docDetailMixin], mixins: [docDetailMixin],
components: {CustomerSelector, StockSelector}, components: {CustomerSelector, StockSelector},
data() { data() {
return { return {
baseUrl, baseUrl,
docName: '销售订单', docName: '销售订单',
api: { api: {
getById, add, update, commit, withdraw, pass, reject getById, add, update, commit, withdraw, pass, reject
},
form: {
customerId: null,
customerName: null,
finish: 0,
ftime: null,
total: 0
},
rules: {
customerName: [{required: true, message: '客户不能为空', trigger: 'change'}]
},
customerDialog: false,
stockDialog: false
}
},
computed: {
headerDescription() {
const f = this.$options.filters.timestamp2Date
return [
{label: '创建人:', content: this.form.cname},
{label: '创建时间:', content: f(this.form.ctime)},
{label: '审核人:', content: this.form.vname},
{label: '审核时间:', content: f(this.form.vtime)},
{label: '完成时间:', content: f(this.form.ftime)}
]
}, },
headerExtra() { form: {
return [ customerId: null,
{title: '单据状态', content: this.getStatus(this.form.status)}, customerName: null,
{title: '完成情况', content: this.getFinish(this.form.finish)} finish: 0,
] ftime: null,
total: 0
}, },
selectedCategories() { rules: {
return this.form.data.map(i => i.cid) customerName: [{required: true, message: '客户不能为空', trigger: 'change'}]
}, },
canAddSub() { customerDialog: false,
return this.form.data.every(i => !i._editable) stockDialog: false
} }
},
computed: {
headerDescription() {
const f = this.$options.filters.timestamp2Date
return [
{label: '创建人:', content: this.form.cname},
{label: '创建时间:', content: f(this.form.ctime)},
{label: '审核人:', content: this.form.vname},
{label: '审核时间:', content: f(this.form.vtime)},
{label: '完成时间:', content: f(this.form.ftime)}
]
}, },
headerExtra() {
return [
{title: '单据状态', content: this.getStatus(this.form.status)},
{title: '完成情况', content: this.getFinish(this.form.finish)}
]
},
selectedCategories() {
return this.form.data.map(i => i.cid)
},
canAddSub() {
return this.form.data.every(i => !i._editable)
}
},
methods: { methods: {
selectCustomer(row) { selectCustomer(row) {
this.form.customerId = row.id this.form.customerId = row.id
this.form.customerName = row.name this.form.customerName = row.name
this.customerDialog = false this.customerDialog = false
}, },
selectStock(stocks) { selectStock(stocks) {
this.form.data = stocks.map(i => ({cid: i.cid, cname: i.cname, num: i.totalNum, price: 0})) this.form.data = stocks.map(i => ({cid: i.cid, cname: i.cname, num: i.totalNum, price: 0}))
this.stockDialog = false this.stockDialog = false
}, },
addSub() { addSub() {
this.form.data.push({ this.form.data.push({
cid: null, cid: null,
cname: null, cname: null,
num: 0, num: 0,
price: 0, price: 0,
remainNum: 0 remainNum: 0
}) })
}, },
delSub(row, index) { delSub(row, index) {
this.form.data.splice(index, 1) this.form.data.splice(index, 1)
}, },
validate() { validate() {
if (this.form.data.length <= 0) return '销售商品不能为空' if (this.form.data.length <= 0) return '销售商品不能为空'
if (this.type === 'edit' && isEmpty(this.form.id)) { if (this.type === 'edit' && isEmpty(this.form.id)) {
return '属性缺失,请关闭弹窗刷新重试' return '属性缺失,请关闭弹窗刷新重试'
} }
let index = 1 let index = 1
for (let sub of this.form.data) { for (let sub of this.form.data) {
if (isEmpty(sub.cid, sub.cname)) return `${index}个商品分类不能为空` if (isEmpty(sub.cid, sub.cname)) return `${index}个商品分类不能为空`
if (isEmpty(sub.num)) return `${index}个商品数量不能为空` if (isEmpty(sub.num)) return `${index}个商品数量不能为空`
if (isEmpty(sub.price)) return `${index + 1}个商品单价不能为空` if (isEmpty(sub.price)) return `${index + 1}个商品单价不能为空`
if (sub.num < 0 || !isInteger(sub.num)) return `${index}个商品数量有误` if (sub.num < 0 || !isInteger(sub.num)) return `${index}个商品数量有误`
if (sub.price <= 0 || !isInteger(sub.price)) return `${index}个商品单价有误` if (sub.price <= 0 || !isInteger(sub.price)) return `${index}个商品单价有误`
this.form.total = plus(this.form.total, mul(sub.num, sub.price)) this.form.total = plus(this.form.total, mul(sub.num, sub.price))
index++ index++
}
return null
} }
return null
} }
} }
}
</script> </script>

@ -1,13 +1,13 @@
<template> <template>
<el-dialog <el-dialog
v-drag-dialog v-drag-dialog
:visible="value" :visible="value"
append-to-body append-to-body
title="选择销售订单" title="选择销售订单"
width="70%" width="70%"
top="50px" top="50px"
@close="cancel" @close="cancel"
@open="search" @open="search"
> >
<el-scrollbar> <el-scrollbar>
<el-row> <el-row>
@ -22,16 +22,16 @@
<liner-progress :show="row._loading"/> <liner-progress :show="row._loading"/>
<div style="text-align: center" v-show="!row._loading"> <div style="text-align: center" v-show="!row._loading">
<el-table <el-table
:data="row.data" :data="row.data"
row-key="id" row-key="id"
border border
@selection-change="row._selection=$event" @selection-change="row._selection=$event"
> >
<el-table-column <el-table-column
:selectable="p=>p.remainNum>0" :selectable="p=>p.remainNum>0"
align="center" align="center"
type="selection" type="selection"
width="60" width="60"
/> />
<el-table-column align="center" label="#" type="index" width="80"/> <el-table-column align="center" label="#" type="index" width="80"/>
<el-table-column align="center" label="商品" prop="cname" show-overflow-tooltip/> <el-table-column align="center" label="商品" prop="cname" show-overflow-tooltip/>
@ -39,10 +39,10 @@
<el-table-column align="center" label="未出库数量" prop="remainNum"/> <el-table-column align="center" label="未出库数量" prop="remainNum"/>
</el-table> </el-table>
<el-button <el-button
size="small" size="small"
style="margin-top: 10px" style="margin-top: 10px"
type="primary" type="primary"
@click="() => confirm(row)" @click="() => confirm(row)"
> >
确定 确定
</el-button> </el-button>
@ -53,11 +53,11 @@
<el-table-column align="center" label="单号" prop="id" show-overflow-tooltip/> <el-table-column align="center" label="单号" prop="id" show-overflow-tooltip/>
<el-table-column align="center" label="创建人" prop="cname" show-overflow-tooltip/> <el-table-column align="center" label="创建人" prop="cname" show-overflow-tooltip/>
<el-table-column align="center" label="创建时间" show-overflow-tooltip> <el-table-column align="center" label="创建时间" show-overflow-tooltip>
<template v-slot="{row}">{{row.ctime | timestamp2Date}}</template> <template v-slot="{row}">{{ row.ctime | timestamp2Date }}</template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="审核人" prop="vname" show-overflow-tooltip/> <el-table-column align="center" label="审核人" prop="vname" show-overflow-tooltip/>
<el-table-column align="center" label="审核时间" show-overflow-tooltip> <el-table-column align="center" label="审核时间" show-overflow-tooltip>
<template v-slot="{row}">{{row.vtime | timestamp2Date}}</template> <template v-slot="{row}">{{ row.vtime | timestamp2Date }}</template>
</el-table-column> </el-table-column>
</abstract-table> </abstract-table>
</el-row> </el-row>
@ -66,77 +66,77 @@
</template> </template>
<script> <script>
import LinerProgress from '@/components/LinerProgress' import LinerProgress from '@/components/LinerProgress'
import dialogMixin from "@/mixins/dialogMixin" import dialogMixin from "@/mixins/dialogMixin"
import tableMixin from '@/mixins/tablePageMixin' import tableMixin from '@/mixins/tablePageMixin'
import {getSubById, search} from "@/api/doc/sell/order" import {getSubById, search} from "@/api/doc/sell/order"
import {elError} from "@/utils/message" import {elError} from "@/utils/message"
export default { export default {
name: "OrderSelector", name: "OrderSelector",
mixins: [dialogMixin, tableMixin], mixins: [dialogMixin, tableMixin],
components: {LinerProgress}, components: {LinerProgress},
props: {value: Boolean}, props: {value: Boolean},
data() { data() {
return { return {
searchForm: { searchForm: {
status: 2, status: 2,
finish: '0,1' finish: '0,1'
}
} }
}, }
},
methods: { methods: {
search() { search() {
if (!this.value || this.config.loading) return if (!this.value || this.config.loading) return
this.config.loading = true this.config.loading = true
this.row = null this.row = null
search(this.searchForm) search(this.searchForm)
.then(({list, total}) => { .then(({list, total}) => {
list.forEach(i => { list.forEach(i => {
i._loading = false // i._loading = false //
i._loaded = false // i._loaded = false //
i._selection = [] // i._selection = [] //
})
this.searchForm.total = total
this.tableData = list
}) })
.finally(() => this.config.loading = false) this.searchForm.total = total
}, this.tableData = list
})
.finally(() => this.config.loading = false)
},
getSubList(row) { getSubList(row) {
if (row._loaded || row._loading) return if (row._loaded || row._loading) return
row._loading = true row._loading = true
getSubById(row.id) getSubById(row.id)
.then(data => { .then(data => {
row.data = data row.data = data
row._loaded = true row._loaded = true
}) })
.finally(() => row._loading = false) .finally(() => row._loading = false)
}, },
confirm(row) { confirm(row) {
if (row._selection.length <= 0) return elError('请选择要出库的商品') if (row._selection.length <= 0) return elError('请选择要出库的商品')
this.$emit('select', row.id, row._selection) this.$emit('select', row.id, row._selection)
this.cancel() this.cancel()
}, },
cancel() { cancel() {
this.closeDialog() this.closeDialog()
this.tableData = [] this.tableData = []
}
} }
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.el-table { .el-table {
.nets-table-btn { .nets-table-btn {
margin: 0 auto; margin: 0 auto;
}
} }
}
</style> </style>

@ -1,13 +1,13 @@
<template> <template>
<el-dialog <el-dialog
v-drag-dialog v-drag-dialog
:visible="value" :visible="value"
append-to-body append-to-body
title="选择库存商品" title="选择库存商品"
width="70%" width="70%"
top="50px" top="50px"
@close="cancel" @close="cancel"
@open="search" @open="search"
> >
<el-scrollbar> <el-scrollbar>
<el-row> <el-row>
@ -20,11 +20,11 @@
<liner-progress :show="loading"/> <liner-progress :show="loading"/>
<el-table <el-table
ref="table" ref="table"
:data="tableData" :data="tableData"
current-row-key="id" current-row-key="id"
row-key="id" row-key="id"
@selection-change="selection=$event" @selection-change="selection=$event"
> >
<el-table-column align="center" type="selection" reserve-selection width="80"/> <el-table-column align="center" type="selection" reserve-selection width="80"/>
<el-table-column align="center" label="商品分类" prop="cname" show-overflow-tooltip/> <el-table-column align="center" label="商品分类" prop="cname" show-overflow-tooltip/>
@ -38,52 +38,52 @@
</template> </template>
<script> <script>
import dialogMixin from "@/mixins/dialogMixin" import dialogMixin from "@/mixins/dialogMixin"
import LinerProgress from '@/components/LinerProgress' import LinerProgress from '@/components/LinerProgress'
import {getDetail} from "@/api/stock/current" import {getDetail} from "@/api/stock/current"
import {elError} from "@/utils/message" import {elError} from "@/utils/message"
export default { export default {
name: "StockSelector", name: "StockSelector",
mixins: [dialogMixin], mixins: [dialogMixin],
components: {LinerProgress}, components: {LinerProgress},
props: { props: {
value: Boolean, value: Boolean,
cid: Number, cid: Number,
row: Object row: Object
}, },
data() { data() {
return { return {
loading: false, loading: false,
tableData: [], tableData: [],
selection: [] selection: []
} }
}, },
methods: { methods: {
search() { search() {
if (!this.value || this.loading) return if (!this.value || this.loading) return
this.loading = true this.loading = true
this.$refs.table && this.$refs.table.clearSelection() this.$refs.table && this.$refs.table.clearSelection()
getDetail(this.cid) getDetail(this.cid)
.then(data => this.tableData = data) .then(data => this.tableData = data)
.finally(() => this.loading = false) .finally(() => this.loading = false)
}, },
confirm() { confirm() {
if (this.selection.length <= 0) return elError('请选择商品') if (this.selection.length <= 0) return elError('请选择商品')
this.$emit('select', this.selection) this.$emit('select', this.selection)
}, },
cancel() { cancel() {
this.closeDialog() this.closeDialog()
this.$refs.table.clearSelection() this.$refs.table.clearSelection()
this.tableData = [] this.tableData = []
}
} }
} }
}
</script> </script>

@ -3,34 +3,34 @@
<liner-progress :show="loading"/> <liner-progress :show="loading"/>
<abstract-table <abstract-table
:data="tableData" :data="tableData"
:highlight-current-row="false" :highlight-current-row="false"
border border
show-summary show-summary
:summary-method="summary" :summary-method="summary"
:span-method="span" :span-method="span"
> >
<el-table-column align="center" label="采购订单" show-overflow-tooltip> <el-table-column align="center" label="采购订单" show-overflow-tooltip>
<router-link <router-link
slot-scope="{row}" slot-scope="{row}"
:to="{name:'purchaseOrderDetail',params:{type:'see',id:row.cgddid}}" :to="{name:'purchaseOrderDetail',params:{type:'see',id:row.cgddid}}"
@click.native="cancel" @click.native="cancel"
> >
{{row.cgddid}} {{ row.cgddid }}
</router-link> </router-link>
</el-table-column> </el-table-column>
<el-table-column align="center" label="采购单价" prop="price" show-overflow-tooltip/> <el-table-column align="center" label="采购单价" prop="price" show-overflow-tooltip/>
<el-table-column align="center" label="采购入库单" show-overflow-tooltip> <el-table-column align="center" label="采购入库单" show-overflow-tooltip>
<router-link <router-link
slot-scope="{row}" slot-scope="{row}"
:to="{name:'purchaseInboundDetail',params:{type:'see',id:row.cgrkid}}" :to="{name:'purchaseInboundDetail',params:{type:'see',id:row.cgrkid}}"
@click.native="cancel" @click.native="cancel"
> >
{{row.cgrkid}} {{ row.cgrkid }}
</router-link> </router-link>
</el-table-column> </el-table-column>
<el-table-column align="center" label="入库时间" show-overflow-tooltip> <el-table-column align="center" label="入库时间" show-overflow-tooltip>
<template v-slot="{row}">{{row.ctime | timestamp2Date}}</template> <template v-slot="{row}">{{ row.ctime | timestamp2Date }}</template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="库存数量" prop="num" show-overflow-tooltip/> <el-table-column align="center" label="库存数量" prop="num" show-overflow-tooltip/>
<el-table-column align="center" label="总 值" prop="total" show-overflow-tooltip/> <el-table-column align="center" label="总 值" prop="total" show-overflow-tooltip/>
@ -39,96 +39,96 @@
</template> </template>
<script> <script>
import AbstractTable from '@/components/AbstractTable' import AbstractTable from '@/components/AbstractTable'
import LinerProgress from '@/components/LinerProgress' import LinerProgress from '@/components/LinerProgress'
import FormDialog from "@/components/FormDialog" import FormDialog from "@/components/FormDialog"
import dialogMixin from "@/mixins/dialogMixin" import dialogMixin from "@/mixins/dialogMixin"
import {isEmpty} from "@/utils" import {isEmpty} from "@/utils"
import {plus, mul} from "@/utils/math" import {plus, mul} from "@/utils/math"
import {getDetail} from "@/api/stock/current" import {getDetail} from "@/api/stock/current"
export default { export default {
name: "DetailDialog", name: "DetailDialog",
mixins: [dialogMixin], mixins: [dialogMixin],
components: {AbstractTable, LinerProgress, FormDialog}, components: {AbstractTable, LinerProgress, FormDialog},
props: {value: Boolean, title: String, cid: Number}, props: {value: Boolean, title: String, cid: Number},
data() { data() {
return { return {
loading: false, loading: false,
tableData: [] tableData: []
}
},
methods: {
summary({data}) {
let sum = ['合计', '', '', '', 0, 0]
data.forEach(i => {
sum[4] = plus(sum[4], i.num)
sum[5] = plus(sum[5], i.total)
})
return sum
},
span({row, column, rowIndex, columnIndex}) {
if (columnIndex === 0 || columnIndex === 1) {
return row._span
} }
}, },
methods: { search() {
summary({data}) { this.tableData = []
let sum = ['合计', '', '', '', 0, 0] if (!this.value || isEmpty(this.cid) || this.loading) return
data.forEach(i => { this.loading = true
sum[4] = plus(sum[4], i.num) getDetail(this.cid)
sum[5] = plus(sum[5], i.total) .then(data => {
this.prepareData(data)
this.tableData = data
}) })
return sum .finally(() => this.loading = false)
}, },
span({row, column, rowIndex, columnIndex}) { prepareData(data) {
if (columnIndex === 0 || columnIndex === 1) { data.sort((a, b) => {
return row._span if (a.cgddid > b.cgddid) return -1
if (a.cgddid < b.cgddid) return 1
return 0
})
let count = {}
data.forEach(i => {
i.total = mul(i.num, i.price)
i._span = {rowspan: 0, colspan: 0}
count[i.cgddid] ? count[i.cgddid]++ : count[i.cgddid] = 1
})
for (let i = 0; i < data.length; i++) {
let num = count[data[i].cgddid]
if (num === 1) {
data[i]._span = {rowspan: 1, colspan: 1}
} }
}, if (num > 1) {
data[i]._span = {rowspan: num, colspan: 1}
search() { i += num - 1
this.tableData = []
if (!this.value || isEmpty(this.cid) || this.loading) return
this.loading = true
getDetail(this.cid)
.then(data => {
this.prepareData(data)
this.tableData = data
})
.finally(() => this.loading = false)
},
prepareData(data) {
data.sort((a, b) => {
if (a.cgddid > b.cgddid) return -1
if (a.cgddid < b.cgddid) return 1
return 0
})
let count = {}
data.forEach(i => {
i.total = mul(i.num, i.price)
i._span = {rowspan: 0, colspan: 0}
count[i.cgddid] ? count[i.cgddid]++ : count[i.cgddid] = 1
})
for (let i = 0; i < data.length; i++) {
let num = count[data[i].cgddid]
if (num === 1) {
data[i]._span = {rowspan: 1, colspan: 1}
}
if (num > 1) {
data[i]._span = {rowspan: num, colspan: 1}
i += num - 1
}
} }
},
cancel() {
this.closeDialog()
this.tableData = []
} }
},
cancel() {
this.closeDialog()
this.tableData = []
} }
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~@/assets/styles/variables.scss"; @import "~@/assets/styles/variables.scss";
.el-table { .el-table {
a { a {
color: $--color-primary; color: $--color-primary;
}
} }
}
</style> </style>

@ -3,13 +3,13 @@
<extra-area> <extra-area>
<template v-slot:extra> <template v-slot:extra>
<el-input <el-input
v-model="temp.filter" v-model="temp.filter"
size="small" size="small"
placeholder="筛选分类" placeholder="筛选分类"
clearable clearable
suffix-icon="el-icon-search" suffix-icon="el-icon-search"
style="margin-bottom: 10px" style="margin-bottom: 10px"
@input="filter" @input="filter"
/> />
<category-tree ref="tree" :filter-node-method="filterNode" @node-click="nodeClick"/> <category-tree ref="tree" :filter-node-method="filterNode" @node-click="nodeClick"/>
</template> </template>
@ -26,11 +26,11 @@
</search-form-item> </search-form-item>
<search-form-item label="入库时间:"> <search-form-item label="入库时间:">
<el-date-picker <el-date-picker
v-model="temp.ctime" v-model="temp.ctime"
format="yyyy-MM-dd" format="yyyy-MM-dd"
range-separator="-" range-separator="-"
type="daterange" type="daterange"
value-format="timestamp" value-format="timestamp"
/> />
</search-form-item> </search-form-item>
</search-form> </search-form>
@ -45,19 +45,19 @@
:summary-method="summary"> :summary-method="summary">
<el-table-column align="center" label="#" type="index" width="80"/> <el-table-column align="center" label="#" type="index" width="80"/>
<el-table-column align="center" label="商品分类"> <el-table-column align="center" label="商品分类">
<el-link slot-scope="{row}" @click="more(row)">{{row.cname}}</el-link> <el-link slot-scope="{row}" @click="more(row)">{{ row.cname }}</el-link>
</el-table-column> </el-table-column>
<el-table-column align="center" label="库存数量" prop="totalNum" show-overflow-tooltip/> <el-table-column align="center" label="库存数量" prop="totalNum" show-overflow-tooltip/>
<el-table-column align="center" label="总 值" prop="totalPrice" show-overflow-tooltip/> <el-table-column align="center" label="总 值" prop="totalPrice" show-overflow-tooltip/>
</abstract-table> </abstract-table>
<el-pagination <el-pagination
background background
:current-page="searchForm.page" :current-page="searchForm.page"
:page-size="searchForm.pageSize" :page-size="searchForm.pageSize"
:total="searchForm.total" :total="searchForm.total"
layout="total, prev, pager, next, jumper" layout="total, prev, pager, next, jumper"
@current-change="pageChange" @current-change="pageChange"
/> />
</el-row> </el-row>
</extra-area> </extra-area>
@ -67,130 +67,130 @@
</template> </template>
<script> <script>
import CategoryTree from '@/components/biz/CategoryTree' import CategoryTree from '@/components/biz/CategoryTree'
import DetailDialog from "./DetailDialog" import DetailDialog from "./DetailDialog"
import ExtraArea from '@/components/ExtraArea' import ExtraArea from '@/components/ExtraArea'
import SearchForm from "@/components/SearchForm" import SearchForm from "@/components/SearchForm"
import SearchFormItem from "@/components/SearchForm/SearchFormItem" import SearchFormItem from "@/components/SearchForm/SearchFormItem"
import {baseUrl, search} from "@/api/stock/current" import {baseUrl, search} from "@/api/stock/current"
import {isEmpty, debounce} from "@/utils" import {isEmpty, debounce} from "@/utils"
import {plus} from "@/utils/math" import {plus} from "@/utils/math"
import {exportExcel} from "@/utils/excel" import {exportExcel} from "@/utils/excel"
import {getNodeId} from "@/utils/tree" import {getNodeId} from "@/utils/tree"
import tableMixin from '@/mixins/tablePageMixin' import tableMixin from '@/mixins/tablePageMixin'
export default { export default {
name: "currentStock", name: "currentStock",
mixins: [tableMixin], mixins: [tableMixin],
components: {CategoryTree, DetailDialog, ExtraArea, SearchForm, SearchFormItem}, components: {CategoryTree, DetailDialog, ExtraArea, SearchForm, SearchFormItem},
data() { data() {
return { return {
searchForm: { searchForm: {
cids: null, cids: null,
cgddids: null, cgddids: null,
cgrkids: null cgrkids: null
},
temp: {
filter: '',
cname: null,
cids: null,
ctime: []
},
detailDialog: false,
excel: {
columns: [
{header: '序号', prop: 'id', width: 20, merge: true},
{header: '分类名称', prop: 'cname', width: 20, merge: true},
{header: '库存总数', prop: 'totalNum', width: 20, merge: true},
{header: '库存总值', prop: 'totalPrice', width: 20, merge: true},
{header: '采购订单号', prop: 'cgddid', width: 20, merge: true},
{header: '采购单价', prop: 'cgPrice', width: 20, merge: true},
{header: '采购数量', prop: 'cgNum', width: 20, merge: true},
{header: '采购入库单号', prop: 'cgrkid', width: 20},
{header: '入库时间', prop: 'ctime', width: 20},
{header: '入库数量', prop: 'rkNum', width: 20},
],
merge: {primaryKey: 'cid', orderKey: 'id'}
}
}
},
computed: {
title() {
return this.row ? `库存详细(${this.row.cname})` : ''
}, },
temp: {
cid() { filter: '',
return this.row ? this.row.cid : null cname: null,
cids: null,
ctime: []
},
detailDialog: false,
excel: {
columns: [
{header: '序号', prop: 'id', width: 20, merge: true},
{header: '分类名称', prop: 'cname', width: 20, merge: true},
{header: '库存总数', prop: 'totalNum', width: 20, merge: true},
{header: '库存总值', prop: 'totalPrice', width: 20, merge: true},
{header: '采购订单号', prop: 'cgddid', width: 20, merge: true},
{header: '采购单价', prop: 'cgPrice', width: 20, merge: true},
{header: '采购数量', prop: 'cgNum', width: 20, merge: true},
{header: '采购入库单号', prop: 'cgrkid', width: 20},
{header: '入库时间', prop: 'ctime', width: 20},
{header: '入库数量', prop: 'rkNum', width: 20},
],
merge: {primaryKey: 'cid', orderKey: 'id'}
} }
}, }
},
methods: { computed: {
summary({data}) { title() {
let sum = ['合计', '', '', 0] return this.row ? `库存详细(${this.row.cname})` : ''
data.forEach(i => { },
sum[3] = plus(sum[3], i.totalPrice)
})
return sum
},
filterNode(value, data) { cid() {
if (isEmpty(value)) return true return this.row ? this.row.cid : null
return data.name.includes(value) }
}, },
methods: {
summary({data}) {
let sum = ['合计', '', '', 0]
data.forEach(i => {
sum[3] = plus(sum[3], i.totalPrice)
})
return sum
},
filter(v) { filterNode(value, data) {
this.$refs.tree.filter(v) if (isEmpty(value)) return true
}, return data.name.includes(value)
},
mergeSearchForm() { filter(v) {
return { this.$refs.tree.filter(v)
...this.searchForm, },
startTime: this.temp.ctime ? this.temp.ctime[0] : null,
endTime: this.temp.ctime ? this.temp.ctime[1] + 86400000 : null
}
},
search() { mergeSearchForm() {
if (this.config.loading) return return {
this.config.loading = true ...this.searchForm,
search(this.mergeSearchForm()) startTime: this.temp.ctime ? this.temp.ctime[0] : null,
.then(({list, total}) => { endTime: this.temp.ctime ? this.temp.ctime[1] + 86400000 : null
this.searchForm.total = total }
this.tableData = list },
})
.finally(() => this.config.loading = false)
},
downloadExcel() { search() {
exportExcel(`${baseUrl}/export`, this.mergeSearchForm(), this.excel) if (this.config.loading) return
}, this.config.loading = true
search(this.mergeSearchForm())
.then(({list, total}) => {
this.searchForm.total = total
this.tableData = list
})
.finally(() => this.config.loading = false)
},
nodeClick(obj) { downloadExcel() {
const ids = getNodeId(obj.children) exportExcel(`${baseUrl}/export`, this.mergeSearchForm(), this.excel)
ids.unshift(obj.id) },
this.searchForm.cids = ids.join(',')
this.temp.cname = obj.name
},
clearCidSearch() { nodeClick(obj) {
this.searchForm.cids = null const ids = getNodeId(obj.children)
this.temp.cname = null ids.unshift(obj.id)
let tree = this.$refs.tree.$refs.tree this.searchForm.cids = ids.join(',')
tree.setCurrentKey() this.temp.cname = obj.name
}, },
more(row) { clearCidSearch() {
this.row = row this.searchForm.cids = null
this.detailDialog = true this.temp.cname = null
} let tree = this.$refs.tree.$refs.tree
tree.setCurrentKey()
}, },
created() { more(row) {
this.filter = debounce(this.filter, 500) this.row = row
this.detailDialog = true
} }
},
created() {
this.filter = debounce(this.filter, 500)
} }
}
</script> </script>

@ -1,13 +1,13 @@
<template> <template>
<el-card v-show="show" v-loading="loading" :header="title" class="card-container" style="min-width: 520px;"> <el-card v-show="show" v-loading="loading" :header="title" class="card-container" style="min-width: 520px;">
<el-form <el-form
ref="form" ref="form"
:model="form" :model="form"
:rules="rules" :rules="rules"
class="edit-form" class="edit-form"
label-position="right" label-position="right"
label-width="100px" label-width="100px"
status-icon status-icon
> >
<el-form-item label="上级分类:"> <el-form-item label="上级分类:">
<el-input :value="parentObj?parentObj.name:''" readonly/> <el-input :value="parentObj?parentObj.name:''" readonly/>
@ -24,10 +24,10 @@
<el-form-item> <el-form-item>
<el-button plain size="small" @click="cancel"> </el-button> <el-button plain size="small" @click="cancel"> </el-button>
<el-button <el-button
v-show="['add','edit'].includes(type)" v-show="['add','edit'].includes(type)"
size="small" size="small"
type="primary" type="primary"
@click="confirm" @click="confirm"
> >
</el-button> </el-button>
@ -37,122 +37,122 @@
</template> </template>
<script> <script>
import {isEmpty, mergeObj, waitUntilSuccess} from '@/utils' import {isEmpty, mergeObj, waitUntilSuccess} from '@/utils'
import {elConfirm} from "@/utils/message" import {elConfirm} from "@/utils/message"
import {add, update} from "@/api/system/category" import {add, update} from "@/api/system/category"
export default { export default {
props: ['list'], props: ['list'],
data() { data() {
let typeValidate = (r, v, c) => { let typeValidate = (r, v, c) => {
if (v === 1 && this.hasChildren) c('该分类下已有子节点') if (v === 1 && this.hasChildren) c('该分类下已有子节点')
c() c()
} }
return { return {
loading: false, loading: false,
title: '', title: '',
type: 'none', type: 'none',
form: { form: {
id: null, id: null,
pid: 0, pid: 0,
name: '', name: '',
type: 0 type: 0
},
parentObj: null,
hasChildren: false,
rules: {
name: [{required: true, message: '请输入分类名称', trigger: 'change'}],
type: [
{required: true, message: '请选择分类类型', trigger: 'change'},
{validator: typeValidate, trigger: 'change'}
]
}
}
},
computed: {
show() {
return this.list() && (this.type === 'add' || ['see', 'edit'].includes(this.type) && this.list().currentCategory)
}, },
parentObj: null,
confirmMessage() { hasChildren: false,
switch (this.type) { rules: {
case 'add': name: [{required: true, message: '请输入分类名称', trigger: 'change'}],
return `确认添加新的分类【${this.form.name}】?` type: [
case 'edit': {required: true, message: '请选择分类类型', trigger: 'change'},
return `确认提交对【${this.form.name}】作出的修改?` {validator: typeValidate, trigger: 'change'}
default: ]
return ''
}
} }
}
},
computed: {
show() {
return this.list() && (this.type === 'add' || ['see', 'edit'].includes(this.type) && this.list().currentCategory)
}, },
methods: { confirmMessage() {
add(parent) { switch (this.type) {
this.title = '添加新分类' case 'add':
this.parentObj = parent return `确认添加新的分类【${this.form.name}】?`
this.type = 'add' case 'edit':
this.form.pid = parent ? parent.id : 0 return `确认提交对【${this.form.name}】作出的修改?`
this.form.name = '' default:
this.form.type = 0 return ''
this.hasChildren = false }
}, }
},
see(current, parent) { methods: {
this.title = `分类信息(${current.name})` add(parent) {
this.parentObj = parent this.title = '添加新分类'
this.type = 'see' this.parentObj = parent
mergeObj(this.form, current) this.type = 'add'
}, this.form.pid = parent ? parent.id : 0
this.form.name = ''
this.form.type = 0
this.hasChildren = false
},
edit(current, parent) { see(current, parent) {
this.title = `编辑分类(${current.name})` this.title = `分类信息(${current.name})`
this.parentObj = parent this.parentObj = parent
this.type = 'edit' this.type = 'see'
mergeObj(this.form, current) mergeObj(this.form, current)
this.hasChildren = current.children.length > 0 },
},
confirm() { edit(current, parent) {
this.$refs.form.validate(v => { this.title = `编辑分类(${current.name})`
if (!v) return this.parentObj = parent
elConfirm(this.confirmMessage) this.type = 'edit'
.then(() => this.type === 'add' ? add(this.form) : update(this.form)) mergeObj(this.form, current)
.then(() => { this.hasChildren = current.children.length > 0
this.$emit('commit-success', this.type === 'add' ? '添加成功' : '修改成功') },
this.cancel()
})
})
},
cancel() { confirm() {
this.type = 'none' this.$refs.form.validate(v => {
this.clearForm() if (!v) return
this.$nextTick(() => this.$refs.form.clearValidate()) elConfirm(this.confirmMessage)
}, .then(() => this.type === 'add' ? add(this.form) : update(this.form))
.then(() => {
this.$emit('commit-success', this.type === 'add' ? '添加成功' : '修改成功')
this.cancel()
})
})
},
clearForm() { cancel() {
this.form.id = null this.type = 'none'
this.form.pid = 0 this.clearForm()
this.form.name = '' this.$nextTick(() => this.$refs.form.clearValidate())
this.form.type = 0
this.parentObj = null
this.hasChildren = false
}
}, },
mounted() { clearForm() {
waitUntilSuccess( this.form.id = null
() => !isEmpty(this.list()), this.form.pid = 0
() => ['add', 'see', 'edit'].forEach(event => this.list().$on(event, this[event])) this.form.name = ''
) this.form.type = 0
this.parentObj = null
this.hasChildren = false
} }
},
mounted() {
waitUntilSuccess(
() => !isEmpty(this.list()),
() => ['add', 'see', 'edit'].forEach(event => this.list().$on(event, this[event]))
)
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.edit-form { .edit-form {
width: 85%; width: 85%;
} }
</style> </style>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save