Commit 3d455d7d75e40acd988af7fc2ae775c6b50c8951

Authored by ly525
1 parent 4aac184e

feat: support work template

back-end/h5-api/api/work/config/routes.json
@@ -71,6 +71,22 @@ @@ -71,6 +71,22 @@
71 "config": { 71 "config": {
72 "policies": [] 72 "policies": []
73 } 73 }
  74 + },
  75 + {
  76 + "method": "POST",
  77 + "path": "/works/set-as-template/:id",
  78 + "handler": "Work.setAsTemplate",
  79 + "config": {
  80 + "policies": []
  81 + }
  82 + },
  83 + {
  84 + "method": "POST",
  85 + "path": "/works/use-template/:id",
  86 + "handler": "Work.useTemplate",
  87 + "config": {
  88 + "policies": []
  89 + }
74 } 90 }
75 ] 91 ]
76 } 92 }
back-end/h5-api/api/work/controllers/Work.js
@@ -49,4 +49,20 @@ module.exports = { @@ -49,4 +49,20 @@ module.exports = {
49 // eslint-disable-next-line require-atomic-updates 49 // eslint-disable-next-line require-atomic-updates
50 return ctx.body = { uuidMap2Name, formDetails }; 50 return ctx.body = { uuidMap2Name, formDetails };
51 }, 51 },
  52 + setAsTemplate: async (ctx) => {
  53 + let work = await strapi.services.work.findOne(ctx.params);
  54 + work = work.toJSON();
  55 +
  56 + // eslint-disable-next-line no-unused-vars
  57 + const templateWork = await strapi.services.work.create();
  58 + return strapi.services.work.update({id: templateWork.id}, { pages: work.pages, is_template: true });
  59 + },
  60 + useTemplate: async (ctx) => {
  61 + let templateWork = await strapi.services.work.findOne(ctx.params);
  62 + templateWork = templateWork.toJSON();
  63 +
  64 + // eslint-disable-next-line no-unused-vars
  65 + const work = await strapi.services.work.create();
  66 + return strapi.services.work.update({id: work.id}, { pages: templateWork.pages, is_template: false });
  67 + },
52 }; 68 };
back-end/h5-api/api/work/models/Work.js
@@ -7,19 +7,19 @@ @@ -7,19 +7,19 @@
7 module.exports = { 7 module.exports = {
8 // Before saving a value. 8 // Before saving a value.
9 // Fired before an `insert` or `update` query. 9 // Fired before an `insert` or `update` query.
10 - beforeSave: async (model, attrs, options) => {  
11 - // https://github.com/strapi/strapi/issues/2882  
12 - // need to remove this after this pr will be merged(https://github.com/strapi/strapi/pull/3664)  
13 - Object.keys(model.constructor.attributes).forEach(k => {  
14 - if (model.constructor.attributes[k].type === 'json') {  
15 - const value = model.get(k); 10 + // beforeSave: async (model, attrs, options) => {
  11 + // // https://github.com/strapi/strapi/issues/2882
  12 + // // need to remove this after this pr will be merged(https://github.com/strapi/strapi/pull/3664)
  13 + // Object.keys(model.constructor.attributes).forEach(k => {
  14 + // if (model.constructor.attributes[k].type === 'json') {
  15 + // const value = model.get(k);
16 16
17 - if (Array.isArray(value)) {  
18 - model.set(k, JSON.stringify(value));  
19 - }  
20 - }  
21 - });  
22 - }, 17 + // if (Array.isArray(value)) {
  18 + // model.set(k, JSON.stringify(value));
  19 + // }
  20 + // }
  21 + // });
  22 + // },
23 23
24 // After saving a value. 24 // After saving a value.
25 // Fired after an `insert` or `update` query. 25 // Fired after an `insert` or `update` query.
@@ -48,6 +48,7 @@ module.exports = { @@ -48,6 +48,7 @@ module.exports = {
48 elements: [] 48 elements: []
49 }]; 49 }];
50 model.set('pages', JSON.stringify(defaultPages)); 50 model.set('pages', JSON.stringify(defaultPages));
  51 + model.set('is_template', false);
