mirror of
https://github.com/HackPlan/RootPanel.git
synced 2026-03-27 22:44:32 +08:00
refactor ticket front-end
This commit is contained in:
15
README.md
15
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
|
||||
|
||||
开发版本
|
||||
主分支
|
||||
[](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 (配置文件和示例)
|
||||
* 商业授权(计划中)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)}}
|
||||
|
||||
@@ -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}}')
|
||||
{% }); %}
|
||||
|
||||
Reference in New Issue
Block a user