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 71 "config": {
72 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 49 // eslint-disable-next-line require-atomic-updates
50 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 7 module.exports = {
8 8 // Before saving a value.
9 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 24 // After saving a value.
25 25 // Fired after an `insert` or `update` query.
... ... @@ -48,6 +48,7 @@ module.exports = {
48 48 elements: []
49 49 }];
50 50 model.set('pages', JSON.stringify(defaultPages));
  51 + model.set('is_template', false);
51 52 },
52 53  
53 54 // After creating a value.
... ... @@ -56,7 +57,17 @@ module.exports = {
56 57  
57 58 // Before updating a value.
58 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 72 // After updating a value.
62 73 // Fired after an `update` query.
... ...
back-end/h5-api/api/work/models/Work.settings.json
... ... @@ -35,10 +35,13 @@
35 35 "update_time": {
36 36 "type": "date"
37 37 },
38   - "pages": {
  38 + "formData": {
39 39 "type": "json"
40 40 },
41   - "formData": {
  41 + "is_template": {
  42 + "type": "boolean"
  43 + },
  44 + "pages": {
42 45 "type": "json"
43 46 }
44 47 }
... ...
front-end/h5/src/components/core/editor/index.js
... ... @@ -100,7 +100,8 @@ export default {
100 100 work: state => state.work
101 101 }),
102 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 107 methods: {
... ... @@ -109,8 +110,12 @@ export default {
109 110 'pageManager',
110 111 'saveWork',
111 112 'createWork',
112   - 'fetchWork'
  113 + 'fetchWork',
  114 + 'setWorkAsTemplate'
113 115 ]),
  116 + ...mapActions('loading', {
  117 + updateLoading: 'update'
  118 + }),
114 119 /**
115 120 * !#zh 点击插件,copy 其基础数据到组件树(中间画布)
116 121 * #!en click the plugin shortcut, create new Element with the plugin's meta data
... ... @@ -165,7 +170,29 @@ export default {
165 170 >
166 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 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 196 </a-menu>
170 197 <ExternalLinksOfHeader />
171 198 </a-layout-header>
... ...
front-end/h5/src/router.js
... ... @@ -21,6 +21,11 @@ export default new Router({
21 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 29 path: '/work-manager/form-stat',
25 30 name: 'form-stat',
26 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 13 formDetailOfWork: {
14 14 uuidMap2Name: {},
15 15 formDetails: []
16   - }
  16 + },
  17 + workTemplates: []
17 18 }
18 19  
19 20 // getters
... ...
front-end/h5/src/store/modules/loading.js
1 1 // initial state
2 2 const state = {
3 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 10 // getters
... ...
front-end/h5/src/store/modules/work.js
... ... @@ -13,6 +13,7 @@ export const actions = {
13 13 },
14 14 createWork ({ commit }, payload) {
15 15 strapi.createEntry('works').then(entry => {
  16 + commit('setWork', entry)
16 17 router.replace({ name: 'editor', params: { workId: entry.id } })
17 18 // window.location = `${window.location.origin}/#/editor/${entry.id}`
18 19 })
... ... @@ -35,7 +36,7 @@ export const actions = {
35 36 ...payload
36 37 }
37 38  
38   - new AxiosWrapper({
  39 + return new AxiosWrapper({
39 40 dispatch,
40 41 commit,
41 42 loading_name: 'saveWork_loading',
... ... @@ -57,7 +58,17 @@ export const actions = {
57 58 loading_name: 'fetchWorks_loading',
58 59 successMsg: '获取作品列表成功',
59 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 139 loading_name: 'queryFormsOfWork_loading',
129 140 successMsg: '表单查询完毕'
130 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 172 value.sort((a, b) => b.id - a.id)
144 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 185 setWork (state, work) {
147 186 window.__work = work
148 187 work.pages = work.pages.map(page => {
... ...
front-end/h5/src/utils/http.js
... ... @@ -43,6 +43,14 @@ export class AxiosWrapper {
43 43 this.setDefaultLoadingName(args)
44 44  
45 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 54 return this.instance.get(...args).then(response => {
47 55 const handler = this.getCommonResponseHandler({ failMsg: 'Query Failed.' })
48 56 handler.call(this, response)
... ... @@ -59,6 +67,7 @@ export class AxiosWrapper {
59 67 return this.instance.post(...args).then(response => {
60 68 const handler = this.getCommonResponseHandler({ failMsg: 'Save Failed.' })
61 69 handler.call(this, response)
  70 + return response.data
62 71 }).catch(error => {
63 72 // handle error
64 73 myMessage.error(error.message)
... ...
front-end/h5/src/views/work-manager/index.vue
... ... @@ -38,7 +38,8 @@ const sidebarMenus = [
38 38 label: '免费模板',
39 39 value: 'freeTemplates',
40 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>
... ...