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>
<li
class="el-menu-item"
:style="paddingStyle"
:class="{'is-active': active,'is-disabled': disabled}"
@click="handleClick"
class="el-menu-item"
:style="paddingStyle"
:class="{'is-active': active,'is-disabled': disabled}"
@click="handleClick"
>
<el-tooltip
v-if="parentMenu.$options.componentName === 'ElMenu' && rootMenu.collapse && $slots.title"
effect="dark"
placement="right"
v-if="parentMenu.$options.componentName === 'ElMenu' && rootMenu.collapse && $slots.title"
effect="dark"
placement="right"
>
<div slot="content">
<slot name="title"/>
</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/>
</div>
</el-tooltip>
@ -26,49 +27,49 @@
</template>
<script>
import MenuMixin from 'element-ui/packages/menu/src/menu-mixin'
import Emitter from 'element-ui/lib/mixins/emitter'
import MenuMixin from 'element-ui/packages/menu/src/menu-mixin'
import Emitter from 'element-ui/lib/mixins/emitter'
export default {
name: 'ElMenuItem',
export default {
name: 'ElMenuItem',
componentName: 'ElMenuItem',
componentName: 'ElMenuItem',
mixins: [MenuMixin, Emitter],
mixins: [MenuMixin, Emitter],
data() {
return {}
},
data() {
return {}
},
props: {
index: String,
route: [String, Object],
disabled: Boolean
},
props: {
index: String,
route: [String, Object],
disabled: Boolean
},
computed: {
active() {
return this.index === this.rootMenu.activeIndex
}
},
computed: {
active() {
return this.index === this.rootMenu.activeIndex
}
},
methods: {
handleClick() {
if (!this.disabled) {
this.dispatch('ElMenu', 'item-click', this)
this.$emit('click', this)
}
methods: {
handleClick() {
if (!this.disabled) {
this.dispatch('ElMenu', 'item-click', this)
this.$emit('click', this)
}
},
}
},
mounted() {
this.parentMenu.addItem(this)
this.rootMenu.addItem(this)
},
mounted() {
this.parentMenu.addItem(this)
this.rootMenu.addItem(this)
},
beforeDestroy() {
this.parentMenu.removeItem(this)
this.rootMenu.removeItem(this)
}
beforeDestroy() {
this.parentMenu.removeItem(this)
this.rootMenu.removeItem(this)
}
}
</script>

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

@ -14,94 +14,94 @@
<p v-else v-html="message" class="el-message__content"/>
</slot>
<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>
</transition>
</template>
<script>
export default {
data() {
export default {
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 {
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
'top': `${this.verticalOffset}px`
}
},
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 {
'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`
}
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: {
closed(newVal) {
if (newVal) this.visible = false
}
},
methods: {
handleAfterLeave() {
this.$destroy(true)
this.$el.parentNode.removeChild(this.$el)
},
watch: {
closed(newVal) {
if (newVal) this.visible = false
}
close() {
this.closed = true
typeof this.onClose === 'function' && this.onClose(this)
},
methods: {
handleAfterLeave() {
this.$destroy(true)
this.$el.parentNode.removeChild(this.$el)
},
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)
}
}
clearTimer(reset) {
this.pause = true
this.remainTime = reset === true ? this.duration : this.remainTime - (Date.now() - this.startAt)
window.clearTimeout(this.timer)
},
mounted() {
this.startTimer()
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>

File diff suppressed because it is too large Load Diff

@ -1,48 +1,48 @@
<template>
<div
class="el-select-dropdown el-popper"
:class="[{ 'is-multiple': $parent.multiple }, popperClass]"
:style="{ minWidth }"
class="el-select-dropdown el-popper"
:class="[{ 'is-multiple': $parent.multiple }, popperClass]"
:style="{ minWidth }"
>
<slot/>
</div>
</template>
<script type="text/babel">
import Popper from '@ele/mixins/vue-popper'
export default {
name: 'ElSelectDropdown',
componentName: 'ElSelectDropdown',
mixins: [Popper],
data() {
return {
minWidth: ''
}
},
computed: {
popperClass() {
return this.$parent.popperClass
}
},
watch: {
'$parent.inputWidth'() {
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)
import Popper from '@ele/mixins/vue-popper'
export default {
name: 'ElSelectDropdown',
componentName: 'ElSelectDropdown',
mixins: [Popper],
data() {
return {
minWidth: ''
}
},
computed: {
popperClass() {
return this.$parent.popperClass
}
},
watch: {
'$parent.inputWidth'() {
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)
}
}
</script>

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

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

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

@ -1,36 +1,36 @@
<script type="text/jsx">
import Empty from "@/components/Empty"
import Empty from "@/components/Empty"
export default {
name: "AbstractTable",
export default {
name: "AbstractTable",
functional: true,
functional: true,
render(h, context) {
const {data, children, scopedSlots} = context
render(h, context) {
const {data, children, scopedSlots} = context
if (!scopedSlots.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>
)
if (!scopedSlots.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>
)
}
}
</script>

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

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

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

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

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

@ -5,87 +5,87 @@
</template>
<script>
export default {
name: "ContextMenu",
export default {
name: "ContextMenu",
props: {
value: Boolean,
left: Number,
top: Number
},
props: {
value: Boolean,
left: Number,
top: Number
},
data() {
return {
realLeft: '0px',
realTop: '0px'
}
data() {
return {
realLeft: '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: {
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)
methods: {
closeContextMenu() {
this.$emit('input', false)
},
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'
},
methods: {
closeContextMenu() {
this.$emit('input', false)
},
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'
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'
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/variables.scss";
@import "~@/assets/styles/variables.scss";
.contextmenu {
margin: 0;
background: $--menu-background-color;
z-index: 10;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #fff;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
.contextmenu {
margin: 0;
background: $--menu-background-color;
z-index: 10;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #fff;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
&-item {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&-item {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
color: $--color-primary;
}
&:hover {
color: $--color-primary;
}
}
}
</style>

@ -1,51 +1,51 @@
<script type="text/jsx">
export default {
name: 'DefaultEmptyImage',
export default {
name: 'DefaultEmptyImage',
functional: true,
functional: true,
render() {
return (
<svg width="184" height="152" viewBox="0 0 184 152" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<g transform="translate(24 31.67)">
<ellipse
fillOpacity=".8"
fill="#F5F5F7"
cx="67.797"
cy="106.89"
rx="67.797"
ry="12.668"
/>
<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"
fill="#AEB8C2"
/>
<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"
fill="url(#linearGradient-1)"
transform="translate(13.56)"
/>
<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"
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>
render() {
return (
<svg width="184" height="152" viewBox="0 0 184 152" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<g transform="translate(24 31.67)">
<ellipse
fillOpacity=".8"
fill="#F5F5F7"
cx="67.797"
cy="106.89"
rx="67.797"
ry="12.668"
/>
<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"
fill="#AEB8C2"
/>
<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"
fill="url(#linearGradient-1)"
transform="translate(13.56)"
/>
<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"
fill="#F5F5F7"
/>
<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"
/>
<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>
)
}
<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>

@ -1,38 +1,38 @@
<script type="text/jsx">
import DefaultEmptyImage from "./DefaultEmptyImage"
import DefaultEmptyImage from "./DefaultEmptyImage"
export default {
name: "AEmpty",
export default {
name: "AEmpty",
functional: true,
functional: true,
props: {
description: String,
image: [String, Object],
imageStyle: [String, Object]
},
props: {
description: String,
image: [String, Object],
imageStyle: [String, Object]
},
render(h, context) {
const {description = '暂无数据', image, imageStyle} = context.props
const prefixCls = 'ant-empty'
const cls = {[prefixCls]: true}
render(h, context) {
const {description = '暂无数据', image, imageStyle} = context.props
const prefixCls = 'ant-empty'
const cls = {[prefixCls]: true}
let imageNode = null
if (typeof image === 'string') {
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>
)
let imageNode = null
if (typeof image === 'string') {
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>
)
}
}
</script>
<style lang="scss" src="./style.scss"></style>

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

@ -1,54 +1,54 @@
<script type="text/jsx">
import LoadingMask from '@/components/LoadingMask'
export default {
name: "FormDialog",
functional: true,
props: {
value: Boolean,
title: String,
loading: {type: Boolean, default: true},
top: {type: String, default: '50px'},
width: {type: String, default: '30%'}
},
render(h, context) {
const {value, title, loading, top, width} = context.props
const l = context.listeners
function onClose() {
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>
)
import LoadingMask from '@/components/LoadingMask'
export default {
name: "FormDialog",
functional: true,
props: {
value: Boolean,
title: String,
loading: {type: Boolean, default: true},
top: {type: String, default: '50px'},
width: {type: String, default: '30%'}
},
render(h, context) {
const {value, title, loading, top, width} = context.props
const l = context.listeners
function onClose() {
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>
)
}
}
</script>
<style lang="scss" src="./style.scss"></style>

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

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

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

@ -1,84 +1,84 @@
<script type="text/jsx">
import {store, init} from '../store'
import FormDialog from "@/components/FormDialog"
import {createLimitTree, getNodeId} from "@/utils/tree"
import {store, init} from '../store'
import FormDialog from "@/components/FormDialog"
import {createLimitTree, getNodeId} from "@/utils/tree"
export default {
components: {FormDialog},
export default {
components: {FormDialog},
data() {
return {
loading: false,
visible: false,
handler: null,
limitTree: [],
getChildrenOnSelect: false,
limit: false,
limitApi: null
}
},
computed: {
regionTree() {
return store.data
}
},
data() {
return {
loading: false,
visible: false,
handler: null,
limitTree: [],
getChildrenOnSelect: false,
limit: false,
limitApi: null
}
},
methods: {
closeDialog() {
this.visible = false
},
computed: {
regionTree() {
return store.data
}
},
nodeClick(obj) {
const payload = [obj]
methods: {
closeDialog() {
this.visible = false
},
if (this.getChildrenOnSelect) {
const ids = getNodeId(obj.children)
ids.unshift(obj.id)
payload.push(ids)
}
nodeClick(obj) {
const payload = [obj]
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.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)
}
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}
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 => data && (this.limitTree = createLimitTree(this.regionTree, data)))
.finally(() => this.loading = false)
}
},
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={this.limit ? this.limitTree : this.regionTree}
expand-on-click-node={false}
node-key="id"
v-on:node-click={this.nodeClick}
>
{({data}) => (
<span className="el-tree-node__label">
{({data}) => (
<span className="el-tree-node__label">
{this.limit ? data.name + `(${data.value})` : data.name}
</span>
)}
</el-tree>
</form-dialog>
)
}
)}
</el-tree>
</form-dialog>
)
}
}
</script>

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

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

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

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

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

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

@ -1,50 +1,50 @@
<script type="text/jsx">
import {isExternal} from '@/utils/validate'
import {isExternal} from '@/utils/validate'
export default {
name: 'SvgIcon',
export default {
name: 'SvgIcon',
functional: true,
functional: true,
props: {
icon: String,
className: String
},
props: {
icon: String,
className: String
},
render(h, context) {
const {listeners} = context
const {icon, className} = context.props
const c = 'svg-icon ' + (className || '')
if (isExternal(icon)) {
const style = {
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>
)
render(h, context) {
const {listeners} = context
const {icon, className} = context.props
const c = 'svg-icon ' + (className || '')
if (isExternal(icon)) {
const style = {
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>
)
}
}
}
</script>
<style>
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
overflow: hidden;
}
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover !important;
display: inline-block;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover !important;
display: inline-block;
}
</style>

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

@ -1,47 +1,47 @@
<script type="text/jsx">
export default {
name: "ZoomControl",
export default {
name: "ZoomControl",
functional: true,
functional: true,
props: {
value: {type: Number, default: 100},
step: {type: Number, default: 20},
min: {type: Number, default: 10},
max: {type: Number, default: 200}
},
props: {
value: {type: Number, default: 100},
step: {type: Number, default: 20},
min: {type: Number, default: 10},
max: {type: Number, default: 200}
},
render(h, context) {
const {value, step, max, min} = context.props
const input = context.listeners.input
render(h, context) {
const {value, step, max, min} = context.props
const input = context.listeners.input
function zoomIn() {
const zoom = value + step
if (zoom <= max) input(zoom)
}
function zoomOut() {
const zoom = value - step
if (zoom >= min) input(zoom)
}
function zoomIn() {
const zoom = value + step
if (zoom <= max) input(zoom)
}
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>
)
function zoomOut() {
const zoom = value - step
if (zoom >= min) input(zoom)
}
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>
<style>
.zoom-wrapper .zoom-number {
color: #657180;
padding: 0 10px;
display: inline-block;
width: 60px;
text-align: center;
}
.zoom-wrapper .zoom-number {
color: #657180;
padding: 0 10px;
display: inline-block;
width: 60px;
text-align: center;
}
</style>

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

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

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

@ -5,15 +5,15 @@
<el-row>
<el-row class="detail-page-header-left">
<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-content">{{i.content}}</span>
<span class="detail-page-header-description-label">{{ i.label }}</span>
<span class="detail-page-header-description-content">{{ i.content }}</span>
</el-col>
</el-row>
<div class="detail-page-header-extra">
<div>
<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-content">{{i.content}}</div>
<div class="detail-page-header-extra-title">{{ i.title }}</div>
<div class="detail-page-header-extra-content">{{ i.content }}</div>
</div>
</div>
</div>
@ -22,19 +22,19 @@
</template>
<script>
export default {
name: "DocDetailHeader",
export default {
name: "DocDetailHeader",
props: {
title: String,
description: {
type: Array,
default: () => []
},
extra: {
type: Array,
default: () => []
}
props: {
title: String,
description: {
type: Array,
default: () => []
},
extra: {
type: Array,
default: () => []
}
}
}
</script>

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

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

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

@ -1,13 +1,13 @@
<template>
<div v-show="visible" class="signature-board">
<canvas
ref="canvas"
@mousedown.prevent="drawStart"
@mousemove.prevent="drawMove"
@mouseup.prevent="drawEnd"
@touchstart.prevent="drawStart"
@touchmove.prevent="drawMove"
@touchend.prevent="drawEnd"
ref="canvas"
@mousedown.prevent="drawStart"
@mousemove.prevent="drawMove"
@mouseup.prevent="drawEnd"
@touchstart.prevent="drawStart"
@touchmove.prevent="drawMove"
@touchend.prevent="drawEnd"
/>
<div class="signature-board__footer">
@ -19,143 +19,143 @@
</template>
<script>
/*
* 突然发现没办法做到竖屏横屏都支持坑爹
* */
function getEventPoint(e) {
let x, y
if (['mousedown', 'mousemove'].includes(e.type)) {
x = e.clientX - e.target.offsetLeft
y = e.clientY - e.target.offsetTop
}
else {
x = e.changedTouches[0].clientX - e.target.offsetLeft
y = e.changedTouches[0].clientY - e.target.offsetTop
}
return {x, y}
/*
* 突然发现没办法做到竖屏横屏都支持坑爹
* */
function getEventPoint(e) {
let x, y
if (['mousedown', 'mousemove'].includes(e.type)) {
x = e.clientX - e.target.offsetLeft
y = e.clientY - e.target.offsetTop
}
else {
x = e.changedTouches[0].clientX - e.target.offsetLeft
y = e.changedTouches[0].clientY - e.target.offsetTop
}
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 {
name: 'SignatureBoard',
drawMove(e) {
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: {
image: String,
lineWidth: Number,
lineColor: String,
onConfirm: Function
drawEnd() {
this.drawing = false
},
data() {
return {
visible: false,
drew: false,
drawing: false,
startX: 0,
startY: 0
}
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
},
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
},
drawMove(e) {
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
},
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
}
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() {
window.addEventListener('resize', this.resize)
clear() {
this.drew = false
this.drawing = false
this.$refs.canvas && (this.$refs.canvas.height = window.innerHeight)
},
this.$once('hook:beforeDestroy', () => {
this.clear()
window.removeEventListener('resize', this.resize)
})
close() {
this.visible = false
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>
<style lang="scss">
.signature-board {
position: fixed;
background-color: white;
top: 0;
right: 0;
height: 100vh;
width: 100vw;
z-index: 100;
overflow: hidden;
&__footer {
position: absolute;
bottom: 20px;
right: 20px;
}
.signature-board {
position: fixed;
background-color: white;
top: 0;
right: 0;
height: 100vh;
width: 100vw;
z-index: 100;
overflow: hidden;
&__footer {
position: absolute;
bottom: 20px;
right: 20px;
}
}
</style>

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

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

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

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

@ -9,39 +9,39 @@
</template>
<script>
export default {
data: () => ({data: []}),
export default {
data: () => ({data: []}),
watch: {
$route(route) {
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() {
watch: {
$route(route) {
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()
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/variables.scss";
@import "~@/assets/styles/variables.scss";
.app-breadcrumb {
margin-left: 8px;
line-height: $nav-height;
.app-breadcrumb {
margin-left: 8px;
line-height: $nav-height;
.no-redirect {
color: $--color-gray;
cursor: text;
}
.no-redirect {
color: $--color-gray;
cursor: text;
}
}
</style>

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

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

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

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

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

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

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

@ -1,149 +1,149 @@
<script type="text/jsx">
import {mapState} from 'vuex'
import Logo from './components/Logo'
import SidebarItem from './components/SidebarItem'
import {isExternal} from "@/utils/validate"
import {mapState} from 'vuex'
import Logo from './components/Logo'
import SidebarItem from './components/SidebarItem'
import {isExternal} from "@/utils/validate"
export default {
name: 'sidebar',
export default {
name: 'sidebar',
components: {SidebarItem, Logo},
components: {SidebarItem, Logo},
data() {
return {
mouseOutside: true
data() {
return {
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: {
...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
},
//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
}
}
//pc
collapse() {
return this.sidebarCollapse && this.device === 'pc'
},
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)
},
hideSidebar(v) {
if (v) document.addEventListener('mousemove', this.moveEvent)
else document.removeEventListener('mousemove', this.moveEvent)
}
hideSidebar() {
return this.sidebarAutoHidden && this.mouseOutside
|| this.sidebarCollapse && this.device === 'mobile'
},
methods: {
moveEvent(e) {
if (e.clientX <= 15) this.mouseOutside = false
},
select(index, indexPath, item, jump = true) {
//
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)
sidebarClass() {
return {
'sidebar-container': true,
'mobile': this.device === 'mobile',
'collapse-sidebar': this.collapse,
'hide-sidebar': this.hideSidebar
}
}
},
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() {
if (this.sidebarAutoHidden) {
document.addEventListener('mousemove', this.moveEvent)
}
hideSidebar(v) {
if (v) document.addEventListener('mousemove', this.moveEvent)
else document.removeEventListener('mousemove', this.moveEvent)
}
},
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('mousemove', this.moveEvent)
})
methods: {
moveEvent(e) {
if (e.clientX <= 15) this.mouseOutside = false
},
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>
)
select(index, indexPath, item, jump = true) {
//
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)
}
},
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>
<style lang="scss" src="./style.scss"></style>

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

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

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

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

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

@ -21,21 +21,21 @@
<svg-icon icon="password"/>
</span>
<el-input
v-model="form.repwd"
placeholder="请确认密码"
type="password"
:maxlength="20"
@keyup.enter.native="register"
v-model="form.repwd"
placeholder="请确认密码"
type="password"
:maxlength="20"
@keyup.enter.native="register"
/>
</el-form-item>
<el-button
:loading="loading"
class="submit-btn"
type="primary"
@click="register"
:loading="loading"
class="submit-btn"
type="primary"
@click="register"
>
{{loading ? '注 册 中...' : '注 册'}}
{{ loading ? '注 册 中...' : '注 册' }}
</el-button>
<div class="flex" style="margin-top: 20px">
@ -46,84 +46,84 @@
</template>
<script>
import md5 from "js-md5"
import {register, checkName} from "@/api/account"
import {elSuccess} from "@/utils/message"
import md5 from "js-md5"
import {register, checkName} from "@/api/account"
import {elSuccess} from "@/utils/message"
export default {
name: "RegisterForm",
export default {
name: "RegisterForm",
data() {
const validateName = (r, v, c) => {
checkName(this.form.username)
.then(({msg}) => msg ? c(msg) : c())
.catch(e => c(e))
}
const validateRepwd = (r, v, c) => {
return v !== this.form.pwd ? c('两次密码输入不一致') : c()
}
return {
form: {
username: '',
pwd: '',
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)
})
data() {
const validateName = (r, v, c) => {
checkName(this.form.username)
.then(({msg}) => msg ? c(msg) : c())
.catch(e => c(e))
}
const validateRepwd = (r, v, c) => {
return v !== this.form.pwd ? c('两次密码输入不一致') : c()
}
return {
form: {
username: '',
pwd: '',
repwd: ''
},
login() {
!this.loading && this.$router.push('/login')
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
}
},
capsLockTip({keyCode}) {
if (keyCode === 20) this.capsTooltip = !this.capsTooltip
},
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)
})
},
addEvent() {
document.addEventListener('keyup', this.capsLockTip)
},
login() {
!this.loading && this.$router.push('/login')
},
removeEvent() {
document.removeEventListener('keyup', this.addCapsLockEvent)
}
capsLockTip({keyCode}) {
if (keyCode === 20) this.capsTooltip = !this.capsTooltip
},
mounted() {
this.addEvent()
this.$nextTick(() => this.$refs.username.focus())
addEvent() {
document.addEventListener('keyup', this.capsLockTip)
},
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>

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

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

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

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

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

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

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

@ -9,32 +9,32 @@
</template>
<script>
import TreeSelect from '@/components/TreeSelect'
import TreeSelect from '@/components/TreeSelect'
export default {
name: "treeSelectExample",
export default {
name: "treeSelectExample",
components: {TreeSelect},
components: {TreeSelect},
data() {
return {
single: '',
multiple: [],
data: [
{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'}]},
],
}
},
data() {
return {
single: '',
multiple: [],
data: [
{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'}]},
],
}
},
methods: {
filter(v, data) {
return data.label.includes(v)
},
methods: {
filter(v, data) {
return data.label.includes(v)
},
singleInput(v,data) {
console.log(data)
}
singleInput(v, data) {
console.log(data)
}
}
}
</script>

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

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

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

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

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

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

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

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

@ -53,62 +53,62 @@
</template>
<script>
import AbstractTable from '@/components/AbstractTable'
import LinerProgress from '@/components/LinerProgress'
import FormDialog from "@/components/FormDialog"
import AbstractTable from '@/components/AbstractTable'
import LinerProgress from '@/components/LinerProgress'
import FormDialog from "@/components/FormDialog"
export default {
name: "stylePage",
export default {
name: "stylePage",
components: {AbstractTable, LinerProgress, FormDialog},
components: {AbstractTable, LinerProgress, FormDialog},
data() {
return {
dialog: false,
tableData: [
{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
})
data() {
return {
dialog: false,
tableData: [
{name: '老王', address: '中国', tel: '13232423232', age: 18},
{name: '老王', address: '中国', tel: '13232423232', age: 18},
{name: '老王', address: '中国', tel: '13232423232', age: 18}
]
}
},
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>
<style lang="scss" scoped>
.el-message {
position: static;
transform: translateX(0);
.el-message {
position: static;
transform: translateX(0);
& + .el-message {
margin-left: 20px;
}
& + .el-message {
margin-left: 20px;
}
}
.el-row + .el-row {
margin-top: 20px;
}
.el-row + .el-row {
margin-top: 20px;
}
.el-tag + .el-tag {
margin-left: 20px;
}
.el-tag + .el-tag {
margin-left: 20px;
}
</style>

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

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

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

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

@ -9,64 +9,64 @@
</template>
<script>
import PieChart from "@/components/Charts/PieChart"
import {getTotalProfitGoods} from '@/api/statistic'
import PieChart from "@/components/Charts/PieChart"
import {getTotalProfitGoods} from '@/api/statistic'
export default {
name: "TotalProfitGoods",
export default {
name: "TotalProfitGoods",
components: {PieChart},
components: {PieChart},
data() {
return {
loading: false,
purchase: {
title: '各个商品总采购额',
data: []
},
sell: {
title: '各个商品总销售额',
data: []
},
profit: {
title: '各个商品总毛利润',
data: []
}
}
},
methods: {
getTitle(type) {
return this[type].title
data() {
return {
loading: false,
purchase: {
title: '各个商品总采购额',
data: []
},
getData(type) {
return this[type].data
sell: {
title: '各个商品总销售额',
data: []
},
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)
profit: {
title: '各个商品总毛利润',
data: []
}
}
},
methods: {
getTitle(type) {
return this[type].title
},
getData(type) {
return this[type].data
},
mounted() {
this.init()
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>

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

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

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

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

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

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

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

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

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

@ -1,24 +1,24 @@
<template>
<el-dialog
v-drag-dialog
:visible="value"
append-to-body
title="选择供应商"
width="70%"
top="50px"
@close="cancel"
@open="search"
v-drag-dialog
:visible="value"
append-to-body
title="选择供应商"
width="70%"
top="50px"
@close="cancel"
@open="search"
>
<el-row>
<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 plain size="small" @click="closeDialog"> </el-button>
<el-button
v-if="canGoToSysSupplierPage"
plain
size="small"
type="dashed"
@click="goToSysSupplierPage"
v-if="canGoToSysSupplierPage"
plain
size="small"
type="dashed"
@click="goToSysSupplierPage"
>
添加供应商
</el-button>
@ -27,13 +27,13 @@
<el-row class="table-container">
<liner-progress :show="config.loading"/>
<el-table
ref="table"
:data="tableData"
current-row-key="id"
highlight-current-row
row-key="id"
@row-click="rowClick"
@row-dblclick="dbclick"
ref="table"
:data="tableData"
current-row-key="id"
highlight-current-row
row-key="id"
@row-click="rowClick"
@row-dblclick="dbclick"
>
<el-table-column align="center" label="#" type="index" width="80"/>
<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="linkphone" 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 align="center" label="状 态" width="120">
<template v-slot="{row}">
<span :class="row.status===1?'success':'error'" class="dot"/>
<span>{{row.status===1?'启用':'禁用'}}</span>
<span>{{ row.status === 1 ? '启用' : '禁用' }}</span>
</template>
</el-table-column>
</el-table>
<el-pagination
background
:current-page="searchForm.page"
:page-size="searchForm.pageSize"
:total="searchForm.total"
layout="total, prev, pager, next, jumper"
@current-change="pageChange"
background
:current-page="searchForm.page"
:page-size="searchForm.pageSize"
:total="searchForm.total"
layout="total, prev, pager, next, jumper"
@current-change="pageChange"
/>
</el-row>
</el-scrollbar>
@ -65,60 +65,60 @@
</template>
<script>
import LinerProgress from '@/components/LinerProgress'
import dialogMixin from "@/mixins/dialogMixin"
import tableMixin from '@/mixins/tablePageMixin'
import {getSuppliers} from "@/api/system/supplier"
import {elError} from "@/utils/message"
import {auth} from "@/utils/auth"
import LinerProgress from '@/components/LinerProgress'
import dialogMixin from "@/mixins/dialogMixin"
import tableMixin from '@/mixins/tablePageMixin'
import {getSuppliers} from "@/api/system/supplier"
import {elError} from "@/utils/message"
import {auth} from "@/utils/auth"
export default {
name: "SupplierSelector",
mixins: [dialogMixin, tableMixin],
components: {LinerProgress},
props: {value: Boolean},
data() {
return {
searchForm: {
status: 1
}
export default {
name: "SupplierSelector",
mixins: [dialogMixin, tableMixin],
components: {LinerProgress},
props: {value: Boolean},
data() {
return {
searchForm: {
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: {
canGoToSysSupplierPage() {
return auth('/system/supplier')
}
dbclick(row) {
this.row = row
this.confirm()
},
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)
},
dbclick(row) {
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'))
}
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>

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save