diff --git a/core/billing-manager.coffee b/core/billing-manager.coffee
index b5d8e75..efb032f 100644
--- a/core/billing-manager.coffee
+++ b/core/billing-manager.coffee
@@ -1,7 +1,7 @@
_ = require 'lodash'
Q = require 'q'
-{Component} = root
+{Account, Component} = root
###
Class: Billing Plan, Managed by {BillingManager}.
@@ -63,6 +63,9 @@ class BillingPlan
@state(account).removePlan().then =>
@manager.destroyOverflowedComponents account
+ membersInPlan: ->
+ Account.find _.zipObject ["plans.#{@name}"], [$exists: true]
+
setupDefaultComponents: (account) ->
Q.all _.values(@components).map ({type, defaults}) ->
if defaults
diff --git a/core/public/script/admin.coffee b/core/public/script/admin.coffee
index db9d504..b45736d 100644
--- a/core/public/script/admin.coffee
+++ b/core/public/script/admin.coffee
@@ -11,12 +11,6 @@ $ ->
, ->
location.reload()
- $('.action-details').click ->
- request "/admin/account_details?account_id=#{$(@).parents('tr').data 'id'}", {}, {method: 'get'}, (account) ->
- $('.account-details-modal .label-account-id').text account._id
- $('.account-details-modal .label-details').html JSON.stringify account, null, ' '
- $('.account-details-modal').modal 'show'
-
$('.confirm-payment-modal .action-confirm-payment').click ->
request '/admin/confirm_payment',
account_id: $('.input-account-id').text()
diff --git a/core/public/style/admin.less b/core/public/style/admin.less
index da44f16..201397c 100644
--- a/core/public/style/admin.less
+++ b/core/public/style/admin.less
@@ -8,10 +8,6 @@
}
}
-.tab-pane .row > header {
- margin-left: 15px;
-}
-
pre {
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
}
diff --git a/core/root.coffee b/core/root.coffee
index bd4d64b..bdd1899 100644
--- a/core/root.coffee
+++ b/core/root.coffee
@@ -11,6 +11,8 @@ fs = require 'q-io/fs'
_ = require 'lodash'
Q = require 'q'
+require('node-jsx').install harmony: true
+
###
Class: Root object for control RootPanel, An instance is always available as the `root` global.
###
diff --git a/core/router/admin.coffee b/core/router/admin.coffee
index 784c093..346b0bd 100644
--- a/core/router/admin.coffee
+++ b/core/router/admin.coffee
@@ -1,4 +1,5 @@
{Router} = require 'express'
+React = require 'react'
_ = require 'lodash'
Q = require 'q'
@@ -25,11 +26,42 @@ router.get '/dashboard', (req, res, next) ->
limit: 10
closed:
limit: 10
- ]).done ([accounts, components, tickets]) ->
- res.render 'admin',
+ Q.all root.billing.all().map (plan) ->
+ plan.membersInPlan().then (accounts) ->
+ return {
+ name: plan.name
+ count: accounts.length
+ }
+ .then (result) ->
+ return _.mapValues _.indexBy(result, 'name'), 'count'
+ ]).done ([accounts, components, tickets, accountsInPlan]) ->
+ props =
accounts: accounts
components: components
tickets: tickets
+ package: root.package
+ plans: root.billing.all().map (plan) ->
+ plan = _.extend {}, plan,
+ users: accountsInPlan[plan.name]
+ return _.pick plan, 'components', 'join_freely', 'billing', 'name', 'users'
+ plugins: root.plugins.all().map (plugin) ->
+ return _.extend {}, _.pick(plugin, 'dependencies', 'name'),
+ registered: do ->
+ {routers, hooks, views, widgets, components, couponTypes, paymentProviders} = root.plugins.getRegisteredExtends plugin
+
+ return {
+ routers: _.pluck routers, 'path'
+ hooks: _.pluck hooks, 'path'
+ views: _.pluck views, 'view'
+ widgets: _.pluck widgets, 'view'
+ components: _.pluck components, 'name'
+ couponTypes: _.pluck couponTypes, 'name'
+ paymentProviders: _.pluck paymentProviders, 'name'
+ }
+
+ res.render 'admin/layout',
+ mainBlock: React.renderToString React.createElement require('../view/admin/dashboard.jsx'), props
+ initializeProps: props
, next
###
@@ -46,7 +78,7 @@ router.use '/users', do (router = new Router) ->
router.param 'id', (req, res, next, user_id) ->
Account.findById(user_id).then (user) ->
if req.user = user
- next
+ next()
else
next new Error 'user not found'
.catch next
@@ -66,19 +98,29 @@ router.use '/users', do (router = new Router) ->
res.json req.user.pick 'admin'
###
- Router: GET /admin/users/:id/plans/join
+ Router: POST /admin/users/:id/plans/join
+
+ Request {Object}
+
+ * `plan` {String}
+
###
router.post '/:id/plans/join', (req, res, next) ->
- req.plan.addMember(req.account).done ->
- res.sendStatus 204
+ root.billing.byName(req.body.plan).addMember(req.user).done ->
+ res.json req.user
, next
###
- Router: GET /admin/users/:id/plans/leave
+ Router: POST /admin/users/:id/plans/leave
+
+ Request {Object}
+
+ * `plan` {String}
+
###
router.post '/:id/plans/leave', (req, res, next) ->
- req.plan.removeMember(req.account).done ->
- res.sendStatus 204
+ root.billing.byName(req.body.plan).removeMember(req.user).done ->
+ res.json req.user
, next
###
diff --git a/core/view/admin/accounts.jade b/core/view/admin/accounts.jade
deleted file mode 100644
index a14d075..0000000
--- a/core/view/admin/accounts.jade
+++ /dev/null
@@ -1,36 +0,0 @@
-table.table.table-hover
- thead
- tr
- th= t('account.username')
- th= t('account.email')
- th= t('plan.')
- th= t('common.amount')
- th= t('common.actions')
- tbody
- for account in accounts
- tr(data-id='#{account._id}')
- td= account.username
- td= account.email
- td= _.keys(account.plans).join(', ')
- td= account.balance.toFixed(2)
- td
- button.btn.btn-info.btn-sm.action-details(type='button')= t('common.details')
- .btn-group
- button(type='button', data-toggle='dropdown').btn.btn-warning.btn-sm.dropdown-toggle
- | 计划
- span.caret
- ul.dropdown-menu
- li
- a(href='#') 加入套餐 A
- li
- a(href='#') 离开套餐 B
- .btn-group
- button(type='button', data-toggle='dropdown').btn.btn-primary.btn-sm.dropdown-toggle
- | #{t('common.actions')}
- span.caret
- ul.dropdown-menu
- li
- a.action-confirm-payment(href='#')= t('view.admin.confirm_payment')
- if account.balance <= 0 && !_.isEmpty(account.plans)
- li
- a.action-delete-account(href='#')= t('view.admin.delete_account')
diff --git a/core/view/admin/accounts.jsx b/core/view/admin/accounts.jsx
new file mode 100644
index 0000000..796ba25
--- /dev/null
+++ b/core/view/admin/accounts.jsx
@@ -0,0 +1,119 @@
+var React = require('react');
+var {Table, Button, DropdownButton, MenuItem, Modal} = require('react-bootstrap');
+var _ = require('lodash');
+var agent = require('../scripts/agent.coffee');
+var Cookies = require('js-cookie');
+var $ = require('jquery');
+
+module.exports = AdminAccounts = React.createClass({
+ getInitialState: function() {
+ return {
+ accounts: this.props.accounts
+ };
+ },
+
+ componentDidMount: function() {
+ $.ajaxSetup({
+ headers: {'X-Token': Cookies.get('token')}
+ });
+ },
+
+ showAccountDetails: function(account_id) {
+ this.setState({
+ detailsModal: account_id
+ });
+ },
+
+ closeAccountDetails: function() {
+ this.setState({
+ detailsModal: null
+ });
+ },
+
+ joinPlan: function(account_id, plan_name) {
+ agent.post(`/admin/users/${account_id}/plans/join`, {
+ plan: plan_name
+ }).then( account => {
+ this.updateAccount(account);
+ });
+ },
+
+ leavePlan: function(account_id, plan_name) {
+ agent.post(`/admin/users/${account_id}/plans/leave`, {
+ plan: plan_name
+ }).then( account => {
+ this.updateAccount(account);
+ });
+ },
+
+ updateAccount: function(account) {
+ this.setState({
+ accounts: this.state.accounts.map(function(originalAccount) {
+ if (originalAccount._id == account._id) {
+ return account;
+ } else {
+ return originalAccount;
+ }
+ })
+ });
+ },
+
+ render: function() {
+ return (
+
+
+
+ | 用户名 |
+ 邮箱 |
+ 付费计划 |
+ 余额 |
+ 操作 |
+
+
+
+ {this.state.accounts.map( account => {
+ return (
+
+ | {account.username} |
+ {account.email} |
+ {_.keys(account.plans).join()} |
+ {account.balance.toFixed(2)} |
+
+
+ {this.state.detailsModal == account._id && (
+
+
+ {account._id}
+
+
+ {JSON.stringify(account, null, ' ')}
+
+
+
+
+
+ )}
+
+ {this.props.plans.map( plan => {
+ if (account.plans[plan.name]) {
+ return ;
+ } else {
+ return ;
+ }
+ })}
+
+
+
+
+
+ |
+
+ )
+ })}
+
+
+ )
+ }
+});
diff --git a/core/view/admin/admin.coffee b/core/view/admin/admin.coffee
new file mode 100644
index 0000000..1ceda53
--- /dev/null
+++ b/core/view/admin/admin.coffee
@@ -0,0 +1,17 @@
+Backbone = require 'backbone'
+React = require 'react'
+
+AdminDashboard = require './dashboard.jsx'
+
+getInitializeProps = ->
+ return JSON.parse $('#initialize-props').html()
+
+AdminRouter = Backbone.Router.extend
+ routes:
+ 'admin/dashboard': 'dashboard'
+
+ dashboard: ->
+ React.render React.createElement(AdminDashboard, getInitializeProps()), document.querySelector('#main-block')
+
+new AdminRouter()
+Backbone.history.loadUrl location.pathname
diff --git a/core/view/admin/admin.less b/core/view/admin/admin.less
new file mode 100644
index 0000000..e69de29
diff --git a/core/view/admin/dashboard.jade b/core/view/admin/dashboard.jade
deleted file mode 100644
index bd56a3b..0000000
--- a/core/view/admin/dashboard.jade
+++ /dev/null
@@ -1,4 +0,0 @@
-.page-header
- h1
- | RootPanel
- small= root.package.version
diff --git a/core/view/admin/dashboard.jsx b/core/view/admin/dashboard.jsx
new file mode 100644
index 0000000..4119cdb
--- /dev/null
+++ b/core/view/admin/dashboard.jsx
@@ -0,0 +1,26 @@
+var React = require('react');
+var {TabbedArea, TabPane} = require('react-bootstrap');
+var AdminExtensions = require('./extensions.jsx');
+var AdminAccounts = require('./accounts.jsx');
+
+module.exports = AdminDashboard = React.createClass({
+ render: function() {
+ return (
+
+
+ RootPanel {this.props.package.version}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+});
diff --git a/core/view/admin/extensions.jade b/core/view/admin/extensions.jade
deleted file mode 100644
index 51191a6..0000000
--- a/core/view/admin/extensions.jade
+++ /dev/null
@@ -1,33 +0,0 @@
-.row
- header 付费方案
-
- for plan in root.billing.all()
- .col-md-3
- .panel.panel-success
- .panel-heading
- strong= plan.name
- .panel-body
- p join_freely: #{plan.join_freely}
- p components: #{_.keys(plan.components).join()}
- p billing: #{_.keys(plan.billing).join()}
- p 用户数量:0
-
-.row
- header 已加载的插件
-
- for plugin in root.plugins.all()
- - registered = root.plugins.getRegisteredExtends(plugin)
-
- .col-md-6
- .panel.panel-default
- .panel-heading
- strong= plugin.name
- .panel-body
- p dependencies: #{_.keys(plugin.dependencies).join()}
- p routes: #{_.pluck(registered.routers, 'path').join()}
- p hooks: #{_.pluck(registered.hooks, 'path').join()}
- p views: #{_.pluck(registered.views, 'view').join()}
- p widgets: #{_.pluck(registered.widgets, 'view').join()}
- p components: #{_.pluck(registered.components, 'name').join()}
- p couponTypes: #{_.pluck(registered.couponTypes, 'name').join()}
- p paymentProviders: #{_.pluck(registered.paymentProviders, 'name').join()}
diff --git a/core/view/admin/extensions.jsx b/core/view/admin/extensions.jsx
new file mode 100644
index 0000000..a0c620c
--- /dev/null
+++ b/core/view/admin/extensions.jsx
@@ -0,0 +1,48 @@
+var {Row, Col, Panel} = require('react-bootstrap');
+var React = require('react');
+var _ = require('lodash');
+
+module.exports = AdminExtensions = React.createClass({
+ render: function() {
+ var {plugins, plans} = this.props;
+
+ return (
+
+
+
+ {plans.map(function(plan) {
+ return (
+
+
+ join_freely: {plan.join_freely.toString()}
+ components: {_.keys(plan.components).join()}
+ billing: {_.keys(plan.billing).join()}
+ users: {plan.users}
+
+
+ );
+ })}
+
+
+
+ {plugins.map(function(plugin) {
+ return (
+
+
+ dependencies: {_.keys(plugin.dependencies).join()}
+ routes: {_.pluck(plugin.registered.routers, 'path').join()}
+ hooks: {_.pluck(plugin.registered.hooks, 'path').join()}
+ views: {_.pluck(plugin.registered.views, 'view').join()}
+ widgets: {_.pluck(plugin.registered.widgets, 'view').join()}
+ components: {_.pluck(plugin.registered.components, 'name').join()}
+ couponTypes: {_.pluck(plugin.registered.couponTypes, 'name').join()}
+ paymentProviders: {_.pluck(plugin.registered.paymentProviders, 'name').join()}
+
+
+ );
+ })}
+
+
+ );
+ }
+});
diff --git a/core/view/admin/layout.jade b/core/view/admin/layout.jade
new file mode 100644
index 0000000..c4dd3d5
--- /dev/null
+++ b/core/view/admin/layout.jade
@@ -0,0 +1,15 @@
+extends ../layout
+
+prepend header
+ title= helpers.title('admin.admin_panel')
+
+block main
+ != mainBlock
+
+prepend sidebar
+ .row
+ a.btn.btn-lg.btn-success(href='/admin/ticket/')= t('ticket.ticket_list')
+
+append footer
+ script(id='initialize-props', type='application/json')!= JSON.stringify(initializeProps)
+ script(src='/public/admin.js')
diff --git a/core/view/layout.jade b/core/view/layout.jade
index e0faf22..be4e079 100644
--- a/core/view/layout.jade
+++ b/core/view/layout.jade
@@ -3,7 +3,7 @@ html
head
meta(charset='utf-8')
block header
- link(rel='stylesheet', href='/public/vendor/vendor.css')
+ link(rel='stylesheet', href='/public/bootstrap.css')
link(rel='stylesheet', href='/public/core.css')
body(data-username="#{account ? account.username : ''}")
@@ -50,8 +50,9 @@ html
block content
#content.container
.row
- .col-md-9
+ #main-block.col-md-9
block main
+ != mainBlock
#sidebar.col-md-3
block sidebar
diff --git a/core/view/scripts/agent.coffee b/core/view/scripts/agent.coffee
new file mode 100644
index 0000000..a972537
--- /dev/null
+++ b/core/view/scripts/agent.coffee
@@ -0,0 +1,25 @@
+$ = require 'jquery'
+_ = require 'lodash'
+
+methods = ['get', 'post', 'delete', 'put', 'patch', 'head', 'options']
+
+agent = {}
+
+methods.forEach (method) ->
+ agent[method] = (url, data = {}, options = {}) ->
+ unless method == 'get'
+ data = JSON.stringify data
+ options.contentType = 'application/json; charset=UTF-8'
+
+ _.extend options,
+ url: url
+ data: data
+ type: method.toUpperCase()
+
+ $.ajax(options).fail (jqXHR) ->
+ if jqXHR.responseJSON?.error
+ alert root.t jqXHR.responseJSON.error
+ else
+ alert jqXHR.statusText
+
+module.exports = agent
diff --git a/core/view/styles/bootstrap.less b/core/view/styles/bootstrap.less
new file mode 100644
index 0000000..ae70856
--- /dev/null
+++ b/core/view/styles/bootstrap.less
@@ -0,0 +1,2 @@
+@import "less/bootstrap.less";
+@icon-font-path: "";
diff --git a/gulpfile.coffee b/gulpfile.coffee
index c86e68b..e92362f 100644
--- a/gulpfile.coffee
+++ b/gulpfile.coffee
@@ -12,17 +12,34 @@ uglify = require 'gulp-uglify'
minifyCss = require 'gulp-minify-css'
bowerFiles = require 'main-bower-files'
runSequence = require 'run-sequence'
+browserify = require 'browserify'
+reactify = require 'reactify'
+source = require 'vinyl-source-stream'
+coffeeify = require 'coffeeify'
gulp.task 'clean', ->
del 'public/*'
-gulp.task 'vendor:styles', ->
- gulp.src bowerFiles()
- .pipe filter '*.less'
- .pipe less()
- .pipe concat 'vendor.css'
+gulp.task 'vendor:bootstrap:styles', ->
+ gulp.src 'core/view/styles/bootstrap.less'
+ .pipe less
+ paths: ['bower_components/bootstrap']
.pipe minifyCss()
- .pipe gulp.dest 'public/vendor'
+ .pipe gulp.dest 'public'
+
+gulp.task 'scripts:admin', ->
+ browserify 'core/view/admin/admin.coffee'
+ .transform coffeeify
+ .transform reactify, es6: true
+ .bundle()
+ .pipe source 'admin.js'
+ .pipe gulp.dest 'public'
+
+gulp.task 'styles:admin', ->
+ gulp.src 'core/view/admin/admin.less'
+ .pipe less()
+ .pipe minifyCss()
+ .pipe gulp.dest 'public'
gulp.task 'vendor:scripts', ->
gulp.src bowerFiles()
@@ -35,16 +52,9 @@ gulp.task 'vendor:scripts', ->
gulp.task 'vendor:fonts', ->
gulp.src bowerFiles()
.pipe filter ['*.eot', '*.svg', '*.ttf', '*.woff', '*.woff2']
- .pipe gulp.dest 'public/fonts'
+ .pipe gulp.dest 'public'
-gulp.task 'build:vendor', ->
- runSequence [
- 'clean'
- ], [
- 'vendor:styles'
- 'vendor:scripts'
- 'vendor:fonts'
- ]
+gulp.task 'build:vendor', ['vendor:bootstrap:styles', 'vendor:scripts', 'vendor:fonts']
gulp.task 'build:styles', ->
gulp.src 'core/public/style/*.less'
@@ -64,8 +74,9 @@ gulp.task 'build:scripts', ->
gulp.task 'watch', ->
gulp.watch 'core/public/style/*.less', ['build:styles']
gulp.watch 'core/public/script/*.coffee', ['build:scripts']
+ gulp.watch ['core/view/admin/*.jsx', 'core/view/admin/*.coffee'], ['scripts:admin']
-gulp.task 'build', ['build:vendor', 'build:styles', 'build:scripts']
+gulp.task 'build', ['build:vendor', 'build:styles', 'build:scripts', 'scripts:admin', 'styles:admin']
gulp.task 'build:docs', shell.task 'node_modules/.bin/endokken --extension html --theme bullet --dest ./docs-public'
diff --git a/package.json b/package.json
index 10c8147..70eb55f 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,13 @@
"ssh2": "0.3.6",
"underscore": "1.7.0",
"validator": "3.37.0",
- "ioredis": "1.3.6"
+ "ioredis": "1.3.6",
+ "react": "0.13.3",
+ "node-jsx": "0.13.3",
+ "react-bootstrap": "0.24.5",
+ "backbone": "1.1.2",
+ "jquery": "2.1.4",
+ "js-cookie": "2.0.3"
},
"devDependencies": {
"chai": "1.10.0",
@@ -80,6 +86,10 @@
"mocha": "2.0.1",
"mocha-reporter-cov-summary": "0.1.0",
"run-sequence": "1.1.0",
- "supertest": "0.15.0"
+ "supertest": "0.15.0",
+ "browserify": "11.0.1",
+ "reactify": "1.1.1",
+ "vinyl-source-stream": "1.1.0",
+ "coffeeify": "1.1.0"
}
}