Commit 0252319111de5e28ad6ec38632d1516f2747c867

Authored by ly525
1 parent 16f173df

feat(#30): collect and show form data

back-end/h5-api/api/work/config/routes.json
@@ -63,6 +63,14 @@ @@ -63,6 +63,14 @@
63 "config": { 63 "config": {
64 "policies": [] 64 "policies": []
65 } 65 }
  66 + },
  67 + {
  68 + "method": "GET",
  69 + "path": "/works/form/query/:id",
  70 + "handler": "Work.queryFormsOfOneWork",
  71 + "config": {
  72 + "policies": []
  73 + }
66 } 74 }
67 ] 75 ]
68 } 76 }
back-end/h5-api/api/work/controllers/Work.js
@@ -21,4 +21,32 @@ module.exports = { @@ -21,4 +21,32 @@ module.exports = {
21 // eslint-disable-next-line require-atomic-updates 21 // eslint-disable-next-line require-atomic-updates
22 ctx.body = { message: 'success', status: 0 }; 22 ctx.body = { message: 'success', status: 0 };
23 }, 23 },
  24 + queryFormsOfOneWork: async (ctx) => {
  25 + // move to util module or front-end
  26 + function getUuidMap2Name(work) {
  27 + const uuidMap2Name = {};
  28 + work.pages.forEach(page => {
  29 + page.elements.forEach(ele => {
  30 + if (ele.name === 'lbp-form-input') {
  31 + uuidMap2Name[ele.uuid] = ele.pluginProps.placeholder;
  32 + }
  33 + });
  34 + });
  35 + return uuidMap2Name;
  36 + }
  37 +
  38 + let work = await strapi.services.work.findOne(ctx.params);
  39 + work = work.toJSON();
  40 +
  41 + // learn the query from: https://github.com/strapi/foodadvisor/blob/master/api/api/restaurant/controllers/Restaurant.js#L40
  42 + // eslint-disable-next-line no-undef
  43 + let formDetails = await Workform.query(qb => {
  44 + qb.where('work', '=', work.id);
  45 + }).fetchAll();
  46 + formDetails = formDetails.toJSON();
  47 +
  48 + const uuidMap2Name = getUuidMap2Name(work);
  49 + // eslint-disable-next-line require-atomic-updates
  50 + return ctx.body = { uuidMap2Name, formDetails };
  51 + },