51 }, 52 },
52 53
53 // After creating a value. 54 // After creating a value.
@@ -56,7 +57,17 @@ module.exports = { @@ -56,7 +57,17 @@ module.exports = {
56 57
57 // Before updating a value. 58 // Before updating a value.
58 // Fired before an `update` query. 59 // Fired before an `update` query.
59 - // beforeUpdate: async (model, attrs, options) => {}, 60 + // beforeUpdate: async (model, attrs, options) => {
  61 + // Object.keys(model.constructor.attributes).forEach(k => {
  62 + // if (model.constructor.attributes[k].type === 'json') {
  63 + // const value = model.get(k);
  64 +
  65 + // if (Array.isArray(value)) {
  66 + // model.set(k, JSON.stringify(value));
  67 + // }
  68 + // }
  69 + // });
  70 + // },
60 71
61 // After updating a value. 72 // After updating a value.
62 // Fired after an `update` query. 73 // Fired after an `update` query.
back-end/h5-api/api/work/models/Work.settings.json
@@ -35,10 +35,13 @@ @@ -35,10 +35,13 @@
35 "update_time": { 35 "update_time": {
36 "type": "date" 36 "type": "date"
37 }, 37 },
38 - "pages": { 38 + "formData": {
39 "type": "json" 39 "type": "json"
40 }, 40 },
41 - "formData": { 41 + "is_template": {
  42 + "type": "boolean"
  43 + },
  44 + "pages": {
42 "type": "json" 45 "type": "json"
43 } 46 }
44 } 47 }
front-end/h5/src/components/core/editor/index.js
@@ -100,7 +100,8 @@ export default { @@ -100,7 +100,8 @@ export default {
100 work: state => state.work 100 work: state => state.work
101 }), 101 }),
102 ...mapState('loading', { 102 ...mapState('loading', {
103 - saveWork_loading: state => state.saveWork_loading 103 + saveWork_loading: state => state.saveWork_loading,
  104 + setWorkAsTemplate_loading: state => state.setWorkAsTemplate_loading
104 }) 105 })
105 }, 106 },
106 methods: { 107 methods: {
@@ -109,8 +110,12 @@ export default { @@ -109,8 +110,12 @@ export default {
109 'pageManager', 110 'pageManager',
110 'saveWork', 111 'saveWork',
111 'createWork', 112 'createWork',
112 - 'fetchWork' 113 + 'fetchWork',
  114 + 'setWorkAsTemplate'
113 ]), 115 ]),
  116 + ...mapActions('loading', {
  117 + updateLoading: 'update'
  118 + }),
114 /** 119 /**
115 * !#zh 点击插件,copy 其基础数据到组件树(中间画布) 120 * !#zh 点击插件,copy 其基础数据到组件树(中间画布)
116 * #!en click the plugin shortcut, create new Element with the plugin's meta data 121 * #!en click the plugin shortcut, create new Element with the plugin's meta data
@@ -165,7 +170,29 @@ export default { @@ -165,7 +170,29 @@ export default {
165 > 170 >
166 <a-menu-item key="1" class="transparent-bg"><a-button type="primary" size="small" onClick={() => { this.previewVisible = true }}>预览</a-button></a-menu-item> 171 <a-menu-item key="1" class="transparent-bg"><a-button type="primary" size="small" onClick={() => { this.previewVisible = true }}>预览</a-button></a-menu-item>
167 <a-menu-item key="2" class="transparent-bg"><a-button size="small" onClick={() => this.saveWork()} loading={this.saveWork_loading}>保存</a-button></a-menu-item> 172 <a-menu-item key="2" class="transparent-bg"><a-button size="small" onClick={() => this.saveWork()} loading={this.saveWork_loading}>保存</a-button></a-menu-item>
168 - <a-menu-item key="3" class="transparent-bg"><a-button size="small">发布</a-button></a-menu-item> 173 + {/* <a-menu-item key="3" class="transparent-bg"><a-button size="small">发布</a-button></a-menu-item> */}
  174 + <a-menu-item key="3" class="transparent-bg">
  175 + <a-dropdown-button onClick={() => {}} size="small">
  176 + 发布
  177 + <a-menu slot="overlay" onClick={({ key }) => {
  178 + switch (key) {
  179 + case 'setAsTemplate':
  180 + this.updateLoading({ type: 'setWorkAsTemplate_loading', value: true })
  181 + this.saveWork().then(() => {
  182 + this.setWorkAsTemplate()
  183 + })
  184 + }
  185 + }}>
  186 + <a-menu-item key="setAsTemplate">
  187 + <a-spin spinning={this.setWorkAsTemplate_loading} size="small">
  188 + <a-icon type="cloud-upload" />设置为模板
  189 + </a-spin>
  190 + </a-menu-item>
  191 + {/* <a-menu-item key="2"><a-icon type="user" />2nd menu item</a-menu-item> */}
  192 + {/* <a-menu-item key="3"><a-icon type="user" />3rd item</a-menu-item> */}
  193 + </a-menu>
  194 + </a-dropdown-button>
  195 + </a-menu-item>
