前端简易模板分离完成

master
toesbieya 6 years ago
parent 066525964f
commit ce2623fc4f
  1. 11755
      vue/template/package-lock.json
  2. 10
      vue/template/package.json
  3. 1
      vue/template/public/static/json/region-pca.json
  4. 48
      vue/template/src/component/Charts/PieChart.vue
  5. 52
      vue/template/src/component/CollapseCard/index.vue
  6. 39
      vue/template/src/component/ExtraArea/index.vue
  7. 23
      vue/template/src/component/LinerProgress/index.vue
  8. 38
      vue/template/src/component/LinerProgress/style.scss
  9. 139
      vue/template/src/component/OrgTree/index.vue
  10. 121
      vue/template/src/component/OrgTree/node.vue
  11. 261
      vue/template/src/component/OrgTree/style.scss
  12. 304
      vue/template/src/component/RegionSelector/Tab/index.vue
  13. 139
      vue/template/src/component/RegionSelector/Tab/style.scss
  14. 82
      vue/template/src/component/RegionSelector/Tree/TreeDialog.vue
  15. 79
      vue/template/src/component/RegionSelector/Tree/index.vue
  16. 33
      vue/template/src/component/RegionSelector/index.vue
  17. 11
      vue/template/src/component/RegionSelector/mixin.js
  18. 11
      vue/template/src/component/RegionSelector/store.js
  19. 217
      vue/template/src/component/TreeSelect/index.vue
  20. 242
      vue/template/src/component/UploadFile/index.vue
  21. 34
      vue/template/src/component/UploadFile/style.scss
  22. 42
      vue/template/src/component/biz/CategoryTree/index.vue
  23. 34
      vue/template/src/component/biz/UserSelector/SimpleMultipleUserSelector.vue
  24. 5
      vue/template/src/component/biz/UserSelector/index.js
  25. 89
      vue/template/src/component/biz/doc/DocHistory.vue
  26. 22
      vue/template/src/component/biz/doc/DocSteps.vue
  27. 46
      vue/template/src/component/editor/JsonEditor.vue
  28. 121
      vue/template/src/component/editor/RichTextEditor.vue
  29. 157
      vue/template/src/component/form/Anchor/index.vue
  30. 156
      vue/template/src/component/form/Search/index.vue
  31. 86
      vue/template/src/component/form/ValidateInfo/index.vue
  32. 50
      vue/template/src/component/form/ValidateInfo/style.scss
  33. 24
      vue/template/src/config/index.js
  34. 24
      vue/template/src/globalMethod/BottomTip/index.js
  35. 49
      vue/template/src/globalMethod/BottomTip/main.vue
  36. 2
      vue/template/src/globalMethod/index.js
  37. 5
      vue/template/src/layout/component/Navbar/index.vue
  38. 3
      vue/template/src/layout/index.vue
  39. 17
      vue/template/src/layout/mixin/offline.js
  40. 39
      vue/template/src/layout/布局设计.md
  41. 3
      vue/template/src/main.js
  42. 4
      vue/template/src/mixin/chart/index.js
  43. 42
      vue/template/src/mixin/chart/logic.js
  44. 29
      vue/template/src/mixin/chart/resize.js
  45. 7
      vue/template/src/mixin/dialogMixin.js
  46. 286
      vue/template/src/mixin/docDetailMixin.js
  47. 168
      vue/template/src/mixin/docTableMixin.js
  48. 94
      vue/template/src/mixin/tablePageMixin.js
  49. 34
      vue/template/src/mixin/treePageMixin.js
  50. 22
      vue/template/src/plugin/imageCompress/cjpeg.js
  51. 22
      vue/template/src/plugin/imageCompress/pngquant.js
  52. 42
      vue/template/src/plugin/imageCompress/util.js
  53. 291
      vue/template/src/plugin/live2d/index.js
  54. 44
      vue/template/src/plugin/webgl/fluid/Material.js
  55. 16
      vue/template/src/plugin/webgl/fluid/Pointer.js
  56. 15
      vue/template/src/plugin/webgl/fluid/Program.js
  57. 27
      vue/template/src/plugin/webgl/fluid/config.js
  58. 50
      vue/template/src/plugin/webgl/fluid/factory.js
  59. 564
      vue/template/src/plugin/webgl/fluid/index.js
  60. 547
      vue/template/src/plugin/webgl/fluid/obj.js
  61. 575
      vue/template/src/plugin/webgl/fluid/shader.js
  62. 387
      vue/template/src/plugin/webgl/fluid/util.js
  63. 6
      vue/template/src/router/module/admin/child/default.js
  64. 22
      vue/template/src/router/module/admin/child/message.js
  65. 68
      vue/template/src/router/module/admin/child/purchase.js
  66. 68
      vue/template/src/router/module/admin/child/sell.js
  67. 16
      vue/template/src/router/module/admin/child/stock.js
  68. 52
      vue/template/src/router/module/admin/child/system.js
  69. 7
      vue/template/src/router/module/dev/child/uploadFileDev.js
  70. 11
      vue/template/src/router/module/dev/index.js
  71. 17
      vue/template/src/router/module/example/child/component.js
  72. 16
      vue/template/src/router/module/example/child/cool.js
  73. 5
      vue/template/src/router/module/example/child/globalMethod.js
  74. 7
      vue/template/src/router/module/example/child/test.js
  75. 3
      vue/template/src/router/module/index.js
  76. 24
      vue/template/src/store/module/dataCache.js
  77. 23
      vue/template/src/store/module/message.js
  78. 28
      vue/template/src/store/module/needSearch.js
  79. 127
      vue/template/src/store/module/resource.js
  80. 94
      vue/template/src/store/module/socket.js
  81. 38
      vue/template/src/store/module/user.js
  82. 240
      vue/template/src/util/excel/common.js
  83. 109
      vue/template/src/util/excel/exceljs.js
  84. 1
      vue/template/src/util/excel/index.js
  85. 68
      vue/template/src/util/file.js
  86. 56
      vue/template/src/util/math.js
  87. 3
      vue/template/src/util/secret.js
  88. 6
      vue/template/src/view/_app/login/LoginForm.vue
  89. 44
      vue/template/src/view/_app/login/RegisterForm.vue
  90. 94
      vue/template/src/view/_common/DetailPage/index.vue
  91. 125
      vue/template/src/view/_common/DetailPage/style.scss
  92. 55
      vue/template/src/view/_common/ListPage/autoAdaptHeightMixin.js
  93. 73
      vue/template/src/view/_common/ListPage/index.vue
  94. 89
      vue/template/src/view/admin/index/component/DailyFinishOrderStat.vue
  95. 97
      vue/template/src/view/admin/index/component/DailyProfitStat.vue
  96. 99
      vue/template/src/view/admin/index/component/PanelGroup/PanelGroupItem.vue
  97. 89
      vue/template/src/view/admin/index/component/PanelGroup/index.vue
  98. 73
      vue/template/src/view/admin/index/component/TotalProfitGoods.vue
  99. 31
      vue/template/src/view/admin/index/indexPage.vue
  100. 234
      vue/template/src/view/admin/message/manage/EditDialog.vue
  101. Some files were not shown because too many files have changed in this diff Show More

File diff suppressed because it is too large Load Diff

@ -8,33 +8,23 @@
"svgo": "svgo -f src/asset/icon/svg --config=src/asset/icon/svgo.yml"
},
"dependencies": {
"axios": "^0.19.2",
"codemirror": "^5.56.0",
"core-js": "^3.6.5",
"decimal.js": "^10.2.0",
"element-ui": "^2.13.2",
"js-md5": "^0.7.3",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"pako": "^1.0.11",
"socket.io-client": "^2.3.0",
"vue": "^2.6.11",
"vue-count-to": "^1.0.13",
"vue-cropper": "^0.5.4",
"vue-router": "^3.3.4",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.4.6",
"@vue/cli-service": "^4.4.6",
"body-parser": "^1.19.0",
"compression-webpack-plugin": "^5.0.2",
"sass": "^1.26.10",
"sass-loader": "^8.0.2",
"svg-sprite-loader": "^4.3.0",
"svgo": "^1.3.2",
"vue-template-compiler": "^2.6.11",
"webpack-bundle-analyzer": "^3.6.1",
"webpack-theme-color-replacer": "^1.3.14"
},
"postcss": {

File diff suppressed because one or more lines are too long

@ -1,48 +0,0 @@
<template>
<div :style="{height,width}"/>
</template>
<script>
import {logic, resize} from "@/mixin/chart"
export default {
mixins: [resize, logic],
props: {
title: String,
data: Array
},
watch: {
data: {
deep: true,
handler(val) {
this.init(val)
}
}
},
methods: {
init(data) {
this.chart && 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,52 +0,0 @@
<template>
<el-card :class="{collapsed}">
<div slot="header" class="clearfix">
<slot name="header">{{ header }}</slot>
<i class="el-icon-arrow-up collapse-card-icon"/>
</div>
<slot/>
</el-card>
</template>
<script>
export default {
name: "CollapseCard",
props: {
header: String
},
data: () => ({collapsed: false}),
methods: {
collapse() {
this.collapsed = !this.collapsed
}
},
mounted() {
const dom = this.$el.querySelector('.el-card__header')
dom.addEventListener('click', this.collapse)
this.$once('hook:beforeDestroy', () => {
dom.removeEventListener('click', this.collapse)
})
}
}
</script>
<style>
.collapse-card-icon {
float: right;
font-weight: bold;
transition: transform .3s;
cursor: pointer;
}
.collapsed .collapse-card-icon {
transform: rotate(180deg);
}
.el-card.collapsed > .el-card__body {
display: none;
}
</style>

@ -1,39 +0,0 @@
<template>
<el-row type="flex" justify="space-between">
<el-col v-show="!expanded" :span="extraSpan">
<slot name="extra"/>
</el-col>
<div style="display: flex;justify-content: center;align-items: center">
<el-link :icon="iconClass" :underline="false" style="font-size: 16px" @click="expanded = !expanded"/>
</div>
<el-col :span="defaultSpan">
<slot/>
</el-col>
</el-row>
</template>
<script>
export default {
name: "ExtraArea",
props: {
extra: {type: Number, default: 5}
},
data: () => ({expanded: false}),
computed: {
extraSpan() {
return this.expanded ? 0 : this.extra
},
defaultSpan() {
return 23 - this.extraSpan
},
iconClass() {
return `el-icon-arrow-${this.expanded ? 'right' : 'left'}`
}
}
}
</script>

@ -1,23 +0,0 @@
<script type="text/jsx">
//muse-uihttps://muse-ui.org/#/zh-CN/progress
export default {
name: "LinerProgress",
functional: true,
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>
)
}
}
}
</script>
<style lang="scss" src="./style.scss"></style>

@ -1,38 +0,0 @@
@import "~@/asset/style/variables.scss";
.liner-progress {
position: relative;
height: 4px;
display: block;
width: 100%;
margin: 0;
overflow: hidden;
.liner-progress-background {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: $--color-primary;
opacity: .3;
}
.linear-progress-indeterminate {
position: absolute;
top: 0;
bottom: 0;
width: 40%;
background-color: $--color-primary;
animation: linear-progress-animate .84s cubic-bezier(.445, .05, .55, .95) infinite;
}
@keyframes linear-progress-animate {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
}

@ -1,139 +0,0 @@
<template>
<div class="org-tree-container">
<div class="org-tree" :class="{horizontal, collapsable}">
<org-tree-node
:data="dataCloned"
:props="props"
:horizontal="horizontal"
:label-width="labelWidth"
:collapsable="collapsable"
:label-class-name="labelClassName"
@on-expand="handleExpand"
@on-node-click="handleNodeClick"
/>
</div>
</div>
</template>
<script>
import OrgTreeNode from "./node"
export default {
name: 'OrgTree',
components: {OrgTreeNode},
props: {
data: {required: true},
props: {
type: Object,
default: () => ({
id: 'id',
label: 'label',
expand: 'expand',
children: 'children'
})
},
horizontal: Boolean,
collapsable: Boolean,
labelWidth: [String, Number],
labelClassName: [Function, String],
expandAll: Boolean
},
data() {
return {
flatData: {},
dataCloned: {}
}
},
watch: {
data(newData) {
this._cloneData(newData)
this._mapData(this.dataCloned, item => {
const {expand} = this.flatData[item[this.props.id]] || {}
if (expand) this.$set(item, this.props.expand, true)
})
this._toggleExpand(this.dataCloned, this.expandAll)
},
expandAll(status) {
this._toggleExpand(this.dataCloned, status)
}
},
methods: {
_cloneData(newData) {
this.dataCloned = JSON.parse(JSON.stringify(newData))
},
_setFlatData(data) {
this.flatData[data[this.props.id]] = data
},
/**
* 工具方法用于遍历树状数据的每个节点 fn为在该节点做的操作其有一个参数即当前节点数据
*/
_mapData(data, fn) {
fn(data)
const children = data[this.props.children]
children && children.forEach(child => this._mapData(child, fn))
},
/**
* 用来便利所有节点数据将树状数据扁平化存放到flatData用于数据更新后展开状态的恢复
*/
_updateExpandStatus() {
this._mapData(this.dataCloned, this._setFlatData)
},
_toggleExpand(data, status) {
if (Array.isArray(data)) {
data.forEach(item => {
this.$set(item, this.props.expand, status)
const children = item[this.props.children]
if (children) {
this._toggleExpand(children, status)
}
})
}
else {
this.$set(data, this.props.expand, status)
const children = data[this.props.children]
if (children) {
this._toggleExpand(children, status)
}
}
},
collapse(list) {
list.forEach(child => {
if (child[this.props.expand]) {
child[this.props.expand] = false
}
const children = child[this.props.children]
children && this.collapse(children)
})
},
handleExpand(data) {
if (this.props.expand in data) {
data[this.props.expand] = !data[this.props.expand]
const children = data[this.props.children]
if (!data[this.props.expand] && children) {
this.collapse(children)
}
}
else this.$set(data, this.props.expand, true)
this.$emit('on-expand', data, data[this.props.expand])
this._updateExpandStatus()
},
handleNodeClick(e, data) {
this.$emit('on-node-click', e, data, () => this.handleExpand(data))
}
},
mounted() {
this._cloneData(this.data)
this._updateExpandStatus()
this._toggleExpand(this.dataCloned, this.expandAll)
}
}
</script>
<style lang="scss" src="./style.scss"></style>

@ -1,121 +0,0 @@
<script type="text/jsx">
//
const isLeaf = (data, prop) => {
return !Array.isArray(data[prop]) || data[prop].length === 0
}
// node
const renderNode = (h, data, context) => {
const {props} = context
const cls = ['org-tree-node']
const childNodes = []
const children = data[props.props.children]
if (isLeaf(data, props.props.children)) {
cls.push('is-leaf')
}
else if (props.collapsable && !data[props.props.expand]) {
cls.push('collapsed')
}
childNodes.push(renderLabel(h, data, context))
if (!props.collapsable || data[props.props.expand]) {
childNodes.push(renderChildren(h, children, context))
}
return <div class={cls.join(' ')}>{childNodes}</div>
}
// label
const renderLabel = (h, data, context) => {
const {props} = context
const label = data[props.props.label]
const clickHandler = context.listeners['on-node-click']
const mousedownHandler = e => {
e.stopPropagation()
if (context.listeners['on-node-mousedown']) {
context.listeners['on-node-mousedown'](e, data)
}
}
const mouseupHandler = context.listeners['on-node-mouseup']
const touchstartHandler = context.listeners['on-node-touchstart']
const touchleaveHandler = context.listeners['on-node-touchleave']
const childNodes = []
if (context.parent.$scopedSlots.default) {
childNodes.push(context.parent.$scopedSlots.default(data))
}
else childNodes.push(label)
if (props.collapsable && !isLeaf(data, props.props.children)) {
childNodes.push(renderBtn(h, data, context))
}
const cls = ['org-tree-node-label-inner']
let {labelWidth, labelClassName} = props
if (typeof labelWidth === 'number') {
labelWidth += 'px'
}
if (typeof labelClassName === 'function') {
labelClassName = labelClassName(data)
}
labelClassName && cls.push(labelClassName)
return (
<div
class="org-tree-node-label"
on-click={e => clickHandler && clickHandler(e, data)}
on-mousedown={mousedownHandler}
on-mouseup={e => mouseupHandler && mouseupHandler(e, data)}
on-touchstart={e => touchstartHandler && touchstartHandler(e, data)}
on-touchleave={e => touchleaveHandler && touchleaveHandler(e, data)}
>
<div class={cls.join(' ')} style={{width: labelWidth}}>
{childNodes}
</div>
</div>
)
}
//
const renderBtn = (h, data, context) => {
const {props} = context
const expandHandler = context.listeners['on-expand']
const onClock = e => {
e.stopPropagation()
expandHandler && expandHandler(data)
}
const onMousedown = e => e.stopPropagation()
const cls = ['org-tree-node-btn']
data[props.props.expand] && cls.push('expanded')
return (
<span class="org-tree-button-wrapper" on-click={onClock} on-mousedown={onMousedown}>
<span class={cls.join(' ')}/>
</span>
)
}
// node
const renderChildren = (h, list, context) => {
if (Array.isArray(list) && list.length) {
return (
<div class="org-tree-node-children">
{list.map(item => renderNode(h, item, context))}
</div>
)
}
}
export default {
name: "OrgTreeNode",
functional: true,
render(h, context) {
return renderNode(h, context.props.data, context)
}
}
</script>

@ -1,261 +0,0 @@
.org-tree-container {
display: inline-block;
padding: 15px;
.org-tree {
display: table;
text-align: center;
&:before, &:after {
content: '';
display: table;
}
&:after {
clear: both;
}
}
.org-tree-node,
.org-tree-node-children {
position: relative;
margin: 0;
padding: 0;
list-style-type: none;
&:before, &:after {
transition: all .35s;
}
}
.org-tree-node-label {
position: relative;
display: inline-block;
z-index: 1;
.org-tree-node-label-inner {
padding: 10px 15px;
text-align: center;
border-radius: 3px;
box-shadow: 0 1px 5px rgba(0, 0, 0, .15);
}
}
.org-tree-node-btn {
position: absolute;
top: 100%;
left: 50%;
width: 20px;
height: 20px;
z-index: 10;
margin-left: -10px;
margin-top: 10px;
background-color: #ffffff;
border: 1px solid #ccc;
border-radius: 50%;
box-shadow: 0 0 2px rgba(0, 0, 0, .15);
cursor: pointer;
transition: all .35s ease;
&:hover {
background-color: #e7e8e9;
transform: scale(1.15);
}
&:before, &:after {
content: '';
position: absolute;
}
&:before {
top: 50%;
left: 4px;
right: 4px;
height: 0;
border-top: 1px solid #ccc;
}
&:after {
top: 4px;
left: 50%;
bottom: 4px;
width: 0;
border-left: 1px solid #ccc;
}
&.expanded:after {
border: none;
}
}
.org-tree-node {
padding-top: 20px;
display: table-cell;
vertical-align: top;
&.is-leaf, &.collapsed {
padding-left: 10px;
padding-right: 10px;
}
&:before, &:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 19px;
}
&:after {
left: 50%;
border-left: 1px solid #ddd;
}
&:not(:first-child):before,
&:not(:last-child):after {
border-top: 1px solid #ddd;
}
}
.collapsable .org-tree-node.collapsed {
padding-bottom: 30px;
.org-tree-node-label:after {
content: '';
position: absolute;
top: 100%;
left: 0;
width: 50%;
height: 20px;
border-right: 1px solid #ddd;
}
}
.org-tree > .org-tree-node {
padding-top: 0;
&:after {
border-left: 0;
}
}
.org-tree-node-children {
padding-top: 20px;
display: table;
&:before {
content: '';
position: absolute;
top: 0;
left: 50%;
width: 0;
height: 20px;
border-left: 1px solid #ddd;
}
&:after {
content: '';
display: table;
clear: both;
}
}
.horizontal {
.org-tree-node {
display: table-cell;
float: none;
padding-top: 0;
padding-left: 20px;
&.is-leaf, &.collapsed {
padding-top: 10px;
padding-bottom: 10px;
}
&:before, &:after {
width: 19px;
height: 50%;
}
&:after {
top: 50%;
left: 0;
border-left: 0;
}
&:only-child:before {
top: 1px;
border-bottom: 1px solid #ddd;
}
&:not(:first-child):before,
&:not(:last-child):after {
border-top: 0;
border-left: 1px solid #ddd;
}
&:not(:only-child):after {
border-top: 1px solid #ddd;
}
.org-tree-node-inner {
display: table;
}
}
.org-tree-node-label {
display: table-cell;
vertical-align: middle;
}
&.collapsable .org-tree-node.collapsed {
padding-right: 30px;
.org-tree-node-label:after {
top: 0;
left: 100%;
width: 20px;
height: 50%;
border-right: 0;
border-bottom: 1px solid #ddd;
}
}
.org-tree-node-btn {
top: 50%;
left: 100%;
margin-top: -11px;
margin-left: 9px;
}
& > .org-tree-node:only-child:before {
border-bottom: 0;
}
.org-tree-node-children {
display: table-cell;
padding-top: 0;
padding-left: 20px;
&:before {
top: 50%;
left: 0;
width: 20px;
height: 0;
border-left: 0;
border-top: 1px solid #ddd;
}
&:after {
display: none;
}
& > .org-tree-node {
display: block;
}
}
}
}

@ -1,304 +0,0 @@
<template>
<el-select
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">
<h3>行政区划选择</h3>
<button class="rg-removeall-button" type="button" title="清除已选" @click="removeAll">
<i class="el-icon-delete"/>
</button>
<button class="rg-done-button" type="button" title="完成" @click="done">
<i class="el-icon-check"/>
</button>
</div>
<div class="rg-search">
<input
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>
</li>
</ul>
</div>
<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)"
>
{{ limit ? item.name + `(${item.value})` : item.name }}
</li>
<li v-if="items.length === 0" class="rg-message-box">无匹配项目</li>
</ul>
</div>
</template>
</el-select>
</template>
<script>
import {debounce, deepClone} from "@/util"
import {createLimitTree, getNodeId} from "@/util/tree"
import {store, init} from '../store'
import common from '../mixin'
const PROVINCE_CITIES = [
{id: '11', name: '北京市'},
{id: '12', name: '天津市'},
{id: '31', name: '上海市'},
{id: '50', name: '重庆市'}
]
const DEFAULT_TABS = [{name: '省/直辖市'}, {name: '市'}, {name: '区/县'}, {name: '乡/镇/街道'}]
//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
}
}
//id
function topDownById(tree, id) {
//=id.length/2
const depth = Math.floor(id.length / 2)
if (depth < 1) return []
const result = []
for (let i = 1; i <= depth; i++) {
const parentId = id.substring(0, i * 2)
const parent = tree.find(node => node.id === parentId)
if (!parent) return result
result.push(parent)
if (i === 1 && PROVINCE_CITIES.some(i => i.id === parentId)) {
i++
}
tree = parent.children || []
}
return result
}
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: []
}
},
computed: {
regionTree() {
return store.data
},
},
watch: {
currentLevel(v) {
this.setItems(v)
}
},
methods: {
//select
visibleChange(v) {
if (v) {
this.transformValueToSelected()
this.$nextTick(() => this.$refs.searchInput.focus())
}
else this.removeAll()
},
search() {
const parent = this.selected[this.currentLevel - 2] || {children: this.treeData}
if (!this.searchText) return this.items = parent.children
//0-9id
const firstCharCode = this.searchText.charCodeAt(0)
const useId = 48 <= firstCharCode && firstCharCode <= 57
const predicate = (
() => useId
? item => item.id.startsWith(this.searchText)
: item => item.name.startsWith(this.searchText)
)()
this.items = parent.children.filter(predicate)
},
clickTab(index, tab) {
//tab
if (index + 1 === this.currentLevel) return
this.currentLevel = index + 1
},
clickItem(item) {
this.setTabs(item, this.currentLevel)
this.nextTab()
},
setTabs(item, level) {
const prev = this.selected.slice(0, level - 1)
prev.push(item)
const next = deepClone(DEFAULT_TABS)
//
if (level === 1) {
//-1''tab
if (PROVINCE_CITIES.some(i => i.id === item.id)) {
next.splice(1, 1)
this.realMaxLevel = this.maxLevel - 1
}
else this.realMaxLevel = this.maxLevel
}
this.selected = prev.concat(next.slice(level, this.realMaxLevel))
},
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 || [] : []
}
},
//value
transformValueToSelected() {
if (!this.value) return
const result = [],
loopArray = Array.isArray(this.value)
? this.value
: this.value.split(this.separation).filter(Boolean)
for (let i = 0; i < loopArray.length; i++) {
const str = loopArray[i], parent = result[i - 1] || {children: this.treeData}
const node = parent.children.find(predicate(str))
//退
if (!node) break
result.push(node)
}
result.forEach((item, index) => this.setTabs(item, index + 1))
},
removeAll() {
this.realMaxLevel = this.maxLevel
this.selected = deepClone(DEFAULT_TABS)
this.currentLevel = 1
this.searchText = ''
this.setItems(this.currentLevel)
},
done() {
const result = this.selected.filter(item => item.id)
this.$emit('input', result.map(item => item.name).join(this.separation))
if (result.length === 0) {
this.$emit('select', {}, [])
return this.$refs.select.blur()
}
const node = result[result.length - 1]
const payload = [node]
if (this.getChildrenOnSelect) {
const ids = getNodeId(node.children)
ids.unshift(node.id)
payload.push(ids)
}
this.$emit('select', ...payload)
this.$refs.select.blur()
},
init() {
this.loading = true
const hasInit = this.regionTree.length > 0
const promise = () => hasInit ? Promise.resolve() : init(this.regionDataUrl)
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 = deepClone(DEFAULT_TABS)
},
mounted() {
this.init()
}
}
</script>
<style lang="scss" src="./style.scss"></style>

