refactor ticket front-end

This commit is contained in:
jysperm
2015-01-22 01:05:18 +08:00
parent 40080f4ffb
commit 17a58237a6
7 changed files with 229 additions and 157 deletions

View File

@@ -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 (配置文件和示例)
* 商业授权(计划中)

View File

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

View File

@@ -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')

View File

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

View File

@@ -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')

View File

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

View File

@@ -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}}')
{% }); %}