169 </a-menu> 196 </a-menu>
170 <ExternalLinksOfHeader /> 197 <ExternalLinksOfHeader />
171 </a-layout-header> 198 </a-layout-header>
front-end/h5/src/router.js
@@ -21,6 +21,11 @@ export default new Router({ @@ -21,6 +21,11 @@ export default new Router({
21 component: () => import('@/views/work-manager/list.vue') 21 component: () => import('@/views/work-manager/list.vue')
22 }, 22 },
23 { 23 {
  24 + path: '/work-manager/templates',
  25 + name: 'work-manager-templates',
  26 + component: () => import('@/views/work-manager/templates.vue')
  27 + },
  28 + {
24 path: '/work-manager/form-stat', 29 path: '/work-manager/form-stat',
25 name: 'form-stat', 30 name: 'form-stat',
26 component: () => import('@/views/work-manager/form-stat/index.vue') 31 component: () => import('@/views/work-manager/form-stat/index.vue')
front-end/h5/src/store/modules/editor.js
@@ -13,7 +13,8 @@ const state = { @@ -13,7 +13,8 @@ const state = {
13 formDetailOfWork: { 13 formDetailOfWork: {
14 uuidMap2Name: {}, 14 uuidMap2Name: {},
15 formDetails: [] 15 formDetails: []
16 - } 16 + },
  17 + workTemplates: []
17 } 18 }
18 19
19 // getters 20 // getters
front-end/h5/src/store/modules/loading.js
1 // initial state 1 // initial state
2 const state = { 2 const state = {
3 saveWork_loading: false, 3 saveWork_loading: false,
4 - fetchWorks_loading: false 4 + fetchWorks_loading: false,
  5 + setWorkAsTemplate_loading: false,
  6 + fetchWorkTemplates_loading: false,
  7 + useTemplate_loading: false
5 } 8 }
6 9
7 // getters 10 // getters
front-end/h5/src/store/modules/work.js
@@ -13,6 +13,7 @@ export const actions = { @@ -13,6 +13,7 @@ export const actions = {
13 }, 13 },
14 createWork ({ commit }, payload) { 14 createWork ({ commit }, payload) {
15 strapi.createEntry('works').then(entry => { 15 strapi.createEntry('works').then(entry => {
  16 + commit('setWork', entry)
16 router.replace({ name: 'editor', params: { workId: entry.id } }) 17 router.replace({ name: 'editor', params: { workId: entry.id } })
17 // window.location = `${window.location.origin}/#/editor/${entry.id}` 18 // window.location = `${window.location.origin}/#/editor/${entry.id}`
18 }) 19 })
@@ -35,7 +36,7 @@ export const actions = { @@ -35,7 +36,7 @@ export const actions = {
35 ...payload 36 ...payload
36 } 37 }
37 38
38 - new AxiosWrapper({ 39 + return new AxiosWrapper({
39 dispatch, 40 dispatch,
40 commit, 41 commit,
41 loading_name: 'saveWork_loading', 42 loading_name: 'saveWork_loading',
@@ -57,7 +58,17 @@ export const actions = { @@ -57,7 +58,17 @@ export const actions = {
57 loading_name: 'fetchWorks_loading', 58 loading_name: 'fetchWorks_loading',
58 successMsg: '获取作品列表成功', 59 successMsg: '获取作品列表成功',
59 customRequest: strapi.getEntries.bind(strapi) 60 customRequest: strapi.getEntries.bind(strapi)
60 - }).get('works', {}) 61 + }).get('works', { is_template: false })
  62 + },
  63 + fetchWorkTemplates ({ commit, dispatch, state }, workId) {
  64 + new AxiosWrapper({
  65 + dispatch,
  66 + commit,
  67 + name: 'editor/setWorkTemplates',
  68 + loading_name: 'fetchWorkTemplates_loading',
  69 + successMsg: '获取模板列表成功',
  70 + customRequest: strapi.getEntries.bind(strapi)
  71 + }).get('works', { is_template: true })
61 }, 72 },
62 /** 73 /**
63 * 74 *
@@ -128,6 +139,24 @@ export const actions = { @@ -128,6 +139,24 @@ export const actions = {
128 loading_name: 'queryFormsOfWork_loading', 139 loading_name: 'queryFormsOfWork_loading',
129 successMsg: '表单查询完毕' 140 successMsg: '表单查询完毕'
130 }).get(`/works/form/query/${workId}`) 141 }).get(`/works/form/query/${workId}`)
  142 + },
  143 + setWorkAsTemplate ({ commit, state, dispatch }, workId) {
  144 + new AxiosWrapper({
  145 + dispatch,
  146 + commit,
  147 + // name: 'editor/formDetailOfWork',
  148 + loading_name: 'setWorkAsTemplate_loading',
  149 + successMsg: '设置为模板成功'
  150 + }).post(`/works/set-as-template/${workId || state.work.id}`)
  151 + },
  152 + useTemplate ({ commit, state, dispatch }, workId) {
  153 + return new AxiosWrapper({
  154 + dispatch,
  155 + commit,
  156 + // name: 'editor/formDetailOfWork',
  157 + loading_name: 'useTemplate_loading',
  158 + successMsg: '使用模板成功'
  159 + }).post(`/works/use-template/${workId}`)
131 } 160 }
132 } 161 }
133 162
@@ -143,6 +172,16 @@ export const mutations = { @@ -143,6 +172,16 @@ export const mutations = {
143 value.sort((a, b) => b.id - a.id) 172 value.sort((a, b) => b.id - a.id)
144 state.works = value 173 state.works = value
145 }, 174 },
  175 + /**
  176 + * payload: {
  177 + * type: @params {String} "editor/setWorks",
  178 + * value: @params {Array} work list
  179 + * }
  180 + */
  181 + setWorkTemplates (state, { type, value }) {
  182 + value.sort((a, b) => b.id - a.id)
  183 + state.workTemplates = value
  184 + },
146 setWork (state, work) { 185 setWork (state, work) {
147 window.__work = work 186 window.__work = work
148 work.pages = work.pages.map(page => { 187 work.pages = work.pages.map(page => {
front-end/h5/src/utils/http.js
@@ -43,6 +43,14 @@ export class AxiosWrapper { @@ -43,6 +43,14 @@ export class AxiosWrapper {
43 this.setDefaultLoadingName(args) 43 this.setDefaultLoadingName(args)
44 44
45 this.setLoadingValue(true) 45 this.setLoadingValue(true)
  46 + if (this.customRequest) {
  47 + return this.customRequest(...args)
  48 + .then(data => {
  49 + const handler = this.getCommonResponseHandler({ failMsg: 'Save Failed.' })
  50 + handler.call(this, { status: 200, data })
  51 + })
  52 + .finally(() => this.setLoadingValue(false))
  53 + }
46 return this.instance.get(...args).then(response => { 54 return this.instance.get(...args).then(response => {
47 const handler = this.getCommonResponseHandler({ failMsg: 'Query Failed.' }) 55 const handler = this.getCommonResponseHandler({ failMsg: 'Query Failed.' })
48 handler.call(this, response) 56 handler.call(this, response)
@@ -59,6 +67,7 @@ export class AxiosWrapper { @@ -59,6 +67,7 @@ export class AxiosWrapper {
59 return this.instance.post(...args).then(response => { 67 return this.instance.post(...args).then(response => {
60 const handler = this.getCommonResponseHandler({ failMsg: 'Save Failed.' }) 68 const handler = this.getCommonResponseHandler({ failMsg: 'Save Failed.' })
61 handler.call(this, response) 69 handler.call(this, response)
  70 + return response.data
62 }).catch(error => { 71 }).catch(error => {
63 // handle error 72 // handle error
64 myMessage.error(error.message) 73 myMessage.error(error.message)
front-end/h5/src/views/work-manager/index.vue
@@ -38,7 +38,8 @@ const sidebarMenus = [ @@ -38,7 +38,8 @@ const sidebarMenus = [
38 label: '免费模板', 38 label: '免费模板',
39 value: 'freeTemplates', 39 value: 'freeTemplates',
40 antIcon: 'snippets', 40 antIcon: 'snippets',
41 - key: '3-1' 41 + key: '3-1',
  42 + routerName: 'work-manager-templates'
42 } 43 }
43 ] 44 ]
44 }, 45 },
front-end/h5/src/views/work-manager/templates.vue 0 → 100644
  1 +<script>
  2 +import { mapState, mapActions } from 'vuex'
  3 +import QRCode from 'qrcode'
  4 +
  5 +import { API_ORIGIN } from '@/constants/api.js'
  6 +import PreviewDialog from '@/components/core/editor/modals/preview.vue'
  7 +
  8 +const ListItemCard = {
  9 + props: {
  10 + work: {
  11 + type: Object,
  12 + default: () => {}
  13 + },
  14 + handleClickEdit: {
  15 + type: Function,
  16 + default: () => {}
  17 + },
  18 + handleClickPreview: {
  19 + type: Function,
  20 + default: () => {}
  21 + },
  22 + handleUseTemplate: {
  23 + type: Function,
  24 + default: () => {}
  25 + }
  26 + },
  27 + data: () => ({
  28 + qrcodeUrl: ''
  29 + }),
  30 + methods: {
  31 + timeFmt (date) {
  32 + const dateTime = new Date(date)
  33 + const displayTime = `${dateTime.getFullYear()}-${dateTime.getMonth() +
  34 + 1}-${dateTime.getDate()}`
  35 + return displayTime
  36 + },
  37 + genQRCodeUrl (work) {
  38 + const url = `${API_ORIGIN}/works/preview/${work.id}`
  39 + QRCode.toDataURL(url, (err, url) => {
  40 + if (err) console.log(err)
  41 + this.qrcodeUrl = url
  42 + })
  43 + }
  44 + },
  45 + render (h) {
  46 + return (
  47 + <a-card hoverable >
  48 + <div slot="cover" class="flex-center" style="height: 200px;font-size: 24px;border: 1px dashed #eee;color: #aaa;background: #f7f5f557;" >
  49 + { this.qrcodeUrl ? <img src={this.qrcodeUrl} /> : <span>Luban H5</span> }
  50 + </div>
  51 + <template class="ant-card-actions" slot="actions">
  52 + {/**
  53 + <router-link to={{ name: 'editor', params: { workId: this.work.id } }} target="_blank">
  54 + <a-tooltip effect="dark" placement="bottom" title="立即使用">
  55 + <a-icon type="plus-square" title="立即使用"/>
  56 + </a-tooltip>
  57 + </router-link>
  58 + */}
  59 + <a-tooltip effect="dark" placement="bottom" title="立即使用">
  60 + <a-icon type="plus-square" title="立即使用" onClick={() => {
  61 + this.handleUseTemplate(this.work)
  62 + }} />
  63 + </a-tooltip>
  64 + <a-tooltip effect="dark" placement="bottom" title="预览">
  65 + <a-icon type="eye" title="预览" onClick={this.handleClickPreview} />
  66 + </a-tooltip>
  67 + {
  68 + this.qrcodeUrl
  69 + ? <a-icon type="close-circle" onClick={() => { this.qrcodeUrl = '' }} />
  70 + : <a-icon type="qrcode" onClick={() => this.genQRCodeUrl(this.work)} />
  71 + }
  72 + {/**
  73 + <a-icon type="setting" />
  74 + <a-icon type="ellipsis" />
  75 + */}
  76 + </template>
  77 + <a-card-meta
  78 + >
  79 + <div slot="title" class="ant-card-meta-title" style="font-size: 14px;">
  80 + {this.work.title}({this.work.id})
  81 + </div>
  82 + <div slot="description" style="font-size: 12px;">
  83 + <div>描述:{this.work.description}</div>
  84 + <div>时间:{this.timeFmt(this.work.created_at)}</div>
  85 + </div>
  86 + </a-card-meta>
  87 + </a-card>
  88 + )
  89 + }
  90 +}
  91 +
  92 +export default {
  93 + components: {
  94 + ListItemCard
  95 + },
  96 + data: () => ({
  97 + activeWork: null,
  98 + previewVisible: false,
  99 + useTemplateDialogVisible: false,
  100 + clonedWorkFromTemplate: null // 从某个模板复制出来的作品
  101 + }),
  102 + computed: {
  103 + ...mapState('editor', ['works', 'workTemplates']),
  104 + ...mapState('loading', ['fetchWorks_loading'])
  105 + },
  106 + methods: {
  107 + ...mapActions('editor', [
  108 + 'fetchWorks',
  109 + 'fetchWorkTemplates',
  110 + 'useTemplate'
  111 + ]),
  112 + deleteWork (item) {
  113 + // TODO delete work from work list
  114 + },
  115 + createWork () {
  116 + this.$router.push({ name: 'editor' })
  117 + // window.open('#/editor', '_blank')
  118 + }
  119 + },
  120 + render (h) {
  121 + return (
  122 + <div class="works-wrapper">
  123 + <a-row gutter={24}>
  124 + {
  125 + this.fetchWorkTemplates_loading
  126 + ? <a-col span={24} style="margin-bottom: 10px;text-align: center;height: 355px;line-height: 355px;border-bottom: 1px solid #ebedf0;background: rgba(255, 255, 255, 0.5);">
  127 + <a-spin tip="作品列表获取中..."/>
  128 + </a-col>
  129 + : this.workTemplates.map(work => (
  130 + <a-col span={6} key={work.id} style="margin-bottom: 20px;">
  131 + <ListItemCard work={work}
  132 + handleClickPreview={e => {
  133 + this.previewVisible = true
  134 + this.activeWork = work
  135 + }}
  136 + handleUseTemplate={templateWork => {
  137 + this.useTemplateDialogVisible = true
  138 + this.useTemplate(templateWork.id).then((clonedWork) => {
  139 + this.clonedWorkFromTemplate = clonedWork
  140 + })
  141 + }}
  142 + />
  143 + </a-col>
  144 + ))
  145 + }
  146 + </a-row>
  147 + {
  148 + this.previewVisible &&
  149 + <PreviewDialog
  150 + work={this.activeWork}
  151 + visible={this.previewVisible}
  152 + handleClose={() => { this.previewVisible = false }}
  153 + />
  154 + }
  155 + {
  156 + this.useTemplateDialogVisible &&
  157 + <a-modal
  158 + visible={true}
  159 + // onOk={() => { this.useTemplateDialogVisible = true }}
  160 + // onCancel={() => { this.useTemplateDialogVisible = false }}
  161 + width="240px"
  162 + okText="保存"
  163 + footer={null}
  164 + closable={false}
  165 + centered
  166 + >
  167 + <div style="text-align: center;">
  168 + {
  169 + this.clonedWorkFromTemplate
  170 + ? <div>
  171 + <div style={{ margin: '12px' }}><a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" /> 模板已保存至"我的作品"中</div>
  172 + <a-button onClick={() => {
  173 + this.useTemplateDialogVisible = false
  174 + this.clonedWorkFromTemplate = null
  175 + }}>我再逛逛</a-button>
  176 + <a-button type="primary"
  177 + onClick={() => {
  178 + const routeData = this.$router.resolve({ name: 'editor', params: { workId: this.clonedWorkFromTemplate.id } })
  179 + window.open(routeData.href, '_blank')
  180 + }}
  181 + style={{ marginLeft: '12px' }}
  182 + >立即使用</a-button>
  183 + </div>
  184 + : <a-spin tip="复制中" />
  185 + }
  186 + </div>
  187 + </a-modal>
  188 + }
  189 + </div>
  190 + )
  191 + },
  192 + created () {
  193 + this.fetchWorkTemplates()
  194 + }
  195 +}
  196 +</script>