@ -1,139 +0,0 @@
.rg-header, .rg-search {
padding: 2px 10px 0;
background-color: #ffffff;
}
.rg-header {
h3 {
padding: 6px;
margin: 0;
color: #24292e;
font-size: 16px;
}
button {
position: absolute;
-webkit-appearance: none;
padding: 0;
cursor: pointer;
background: 0 0;
border: 0;
outline: none;
color: #999;
top: 9px;
i {
font-size: 16px;
}
&:hover {
color: #000000;
}
&.rg-removeall-button {
right: 32px
}
&.rg-done-button {
right: 8px;
}
}
}
.rg-level-tabs {
margin-top: 10px;
background-color: #ffffff;
ul {
padding: 0;
margin: 0;
line-height: 1.5;
border-bottom: 1px solid #eee;
li {
display: inline-block;
position: relative;
a {
display: block;
padding: 0.2rem 1rem 0.6rem;
font-size: 14px;
color: #bbb;
text-decoration: none;
cursor: pointer;
line-height: 1.43;
}
&.active {
a {
color: #333;
background-color: #ffffff;
font-weight: 600;
}
&::after {
content: "";
display: block;
position: absolute;
bottom: 0;
height: 0.2rem;
width: 100%;
background-color: #bbb;
}
}
}
}
}
.rg-results-container {
.rg-results {
margin: 0;
padding: 5px;
width: 364px;
line-height: 1.5;
li {
overflow: hidden;
padding: 3px 10px;
font-size: 14px;
color: #000000;
cursor: pointer;
&.rg-item {
display: inline-block;
color: #777;
&:hover {
color: #000000;
background-color: #F5F5F5;
}
&.active {
background-color: #E4EAEE;
color: #000000;
}
}
&.rg-message-box {
height: 30px;
line-height: 30px;
text-align: center;
cursor: default;
}
}
}
}
.rg-input {
background-color: #f5f5f5;
border: 0;
width: 100%;
font-size: 14px;
padding: 6px;
outline: none !important;
border-radius: 2px;
&::-webkit-input-placeholder {
color: #999;
}
}

@ -1,82 +0,0 @@
<script type="text/jsx">
import {store, init} from '../store'
import AbstractDialog from "@/component/abstract/Dialog"
import {createLimitTree, getNodeId} from "@/util/tree"
export default {
components: {AbstractDialog},
data() {
return {
loading: false,
visible: false,
handler: null,
limitTree: [],
getChildrenOnSelect: false,
limit: false,
limitApi: null
}
},
computed: {
regionTree() {
return store.data
}
},
methods: {
closeDialog() {
this.visible = false
},
nodeClick(obj) {
const payload = [obj]
if (this.getChildrenOnSelect) {
const ids = getNodeId(obj.children)
ids.unshift(obj.id)
payload.push(ids)
}
this.handler(payload)
this.closeDialog()
},
init() {
this.loading = true
const hasInit = this.regionTree.length > 0
const promise = () => hasInit ? Promise.resolve() : init(this.regionDataUrl)
return promise()
.then(() => this.limit ? this.limitApi() : Promise.resolve())
.then(data => data && (this.limitTree = createLimitTree(this.regionTree, data)))
.finally(() => this.loading = false)
}
},
render() {
return (
<abstract-dialog
v-model={this.visible}
class="tree-dialog"
title="选择行政区域"
loading={this.loading}
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}
>
{({data}) => (
<span class="el-tree-node__label">
{this.limit ? data.name + `(${data.value})` : data.name}
</span>
)}
</el-tree>
</abstract-dialog>
)
}
}
</script>

@ -1,79 +0,0 @@
<script type="text/jsx">
import Vue from 'vue'
import TreeDialog from "./TreeDialog"
import common from '../mixin'
const TreeDialogConstructor = Vue.extend(TreeDialog)
let limit, full
export default {
name: "RegionTreeSelector",
mixins: [common],
data() {
return {
dialogVisible: false,
limitTree: []
}
},
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)
return limit.init()
},
initFull() {
if (full) {
return full.getChildrenOnSelect = this.getChildrenOnSelect
}
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,33 +0,0 @@
<script>
const Tree = () => import('./Tree').then(_ => _.default)
const Tab = () => import('./Tab').then(_ => _.default)
export default {
name: "RegionSelector",
functional: true,
props: {
type: {
type: String,
default: 'tab',
validator: v => ['tree', 'tab'].includes(v)
},
//json
regionDataUrl: {
type: String,
default: `${process.env.BASE_URL}static/json/region-pca.json`
}
},
render(h, context) {
if (!context.data.props) {
context.data.props = {}
}
context.data.props.regionDataUrl = context.props.regionDataUrl
return h(context.props.type === 'tree' ? Tree : Tab, context.data)
}
}
</script>

@ -1,11 +0,0 @@
export default {
props: {
value: String,
readonly: Boolean,
size: String,
getChildrenOnSelect: Boolean,
limit: Boolean,
limitApi: Function,
regionDataUrl: String
}
}

@ -1,11 +0,0 @@
import Vue from 'vue'
export const store = Vue.observable({
data: []
})
export function init(url) {
return fetch(url)
.then(r => r.json())
.then(r => store.data = r || [])
}

@ -1,217 +0,0 @@
<template>
<el-select
ref="select"
:value="value"
:multiple="multiple"
:disabled="disabled"
:size="size"
:clearable="clearable"
:placeholder="placeholder"
:popper-append-to-body="popperAppendToBody"
:automatic-dropdown="automaticDropdown"
@input="e => $emit('input',e)"
@change="e => $emit('change',e)"
@remove-tag="removeTag"
>
<div class="tree-select-container" slot="empty">
<el-input
ref="search"
v-if="filterable && filterMethod"
v-model="search"
size="mini"
clearable
@input="onSearch"
/>
<el-tree
ref="tree"
:data="data"
:node-key="nodeKey"
:props="props"
:show-checkbox="multiple"
:default-checked-keys="defaultCheckedKeys"
:current-node-key="multiple ? undefined : value"
:highlight-current="!multiple"
:expand-on-click-node="false"
:check-on-click-node="multiple"
:filter-node-method="filterMethod"
:accordion="accordion"
@node-click="nodeClick"
@check="check"
>
<slot slot-scope="{node,data}">
<span :class="{'el-tree-node__label':true,'is-disabled':node.disabled}">{{ node.label }}</span>
</slot>
</el-tree>
</div>
</el-select>
</template>
<script>
import {debounce} from "@/util"
import {flatTree} from "@/util/tree"
export default {
name: "TreeSelect",
props: {
value: {required: true},
data: {type: Array, default: () => []},
multiple: Boolean,
disabled: Boolean,
size: String,
clearable: {type: Boolean, default: true},
placeholder: String,
filterable: Boolean,
filterMethod: Function,
nodeKey: {type: String, default: 'id'},
props: {
default() {
return {
children: 'children',
label: 'label',
disabled: 'disabled'
}
}
},
accordion: {type: Boolean, default: true},
popperAppendToBody: Boolean,
automaticDropdown: Boolean
},
data() {
return {
currentNode: null,
search: ''
}
},
computed: {
defaultCheckedKeys() {
return this.multiple ? this.value : []
}
},
watch: {
value(v) {
const method = this.multiple ? 'setCheckedKeys' : 'setCurrentKey'
this.$refs.tree[method](v || null)
if (!v && !this.multiple) this.currentNode = null
},
data() {
this.initSelectOptions()
}
},
methods: {
removeTag(v) {
const tree = this.$refs.tree
const value = [...this.value]
//ID
const parents = []
let removeNode = tree.getNode(v)
while (removeNode && removeNode.parent) {
parents.push(removeNode.parent.key)
removeNode = removeNode.parent
}
//
for (let i = value.length - 1; i >= 0; i--) {
const isChild = this.getNodeParent(tree.getNode(value[i]), ({key}) => key === v)
if (isChild || parents.includes(value[i])) {
value.splice(i, 1)
}
}
value.length < this.value.length && this.$emit('input', value)
},
onSearch(v) {
this.$refs.tree.filter(v)
},
nodeClick(data, node) {
if (this.multiple) return
if (node.disabled) {
return this.$refs.tree.setCurrentKey(null)
}
if (this.currentNode === data) {
this.currentNode = null
this.$refs.tree.setCurrentKey(null)
}
else this.currentNode = data
this.$emit('input', this.currentNode && this.currentNode[this.nodeKey], this.currentNode)
this.$refs.select.blur()
},
check(data, {checkedKeys}) {
this.multiple && this.$emit('input', checkedKeys)
},
getNodeParent(node, predicate) {
if (!node || predicate(node)) return node
return this.getNodeParent(node.parent, predicate)
},
initSelectOptions() {
this.$refs.select.cachedOptions =
flatTree(this.data, this.props.children)
.map(i => ({
value: i[this.nodeKey],
currentLabel: i[this.props.label]
}))
this.selectOptionsTrigger()
},
//el-selectoptionswatch
selectOptionsTrigger() {
const select = this.$refs.select
if (select.$isServer) return
this.$nextTick(() => {
select.broadcast('ElSelectDropdown', 'updatePopper')
})
if (select.multiple) {
select.resetInputHeight()
}
let inputs = select.$el.querySelectorAll('input')
if ([].indexOf.call(inputs, document.activeElement) === -1) {
select.setSelected()
}
if (select.defaultFirstOption && (select.filterable || select.remote) && select.filteredOptionsCount) {
select.checkDefaultFirstOption()
}
}
},
created() {
this.onSearch = debounce(this.onSearch)
},
mounted() {
this.currentNode = this.$refs.tree.getCurrentNode()
this.initSelectOptions()
}
}
</script>
<style lang="scss">
@import "~@/asset/style/variables.scss";
.tree-select-container {
padding: 8px;
> .el-input {
margin-bottom: 8px;
}
.el-tree-node__label.is-disabled {
background-color: #edf2fc;
border-color: $border-color-light;
cursor: not-allowed;
}
}
</style>

@ -1,242 +0,0 @@
<template>
<el-upload
ref="upload"
:before-upload="beforeUpload"
:class="{disabled: hideUploader}"
:file-list="tempData"
:http-request="httpRequest"
:limit="limit"
:multiple="multiple"
:on-error="error"
:on-exceed="exceed"
:on-success="success"
action=""
list-type="picture-card"
>
<i class="el-icon-plus"/>
<template v-slot:file="{file}">
<span
class="el-upload-list__item-actions"
@mouseenter="e => handleBlockMouseEnter(e,file)"
@mouseleave="handleBlockMouseLeave"
>
<span class="el-upload-list__item-preview" @click="() => preview(file)">
<i class="el-icon-zoom-in"/>
</span>
<span class="el-upload-list__item-delete" @click="() => download(file)">
<i class="el-icon-download"/>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete" @click="() => remove(file)">
<i class="el-icon-delete"/>
</span>
</span>
<template v-if="file.status === 'success'">
<img :src="file.url" class="el-upload-list__item-thumbnail">
<label class="el-upload-list__item-status-label">
<i class="el-icon-upload-success el-icon-check"/>
</label>
</template>
<div v-if="file.status === 'uploading'" class="progress-mask">
<el-progress :percentage="file.percentage" type="circle"/>
</div>
</template>
<el-tooltip ref="tooltip" :content="tooltipContent" popper-class="upload-tooltip"/>
</el-upload>
</template>
<script>
/*
* 直传七牛云
* 传入fileList自动拼接七牛云外链前缀并增加downloadUrl属性图片类型的文件url与downloadUrl相同
* 非图片类型的文件使用kkFileView预览
* successremove事件中的file.url均不带七牛云外链前缀
* */
import axios from 'axios'
import {deleteUpload} from '@/api/file'
import {file as fileConfig} from '@/config'
import {debounce, isEmpty, deepClone} from '@/util'
import {elError} from "@/util/message"
import {isImage, isDoc, isPdf, isPpt, isRar, isXls, isTxt, isZip} from "@/util/validate"
import {preview, download, upload, autoCompleteUrl} from "@/util/file"
const typeMapper = [
{test: isImage}, {test: isDoc, type: 'doc'}, {test: isPdf, type: 'pdf'}, {test: isPpt, type: 'ppt'},
{test: isRar, type: 'rar'}, {test: isXls, type: 'xls'}, {test: isTxt, type: 'txt'}, {test: isZip, type: 'zip'},
]
function parseMaxSize(maxSize) {
if (typeof maxSize === 'number') {
return maxSize
}
const map = {KB: 1024, MB: 1024 * 1024},
upper = maxSize.toUpperCase(),
num = upper.replace(/[^0-9]/ig, ""),
unit = upper.replace(num, "")
return parseInt(num) * (map[unit] || 1)
}
function getCoverImage(url, fileType) {
const mapper = typeMapper.find(({test}) => test(fileType))
return mapper && mapper.type ? `${process.env.BASE_URL}static/img/fileType/${mapper.type}.png` : url
}
export default {
name: 'UploadFile',
props: {
disabled: Boolean,
multiple: {type: Boolean, default: true},
fileList: {type: Array, default: () => []},
limit: {type: Number, default: 10},
maxSize: {type: [Number, String], default: '10MB'}
},
data() {
return {
tempData: this.fileList,
data: this.fileList,
tooltipContent: null
}
},
computed: {
count() {
return this.data.length
},
hideUploader() {
return this.disabled || this.count >= this.limit
},
previewUrlList() {
return this.data ? this.data.map(i => i.downloadUrl) : []
}
},
watch: {
fileList: {
immediate: true,
handler(fileList) {
if (isEmpty(fileList)) this.data = []
else {
this.data = [...this.fileList.map(file => {
const downloadUrl = autoCompleteUrl(file.url)
return {...file, url: getCoverImage(downloadUrl, file.name), downloadUrl}
})]
}
this.tempData = deepClone(this.data)
}
}
},
methods: {
//仿el-tabletooltip
handleBlockMouseEnter(event, file) {
const tooltip = this.$refs.tooltip
this.tooltipContent = file.name
tooltip.referenceElm = event.target
tooltip.$refs.popper && (tooltip.$refs.popper.style.display = 'none')
tooltip.doDestroy()
tooltip.setExpectedState(true)
this.activateTooltip(tooltip)
},
handleBlockMouseLeave() {
const tooltip = this.$refs.tooltip
if (tooltip) {
tooltip.setExpectedState(false)
tooltip.handleClosePopper()
}
},
//remove
remove(file) {
//tooltip
this.handleBlockMouseLeave()
//
//
if (!file.raw) this.$emit('remove', {url: file.downloadUrl.replace(fileConfig.storePrefix, '')})
//
else if (file.response && !file.response.err) deleteUpload.request(file.raw.key)
this.$refs.upload.handleRemove(file)
const index = this.data.findIndex(i => i.downloadUrl === file.downloadUrl)
if (index > -1) this.data.splice(index, 1)
},
//
preview(file) {
if (!isImage(file.name)) return preview(file.downloadUrl)
const index = this.data.findIndex(i => i.downloadUrl === file.downloadUrl)
this.$image({index, urlList: this.previewUrlList})
},
//
download(file) {
download(file.downloadUrl, file.name)
},
//
exceed() {
elError(`最多只能上传${this.limit}个文件`)
},
//successkey
success(res, file) {
file.url = getCoverImage(file.url, file.name)
file.raw.key = res.key
file.downloadUrl = autoCompleteUrl(res.key)
this.data.push(file)
this.$emit('success', file, res)
},
//
error(err, file) {
elError('上传文件失败,' + err)
file.response.err = err
this.remove(file)
},
//
beforeUpload(file) {
if (!typeMapper.some(({test}) => test(file.name))) {
elError('只支持图片、word、pdf、ppt、rar、excel、txt、zip类型的文件')
return false
}
const maxSize = parseMaxSize(this.maxSize)
if (file.size > maxSize) {
elError(`${file.name}的大小超出${this.$options.filters.number2StorageUnit(maxSize)}`)
return false
}
},
httpRequest({file, onProgress}) {
const source = axios.CancelToken.source()
const promise = upload(file, file.name, {
onUploadProgress(e) {
if (e.total > 0) {
e.percent = (Number)((e.loaded / e.total * 100).toFixed(2))
}
onProgress(e)
},
cancelToken: source.token
})
//eleajaxabort
promise.abort = source.cancel
return promise
}
},
created() {
this.activateTooltip = debounce(tooltip => tooltip.handleShowPopper(), 200)
}
}
</script>
<style lang="scss" src="./style.scss"></style>

@ -1,34 +0,0 @@
@import "~@/asset/style/variables.scss";
.disabled .el-upload--picture-card {
display: none;
}
.el-upload-list--picture-card {
.el-progress {
width: 100%;
height: 100%;
.el-progress-circle {
height: 100% !important;
width: 100% !important;
}
}
.progress-mask {
position: absolute;
top: 0;
height: 100%;
width: 100%;
background-color: #ffffff;
opacity: 0.9;
.el-progress__text {
color: $--color-primary;
}
}
}
.upload-tooltip {
max-width: 146px;
}

@ -1,42 +0,0 @@
<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"
/>
</template>
<script>
import {getAll} from "@/api/system/category"
export default {
name: "CategoryTree",
props: {filterNodeMethod: Function},
computed: {
data() {
return this.$store.state.dataCache.categoryTree
}
},
methods: {
filter(v) {
this.$refs.tree.filter(v)
}
},
mounted() {
if (this.data.length > 0) return
getAll
.request()
.then(({data}) => this.$store.commit('dataCache/categories', data))
.finally(() => this.loading = false)
}
}
</script>

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

@ -1,5 +0,0 @@
import SimpleMultipleUserSelector from "./SimpleMultipleUserSelector"
export {
SimpleMultipleUserSelector
}

@ -1,89 +0,0 @@
<template>
<div>
<abstract-table v-loading="loading" :data="data" :highlight-current-row="false">
<el-table-column align="center" label="#" type="index" width="80"/>
<el-table-column align="center" label="操作人" prop="uname"/>
<el-table-column align="center" label="操作类型" prop="type"/>
<el-table-column align="center" label="时 间" prop="time"/>
<el-table-column align="center" label="备注" prop="info"/>
</abstract-table>
<abstract-pagination :model="searchForm" @current-change="pageChange"/>
</div>
</template>
<script>
import AbstractPagination from '@/component/abstract/Pagination'
import AbstractTable from "@/component/abstract/Table"
import {search} from "@/api/doc/history"
import {isEmpty, timeFormat} from "@/util"
export default {
name: "DocHistory",
components: {AbstractPagination, AbstractTable},
props: {id: String},
data() {
return {
loading: false,
searchForm: {
page: 1,
pageSize: 15,
total: 0
},
data: []
}
},
watch: {
id: {
immediate: true,
handler() {
this.search()
}
}
},
methods: {
search() {
if (isEmpty(this.id)) return
search
.request({pid: this.id, ...this.searchForm})
.then(({data: {list, total}}) => {
this.transformData(list)
this.data = list
this.total = total
})
},
pageChange(v) {
this.searchForm.page = v
this.search()
},
transformData(data) {
data.forEach(i => {
i.time = timeFormat(null, new Date(i.time))
let type = null
switch (i.type) {
case 0:
type = '撤回'
break
case 1:
type = '提交'
break
case 2:
type = '通过'
break
case 3:
type = '驳回'
break
}
i.type = type
})
}
}
}
</script>

@ -1,22 +0,0 @@
<script type="text/jsx">
export default {
name: "DocSteps",
functional: true,
props: {status: Number, finish: Number},
render(h, context) {
const {status, finish} = context.props
const active = status === 2 && (finish === undefined || finish === 2) ? 4 : status
return (
<el-steps align-center active={active === 2 ? 3 : active}>
<el-step title="填写单据"/>
<el-step title="提交单据,等待审核"/>
<el-step title="审核通过"/>
<el-step title="单据完成"/>
</el-steps>
)
}
}
</script>

@ -1,46 +0,0 @@
<template>
<textarea/>
</template>
<script>
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/rubyblue.css'
import 'codemirror/mode/javascript/javascript'
import {isEmpty} from "@/util"
export default {
name: "JsonEditor",
props: ['value'],
watch: {
value(value) {
const editorValue = this.jsonEditor.getValue()
if (value !== editorValue) {
this.jsonEditor.setValue(this.value)
}
}
},
mounted() {
this.jsonEditor = CodeMirror.fromTextArea(
this.$el,
{
mode: 'application/json',
theme: 'rubyblue'
}
)
!isEmpty(this.value) && this.jsonEditor.setValue(this.value)
this.jsonEditor.on('change', cm => {
this.$emit('input', cm.getValue())
})
},
beforeDestroy() {
const element = this.jsonEditor.doc.cm.getWrapperElement()
element && element.remove && element.remove()
}
}
</script>

@ -1,121 +0,0 @@
<template>
<div v-loading="!hasInit" element-loading-text="正在加载编辑器..." class="tinymce-container" :style="{width}">
<div class="tinymce-wrapper">
<textarea :id="id" class="tinymce-textarea"/>
</div>
</div>
</template>
<script>
import {waitUntilSuccess} from "@/util"
import {elError} from "@/util/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
}
export default {
name: "RichTextEditor",
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,
}
},
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
})
}
})
})
.catch(() => elError('tinymce初始化失败,请检查js是否引入成功'))
},
destroy() {
const tinymce = this.getInstance()
if (!tinymce) return
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-wrapper {
width: 100%;
.tinymce-textarea {
visibility: hidden;
}
}
}
.tox-tinymce-aux {
z-index: 10000 !important;
}
</style>

@ -1,157 +0,0 @@
<template>
<div class="form-anchor">
<div
v-for="i in data"
:key="i.ref"
:class="{'form-anchor-item': true,'active': i.ref === cur}"
@click="() => click(i.ref)"
>
{{ i.label }}
</div>
<div class="form-anchor-item" @click="close">关闭导航</div>
</div>
</template>
<script>
import {debounce} from "@/util"
import {getScroll, getTopDistance, scrollTo} from "@/util/browser"
export default {
name: "FormAnchor",
props: {
reference: Function,
//{refref使reference()[ref]dom, label}
data: {type: Array, default: () => []},
//px
offsetTop: {type: Number, default: 15},
//px
bounds: {type: Number, default: 0},
//
getContainer: {
type: Function,
default: () => document.querySelector('#app .page-main>.scroll-container')
},
},
data: () => ({cur: null}),
methods: {
//
close() {
this.$el.remove()
this.$destroy()
},
//ref
getCur(offsetTop, bounds) {
const sections = []
const container = this.getContainer()
for (const {ref} of this.data) {
const target = this.getDomFromRef(ref)
if (!target) continue
const top = getTopDistance(target, container)
if (top <= offsetTop + bounds) {
sections.push({ref, top})
}
}
//dom
if (sections.length) {
const max = sections.reduce((prev, curr) => (curr.top > prev.top ? curr : prev))
return max.ref
}
return null
},
getDomFromRef(ref) {
const refs = this.reference()
if (!refs) return null
const target = Array.isArray(refs[ref]) ? refs[ref][0] : refs[ref]
//vuedom
return target.$el || target
},
click(ref) {
if (!ref || this.animating || !this.reference) return
this.cur = ref
const targetElement = this.getDomFromRef(ref)
if (!targetElement) return
this.animating = true
const container = this.getContainer()
const scrollTop = getScroll(container, true)
const elOffsetTop = getTopDistance(targetElement, container)
const y = scrollTop + elOffsetTop - this.offsetTop
//100ms
scrollTo(container, y, {callback: () => window.setTimeout(() => this.animating = false, 110)})
},
scroll() {
if (this.animating || !this.reference) return
this.cur = this.getCur(this.offsetTop, this.bounds)
}
},
mounted() {
this.scroll = debounce(this.scroll)
this.scroll()
const container = this.getContainer()
container.addEventListener('scroll', this.scroll)
this.$once('hook:beforeDestroy', () => {
container.removeEventListener('scroll', this.scroll)
})
}
}
</script>
<style lang="scss">
@import "~@/asset/style/variables.scss";
.form-anchor {
position: fixed;
right: 0;
top: 200px;
min-width: 72px;
border-radius: 4px 0 0 4px;
background: #ffffff;
box-shadow: 0 2px 12px 0 rgba(166, 167, 173, 0.5);
z-index: 100;
&-item {
width: inherit;
height: 28px;
line-height: 28px;
text-align: center;
color: #909399;
padding: 0 10px;
font-size: 12px;
cursor: pointer;
&:hover,
&.active {
color: $--color-primary
}
&:not(:last-child) {
border-bottom: 1px solid rgba(166, 167, 173, 0.1);
}
}
}
</style>

