zjxpcyc 6 年 前
コミット
f42c92a716

+ 2
- 0
package.json ファイルの表示

13
     "axios": "^0.18.0",
13
     "axios": "^0.18.0",
14
     "blueimp-md5": "^2.10.0",
14
     "blueimp-md5": "^2.10.0",
15
     "dayjs": "^1.8.12",
15
     "dayjs": "^1.8.12",
16
+    "echarts": "^4.2.1",
16
     "element-ui": "^2.6.1",
17
     "element-ui": "^2.6.1",
17
     "normalize.css": "^8.0.1",
18
     "normalize.css": "^8.0.1",
18
     "nprogress": "^0.2.0",
19
     "nprogress": "^0.2.0",
19
     "vue": "^2.6.6",
20
     "vue": "^2.6.6",
21
+    "vue-echarts": "^4.0.1",
20
     "vue-router": "^3.0.2",
22
     "vue-router": "^3.0.2",
21
     "vuex": "^3.1.0"
23
     "vuex": "^3.1.0"
22
   },
24
   },

+ 54
- 0
src/components/EditableInput.vue ファイルの表示

1
+<template>
2
+  <span @click="toggleFocus" v-if="!focus">
3
+    {{value}}
4
+    <span class="empty-tip" v-if="!value">(empty)</span>
5
+  </span>
6
+  <el-input ref="ctl" v-model="inputVal" @blur="handleBlur" v-else></el-input>
7
+</template>
8
+
9
+<script>
10
+export default {
11
+  name: 'editable-input',
12
+  props: [
13
+    'value',
14
+  ],
15
+  data () {
16
+    return {
17
+      focus: false,
18
+      inputVal: undefined,
19
+    }
20
+  },
21
+  mounted () {
22
+    if (!window['__editable-blur']) {
23
+      window['__editable-blur'] = () => {}
24
+    }
25
+  },
26
+  methods: {
27
+    toggleFocus () {
28
+      window['__editable-blur']()
29
+      window['__editable-blur'] = this.handleBlur.bind(this)
30
+
31
+      this.focus = true
32
+      this.inputVal = this.value
33
+      this.$nextTick(() => {
34
+        if (this.$refs.ctl) this.$refs.ctl.focus()
35
+      })
36
+    },
37
+    handleBlur() {
38
+      this.focus = false
39
+
40
+      if (this.inputVal != this.value) {
41
+        this.$emit('change', this.inputVal )
42
+      }
43
+    }
44
+  }
45
+}
46
+</script>
47
+
48
+<style lang="scss" scoped>
49
+.empty-tip {
50
+  font-size: 0.8em;
51
+  color: #aaa;
52
+}
53
+</style>
54
+

+ 69
- 0
src/components/EditableSelect.vue ファイルの表示

1
+<template>
2
+  <span v-if="!focus" @click.stop="toggleFocus">
3
+    {{valName}}
4
+    <span class="empty-tip" v-if="!value">(empty)</span>
5
+  </span>
6
+  <el-select ref="ctl" v-model="inputVal" @change="handleChange" v-else>
7
+    <el-option
8
+      v-for="item in dict"
9
+      :key="item.value"
10
+      :label="item.label"
11
+      :value="item.value">
12
+    </el-option>
13
+  </el-select>
14
+</template>
15
+
16
+<script>
17
+export default {
18
+  name: 'editable-select',
19
+  props: [
20
+    'value',
21
+    'dict',
22
+  ],
23
+  computed: {
24
+    valName () {
25
+      return ((this.dict || []).filter(x => x.value === this.value)[0] || {}).label
26
+    },
27
+  },
28
+  data () {
29
+    return {
30
+      focus: false,
31
+      inputVal: undefined,
32
+    }
33
+  },
34
+  mounted () {
35
+    if (!window['__editable-blur']) {
36
+      window['__editable-blur'] = () => {}
37
+    }
38
+  },
39
+  methods: {
40
+    toggleFocus () {
41
+      window['__editable-blur']()
42
+      window['__editable-blur'] = this.handleBlur.bind(this)
43
+
44
+      this.focus = true
45
+      this.inputVal = this.value
46
+      this.$nextTick(() => {
47
+        if (this.$refs.ctl) this.$refs.ctl.focus()
48
+      })
49
+    },
50
+    handleBlur () {
51
+      this.focus = false
52
+    },
53
+    handleChange() {
54
+      this.focus = false
55
+
56
+      if (this.inputVal != this.value) {
57
+        this.$emit('change', this.inputVal )
58
+      }
59
+    }
60
+  }
61
+}
62
+</script>
63
+
64
+<style lang="scss" scoped>
65
+.empty-tip {
66
+  font-size: 0.8em;
67
+  color: #aaa;
68
+}
69
+</style>

+ 79
- 0
src/components/charts/StatCard.vue ファイルの表示

1
+<template>
2
+  <el-card
3
+    shadow="hover"
4
+    :body-style="{ paddingLeft: 0, paddingRight: 0 }"
5
+    :class="{ 'card-box': true, [`theme-${theme || 'default'}`]: true }">
6
+    <el-row>
7
+      <el-col :span="10" class="icon">
8
+        <i :class="icon" v-if="icon"></i>
9
+        <span v-else>&nbsp;</span>
10
+      </el-col>
11
+      <el-col :span="14">
12
+        <div class="cat">{{tip}}</div>
13
+        <div class="val">{{val}}</div>
14
+      </el-col>
15
+    </el-row>
16
+  </el-card>
17
+</template>
18
+
19
+<script>
20
+export default {
21
+  name: 'statis-card',
22
+  props: [
23
+    'tip',
24
+    'val',
25
+    'icon',
26
+    'theme',
27
+  ]
28
+}
29
+</script>
30
+
31
+<style lang="scss" scoped>
32
+.card-box {
33
+  color: #333;
34
+  font-size: 16px;
35
+
36
+  .icon {
37
+    font-size: 2em;
38
+    text-align: left;
39
+    line-height: 1.4em;
40
+    text-align: center;
41
+  }
42
+
43
+  .cat {
44
+    font-size: 1em;
45
+    line-height: 1.2em;
46
+  }
47
+
48
+  .val {
49
+    font-size: 1.5em;
50
+    line-height: 1.2em;
51
+  }
52
+}
53
+
54
+.theme-default {
55
+  background-color: #fff;
56
+  color: #333;
57
+}
58
+
59
+.theme-primary {
60
+  background-color: #409eff;
61
+  color: #fff;
62
+}
63
+
64
+.theme-success {
65
+  background-color: #67c23a;
66
+  color: #fff;
67
+}
68
+
69
+.theme-warn {
70
+  background-color: #e6a23c;
71
+  color: #fff;
72
+}
73
+
74
+.theme-danger {
75
+  background-color: #f56c6c;
76
+  color: #fff;
77
+}
78
+</style>
79
+

+ 88
- 0
src/components/charts/VisitingDayLine.vue ファイルの表示

1
+<template>
2
+  <v-chart class="x-chart" :options="options" autoresize></v-chart>
3
+</template>
4
+
5
+<script>
6
+import 'echarts/lib/chart/line'
7
+import 'echarts/lib/component/title'
8
+import 'echarts/lib/component/dataZoom'
9
+import 'echarts/lib/component/tooltip'
10
+
11
+export default {
12
+  name: 'visiting-day-line',
13
+  components: {
14
+    // vChart: () => import('vue-echarts'),
15
+  },
16
+  props: [
17
+    'source',
18
+  ],
19
+  data () {
20
+    return {
21
+      opts: {
22
+        title: { text: '日访问量' },
23
+        tooltip: {
24
+          formatter: (params, ticket, callback) => {
25
+            const { visiteDate, amount } = params.data
26
+            const tip = `${visiteDate}: ${amount}`
27
+            callback(ticket, tip)
28
+            return tip
29
+          }
30
+        },
31
+        xAxis: { type: 'category' },
32
+        yAxis: { name: '人/日' },
33
+        series: [
34
+          {
35
+            type: 'line',
36
+            smooth: true,
37
+            encode: {
38
+              x: 'visiteDate',
39
+              y: 'amount',
40
+            },
41
+          },
42
+        ],
43
+        dataZoom: [
44
+          {
45
+            type: 'inside',
46
+            show: true,
47
+            start: 0,
48
+            end: 100,
49
+          },
50
+          // {
51
+          //   type: 'slider',
52
+          //   show: true,
53
+          //   start: 0,
54
+          //   end: 100,
55
+          // }
56
+        ],
57
+        dataset: {
58
+          sourceHeader: false,
59
+          dimensions: ['visiteDate', 'amount'],
60
+        },
61
+      }
62
+    }
63
+  },
64
+  computed: {
65
+    options () {
66
+      return {
67
+        ...this.opts,
68
+        title: {
69
+          ...this.opts.title,
70
+        },
71
+        dataset: {
72
+          ...this.opts.dataset,
73
+          source: this.source || [],
74
+        }
75
+      }
76
+    }
77
+  }
78
+}
79
+</script>
80
+
81
+<style lang="scss" scoped>
82
+.x-chart {
83
+  margin: 0 auto;
84
+  width: 100%;
85
+  height: 100%;
86
+}
87
+</style>
88
+

+ 93
- 0
src/components/charts/VisitingDayTable.vue ファイルの表示

