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 离开计划 {plan.name}; + } else { + return 加入计划 {plan.name}; + } + })} + + + 确认充值 + 删除账号 + +
+ ) + } +}); 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" } }