@ -1,156 +0,0 @@
<script type="text/jsx">
import {deepClone} from "@/util"
import {getElementInnerWidth} from '@/util/browser'
export default {
name: "SearchForm",
model: {
prop: 'model',
event: 'reset'
},
props: {
model: Object,
labelWidth: {type: String, default: '120px'},
/*每个宽度下,一行能有多少个控件,需要是24的因数*/
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, //
span: 8 //24
}
},
methods: {
handleCollapse() {
this.collapse = !this.collapse
},
handleSearch() {
this.$emit('search')
},
handleReset() {
if (!this.initialModel || !this.model) {
return
}
this.$emit('reset', deepClone(this.initialModel))
},
getElementNumInRow() {
const vw = getElementInnerWidth(this.$el.parentNode)
if (vw < 768) return this.xs
if (vw < 998) return this.sm
if (vw < 1200) return this.md
return this.lg
},
resize() {
const num = this.getElementNumInRow()
this.span = 24 / num
//
this.num = num === 1 ? num : num - 1
this.showCollapse = this.num < this.$slots.default.length
},
renderChildren(children, hide) {
return children.map(child => (
<el-col span={this.span} class={{hide}}>{child}</el-col>
))
},
renderAction() {
const ctrl = this.collapse
? {i: 'el-icon-arrow-down', t: '展开'}
: {i: 'el-icon-arrow-up', t: '收起'}
return (
<div class="search-form-action">
<el-button type="primary" size="small" on-click={this.handleSearch}> </el-button>
<el-button type="dashed" size="small" plain on-click={this.handleReset}> </el-button>
{this.showCollapse && (
<el-button
type="text"
size="small"
style="padding-left: 0"
on-click={this.handleCollapse}
>
{ctrl.t}
<v-icon icon={ctrl.i} style="margin-left: 0.5em"/>
</el-button>
)}
</div>
)
}
},
mounted() {
//
this.initialModel = deepClone(this.model)
this.resize()
this.resizeObserver = new ResizeObserver(this.resize)
this.resizeObserver.observe(this.$el.parentNode)
this.$once('hook:beforeDestroy', () => {
if (this.resizeObserver) {
this.resizeObserver.disconnect()
this.resizeObserver = null
}
})
},
render() {
const slots = this.$slots.default, collapse = this.showCollapse && this.collapse
const display = collapse ? slots.slice(0, this.num) : slots
const hidden = collapse ? slots.slice(this.num) : []
return (
<el-form
class="search-form"
label-position="right"
label-width={this.labelWidth}
label-suffix=":"
size="small"
>
<el-row gutter={20}>
{this.renderChildren(display)}
{this.renderChildren(hidden, true)}
{this.renderAction()}
</el-row>
</el-form>
)
}
}
</script>
<style lang="scss">
.search-form {
&-action {
margin-left: auto;
}
> .el-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.hide {
display: none;
}
}
</style>

@ -1,86 +0,0 @@
<template>
<el-popover
ref="popover"
:disabled="error.length === 0"
placement="top"
width="256"
trigger="click"
popper-class="form-validate-info"
>
<div class="form-validate-info-title">表单校验信息</div>
<div class="form-validate-info-content">
<div v-for="i in error" :key="i.prop" class="form-validate-info-item" @click="() => onClick(i)">
<i class="el-icon-circle-close"/>
<div class="form-validate-info-item-msg">{{ i.msg }}</div>
<div class="form-validate-info-item-label">{{ i.label }}</div>
</div>
</div>
<template slot="reference">
<span v-show="error.length > 0" class="form-validate-info-ref">
<i class="el-icon-circle-close"/>
{{ error.length }}
</span>
</template>
</el-popover>
</template>
<script>
export default {
name: "FormValidateInfo",
props: {form: [Function, Object]},
data() {
return {
visible: false,
error: []
}
},
methods: {
//
onClick({prop}) {
const formItem = this.getFormItem(prop)
formItem && formItem.$el.scrollIntoView({block: 'center'})
this.$refs.popover.doClose()
},
//el-form
getForm() {
return typeof this.form === 'function' ? this.form() : this.form
},
//propel-form-item
getFormItem(prop) {
const form = this.getForm()
return form && form.fields.find(i => i.prop === prop)
},
//el-formvalidate
onValidate(prop, success, msg) {
const alreadyIndex = this.error.findIndex(i => i.prop === prop)
if (alreadyIndex !== -1) {
if (success) this.error.splice(alreadyIndex, 1)
else this.$set(this.error, alreadyIndex, {...this.error[alreadyIndex], msg})
}
else {
if (success) return
const formItem = this.getFormItem(prop)
this.error.push({prop, msg, label: formItem.label})
}
}
},
mounted() {
const form = this.getForm()
if (!form) return
form.$on('validate', this.onValidate)
this.$once('hook:beforeDestroy', () => {
form.$off('validate', this.onValidate)
})
}
}
</script>
<style lang="scss" src="./style.scss"></style>

@ -1,50 +0,0 @@
@import "~@/asset/style/variables.scss";
.form-validate-info {
&.el-popper {
padding: 0;
}
&-ref {
padding: 0 10px;
cursor: pointer;
}
&-ref,
&-item .el-icon-circle-close {
color: $--color-danger;
}
&-title {
text-align: center;
padding: 5px 16px 4px;
}
&-content {
max-height: 300px;
overflow: auto;
}
&-item {
padding: 8px 16px;
border-top: 1px solid $border-color-light;
cursor: pointer;
&:hover {
background-color: rgba($--color-primary, .1);
}
.el-icon-circle-close {
float: left;
margin-top: 4px;
margin-right: 12px;
padding-bottom: 22px;
}
&-label {
margin-top: 2px;
color: #909399;
font-size: 12px;
}
}
}

@ -9,23 +9,11 @@ module.exports = {
//页面标题、登录注册页的标题
title: 'BiuBiuBiu~',
//全局axios的baseUrl、devServer的路由前缀
apiPrefix: '/api',
//是否在开发时使用mock,为true时不会启动websocket和代理
useMock: false,
//socket连接地址
socketUrl: isDev ? 'http://localhost:12580' : 'https://toesbieya.cn',
//路由配置
route: {
//路由模式,['hash','history']
mode: 'history',
//是否使用由后端返回的数据作为路由
useBackendDataAsRoute: false,
//路由过渡动画设置,关联transition.css
animate: {
//当未启用多页签时的路由动画
@ -37,18 +25,6 @@ module.exports = {
}
},
//文件配置
file: {
//上传地址
uploadUrl: 'https://upload.qiniup.com',
//上传后的存储地址前缀
storePrefix: 'https://static.toesbieya.cn/',
//预览地址前缀
previewPrefix: isDev ? 'http://localhost:8012' : 'https://preview.toesbieya.cn',
},
//本地存储配置
storage: {
//键名前缀

@ -1,24 +0,0 @@
import Vue from 'vue'
import Main from './main.vue'
const BottomTipConstructor = Vue.extend(Main)
let instance
const bottomTip = function (message) {
instance && instance.close()
instance = new BottomTipConstructor({data: {message}})
instance.$mount()
document.body.appendChild(instance.$el)
instance.value = true
return instance
}
bottomTip.close = function () {
if (instance) {
instance.close()
instance = null
}
}
export default bottomTip

@ -1,49 +0,0 @@
<template>
<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>
</div>
</transition>
</template>
<script>
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
},
}
}
</script>
<style lang="scss">
.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;
}
}
</style>

