Commit 0252319111de5e28ad6ec38632d1516f2747c867
1 parent
16f173df
feat(#30): collect and show form data
Showing
11 changed files
with
340 additions
and
11 deletions
back-end/h5-api/api/work/config/routes.json
back-end/h5-api/api/work/controllers/Work.js
| ... | ... | @@ -21,4 +21,32 @@ module.exports = { |
| 21 | 21 | // eslint-disable-next-line require-atomic-updates |
| 22 | 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 | 23 | { |
| 24 | 24 | path: '/work-manager/form-stat', |
| 25 | 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 | 9 | work: new Work(), |
| 10 | 10 | editingPage: { elements: [] }, |
| 11 | 11 | editingElement: null, |
| 12 | - editingElementEditorConfig: null | |
| 12 | + editingElementEditorConfig: null, | |
| 13 | + formDetailOfWork: { | |
| 14 | + uuidMap2Name: {}, | |
| 15 | + formDetails: [] | |
| 16 | + } | |
| 13 | 17 | } |
| 14 | 18 | |
| 15 | 19 | // getters | ... | ... |
front-end/h5/src/store/modules/work.js
| ... | ... | @@ -37,6 +37,7 @@ export const actions = { |
| 37 | 37 | |
| 38 | 38 | new AxiosWrapper({ |
| 39 | 39 | dispatch, |
| 40 | + commit, | |
| 40 | 41 | loading_name: 'saveWork_loading', |
| 41 | 42 | successMsg: '保存作品成功', |
| 42 | 43 | customRequest: strapi.updateEntry.bind(strapi) |
| ... | ... | @@ -52,6 +53,76 @@ export const actions = { |
| 52 | 53 | strapi.getEntries('works', {}).then(entries => { |
| 53 | 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 | 143 | // state.work = new Work() |
| 73 | 144 | // }, |
| 74 | 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 | 17 | |
| 18 | 18 | export class AxiosWrapper { |
| 19 | 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 | 21 | this.name = name |
| 22 | 22 | // eslint-disable-next-line camelcase |
| 23 | 23 | this.loading_name = loading_name |
| 24 | 24 | // eslint-disable-next-line camelcase |
| 25 | 25 | this.responseType = responseType |
| 26 | 26 | this.dispatch = dispatch |
| 27 | + this.commit = commit | |
| 27 | 28 | this.router = router |
| 28 | 29 | this.successMsg = successMsg |
| 29 | 30 | this.failMsg = failMsg |
| ... | ... | @@ -80,7 +81,7 @@ export class AxiosWrapper { |
| 80 | 81 | return this.customRequest(...args) |
| 81 | 82 | .then(data => { |
| 82 | 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 | 86 | .finally(() => this.setLoadingValue(false)) |
| 86 | 87 | } |
| ... | ... | @@ -112,7 +113,8 @@ export class AxiosWrapper { |
| 112 | 113 | } |
| 113 | 114 | |
| 114 | 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 | 120 | setDefaultLoadingName (...args) { |
| ... | ... | @@ -134,16 +136,16 @@ export class AxiosWrapper { |
| 134 | 136 | return (response) => { |
| 135 | 137 | if (!response.data) { |
| 136 | 138 | myMessage.warn(this.failMsg || failMsg) |
| 137 | - } else if (response.data.status === 200) { | |
| 139 | + } else if (response.status === 200) { | |
| 138 | 140 | this.successMsg && myMessage.success(this.successMsg) |
| 139 | 141 | if (this.successCallback) { |
| 140 | 142 | this.successCallback(response) |
| 141 | 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 | 146 | } else if (this.responseType === 'json') { |
| 145 | 147 | myMessage.error(response.data.msg) |
| 146 | - if (response.data.status === 401) { | |
| 148 | + if (response.status === 401) { | |
| 147 | 149 | if (this.router) { |
| 148 | 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 | 20 | label: '基础数据', |
| 21 | 21 | value: 'basicData', |
| 22 | 22 | antIcon: 'snippets', |
| 23 | - key: '2-1' | |
| 23 | + key: '2-1', | |
| 24 | + routerName: 'form-stat' | |
| 24 | 25 | }, |
| 25 | 26 | { |
| 26 | 27 | label: '表单统计', |
| ... | ... | @@ -89,7 +90,11 @@ export default { |
| 89 | 90 | ? <a-sub-menu key={menu.key}> |
| 90 | 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 | 99 | </a-sub-menu> |
| 95 | 100 | : <a-menu-item key={menu.key}> | ... | ... |