From 17a58237a62f148286d50005f80db15b117c1e5c Mon Sep 17 00:00:00 2001 From: jysperm Date: Thu, 22 Jan 2015 01:05:18 +0800 Subject: [PATCH] refactor ticket front-end --- README.md | 15 ++-- bower.json | 3 +- core/static/script/layout.coffee | 21 +++-- core/static/script/ticket.coffee | 139 +++++++++++++++++++++++++------ core/view/ticket/create.jade | 4 +- core/view/ticket/list.jade | 66 ++++----------- core/view/ticket/view.jade | 138 +++++++++++++++--------------- 7 files changed, 229 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 6d6f112..f04e137 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ RootPanel 是一个 PaaS 开发框架,提供了用户系统、计费和订单 RootPanel 具有良好的设计,高度的可定制性,支持多语言和多时区,以及非常高的单元测试覆盖率。 -RootPanel 的文档位于 [Github Wiki](https://github.com/jysperm/RootPanel/wiki). +RootPanel 的文档位于 [Github Wiki](https://github.com/jysperm/RootPanel/wiki), 包括常见问题、终端用户文档、使用文档、开发文档。 ## 安装 @@ -12,7 +12,7 @@ RootPanel 的文档位于 [Github Wiki](https://github.com/jysperm/RootPanel/wik git clone -b stable https://github.com/jysperm/RootPanel.git -开发版本 +主分支 [![Build Status](https://travis-ci.org/jysperm/RootPanel.svg?branch=master)](https://travis-ci.org/jysperm/RootPanel) git clone https://github.com/jysperm/RootPanel.git @@ -25,9 +25,9 @@ RootPanel 的文档位于 [Github Wiki](https://github.com/jysperm/RootPanel/wik 请从 `sample` 中选择一个配置文件复制到根目录,重命名为 `config.coffee`: - core.config.coffee # 仅核心模块 - rpvhost.config.coffee # 虚拟主机 (正在重构,目前支持 SSH 和 Supervisor) - shadowsocks.config.coffee # ShadowSocks 代理服务 + core.config.coffee # 仅核心模块 + rpvhost.config.coffee # 虚拟主机 (正在重构,目前支持 SSH 和 Supervisor) + shadowsocks.config.coffee # ShadowSocks 代理服务 ## 从旧版本升级 @@ -54,9 +54,9 @@ RootPanel 的文档位于 [Github Wiki](https://github.com/jysperm/RootPanel/wik ## 技术构成 -* 前端:Bootstrap(3), jQuery, Jade, Less +* 前端:Bootstrap, jQuery, Jade, Less * 后端:Express, Coffee -* 数据库:MongoDB(2.4), Redis +* 数据库:MongoDB, Redis * 操作系统支持:Ubuntu 14.04 amd64 RootPanel 默认会通过 Google Analytics 向开发人员发送匿名的统计信息。 @@ -77,6 +77,7 @@ RootPanel 默认会通过 Google Analytics 向开发人员发送匿名的统计 在你首次向 RootPanel 贡献代码时,我们还会人工向你确认一次上述协议。 ## 许可协议 +RootPanel 采用开源与商业双授权模式。 * 开源授权:[AGPLv3](https://github.com/jysperm/RootPanel/blob/master/LICENSE) | [CC-SA](http://creativecommons.org/licenses/sa/1.0/) (文档) | Public Domain (配置文件和示例) * 商业授权(计划中) diff --git a/bower.json b/bower.json index c66d677..55281b0 100644 --- a/bower.json +++ b/bower.json @@ -5,6 +5,7 @@ "jquery-cookie": "~1.4.1", "underscore": "~1.7.0", "jquery": "~2.1.2", - "backbone": "~1.1.2" + "backbone": "~1.1.2", + "moment": "~2.9.0" } } diff --git a/core/static/script/layout.coffee b/core/static/script/layout.coffee index ec878ad..70c97ba 100644 --- a/core/static/script/layout.coffee +++ b/core/static/script/layout.coffee @@ -1,11 +1,16 @@ $ -> - window.rp ?= {} + window.RP ?= {} $.ajaxSetup headers: 'X-Csrf-Token': $('body').data 'csrf-token' - _.extend window.rp, + _.templateSettings = + evaluate: /\{%([\s\S]+?)%\}/g, + interpolate: /\{:([\s\S]+?)\}\}/g + escape: /\{\{([\s\S]+?)\}\}/g + + _.extend window.RP, i18n_data: {} initLocale: -> @@ -13,17 +18,17 @@ $ -> latest_version = $('body').data 'locale-version' if client_version == latest_version - rp.i18n_data = JSON.parse localStorage.getItem 'locale_cache' + RP.i18n_data = JSON.parse localStorage.getItem 'locale_cache' else $.getJSON "/account/locale/", (result) -> - rp.i18n_data = result + RP.i18n_data = result localStorage.setItem 'locale_version', latest_version localStorage.setItem 'locale_cache', JSON.stringify result t: (name) -> keys = name.split '.' - result = window.i18n_data + result = RP.i18n_data for item in keys if result[item] == undefined @@ -37,7 +42,7 @@ $ -> return result tErr: (name) -> - return rp.t "error_code.#{name}" + return RP.t "error_code.#{name}" request: (url, param, options, callback) -> unless callback @@ -54,11 +59,13 @@ $ -> data: param .fail (jqXHR) -> if jqXHR.responseJSON?.error - alert rp.tErr jqXHR.responseJSON.error + alert RP.tErr jqXHR.responseJSON.error else alert jqXHR.statusText .success callback + RP.initLocale() + $('nav a').each -> if $(@).attr('href') == location.pathname $(@).parent().addClass('active') diff --git a/core/static/script/ticket.coffee b/core/static/script/ticket.coffee index 79214ff..c9601d9 100644 --- a/core/static/script/ticket.coffee +++ b/core/static/script/ticket.coffee @@ -1,8 +1,32 @@ $ -> - Ticket = Backbone.Model.extend - url: '/ticket/resource/' + color_mapping = + closed: 'muted' + open: 'primary' + pending: 'warning' + finish: 'success' + + Reply = Backbone.Model.extend idAttribute: '_id' + Ticket = Backbone.Model.extend + urlRoot: '/ticket/resource/' + idAttribute: '_id' + + initialize: -> + ReplyCollection = Backbone.Collection.extend + url: @url() + '/replies' + model: Reply + + @replies = new ReplyCollection() + @replies.url = @url() + '/replies' + + @once 'change', => + @replies.reset @get 'replies' + + TicketCollection = Backbone.Collection.extend + model: Ticket + url: '/ticket/resource/' + CreateView = Backbone.View.extend el: '#create-view' @@ -11,34 +35,104 @@ $ -> createTicket: -> ticket = new Ticket - title: @$('.input-title').val() - content: @$('.input-content').val() + title: @$('[name=title]').val() + content: @$('[name=content]').val() ticket.save().success (ticket) -> - location.href = "/ticket/view/#{ticket.id}" + location.href = "/ticket/view/#{ticket._id}" + + ReplyView = Backbone.View.extend + tagName: 'li' + className: 'list-group-item clearfix' + + initialize: -> + @template = _.template $('#reply-template').html() + @model.on 'change', @render.bind @ + + render: -> + @$el.html @template @model.toJSON() + return @ TicketView = Backbone.View.extend - el: '#ticket-view' + el: 'body' events: 'click .action-reply': 'replyTicket' - 'click .action-update-status': 'updateStatus' + 'click .action-status': 'setStatus' + + id: null + model: null + + initialize: (options) -> + @id = options.id + @model = new Ticket _id: @id + + @model.on 'change', @render.bind @ + @model.replies.on 'add', @appendReply.bind @ + @model.replies.on 'reset', (replies) => + replies.each @appendReply.bind @ + + @model.fetch() + + @templateContent = _.template $('#content-template').html() + @templateActions = _.template $('#actions-template').html() + @templateAccountInfo = _.template $('#account-info-template').html() + @templateMembers = _.template $('#members-template').html() + + render: -> + view_data = @model.toJSON() + view_data.color = color_mapping[view_data.status] + @$('.content').html @templateContent view_data + @$('.actions').html @templateActions view_data + @$('.account-info').html @templateAccountInfo view_data + @$('.members').html @templateMembers view_data + return @ + + appendReply: (reply) -> + view = new ReplyView + model: reply + @$('.replies').append view.render().el replyTicket: -> - request "/ticket/reply/#{id}", - content: $('.input-content').val() - , -> - location.reload() + @model.replies.create + # TODO: use current account + account: @model.get 'account' + content: @$('[name=content]').val() + content_html: null + created_at: null + @$('[name=content]').val '' - updateStatus: -> - request "/ticket/update_status/#{id}", - status: $(@).data 'status' - , -> - location.reload() + setStatus: (e) -> + @model.save + status: $(e.target).data 'status' + , + url: @model.url() + '/status' - ListItemView = Backbone.View.extend() + ListItemView = Backbone.View.extend + tagName: 'tr' - ListView = Backbone.View.extend() + initialize: -> + @template = _.template $('#list-item-template').html() + + render: -> + view_data = @model.toJSON() + view_data.color = color_mapping[view_data.status] + @$el.html @template view_data + return @ + + ListView = Backbone.View.extend + el: '#list-view' + + tickets: new TicketCollection() + + initialize: -> + @tickets.on 'reset', => + @tickets.each (ticket) => + view = new ListItemView + model: ticket + @$('tbody').append view.render().el + + @tickets.fetch reset: true TicketRouter = Backbone.Router.extend routes: @@ -46,12 +140,9 @@ $ -> 'ticket/list(/)': 'list' 'ticket/view/:id(/)': 'view' - create: -> - new CreateView() - - list: -> - - view: (id) -> + create: -> new CreateView() + list: -> new ListView() + view: (id) -> new TicketView id: id new TicketRouter() Backbone.history.loadUrl location.pathname diff --git a/core/view/ticket/create.jade b/core/view/ticket/create.jade index a111720..13cccca 100644 --- a/core/view/ticket/create.jade +++ b/core/view/ticket/create.jade @@ -11,9 +11,9 @@ block main header= t('ticket.create_ticket') form.form-horizontal .form-group.padding - input.input-title.form-control(type='text', placeholder= t('ticket.title')) + input.form-control(name='title', type='text', placeholder= t('ticket.title')) .form-group.padding - textarea.input-content.form-control(rows='15') + textarea.form-control(name='content', rows='15') .form-group.padding button.btn.btn-lg.btn-primary.action-create(type='button')= t('ticket.create') diff --git a/core/view/ticket/list.jade b/core/view/ticket/list.jade index cda718c..39214c7 100644 --- a/core/view/ticket/list.jade +++ b/core/view/ticket/list.jade @@ -1,62 +1,30 @@ extends ../layout -mixin displayTicketStatus(status) - - l_status = t('ticket_status.' + status) - - if status == 'closed' - span.text-muted= l_status - else if status == 'open' - span.text-primary= l_status - else if status == 'pending' - span.text-warning= l_status - else if status == 'finish' - span.text-success= l_status - else - | #{l_status} - -mixin displayTicketsTable(status, tickets) - h4= t('ticket_status.' + status) - - table.table.table-hover - thead - tr - th= t('ticket.title') - th= t('ticket.status') - tbody - for ticket in tickets - tr(data-id='#{ticket._id}') - td - a(href='/ticket/view/#{ticket._id}')= ticket.title - td - mixin displayTicketStatus(ticket.status) - prepend header title #{t('ticket.ticket_list')} | #{t(config.web.t_name)} block main - header= t('ticket.ticket_list') - - if tickets - mixin displayTicketsTable('related', tickets) - - if pending && pending.length - mixin displayTicketsTable('pending', pending) - - if open && open.length - mixin displayTicketsTable('open', open) - - if finish && finish.length - mixin displayTicketsTable('finish', finish) - - if closed && closed.length - mixin displayTicketsTable('closed', closed) + #list-view + header= t('ticket.ticket_list') + + table.table.table-hover + thead + tr + th= t('ticket.title') + th= t('ticket.status') + tbody prepend sidebar .row - if tickets - a.btn.btn-lg.btn-success(href='/ticket/create/')= t('ticket.create_ticket') - else + if account.isAdmin() a.btn.btn-lg.btn-success(href='/admin/ticket/')= t('ticket.ticket_list') + else + a.btn.btn-lg.btn-success(href='/ticket/create/')= t('ticket.create_ticket') append footer script(src='/script/ticket.js') + script(id='list-item-template', type='text/template') + td + a(href='/ticket/view/{{_id}}') {{title}} + td + span(class='text-{{color}}') {{RP.t('ticket_status.' + status)}} diff --git a/core/view/ticket/view.jade b/core/view/ticket/view.jade index ff700b2..3b35d56 100644 --- a/core/view/ticket/view.jade +++ b/core/view/ticket/view.jade @@ -1,85 +1,89 @@ extends ../layout prepend header - title #{ticket.title} | #{t(config.web.t_name)} + title #{req.ticket.title} | #{t(config.web.t_name)} append header link(rel='stylesheet', href='/style/ticket.css') block main - #ticket-view.row - .row.content(data-id='#{ticket._id}') - header - | #{ticket.title}   + .row.content - - l_status = t('ticket_status.' + ticket.status) - - if ticket.status == 'closed' - span.small.text-muted= l_status - else if ticket.status == 'open' - span.small.text-primary= l_status - else if ticket.status == 'pending' - span.small.text-warning= l_status - else if ticket.status == 'finish' - span.small.text-success= l_status - - p!= ticket.content_html - - .row - header= t('ticket.replies') - - ul.list-group - for reply in ticket.replies - li.list-group-item.clearfix - a.pull-left - img.img-avatar(src= reply.account.preferences.avatar_url) - .list-content - p!= reply.content_html - p - span.label.label-info= reply.account.username - span.label.label-default(title=reply.created_at)= moment(reply.created_at).fromNow() - - .row - if ticket.status != 'closed' - header= t('ticket.create_reply') - - form.form-horizontal - if ticket.status != 'closed' - .form-group.padding - textarea.form-control.input-content(rows='5') - .form-group.padding - if ticket.status == 'closed' - button(disabled).btn.btn-lg.btn-primary= t('ticket_status.closed') - else - button.btn.btn-lg.btn-primary.action-reply(type='button')= t('ticket.create_reply') - button(type='button', data-status='closed').btn.btn-lg.btn-danger.action-update-status= t('ticket.close_ticket') - - if req.account.inGroup('root') && (ticket.status == 'open' || ticket.status == 'pending') - button(type='button', data-status='finish').btn.btn-lg.btn-success.action-update-status= t('ticket.finish_ticket') - if req.account.inGroup('root') && ticket.status == 'closed' - button(type='button', data-status='open').btn.btn-lg.btn-success.action-update-status= t('ticket.reopen_ticket') + .row + header= t('ticket.replies') + ul.replies.list-group + .row.actions + prepend sidebar .row a.btn.btn-lg.btn-success(href='/ticket/list/')= t('ticket.ticket_list') - .row - if ticket.account - header= t('ticket.creator') - li.list-group-item.clearfix - a.pull-left - img.img-avatar(src=ticket.account.preferences.avatar_url) - p - span.label.label-info= ticket.account.username - br - span.label.label-default(title=ticket.created_at)= moment(ticket.created_at).fromNow() + .row.account-info - .row - header= t('ticket.members') - for member in ticket.members - if member - a.pull-left - img.img-avatar(src=member.preferences.avatar_url, alt=member.username) + .row.members append footer + script(src='/bower_components/moment/moment.js') script(src='/script/ticket.js') + script(id='content-template', type='text/template') + header + | {{title}}   + span.small(class='text-{{color}}') {{RP.t('ticket_status.' + status)}} + + p {:content_html}} + script(id='reply-template', type='text/template') + a.pull-left + img.img-avatar(src='{{account.preferences.avatar_url}}') + .list-content + {% if (content_html) { %} + p {:content_html}} + {% } else { %} + p {{content}} + {% }; %} + p + span.label.label-info {{account.username}} + {% if (created_at) { %} + span.label.label-default(title='{{created_at}}') {{moment(created_at).fromNow()}} + {% } else { %} + span.label.label-default ... + {% }; %} + script(id='actions-template', type='text/template') + {% if (status != 'closed') { %} + header= t('ticket.create_reply') + {% }; %} + + form.form-horizontal + {% if (status != 'closed') { %} + .form-group.padding + textarea.form-control(name='content', rows='5') + {% }; %} + .form-group.padding + {% if (status == 'closed') { %} + button(disabled).btn.btn-lg.btn-primary= t('ticket_status.closed') + {% } else { %} + button.btn.btn-lg.btn-primary.action-reply(type='button')= t('ticket.create_reply') + button(type='button', data-status='closed').btn.btn-lg.btn-danger.action-status= t('ticket.close_ticket') + {% }; %} + if account.isAdmin() + {% if (status == 'open' || status == 'pending') { %} + button(type='button', data-status='finish').btn.btn-lg.btn-success.action-status= t('ticket.finish_ticket') + {% }; %} + {% if (status == 'closed') { %} + button(type='button', data-status='open').btn.btn-lg.btn-success.action-status= t('ticket.reopen_ticket') + {% }; %} + script(id='account-info-template', type='text/template') + {% if (account) { %} + a.pull-left + img.img-avatar(src='{{account.preferences.avatar_url}}') + p + span.label.label-info {{account.username}} + br + span.label.label-default(title='{{created_at}}') {{moment(created_at).fromNow()}} + {% }; %} + script(id='members-template', type='text/template') + header= t('ticket.members') + {% members.forEach(function(member) { %} + a.pull-left + img.img-avatar(src='{{member.preferences.avatar_url}}', alt='{{member.username}}') + {% }); %}