@ -1,11 +1,9 @@
import BottomTip from './BottomTip'
import Guide from './Guide'
import ImageViewer from './ImageViewer'
import Verify from './Verify'
import Signature from './SignautreBoard'
export default function (Vue) {
Vue.prototype.$bottomTip = BottomTip
Vue.prototype.$guide = Guide
Vue.prototype.$image = ImageViewer
Vue.prototype.$verify = Verify

@ -1,7 +1,6 @@
<script type="jsx">
import {mapState} from 'vuex'
import hamburgerMixin from '@/layout/mixin/hamburger'
import Bell from "./component/Bell"
import HeadMenu from "./component/HeadMenu"
import Logo from "@/layout/component/Logo"
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app"
@ -15,7 +14,7 @@ export default {
mixins: [hamburgerMixin],
components: {Bell, HeadMenu, Logo},
components: {HeadMenu, Logo},
data() {
return {
@ -104,8 +103,6 @@ export default {
</div>
<div>
<bell class="navbar-item"/>
<div
class="navbar-item"
title="刷新"

@ -1,5 +1,4 @@
<script type="text/jsx">
import offlineMixin from './mixin/offline'
import VAside from './component/Aside'
import VNavbar from './component/Navbar'
import VPage from './component/Page'
@ -11,8 +10,6 @@ import {getters as tagsViewGetters} from "@/layout/store/tagsView"
export default {
name: 'Layout',
mixins: [offlineMixin],
components: {VAside, VNavbar, VPage, SettingDrawer},
computed: {

@ -1,17 +0,0 @@
import {isEmpty} from "@/util"
export default {
computed: {
showOfflineTip() {
const state = this.$store.state
const isLogin = !isEmpty(state.user.id, state.user.token)
return !state.socket.online && isLogin
},
},
watch: {
showOfflineTip(v) {
v ? this.$bottomTip('与服务器失去连接') : this.$bottomTip.close()
}
}
}

@ -1,39 +0,0 @@
# 布局设计
## 导航模式
### 桌面端
1.侧边栏导航:侧边栏菜单为完整菜单,顶部菜单不渲染
2.顶部导航:侧边栏菜单不渲染,顶部菜单为完整菜单
3.混合导航:顶部菜单为完整菜单的所有根节点,侧边栏菜单为当前激活的顶部菜单的子级
4.双层侧边栏导航:侧边栏分为最左侧的根节点栏、二级子菜单,顶部菜单不渲染
### 移动端
1.侧边栏导航:侧边栏菜单为完整菜单,顶部菜单不渲染,侧边栏为抽屉模式
2.顶部导航:设置无效,效果等同于侧边栏导航
3.混合导航:设置无效,效果等同于侧边栏导航
4.双层侧边栏导航:设置无效,效果等同于侧边栏导航
## 侧边栏
### 桌面端
未设置自动隐藏时,折叠状态依据用户设置
设置了自动隐藏时,折叠状态无效,侧边栏为抽屉模式
当导航模式为*双层侧边栏导航*时,以下设置无效:
- 折叠
- 折叠时显示父级
- 自动隐藏
### 移动端
侧边栏只能为抽屉模式

@ -23,6 +23,3 @@ new Vue({
store,
render: h => h(App)
})
//页面刷新时socket重连
store.dispatch('socket/init').catch()

@ -1,4 +0,0 @@
import logic from "./logic"
import resize from "./resize"
export {logic, resize}

@ -1,42 +0,0 @@
import {isEmpty, waitUntilSuccess} from "@/util"
export default {
props: {
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '350px'
}
},
data() {
return {
loading: false
}
},
methods: {
$_getChartInstance() {
return waitUntilSuccess(
() => !isEmpty(window.echarts),
() => this.chart = window.echarts.init(this.$_getChartDom(), 'macarons')
)
},
$_getChartDom() {
return this.$el
}
},
mounted() {
this.$_getChartInstance().then(() => this.init())
},
beforeDestroy() {
if (!this.chart) return
this.chart.dispose()
this.chart = null
}
}

@ -1,29 +0,0 @@
import {debounce} from '@/util'
export default {
methods: {
$_initResizeEvent() {
this.resizeObserver = new ResizeObserver(this.$_resizeHandler)
this.resizeObserver.observe(this.$el)
},
$_destroyResizeEvent() {
if (this.resizeObserver) {
this.resizeObserver.disconnect()
this.resizeObserver = null
}
}
},
mounted() {
this.$_resizeHandler = debounce(() => this.chart && this.chart.resize())
this.$_initResizeEvent()
this.$nextTick(() => this.$_resizeHandler())
this.$once('hook:deactivated', this.$_destroyResizeEvent)
this.$once('hook:beforeDestroy', this.$_destroyResizeEvent)
},
activated() {
this.$_initResizeEvent()
this.$nextTick(() => this.$_resizeHandler())
}
}

@ -1,7 +0,0 @@
export default {
methods: {
closeDialog() {
this.$emit('input', false)
}
}
}

@ -1,286 +0,0 @@
import {commonMethods} from "@/mixin/docTableMixin"
import AbstractForm from '@/component/abstract/Form'
import AbstractFormItem from "@/component/abstract/Form/item"
import AbstractTable from "@/component/abstract/Table"
import CollapseCard from '@/component/CollapseCard'
import DetailPage from "@/view/_common/DetailPage"
import DocHistory from '@/component/biz/doc/DocHistory'
import DocSteps from '@/component/biz/doc/DocSteps'
import UploadFile from '@/component/UploadFile'
import {isEmpty, mergeObj} from '@/util'
import {auth} from "@/util/auth"
import {deleteUpload} from "@/api/file"
import {elAlert, elConfirm, elPrompt, elSuccess} from "@/util/message"
import {closeCurrentPage} from "@/util/route"
export default {
components: {
AbstractForm,
AbstractFormItem,
AbstractTable,
CollapseCard,
DetailPage,
DocHistory,
DocSteps,
UploadFile
},
props: {
//单据id
id: String,
//编辑模式,see,add,edit
type: String
},
data() {
return {
attachmentSortSeed: 0,
loading: true,
//单据提交历史
history: [],
form: {
id: null,
cid: null,
cname: null,
ctime: null,
vid: null,
vname: null,
vtime: null,
status: 0,
remark: null,
data: [],
imageList: [],
uploadImageList: [],
deleteImageList: []
},
rules: {}
}
},
computed: {
title() {
if (isEmpty(this.type)) return ''
switch (this.type) {
case 'see':
return `查看${this.docName}${this.form.id}`
case 'add':
return `添加${this.docName}`
case 'edit':
return `编辑${this.docName}${this.form.id}`
}
},
user() {
return this.$store.state.user
},
//权限判断以及根据状态控制是否可编辑
canSave() {
//add模式有添加权限、edit模式有编辑权限且status=0
return this.type === 'add' && auth(this.api.add.url)
|| this.form.status === 0 && this.type === 'edit' && auth(this.api.update.url)
},
canCommit() {
//有提交权限、add模式或edit模式且status=0
return auth(this.api.commit.url)
&& (this.type === 'add' || this.type === 'edit' && this.form.status === 0)
},
canWithdraw() {
//有撤回权限、当前用户是创建人、edit模式且status=1
return auth(this.api.withdraw.url)
&& this.type === 'edit'
&& this.user.id === this.form.cid
&& this.form.status === 1
},
canPass() {
//有通过权限、edit模式且status=1
return auth(this.api.pass.url) && this.type === 'edit' && this.form.status === 1
},
canReject() {
//有驳回权限、edit模式且status=1
return auth(this.api.reject.url) && this.type === 'edit' && this.form.status === 1
},
//底部按钮
buttons() {
return [
{_if: 'canSave', type: 'primary', content: '保 存', e: this.save},
{_if: 'canCommit', type: 'primary', content: '提 交', e: this.commit},
{_if: 'canWithdraw', type: 'danger', content: '撤 回', e: this.withdraw},
{_if: 'canPass', type: 'success', content: '通 过', e: this.pass},
{_if: 'canReject', type: 'danger', content: '驳 回', e: this.reject},
]
.filter(i => this[i._if])
}
},
methods: {
...commonMethods,
//保存,分为添加或修改
save() {
if (this.loading) return
this.$refs.form.validate(v => {
if (!v) return
if (this.validate) {
const valid = this.validate()
if (!isEmpty(valid)) return elAlert(valid)
}
this.loading = true
const method = this.type === 'add' ? this.api.add : this.api.update
method
.request(this.form)
.then(({data, msg}) => {
this.needSearch()
elSuccess(msg)
this.afterSaveOrCommit(data)
})
.catch(() => this.loading = false)
})
},
//提交,状态由拟定->待审核
commit() {
if (this.loading) return
this.$refs.form.validate(v => {
if (!v) return
if (this.validate) {
const valid = this.validate()
if (!isEmpty(valid)) return elAlert(valid)
}
elConfirm('确认提交审核?')
.then(() => this.loading = true)
.then(() => this.api.commit.request(this.form))
.then(({data, msg}) => {
elSuccess(msg)
this.needSearch()
this.afterSaveOrCommit(data)
})
.catch(() => this.loading = false)
})
},
//撤回自己的单据,状态由待审核->拟定
withdraw() {
if (this.loading) return
elConfirm('确认撤回?')
.then(() => {
this.loading = true
return this.api.withdraw.request({id: this.form.id, pid: this.form.pid})
})
.then(({msg}) => {
elSuccess(msg)
this.needSearch()
this.init(this.form.id)
})
.catch(() => this.loading = false)
},
//通过单据,状态由待审核->已审核
pass() {
if (this.loading) return
elConfirm('确认通过审核?')
.then(() => {
this.loading = true
return this.api.pass.request({id: this.form.id, pid: this.form.pid})
})
.then(({msg}) => {
elSuccess(msg)
this.needSearch()
return this.close()
})
.catch(() => this.loading = false)
},
//驳回单据,状态由待审核->拟定
reject() {
if (this.loading) return
elPrompt('请输入驳回理由')
.then(info => {
this.loading = true
return this.api.reject.request({id: this.form.id, pid: this.form.pid, info})
})
.then(({msg}) => {
elSuccess(msg)
this.needSearch()
return this.close()
})
.catch(() => this.loading = false)
},
//初始化单据信息
init(id) {
this.loading = true
if (isEmpty(id)) {
return elAlert(`获取${this.docName}数据失败,请传入id`, this.close)
}
this.api.getById
.request(id)
.then(({data}) => {
if (!data || id !== data.id) return Promise.reject()
this.modifyDataBeforeMerge && this.modifyDataBeforeMerge(data)
return Promise.resolve(mergeObj(this.form, data))
})
.then(() => this.afterInit ? this.afterInit() : Promise.resolve())
.catch(e => {
console.error(e)
return elAlert(`获取${this.docName}数据失败,请重试`, this.close)
})
.finally(() => this.loading = false)
},
//保存、提交成功后需要判断后续动作
afterSaveOrCommit(id) {
if (this.type === 'add') {
const editUrl = this.$route.path.replace('/add', `/edit/${id}`)
return closeCurrentPage(editUrl)
}
return this.init(this.form.id)
},
//关闭页面
close() {
//删除未保存的上传附件
const deleteArr = []
if (this.form.uploadImageList.length > 0) {
deleteArr.push(...this.form.uploadImageList.map(i => i.url))
}
if (deleteArr.length > 0) {
deleteUpload.request(deleteArr).catch(e => ({}))
}
return closeCurrentPage(this.getTablePageUrl())
},
//获取列表页的地址
getTablePageUrl() {
const url = this.$route.path
const i = url.indexOf('/detail')
const tablePageUrl = url.substring(0, i)
return [...tablePageUrl].join('')
},
//列表页是否需要刷新数据
needSearch() {
this.$store.commit('needSearch/emit', this.getTablePageUrl())
},
//附件操作
uploadSuccess(file, res) {
this.form.uploadImageList.push({
url: res.key,
name: file.name,
sort: this.attachmentSortSeed,
size: file.size
})
this.attachmentSortSeed++
},
removeUpload(file) {
this.form.deleteImageList.push(file.url)
const index = this.form.uploadImageList.findIndex(i => i.url === file.url)
if (index > -1) this.form.uploadImageList.splice(index, 1)
}
},
mounted() {
this.loading = false
if (this.type !== 'add') return this.init(this.id)
}
}

@ -1,168 +0,0 @@
import tableMixin from '@/mixin/tablePageMixin'
import LinerProgress from '@/component/LinerProgress'
import ListPage from '@/view/_common/ListPage'
import {isEmpty} from "@/util"
import {elConfirm, elError, elSuccess} from "@/util/message"
import {auth} from "@/util/auth"
import {exportExcel} from "@/util/excel"
export const commonMethods = {
getStatus(status) {
switch (status) {
case 0:
return '拟定'
case 1:
return '待审核'
case 2:
return '已审核'
}
return null
},
getFinish(finish) {
switch (finish) {
case 0:
return '未开始'
case 1:
return '进行中'
case 2:
return '已完成'
}
return null
}
}
export default {
mixins: [tableMixin],
components: {LinerProgress, ListPage},
data() {
return {
searchForm: {
idFuzzy: null,
cname: null,
vname: null
},
temp: {
status: [],
ctime: [],
vtime: []
}
}
},
computed: {
canAdd() {
return auth(this.api.add.url)
},
canUpdate() {
return auth(this.api.update.url)
|| auth(this.api.withdraw.url)
|| auth(this.api.pass.url)
|| auth(this.api.reject.url)
},
canDel() {
return auth(this.api.del.url)
},
canExport() {
return auth(this.api.exportExcel.url)
},
listPageConfig() {
return {
pageLoading: this.config.operating,
buttons: [
this.canAdd && {icon: 'el-icon-plus', e: this.add, content: '添 加'},
{icon: 'el-icon-view', e: this.see, content: '查 看'},
this.canUpdate && {icon: 'el-icon-edit', e: this.edit, content: '编 辑'},
this.canDel && {icon: 'el-icon-delete', e: this.del, content: '删 除'},
this.canExport && {icon: 'el-icon-download', e: this.downloadExcel, content: '导 出'}
],
dataLoading: this.config.loading,
search: {
props: {model: this.searchForm},
on: {search: this.search, reset: v => this.searchForm = v}
},
table: {
props: {data: this.tableData},
on: {'row-click': this.rowClick, 'expand-change': this.getSubList}
},
pagination: {
props: {model: this.searchForm},
on: {'current-change': this.pageChange}
}
}
}
},
methods: {
...commonMethods,
search() {
if (this.config.loading) return
this.config.loading = true
//折叠所有行
this.tableData.forEach(row => this.$children[0].$refs.table.toggleRowExpansion(row, false))
this.row = null
this.api.search
.request(this.mergeSearchForm())
.then(({data: {list, total}}) => {
list.forEach(i => {
i._loading = false //加载状态
i._loaded = false //是否已经加载完成
})
this.searchForm.total = total
this.tableData = list
})
.finally(() => this.config.loading = false)
},
getSubList(row) {
if (row._loaded || row._loading) return
row._loading = true
this.api.getSubById
.request(row.id)
.then(({data}) => {
row.data = data
row._loaded = true
})
.finally(() => row._loading = false)
},
add() {
this.row = null
this.$router.push(`${this.$route.path}/detail/add`)
},
see() {
if (isEmpty(this.row)) return elError('请选择要查看的单据')
this.$router.push(`${this.$route.path}/detail/see/${this.row.id}`)
},
edit() {
if (isEmpty(this.row)) return elError('请选择要编辑的单据')
this.$router.push(`${this.$route.path}/detail/edit/${this.row.id}`)
},
del() {
if (isEmpty(this.row)) {
return elError('请选择要删除的单据')
}
if (this.row.status !== 0) {
return elError('只有状态为【拟定】时才能删除')
}
if (this.config.operating) return
elConfirm(`确定删除单据【${this.row.id}】?`)
.then(() => {
this.config.operating = true
return this.api.del.request(this.row.id)
})
.then(() => {
elSuccess('删除成功')
this.search()
})
.finally(() => this.config.operating = false)
},
downloadExcel() {
exportExcel(this.api.exportExcel.url, this.mergeSearchForm(), this.excel)
}
}
}

@ -1,94 +0,0 @@
/*
* 列表页通用混入
* 引用者必须要有search方法
* */
import AbstractPagination from '@/component/abstract/Pagination'
import AbstractTable from '@/component/abstract/Table'
import {findComponentByTag} from "@/util/vue"
const mixin = {
components: {AbstractPagination, AbstractTable},
data() {
return {
searchForm: {
page: 1,
pageSize: 15,
total: 0
},
config: {
loading: false,
operating: false
},
tableData: [],
row: null,
type: 'see'
}
},
computed: {
tablePageNeedSearchMap() {
return this.$store.state.needSearch.map
}
},
watch: {
row(v) {
!v && this.$_getElTableInstance().setCurrentRow()
},
tablePageNeedSearchMap: {
deep: true,
handler(v) {
if (this._routePath === this.$route.path) {
this.reSearch(v)
}
}
}
},
methods: {
$_getElTableInstance() {
if (!this.$_elTableInstance) {
this.$_elTableInstance = findComponentByTag(this, 'el-table')
}
return this.$_elTableInstance
},
rowClick(row) {
if (this.row === row) {
this.$_getElTableInstance().setCurrentRow()
this.row = null
}
else this.row = row
},
pageChange(v) {
this.searchForm.page = v
this.search()
},
reSearch(map) {
const path = this._routePath
if (map.hasOwnProperty(path) && map[path]) {
this.search()
this.$store.commit('needSearch/renew', path)
}
}
},
activated() {
this.reSearch(this.tablePageNeedSearchMap)
},
mounted() {
//注册needSearch事件
this._routePath = this.$route.path
this.$store.commit('needSearch/init', this._routePath)
this.search()
}
}
export default mixin

@ -1,34 +0,0 @@
/*
* 树形控件页通用混入
* */
const mixin = {
data() {
return {
curNode: null
}
},
watch: {
curNode(v) {
const ref = this.$refs.tree
!v && ref && ref.setCurrentKey()
}
},
methods: {
nodeClick(data, node, ref) {
if (this.curNode === node) {
this.$refs.tree.setCurrentKey()
this.curNode = null
}
else this.curNode = node
if (this.afterNodeClick) {
this.afterNodeClick(data, node, ref, !this.curNode)
}
}
}
}
export default mixin

@ -1,22 +0,0 @@
import {createWorker} from "@/util/worker"
export default function (uint8Array, quality = 75) {
const {protocol, host} = document.location
const url = `${protocol}//${host}${process.env.BASE_URL}static/js/cjpeg.min.js`
return new Promise(resolve => {
let worker = createWorker(handler, {url, data: uint8Array, quality}, ({data}) => {
resolve(data)
worker.terminate()
})
})
}
function handler() {
addEventListener('message', e => {
const {url, data, quality} = e.data
importScripts(url)
const start = performance.now()
const result = cjpeg(data, ['-quality', quality])
postMessage({time: performance.now() - start, data: result.data})
})
}

@ -1,22 +0,0 @@
import {createWorker} from "@/util/worker"
export default function (uint8Array, quality = 75, speed = 4) {
const {protocol, host} = document.location
const url = `${protocol}//${host}${process.env.BASE_URL}static/js/pngquant.min.js`
return new Promise(resolve => {
let worker = createWorker(handler, {url, data: uint8Array, quality, speed}, ({data}) => {
resolve(data)
worker.terminate()
})
})
}
function handler() {
addEventListener('message', e => {
const {url, data, quality, speed} = e.data
importScripts(url)
const start = performance.now()
const result = pngquant(data, {quality: quality.join('-'), speed: speed + ''}, () => ({}))
postMessage({time: performance.now() - start, data: result.data})
})
}

@ -1,42 +0,0 @@
export function base64ToUint8(base64) {
let bstr = atob(base64.split(',')[1]),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return u8arr
}
export function uint8Array2base64(o) {
let g = ""
let j = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
let r = new Uint8Array(o)
let p = r.byteLength
let f = p % 3
let q = p - f
let n, l, k, h
let m
for (let e = 0; e < q; e = e + 3) {
m = (r[e] << 16) | (r[e + 1] << 8) | r[e + 2]
n = (m & 16515072) >> 18
l = (m & 258048) >> 12
k = (m & 4032) >> 6
h = m & 63
g += j[n] + j[l] + j[k] + j[h]
}
if (f === 1) {
m = r[q]
n = (m & 252) >> 2
l = (m & 3) << 4
g += j[n] + j[l] + "=="
}
else if (f === 2) {
m = (r[q] << 8) | r[q + 1]
n = (m & 16128) >> 8
l = (m & 1008) >> 4
k = (m & 15) << 2
g += j[n] + j[l] + j[k] + "="
}
return "data:image/jpeg;base64," + g
}

@ -1,291 +0,0 @@
/*
* Live2D Widget
* https://github.com/stevenjoezhang/live2d-widget
*
* API 后端可自行搭建参考 https://github.com/fghrsh/live2d_api
*/
export default class Waifu {
constructor(tipsPath, apiPath) {
this.tipsPath = tipsPath
this.apiPath = apiPath
this.setUserAction = this.setUserAction.bind(this)
this.visibilityChange = this.visibilityChange.bind(this)
this.userAction = false
this.userActionTimer = null
this.messageTimer = null
this.messageArray = [
"好久不见,日子过得好快呢……",
"大坏蛋!你都多久没理人家了呀,嘤嘤嘤~",
"嗨~快来逗我玩吧!",
"拿小拳拳锤你胸口!",
"记得把小家加入 Adblock 白名单哦!"
]
sessionStorage.removeItem("waifu-text")
this.insertWaifu()
this.addEventListener()
this.welcomeMessage()
this.detectUserAction()
this.initModel()
this.extra()
}
insertWaifu() {
document.body.insertAdjacentHTML("beforeend",
`
<div id="waifu">
<div id="waifu-tips"></div>
<canvas id="live2d" width="300" height="300"></canvas>
<div id="waifu-tool">
<span class="el-icon-s-promotion" title="打飞机"></span>
<span class="el-icon-refresh" title="换人"></span>
<span class="el-icon-s-operation" title="换装"></span>
<span class="el-icon-camera" title="拍照"></span>
<span class="el-icon-switch-button" title="关闭"></span>
</div>
</div>
`
)
window.setTimeout(() => document.getElementById("waifu").style.bottom = '0', 0)
}
addEventListener() {
document.querySelector("#waifu-tool .el-icon-s-promotion").addEventListener("click", this.startAsteroids)
document.querySelector("#waifu-tool .el-icon-refresh").addEventListener("click", this.loadOtherModel.bind(this))
document.querySelector("#waifu-tool .el-icon-s-operation").addEventListener("click", this.loadRandModel.bind(this))
document.querySelector("#waifu-tool .el-icon-camera").addEventListener("click", this.takePicture.bind(this))
document.querySelector("#waifu-tool .el-icon-switch-button").addEventListener("click", this.exit.bind(this))
window.addEventListener("mousemove", this.setUserAction)
window.addEventListener("keydown", this.setUserAction)
window.addEventListener("visibilitychange", this.visibilityChange)
}
removeEventListener() {
window.removeEventListener("mousemove", this.setUserAction)
window.removeEventListener("keydown", this.setUserAction)
window.removeEventListener("visibilitychange", this.visibilityChange)
}
detectUserAction() {
window.setInterval(() => {
if (this.userAction) {
this.userAction = false
window.clearInterval(this.userActionTimer)
this.userActionTimer = null
}
else if (!this.userActionTimer) {
this.userActionTimer = window.setInterval(() => {
this.showMessage(this.randomSelection(this.messageArray), 6000, 9)
}, 20000)
}
}, 1000)
}
setUserAction() {
this.userAction = true
}
startAsteroids() {
if (window.Asteroids) {
if (!window.ASTEROIDSPLAYERS) window.ASTEROIDSPLAYERS = []
window.ASTEROIDSPLAYERS.push(new window.Asteroids())
}
else {
const script = document.createElement("script")
script.src = "https://cdn.jsdelivr.net/gh/GalaxyMimi/CDN/asteroids.js"
document.head.appendChild(script)
}
}
takePicture() {
this.showMessage("照好了嘛,是不是很可爱呢?", 6000, 9)
window.Live2D.captureName = "photo.png"
window.Live2D.captureFrame = true
}
welcomeMessage() {
let text, now = new Date().getHours()
if (now > 5 && now <= 7) text = "早上好!一日之计在于晨,美好的一天就要开始了。"
else if (now > 7 && now <= 11) text = "上午好!工作顺利嘛,不要久坐,多起来走动走动哦!"
else if (now > 11 && now <= 13) text = "中午了,工作了一个上午,现在是午餐时间!"
else if (now > 13 && now <= 17) text = "午后很容易犯困呢,今天的运动目标完成了吗?"
else if (now > 17 && now <= 19) text = "傍晚了!窗外夕阳的景色很美丽呢,最美不过夕阳红~"
else if (now > 19 && now <= 21) text = "晚上好,今天过得怎么样?"
else if (now > 21 && now <= 23) text = ["已经这么晚了呀,早点休息吧,晚安~", "深夜时要爱护眼睛呀!"]
else text = "你是夜猫子呀?这么晚还不睡觉,明天起的来嘛?"
this.showMessage(text, 7000, 8)
}
visibilityChange() {
!document.hidden && this.showMessage("哇,你终于回来了~", 6000, 9)
}
randomSelection(array) {
return Array.isArray(array) ? array[Math.floor(Math.random() * array.length)] : array
}
initModel() {
let modelId = localStorage.getItem("modelId"),
modelTexturesId = localStorage.getItem("modelTexturesId")
if (modelId == null) {
// 首次访问加载 指定模型 的 指定材质
modelId = 1 // 模型 ID
modelTexturesId = 53 // 材质 ID
}
this.loadModel(modelId, modelTexturesId)
fetch(this.tipsPath)
.then(response => response.json())
.then(result => {
for (const key of ['mouseover', 'click']) {
window.addEventListener(key, event => {
for (const tips of result[key]) {
if (!event.target.matches(tips.selector)) continue
const text = this.randomSelection(tips.text).replace("{text}", event.target.innerText)
return this.showMessage(text, 4000, 8)
}
})
}
})
}
loadModel(modelId, modelTexturesId) {
localStorage.setItem("modelId", modelId)
if (modelTexturesId === undefined) modelTexturesId = 0
localStorage.setItem("modelTexturesId", modelTexturesId)
window.loadlive2d(
"live2d",
`${this.apiPath}/get/?id=${modelId}-${modelTexturesId}`,
console.log(`Live2D 模型 ${modelId}-${modelTexturesId} 加载完成`)
)
}
//调用一言api
showHitokoto() {
// 增加 hitokoto.cn 的 API
fetch("https://v1.hitokoto.cn")
.then(response => response.json())
.then(result => {
const text = `这句一言来自 <span>「${result.from}」</span>,是 <span>${result.creator}</span> 在 hitokoto.cn 投稿的。`
this.showMessage(result.hitokoto, 6000, 9)
window.setTimeout(() => this.showMessage(text, 4000, 9), 6000)
})
}
showMessage(text, timeout, priority) {
if (!text) return
if (!sessionStorage.getItem("waifu-text") ||
sessionStorage.getItem("waifu-text") <= priority) {
if (this.messageTimer) {
window.clearTimeout(this.messageTimer)
this.messageTimer = null
}
if (Array.isArray(text)) text = this.randomSelection(text)
sessionStorage.setItem("waifu-text", priority)
const tips = document.getElementById("waifu-tips")
if (!tips) return
tips.innerHTML = text
tips.classList.add("waifu-tips-active")
this.messageTimer = window.setTimeout(() => {
sessionStorage.removeItem("waifu-text")
tips.classList.remove("waifu-tips-active")
}, timeout)
}
}
//换装
loadRandModel() {
const modelId = localStorage.getItem("modelId"),
modelTexturesId = localStorage.getItem("modelTexturesId")
// 可选 "rand"(随机), "switch"(顺序)
fetch(`${this.apiPath}/rand_textures/?id=${modelId}-${modelTexturesId}`)
.then(response => response.json())
.then(result => {
if (result.textures.id === 1 &&
(modelTexturesId === '1' || modelTexturesId === '0')) {
this.showMessage("我还没有其他衣服呢!", 4000, 10)
}
else this.showMessage("我的新衣服好看嘛?", 4000, 10)
this.loadModel(modelId, result.textures.id)
})
}
//切换其他模型
loadOtherModel() {
const modelId = localStorage.getItem("modelId")
fetch(`${this.apiPath}/switch/?id=${modelId}`)
.then(response => response.json())
.then(result => {
this.loadModel(result.model.id)
this.showMessage(result.model.message, 4000, 10)
})
}
extra() {
const devtools = () => ({})
console.log("%c", devtools)
devtools.toString = () => this.showMessage("哈哈,你打开了控制台,是想要看看我的小秘密吗?", 6000, 9)
console.log(`
__,.ヘヽ. / ,
', !--i / /´
' L/
/ /, /| , , ',
/ /-‐/ L_ ! i
7 '-!| |
!,/7 '0' ´0i| |
|." _ ,,,, / |./ |
'| i.,,__ _,. / .i |
'| | / k_7_/レ', . |
| |/i 〈|/ i ,. | i |
.|/ / ! |
kヽ> _,. /!
!'〈//`T´', '7'ーr'
'ヽL__|___i,___,ンレ|
-,/ |___./
'ー' !_,.:
`)
}
exit() {
this.showMessage("愿你有一天能与重要的人重逢。", 2000, 11)
this.removeEventListener()
const dom = document.getElementById("waifu")
dom.style.bottom = "-500px"
window.setTimeout(() => dom.remove && dom.remove(), 3000)
}
}

@ -1,44 +0,0 @@
import {compileShader, createProgram, getUniforms} from "./util"
class Material {
constructor(vertexShader, fragmentShaderSource) {
this.vertexShader = vertexShader
this.fragmentShaderSource = fragmentShaderSource
this.programs = []
this.activeProgram = null
this.uniforms = []
}
setKeywords(gl, keywords) {
let hash = 0
for (let i = 0; i < keywords.length; i++) hash += hashCode(keywords[i])
let program = this.programs[hash]
if (program == null) {
let fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords)
program = createProgram(gl, this.vertexShader, fragmentShader)
this.programs[hash] = program
}
if (program === this.activeProgram) return
this.uniforms = getUniforms(gl, program)
this.activeProgram = program
}
bind(gl) {
gl.useProgram(this.activeProgram)
}
}
function hashCode(s) {
if (s.length === 0) return 0
let hash = 0
for (let i = 0; i < s.length; i++) {
hash = (hash << 5) - hash + s.charCodeAt(i)
hash |= 0 // Convert to 32bit integer
}
return hash
}
export default Material

@ -1,16 +0,0 @@
class Pointer {
constructor() {
this.id = -1
this.texcoordX = 0
this.texcoordY = 0
this.prevTexcoordX = 0
this.prevTexcoordY = 0
this.deltaX = 0
this.deltaY = 0
this.down = false
this.moved = false
this.color = [30, 0, 300]
}
}
export default Pointer

@ -1,15 +0,0 @@
import {createProgram, getUniforms} from "./util"
class Program {
constructor(gl, vertexShader, fragmentShader) {
this.uniforms = {}
this.program = createProgram(gl, vertexShader, fragmentShader)
this.uniforms = getUniforms(gl, this.program)
}
bind(gl) {
gl.useProgram(this.program)
}
}
export default Program

@ -1,27 +0,0 @@
export default {
SIM_RESOLUTION: 128,
DYE_RESOLUTION: 1024,
CAPTURE_RESOLUTION: 512,
DENSITY_DISSIPATION: 1,
VELOCITY_DISSIPATION: 0.2,
PRESSURE: 0.8,
PRESSURE_ITERATIONS: 20,
CURL: 30,
SPLAT_RADIUS: 0.25,
SPLAT_FORCE: 6000,
SHADING: true,
COLORFUL: true,
COLOR_UPDATE_SPEED: 10,
PAUSED: false,
BACK_COLOR: {r: 0, g: 0, b: 0},
TRANSPARENT: false,
BLOOM: true,
BLOOM_ITERATIONS: 8,
BLOOM_RESOLUTION: 256,
BLOOM_INTENSITY: 0.8,
BLOOM_THRESHOLD: 0.6,
BLOOM_SOFT_KNEE: 0.7,
SUNRAYS: true,
SUNRAYS_RESOLUTION: 196,
SUNRAYS_WEIGHT: 1.0
}

@ -1,50 +0,0 @@
import Material from './Material'
import Program from './Program'
import {
advectionShader,
baseVertexShader,
bloomBlurShader,
bloomFinalShader,
bloomPrefilterShader,
blurShader,
blurVertexShader,
checkerboardShader,
clearShader,
colorShader,
copyShader,
curlShader,
displayShaderSource,
divergenceShader,
gradientSubtractShader,
pressureShader,
splatShader,
sunraysMaskShader,
sunraysShader,
vorticityShader
} from './shader'
export default function (gl, ext) {
const blurProgram = new Program(gl, blurVertexShader(gl), blurShader(gl))
const copyProgram = new Program(gl, baseVertexShader(gl), copyShader(gl))
const clearProgram = new Program(gl, baseVertexShader(gl), clearShader(gl))
const colorProgram = new Program(gl, baseVertexShader(gl), colorShader(gl))
const checkerboardProgram = new Program(gl, baseVertexShader(gl), checkerboardShader(gl))
const bloomPrefilterProgram = new Program(gl, baseVertexShader(gl), bloomPrefilterShader(gl))
const bloomBlurProgram = new Program(gl, baseVertexShader(gl), bloomBlurShader(gl))
const bloomFinalProgram = new Program(gl, baseVertexShader(gl), bloomFinalShader(gl))
const sunraysMaskProgram = new Program(gl, baseVertexShader(gl), sunraysMaskShader(gl))
const sunraysProgram = new Program(gl, baseVertexShader(gl), sunraysShader(gl))
const splatProgram = new Program(gl, baseVertexShader(gl), splatShader(gl))
const advectionProgram = new Program(gl, baseVertexShader(gl), advectionShader(gl, ext))
const divergenceProgram = new Program(gl, baseVertexShader(gl), divergenceShader(gl))
const curlProgram = new Program(gl, baseVertexShader(gl), curlShader(gl))
const vorticityProgram = new Program(gl, baseVertexShader(gl), vorticityShader(gl))
const pressureProgram = new Program(gl, baseVertexShader(gl), pressureShader(gl))
const gradienSubtractProgram = new Program(gl, baseVertexShader(gl), gradientSubtractShader(gl))
const displayMaterial = new Material(baseVertexShader(gl), displayShaderSource(gl))
return {
blurProgram, copyProgram, clearProgram, colorProgram, checkerboardProgram, bloomPrefilterProgram,
bloomBlurProgram, bloomFinalProgram, sunraysMaskProgram, sunraysProgram, splatProgram, advectionProgram,
divergenceProgram, curlProgram, vorticityProgram, pressureProgram, gradienSubtractProgram, displayMaterial
}
}

@ -1,564 +0,0 @@
'use strict'
import {
createDoubleFBO,
createFBO,
downloadURI,
framebufferToTexture,
generateColor,
getResolution,
getWebGLContext,
isMobile,
normalizeColor,
normalizeTexture,
resizeCanvas,
scaleByPixelRatio,
textureToCanvas,
updatePointerDownData,
updatePointerMoveData,
updatePointerUpData,
wrap
} from "./util"
import factory from './factory'
import config from './config'
import Pointer from './Pointer'
const WebGLFluid = canvas => {
resizeCanvas(canvas)
let pointers = []
let splatStack = []
let bloomFrameBuffers = []
let lastUpdateTime = Date.now()
let colorUpdateTimer = 0.0
pointers.push(new Pointer())
const {gl, ext} = getWebGLContext(canvas)
if (isMobile()) {
config.DYE_RESOLUTION = 512
}
if (!ext.supportLinearFiltering) {
config.DYE_RESOLUTION = 512
config.SHADING = false
config.BLOOM = false
config.SUNRAYS = false
}
let dye
let velocity
let divergence
let curl
let pressure
let bloom
let sunrays
let sunraysTemp
const blit = (() => {
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer())
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer())
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW)
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(0)
return destination => {
gl.bindFramebuffer(gl.FRAMEBUFFER, destination)
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0)
}
})()
const {
blurProgram, copyProgram, clearProgram, colorProgram, checkerboardProgram, bloomPrefilterProgram,
bloomBlurProgram, bloomFinalProgram, sunraysMaskProgram, sunraysProgram, splatProgram, advectionProgram,
divergenceProgram, curlProgram, vorticityProgram, pressureProgram, gradienSubtractProgram, displayMaterial
} = factory(gl, ext)
function startGUI() {
if (isMobile()) return
let gui = new dat.GUI({width: 300})
gui
.add(config, 'DYE_RESOLUTION', {high: 1024, medium: 512, low: 256, 'very low': 128})
.name('quality')
.onFinishChange(initFrameBuffers())
gui
.add(config, 'SIM_RESOLUTION', {'32': 32, '64': 64, '128': 128, '256': 256})
.name('sim resolution')
.onFinishChange(initFrameBuffers())
gui.add(config, 'DENSITY_DISSIPATION', 0, 4.0).name('density diffusion')
gui.add(config, 'VELOCITY_DISSIPATION', 0, 4.0).name('velocity diffusion')
gui.add(config, 'PRESSURE', 0.0, 1.0).name('pressure')
gui
.add(config, 'CURL', 0, 50)
.name('vorticity')
.step(1)
gui.add(config, 'SPLAT_RADIUS', 0.01, 1.0).name('splat radius')
gui
.add(config, 'SHADING')
.name('shading')
.onFinishChange(updateKeywords)
gui.add(config, 'COLORFUL').name('colorful')
gui
.add(config, 'PAUSED')
.name('paused')
.listen()
gui
.add({fun: () => splatStack.push(parseInt(Math.random() * 20) + 5)}, 'fun')
.name('Random splats')
let bloomFolder = gui.addFolder('Bloom')
bloomFolder
.add(config, 'BLOOM')
.name('enabled')
.onFinishChange(updateKeywords)
bloomFolder.add(config, 'BLOOM_INTENSITY', 0.1, 2.0).name('intensity')
bloomFolder.add(config, 'BLOOM_THRESHOLD', 0.0, 1.0).name('threshold')
let sunraysFolder = gui.addFolder('Sunrays')
sunraysFolder
.add(config, 'SUNRAYS')
.name('enabled')
.onFinishChange(updateKeywords)
sunraysFolder.add(config, 'SUNRAYS_WEIGHT', 0.3, 1.0).name('weight')
let captureFolder = gui.addFolder('Capture')
captureFolder.addColor(config, 'BACK_COLOR').name('background color')
captureFolder.add(config, 'TRANSPARENT').name('transparent')
captureFolder.add({fun: captureScreenShot}, 'fun').name('take screenshot')
}
function initFrameBuffers() {
let simRes = getResolution(gl, config.SIM_RESOLUTION)
let dyeRes = getResolution(gl, config.DYE_RESOLUTION)
const texType = ext.halfFloatTexType
const rgba = ext.formatRGBA
const rg = ext.formatRG
const r = ext.formatR
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST
if (dye == null) dye = createDoubleFBO(gl, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering)
else dye = resizeDoubleFBO(dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering)
if (velocity == null) velocity = createDoubleFBO(gl, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering)
else velocity = resizeDoubleFBO(velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering)
divergence = createFBO(gl, simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST)
curl = createFBO(gl, simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST)
pressure = createDoubleFBO(gl, simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST)
initBloomFrameBuffers()
initSunraysFrameBuffers()
}
function initBloomFrameBuffers() {
let res = getResolution(gl, config.BLOOM_RESOLUTION)
const texType = ext.halfFloatTexType
const rgba = ext.formatRGBA
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST
bloom = createFBO(gl, res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering)
bloomFrameBuffers.length = 0
for (let i = 0; i < config.BLOOM_ITERATIONS; i++) {
let width = res.width >> (i + 1)
let height = res.height >> (i + 1)
if (width < 2 || height < 2) break
let fbo = createFBO(gl, width, height, rgba.internalFormat, rgba.format, texType, filtering)
bloomFrameBuffers.push(fbo)
}
}
function initSunraysFrameBuffers() {
let res = getResolution(gl, config.SUNRAYS_RESOLUTION)
const texType = ext.halfFloatTexType
const r = ext.formatR
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST
sunrays = createFBO(gl, res.width, res.height, r.internalFormat, r.format, texType, filtering)
sunraysTemp = createFBO(gl, res.width, res.height, r.internalFormat, r.format, texType, filtering)
}
function resizeFBO(target, w, h, internalFormat, format, type, param) {
let newFBO = createFBO(gl, w, h, internalFormat, format, type, param)
copyProgram.bind(gl)
gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0))
blit(newFBO.fbo)
return newFBO
}
function resizeDoubleFBO(target, w, h, internalFormat, format, type, param) {
if (target.width === w && target.height === h) return target
target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param)
target.write = createFBO(gl, w, h, internalFormat, format, type, param)
target.width = w
target.height = h
target.texelSizeX = 1.0 / w
target.texelSizeY = 1.0 / h
return target
}
function updateKeywords() {
let displayKeywords = []
if (config.SHADING) displayKeywords.push('SHADING')
if (config.BLOOM) displayKeywords.push('BLOOM')
if (config.SUNRAYS) displayKeywords.push('SUNRAYS')
displayMaterial.setKeywords(gl, displayKeywords)
}
function update() {
const dt = calcDeltaTime()
if (resizeCanvas(canvas)) initFrameBuffers()
updateColors(dt)
applyInputs()
if (!config.PAUSED) step(dt)
render(null)
window.requestAnimationFrame(update)
}
function calcDeltaTime() {
let now = Date.now()
let dt = (now - lastUpdateTime) / 1000
dt = Math.min(dt, 0.016666)
lastUpdateTime = now
return dt
}
function updateColors(dt) {
if (!config.COLORFUL) return
colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED
if (colorUpdateTimer >= 1) {
colorUpdateTimer = wrap(colorUpdateTimer, 0, 1)
pointers.forEach(p => {
p.color = generateColor()
})
}
}
function applyInputs() {
if (splatStack.length > 0) multipleSplats(splatStack.pop())
pointers.forEach(p => {
if (p.moved) {
p.moved = false
splatPointer(p)
}
})
}
function step(dt) {
gl.disable(gl.BLEND)
gl.viewport(0, 0, velocity.width, velocity.height)
curlProgram.bind(gl)
gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY)
gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0))
blit(curl.fbo)
vorticityProgram.bind(gl)
gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY)
gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0))
gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1))
gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL)
gl.uniform1f(vorticityProgram.uniforms.dt, dt)
blit(velocity.write.fbo)
velocity.swap()
divergenceProgram.bind(gl)
gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY)
gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0))
blit(divergence.fbo)
clearProgram.bind(gl)
gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0))
gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE)
blit(pressure.write.fbo)
pressure.swap()
pressureProgram.bind(gl)
gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY)
gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0))
for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1))
blit(pressure.write.fbo)
pressure.swap()
}
gradienSubtractProgram.bind(gl)
gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY)
gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0))
gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1))
blit(velocity.write.fbo)
velocity.swap()
advectionProgram.bind(gl)
gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY)
if (!ext.supportLinearFiltering) gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY)
let velocityId = velocity.read.attach(0)
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId)
gl.uniform1i(advectionProgram.uniforms.uSource, velocityId)
gl.uniform1f(advectionProgram.uniforms.dt, dt)
gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION)
blit(velocity.write.fbo)
velocity.swap()
gl.viewport(0, 0, dye.width, dye.height)
if (!ext.supportLinearFiltering) gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY)
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0))
gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1))
gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION)
blit(dye.write.fbo)
dye.swap()
}
function render(target) {
if (config.BLOOM) {
applyBloom(dye.read, bloom)
}
if (config.SUNRAYS) {
applySunrays(dye.read, dye.write, sunrays)
blur(sunrays, sunraysTemp, 1)
}
if (target == null || !config.TRANSPARENT) {
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
gl.enable(gl.BLEND)
}
else {
gl.disable(gl.BLEND)
}
let width = target == null ? gl.drawingBufferWidth : target.width
let height = target == null ? gl.drawingBufferHeight : target.height
gl.viewport(0, 0, width, height)
let fbo = target == null ? null : target.fbo
if (!config.TRANSPARENT) drawColor(fbo, normalizeColor(config.BACK_COLOR))
if (target == null && config.TRANSPARENT) drawCheckerboard(fbo)
drawDisplay(fbo, width, height)
}
function drawColor(fbo, color) {
colorProgram.bind(gl)
gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1)
blit(fbo)
}
function drawCheckerboard(fbo) {
checkerboardProgram.bind(gl)
gl.uniform1f(checkerboardProgram.uniforms.aspectRatio, canvas.width / canvas.height)
blit(fbo)
}
function drawDisplay(fbo, width, height) {
displayMaterial.bind(gl)
if (config.SHADING) gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height)
gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0))
if (config.BLOOM) {
gl.uniform1i(displayMaterial.uniforms.uBloom, bloom.attach(1))
}
if (config.SUNRAYS) gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3))
blit(fbo)
}
function applyBloom(source, destination) {
if (bloomFrameBuffers.length < 2) return
let last = destination
gl.disable(gl.BLEND)
bloomPrefilterProgram.bind(gl)
let knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001
let curve0 = config.BLOOM_THRESHOLD - knee
let curve1 = knee * 2
let curve2 = 0.25 / knee
gl.uniform3f(bloomPrefilterProgram.uniforms.curve, curve0, curve1, curve2)
gl.uniform1f(bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD)
gl.uniform1i(bloomPrefilterProgram.uniforms.uTexture, source.attach(0))
gl.viewport(0, 0, last.width, last.height)
blit(last.fbo)
bloomBlurProgram.bind(gl)
for (let i = 0; i < bloomFrameBuffers.length; i++) {
let dest = bloomFrameBuffers[i]
gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY)
gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0))
gl.viewport(0, 0, dest.width, dest.height)
blit(dest.fbo)
last = dest
}
gl.blendFunc(gl.ONE, gl.ONE)
gl.enable(gl.BLEND)
for (let i = bloomFrameBuffers.length - 2; i >= 0; i--) {
let baseTex = bloomFrameBuffers[i]
gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY)
gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0))
gl.viewport(0, 0, baseTex.width, baseTex.height)
blit(baseTex.fbo)
last = baseTex
}
gl.disable(gl.BLEND)
bloomFinalProgram.bind(gl)
gl.uniform2f(bloomFinalProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY)
gl.uniform1i(bloomFinalProgram.uniforms.uTexture, last.attach(0))
gl.uniform1f(bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY)
gl.viewport(0, 0, destination.width, destination.height)
blit(destination.fbo)
}
function applySunrays(source, mask, destination) {
gl.disable(gl.BLEND)
sunraysMaskProgram.bind(gl)
gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0))
gl.viewport(0, 0, mask.width, mask.height)
blit(mask.fbo)
sunraysProgram.bind(gl)
gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT)
gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0))
gl.viewport(0, 0, destination.width, destination.height)
blit(destination.fbo)
}
function blur(target, temp, iterations) {
blurProgram.bind(gl)
for (let i = 0; i < iterations; i++) {
gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0)
gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0))
blit(temp.fbo)
gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY)
gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0))
blit(target.fbo)
}
}
function splatPointer(pointer) {
let dx = pointer.deltaX * config.SPLAT_FORCE
let dy = pointer.deltaY * config.SPLAT_FORCE
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color)
}
function multipleSplats(amount) {
for (let i = 0; i < amount; i++) {
const color = generateColor()
color.r *= 10.0
color.g *= 10.0
color.b *= 10.0
const x = Math.random()
const y = Math.random()
const dx = 1000 * (Math.random() - 0.5)
const dy = 1000 * (Math.random() - 0.5)
splat(x, y, dx, dy, color)
}
}
function splat(x, y, dx, dy, color) {
gl.viewport(0, 0, velocity.width, velocity.height)
splatProgram.bind(gl)
gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0))
gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height)
gl.uniform2f(splatProgram.uniforms.point, x, y)
gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0)
gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0))
blit(velocity.write.fbo)
velocity.swap()
gl.viewport(0, 0, dye.width, dye.height)
gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0))
gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b)
blit(dye.write.fbo)
dye.swap()
}
function correctRadius(radius) {
let aspectRatio = canvas.width / canvas.height
if (aspectRatio > 1) radius *= aspectRatio
return radius
}
function captureScreenShot() {
let res = getResolution(gl, config.CAPTURE_RESOLUTION)
let target = createFBO(gl, res.width, res.height, ext.formatRGBA.internalFormat, ext.formatRGBA.format, ext.halfFloatTexType, gl.NEAREST)
render(target)
let texture = framebufferToTexture(gl, target)
texture = normalizeTexture(texture, target.width, target.height)
let captureCanvas = textureToCanvas(texture, target.width, target.height)
let dataUri = captureCanvas.toDataURL()
downloadURI('fluid.png', dataUri)
URL.revokeObjectURL(dataUri)
}
//startGUI();
updateKeywords()
initFrameBuffers()
multipleSplats(parseInt(Math.random() * 20) + 5)
update()
canvas.addEventListener('mousedown', e => {
let posX = scaleByPixelRatio(e.offsetX)
let posY = scaleByPixelRatio(e.offsetY)
let pointer = pointers.find(p => p.id === -1)
if (pointer == null) pointer = new Pointer()
updatePointerDownData(canvas, pointer, -1, posX, posY)
})
canvas.addEventListener('mousemove', e => {
let posX = scaleByPixelRatio(e.offsetX)
let posY = scaleByPixelRatio(e.offsetY)
updatePointerMoveData(canvas, pointers[0], posX, posY)
})
canvas.addEventListener('touchstart', e => {
e.preventDefault()
const touches = e.targetTouches
while (touches.length >= pointers.length) pointers.push(new Pointer())
for (let i = 0; i < touches.length; i++) {
let posX = scaleByPixelRatio(touches[i].pageX)
let posY = scaleByPixelRatio(touches[i].pageY)
updatePointerDownData(canvas, pointers[i + 1], touches[i].identifier, posX, posY)
}
})
canvas.addEventListener('touchmove', e => {
e.preventDefault()
const touches = e.targetTouches
for (let i = 0; i < touches.length; i++) {
let posX = scaleByPixelRatio(touches[i].pageX)
let posY = scaleByPixelRatio(touches[i].pageY)
updatePointerMoveData(canvas, pointers[i + 1], posX, posY)
}
})
window.addEventListener('mouseup', () => {
updatePointerUpData(pointers[0])
})
window.addEventListener('touchend', e => {
const touches = e.changedTouches
for (let i = 0; i < touches.length; i++) {
let pointer = pointers.find(p => p.id === touches[i].identifier)
updatePointerUpData(pointer)
}
})
window.addEventListener('keydown', e => {
if (e.code === 'KeyP') config.PAUSED = !config.PAUSED
if (e.key === ' ') splatStack.push(parseInt(Math.random() * 20) + 5)
if (e.key === 's') {
captureScreenShot()
}
})
}
export default WebGLFluid

@ -1,547 +0,0 @@
'use strict'
import {
createDoubleFBO,
createFBO,
downloadURI,
framebufferToTexture,
generateColor,
getResolution,
getWebGLContext,
isMobile,
normalizeColor,
normalizeTexture,
resizeCanvas,
scaleByPixelRatio,
textureToCanvas,
updatePointerDownData,
updatePointerMoveData,
updatePointerUpData,
wrap
} from "./util"
import factory from './factory'
import config from './config'
import Pointer from './Pointer'
export default class WebGlFluid {
constructor(canvas) {
this.pointers = [new Pointer()]
this.splatStack = []
this.bloomFrameBuffers = []
this.lastUpdateTime = Date.now()
this.colorUpdateTimer = 0.0
this.canvas = canvas
const {gl, ext} = getWebGLContext(canvas)
this.gl = gl
this.ext = ext
if (isMobile()) {
config.DYE_RESOLUTION = 512
}
if (!ext.supportLinearFiltering) {
config.DYE_RESOLUTION = 512
config.SHADING = false
config.BLOOM = false
config.SUNRAYS = false
}
this.blit = (() => {
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer())
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer())
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW)
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(0)
return destination => {
gl.bindFramebuffer(gl.FRAMEBUFFER, destination)
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0)
}
})()
this.factory = factory(gl, ext)
this.rAF = null
this.stopSign = false
this.loop = this.loop.bind(this)
this.onMousedown = this.onMousedown.bind(this)
this.onMousemove = this.onMousemove.bind(this)
this.onMouseup = this.onMouseup.bind(this)
this.onTouchstart = this.onTouchstart.bind(this)
this.onTouchmove = this.onTouchmove.bind(this)
this.onTouchend = this.onTouchend.bind(this)
this.onKeydown = this.onKeydown.bind(this)
this.start()
}
start() {
this.resize()
this.updateKeywords()
this.initFrameBuffers()
this.multipleSplats(~~(Math.random() * 20) + 5)
this.loop()
this.canvas.addEventListener('mousedown', this.onMousedown)
this.canvas.addEventListener('mousemove', this.onMousemove)
window.addEventListener('mouseup', this.onMouseup)
this.canvas.addEventListener('touchstart', this.onTouchstart)
this.canvas.addEventListener('touchmove', this.onTouchmove)
window.addEventListener('touchend', this.onTouchend)
window.addEventListener('keydown', this.onKeydown)
}
stop() {
this.canvas.removeAllListeners()
window.removeListener('mouseup', this.onMouseup)
window.removeListener('touchend', this.onTouchend)
window.removeListener('keydown', this.onKeydown)
this.stopSign = true
this.gl.getExtension('WEBGL_lose_context').loseContext()
}
loop() {
if (this.stopSign) {
this.rAF && window.cancelAnimationFrame(this.rAF)
return
}
const dt = this.calcDeltaTime()
if (this.resize()) this.initFrameBuffers()
this.updateColors(dt)
this.applyInputs()
if (!config.PAUSED) this.step(dt)
this.render(null)
this.rAF = window.requestAnimationFrame(this.loop)
}
resize() {
return resizeCanvas(this.canvas)
}
onMousedown(e) {
const posX = scaleByPixelRatio(e.offsetX)
const posY = scaleByPixelRatio(e.offsetY)
let pointer = this.pointers.find(p => p.id === -1)
if (!pointer) pointer = new Pointer()
updatePointerDownData(this.canvas, pointer, -1, posX, posY)
}
onMousemove(e) {
const posX = scaleByPixelRatio(e.offsetX)
const posY = scaleByPixelRatio(e.offsetY)
updatePointerMoveData(this.canvas, this.pointers[0], posX, posY)
}
onMouseup() {
updatePointerUpData(this.pointers[0])
}
onTouchstart(e) {
e.preventDefault()
const touches = e.targetTouches
while (touches.length >= this.pointers.length) this.pointers.push(new Pointer())
for (let i = 0; i < touches.length; i++) {
let posX = scaleByPixelRatio(touches[i].pageX)
let posY = scaleByPixelRatio(touches[i].pageY)
updatePointerDownData(this.canvas, this.pointers[i + 1], touches[i].identifier, posX, posY)
}
}
onTouchmove(e) {
e.preventDefault()
const touches = e.targetTouches
for (let i = 0; i < touches.length; i++) {
const posX = scaleByPixelRatio(touches[i].pageX)
const posY = scaleByPixelRatio(touches[i].pageY)
updatePointerMoveData(this.canvas, this.pointers[i + 1], posX, posY)
}
}
onTouchend(e) {
for (const {identifier} of e.changedTouches) {
const pointer = this.pointers.find(p => p.id === identifier)
updatePointerUpData(pointer)
}
}
onKeydown(e) {
if (e.code === 'KeyP') config.PAUSED = !config.PAUSED
if (e.key === ' ') this.splatStack.push(~~(Math.random() * 20) + 5)
if (e.key === 's') this.captureScreenShot()
}
initFrameBuffers() {
const simRes = getResolution(this.gl, config.SIM_RESOLUTION)
const dyeRes = getResolution(this.gl, config.DYE_RESOLUTION)
const texType = this.ext.halfFloatTexType
const rgba = this.ext.formatRGBA
const rg = this.ext.formatRG
const r = this.ext.formatR
const filtering = this.ext.supportLinearFiltering ? this.gl.LINEAR : this.gl.NEAREST
if (this.dye == null) this.dye = createDoubleFBO(this.gl, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering)
else this.dye = this.resizeDoubleFBO(this.dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering)
if (this.velocity == null) this.velocity = createDoubleFBO(this.gl, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering)
else this.velocity = this.resizeDoubleFBO(this.velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering)
this.divergence = createFBO(this.gl, simRes.width, simRes.height, r.internalFormat, r.format, texType, this.gl.NEAREST)
this.curl = createFBO(this.gl, simRes.width, simRes.height, r.internalFormat, r.format, texType, this.gl.NEAREST)
this.pressure = createDoubleFBO(this.gl, simRes.width, simRes.height, r.internalFormat, r.format, texType, this.gl.NEAREST)
this.initBloomFrameBuffers()
this.initSunraysFrameBuffers()
}
initBloomFrameBuffers() {
let res = getResolution(this.gl, config.BLOOM_RESOLUTION)
const texType = this.ext.halfFloatTexType
const rgba = this.ext.formatRGBA
const filtering = this.ext.supportLinearFiltering ? this.gl.LINEAR : this.gl.NEAREST
this.bloom = createFBO(this.gl, res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering)
this.bloomFrameBuffers.length = 0
for (let i = 0; i < config.BLOOM_ITERATIONS; i++) {
let width = res.width >> (i + 1)
let height = res.height >> (i + 1)
if (width < 2 || height < 2) break
let fbo = createFBO(this.gl, width, height, rgba.internalFormat, rgba.format, texType, filtering)
this.bloomFrameBuffers.push(fbo)
}
}
initSunraysFrameBuffers() {
let res = getResolution(this.gl, config.SUNRAYS_RESOLUTION)
const texType = this.ext.halfFloatTexType
const r = this.ext.formatR
const filtering = this.ext.supportLinearFiltering ? this.gl.LINEAR : this.gl.NEAREST
this.sunrays = createFBO(this.gl, res.width, res.height, r.internalFormat, r.format, texType, filtering)
this.sunraysTemp = createFBO(this.gl, res.width, res.height, r.internalFormat, r.format, texType, filtering)
}
resizeFBO(target, w, h, internalFormat, format, type, param) {
let newFBO = createFBO(this.gl, w, h, internalFormat, format, type, param)
this.factory.copyProgram.bind(this.gl)
this.gl.uniform1i(this.factory.copyProgram.uniforms.uTexture, target.attach(0))
this.blit(newFBO.fbo)
return newFBO
}
resizeDoubleFBO(target, w, h, internalFormat, format, type, param) {
if (target.width === w && target.height === h) return target
target.read = this.resizeFBO(target.read, w, h, internalFormat, format, type, param)
target.write = createFBO(this.gl, w, h, internalFormat, format, type, param)
target.width = w
target.height = h
target.texelSizeX = 1.0 / w
target.texelSizeY = 1.0 / h
return target
}
updateKeywords() {
const displayKeywords = []
config.SHADING && displayKeywords.push('SHADING')
config.BLOOM && displayKeywords.push('BLOOM')
config.SUNRAYS && displayKeywords.push('SUNRAYS')
this.factory.displayMaterial.setKeywords(this.gl, displayKeywords)
}
calcDeltaTime() {
const now = Date.now()
this.lastUpdateTime = now
return Math.min((now - this.lastUpdateTime) / 1000, 0.016666)
}
updateColors(dt) {
if (!config.COLORFUL) return
this.colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED
if (this.colorUpdateTimer >= 1) {
this.colorUpdateTimer = wrap(this.colorUpdateTimer, 0, 1)
this.pointers.forEach(p => p.color = generateColor())
}
}
applyInputs() {
if (this.splatStack.length > 0) this.multipleSplats(this.splatStack.pop())
this.pointers.forEach(p => {
if (p.moved) {
p.moved = false
this.splatPointer(p)
}
})
}
step(dt) {
const gl = this.gl
gl.disable(gl.BLEND)
gl.viewport(0, 0, this.velocity.width, this.velocity.height)
this.factory.curlProgram.bind(gl)
gl.uniform2f(this.factory.curlProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY)
gl.uniform1i(this.factory.curlProgram.uniforms.uVelocity, this.velocity.read.attach(0))
this.blit(this.curl.fbo)
this.factory.vorticityProgram.bind(gl)
gl.uniform2f(this.factory.vorticityProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY)
gl.uniform1i(this.factory.vorticityProgram.uniforms.uVelocity, this.velocity.read.attach(0))
gl.uniform1i(this.factory.vorticityProgram.uniforms.uCurl, this.curl.attach(1))
gl.uniform1f(this.factory.vorticityProgram.uniforms.curl, config.CURL)
gl.uniform1f(this.factory.vorticityProgram.uniforms.dt, dt)
this.blit(this.velocity.write.fbo)
this.velocity.swap()
this.factory.divergenceProgram.bind(gl)
gl.uniform2f(this.factory.divergenceProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY)
gl.uniform1i(this.factory.divergenceProgram.uniforms.uVelocity, this.velocity.read.attach(0))
this.blit(this.divergence.fbo)
this.factory.clearProgram.bind(gl)
gl.uniform1i(this.factory.clearProgram.uniforms.uTexture, this.pressure.read.attach(0))
gl.uniform1f(this.factory.clearProgram.uniforms.value, config.PRESSURE)
this.blit(this.pressure.write.fbo)
this.pressure.swap()
this.factory.pressureProgram.bind(gl)
gl.uniform2f(this.factory.pressureProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY)
gl.uniform1i(this.factory.pressureProgram.uniforms.uDivergence, this.divergence.attach(0))
for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) {
gl.uniform1i(this.factory.pressureProgram.uniforms.uPressure, this.pressure.read.attach(1))
this.blit(this.pressure.write.fbo)
this.pressure.swap()
}
this.factory.gradienSubtractProgram.bind(gl)
gl.uniform2f(this.factory.gradienSubtractProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY)
gl.uniform1i(this.factory.gradienSubtractProgram.uniforms.uPressure, this.pressure.read.attach(0))
gl.uniform1i(this.factory.gradienSubtractProgram.uniforms.uVelocity, this.velocity.read.attach(1))
this.blit(this.velocity.write.fbo)
this.velocity.swap()
this.factory.advectionProgram.bind(gl)
gl.uniform2f(this.factory.advectionProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY)
if (!this.ext.supportLinearFiltering) {
gl.uniform2f(this.factory.advectionProgram.uniforms.dyeTexelSize, this.velocity.texelSizeX, this.velocity.texelSizeY)
}
let velocityId = this.velocity.read.attach(0)
gl.uniform1i(this.factory.advectionProgram.uniforms.uVelocity, velocityId)
gl.uniform1i(this.factory.advectionProgram.uniforms.uSource, velocityId)
gl.uniform1f(this.factory.advectionProgram.uniforms.dt, dt)
gl.uniform1f(this.factory.advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION)
this.blit(this.velocity.write.fbo)
this.velocity.swap()
gl.viewport(0, 0, this.dye.width, this.dye.height)
if (!this.ext.supportLinearFiltering) {
gl.uniform2f(this.factory.advectionProgram.uniforms.dyeTexelSize, this.dye.texelSizeX, this.dye.texelSizeY)
}
gl.uniform1i(this.factory.advectionProgram.uniforms.uVelocity, this.velocity.read.attach(0))
gl.uniform1i(this.factory.advectionProgram.uniforms.uSource, this.dye.read.attach(1))
gl.uniform1f(this.factory.advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION)
this.blit(this.dye.write.fbo)
this.dye.swap()
}
render(target) {
const gl = this.gl
if (config.BLOOM) {
this.applyBloom(this.dye.read, this.bloom)
}
if (config.SUNRAYS) {
this.applySunrays(this.dye.read, this.dye.write, this.sunrays)
this.blur(this.sunrays, this.sunraysTemp, 1)
}
if (target == null || !config.TRANSPARENT) {
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
gl.enable(gl.BLEND)
}
else gl.disable(gl.BLEND)
let width = target == null ? gl.drawingBufferWidth : target.width
let height = target == null ? gl.drawingBufferHeight : target.height
gl.viewport(0, 0, width, height)
let fbo = target == null ? null : target.fbo
if (!config.TRANSPARENT) this.drawColor(fbo, normalizeColor(config.BACK_COLOR))
if (target == null && config.TRANSPARENT) this.drawCheckerboard(fbo)
this.drawDisplay(fbo, width, height)
}
drawColor(fbo, color) {
this.factory.colorProgram.bind(this.gl)
this.gl.uniform4f(this.factory.colorProgram.uniforms.color, color.r, color.g, color.b, 1)
this.blit(fbo)
}
drawCheckerboard(fbo) {
this.factory.checkerboardProgram.bind(this.gl)
this.gl.uniform1f(this.factory.checkerboardProgram.uniforms.aspectRatio, this.canvas.width / this.canvas.height)
this.blit(fbo)
}
drawDisplay(fbo, width, height) {
const gl = this.gl
this.factory.displayMaterial.bind(gl)
if (config.SHADING) gl.uniform2f(this.factory.displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height)
gl.uniform1i(this.factory.displayMaterial.uniforms.uTexture, this.dye.read.attach(0))
if (config.BLOOM) {
gl.uniform1i(this.factory.displayMaterial.uniforms.uBloom, this.bloom.attach(1))
}
if (config.SUNRAYS) gl.uniform1i(this.factory.displayMaterial.uniforms.uSunrays, this.sunrays.attach(3))
this.blit(fbo)
}
applyBloom(source, destination) {
if (this.bloomFrameBuffers.length < 2) return
const gl = this.gl
let last = destination
gl.disable(gl.BLEND)
this.factory.bloomPrefilterProgram.bind(gl)
let knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001
let curve0 = config.BLOOM_THRESHOLD - knee
let curve1 = knee * 2
let curve2 = 0.25 / knee
gl.uniform3f(this.factory.bloomPrefilterProgram.uniforms.curve, curve0, curve1, curve2)
gl.uniform1f(this.factory.bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD)
gl.uniform1i(this.factory.bloomPrefilterProgram.uniforms.uTexture, source.attach(0))
gl.viewport(0, 0, last.width, last.height)
this.blit(last.fbo)
this.factory.bloomBlurProgram.bind(gl)
for (let i = 0; i < this.bloomFrameBuffers.length; i++) {
let dest = this.bloomFrameBuffers[i]
gl.uniform2f(this.factory.bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY)
gl.uniform1i(this.factory.bloomBlurProgram.uniforms.uTexture, last.attach(0))
gl.viewport(0, 0, dest.width, dest.height)
this.blit(dest.fbo)
last = dest
}
gl.blendFunc(gl.ONE, gl.ONE)
gl.enable(gl.BLEND)
for (let i = this.bloomFrameBuffers.length - 2; i >= 0; i--) {
let baseTex = this.bloomFrameBuffers[i]
gl.uniform2f(this.factory.bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY)
gl.uniform1i(this.factory.bloomBlurProgram.uniforms.uTexture, last.attach(0))
gl.viewport(0, 0, baseTex.width, baseTex.height)
this.blit(baseTex.fbo)
last = baseTex
}
gl.disable(gl.BLEND)
this.factory.bloomFinalProgram.bind(gl)
gl.uniform2f(this.factory.bloomFinalProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY)
gl.uniform1i(this.factory.bloomFinalProgram.uniforms.uTexture, last.attach(0))
gl.uniform1f(this.factory.bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY)
gl.viewport(0, 0, destination.width, destination.height)
this.blit(destination.fbo)
}
applySunrays(source, mask, destination) {
const gl = this.gl
gl.disable(gl.BLEND)
this.factory.sunraysMaskProgram.bind(gl)
gl.uniform1i(this.factory.sunraysMaskProgram.uniforms.uTexture, source.attach(0))
gl.viewport(0, 0, mask.width, mask.height)
this.blit(mask.fbo)
this.factory.sunraysProgram.bind(gl)
gl.uniform1f(this.factory.sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT)
gl.uniform1i(this.factory.sunraysProgram.uniforms.uTexture, mask.attach(0))
gl.viewport(0, 0, destination.width, destination.height)
this.blit(destination.fbo)
}
blur(target, temp, iterations) {
const gl = this.gl
this.factory.blurProgram.bind(gl)
for (let i = 0; i < iterations; i++) {
gl.uniform2f(this.factory.blurProgram.uniforms.texelSize, target.texelSizeX, 0.0)
gl.uniform1i(this.factory.blurProgram.uniforms.uTexture, target.attach(0))
this.blit(temp.fbo)
gl.uniform2f(this.factory.blurProgram.uniforms.texelSize, 0.0, target.texelSizeY)
gl.uniform1i(this.factory.blurProgram.uniforms.uTexture, temp.attach(0))
this.blit(target.fbo)
}
}
splatPointer(pointer) {
let dx = pointer.deltaX * config.SPLAT_FORCE
let dy = pointer.deltaY * config.SPLAT_FORCE
this.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color)
}
multipleSplats(amount) {
for (let i = 0; i < amount; i++) {
const color = generateColor()
color.r *= 10.0
color.g *= 10.0
color.b *= 10.0
const x = Math.random()
const y = Math.random()
const dx = 1000 * (Math.random() - 0.5)
const dy = 1000 * (Math.random() - 0.5)
this.splat(x, y, dx, dy, color)
}
}
splat(x, y, dx, dy, color) {
const gl = this.gl
gl.viewport(0, 0, this.velocity.width, this.velocity.height)
this.factory.splatProgram.bind(gl)
gl.uniform1i(this.factory.splatProgram.uniforms.uTarget, this.velocity.read.attach(0))
gl.uniform1f(this.factory.splatProgram.uniforms.aspectRatio, this.canvas.width / this.canvas.height)
gl.uniform2f(this.factory.splatProgram.uniforms.point, x, y)
gl.uniform3f(this.factory.splatProgram.uniforms.color, dx, dy, 0.0)
gl.uniform1f(this.factory.splatProgram.uniforms.radius, this.correctRadius(config.SPLAT_RADIUS / 100.0))
this.blit(this.velocity.write.fbo)
this.velocity.swap()
gl.viewport(0, 0, this.dye.width, this.dye.height)
gl.uniform1i(this.factory.splatProgram.uniforms.uTarget, this.dye.read.attach(0))
gl.uniform3f(this.factory.splatProgram.uniforms.color, color.r, color.g, color.b)
this.blit(this.dye.write.fbo)
this.dye.swap()
}
correctRadius(radius) {
const aspectRatio = this.canvas.width / this.canvas.height
if (aspectRatio > 1) radius *= aspectRatio
return radius
}
captureScreenShot() {
const gl = this.gl
let res = getResolution(gl, config.CAPTURE_RESOLUTION)
let target = createFBO(gl, res.width, res.height, this.ext.formatRGBA.internalFormat, this.ext.formatRGBA.format, this.ext.halfFloatTexType, gl.NEAREST)
this.render(target)
let texture = framebufferToTexture(gl, target)
texture = normalizeTexture(texture, target.width, target.height)
const dataUri = textureToCanvas(texture, target.width, target.height).toDataURL()
downloadURI('fluid.png', dataUri)
URL.revokeObjectURL(dataUri)
}
}

@ -1,575 +0,0 @@
import {compileShader} from "./util"
const baseVertexShader = (gl) => {
return compileShader(gl,
gl.VERTEX_SHADER,
`
precision highp float;
attribute vec2 aPosition;
varying vec2 vUv;
varying vec2 vL;
varying vec2 vR;
varying vec2 vT;
varying vec2 vB;
uniform vec2 texelSize;
void main () {
vUv = aPosition * 0.5 + 0.5;
vL = vUv - vec2(texelSize.x, 0.0);
vR = vUv + vec2(texelSize.x, 0.0);
vT = vUv + vec2(0.0, texelSize.y);
vB = vUv - vec2(0.0, texelSize.y);
gl_Position = vec4(aPosition, 0.0, 1.0);
}`
)
}
const blurVertexShader = (gl) => {
return compileShader(gl,
gl.VERTEX_SHADER,
`
precision highp float;
attribute vec2 aPosition;
varying vec2 vUv;
varying vec2 vL;
varying vec2 vR;
uniform vec2 texelSize;
void main () {
vUv = aPosition * 0.5 + 0.5;
float offset = 1.33333333;
vL = vUv - texelSize * offset;
vR = vUv + texelSize * offset;
gl_Position = vec4(aPosition, 0.0, 1.0);
}`
)
}
const blurShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying vec2 vUv;
varying vec2 vL;
varying vec2 vR;
uniform sampler2D uTexture;
void main () {
vec4 sum = texture2D(uTexture, vUv) * 0.29411764;
sum += texture2D(uTexture, vL) * 0.35294117;
sum += texture2D(uTexture, vR) * 0.35294117;
gl_FragColor = sum;
}`
)
}
const copyShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
uniform sampler2D uTexture;
void main () {
gl_FragColor = texture2D(uTexture, vUv);
}`
)
}
const clearShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
uniform sampler2D uTexture;
uniform float value;
void main () {
gl_FragColor = value * texture2D(uTexture, vUv);
}`
)
}
const colorShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
uniform vec4 color;
void main () {
gl_FragColor = color;
}`
)
}
const checkerboardShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
uniform sampler2D uTexture;
uniform float aspectRatio;
#define SCALE 25.0
void main () {
vec2 uv = floor(vUv * SCALE * vec2(aspectRatio, 1.0));
float v = mod(uv.x + uv.y, 2.0);
v = v * 0.1 + 0.8;
gl_FragColor = vec4(vec3(v), 1.0);
}
`
)
}
const displayShaderSource = (gl) => {
return `
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
varying vec2 vL;
varying vec2 vR;
varying vec2 vT;
varying vec2 vB;
uniform sampler2D uTexture;
uniform sampler2D uBloom;
uniform sampler2D uSunrays;
uniform sampler2D uDithering;
uniform vec2 ditherScale;
uniform vec2 texelSize;
vec3 linearToGamma (vec3 color) {
color = max(color, vec3(0));
return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0));
}
void main () {
vec3 c = texture2D(uTexture, vUv).rgb;
#ifdef SHADING
vec3 lc = texture2D(uTexture, vL).rgb;
vec3 rc = texture2D(uTexture, vR).rgb;
vec3 tc = texture2D(uTexture, vT).rgb;
vec3 bc = texture2D(uTexture, vB).rgb;
float dx = length(rc) - length(lc);
float dy = length(tc) - length(bc);
vec3 n = normalize(vec3(dx, dy, length(texelSize)));
vec3 l = vec3(0.0, 0.0, 1.0);
float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0);
c *= diffuse;
#endif
#ifdef BLOOM
vec3 bloom = texture2D(uBloom, vUv).rgb;
#endif
#ifdef SUNRAYS
float sunrays = texture2D(uSunrays, vUv).r;
c *= sunrays;
#ifdef BLOOM
bloom *= sunrays;
#endif
#endif
#ifdef BLOOM
float noise = texture2D(uDithering, vUv * ditherScale).r;
noise = noise * 2.0 - 1.0;
bloom += noise / 255.0;
bloom = linearToGamma(bloom);
c += bloom;
#endif
float a = max(c.r, max(c.g, c.b));
gl_FragColor = vec4(c, a);
}`
}
const bloomPrefilterShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying vec2 vUv;
uniform sampler2D uTexture;
uniform vec3 curve;
uniform float threshold;
void main () {
vec3 c = texture2D(uTexture, vUv).rgb;
float br = max(c.r, max(c.g, c.b));
float rq = clamp(br - curve.x, 0.0, curve.y);
rq = curve.z * rq * rq;
c *= max(rq, br - threshold) / max(br, 0.0001);
gl_FragColor = vec4(c, 0.0);
}`
)
}
const bloomBlurShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying vec2 vL;
varying vec2 vR;
varying vec2 vT;
varying vec2 vB;
uniform sampler2D uTexture;
void main () {
vec4 sum = vec4(0.0);
sum += texture2D(uTexture, vL);
sum += texture2D(uTexture, vR);
sum += texture2D(uTexture, vT);
sum += texture2D(uTexture, vB);
sum *= 0.25;
gl_FragColor = sum;
}`
)
}
const bloomFinalShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying vec2 vL;
varying vec2 vR;
varying vec2 vT;
varying vec2 vB;
uniform sampler2D uTexture;
uniform float intensity;
void main () {
vec4 sum = vec4(0.0);
sum += texture2D(uTexture, vL);
sum += texture2D(uTexture, vR);
sum += texture2D(uTexture, vT);
sum += texture2D(uTexture, vB);
sum *= 0.25;
gl_FragColor = sum * intensity;
}`
)
}
const sunraysMaskShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
uniform sampler2D uTexture;
void main () {
vec4 c = texture2D(uTexture, vUv);
float br = max(c.r, max(c.g, c.b));
c.a = 1.0 - min(max(br * 20.0, 0.0), 0.8);
gl_FragColor = c;
}`
)
}
const sunraysShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
uniform sampler2D uTexture;
uniform float weight;
#define ITERATIONS 16
void main () {
float Density = 0.3;
float Decay = 0.95;
float Exposure = 0.7;
vec2 coord = vUv;
vec2 dir = vUv - 0.5;
dir *= 1.0 / float(ITERATIONS) * Density;
float illuminationDecay = 1.0;
float color = texture2D(uTexture, vUv).a;
for (int i = 0; i < ITERATIONS; i++)
{
coord -= dir;
float col = texture2D(uTexture, coord).a;
color += col * illuminationDecay * weight;
illuminationDecay *= Decay;
}
gl_FragColor = vec4(color * Exposure, 0.0, 0.0, 1.0);
}`
)
}
const splatShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
uniform sampler2D uTarget;
uniform float aspectRatio;
uniform vec3 color;
uniform vec2 point;
uniform float radius;
void main () {
vec2 p = vUv - point.xy;
p.x *= aspectRatio;
vec3 splat = exp(-dot(p, p) / radius) * color;
vec3 base = texture2D(uTarget, vUv).xyz;
gl_FragColor = vec4(base + splat, 1.0);
}`
)
}
const advectionShader = (gl, ext) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
uniform sampler2D uVelocity;
uniform sampler2D uSource;
uniform vec2 texelSize;
uniform vec2 dyeTexelSize;
uniform float dt;
uniform float dissipation;
vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) {
vec2 st = uv / tsize - 0.5;
vec2 iuv = floor(st);
vec2 fuv = fract(st);
vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize);
vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize);
vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize);
vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize);
return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y);
}
void main () {
#ifdef MANUAL_FILTERING
vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize;
vec4 result = bilerp(uSource, coord, dyeTexelSize);
#else
vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;
vec4 result = texture2D(uSource, coord);
#endif
float decay = 1.0 + dissipation * dt;
gl_FragColor = result / decay;
}`,
ext.supportLinearFiltering ? null : ['MANUAL_FILTERING']
)
}
const divergenceShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
varying highp vec2 vL;
varying highp vec2 vR;
varying highp vec2 vT;
varying highp vec2 vB;
uniform sampler2D uVelocity;
void main () {
float L = texture2D(uVelocity, vL).x;
float R = texture2D(uVelocity, vR).x;
float T = texture2D(uVelocity, vT).y;
float B = texture2D(uVelocity, vB).y;
vec2 C = texture2D(uVelocity, vUv).xy;
if (vL.x < 0.0) { L = -C.x; }
if (vR.x > 1.0) { R = -C.x; }
if (vT.y > 1.0) { T = -C.y; }
if (vB.y < 0.0) { B = -C.y; }
float div = 0.5 * (R - L + T - B);
gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
}`
)
}
const curlShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
varying highp vec2 vL;
varying highp vec2 vR;
varying highp vec2 vT;
varying highp vec2 vB;
uniform sampler2D uVelocity;
void main () {
float L = texture2D(uVelocity, vL).y;
float R = texture2D(uVelocity, vR).y;
float T = texture2D(uVelocity, vT).x;
float B = texture2D(uVelocity, vB).x;
float vorticity = R - L - T + B;
gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
}`
)
}
const vorticityShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision highp float;
precision highp sampler2D;
varying vec2 vUv;
varying vec2 vL;
varying vec2 vR;
varying vec2 vT;
varying vec2 vB;
uniform sampler2D uVelocity;
uniform sampler2D uCurl;
uniform float curl;
uniform float dt;
void main () {
float L = texture2D(uCurl, vL).x;
float R = texture2D(uCurl, vR).x;
float T = texture2D(uCurl, vT).x;
float B = texture2D(uCurl, vB).x;
float C = texture2D(uCurl, vUv).x;
vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
force /= length(force) + 0.0001;
force *= curl * C;
force.y *= -1.0;
vec2 vel = texture2D(uVelocity, vUv).xy;
gl_FragColor = vec4(vel + force * dt, 0.0, 1.0);
}`
)
}
const pressureShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
varying highp vec2 vL;
varying highp vec2 vR;
varying highp vec2 vT;
varying highp vec2 vB;
uniform sampler2D uPressure;
uniform sampler2D uDivergence;
void main () {
float L = texture2D(uPressure, vL).x;
float R = texture2D(uPressure, vR).x;
float T = texture2D(uPressure, vT).x;
float B = texture2D(uPressure, vB).x;
float C = texture2D(uPressure, vUv).x;
float divergence = texture2D(uDivergence, vUv).x;
float pressure = (L + R + B + T - divergence) * 0.25;
gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
}`
)
}
const gradientSubtractShader = (gl) => {
return compileShader(gl,
gl.FRAGMENT_SHADER,
`
precision mediump float;
precision mediump sampler2D;
varying highp vec2 vUv;
varying highp vec2 vL;
varying highp vec2 vR;
varying highp vec2 vT;
varying highp vec2 vB;
uniform sampler2D uPressure;
uniform sampler2D uVelocity;
void main () {
float L = texture2D(uPressure, vL).x;
float R = texture2D(uPressure, vR).x;
float T = texture2D(uPressure, vT).x;
float B = texture2D(uPressure, vB).x;
vec2 velocity = texture2D(uVelocity, vUv).xy;
velocity.xy -= vec2(R - L, T - B);
gl_FragColor = vec4(velocity, 0.0, 1.0);
}`
)
}
export {
baseVertexShader,
blurVertexShader,
blurShader,
copyShader,
clearShader,
colorShader,
checkerboardShader,
displayShaderSource,
bloomPrefilterShader,
bloomBlurShader,
bloomFinalShader,
sunraysMaskShader,
sunraysShader,
splatShader,
advectionShader,
divergenceShader,
curlShader,
vorticityShader,
pressureShader,
gradientSubtractShader
}

@ -1,387 +0,0 @@
function getWebGLContext(canvas) {
const params = {alpha: true, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false}
let gl = canvas.getContext('webgl2', params)
const isWebGL2 = !!gl
if (!isWebGL2) gl = canvas.getContext('webgl', params) || canvas.getContext('experimental-webgl', params)
let halfFloat
let supportLinearFiltering
if (isWebGL2) {
gl.getExtension('EXT_color_buffer_float')
supportLinearFiltering = gl.getExtension('OES_texture_float_linear')
}
else {
halfFloat = gl.getExtension('OES_texture_half_float')
supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear')
}
gl.clearColor(0.0, 0.0, 0.0, 1.0)
const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES
let formatRGBA
let formatRG
let formatR
if (isWebGL2) {
formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType)
formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType)
formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType)
}
else {
formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType)
formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType)
formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType)
}
return {
gl,
ext: {
formatRGBA,
formatRG,
formatR,
halfFloatTexType,
supportLinearFiltering
}
}
}
function getSupportedFormat(gl, internalFormat, format, type) {
if (!supportRenderTextureFormat(gl, internalFormat, format, type)) {
switch (internalFormat) {
case gl.R16F:
return getSupportedFormat(gl, gl.RG16F, gl.RG, type)
case gl.RG16F:
return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type)
default:
return null
}
}
return {internalFormat, format}
}
function supportRenderTextureFormat(gl, internalFormat, format, type) {
let texture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null)
let fbo = gl.createFramebuffer()
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER)
return status === gl.FRAMEBUFFER_COMPLETE
}
function isMobile() {
return /Mobi|Android/i.test(navigator.userAgent)
}
function addKeywords(source, keywords) {
if (keywords == null) return source
let keywordsString = ''
keywords.forEach(keyword => {
keywordsString += '#define ' + keyword + '\n'
})
return keywordsString + source
}
function compileShader(gl, type, source, keywords) {
source = addKeywords(source, keywords)
const shader = gl.createShader(type)
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw gl.getShaderInfoLog(shader)
return shader
}
function createProgram(gl, vertexShader, fragmentShader) {
let program = gl.createProgram()
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw gl.getProgramInfoLog(program)
return program
}
function getUniforms(gl, program) {
let uniforms = []
let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS)
for (let i = 0; i < uniformCount; i++) {
let uniformName = gl.getActiveUniform(program, i).name
uniforms[uniformName] = gl.getUniformLocation(program, uniformName)
}
return uniforms
}
function getResolution(gl, resolution) {
let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight
if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio
let min = Math.round(resolution)
let max = Math.round(resolution * aspectRatio)
if (gl.drawingBufferWidth > gl.drawingBufferHeight) return {width: max, height: min}
else return {width: min, height: max}
}
function clamp01(input) {
return Math.min(Math.max(input, 0), 1)
}
function normalizeTexture(texture, width, height) {
let result = new Uint8Array(texture.length)
let id = 0
for (let i = height - 1; i >= 0; i--) {
for (let j = 0; j < width; j++) {
let nid = i * width * 4 + j * 4
result[nid] = clamp01(texture[id]) * 255
result[nid + 1] = clamp01(texture[id + 1]) * 255
result[nid + 2] = clamp01(texture[id + 2]) * 255
result[nid + 3] = clamp01(texture[id + 3]) * 255
id += 4
}
}
return result
}
function framebufferToTexture(gl, target) {
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo)
let length = target.width * target.height * 4
let texture = new Float32Array(length)
gl.readPixels(0, 0, target.width, target.height, gl.RGBA, gl.FLOAT, texture)
return texture
}
function textureToCanvas(texture, width, height) {
let captureCanvas = document.createElement('canvas')
let ctx = captureCanvas.getContext('2d')
captureCanvas.width = width
captureCanvas.height = height
let imageData = ctx.createImageData(width, height)
imageData.data.set(texture)
ctx.putImageData(imageData, 0, 0)
return captureCanvas
}
function downloadURI(filename, uri) {
let link = document.createElement('a')
link.download = filename
link.href = uri
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
function createFBO(gl, w, h, internalFormat, format, type, param) {
gl.activeTexture(gl.TEXTURE0)
let texture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null)
let fbo = gl.createFramebuffer()
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
gl.viewport(0, 0, w, h)
gl.clear(gl.COLOR_BUFFER_BIT)
let texelSizeX = 1.0 / w
let texelSizeY = 1.0 / h
return {
texture, fbo, width: w, height: h, texelSizeX, texelSizeY, attach(id) {
gl.activeTexture(gl.TEXTURE0 + id)
gl.bindTexture(gl.TEXTURE_2D, texture)
return id
}
}
}
function createDoubleFBO(gl, w, h, internalFormat, format, type, param) {
let fbo1 = createFBO(gl, w, h, internalFormat, format, type, param)
let fbo2 = createFBO(gl, w, h, internalFormat, format, type, param)
return {
width: w,
height: h,
texelSizeX: fbo1.texelSizeX,
texelSizeY: fbo1.texelSizeY,
get read() {
return fbo1
},
set read(value) {
fbo1 = value
},
get write() {
return fbo2
},
set write(value) {
fbo2 = value
},
swap() {
let temp = fbo1
fbo1 = fbo2
fbo2 = temp
}
}
}
function HSVtoRGB(h, s, v) {
let r, g, b, i, f, p, q, t
i = Math.floor(h * 6)
f = h * 6 - i
p = v * (1 - s)
q = v * (1 - f * s)
t = v * (1 - (1 - f) * s)
switch (i % 6) {
case 0:
r = v
g = t
b = p
break
case 1:
r = q
g = v
b = p
break
case 2:
r = p
g = v
b = t
break
case 3:
r = p
g = q
b = v
break
case 4:
r = t
g = p
b = v
break
case 5:
r = v
g = p
b = q
break
}
return {r, g, b}
}
function generateColor() {
let c = HSVtoRGB(Math.random(), 1.0, 1.0)
c.r *= 0.15
c.g *= 0.15
c.b *= 0.15
return c
}
function scaleByPixelRatio(input) {
let pixelRatio = window.devicePixelRatio || 1
return Math.floor(input * pixelRatio)
}
function wrap(value, min, max) {
let range = max - min
if (range === 0) return min
return ((value - min) % range) + min
}
function normalizeColor(input) {
return {
r: input.r / 255,
g: input.g / 255,
b: input.b / 255
}
}
function correctDeltaX(canvas, delta) {
let aspectRatio = canvas.width / canvas.height
if (aspectRatio < 1) delta *= aspectRatio
return delta
}
function correctDeltaY(canvas, delta) {
let aspectRatio = canvas.width / canvas.height
if (aspectRatio > 1) delta /= aspectRatio
return delta
}
function updatePointerDownData(canvas, pointer, id, posX, posY) {
pointer.id = id
pointer.down = true
pointer.moved = false
pointer.texcoordX = posX / canvas.width
pointer.texcoordY = 1.0 - posY / canvas.height
pointer.prevTexcoordX = pointer.texcoordX
pointer.prevTexcoordY = pointer.texcoordY
pointer.deltaX = 0
pointer.deltaY = 0
pointer.color = generateColor()
}
function updatePointerMoveData(canvas, pointer, posX, posY) {
pointer.moved = pointer.down
pointer.prevTexcoordX = pointer.texcoordX
pointer.prevTexcoordY = pointer.texcoordY
pointer.texcoordX = posX / canvas.width
pointer.texcoordY = 1.0 - posY / canvas.height
pointer.deltaX = correctDeltaX(canvas, pointer.texcoordX - pointer.prevTexcoordX)
pointer.deltaY = correctDeltaY(canvas, pointer.texcoordY - pointer.prevTexcoordY)
}
function updatePointerUpData(pointer) {
pointer.down = false
}
function resizeCanvas(canvas) {
let width = scaleByPixelRatio(canvas.clientWidth)
let height = scaleByPixelRatio(canvas.clientHeight)
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width
canvas.height = height
return true
}
return false
}
export {
getWebGLContext,
isMobile,
compileShader,
createProgram,
getUniforms,
getResolution,
normalizeTexture,
framebufferToTexture,
textureToCanvas,
downloadURI,
createFBO,
createDoubleFBO,
generateColor,
scaleByPixelRatio,
wrap,
normalizeColor,
updatePointerDownData,
updatePointerMoveData,
updatePointerUpData,
resizeCanvas,
}

@ -6,12 +6,6 @@ const router = [
component: 'admin/index/',
name: 'index',
meta: {title: '首页', affix: true, icon: 'svg-home', noAuth: true, pageHeader: false, sort: 0}
},
{
path: 'user',
name: 'userCenter',
component: 'admin/userCenter/',
meta: {title: '个人中心', noCache: true, icon: 'svg-user', noAuth: true, pageHeader: false, hidden: true},
}
]

@ -1,22 +0,0 @@
/*路由表:消息中心*/
const router = {
path: 'message',
meta: {title: '消息中心', icon: 'svg-message', alwaysShow: true},
children: [
{
path: 'manage',
name: 'messageManagement',
component: 'admin/message/manage/',
meta: {title: '消息管理'}
},
{
path: 'user',
name: 'userMessage',
component: 'admin/message/user/',
meta: {title: '个人消息', noAuth: true, noCache: true}
}
]
}
export default router

@ -1,68 +0,0 @@
/*路由表:采购管理*/
const router = {
path: 'purchase',
meta: {title: '采购管理', icon: 'svg-shopping', alwaysShow: true},
children: [
{
path: 'order',
name: 'purchaseOrder',
component: 'admin/purchase/order/',
meta: {title: '采购订单'}
},
{
path: 'order/detail/:type(see|add|edit)/:id?',
component: 'admin/purchase/order/detail',
meta: {
title: '采购订单详情页',
hidden: true,
pageHeader: false,
dynamicTitle(to) {
const {type, id} = to.params
switch (type) {
case 'add':
return '添加采购订单'
case 'edit':
return `编辑采购订单${id}`
case 'see':
return `查看采购订单${id}`
}
},
activeMenu: '/purchase/order',
usePathKey: true,
commonModule: '@/view/admin/purchase/order/detail'
}
},
{
path: 'inbound',
name: 'purchaseInbound',
component: 'admin/purchase/inbound/',
meta: {title: '采购入库'}
},
{
path: 'inbound/detail/:type(see|add|edit)/:id?',
component: 'admin/purchase/inbound/detail',
meta: {
title: '采购入库详情页',
hidden: true,
pageHeader: false,
dynamicTitle(to) {
const {type, id} = to.params
switch (type) {
case 'add':
return '添加采购入库单'
case 'edit':
return `编辑采购入库单${id}`
case 'see':
return `查看采购入库单${id}`
}
},
activeMenu: '/purchase/inbound',
usePathKey: true,
commonModule: '@/view/admin/purchase/inbound/detail'
}
}
]
}
export default router

@ -1,68 +0,0 @@
/*路由表:销售管理*/
const router = {
path: 'sell',
meta: {title: '销售管理', icon: 'svg-sell', alwaysShow: true},
children: [
{
path: 'order',
name: 'sellOrder',
component: 'admin/sell/order/',
meta: {title: '销售订单'}
},
{
path: 'order/detail/:type(see|add|edit)/:id?',
component: 'admin/sell/order/detail',
meta: {
title: '销售订单详情页',
hidden: true,
pageHeader: false,
dynamicTitle(to) {
const {type, id} = to.params
switch (type) {
case 'add':
return '添加销售订单'
case 'edit':
return `编辑销售订单${id}`
case 'see':
return `查看销售订单${id}`
}
},
activeMenu: '/sell/order',
usePathKey: true,
commonModule: '@/view/admin/sell/order/detail'
}
},
{
path: 'outbound',
name: 'sellOutbound',
component: 'admin/sell/outbound/',
meta: {title: '销售出库'}
},
{
path: 'outbound/detail/:type(see|add|edit)/:id?',
component: 'admin/sell/outbound/detail',
meta: {
title: '销售出库详情页',
hidden: true,
pageHeader: false,
dynamicTitle(to) {
const {type, id} = to.params
switch (type) {
case 'add':
return '添加销售出库单'
case 'edit':
return `编辑销售出库单${id}`
case 'see':
return `查看销售出库单${id}`
}
},
activeMenu: '/sell/outbound',
usePathKey: true,
commonModule: '@/view/admin/sell/outbound/detail'
}
}
]
}
export default router

@ -1,16 +0,0 @@
/*路由表:库存管理*/
const router = {
path: 'stock',
meta: {title: '库存管理', icon: 'svg-stock', alwaysShow: true},
children: [
{
path: 'current',
name: 'currentStock',
component: 'admin/stock/current/',
meta: {title: '当前库存'}
}
]
}
export default router

@ -1,52 +0,0 @@
/*路由表:系统管理*/
const router = {
path: 'system',
meta: {title: '系统管理', icon: 'svg-system', alwaysShow: true},
children: [
{
path: 'department',
name: 'departmentManagement',
component: 'admin/system/department/',
meta: {title: '部门管理'}
},
{
path: 'menu',
name: 'menuManagement',
component: 'admin/system/menu/',
meta: {title: '菜单管理', noCache: true}
},
{
path: 'role',
name: 'roleManagement',
component: 'admin/system/role/',
meta: {title: '角色管理'}
},
{
path: 'user',
name: 'userManagement',
component: 'admin/system/user/',
meta: {title: '用户管理'}
},
{
path: 'category',
name: 'categorySetting',
component: 'admin/system/category/',
meta: {title: '商品分类'}
},
{
path: 'customer',
name: 'customerManagement',
component: 'admin/system/customer/',
meta: {title: '客户管理'}
},
{
path: 'supplier',
name: 'supplierManagement',
component: 'admin/system/supplier/',
meta: {title: '供应商管理'}
}
]
}
export default router

@ -1,7 +0,0 @@
import component from '@/view/dev/uploadFileDev'
export default {
path: 'uploadFile',
component,
meta: {title: '上传'}
}

@ -1,11 +0,0 @@
/*顶级菜单:开发用的页面*/
import {context2array} from '@/router/util'
const files = require.context('./child', false, /\.js$/)
export default {
path: '/dev',
component: {template: `<div><div>开发专用页面</div><router-view/></div>`},
meta: {title: '开发专用页面', icon: 'svg-develop', noAuth: true, noCache: true, alwaysShow: true},
children: context2array(files)
}

@ -1,26 +1,11 @@
export default {
path: 'component',
meta: {title: '组件', icon: 'el-icon-s-grid'},
meta: {title: '组件', icon: 'el-icon-s-grid', alwaysShow: true},
children: [
{
path: 'upload',
component: 'example/component/upload',
meta: {title: '上传文件'}
},
{
path: 'skeleton',
component: 'example/component/skeleton',
meta: {title: '骨架屏'}
},
{
path: 'regionSelector',
component: 'example/component/regionSelector',
meta: {title: '行政区划选择'}
},
{
path: 'treeSelect',
component: 'example/component/treeSelect',
meta: {title: '树选择'}
}
]
}

@ -1,16 +0,0 @@
export default {
path: 'cool',
meta: {title: '好玩的东东', icon: 'el-icon-s-opportunity'},
children: [
{
path: 'fluid',
component: 'example/cool/fluid',
meta: {title: '流体动画'}
},
{
path: 'l2d',
component: 'example/cool/l2d',
meta: {title: '看板娘'}
},
]
}

@ -7,11 +7,6 @@ export default {
component: 'example/globalMethod/message',
meta: {title: '消息提示'}
},
{
path: 'bottomTip',
component: 'example/globalMethod/bottomTip',
meta: {title: '底部消息提示'}
},
{
path: 'guide',
component: 'example/globalMethod/guide',

@ -1,12 +1,7 @@
export default {
path: 'test',
meta: {title: '测试页面', icon: 'el-icon-warning'},
meta: {title: '测试页面', icon: 'el-icon-warning', alwaysShow: true},
children: [
{
path: 'develop',
component: 'example/test/develop/',
meta: {title: '开发测试'}
},
{
path: 'cache',
meta: {title: '详情页缓存测试', noCache: false},

@ -1,6 +1,5 @@
import admin from './admin'
import example from './example'
import dev from './dev'
import doc from './doc'
export default [admin, example, dev, doc]
export default [admin, example, doc]

@ -1,24 +0,0 @@
/*
* 数据缓存
* 比如商品分类信息等等
* */
import {createTree} from '@/util/tree'
const state = {
categories: [],
categoryTree: []
}
const mutations = {
categories(state, categories) {
state.categories = categories || []
state.categoryTree = createTree(state.categories)
}
}
export default {
namespaced: true,
state,
mutations
}

@ -1,23 +0,0 @@
import {createMutations} from "@/store/util"
import {search} from '@/api/message/user'
const state = {
unreadCount: 0
}
const mutations = createMutations(state)
const actions = {
refresh({commit}) {
search
.request({page: 1, pageSize: 1, unread: true})
.then(({data}) => commit('unreadCount', data.total))
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

@ -1,28 +0,0 @@
import Vue from "vue"
const state = {
map: {}
}
const mutations = {
init(state, key) {
if (state.map.hasOwnProperty(key)) {
state.map[key] = false
}
else Vue.set(state.map, key, false)
},
emit(state, key) {
state.map[key] = true
},
renew(state, key) {
state.map[key] = false
}
}
export default {
namespaced: true,
state,
mutations
}

@ -1,20 +1,16 @@
import path from 'path'
import {route as routeConfig} from '@/config'
import {mutations as appMutations} from "@/layout/store/app"
import {addDynamicRoutes} from '@/router'
import {getDynamicRoutes} from '@/router/define'
import {str2routeConfig, metaExtend} from "@/router/util"
import {getAll} from "@/api/system/resource"
import {metaExtend} from "@/router/util"
import {isEmpty, deepClone} from "@/util"
import {needAuth, auth} from "@/util/auth" // 此处存在循环引用?
import {createTree, shakeTree} from "@/util/tree"
import {shakeTree} from "@/util/tree"
import {isExternal} from "@/util/validate"
const state = {
//权限映射表:<权限路径,权限id>,用于util.auth
resourceMap: {},
//权限树(当用户非admin时过滤了admin专属权限),用于菜单管理、角色管理中的权限选择
resourceTree: [],
//判断权限是否已经初始化
init: false
@ -22,21 +18,7 @@ const state = {
const mutations = {
resource(state, {data, admin}) {
data = deepClone(data)
state.resourceMap = generateResourceMap(data)
//如果不是admin用户,那么过滤admin类型的权限
if (!admin) data = data.filter(i => !i.admin)
//解析meta,这里不处理meta.dynamicTitle
data.forEach(i => {
if (typeof i.meta === 'string' && !isEmpty(i.meta)) {
i.meta = JSON.parse(i.meta)
}
})
state.resourceTree = createTree(data)
state.resourceMap = {}
},
init(state, sign) {
state.init = sign
@ -45,49 +27,25 @@ const mutations = {
const actions = {
init({state, commit}, {admin}) {
return getAll
.request()
.then(({data}) => {
commit('resource', {data: data || [], admin})
const routes = transformOriginRouteData(data)
metaExtend(routes)
return new Promise(resolve => {
commit('resource', {data: [], admin})
//动态添加路由,这里不需要进行权限过滤
//可能存在多次调用的情况,所以仅在第一次调用时添加进vue-router
!state.init && addDynamicRoutes(routes)
const routes = getDynamicRoutes()
metaExtend(routes)
//生成经过权限过滤后的菜单
appMutations.menus(getAuthorizedMenus(routes))
//动态添加路由,这里不需要进行权限过滤
//可能存在多次调用的情况,所以仅在第一次调用时添加进vue-router
!state.init && addDynamicRoutes(routes)
//设置初始化完成的标志
commit('init', true)
})
}
}
//生成经过权限过滤后的菜单
appMutations.menus(getAuthorizedMenus(routes))
//将后台返回的数据转换为合法的route数组
function transformOriginRouteData(data) {
if (!Array.isArray(data)) return []
//未开启后端动态路由功能时,返回前端预设的静态路由
if (!routeConfig.useBackendDataAsRoute) return getDynamicRoutes()
const treeArray = deepClone(data).filter(i => {
//过滤掉数据接口或者未启用的项
if (i.type === 3 || !i.enable) {
return false
}
//将字符串形式的meta转为合法的js对象
if (typeof i.meta === 'string') {
i.meta = str2routeConfig(i.meta)
}
return true
})
//设置初始化完成的标志
commit('init', true)
return createTree(treeArray)
resolve()
})
}
}
//获取经过权限控制后的菜单
@ -144,57 +102,6 @@ function addFullPath(routes, basePath = '/') {
})
}
//根据权限列表生成哈希表:<权限路径,权限id>
//该方法只处理叶子节点或者是接口类型的节点
function generateResourceMap(resources) {
if (!Array.isArray(resources) || resources.length <= 0) {
return {}
}
resources = deepClone(resources)
const result = {}
//用于加速查找的临时表:<id,resource>
const map = resources.reduce((map, i) => {
//如果是接口类型的节点,直接放入结果表
if (i.type === 3) result[i.path] = i.id
else {
i.children = []
map[i.id] = i
}
return map
}, {})
//获取所有的菜单叶子节点
const menuLeaves = resources.filter(node => {
const parent = map[node.pid]
if (parent) {
parent.children.push(node)
node.parent = parent
}
return node.type === 2
})
//获取叶子菜单的全路径
const getMenuFullPath = menu => {
const parents = [menu.path]
let {parent} = menu
while (parent) {
parents.push(parent.path)
parent = parent.parent
}
return path.resolve(...parents.reverse())
}
//填充结果表
for (const menu of menuLeaves) {
result[getMenuFullPath(menu)] = menu.id
}
return result
}
export default {
namespaced: true,
state,

@ -1,94 +0,0 @@
import {MessageBox} from "element-ui"
import {isEmpty} from "@/util"
import {useMock, socketUrl} from '@/config'
import SocketIO from 'socket.io-client'
import {createMutations} from "@/store/util"
let socket
const state = {
online: true
}
const mutations = createMutations(state)
const actions = {
init(context) {
const {id, token} = context.rootState.user
if (useMock || isEmpty(id, token)) return
socket = initSocket({id, token})
bindDefaultEvent(socket, context)
bindCustomEvent(socket, context)
},
close() {
socket && socket.close()
}
}
function initSocket({id, token}) {
return new SocketIO(socketUrl, {query: {id, token}, transports: ['websocket']})
}
function bindDefaultEvent(socket, {state, commit, dispatch}) {
socket.on('connect', () => {
commit('online', true)
console.log('socket连接成功')
})
socket.on('disconnect', (reason) => {
commit('online', false)
if (reason === 'io server disconnect') {
return console.log('服务端关闭了socket连接')
}
console.log('socket失去连接')
})
socket.on('reconnecting', (attemptNumber) => {
commit('online', false)
console.log(`socket第${attemptNumber}次重连中...`)
})
socket.on('reconnect', (attemptNumber) => {
commit('online', true)
console.log(`socket第${attemptNumber}次重连成功`)
})
socket.on('reconnect_failed', () => {
// ...
})
socket.on('reconnect_error', (error) => {
// ...
})
socket.on('error', (error) => {
// ...
})
}
function bindCustomEvent(socket, {state, commit, dispatch, rootState}) {
socket.on('logout', msg => {
if (rootState.user.prepareLogout) return
MessageBox.alert(msg || '你已被强制下线,请重新登录', {
type: 'warning',
beforeClose: (action, instance, done) => {
dispatch('user/logout', null, {root: true}).finally(done)
}
})
})
socket.on('new-message', () => {
dispatch('message/refresh', null, {root: true})
})
}
export default {
namespaced: true,
state,
mutations,
actions
}

@ -1,9 +1,8 @@
import {createMutations} from "@/store/util"
import {emptyOrDefault} from "@/util"
import {autoCompleteUrl} from "@/util/file"
import {getUser, setUser} from "@/util/storage"
import {mutations as tagsViewMutations} from "@/layout/store/tagsView"
import {login, logout} from '@/api/account'
import {elError} from "@/util/message"
//刷新时从本地存储中获取用户信息
const user = getUser()
@ -15,8 +14,6 @@ const state = {
/*用户基本信息*/
id: emptyOrDefault(user.id, null),
name: emptyOrDefault(user.name),
roleName: emptyOrDefault(user.roleName),
deptName: emptyOrDefault(user.deptName),
avatar: emptyOrDefault(user.avatar),
admin: emptyOrDefault(user.admin, false),
token: emptyOrDefault(user.token),
@ -28,36 +25,29 @@ const mutations = createMutations(state, true)
const actions = {
login({commit, dispatch}, userInfo) {
const {username, password} = userInfo
return login
.request({username: username.trim(), password})
.then(({data: user}) => {
if (user.admin === true) user.roleName = '超级管理员'
user.avatar = autoCompleteUrl(user.avatar)
commit('$all', user)
setUser(user)
return dispatch('socket/init', null, {root: true})
})
if (username !== 'admin') {
elError('登录时请用admin作为用户名!')
return Promise.reject()
}
const user = {id: 1, admin: true, name: username, avatar: null, token: 'token'}
commit('$all', user)
setUser(user)
return Promise.resolve()
},
logout({commit, state, dispatch}) {
return new Promise((resolve, reject) => {
if (state.prepareLogout) return Promise.reject()
commit('prepareLogout', true)
logout
.request(state.token)
.then(() => {
commit('resource/init', false, {root: true})
return Promise.all([
dispatch('socket/close', null, {root: true}),
dispatch('removeUser'),
tagsViewMutations.delAllTagAndCache()
])
})
commit('resource/init', false, {root: true})
Promise.all([
dispatch('removeUser'),
tagsViewMutations.delAllTagAndCache()
])
.then(() => {
resolve()
window.location.reload()
})
.catch(error => reject(error))
.finally(() => commit('prepareLogout', false))
})
},

@ -1,240 +0,0 @@
import request from "@/api/request"
import {isEmpty} from "@/util"
import {elError} from "@/util/message"
import {download} from "@/util/file"
import {flatTree} from "@/util/tree"
/**
* 导出excel的抽象模板
* 响应头为json时进行前端导出否则直接下载文件
*
* @param url {string} 搜索的请求地址
* @param params {*} 向后台发送请求时携带的参数
* @param options 前端导出时需要的配置项
* @param options.columns {array} json2workbook所需参数
* @param options.merge {object} json2workbook所需参数
* @param json2workbook {function} 将json数据转换为workbook的函数参考./exceljs.js #json2workbook
* @param workbook2excel {function} workbook对象转换为excel文件参考./exceljs.js #workbook2excel
*/
export function abstractExportExcel(url, params, options, json2workbook, workbook2excel) {
request({url, method: 'post', responseType: 'blob', data: params})
.then(({headers, data}) => {
const contentType = headers['content-type']
const contentDisposition = headers['content-disposition']
const filename = window.decodeURI(contentDisposition.split('=')[1])
if (contentType.includes('json')) {
const reader = new FileReader()
reader.onload = () => {
const response = JSON.parse(reader.result)
const {columns, merge} = options
const workbook = json2workbook(response.data, columns, merge)
workbook2excel(workbook, filename)
}
reader.readAsText(data)
}
else download(data, filename)
})
.catch(e => elError(e))
}
/**
* 合并excel
* 此方法会修改原始json数据重写序号
*
* @param props {array} 需要合并的字段名数组用于映射json
* @param data {array} json数组
* @param primaryKey {string} 该行json的唯一标识字段名
* @param orderKey {string} 该行json的序号字段名
* @param ignoreRows {number} 忽略的行数默认为1忽略表头
* @return {array} 包含合并信息的数组形如['A1:A10',...]
*/
export function mergeExcel(props, data, primaryKey, orderKey, ignoreRows = 1) {
const result = [], merge = [], temp = []
props.forEach((prop, index) => {
merge[index] = prop ? [1] : undefined
temp[index] = prop ? 1 : undefined
})
//序号从1开始
let indexAfterMerge = data[0][orderKey] = 1
function compare(currentRow, lastRow, nextRow, colIndex) {
const attr = props[colIndex]
//若与上一行的primaryKey相同且属性相同,则temp对应项+1
if (currentRow[primaryKey] === lastRow[primaryKey]
&& currentRow[attr] === lastRow[attr]) {
temp[colIndex]++
//最后一行特殊处理
if (!nextRow) return merge[colIndex].push(temp[colIndex])
//若与下一行相同
if (currentRow[primaryKey] === nextRow[primaryKey]
&& currentRow[attr] === nextRow[attr]) {
merge[colIndex].push(1)
}
//否则存入
else merge[colIndex].push(temp[colIndex])
}
//否则清零
else {
temp[colIndex] = 1
merge[colIndex].push(1)
}
}
for (let i = 1; i < data.length; i++) {
const currentRow = data[i]
const lastRow = data[i - 1]
const nextRow = data[i + 1]
//重写序号
if (currentRow[primaryKey] === lastRow[primaryKey]) {
currentRow[orderKey] = lastRow[orderKey]
}
else currentRow[orderKey] = ++indexAfterMerge
if (nextRow && nextRow[primaryKey] === currentRow[primaryKey]) {
nextRow[orderKey] = currentRow[orderKey]
}
props.forEach((prop, colIndex) => !isEmpty(prop) && compare(currentRow, lastRow, nextRow, colIndex))
}
function mergeResultConstructor(arr, colIndex) {
const colHeader = number2excelColumnHeader(colIndex)
const startRows = ignoreRows + 1
arr.forEach((rowspan, rowIndex) => {
if (rowspan > 1) {
const start = colHeader + (startRows + rowIndex - (rowspan - 1))
const end = colHeader + (startRows + rowIndex)
result.push(`${start}:${end}`)
}
})
}
merge.forEach((arr, index) => arr && mergeResultConstructor(arr, index))
return result
}
/**
* 生成表头的二维数组以及合并结果
* 参考element的table-header
*
* @param columns {array} 列配置
* @param separator {string} 分隔符
* @return {{header: [], mergeCells: []}}
*/
export function generateHeader(columns, separator = '-') {
const tree = []
let maxDepth = 1
for (const col of columns) {
let header = col.header
if (typeof header === 'string') {
header = header.split(separator)
}
let arr = tree, depth = header.length
if (depth > maxDepth) maxDepth = depth
for (let i = 0; i < depth; i++) {
const value = header[i]
let obj = arr.find(item => item.value === value)
if (!obj) {
obj = {value, depth: i + 1}
if (i !== depth - 1) obj.children = []
arr.push(obj)
}
arr = obj.children
}
}
const traverse = column => {
if (column.children) {
let colSpan = 0
column.children.forEach(subColumn => {
traverse(subColumn)
colSpan += subColumn.colSpan
})
column.rowSpan = 1
column.colSpan = colSpan
}
else {
column.rowSpan = maxDepth - column.depth + 1
column.colSpan = 1
}
}
tree.forEach(node => traverse(node))
const header = []
for (let i = 0; i < maxDepth; i++) header.push([])
const mergeCells = []
let lastDepth = 0, colIndex = 0 //当前是第几列,从0开始
for (const node of flatTree(tree)) {
//判断是否是下一列,节点深度从1开始
if (node.depth <= lastDepth) colIndex++
lastDepth = node.depth
//填充表头
header[node.depth - 1][colIndex] = node.value
//获取合并结果,要么rowSpan大于1要么colSpan大于1,两者不可能同时大于1
const startColHeader = number2excelColumnHeader(colIndex)
const startCell = startColHeader + node.depth
if (node.rowSpan > 1) {
const endCell = startColHeader + (node.depth + node.rowSpan - 1)
mergeCells.push(`${startCell}:${endCell}`)
}
if (node.colSpan > 1) {
const endCell = number2excelColumnHeader(colIndex + node.colSpan - 1) + node.depth
mergeCells.push(`${startCell}:${endCell}`)
}
}
return {header, mergeCells}
}
/**
* 生成二维数组例如json为[{id:1,name:'x'}]结果为[[1,'x']]
*
* @param data {array} json数组
* @param propMap {object} 表头和字段名的映射表propMap[字段名]=对应的表头的数组下标
* @return {array} 基数组为excel中每一行的二维数组
*/
export function jsonArray2rowArray(data, propMap) {
return data.map(i => Object.keys(i)
.reduce((arr, key) => {
arr[propMap[key]] = i[key]
return arr
}, []))
}
/**
* 数字转excel列名
* 1 -> A 27 -> AB
*
* @param n {number}
* @return {string}
*/
export function number2excelColumnHeader(n) {
let s = ""
while (n >= 0) {
s = String.fromCharCode(n % 26 + 65) + s
n = Math.floor(n / 26) - 1
}
return s
}

@ -1,109 +0,0 @@
import {isEmpty} from "@/util"
import {download} from "@/util/file"
import {
abstractExportExcel,
jsonArray2rowArray,
mergeExcel,
generateHeader,
number2excelColumnHeader
} from "./common"
//默认的excel单元格样式
const defaultCellStyle = {
font: {name: '宋体'},
alignment: {
wrapText: true,// 自动换行
horizontal: "center",
vertical: "middle",
indent: 0
}
}
//默认的头部样式
const defaultHeaderStyle = {
...defaultCellStyle,
fill: {type: 'pattern', pattern: 'solid', fgColor: {argb: "FFC0C0C0"}},
font: {name: '宋体', size: 14, bold: true},
border: {top: {style: 'thin'}, left: {style: 'thin'}, bottom: {style: 'thin'}, right: {style: 'thin'}}
}
/**
* excel导出的具体实现
*/
export function exportExcel(url, params, options) {
abstractExportExcel(url, params, options, json2workbook, workbook2excel)
}
export function workbook2excel(workbook, filename) {
return workbook.xlsx.writeBuffer()
.then(buffer => download(new Blob([buffer], {type: "application/octet-stream"}), filename))
}
/**
* 通过json数组生成workbook对象
*
* @param data {array} json数组
* @param columns {array} 列配置
* @param mergeOption 合并配置项
* @param mergeOption.primaryKey {string} ./common.js #mergeExcel所需参数
* @param mergeOption.orderKey {string} ./common.js #mergeExcel所需参数
* @return workbook exceljs的workbook对象
*/
export function json2workbook(data, columns, mergeOption) {
const columnStyle = [] //列设置
const propMap = {} //表头和字段名的映射表,propMap[字段名]=对应的表头的数组下标
const needMerge = [] //合并行配置,[i]=字段名
columns.forEach((col, i) => {
if (!isEmpty(col.prop)) propMap[col.prop] = i
if (col.merge) needMerge[i] = col.prop
columnStyle[i] = {width: col.width || 20, style: defaultCellStyle}
})
const {header, mergeCells} = generateHeader(columns)
if (mergeOption && needMerge.length > 0) {
const {primaryKey, orderKey} = mergeOption
mergeCells.push(...mergeExcel(needMerge, data, primaryKey, orderKey, header.length))
}
const body = jsonArray2rowArray(data, propMap)
return generateWorkbook('Sheet1', columnStyle, header, body, mergeCells)
}
/**
* 生成workbook的代码抽取
*
* @param sheetName {string} 第一个sheet的名称
* @param columnStyle {array} 列样式
* @param header {array} 头部二维数组
* @param body {array} 内容二维数组
* @param mergeCells {array} 需要合并的单元格形如['A1:B1'...]
* @return workbook
*/
function generateWorkbook(sheetName, columnStyle, header, body, mergeCells) {
const workbook = new window.ExcelJS.Workbook()
const sheet = workbook.addWorksheet(sheetName)
if (header) {
sheet.addRows(header)
//设置头部单元格样式
header.forEach((rows, rowIndex) => {
rows.forEach((cellValue, colIndex) => {
if (cellValue !== undefined) {
const cell = sheet.getCell(number2excelColumnHeader(colIndex) + (rowIndex + 1))
Object.keys(defaultHeaderStyle).forEach(key => {
cell[key] = defaultHeaderStyle[key]
})
}
})
})
}
body && sheet.addRows(body)
//exceljs的合并行必须在数据添加后进行
mergeCells && mergeCells.forEach(i => sheet.mergeCells(i))
return workbook
}

@ -1 +0,0 @@
export {exportExcel, json2workbook, workbook2excel} from "./exceljs"

@ -1,68 +0,0 @@
import request from "@/api/request"
import {getToken} from "@/api/file"
import {file as fileConfig} from '@/config'
import {isEmpty, timeFormat} from "@/util"
import {isTxt} from "@/util/validate"
const defaultOptions = {
headers: {"Content-Type": "multipart/form-data"},
generateKey(filename) {
return timeFormat('yyyy/MM/dd/') + Date.now() + '/' + filename
},
onUploadProgress(e) {
}
}
//文件预览
export function preview(url) {
if (isTxt(url)) return window.open(url)
const connectChar = url.includes('?') ? '&' : '?'
url = url + connectChar + 'fullfilename=' + url.replace(fileConfig.storePrefix, '')
const anchor = document.createElement('a')
anchor.style.opacity = '0'
anchor.href = `${fileConfig.previewPrefix}/onlinePreview?url=${encodeURIComponent(url)}`
anchor.target = '_blank'
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
}
//下载文件
export function download(url, name) {
const href = typeof url === 'object' ? window.URL.createObjectURL(url) : url
const anchor = document.createElement('a')
anchor.style.opacity = '0'
anchor.href = href
anchor.download = name
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
if (typeof url === 'object') window.URL.revokeObjectURL(url)
}
//七牛云直传
export function upload(blob, filename, options = {}) {
return getToken
.request()
.then(token => {
if (!options.generateKey) {
options.generateKey = defaultOptions.generateKey
}
const param = new FormData()
param.append('token', token)
param.append('key', options.generateKey(filename))
param.append('file', blob, filename)
return request.post(fileConfig.uploadUrl, param, {...defaultOptions, ...options})
})
}
//自动补全附件链接前缀
export function autoCompleteUrl(url) {
if (isEmpty(url)) return ''
if (url.startsWith('http')) return url
return fileConfig.storePrefix + url
}

@ -1,56 +0,0 @@
/*基于decimal.js的运算封装*/
import Decimal from 'decimal.js'
/**
* 依次相加
*
* @param val {number}
* @return {number}
*/
export function plus(...val) {
const result = val.reduce((total, cur) => total.add(new Decimal(cur)), new Decimal(0))
return result.toNumber()
}
/**
* 依次相减
*
* @param val {number}
* @return {number}
*/
export function sub(...val) {
if (val.length === 0) return 0
let result = new Decimal(val[0])
for (let i = 1; i < val.length; i++) {
result = result.sub(new Decimal(val[i]))
}
return result.toNumber()
}
/**
* 依次相乘
*
* @param val {number}
* @return {number}
*/
export function mul(...val) {
const result = val.reduce((total, cur) => total.mul(new Decimal(cur)), new Decimal(1))
return result.toNumber()
}
/**
* 依次相除
*
* @param val {number}
* @return {number}
*/
export function div(...val) {
if (val.length === 0) return 0
let f = new Decimal(val[0])
for (let i = 1; i < val.length; i++) {
f = f.div(new Decimal(val[i]))
}
return f.toNumber()
}

@ -1,7 +1,4 @@
import pako from 'pako'
import md5 from 'js-md5'
export {md5}
/**
* 解压字符串

@ -56,7 +56,6 @@
<script>
import {isEmpty} from "@/util"
import {elSuccess} from "@/util/message"
import {md5} from "@/util/secret"
export default {
name: "LoginForm",
@ -90,10 +89,7 @@ export default {
if (!valid) return
this.loading = true
this.$verify()
.then(() => this.$store.dispatch('user/login', {
...this.form,
password: md5(this.form.password)
}))
.then(() => this.$store.dispatch('user/login', this.form))
.then(this.success)
.catch(() => this.loading = false)
})

@ -7,13 +7,6 @@
<el-input ref="username" v-model="form.username" :maxlength="20" placeholder="请输入登录名称"/>
</el-form-item>
<el-form-item prop="nick">
<span class="svg-container">
<v-icon icon="svg-user"/>
</span>
<el-input v-model="form.nick" :maxlength="100" placeholder="请输入昵称"/>
</el-form-item>
<el-form-item prop="pwd">
<el-tooltip v-model="capsTooltip" :tabindex="-1" content="大写锁定已打开" manual placement="left">
<span class="svg-container">
@ -53,45 +46,24 @@
</template>
<script>
import {register, checkLoginName, checkNickName} from "@/api/account"
import {debounce} from "@/util"
import {elSuccess} from "@/util/message"
import {md5} from "@/util/secret"
export default {
name: "RegisterForm",
data() {
const validateLoginName = debounce((r, v, c) => {
checkLoginName
.request(this.form.username, this.form.id)
.then(({msg}) => msg ? c(msg) : c())
.catch(e => c(e))
}, 300)
const validateNickName = debounce((r, v, c) => {
checkNickName
.request(this.form.nick, this.form.id)
.then(({msg}) => msg ? c(msg) : c())
.catch(e => c(e))
}, 300)
const validateRepwd = (r, v, c) => {
return v !== this.form.pwd ? c('两次密码输入不一致') : c()
}
return {
form: {
username: '',
nick: '',
pwd: '',
repwd: ''
},
rules: {
username: [
{required: true, message: '请输入登录名称', trigger: 'change'},
{validator: validateLoginName, trigger: 'change'}
],
nick: [
{required: true, message: '请输入昵称', trigger: 'change'},
{validator: validateNickName, trigger: 'change'}
{required: true, message: '请输入登录名称', trigger: 'change'}
],
pwd: [
{required: true, message: '请输入密码', trigger: 'change'},
@ -112,18 +84,8 @@ export default {
if (this.loading) return
this.$refs.form.validate(valid => {
if (!valid) return
this.loading = true
register
.request({
username: this.form.username,
nick: this.form.nick,
password: md5(this.form.pwd)
})
.then(() => {
elSuccess('注册成功')
this.$router.push('/login')
})
.catch(() => this.loading = false)
elSuccess('注册成功')
this.$router.push('/login')
})
},

@ -1,94 +0,0 @@
<script type="jsx">
import FormAnchor from "@/component/form/Anchor"
import FormValidateInfo from "@/component/form/ValidateInfo"
const renderHeader = (h, title, description, extra, close) => (
<div class="detail-page-header">
<el-page-header on-back={close} content={title}/>
<el-row>
<el-row class="detail-page-header-left">
{description.map(({label, content}) => (
<el-col key={label} xs={24} sm={12}>
<span class="detail-page-header-description-label">{label}</span>
<span class="detail-page-header-description-content">{content}</span>
</el-col>
))}
</el-row>
<div class="detail-page-header-extra">
<div>
{extra.map(({title, content}) => (
<div key={title}>
<div class="detail-page-header-extra-title">{title}</div>
<div class="detail-page-header-extra-content">{content}</div>
</div>
))}
</div>
</div>
</el-row>
</div>
)
const renderFooter = (h, form, buttons, close) => (
<div class="detail-page-footer">
<div class="detail-page-footer-left">
<slot name="left"/>
</div>
<div class="detail-page-footer-right">
<FormValidateInfo form={form}/>
{renderButtons(h, buttons, close)}
</div>
</div>
)
const renderButtons = (h, buttons, close) => {
const list = buttons.map(({type, content, e}) =>
<el-button size="small" type={type} on-click={e}>{content}</el-button>
)
list.unshift(<el-button plain size="small" on-click={close}> </el-button>)
return list
}
export default {
name: "DetailPage",
functional: true,
props: {
loading: Boolean,
reference: Function,
anchors: {type: Array, default: () => []},
title: String,
description: {type: Array, default: () => []},
extra: {type: Array, default: () => []},
buttons: {type: Array, default: () => []},
},
render(h, context) {
const {
props: {loading, reference, anchors, title, description, extra, buttons},
listeners: {close},
children
} = context
const getFormRef = () => {
if (!reference) return null
const ref = reference()
if (!ref) return null
return ref.form
}
return (
<div v-loading={loading} class="detail-page">
{renderHeader(h, title, description, extra, close)}
<FormAnchor reference={reference} data={anchors}/>
{children}
{renderFooter(h, getFormRef, buttons, close)}
</div>
)
}
}
</script>
<style lang="scss" src="./style.scss"></style>

@ -1,125 +0,0 @@
@import "~@/asset/style/variables.scss";
.detail-page {
&.el-loading-parent--relative {
position: static !important;
}
&-header {
margin: -#{$page-view-margin} -#{$page-view-margin} 0 -#{$page-view-margin};
padding: 16px 32px 0 32px;
background: #ffffff;
border-bottom: 1px solid $border-color-light;
> .el-page-header {
padding: 0 0 24px 0;
.el-page-header__left:hover {
color: $--color-primary;
}
}
> .el-row {
display: flex;
width: 100%;
margin-bottom: 12px;
@include mobile {
display: block;
}
}
&-left {
width: 100%;
overflow: hidden;
.el-col {
margin-bottom: 16px;
}
}
&-extra {
min-width: 242px;
margin-left: 88px;
@media (max-width: 1200px) {
margin-left: 44px;
}
@media (max-width: 992px) {
margin-left: 20px;
}
@media (max-width: 768px) {
margin-left: 0;
}
> div {
display: flex;
justify-content: space-between;
width: 200px;
}
&-title {
margin-bottom: 4px;
color: rgba(0, 0, 0, .45);
font-size: 14px;
}
&-content {
margin-top: 12px;
color: rgba(0, 0, 0, .85);
font-size: 24px;
}
}
&-description {
span {
display: inline-flex;
align-items: baseline;
}
&-label {
color: rgba(0, 0, 0, .85);
font-weight: 400;
font-size: 14px;
line-height: 1.5715;
text-align: start;
}
&-content {
color: rgba(0, 0, 0, .65);
font-size: 14px;
line-height: 1.5715;
}
}
}
> .el-form > .el-row > .el-card {
margin-top: 16px;
}
&-footer {
@include clearfix;
position: absolute;
width: 100%;
right: 0;
bottom: 0;
z-index: 1;
transition: all 0.3s ease 0s;
height: 56px;
padding: 0 24px;
line-height: 56px;
border-top: 1px solid #f0f0f0;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05);
backdrop-filter: blur(3px);
&-left {
float: left;
}
&-right {
float: right;
}
}
}

@ -1,55 +0,0 @@
/**
* 列表页自适应混入
* 监听el-table动态设置max-height属性
*/
import cssVariables from "@/asset/style/variables.scss"
import {debounce} from "@/util"
export default {
data() {
return {
tableMaxHeight: undefined,
desiredDistance: undefined
}
},
methods: {
//设置表格距离视窗底部的理想距离(分页高度 + el-card的1px边距 + el-card padding + 页面margin)
calcDesiredDistance() {
const pageViewMargin = parseFloat(cssVariables.pageViewMargin)
const paginationHeight = this.$el.querySelector('.table-container > .el-pagination') ? 50 : 0
this.desiredDistance = paginationHeight + 1 + 20 + pageViewMargin
},
resize() {
const clientHeight = window.innerHeight
//计算表格底部距离视窗底部的距离
const tableRect = this.$refs.table.$el.getBoundingClientRect()
const distance = clientHeight - tableRect.top - tableRect.height
if (this.desiredDistance === undefined) {
this.calcDesiredDistance()
}
const overHeight = this.desiredDistance - distance
this.tableMaxHeight = tableRect.height - overHeight
},
createResizeObserver() {
if (this.resizeObserver) return
this.resizeObserver = new ResizeObserver(this.resize)
this.resizeObserver.observe(this.$el)
this.$once('hook:beforeDestroy', () => {
this.resizeObserver && this.resizeObserver.disconnect()
})
},
},
mounted() {
this.resize = debounce(this.resize, 300)
this.createResizeObserver()
}
}

@ -1,73 +0,0 @@
<script type="text/jsx">
import AbstractTable from "@/component/abstract/Table"
import AbstractPagination from "@/component/abstract/Pagination"
import SearchForm from "@/component/form/Search"
import autoAdaptHeightMixin from "./autoAdaptHeightMixin"
import {isEmpty} from "@/util"
export default {
name: "ListPage",
mixins: [autoAdaptHeightMixin],
components: {AbstractTable, AbstractPagination, SearchForm},
props: {data: Object},
methods: {
renderSearchForm(config, slot) {
return slot && <search-form {...config}>{slot()}</search-form>
},
renderButtons(buttons) {
if (buttons.length > 0 && isEmpty(buttons[0].type)) {
buttons[0].type = 'primary'
}
return (
<el-row class="button-group">
{buttons.map(button => {
if (!button) return
const {icon, type, content, e} = button
return (
<el-button
icon={icon}
size="small"
plain={isEmpty(type)}
type={type || 'dashed'}
on-click={e}
>
{content}
</el-button>
)
})}
</el-row>
)
},
renderTableAndPagination({loading, table, pagination}) {
return (
<el-row v-loading={loading} class="table-container">
<abstract-table max-height={this.tableMaxHeight} {...table}/>
<abstract-pagination {...pagination}/>
</el-row>
)
}
},
render() {
const {data: {pageLoading, buttons, dataLoading, search, table, pagination}, $scopedSlots} = this
if (!table.scopedSlots) {
table.scopedSlots = {}
}
table.scopedSlots.default = $scopedSlots.tableColumn
return (
<el-card v-loading={pageLoading} class="max-view-height">
{this.renderSearchForm(search, $scopedSlots.searchForm)}
{this.renderButtons(buttons)}
{this.renderTableAndPagination({loading: dataLoading, table, pagination})}
{$scopedSlots.default && $scopedSlots.default()}
</el-card>
)
}
}
</script>

@ -1,89 +0,0 @@
<template>
<div :style="{height,width}"/>
</template>
<script>
import {logic, resize} from "@/mixin/chart"
import {getDailyFinishOrder} from '@/api/statistic'
import {timeFormat} from "@/util"
export default {
name: "DailyFinishOrderStat",
mixins: [resize, logic],
methods: {
init() {
if (this.loading) return
this.loading = true
getDailyFinishOrder
.request()
.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})
})
.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
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: ['采购订单', '销售订单'],
right: 10
},
series: [
{
name: '采购订单',
smooth: true,
type: 'line',
data: purchase
},
{
name: '销售订单',
smooth: true,
type: 'line',
data: sell
}
]
})
}
}
}
</script>

@ -1,97 +0,0 @@
<template>
<div :style="{height,width}"/>
</template>
<script>
import {logic, resize} from "@/mixin/chart"
import {getDailyProfitStat} from '@/api/statistic'
import {timeFormat} from "@/util"
export default {
name: "DailyProfitStat",
mixins: [resize, logic],
methods: {
init() {
if (this.loading) return
this.loading = true
getDailyProfitStat
.request()
.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})
})
.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
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
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: profit
}
]
})
}
}
}
</script>

@ -1,99 +0,0 @@
<script type="text/jsx">
import CountTo from 'vue-count-to'
export default {
name: "PanelGroupItem",
functional: true,
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}>
<v-icon icon={icon} class="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">
@import "~@/asset/style/variables.scss";
.card-panel {
height: 108px;
cursor: pointer;
font-size: 12px;
position: relative;
overflow: hidden;
color: #666;
background: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
border-color: rgba(0, 0, 0, .05);
&:hover .card-panel-icon-wrapper {
color: #ffffff;
}
.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-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-num {
font-size: 20px;
}
}
}
@include mobile {
.card-panel-description {
display: none;
}
.card-panel-icon-wrapper {
float: none !important;
width: 100%;
height: 100%;
margin: 0 !important;
.icon {
display: block;
margin: 14px auto !important;
float: none !important;
}
}
}
</style>

@ -1,89 +0,0 @@
<template>
<el-row :gutter="40" class="panel-group">
<el-col v-for="i in list" :key="i.id" :xs="12" :sm="12" :lg="6">
<panel-group-item v-bind="i" @click.native="jump(i)"/>
</el-col>
</el-row>
</template>
<script>
import PanelGroupItem from './PanelGroupItem'
import cssVariables from '@/asset/style/variables.scss'
import {getFourBlock} from '@/api/statistic'
import {isEmpty} from "@/util"
import {auth} from "@/util/auth"
export default {
name: 'panelGroup',
components: {PanelGroupItem},
data() {
return {
loading: false,
list: [
{
id: 'online',
path: '/system/user',
icon: 'svg-user',
color: cssVariables.info,
value: 0,
text: '在线用户'
},
{
id: 'purchase',
path: '/purchase/order',
icon: 'svg-shopping',
color: cssVariables.primary,
value: 0,
text: '今日采购额'
},
{
id: 'sell',
path: '/sell/order',
icon: 'svg-sell',
color: cssVariables.warning,
value: 0,
text: '今日销售额'
},
{
id: 'profit',
icon: 'svg-money',
color: cssVariables.success,
value: 0,
text: '今日毛利润'
},
]
}
},
methods: {
init() {
if (this.loading) return
this.loading = true
getFourBlock
.request()
.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)
}
},
mounted() {
this.init()
}
}
</script>
<style>
.panel-group > .el-col {
margin-bottom: 32px;
}
</style>

@ -1,73 +0,0 @@
<template>
<el-row :gutter="32">
<el-col v-for="i in ['purchase','sell','profit']" :key="i" :lg="8" :sm="24">
<el-card style="margin-bottom: 32px;">
<pie-chart :title="getTitle(i)" :data="getData(i)"/>
</el-card>
</el-col>
</el-row>
</template>
<script>
import PieChart from "@/component/Charts/PieChart"
import {getTotalProfitGoods} from '@/api/statistic'
export default {
name: "TotalProfitGoods",
components: {PieChart},
data() {
return {
loading: false,
purchase: {
title: '各个商品总采购额',
data: []
},
sell: {
title: '各个商品总销售额',
data: []
},
profit: {
title: '各个商品总毛利润',
data: []
}
}
},
methods: {
getTitle(type) {
return this[type].title
},
getData(type) {
return this[type].data
},
init() {
if (this.loading) return
this.loading = true
getTotalProfitGoods
.request()
.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>

@ -1,30 +1,9 @@
<script type="text/jsx">
import PanelGroup from './component/PanelGroup'
import DailyProfitStat from "./component/DailyProfitStat"
import DailyFinishOrderStat from "./component/DailyFinishOrderStat"
import TotalProfitGoods from "./component/TotalProfitGoods"
<template functional>
<div>我是首页</div>
</template>
<script>
export default {
name: 'index',
functional: true,
render() {
return (
<div>
<PanelGroup/>
<el-card>
<DailyProfitStat/>
</el-card>
<el-card style="margin: 32px 0">
<DailyFinishOrderStat/>
</el-card>
<TotalProfitGoods/>
</div>
)
}
name: 'index'
}
</script>

@ -1,234 +0,0 @@
<template>
<abstract-dialog :loading="loading" :title="title" :value="value" width="50%" @close="cancel" @open="open">
<abstract-form :model="form" :rules="rules">
<abstract-form-item label="标 题" prop="title" thin>
<el-input v-model="form.title" maxlength="100" :readonly="!canSave"/>
</abstract-form-item>
<abstract-form-item label="消息类型" prop="type" thin>
<el-select v-model="form.type" :disabled="!canSave">
<el-option :value="0" label="通知提醒"/>
<el-option :value="1" label="系统公告"/>
</el-select>
</abstract-form-item>
<abstract-form-item v-if="type!=='add'" label="创建人" thin>
<el-input :value="form.cname" readonly/>
</abstract-form-item>
<abstract-form-item v-if="type!=='add'" label="创建时间" thin>
<el-date-picker :value="form.ctime" format="yyyy-MM-dd HH:mm:ss" readonly type="date"/>
</abstract-form-item>
<abstract-form-item label="通知对象" prop="broadcast" thin>
<el-select v-model="form.broadcast" :disabled="!canSave">
<el-option :value="false" label="指定用户"/>
<el-option :value="true" label="全体用户"/>
</el-select>
</abstract-form-item>
<abstract-form-item v-if="!form.broadcast" label="选择用户" prop="recipient" thin>
<user-selector v-model="form.recipient" :disabled="!canSave"/>
</abstract-form-item>
<abstract-form-item v-if="form.pname" label="发布人" thin>
<el-input :value="form.pname" readonly/>
</abstract-form-item>
<abstract-form-item v-if="form.ptime" label="发布时间" thin>
<el-date-picker :value="form.ptime" format="yyyy-MM-dd HH:mm:ss" readonly type="date"/>
</abstract-form-item>
<abstract-form-item v-if="form.status===2" label="撤回人" thin>
<el-input :value="form.wname" readonly/>
</abstract-form-item>
<abstract-form-item v-if="form.status===2" label="撤回时间" thin>
<el-date-picker :value="form.wtime" format="yyyy-MM-dd HH:mm:ss" readonly type="date"/>
</abstract-form-item>
<abstract-form-item label="内 容" full>
<rich-text-editor v-model="form.content" :readonly="!canSave"/>
</abstract-form-item>
</abstract-form>
<template v-slot:footer>
<el-button plain size="small" @click="closeDialog"> </el-button>
<el-button v-if="canSave" size="small" type="primary" @click="save"> </el-button>
<el-button v-if="canPublish" size="small" type="primary" @click="publish"> </el-button>
<el-button v-if="canWithdraw" size="small" type="danger" @click="withdraw"> </el-button>
</template>
</abstract-dialog>
</template>
<script>
import dialogMixin from "@/mixin/dialogMixin"
import AbstractForm from "@/component/abstract/Form"
import AbstractFormItem from "@/component/abstract/Form/item"
import AbstractDialog from '@/component/abstract/Dialog'
import RichTextEditor from "@/component/editor/RichTextEditor"
import UserSelector from '@/component/biz/UserSelector/SimpleMultipleUserSelector'
import {add, update, publish, withdraw} from "@/api/message/manage"
import {isEmpty, mergeObj, resetObj} from '@/util'
import {auth} from "@/util/auth"
import {elAlert, elConfirm, elSuccess} from "@/util/message"
export default {
name: "EditDialog",
mixins: [dialogMixin],
components: {AbstractForm, AbstractFormItem, AbstractDialog, RichTextEditor, UserSelector},
props: {
value: Boolean,
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: false,
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 '编辑消息'
}
},
canSave() {
//addeditstatus=0
return this.type === 'add' && auth(add.url)
|| this.form.status === 0 && this.type === 'edit' && auth(update.url)
},
canPublish() {
//addeditstatus=0
return auth(publish.url) && (this.type === 'add' || this.type === 'edit' && this.form.status === 0)
},
canWithdraw() {
//editstatus=1
return auth(withdraw.url)
&& this.type === 'edit'
&& this.form.status === 1
},
},
methods: {
open() {
if (this.type === 'add') return
const recipient = (this.data.recipient || '').split(',').filter(Boolean).map(i => parseInt(i))
mergeObj(this.form, {...this.data, recipient})
},
clearForm() {
resetObj(this.form)
this.needSearch = false
this.loading = false
this.$nextTick(() => this.$refs.form.clearValidate())
},
cancel() {
this.closeDialog()
this.needSearch && this.$emit('search')
window.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()
const promise = this.type === 'add' ? add.request(data) : update.request(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.request(this.transformForm()))
.then(({data, msg}) => {
elSuccess(msg)
this.needSearch = true
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.request({id: this.form.id, title: this.form.title})
})
.then(({msg}) => {
elSuccess(msg)
this.needSearch = true
this.closeDialog()
})
.finally(() => this.loading = false)
},
transformForm() {
const recipient =
Array.isArray(this.form.recipient) && this.form.recipient.length > 0
? this.form.recipient.join(',')
: null
return {...this.form, recipient}
}
}
}
</script>

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

Loading…
Cancel
Save