24 }; 52 };
front-end/h5/src/router.js
@@ -23,7 +23,12 @@ export default new Router({ @@ -23,7 +23,12 @@ export default new Router({
23 { 23 {
24 path: '/work-manager/form-stat', 24 path: '/work-manager/form-stat',
25 name: 'form-stat', 25 name: 'form-stat',
26 - component: () => import('@/views/work-manager/form-stat.vue') 26 + component: () => import('@/views/work-manager/form-stat/index.vue')
  27 + },
  28 + {
  29 + path: '/work-manager/stat-detail/:id',
  30 + name: 'stat-detail',
  31 + component: () => import('@/views/work-manager/form-stat/detail.vue')
27 } 32 }
28 ] 33 ]
29 }, 34 },
front-end/h5/src/store/modules/editor.js
@@ -9,7 +9,11 @@ const state = { @@ -9,7 +9,11 @@ const state = {
9 work: new Work(), 9 work: new Work(),
10 editingPage: { elements: [] }, 10 editingPage: { elements: [] },
11 editingElement: null, 11 editingElement: null,
12 - editingElementEditorConfig: null 12 + editingElementEditorConfig: null,
  13 + formDetailOfWork: {
  14 + uuidMap2Name: {},
  15 + formDetails: []
  16 + }
13 } 17 }
14 18
15 // getters 19 // getters
front-end/h5/src/store/modules/work.js
@@ -37,6 +37,7 @@ export const actions = { @@ -37,6 +37,7 @@ export const actions = {
37 37
38 new AxiosWrapper({ 38 new AxiosWrapper({
39 dispatch, 39 dispatch,
  40 + commit,
40 loading_name: 'saveWork_loading', 41 loading_name: 'saveWork_loading',
41 successMsg: '保存作品成功', 42 successMsg: '保存作品成功',
42 customRequest: strapi.updateEntry.bind(strapi) 43 customRequest: strapi.updateEntry.bind(strapi)
@@ -52,6 +53,76 @@ export const actions = { @@ -52,6 +53,76 @@ export const actions = {
52 strapi.getEntries('works', {}).then(entries => { 53 strapi.getEntries('works', {}).then(entries => {
53 commit('setWorks', entries) 54 commit('setWorks', entries)
54 }) 55 })
  56 + },
  57 + /**
  58 + *
  59 + * @param {*} workId
  60 + * response demo:
  61 + {
  62 + "uuidMap2Name": {
  63 + "1565596393441": "姓名",
  64 + "1565596397671": "学校"
  65 + },
  66 + "formDetails": [
  67 + {
  68 + "id": 3,
  69 + "form": {
  70 + "1565369322603": "abc"
  71 + },
  72 + "work": 8,
  73 + "created_at": "2019-08-09T16:52:28.826Z",
  74 + "updated_at": "2019-08-09T16:52:28.832Z"
  75 + },
  76 + {
  77 + "id": 4,
  78 + "form": {
  79 + "1565595388440": "ddd"
  80 + },
  81 + "work": 8,
  82 + "created_at": "2019-08-11T07:36:54.521Z",
  83 + "updated_at": "2019-08-11T07:36:54.526Z"
  84 + },
  85 + {
  86 + "id": 5,
  87 + "form": {
  88 + "1565595388440": "acd"
  89 + },
  90 + "work": 8,
  91 + "created_at": "2019-08-11T07:45:22.000Z",
  92 + "updated_at": "2019-08-11T07:45:22.005Z"
  93 + },
  94 + {
  95 + "id": 6,
  96 + "form": {
  97 + "1565596393441": "b",
  98 + "1565596397671": "a"
  99 + },
  100 + "work": 8,
  101 + "created_at": "2019-08-11T07:59:00.938Z",
  102 + "updated_at": "2019-08-11T07:59:00.943Z"
  103 + },
  104 + {
  105 + "id": 7,
  106 + "form": {
  107 + "1565596393441": "b",
  108 + "1565596397671": "a"
  109 + },
  110 + "work": 8,
  111 + "created_at": "2019-08-11T07:59:37.065Z",
  112 + "updated_at": "2019-08-11T07:59:37.070Z"
  113 + }
  114 + ]
  115 + }
  116 + */
  117 + fetchFormsOfWork ({ commit, state, dispatch }, workId) {
  118 + // TODO 考虑 return Promise
  119 + new AxiosWrapper({
  120 + dispatch,
  121 + commit,
  122 + name: 'editor/formDetailOfWork',
  123 + loading_name: 'queryFormsOfWork_loading',
  124 + successMsg: '表单查询完毕'
  125 + }).get(`/works/form/query/${workId}`)
55 } 126 }
56 } 127 }
57 128
@@ -72,5 +143,8 @@ export const mutations = { @@ -72,5 +143,8 @@ export const mutations = {
72 // state.work = new Work() 143 // state.work = new Work()
73 // }, 144 // },
74 previewWork (state, { type, value }) {}, 145 previewWork (state, { type, value }) {},
75 - deployWork (state, { type, value }) {} 146 + deployWork (state, { type, value }) {},
  147 + formDetailOfWork (state, { type, value }) {
  148 + state.formDetailOfWork = value
  149 + }
76 } 150 }
front-end/h5/src/utils/http.js
@@ -17,13 +17,14 @@ export const baseClient = axios.create({ @@ -17,13 +17,14 @@ export const baseClient = axios.create({
17 17
18 export class AxiosWrapper { 18 export class AxiosWrapper {
19 // eslint-disable-next-line camelcase 19 // eslint-disable-next-line camelcase
20 - constructor ({ name = 'default', loading_name, responseType = 'json', headers, dispatch, router, successMsg, failMsg, successCallback, failCallback, customRequest }) { 20 + constructor ({ name = 'default', loading_name, responseType = 'json', headers, dispatch, commit, router, successMsg, failMsg, successCallback, failCallback, customRequest }) {
21 this.name = name 21 this.name = name
22 // eslint-disable-next-line camelcase 22 // eslint-disable-next-line camelcase
23 this.loading_name = loading_name 23 this.loading_name = loading_name
24 // eslint-disable-next-line camelcase 24 // eslint-disable-next-line camelcase
25 this.responseType = responseType 25 this.responseType = responseType
26 this.dispatch = dispatch 26 this.dispatch = dispatch
  27 + this.commit = commit
27 this.router = router 28 this.router = router
28 this.successMsg = successMsg 29 this.successMsg = successMsg
29 this.failMsg = failMsg 30 this.failMsg = failMsg
@@ -80,7 +81,7 @@ export class AxiosWrapper { @@ -80,7 +81,7 @@ export class AxiosWrapper {
80 return this.customRequest(...args) 81 return this.customRequest(...args)
81 .then(data => { 82 .then(data => {
82 const handler = this.getCommonResponseHandler({ failMsg: 'Save Failed.' }) 83 const handler = this.getCommonResponseHandler({ failMsg: 'Save Failed.' })
83 - handler.call(this, { data: { status: 200, data } }) 84 + handler.call(this, { status: 200, data: { data } })
84 }) 85 })
85 .finally(() => this.setLoadingValue(false)) 86 .finally(() => this.setLoadingValue(false))
86 } 87 }
@@ -112,7 +113,8 @@ export class AxiosWrapper { @@ -112,7 +113,8 @@ export class AxiosWrapper {
112 } 113 }
113 114
114 setLoadingValue (payload) { 115 setLoadingValue (payload) {
115 - this.dispatch('loading/update', { type: this.loading_name, payload }, { root: true }) 116 + // this.dispatch('loading/update', { type: this.loading_name, payload }, { root: true })
  117 + this.commit('loading/update', { type: this.loading_name, payload }, { root: true })
116 } 118 }
117 119
118 setDefaultLoadingName (...args) { 120 setDefaultLoadingName (...args) {
@@ -134,16 +136,16 @@ export class AxiosWrapper { @@ -134,16 +136,16 @@ export class AxiosWrapper {
134 return (response) => { 136 return (response) => {
135 if (!response.data) { 137 if (!response.data) {
136 myMessage.warn(this.failMsg || failMsg) 138 myMessage.warn(this.failMsg || failMsg)
137 - } else if (response.data.status === 200) { 139 + } else if (response.status === 200) {
138 this.successMsg && myMessage.success(this.successMsg) 140 this.successMsg && myMessage.success(this.successMsg)
139 if (this.successCallback) { 141 if (this.successCallback) {
140 this.successCallback(response) 142 this.successCallback(response)
141 } else { 143 } else {
142 - // this.dispatch({ type: this.name, payload: response.data.data }) 144 + this.commit({ type: this.name, value: response.data }, { root: true })
143 } 145 }
144 } else if (this.responseType === 'json') { 146 } else if (this.responseType === 'json') {
145 myMessage.error(response.data.msg) 147 myMessage.error(response.data.msg)
146 - if (response.data.status === 401) { 148 + if (response.status === 401) {
147 if (this.router) { 149 if (this.router) {
148 this.router.push('/login') 150 this.router.push('/login')
149 } 151 }
front-end/h5/src/views/work-manager/form-stat.vue deleted 100644 → 0
front-end/h5/src/views/work-manager/form-stat/column.js 0 → 100644
  1 +export const columns = [
  2 + {
  3 + title: '标题',
  4 + dataIndex: 'title',
  5 + key: 'title'
  6 + },
  7 + {
  8 + title: 'PV',
  9 + dataIndex: 'pv',
  10 + key: 'pv'
  11 + },
  12 + {
  13 + title: 'Uv',
  14 + dataIndex: 'uv',
  15 + key: 'uv'
  16 + },
  17 + {
  18 + title: '表单数',
  19 + key: 'formCount',
  20 + dataIndex: 'formCount'
  21 + },
  22 + {
  23 + title: 'Action',
  24 + key: 'action',
  25 + scopedSlots: { customRender: 'action' }
  26 + }
  27 +]
  28 +
  29 +export const data = [
  30 + {
  31 + key: '1',
  32 + title: 'John Brown',
  33 + pv: 32,
  34 + uv: 32,
  35 + formCount: 2
  36 + },
  37 + {
  38 + key: '2',
  39 + title: 'John Brown2',
  40 + pv: 32,
  41 + uv: 32,
  42 + formCount: 2
  43 + },
  44 + {
  45 + key: '3',
  46 + title: 'John Brown3',
  47 + pv: 32,
  48 + uv: 32,
  49 + formCount: 2
  50 + }
  51 +]
front-end/h5/src/views/work-manager/form-stat/detail.vue 0 → 100644
  1 +<script>
  2 +/**
  3 + * [基础数据](/work-manager/form-stat) 对应的页面
  4 + *
  5 + */
  6 +import { mapState, mapActions } from 'vuex'
  7 +
  8 +export default {
  9 + components: {
  10 + },
  11 + data: () => ({
  12 + activeWork: null,
  13 + previewVisible: false
  14 + }),
  15 + computed: {
  16 + ...mapState('editor', ['works', 'formDetailOfWork']),
  17 + computedWorks () {
  18 + return this.works.map(w => ({
  19 + id: w.id,
  20 + title: w.title,
  21 + pv: w.pv || 0,
  22 + uv: w.uv || 0,
  23 + formCount: w.formCount || 0
  24 + }))
  25 + },
  26 + /**
  27 + * columns demo: [{"1565369322603":"abc"},{"1565595388440":"ddd"},{"1565595388440":"acd"},{"1565596393441":"b","1565596397671":"a"},{"1565596393441":"b","1565596397671":"a"}]
  28 + */
  29 + columns () {
  30 + const { uuidMap2Name } = this.formDetailOfWork
  31 + // the uuid for input plugin
  32 + return Object.entries(uuidMap2Name).map(([uuid, inputName]) => ({
  33 + title: inputName,
  34 + key: `uuid-${uuid}`,
  35 + dataIndex: `uuid-${uuid}`
  36 + }))
  37 + },
  38 + /**
  39 + * rows demo: [{"title":"姓名","key":"1565596393441"},{"title":"学校","key":"1565596397671"}]
  40 + *
  41 + * formDetails example: <[{
  42 + "id": 4,
  43 + "form": {
  44 + "1565595388440": "ddd",
  45 + 1234: 'abc'
  46 + },
  47 + "work": 8,
  48 + "created_at": "2019-08-11T07:36:54.521Z",
  49 + "updated_at": "2019-08-11T07:36:54.526Z"
  50 + }]>
  51 + */
  52 + rows () {
  53 + const { formDetails, uuidMap2Name } = this.formDetailOfWork
  54 + const rows = formDetails.map(({ form, id }) => {
  55 + const row = {}
  56 + Object.entries(form).forEach(([uuid, inputValue = '-']) => {
  57 + if (uuidMap2Name[uuid]) {
  58 + row[`uuid-${uuid}`] = inputValue
  59 + row.id = id
  60 + }
  61 + })
  62 + return row
  63 + })
  64 + return rows.filter(row => Object.keys(row).length)
  65 + }
  66 + },
  67 + methods: {
  68 + ...mapActions('editor', [
  69 + 'fetchWorks',
  70 + 'fetchFormsOfWork'
  71 + ]),
  72 + deleteWork (item) {
  73 + // TODO delete work from work list
  74 + },
  75 + createWork () {
  76 + this.$router.push({ name: 'editor' })
  77 + }
  78 + },
  79 + render (h) {
  80 + return (
  81 + <div class="works-wrapper">
  82 + <a-table columns={this.columns} dataSource={this.rows} row-key="id" scopedSlots={{
  83 + action: function (props) {
  84 + return [<router-link to={{ name: 'stat-detail', params: { id: props.id } }} >查看数据</router-link>]
  85 + }
  86 + }}>
  87 + </a-table>
  88 + </div>
  89 + )
  90 + },
  91 + created () {
  92 + // this.fetchWorks()
  93 + const workId = this.$route.params.id
  94 + this.fetchFormsOfWork(workId)
  95 + }
  96 +}
  97 +</script>
front-end/h5/src/views/work-manager/form-stat/index.vue 0 → 100644
  1 +<script>
  2 +/**
  3 + * [基础数据](/work-manager/form-stat) 对应的页面
  4 + *
  5 + */
  6 +import { mapState, mapActions } from 'vuex'
  7 +import { columns } from './column'
  8 +
  9 +export default {
  10 + components: {
  11 + },
  12 + data: () => ({
  13 + activeWork: null,
  14 + previewVisible: false
  15 + }),
  16 + computed: {
  17 + ...mapState('editor', ['works']),
  18 + computedWorks () {
  19 + return this.works.map(w => ({
  20 + id: w.id,
  21 + title: w.title,
  22 + pv: w.pv || 0,
  23 + uv: w.uv || 0,
  24 + formCount: w.formCount || 0
  25 + }))
  26 + }
  27 + },
  28 + methods: {
  29 + ...mapActions('editor', [
  30 + 'fetchWorks'
  31 + ]),
  32 + deleteWork (item) {
  33 + // TODO delete work from work list
  34 + },
  35 + createWork () {
  36 + this.$router.push({ name: 'editor' })
  37 + }
  38 + },
  39 + render (h) {
  40 + return (
  41 + <div class="works-wrapper">
  42 + <a-table columns={columns} dataSource={this.computedWorks} row-key="id" scopedSlots={{
  43 + action: function (props) {
  44 + return [<router-link to={{ name: 'stat-detail', params: { id: props.id } }} >查看数据</router-link>]
  45 + }
  46 + }}>
  47 + </a-table>
  48 + </div>
  49 + )
  50 + },
  51 + created () {
  52 + this.fetchWorks()
  53 + }
  54 +}
  55 +</script>
front-end/h5/src/views/work-manager/index.vue
@@ -20,7 +20,8 @@ const sidebarMenus = [ @@ -20,7 +20,8 @@ const sidebarMenus = [
20 label: '基础数据', 20 label: '基础数据',
21 value: 'basicData', 21 value: 'basicData',
22 antIcon: 'snippets', 22 antIcon: 'snippets',
23 - key: '2-1' 23 + key: '2-1',
  24 + routerName: 'form-stat'
24 }, 25 },
25 { 26 {
26 label: '表单统计', 27 label: '表单统计',
@@ -89,7 +90,11 @@ export default { @@ -89,7 +90,11 @@ export default {
89 ? <a-sub-menu key={menu.key}> 90 ? <a-sub-menu key={menu.key}>
90 <span slot="title"><a-icon type={menu.antIcon} />{menu.label}</span> 91 <span slot="title"><a-icon type={menu.antIcon} />{menu.label}</span>
91 { 92 {
92 - (menu.children).map(submenu => (<a-menu-item key={submenu.key}>{submenu.label}</a-menu-item>)) 93 + (menu.children).map(submenu => (
  94 + <a-menu-item key={submenu.key}>
  95 + { submenu.routerName ? <router-link to={{ name: submenu.routerName }}>{submenu.label}</router-link> : submenu.label }
  96 + </a-menu-item>
  97 + ))
93 } 98 }
94 </a-sub-menu> 99 </a-sub-menu>
95 : <a-menu-item key={menu.key}> 100 : <a-menu-item key={menu.key}>