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
| @@ -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}> |