1
+<template>
2
+  <div>
3
+    <h3 style="margin: 0">访客记录</h3>
4
+    <el-table
5
+      :data="dataSet"
6
+      stripe
7
+      style="width: 100%">
8
+      <el-table-column
9
+        label="日期"
10
+        width="180">
11
+        <template slot-scope="scope">
12
+          <span>{{formatDate(scope.row.visiteDate)}}</span>
13
+        </template>
14
+      </el-table-column>
15
+      <el-table-column
16
+        prop="name"
17
+        label="姓名"
18
+        width="180">
19
+      </el-table-column>
20
+      <el-table-column
21
+        prop="carStyle"
22
+        label="车辆">
23
+      </el-table-column>
24
+      <el-table-column
25
+        prop="deviceName"
26
+        label="采集设备">
27
+      </el-table-column>
28
+    </el-table>
29
+    <el-pagination
30
+      small
31
+      style="margin-top:10px;"
32
+      layout="prev, pager, next"
33
+      :page-size="pageNavi.size"
34
+      :total="pageNavi.total"
35
+      :current-page="pageNavi.current"
36
+      @current-change="changePage"
37
+    >
38
+    </el-pagination>
39
+  </div>
40
+</template>
41
+
42
+<script>
43
+import dayjs from 'dayjs'
44
+
45
+export default {
46
+  name: 'visiting-day-table',
47
+  props: [
48
+    'source',
49
+    'page',
50
+  ],
51
+  data() {
52
+    return {
53
+      sampleData: [
54
+        { visiteDate: '2019-02-15 17:00', name: '张一', carStyle: '奔驰', channel: '搜房' },
55
+        { visiteDate: '2019-02-15 16:40', name: '张二', carStyle: '奔驰', channel: '搜房' },
56
+        { visiteDate: '2019-02-15 16:30', name: '张三', carStyle: '奔驰', channel: '搜房' },
57
+        { visiteDate: '2019-02-15 15:50', name: '张四', carStyle: '奔驰', channel: '搜房' },
58
+        { visiteDate: '2019-02-15 15:40', name: '张五', carStyle: '奔驰', channel: '搜房' },
59
+        { visiteDate: '2019-02-15 15:20', name: '张六', carStyle: '奔驰', channel: '搜房' },
60
+        { visiteDate: '2019-02-15 15:10', name: '张七', carStyle: '奔驰', channel: '搜房' },
61
+        { visiteDate: '2019-02-15 15:00', name: '张八', carStyle: '奔驰', channel: '搜房' },
62
+      ],
63
+      defPage: {
64
+        current: 1,
65
+        size: 10,
66
+        total: 0,
67
+      }
68
+    }
69
+  },
70
+  computed: {
71
+    pageNavi () {
72
+      return {
73
+        ...this.defPage,
74
+        ...(this.page || {})
75
+      }
76
+    },
77
+    dataSet () {
78
+      return this.source || this.sampleData
79
+    }
80
+  },
81
+  methods: {
82
+    changePage (nwPg) {
83
+      this.$emit('page-change', nwPg)
84
+    },
85
+    formatDate(dt) {
86
+      if (!dt || dt.indexOf('0001-') > -1) return ''
87
+
88
+      return dayjs(dt).format('YYYY-MM-DD HH:mm')
89
+    }
90
+  }
91
+}
92
+</script>
93
+

+ 26
- 2
src/config/api.js ファイルの表示

55
       url: `${commPrefix}/person_type/:id`
55
       url: `${commPrefix}/person_type/:id`
56
     }
56
     }
57
   },
57
   },
58
+  stats: {
59
+    person: {
60
+      method: 'get',
61
+      url: `${commPrefix}/stats/person`
62
+    },
63
+    daily: {
64
+      visiting: {
65
+        method: 'get',
66
+        url: `${commPrefix}/stats/daily/visitinglog`
67
+      }
68
+    }
69
+  },
58
   notice: {
70
   notice: {
59
     add: {
71
     add: {
60
       method: 'post',
72
       method: 'post',
101
     dispatch: {
113
     dispatch: {
102
       method: 'put',
114
       method: 'put',
103
       url: `${commPrefix}/person/:id/avatar`
115
       url: `${commPrefix}/person/:id/avatar`
104
-    }
116
+    },
117
+    merge: {
118
+      method: 'put',
119
+      url: `${commPrefix}/merge/person/:id`
120
+    },
105
   },
121
   },
106
   guard: {
122
   guard: {
107
     dispatch: {
123
     dispatch: {
173
     list: {
189
     list: {
174
       method: 'get',
190
       method: 'get',
175
       url: `${commPrefix}/device`
191
       url: `${commPrefix}/device`
176
-    }
192
+    },
193
+    snapshot: {
194
+      method: 'get',
195
+      url: `${commPrefix}/snapshot`
196
+    },
197
+    dispatch: {
198
+      method: 'post',
199
+      url: `${commPrefix}/device/person`
200
+    },
177
   },
201
   },
178
   words: {
202
   words: {
179
     add: {
203
     add: {

+ 6
- 1
src/layout/default/index.vue ファイルの表示

125
   min-height: calc(100vh - 160px);
125
   min-height: calc(100vh - 160px);
126
   position: relative;
126
   position: relative;
127
   overflow: hidden;
127
   overflow: hidden;
128
-  background-color: #fff;
129
   margin: 0 20px;
128
   margin: 0 20px;
129
+  padding: 0;
130
+
131
+  & > * {
132
+    background-color: #fff;
133
+    padding: 20px;
134
+  }
130
 }
135
 }
131
 </style>
136
 </style>

+ 2
- 0
src/main.js ファイルの表示

1
 import Vue from 'vue'
1
 import Vue from 'vue'
2
 import Element from 'element-ui'
2
 import Element from 'element-ui'
3
+import ECharts from 'vue-echarts'
3
 import XMIcon from '@/components/XMIcon.vue'
4
 import XMIcon from '@/components/XMIcon.vue'
4
 import XMRightLay from '@/components/XMRightLay.vue'
5
 import XMRightLay from '@/components/XMRightLay.vue'
5
 import XMSearchForm from '@/components/XMSearchForm.vue'
6
 import XMSearchForm from '@/components/XMSearchForm.vue'
13
 Vue.use(Element)
14
 Vue.use(Element)
14
 
15
 
15
 Vue.config.productionTip = false
16
 Vue.config.productionTip = false
17
+Vue.component('v-chart', ECharts)
16
 Vue.component('xm-icon', XMIcon)
18
 Vue.component('xm-icon', XMIcon)
17
 Vue.component('xm-rtl', XMRightLay)
19
 Vue.component('xm-rtl', XMRightLay)
18
 Vue.component('xm-search', XMSearchForm)
20
 Vue.component('xm-search', XMSearchForm)

+ 1
- 0
src/router.js ファイルの表示

11
   {
11
   {
12
     path: '/',
12
     path: '/',
13
     name: 'index',
13
     name: 'index',
14
+    redirect: '/dashboard',
14
     component: () => import('@/layout/index.vue'),
15
     component: () => import('@/layout/index.vue'),
15
     children: [
16
     children: [
16
       ...pages,
17
       ...pages,

+ 2
- 0
src/store/index.js ファイルの表示

10
 import person from './modules/person'
10
 import person from './modules/person'
11
 import sysparam from './modules/sysparam'
11
 import sysparam from './modules/sysparam'
12
 import sysuser from './modules/sysuser'
12
 import sysuser from './modules/sysuser'
13
+import stats from './modules/stats'
13
 
14
 
14
 Vue.use(Vuex)
15
 Vue.use(Vuex)
15
 
16
 
25
     person,
26
     person,
26
     sysparam,
27
     sysparam,
27
     sysuser,
28
     sysuser,
29
+    stats,
28
   }
30
   }
29
 })
31
 })
30
 
32
 

+ 26
- 2
src/store/modules/device.js ファイルの表示

5
 export default {
5
 export default {
6
   namespaced: true,
6
   namespaced: true,
7
   state: {
7
   state: {
8
-    devices: {}
8
+    devices: {},
9
+    snapshots: {},
9
   },
10
   },
10
   mutations: {
11
   mutations: {
11
     updateList (state, payload) {
12
     updateList (state, payload) {
12
       state.devices = payload
13
       state.devices = payload
13
-    }
14
+    },
15
+    updateState (state, payload = {}) {
16
+      Object.keys(payload).forEach((key) => {
17
+        const val = payload[key]
18
+        state[key] = Array.isArray(val) ? [ ...val ] : { ...val }
19
+      })
20
+    },
14
   },
21
   },
15
   actions: {
22
   actions: {
16
     getDeviceList ({ commit }, payload) {
23
     getDeviceList ({ commit }, payload) {
25
           }
32
           }
26
         })
33
         })
27
       })
34
       })
35
+    },
36
+    getSnapshots ({ commit }, payload) {
37
+      return new Promise((resolve, reject) => {
38
+        const api = lodash.get(apis, 'device.snapshot')
39
+        interact(api, payload).then((data) => {
40
+          commit('updateState', { snapshots: data })
41
+          resolve(data)
42
+        }).catch(({ message }) => {
43
+          if (typeof message === 'string') {
44
+            reject(message)
45
+          }
46
+        })
47
+      })
48
+    },
49
+    dispatchBatch (ctx, payload) {
50
+      const api = lodash.get(apis, 'device.dispatch')
51
+      return interact({ ...api, params: payload })
28
     }
52
     }
29
   }
53
   }
30
 }
54
 }

+ 23
- 1
src/store/modules/person.js ファイルの表示

15
       const { records } = payload
15
       const { records } = payload
16
       state.persons = records
16
       state.persons = records
17
     },
17
     },
18
+    mergeList (state, payload) {
19
+      state.persons = state.persons.map((person) => {
20
+        return person.personId === payload.personId ? { ...payload } : person
21
+      })
22
+    },
18
     updateGuards (state, payload) {
23
     updateGuards (state, payload) {
19
       const { records } = payload
24
       const { records } = payload
20
       state.guards = records
25
       state.guards = records
21
     },
26
     },
22
     updateDetail (state, payload) {
27
     updateDetail (state, payload) {
23
       state.detail = payload.detail || {}
28
       state.detail = payload.detail || {}
24
-      state.visitingLog = payload.visitingLog || []
29
+      // state.visitingLog = payload.visitingLog || []
30
+    },
31
+    updateLog (state, payload) {
32
+      state.visitingLog = (state.visitingLog || []).map((log) => {
33
+        return log.logId === payload.logId ? { ...payload } : log
34
+      })
25
     },
35
     },
26
     resetVisitingLog (state, payload) {
36
     resetVisitingLog (state, payload) {
27
       state.visitingLog = payload || []
37
       state.visitingLog = payload || []
74
         commit('updateDetail', data)
84
         commit('updateDetail', data)
75
       })
85
       })
76
     },
86
     },
87
+    getPurPerson (ctx, payload) {
88
+      const api = replaceApiParams(lodash.get(apis, 'person.detail'), payload)
89
+      return interact(api);
90
+    },
91
+    mergePerson ({ commit }, payload) {
92
+      const { from, to } = payload
93
+      const api = replaceApiParams(lodash.get(apis, 'person.merge'), { id: from })
94
+      return interact(api, JSON.stringify(to)).then(() => {
95
+        const person = { ...to, personId: from }
96
+        commit('mergeList', person)
97
+      })
98
+    },
77
     addPerson ( _, payload) {
99
     addPerson ( _, payload) {
78
       const { onSuccess } = payload
100
       const { onSuccess } = payload
79
       const api = lodash.get(apis, 'person.add')
101
       const api = lodash.get(apis, 'person.add')

+ 63
- 0
src/store/modules/stats.js ファイルの表示

1
+import lodash from 'lodash'
2
+import { interact } from '../../utils'
3
+import apis from '../../config/api'
4
+
5
+export default {
6
+  namespaced: true,
7
+  state: {
8
+    person: [],
9
+    visitingDaily: [],
10
+    visitingLog: [],
11
+  },
12
+  mutations: {
13
+    updateStat (state, payload) {
14
+      Object.keys(payload).forEach((k) => {
15
+        const val = payload[k]
16
+        state[k] = Array.isArray(val) ? [ ...val ] : { ...val }
17
+      })
18
+    },
19
+  },
20
+  actions: {
21
+    getPersonStats ({ commit }, payload) {
22
+      return new Promise((resolve, reject) => {
23
+        const api = lodash.get(apis, 'stats.person')
24
+        interact(api, payload).then((data) => {
25
+          commit('updateStat', { person :data })
26
+          resolve(data)
27
+        }).catch(({ message }) => {
28
+          if (typeof message === 'string') {
29
+            reject(message)
30
+          }
31
+        })
32
+      })
33
+    },
34
+    
35
+    getVisitingDaily ({ commit }, payload) {
36
+      return new Promise((resolve, reject) => {
37
+        const api = lodash.get(apis, 'stats.daily.visiting')
38
+        interact(api, payload).then((data) => {
39
+          commit('updateStat', { visitingDaily :data })
40
+          resolve(data)
41
+        }).catch(({ message }) => {
42
+          if (typeof message === 'string') {
43
+            reject(message)
44
+          }
45
+        })
46
+      })
47
+    },
48
+    
49
+    getVisitingLog ({ commit }, payload) {
50
+      return new Promise((resolve, reject) => {
51
+        const api = lodash.get(apis, 'visitinglog.list')
52
+        interact(api, payload).then((data) => {
53
+          commit('updateStat', { visitingLog :data })
54
+          resolve(data)
55
+        }).catch(({ message }) => {
56
+          if (typeof message === 'string') {
57
+            reject(message)
58
+          }
59
+        })
60
+      })
61
+    },
62
+  }
63
+}

+ 191
- 6
src/views/Dashboard.vue ファイルの表示

1
 <template>
1
 <template>
2
-  <div>
3
-    <h1>欢迎使用荟房迎宾系统</h1>
2
+  <div class="dash-main">
3
+    <el-row :gutter="24" class="sta-row">
4
+      <el-col :span="8"><stat-card icon="el-icon-time" theme="success" tip="总到访" :val="personDt.total"></stat-card></el-col>
5
+      <el-col :span="8"><stat-card icon="el-icon-date" tip="今日到访" theme="danger" :val="personDt.today"></stat-card></el-col>
6
+      <el-col :span="8"><stat-card icon="el-icon-news" tip="今日老客户" theme="warn" :val="personDt.regular"></stat-card></el-col>
7
+    </el-row>
8
+    <div class="sta-row">
9
+      <el-card shadow="never">
10
+        <div slot="header" class="short-filters">
11
+          <div class="flex-item">&nbsp;</div>
12
+          <div class="static-item item-space">
13
+            <el-radio-group
14
+              size="small"
15
+              v-model="filterData.dateShortcut"
16
+              @change="handeShortDateChange"
17
+            >
18
+              <el-radio-button label="本年"></el-radio-button>
19
+              <el-radio-button label="本季"></el-radio-button>
20
+              <el-radio-button label="本月"></el-radio-button>
21
+            </el-radio-group>
22
+          </div>
23
+          <div class="static-item item-space">
24
+            <el-date-picker
25
+              size="small"
26
+              v-model="filterData.dateRange"
27
+              type="daterange"
28
+              range-separator="至"
29
+              start-placeholder="开始日期"
30
+              end-placeholder="结束日期"
31
+              @change="handleDateChange">
32
+            </el-date-picker>
33
+          </div>
34
+        </div>
35
+        <el-row :gutter="12" type="flex">
36
+          <el-col :span="12">
37
+            <table-chart :page="visitingPage" :source="visitingList" @page-change="nextVistingLogPage"></table-chart>
38
+          </el-col>
39
+          <el-col :span="12">
40
+            <line-chart :source="dailyVisiting" :style="{minHeight: '400px'}"></line-chart>
41
+          </el-col>
42
+        </el-row>
43
+      </el-card>
44
+    </div>
4
   </div>
45
   </div>
5
 </template>
46
 </template>
6
 
47
 
48
+<script>
49
+import dayjs from 'dayjs'
50
+import { createNamespacedHelpers } from 'vuex'
51
+
52
+const {mapState: mapStatsState, mapActions: mapStatsActions} = createNamespacedHelpers('stats')
53
+
54
+export default {
55
+  name: 'dashboard',
56
+  components: {
57
+    lineChart: () => import('@/components/charts/VisitingDayLine.vue'),
58
+    tableChart: () => import('@/components/charts/VisitingDayTable.vue'),
59
+    statCard: () => import('@/components/charts/StatCard.vue'),
60
+  },
61
+  data () {
62
+    return {
63
+      filterData: {
64
+        dateRange: [],
65
+        dateShortcut: '',
66
+      },
67
+      visitingLogCurrentPage: 1,
68
+    }
69
+  },
70
+  computed: {
71
+    ...mapStatsState({
72
+      personDts: x => x.person,
73
+      dailyVisiting: x => x.visitingDaily,
74
+      visitingLog: x => x.visitingLog,
75
+    }),
76
+    personDt () {
77
+      return (this.personDts || [])[0] || {}
78
+    },
79
+    visitingPage () {
80
+      return {
81
+        total: (this.visitingLog || {}).total || 0,
82
+        size: (this.visitingLog || {}).size || 10,
83
+        current: this.visitingLogCurrentPage,
84
+      }
85
+    },
86
+    visitingList () {
87
+      return (this.visitingLog || {}).records || []
88
+    }
89
+  },
90
+  created () {
91
+    this.changeDate('week')
92
+    this.getPersonStats()
93
+  },
94
+  methods: {
95
+    ...mapStatsActions([
96
+      'getPersonStats',
97
+      'getVisitingDaily',
98
+      'getVisitingLog',
99
+    ]),
100
+    handleDateChange (dts) {
101
+      this.filterData.dateRange = dts
102
+      this.filterData.dateShortcut = ''
103
+      this.search()
104
+    },
105
+    search (rg) {
106
+      if (!rg || rg === 'visiting-daily') {
107
+        this.getVisitingDaily({
108
+          startDate: dayjs(this.filterData.dateRange[0]).format('YYYY-MM-DDT00:00:00'),
109
+          endDate: dayjs(this.filterData.dateRange[1]).format('YYYY-MM-DDT23:59:59'),
110
+        })
111
+      }
112
+
113
+      if (!rg || rg === 'visiting-log') {
114
+        this.getVisitingLog({
115
+          pageNum: this.visitingLogCurrentPage,
116
+          pageSize: this.visitingPage.size,
117
+          beginDate: dayjs(this.filterData.dateRange[0]).format('YYYY-MM-DDT00:00:00'),
118
+          endDate: dayjs(this.filterData.dateRange[1]).format('YYYY-MM-DDT23:59:59'),
119
+        })
120
+      }
121
+    },
122
+    changeDate (typ) {
123
+      this.filterData.dateRange = this.getDateRange(typ)
124
+      this.search()
125
+    },
126
+    nextVistingLogPage (page) {
127
+      this.visitingLogCurrentPage = page
128
+      this.search('visiting-log')
129
+    },
130
+    handeShortDateChange (val) {
131
+      switch (val) {
132
+        case '本年':
133
+          this.changeDate('year')
134
+          break
135
+        case '本季':
136
+          this.changeDate('quarter')
137
+          break
138
+        case '本月':
139
+          this.changeDate('month')
140
+          break
141
+      }
142
+    },
143
+    getDateRange(typ) {
144
+      // typ: https://github.com/iamkun/dayjs/blob/dev/src/constant.js#L13
145
+      const now = new Date();
146
+
147
+      const start = typ === 'quarter' ? this.getQuarterStart(now) : dayjs(now).startOf(typ).toDate()
148
+
149
+      return [ start, now ]
150
+    },
151
+    getQuarterStart(dt) {
152
+      const mon = dt.getMonth()
153
+      const year = dt.getFullYear()
154
+
155
+      if (mon < 3) return dayjs(`${year}-01-01`).toDate()
156
+      if (mon < 6) return dayjs(`${year}-03-01`).toDate()
157
+      if (mon < 9) return dayjs(`${year}-06-01`).toDate()
158
+
159
+      return dayjs(`${year}-09-01`).toDate()
160
+    }
161
+  }
162
+}
163
+</script>
164
+
165
+
7
 <style lang="scss" scoped>
166
 <style lang="scss" scoped>
8
-h1 {
9
-  text-align: center;
10
-  color: #333;
11
-  margin-top: 200px;
167
+.dash-main {
168
+  background-color: transparent !important;
169
+  padding: 20px 0;
170
+}
171
+
172
+.sta-row {
173
+  & + .sta-row {
174
+    margin-top: 24px;
175
+  }
176
+}
177
+
178
+.short-filters {
179
+  display: flex;
180
+
181
+  .item-space {
182
+    margin-left: 24px;
183
+  }
184
+
185
+  .flex-item {
186
+    flex: auto;
187
+  }
188
+
189
+  .static-item {
190
+    flex: none;
191
+  }
12
 }
192
 }
13
 </style>
193
 </style>
14
 
194
 
195
+<style lang="scss">
196
+.dash-main .el-card__header {
197
+  padding: 12px;
198
+}
199
+</style>

+ 186
- 3
src/views/device/list.vue ファイルの表示

41
         <span>{{FormatDate(scope.row.onlineDate)}}</span>
41
         <span>{{FormatDate(scope.row.onlineDate)}}</span>
42
       </template>
42
       </template>
43
     </el-table-column>
43
     </el-table-column>
44
+    <el-table-column
45
+      fixed="right"
46
+      label="操作">
47
+      <template slot-scope="scope">
48
+        <el-button type="text" @click="startDispatch(scope.row)" size="small">下发</el-button>
49
+        <el-button type="text" @click="gotoSnapshotList(scope.row)"  size="small">抓拍流水</el-button>
50
+      </template>
51
+    </el-table-column>
44
   </el-table>
52
   </el-table>
45
   <el-pagination
53
   <el-pagination
46
     small
54
     small
52
     @current-change="getDevices"
60
     @current-change="getDevices"
53
   >
61
   >
54
   </el-pagination>
62
   </el-pagination>
63
+  <el-dialog title="人脸批量下发" :visible.sync="dipatchProcess.showDialog" width="30%">
64
+    <el-alert
65
+      :title="`下发需要${ !duration ? '一段时间' : '大约 ' + duration + ' 分钟' }, 过程中, 请不要进行其他操作, 请耐心等候下发完成!`"
66
+      type="warning"
67
+      :closable="false">
68
+    </el-alert>
69
+
70
+    <el-row :style="{ marginTop: '20px' }">
71
+      <el-col :span="18">
72
+        <div :style="{ margin: '0 auto', width: '130px' }">
73
+          <el-progress type="circle" :percentage="dealInfo.percentage" :status="dealInfo.status">{{ `${dealInfo.percentage}%` }}</el-progress>
74
+        </div>
75
+      </el-col>
76
+      <el-col :span="6">
77
+        <el-button
78
+          type="primary"
79
+          @click="dispatchToDevice"
80
+          :loading="dipatchProcess.step == 'process'"
81
+          :disabled="dipatchProcess.step == 'finish'"
82
+          :style="{ marginTop: '30px'}"
83
+          >{{btnText}}</el-button>
84
+      </el-col>
85
+    </el-row>
86
+
87
+  </el-dialog>
55
 </div>
88
 </div>
56
 </template>
89
 </template>
57
 
90
 
63
 export default {
96
 export default {
64
   data () {
97
   data () {
65
     return {
98
     return {
99
+      ws: undefined,
100
+      wsId: undefined,
66
       currentPage: 1,
101
       currentPage: 1,
67
-      pageSize: 20
102
+      pageSize: 20,
103
+      checkDevice: undefined,
104
+      dipatchProcess: {
105
+        step: 'ready',
106
+        showDialog: false,
107
+        total: 0,
108
+        current: 0,
109
+        curPer: 0,
110
+        wsId: undefined,
111
+        exception: false,
112
+      },
113
+      lastTime: undefined,
114
+      duration: undefined,
68
     }
115
     }
69
   },
116
   },
70
   computed: {
117
   computed: {
71
     ...mapDeviceState({
118
     ...mapDeviceState({
72
       devices: x => x.devices
119
       devices: x => x.devices
73
-    })
120
+    }),
121
+    dealInfo () {
122
+      const { current, total, exception } =  this.dipatchProcess
123
+      const percentage = !total || total == 0 ? 0 : window.Math.ceil(current * 100 / total)
124
+      
125
+      const status = exception ? 'exception' : ( percentage >= 100 ? 'success' : 'text' )
126
+
127
+      return { percentage, status }
128
+    },
129
+    btnText () {
130
+      if (this.dipatchProcess.step == 'finish') return '下发完成'
131
+      if (this.dipatchProcess.step == 'process') return '进行中'
132
+
133
+      if (this.dipatchProcess.exception) return '重试'
134
+
135
+      return '开始'
136
+    },
74
   },
137
   },
75
   methods: {
138
   methods: {
76
     ...mapDeviceActions([
139
     ...mapDeviceActions([
77
-      'getDeviceList'
140
+      'getDeviceList',
141
+      'dispatchBatch',
78
     ]),
142
     ]),
79
     GetIndex (inx) {
143
     GetIndex (inx) {
80
       return (this.currentPage - 1) * this.pageSize + inx + 1
144
       return (this.currentPage - 1) * this.pageSize + inx + 1
89
         return ''
153
         return ''
90
       }
154
       }
91
     },
155
     },
156
+    gotoSnapshotList (row) {
157
+      this.$router.push({ name: 'snapshotlist', params: { device: row.deviceId } })
158
+    },
159
+    startDispatch (dev) {
160
+      this.checkDevice = dev
161
+      this.dipatchProcess = {
162
+        step: 'ready',
163
+        showDialog: true,
164
+        total: 0,
165
+        current: 0,
166
+        curPer: 0,
167
+        wsId: undefined,
168
+        exception: false,
169
+      }
170
+      this.lastTime = undefined
171
+      this.duration = undefined
172
+    },
173
+    dispatchToDevice () {
174
+      this.newWs().then(() => {
175
+
176
+        this.dipatchProcess.step = 'process'
177
+        this.dispatchBatch({
178
+          wsId: this.wsId,
179
+          from: this.dipatchProcess.curPer,
180
+          deviceId: this.checkDevice.deviceId,
181
+          lastNum: this.dipatchProcess.current,
182
+          }).then(() => {
183
+
184
+          // 成功完成下发
185
+          this.dipatchProcess.step = 'finish'
186
+          this.dipatchProcess.current = this.dipatchProcess.total
187
+          this.dipatchProcess.exception = false
188
+          this.lastTime = undefined
189
+          this.duration = undefined
190
+
191
+          this.destroyWs(true)
192
+        }).catch((e) => {
193
+          this.dipatchProcess.exception = true
194
+          this.dipatchProcess.step = 'ready'
195
+          this.lastTime = undefined
196
+          this.duration = undefined
197
+
198
+          this.$notify.error(e.message)
199
+        })
200
+      })
201
+
202
+    },
203
+    destroyWs () {
204
+      if (this.ws) {
205
+        this.ws = undefined
206
+        this.wsId = undefined
207
+      }
208
+    },
209
+    newWs () {
210
+      if (this.ws) return Promise.resolve()
211
+
212
+      return new Promise((resolve) => {
213
+        this.wsId = `admin-${window.Math.random().toString(36).substr(2)}`
214
+
215
+        const wsURL = `${window.location.origin.replace('http', 'ws')}/api/websocket/${this.wsId}`
216
+        this.ws = new WebSocket(wsURL)
217
+
218
+        this.ws.onopen = () => {
219
+          resolve()
220
+        }
221
+        
222
+        // 处理过程中
223
+        this.ws.onmessage = (e) => {
224
+          const { step, current, id, total } =  window.JSON.parse(e.data)
225
+
226
+          switch (step) {
227
+            case 'ready':
228
+              this.dipatchProcess.step = 'process'
229
+              this.dipatchProcess.total = total
230
+              this.dipatchProcess.exception = false
231
+              break
232
+            case 'process':
233
+              this.dipatchProcess.current = current
234
+              this.dipatchProcess.exception = false
235
+              this.dipatchProcess.curPer = id
236
+              this.compDuration()
237
+              break;
238
+            case 'finish':
239
+              break
240
+            default:
241
+              break
242
+          }
243
+        }
244
+
245
+        this.ws.onclose = () => {
246
+          window.console.error('websocket 连接被断开')
247
+
248
+          if (this.dipatchProcess.step == 'process') {
249
+            this.$notify('进度监控异常, 请耐心等待完成')
250
+          }
251
+
252
+          this.destroyWs()
253
+        },
254
+
255
+        this.ws.onerror = (e) => {
256
+          window.console.error(e)
257
+        }
258
+      })
259
+    },
260
+    compDuration () {
261
+      const { current, total } =  this.dipatchProcess
262
+      if (!total || total == 0) return 0
263
+
264
+      const now = new Date()
265
+      if (!this.lastTime) {
266
+        this.lastTime = now
267
+        return
268
+      }
269
+
270
+      const durPer = now.valueOf() - this.lastTime.valueOf()
271
+      const totalMin = (total - current) * durPer / ( 1000 * 60 )
272
+      this.duration = window.Number(totalMin).toFixed(1)
273
+      this.lastTime = now
274
+    }
92
   },
275
   },
93
   created () {
276
   created () {
94
     this.getDevices()
277
     this.getDevices()

+ 158
- 0
src/views/device/snapshot.vue ファイルの表示

1
+<template>
2
+<div>
3
+  <xm-search @submit="search">
4
+    <el-form-item>
5
+      <el-date-picker
6
+        v-model="filterData.dateRange"
7
+        type="daterange"
8
+        range-separator="至"
9
+        start-placeholder="开始日期"
10
+        end-placeholder="结束日期">
11
+      </el-date-picker>
12
+    </el-form-item>
13
+  </xm-search>
14
+
15
+  <el-card shadow="hover">
16
+    <el-table
17
+      :data="list"
18
+      style="width: 100%">
19
+      <el-table-column
20
+        label="抓拍日期"
21
+      >
22
+        <template slot-scope="scope">
23
+          <span>{{ formatDate(scope.row.createDate) }}</span>
24
+        </template>
25
+      </el-table-column>
26
+      <el-table-column
27
+        prop="personId"
28
+        label="入库ID">
29
+      </el-table-column>
30
+      <el-table-column
31
+        label="抓拍图">
32
+        <template slot-scope="scope">
33
+          <img class="avatar" :src="scope.row.avatar" alt="">
34
+        </template>
35
+      </el-table-column>
36
+      <el-table-column
37
+        prop="similarity"
38
+        label="相似度">
39
+      </el-table-column>
40
+      <el-table-column
41
+        label="匹配人">
42
+        <template slot-scope="scope">
43
+          <span>{{ scope.row.matchPerson > 0 ? `${scope.row.matchName || ''}(${scope.row.matchPerson})` : '' }}</span>
44
+        </template>
45
+      </el-table-column>
46
+      <el-table-column
47
+        label="匹配图">
48
+        <template slot-scope="scope">
49
+          <img class="avatar" :src="scope.row.matchAvatar" alt="">
50
+        </template>
51
+      </el-table-column>
52
+      <el-table-column
53
+        prop="score"
54
+        label="人像质量(0-100)">
55
+      </el-table-column>
56
+      <el-table-column
57
+        label="比对结果">
58
+        <template slot-scope="scope">
59
+          <el-tag v-if="scope.row.status == 1" type="success">成功</el-tag>
60
+          <el-tag v-else-if="scope.row.status == 2" type="danger">失败</el-tag>
61
+          <el-tag v-else type="info">未知</el-tag>
62
+        </template>
63
+      </el-table-column>
64
+    </el-table>
65
+  </el-card>
66
+  <el-pagination
67
+    small
68
+    style="margin-top:10px;"
69
+    layout="prev, pager, next"
70
+    :current-page="pageNavi.current"
71
+    :pageSize="pageNavi.size"
72
+    :total="pageNavi.total"
73
+    @current-change="handlePageChange"
74
+  >
75
+  </el-pagination>
76
+</div>
77
+</template>
78
+
79
+<script>
80
+import { createNamespacedHelpers } from 'vuex'
81
+import dayjs from 'dayjs';
82
+
83
+const {mapState, mapActions } = createNamespacedHelpers('device')
84
+
85
+export default {
86
+  data () {
87
+    return {
88
+      deviceId: undefined,
89
+      filterData: {
90
+        dateRange: [],
91
+      },
92
+    }
93
+  },
94
+  computed: {
95
+    ...mapState({
96
+      dtSource: x => x.snapshots,
97
+    }),
98
+    startDate () {
99
+      if (!this.filterData.dateRange || this.filterData.dateRange.length < 1) {
100
+        return undefined;
101
+      }
102
+
103
+      return dayjs(this.filterData.dateRange[0]).format('YYYY-MM-DDT00:00:00')
104
+    },
105
+    endDate () {
106
+      if (!this.filterData.dateRange || this.filterData.dateRange.length < 2) {
107
+        return undefined;
108
+      }
109
+
110
+      return dayjs(this.filterData.dateRange[1]).format('YYYY-MM-DDT23:59:59')
111
+    },
112
+    list () {
113
+      const { records = [] } = this.dtSource || {}
114
+      return records
115
+    },
116
+    pageNavi () {
117
+      const { current = 1, size = 20, total = 0 } = this.dtSource || {}
118
+      return { current, size, total }
119
+    }
120
+  },
121
+  methods: {
122
+    ...mapActions([
123
+      'getSnapshots'
124
+    ]),
125
+    handlePageChange (nwPage) {
126
+      this.search(nwPage)
127
+    },
128
+    search (page) {
129
+      this.getSnapshots({
130
+        pageNum: page || this.pageNavi.current,
131
+        pageSize: this.pageNavi.size,
132
+        deviceId: this.deviceId,
133
+        startDate: this.startDate,
134
+        endDate: this.endDate,
135
+      })
136
+    },
137
+    formatDate (dt) {
138
+      if (!dt || dt.indexOf('0001-') > -1) return ''
139
+      return dayjs(dt).format('YYYY-MM-DD HH:mm:ss')
140
+    },
141
+    init () {
142
+      this.deviceId = this.$route.params.device
143
+      this.filterData.dateRange = [dayjs().startOf('week').toDate(), new Date()]
144
+    }
145
+  },
146
+  created () {
147
+    this.init()
148
+    this.search()
149
+  }
150
+}
151
+</script>
152
+
153
+<style lang="scss" scoped>
154
+.avatar {
155
+  width: 64px;
156
+  height: 64px;
157
+}
158
+</style>

+ 11
- 3
src/views/index.js ファイルの表示

1
 
1
 
2
 const pages = [  
2
 const pages = [  
3
   {
3
   {
4
-    path: '',
4
+    path: 'dashboard',
5
     name: 'dashboard',
5
     name: 'dashboard',
6
     component: () => import('./Dashboard.vue'),
6
     component: () => import('./Dashboard.vue'),
7
-    redirect: { name: 'personlist' },
8
     meta: {
7
     meta: {
9
       menuShow: true,
8
       menuShow: true,
10
       title: 'Dashboard',
9
       title: 'Dashboard',
20
     },
19
     },
21
     children: [
20
     children: [
22
       {
21
       {
23
-        path: 'devicelist',
22
+        path: 'device',
24
         name: 'devicelist',
23
         name: 'devicelist',
25
         component: () => import('./device/list.vue'),
24
         component: () => import('./device/list.vue'),
26
         meta: {
25
         meta: {
28
           title: '设备管理',
27
           title: '设备管理',
29
         },
28
         },
30
       },
29
       },
30
+      {
31
+        path: 'snapshot/:device',
32
+        name: 'snapshotlist',
33
+        component: () => import('./device/snapshot.vue'),
34
+        meta: {
35
+          menuShow: false,
36
+          title: '抓拍列表',
37
+        },
38
+      },
31
       {
39
       {
32
         path: 'paramlist',
40
         path: 'paramlist',
33
         name: 'paramlist',
41
         name: 'paramlist',

+ 22
- 4
src/views/person/edit.vue ファイルの表示

5
         <el-form-item label="会员ID">
5
         <el-form-item label="会员ID">
6
           <el-input v-model="memProfile.personId" readonly></el-input>
6
           <el-input v-model="memProfile.personId" readonly></el-input>
7
         </el-form-item>
7
         </el-form-item>
8
+        <el-form-item label="真实ID">
9
+          <el-input v-model="memProfile.realId" readonly></el-input>
10
+        </el-form-item>
8
         <el-form-item label="会员姓名">
11
         <el-form-item label="会员姓名">
9
           <el-input v-model="memProfile.name"></el-input>
12
           <el-input v-model="memProfile.name"></el-input>
10
         </el-form-item>
13
         </el-form-item>
47
           <el-row>
50
           <el-row>
48
             <el-button type="primary" @click="onSubmit">保存</el-button>
51
             <el-button type="primary" @click="onSubmit">保存</el-button>
49
             <el-tooltip content="同步数据至各抓拍机" placement="top">
52
             <el-tooltip content="同步数据至各抓拍机" placement="top">
50
-              <el-button type="primary" class="right-space" @click="downFace" :loading="optLoading.sync" :disabled="!(memProfile.personId && memProfile.avatar)">同步</el-button>
53
+              <el-button type="warn" class="right-space" @click="downFace" :loading="optLoading.sync" :disabled="!(memProfile.personId && memProfile.avatar)">同步</el-button>
51
             </el-tooltip>
54
             </el-tooltip>
55
+            <!--
52
             <el-tooltip content="下发数据至各门禁" placement="top">
56
             <el-tooltip content="下发数据至各门禁" placement="top">
53
               <el-button type="success" @click="showDispatchDialog = true" :disabled="!(memProfile.personId && memProfile.avatar)">下发</el-button>
57
               <el-button type="success" @click="showDispatchDialog = true" :disabled="!(memProfile.personId && memProfile.avatar)">下发</el-button>
54
             </el-tooltip>
58
             </el-tooltip>
55
             <el-tooltip content="禁用各门禁权限" placement="top">
59
             <el-tooltip content="禁用各门禁权限" placement="top">
56
               <el-button type="danger" class="right-space" :loading="optLoading.forbid" :disabled="!(memProfile.personId && memProfile.avatar)" @click="deleteFromGuard">禁用</el-button>
60
               <el-button type="danger" class="right-space" :loading="optLoading.forbid" :disabled="!(memProfile.personId && memProfile.avatar)" @click="deleteFromGuard">禁用</el-button>
57
             </el-tooltip>
61
             </el-tooltip>
62
+            -->
58
             <el-button @click="onCancel">取消</el-button>
63
             <el-button @click="onCancel">取消</el-button>
59
           </el-row>
64
           </el-row>
60
         </el-form-item>
65
         </el-form-item>
71
       </el-dialog>
76
       </el-dialog>
72
     </el-tab-pane>
77
     </el-tab-pane>
73
     <el-tab-pane label="来访记录" name="visiting">
78
     <el-tab-pane label="来访记录" name="visiting">
74
-      <visitinglog :person="memProfile.personId" />
79
+      <visitinglog :person="memProfile.personId" :sales-list="salesList" @change="handleLogUpdate" />
75
     </el-tab-pane>
80
     </el-tab-pane>
76
   </el-tabs>
81
   </el-tabs>
77
 </template>
82
 </template>
81
 import apis from '../../config/api'
86
 import apis from '../../config/api'
82
 
87
 
83
 const { mapState: mapTypeState, mapActions: mapTypeActions } = createNamespacedHelpers('type')
88
 const { mapState: mapTypeState, mapActions: mapTypeActions } = createNamespacedHelpers('type')
89
+const { mapState: mapUserState, mapActions: mapUserActions } = createNamespacedHelpers('sysuser')
84
 const { mapState: mapMemberState, mapActions: mapMemberActions, mapMutations: mapMemberMutations } = createNamespacedHelpers('person')
90
 const { mapState: mapMemberState, mapActions: mapMemberActions, mapMutations: mapMemberMutations } = createNamespacedHelpers('person')
85
 
91
 
86
 export default {
92
 export default {
110
     }),
116
     }),
111
     ...mapMemberState({
117
     ...mapMemberState({
112
       detail: x => x.detail,
118
       detail: x => x.detail,
113
-      logs: x => x.visitingLog,
119
+      // logs: x => x.visitingLog,
114
       guards: x => x.guards,
120
       guards: x => x.guards,
115
     }),
121
     }),
122
+    ...mapUserState({
123
+      salesList: x => (x.list || {}).records || [],
124
+    }),
116
     memProfile: {
125
     memProfile: {
117
       get () {
126
       get () {
118
         return this.detail
127
         return this.detail
127
       'getList'
136
       'getList'
128
     ]),
137
     ]),
129
     ...mapMemberMutations([
138
     ...mapMemberMutations([
130
-      'updateDetail'
139
+      'updateDetail',
140
+      'updateLog',
131
     ]),
141
     ]),
132
     ...mapMemberActions([
142
     ...mapMemberActions([
133
       'getDetail',
143
       'getDetail',
138
       'forbidGuard',
148
       'forbidGuard',
139
       'getGuards',
149
       'getGuards',
140
     ]),
150
     ]),
151
+    ...mapUserActions([
152
+      'getSysUserList',
153
+    ]),
141
     onCancel () {
154
     onCancel () {
142
       // this.$router.push('personlist')
155
       // this.$router.push('personlist')
143
       this.$router.go(-1)
156
       this.$router.go(-1)
165
       this.imgurl = res.message
178
       this.imgurl = res.message
166
       this.loading.close()
179
       this.loading.close()
167
     },
180
     },
181
+    handleLogUpdate (log) {
182
+      this.updateLog(log)
183
+    },
168
     onSubmit () {
184
     onSubmit () {
169
       if (!this.memProfile.personId) {
185
       if (!this.memProfile.personId) {
170
         // 新增
186
         // 新增
234
 
250
 
235
       this.getGuards()
251
       this.getGuards()
236
 
252
 
253
+      this.getSysUserList({ pageNum: 1, pageSize: 999, isSales: 1 })
254
+
237
       this.getList({
255
       this.getList({
238
         pageNum: 1,
256
         pageNum: 1,
239
         pageSize: 999
257
         pageSize: 999

+ 73
- 23
src/views/person/list.vue ファイルの表示

32
     <el-card shadow="hover">
32
     <el-card shadow="hover">
33
       <div slot="header" class="clearfix">
33
       <div slot="header" class="clearfix">
34
         <span>
34
         <span>
35
-          <el-select v-model="activeType" @change="handleTypeSelect" placeholder="请选择">
36
-            <el-option label="全部" value="%"></el-option>
37
-            <el-option
38
-              v-for="item in (types.records || [])"
39
-              :key="item.typeId"
40
-              :label="item.typeName"
41
-              :value="`${item.typeId}`">
42
-            </el-option>
43
-          </el-select>
44
         </span>
35
         </span>
45
         <el-button
36
         <el-button
46
           type="primary"
37
           type="primary"
58
           prop="personId">
49
           prop="personId">
59
         </el-table-column>
50
         </el-table-column>
60
         <el-table-column
51
         <el-table-column
61
-          label="照片"
62
-          width="180">
52
+          label="照片">
63
           <template slot-scope="scope">
53
           <template slot-scope="scope">
64
             <div class="avatar">
54
             <div class="avatar">
65
               <img :src="scope.row.avatar" alt="" />
55
               <img :src="scope.row.avatar" alt="" />
66
             </div>
56
             </div>
67
           </template>
57
           </template>
68
         </el-table-column>
58
         </el-table-column>
69
-        <el-table-column
70
-          label="分类"
71
-        >
59
+        <el-table-column width="180">
60
+          <template slot="header" slot-scope="scope">
61
+            <el-dropdown @command="handleTypeSelect" trigger="click">
62
+              <span>
63
+                {{handleTitleName(scope) || '所属分类'}}
64
+                <i class="el-icon-arrow-down el-icon--right"></i>
65
+              </span>
66
+              <el-dropdown-menu slot="dropdown">
67
+                <el-dropdown-item command="%">全部</el-dropdown-item>
68
+                <el-dropdown-item
69
+                  v-for="item in (types.records || [])"
70
+                  :key="item.typeId"
71
+                  :command="`${item.typeId}`">
72
+                  {{item.typeName}}
73
+                </el-dropdown-item>
74
+              </el-dropdown-menu>
75
+            </el-dropdown>
76
+          </template>
72
           <template slot-scope="scope">
77
           <template slot-scope="scope">
73
-            <span>{{getTypeName(scope.row.typeId)}}</span>
78
+            <editable-select :dict="typeDict" :value="scope.row.typeId" v-on="{ change: handleRowChange(scope.row, 'typeId') }"></editable-select>
74
           </template>
79
           </template>
75
         </el-table-column>
80
         </el-table-column>
76
         <el-table-column
81
         <el-table-column
77
-          prop="name"
78
-          label="姓名"
82
+          label="姓名">
83
+          <template slot-scope="scope">
84
+            <editable-input :value="scope.row.name" v-on="{ change: handleRowChange(scope.row, 'name') }"></editable-input>
85
+          </template>
79
         >
86
         >
80
         </el-table-column>
87
         </el-table-column>
81
         <el-table-column
88
         <el-table-column
82
-          prop="phone"
83
           label="电话"
89
           label="电话"
84
         >
90
         >
91
+          <template slot-scope="scope">
92
+            <editable-input :value="scope.row.phone" v-on="{ change: handleRowChange(scope.row, 'phone') }"></editable-input>
93
+          </template>
85
         </el-table-column>
94
         </el-table-column>
86
         <el-table-column
95
         <el-table-column
87
           label="设备名称"
96
           label="设备名称"
100
         </el-table-column>
109
         </el-table-column>
101
         <el-table-column
110
         <el-table-column
102
           fixed="right"
111
           fixed="right"
103
-          label="操作"
104
-          width="100">
112
+          label="操作">
105
           <template slot-scope="scope">
113
           <template slot-scope="scope">
106
             <el-button type="text" @click="handleClick(scope.row, 'detail')" size="small">编辑</el-button>
114
             <el-button type="text" @click="handleClick(scope.row, 'detail')" size="small">编辑</el-button>
115
+            <el-button type="text" @click="showMergeDialog(scope.row)" size="small">合并</el-button>
107
             <el-button type="text" @click="handleClick(scope.row, 'visiting')" size="small">来访</el-button>
116
             <el-button type="text" @click="handleClick(scope.row, 'visiting')" size="small">来访</el-button>
108
             <!-- <el-button @click="handleDel(scope.row)" type="text" size="small">删除</el-button> -->
117
             <!-- <el-button @click="handleDel(scope.row)" type="text" size="small">删除</el-button> -->
109
           </template>
118
           </template>
124
       >
133
       >
125
       </el-pagination>
134
       </el-pagination>
126
     </xm-rtl>
135
     </xm-rtl>
136
+    <el-dialog title="人员合并" :visible.sync="mergeDialogVisible">
137
+      <merge-person
138
+        :from="mergeFrom"
139
+        :person-types="(types || {}).records"
140
+        @cancel="mergeDialogVisible = false"
141
+      ></merge-person>
142
+    </el-dialog>
127
   </div>
143
   </div>
128
 </template>
144
 </template>
129
 
145
 
132
 import { createNamespacedHelpers } from 'vuex'
148
 import { createNamespacedHelpers } from 'vuex'
133
 
149
 
134
 const {mapState: mapTypeState, mapActions: mapTypeActions} = createNamespacedHelpers('type')
150
 const {mapState: mapTypeState, mapActions: mapTypeActions} = createNamespacedHelpers('type')
135
-const {mapState: mapMemberState, mapActions: mapMemberActions} = createNamespacedHelpers('person')
151
+const {mapState: mapMemberState, mapActions: mapMemberActions, mapMutations: mapMemberMutations } = createNamespacedHelpers('person')
136
 const {mapState: mapDeviceState, mapActions: mapDeviceActions} = createNamespacedHelpers('device')
152
 const {mapState: mapDeviceState, mapActions: mapDeviceActions} = createNamespacedHelpers('device')
137
 
153
 
138
 export default {
154
 export default {
139
   name: 'person-list',
155
   name: 'person-list',
140
   components: {
156
   components: {
141
-    // visitinglog: () => import('./visitinglog.vue')
157
+    // visitinglog: () => import('./visitinglog.vue'),
158
+    editableInput: () => import('@/components/EditableInput.vue'),
159
+    editableSelect: () => import('@/components/EditableSelect.vue'),
160
+    mergePerson: () => import('./merge.vue')
142
   },
161
   },
143
   data () {
162
   data () {
144
     return {
163
     return {
151
       },
170
       },
152
       activeType: '%',
171
       activeType: '%',
153
       commPrefix: '',
172
       commPrefix: '',
173
+      mergeFrom: {},
154
       dialogVisible: false,
174
       dialogVisible: false,
175
+      mergeDialogVisible: false,
155
       searchData: {},
176
       searchData: {},
156
       initPage: 1,
177
       initPage: 1,
157
       currentPerson: 0,
178
       currentPerson: 0,
175
     perType () {
196
     perType () {
176
       return !this.activeType || this.activeType === '%' ? 0 : this.activeType
197
       return !this.activeType || this.activeType === '%' ? 0 : this.activeType
177
     },
198
     },
199
+    typeDict () {
200
+      return ((this.types || {}).records || []).map((item) => {
201
+        return {
202
+          value: item.typeId,
203
+          label: item.typeName,
204
+        }
205
+      })
206
+    }
178
   },
207
   },
179
   methods: {
208
   methods: {
180
     ...mapTypeActions([
209
     ...mapTypeActions([
181
       'getList'
210
       'getList'
182
     ]),
211
     ]),
212
+    ...mapMemberMutations([
213
+      'mergeList',
214
+    ]),
183
     ...mapMemberActions([
215
     ...mapMemberActions([
184
       'getPersonList',
216
       'getPersonList',
217
+      'editPerson',
185
       'setDetailNull',
218
       'setDetailNull',
186
       'deletePerson',
219
       'deletePerson',
187
     ]),
220
     ]),
188
     ...mapDeviceActions([
221
     ...mapDeviceActions([
189
       'getDeviceList'
222
       'getDeviceList'
190
     ]),
223
     ]),
224
+    handleTitleName () { return undefined },
225
+    handleRowChange (row, key) {
226
+      return (val) => {
227
+        const person = { ...row, [`${key}`]: val }
228
+
229
+        this.editPerson({
230
+          onSuccess: () => {
231
+            this.mergeList(person)
232
+          },
233
+          detail: JSON.stringify(person)
234
+        })
235
+      }
236
+    },
191
     handleTypeSelect (tab) {
237
     handleTypeSelect (tab) {
192
       const perType = !tab || tab === '%' ? 0 : tab
238
       const perType = !tab || tab === '%' ? 0 : tab
193
       this.refreshPage({ tab: perType })
239
       this.refreshPage({ tab: perType })
247
         })
293
         })
248
       })
294
       })
249
     },
295
     },
296
+    showMergeDialog(row) {
297
+      this.mergeFrom = row
298
+      this.mergeDialogVisible = true
299
+    },
250
     getSelectVal () {
300
     getSelectVal () {
251
       const bdt = this.filterData.beginDate ? dayjs(this.filterData.beginDate).format('YYYY-MM-DDTHH:mm:ss') : '';
301
       const bdt = this.filterData.beginDate ? dayjs(this.filterData.beginDate).format('YYYY-MM-DDTHH:mm:ss') : '';
252
       const edt = this.filterData.endDate ? dayjs(this.filterData.endDate).format('YYYY-MM-DDTHH:mm:ss') : ''
302
       const edt = this.filterData.endDate ? dayjs(this.filterData.endDate).format('YYYY-MM-DDTHH:mm:ss') : ''
286
     //   this.currentPerson = row.personId
336
     //   this.currentPerson = row.personId
287
     // },
337
     // },
288
     initData () {
338
     initData () {
289
-      this.activeType = this.$route.query.tab || '%'
339
+      this.activeType = this.$route.query.tab == 0 || this.$route.query.tab == undefined ? '%' : this.$route.query.tab
290
       this.initPage = (this.$route.query.page || 1) - 0
340
       this.initPage = (this.$route.query.page || 1) - 0
291
 
341
 
292
       this.getDeviceList({
342
       this.getDeviceList({

+ 196
- 0
src/views/person/merge.vue ファイルの表示

1
+<template>
2
+  <div class="merge-box">
3
+    <el-row class="merge-item merge-head" type="flex">
4
+      <el-col :span="2">#</el-col>
5
+      <el-col :span="5">待合并</el-col>
6
+      <el-col :span="1">&nbsp;</el-col>
7
+      <el-col :span="5">合并目标</el-col>
8
+      <el-col :span="1">&nbsp;</el-col>
9
+      <el-col :span="10">合并结果</el-col>
10
+    </el-row>
11
+    <el-row class="merge-item">
12
+      <el-col :span="2">人员ID</el-col>
13
+      <el-col :span="5">{{ from.personId || '&nbsp;' }}</el-col>
14
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
15
+      <el-col :span="5">
16
+        <el-input size="small" v-model="to.personId" @keyup.enter.native="getMergeTo"></el-input>
17
+      </el-col>
18
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
19
+      <el-col :span="10">
20
+        <el-input size="small" v-model="result.personId" readonly></el-input>
21
+      </el-col>
22
+    </el-row>
23
+    <el-row class="merge-item">
24
+      <el-col :span="2">识别ID</el-col>
25
+      <el-col :span="5">{{ from.realId || '&nbsp;' }}</el-col>
26
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
27
+      <el-col :span="5">{{ to.realId || '&nbsp;' }}</el-col>
28
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
29
+      <el-col :span="10">
30
+        <el-input size="small" v-model="result.realId" readonly></el-input>
31
+      </el-col>
32
+    </el-row>
33
+    <el-row class="merge-item">
34
+      <el-col :span="2">姓名</el-col>
35
+      <el-col :span="5">{{ from.name || '&nbsp;' }}</el-col>
36
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
37
+      <el-col :span="5">{{ to.name || '&nbsp;' }}</el-col>
38
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
39
+      <el-col :span="10">
40
+        <el-input size="small" v-model="result.name"></el-input>
41
+      </el-col>
42
+    </el-row>
43
+    <el-row class="merge-item">
44
+      <el-col :span="2">分类</el-col>
45
+      <el-col :span="5">{{ getTypeName(from.typeId) || '&nbsp;' }}</el-col>
46
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
47
+      <el-col :span="5">{{ getTypeName(to.typeId) || '&nbsp;' }}</el-col>
48
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
49
+      <el-col :span="10">
50
+        <el-select :style="{ width: '100%' }" size="small" v-model="result.typeId">
51
+          <el-option v-for="type in (personTypes || [])" :key="type.typeId" :label="type.typeName" :value="type.typeId"></el-option>
52
+        </el-select>
53
+      </el-col>
54
+    </el-row>
55
+    <el-row class="merge-item">
56
+      <el-col :span="2">手机</el-col>
57
+      <el-col :span="5">{{ from.phone || '&nbsp;' }}</el-col>
58
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
59
+      <el-col :span="5">{{ to.phone || '&nbsp;' }}</el-col>
60
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
61
+      <el-col :span="10">
62
+        <el-input size="small" v-model="result.phone"></el-input>
63
+      </el-col>
64
+    </el-row>
65
+    <el-row class="merge-item">
66
+      <el-col :span="2">邮箱</el-col>
67
+      <el-col :span="5">{{ from.email || '&nbsp;' }}</el-col>
68
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
69
+      <el-col :span="5">{{ to.email || '&nbsp;' }}</el-col>
70
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
71
+      <el-col :span="10">
72
+        <el-input size="small" v-model="result.email"></el-input>
73
+      </el-col>
74
+    </el-row>
75
+    <el-row class="merge-item">
76
+      <el-col :span="2">性别</el-col>
77
+      <el-col :span="5">{{ from.sex == 1 ? '男' : '女' }}</el-col>
78
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
79
+      <el-col :span="5">{{ to.sex == 1 ? '男' : '女' }}</el-col>
80
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
81
+      <el-col :span="10">
82
+        <el-select :style="{ width: '100%' }" size="small" v-model="result.sex">
83
+          <el-option label="男" value="1"></el-option>
84
+          <el-option label="女" value="2"></el-option>
85
+        </el-select>
86
+      </el-col>
87
+    </el-row>
88
+    <el-row class="merge-item">
89
+      <el-col :span="2">头像</el-col>
90
+      <el-col :span="5"><img class="avatar" :src="from.avatar" alt=""></el-col>
91
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
92
+      <el-col :span="5"><img class="avatar" :src="to.avatar" alt=""></el-col>
93
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
94
+      <el-col :span="10">头像不处理, 两个头像均会被保留</el-col>
95
+    </el-row>
96
+    <el-row class="merge-item">
97
+      <el-col :span="2">欢迎语</el-col>
98
+      <el-col :span="5">{{ from.words || '&nbsp;' }}</el-col>
99
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
100
+      <el-col :span="5">{{ to.words || '&nbsp;' }}</el-col>
101
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
102
+      <el-col :span="10">
103
+        <el-input size="small" v-model="result.words"></el-input>
104
+      </el-col>
105
+    </el-row>
106
+    <el-row class="merge-item">
107
+      <el-col :span="2">备注</el-col>
108
+      <el-col :span="5">{{ from.remark || '&nbsp;' }}</el-col>
109
+      <el-col :span="1"><i class="el-icon-plus"></i></el-col>
110
+      <el-col :span="5">{{ to.remark || '&nbsp;' }}</el-col>
111
+      <el-col :span="1"><i class="el-icon-arrow-right"></i></el-col>
112
+      <el-col :span="10">
113
+        <el-input size="small" v-model="result.remark"></el-input>
114
+      </el-col>
115
+    </el-row>
116
+    <div :style="{ margin: '20px auto', width: '300px' }">
117
+      <el-button @click="handleCancel" :style="{ marginRight: '48px' }">取消</el-button>
118
+      <el-button type="primary" @click="submit" >确定</el-button>
119
+    </div>
120
+  </div>
121
+</template>
122
+
123
+<script>
124
+import { createNamespacedHelpers } from 'vuex'
125
+
126
+const { mapActions } = createNamespacedHelpers('person')
127
+
128
+export default {
129
+  name: 'person-merge',
130
+  props: [
131
+    'from',
132
+    'personTypes',
133
+  ],
134
+  data () {
135
+    return {
136
+      to: {
137
+        personId: undefined,
138
+      },
139
+      result: {},
140
+    }
141
+  },
142
+  computed: {
143
+  },
144
+  methods: {
145
+    ...mapActions([
146
+      'getPurPerson',
147
+      'mergePerson',
148
+    ]),
149
+    getMergeTo () {
150
+      this.getPurPerson({ id: this.to.personId }).then(({ detail }) => {
151
+        this.to = detail
152
+        this.result = detail
153
+      })
154
+    },
155
+    getTypeName (typeId) {
156
+      return ((this.personTypes || []).filter(x => x.typeId === typeId)[0] || {}).typeName
157
+    },
158
+    handleCancel () {
159
+      this.$emit('cancel')
160
+    },
161
+    submit() {
162
+      this.mergePerson({ from: this.from.personId, to: this.result }).then(() => {
163
+        this.$notify.success('合并成功')
164
+      })
165
+    }
166
+  }
167
+}
168
+</script>
169
+
170
+<style lang="scss" scoped>
171
+.merge-box {
172
+
173
+  .merge-head {
174
+    font-size: 1.2em;
175
+    font-weight: 500;
176
+  }
177
+
178
+  .merge-item {
179
+    padding: 12px 0;
180
+
181
+    .el-col {
182
+      text-align: center;
183
+      line-height: 32px;
184
+    }
185
+
186
+    & + .merge-item {
187
+      border-top: 1px solid #ebeef5;
188
+    }
189
+
190
+    .avatar {
191
+      width: 64px;
192
+      height: 64px;
193
+    }
194
+  }
195
+}
196
+</style>

+ 36
- 3
src/views/person/visitinglog.vue ファイルの表示

57
           label="来访目的"
57
           label="来访目的"
58
         >
58
         >
59
         </el-table-column>
59
         </el-table-column>
60
+        <el-table-column
61
+          prop="salesName"
62
+          label="销售"
63
+        >
64
+        </el-table-column>
65
+        <el-table-column
66
+          prop="remark"
67
+          label="备注"
68
+        >
69
+        </el-table-column>
60
         <el-table-column
70
         <el-table-column
61
           fixed="right"
71
           fixed="right"
62
           label="操作"
72
           label="操作"
87
           <el-form-item label-width="120px" label="来访目的">
97
           <el-form-item label-width="120px" label="来访目的">
88
             <el-input v-model="logDetail.visitPurpose"></el-input>
98
             <el-input v-model="logDetail.visitPurpose"></el-input>
89
           </el-form-item>
99
           </el-form-item>
100
+          <el-form-item label-width="120px" label="跟踪销售">
101
+            <el-select v-model="logDetail.salesId" @change="handleSalesChange" placeholder="请选择">
102
+              <el-option
103
+                v-for="sales in salesList"
104
+                :key="sales.userId"
105
+                :label="sales.nickname"
106
+                :value="sales.userId">
107
+              </el-option>
108
+            </el-select>
109
+          </el-form-item>
90
           <el-form-item label-width="120px" label="备注">
110
           <el-form-item label-width="120px" label="备注">
91
             <el-input type="textarea" rows="10" v-model="logDetail.remark"></el-input>
111
             <el-input type="textarea" rows="10" v-model="logDetail.remark"></el-input>
92
           </el-form-item>
112
           </el-form-item>
129
   props: [
149
   props: [
130
     'person',
150
     'person',
131
     'pageSize',
151
     'pageSize',
152
+    'salesList',
132
   ],
153
   ],
133
   data () {
154
   data () {
134
     return {
155
     return {
140
         total: 0,
161
         total: 0,
141
         size: 10,
162
         size: 10,
142
       },
163
       },
143
-      logDetail: {}
164
+      logDetail: {},
144
     }
165
     }
145
   },
166
   },
146
   computed: {
167
   computed: {
148
       visitingLogs: x => x.visitingLog,
169
       visitingLogs: x => x.visitingLog,
149
     }),
170
     }),
150
   },
171
   },
172
+  watch: {
173
+    person () {
174
+      this.getDataPaged()
175
+    }
176
+  },
151
   created () {
177
   created () {
152
     this.getDataPaged()
178
     this.getDataPaged()
153
   },
179
   },
161
     ...mapMemberMutations([
187
     ...mapMemberMutations([
162
       'resetVisitingLog',
188
       'resetVisitingLog',
163
     ]),
189
     ]),
190
+    handleSalesChange (salesId) {
191
+      this.logDetail.salesName = this.salesList.filter(x => x.userId === salesId)[0].nickname
192
+    },
164
     getDataPaged (page = 1) {
193
     getDataPaged (page = 1) {
165
       if (!this.person) return
194
       if (!this.person) return
166
 
195
 
179
       this.editVisibleLog({detail:JSON.stringify(this.logDetail), logId: this.logDetail.logId}).then(() => {
208
       this.editVisibleLog({detail:JSON.stringify(this.logDetail), logId: this.logDetail.logId}).then(() => {
180
         this.dialogShow = false
209
         this.dialogShow = false
181
 
210
 
211
+        this.visitingLogs = this.visitingLogs.map((log) => {
212
+          return log.logId === this.logDetail.logId ? { ...this.logDetail } : log
213
+        })
214
+
182
         this.$emit('change', this.logDetail)
215
         this.$emit('change', this.logDetail)
183
       })
216
       })
184
     },
217
     },
185
     handleEdit (dt) {
218
     handleEdit (dt) {
186
-      this.logDetail = dt
219
+      this.logDetail = { ...dt }
187
       this.dialogShow = true
220
       this.dialogShow = true
188
     },
221
     },
189
     dispatchHik (dt) {
222
     dispatchHik (dt) {
204
         return ''
237
         return ''
205
       }
238
       }
206
 
239
 
207
-      return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
240
+      return dayjs(date).format('YYYY-MM-DD HH:mm')
208
     },
241
     },
209
     pageChange (page) {
242
     pageChange (page) {
210
       this.$emit("pageChange", page)
243
       this.$emit("pageChange", page)

+ 23
- 0
src/views/statis/visiting/index.vue ファイルの表示

1
+<template>
2
+<div>
3
+  <xm-search></xm-search>
4
+  <el-card shadow="hover">
5
+    
6
+  </el-card>
7
+  <el-el-pagination></el-el-pagination>
8
+</div>
9
+</template>
10
+
11
+<script>
12
+  export default {
13
+    name: 'sta-visiting',
14
+    data () {
15
+      return {
16
+
17
+      }
18
+    },
19
+    methods: {
20
+
21
+    },
22
+  }
23
+</script>

+ 14
- 12
src/views/words/edit.vue ファイルの表示

23
     <el-form-item label="欢迎语" prop="words" required>
23
     <el-form-item label="欢迎语" prop="words" required>
24
       <el-input v-model="detail.words"></el-input>
24
       <el-input v-model="detail.words"></el-input>
25
     </el-form-item>
25
     </el-form-item>
26
+    <!--
26
     <el-form-item label="权重" prop="weight" required>
27
     <el-form-item label="权重" prop="weight" required>
27
       <el-input type="number" v-model="detail.weight"></el-input>
28
       <el-input type="number" v-model="detail.weight"></el-input>
28
     </el-form-item>
29
     </el-form-item>
30
+    -->
29
     <el-form-item label="状态" prop="status" required>
31
     <el-form-item label="状态" prop="status" required>
30
       <el-select v-model="detail.status" placeholder="请选择">
32
       <el-select v-model="detail.status" placeholder="请选择">
31
         <el-option label="禁用" :value="0"></el-option>
33
         <el-option label="禁用" :value="0"></el-option>
64
         status: [
66
         status: [
65
           { required: true, message: '请选择状态', trigger: 'change' }
67
           { required: true, message: '请选择状态', trigger: 'change' }
66
         ],
68
         ],
67
-        weight: [{
68
-          type: 'integer',
69
-          required: true,
70
-          trigger: 'blur',
71
-          validator: (rule, value, callback) => {
72
-            if (value <= 1 && value >= 0.1) {
73
-              callback()
74
-            } else {
75
-              callback(new Error('权重值 0.1 ~ 1, 默认 0.85'))
76
-            }
77
-          }
78
-        }],
69
+        // weight: [{
70
+        //   type: 'integer',
71
+        //   required: true,
72
+        //   trigger: 'blur',
73
+        //   validator: (rule, value, callback) => {
74
+        //     if (value <= 1 && value >= 0.1) {
75
+        //       callback()
76
+        //     } else {
77
+        //       callback(new Error('权重值 0.1 ~ 1, 默认 0.85'))
78
+        //     }
79
+        //   }
80
+        // }],
79
       }
81
       }
80
     }
82
     }
81
   },
83
   },

+ 17
- 5
vue.config.js ファイルの表示

3
   devServer: {
3
   devServer: {
4
     proxy: {
4
     proxy: {
5
       '/api': {
5
       '/api': {
6
-        target: 'http://10.168.2.125:8000',
6
+        target: 'http://localhost:8080',
7
         changeOrigin: true,
7
         changeOrigin: true,
8
-        // pathRewrite: {
9
-        //   '^/api': '/'
10
-        // },
8
+        pathRewrite: {
9
+          '^/api': '/'
10
+        },
11
+      },
12
+      '/api/websocket': {
13
+        target: 'ws://localhost:8080',
14
+        changeOrigin: true,
15
+        ws: true,
16
+        pathRewrite: {
17
+          '^/api': '/'
18
+        },
11
       },
19
       },
12
     }
20
     }
13
-  }
21
+  },
22
+  transpileDependencies: [
23
+    'vue-echarts',
24
+    'resize-detector'
25
+  ]
14
 }
26
 }