├── .gitignore
├── .jshintrc
├── .ruby-gemset
├── .ruby-version
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
├── assets
│ ├── images
│ │ ├── .keep
│ │ ├── coffee.gif
│ │ ├── logo.png
│ │ └── no_avatar.png
│ ├── javascripts
│ │ ├── admin
│ │ │ ├── app.js
│ │ │ ├── blog-form
│ │ │ │ ├── controller
│ │ │ │ │ └── form.js
│ │ │ │ ├── index.js
│ │ │ │ └── template
│ │ │ │ │ ├── config.html
│ │ │ │ │ └── form.html
│ │ │ ├── blog
│ │ │ │ ├── controller
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ └── template
│ │ │ │ │ └── index.html
│ │ │ ├── comment
│ │ │ │ ├── controller
│ │ │ │ │ └── index.js
│ │ │ │ ├── directive
│ │ │ │ │ └── focus-if.js
│ │ │ │ ├── factory
│ │ │ │ │ └── comment.js
│ │ │ │ ├── index.js
│ │ │ │ └── template
│ │ │ │ │ └── index.html
│ │ │ ├── common
│ │ │ │ ├── ajax-spinner.js
│ │ │ │ ├── controller
│ │ │ │ │ └── nav.js
│ │ │ │ ├── directive
│ │ │ │ │ ├── file-input.js
│ │ │ │ │ ├── loading-btn.js
│ │ │ │ │ ├── popover.js
│ │ │ │ │ └── scroll-top-percent.js
│ │ │ │ ├── factory
│ │ │ │ │ ├── attach.js
│ │ │ │ │ ├── blog.js
│ │ │ │ │ ├── category.js
│ │ │ │ │ ├── confirm.js
│ │ │ │ │ ├── flash.js
│ │ │ │ │ └── page.js
│ │ │ │ ├── index.js
│ │ │ │ ├── template
│ │ │ │ │ ├── ajax-spinner.html
│ │ │ │ │ ├── confirm.html
│ │ │ │ │ └── error-for.html
│ │ │ │ └── validation.js
│ │ │ ├── dashboard
│ │ │ │ ├── controller
│ │ │ │ │ └── index.js
│ │ │ │ ├── factory
│ │ │ │ │ └── dashboard.js
│ │ │ │ ├── index.js
│ │ │ │ └── template
│ │ │ │ │ ├── index.html
│ │ │ │ │ └── logs.html
│ │ │ ├── editor
│ │ │ │ ├── directive
│ │ │ │ │ ├── file-drop.js
│ │ │ │ │ └── selection.js
│ │ │ │ ├── factory
│ │ │ │ │ └── editor.js
│ │ │ │ ├── index.js
│ │ │ │ ├── run
│ │ │ │ │ └── attach-extend.js
│ │ │ │ └── template
│ │ │ │ │ └── tip.html
│ │ │ ├── page
│ │ │ │ ├── controller
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ └── template
│ │ │ │ │ └── index.html
│ │ │ └── setting
│ │ │ │ ├── controller
│ │ │ │ ├── attach.js
│ │ │ │ ├── category.js
│ │ │ │ ├── disqus.js
│ │ │ │ ├── ga.js
│ │ │ │ ├── password.js
│ │ │ │ └── website.js
│ │ │ │ ├── directive
│ │ │ │ └── ng-equal-to.js
│ │ │ │ ├── factory
│ │ │ │ ├── disqus.js
│ │ │ │ ├── ga.js
│ │ │ │ ├── password.js
│ │ │ │ └── website.js
│ │ │ │ ├── index.js
│ │ │ │ └── template
│ │ │ │ ├── attach.html
│ │ │ │ ├── category.html
│ │ │ │ ├── disqus.html
│ │ │ │ ├── ga.html
│ │ │ │ ├── nav.html
│ │ │ │ ├── password.html
│ │ │ │ └── website.html
│ │ ├── application.js
│ │ ├── html5shiv.js
│ │ ├── respond.js
│ │ ├── sea-modules
│ │ │ ├── angular
│ │ │ │ ├── angular-highcharts
│ │ │ │ │ ├── 3.0.7
│ │ │ │ │ │ ├── angular-highcharts-debug.js
│ │ │ │ │ │ ├── angular-highcharts.js
│ │ │ │ │ │ ├── highcharts-debug.js
│ │ │ │ │ │ └── highcharts.js
│ │ │ │ │ ├── package.json
│ │ │ │ │ └── src
│ │ │ │ │ │ ├── adapter.js
│ │ │ │ │ │ ├── angular-highcharts.js
│ │ │ │ │ │ └── highcharts.js
│ │ │ │ ├── angularjs-all
│ │ │ │ │ ├── 1.2.7
│ │ │ │ │ │ ├── index-debug.js
│ │ │ │ │ │ └── index.js
│ │ │ │ │ ├── package.json
│ │ │ │ │ └── src
│ │ │ │ │ │ └── index.js
│ │ │ │ ├── angularjs
│ │ │ │ │ └── 1.2.7
│ │ │ │ │ │ ├── angular-animate-debug.js
│ │ │ │ │ │ ├── angular-animate.js
│ │ │ │ │ │ ├── angular-cookies-debug.js
│ │ │ │ │ │ ├── angular-cookies.js
│ │ │ │ │ │ ├── angular-debug.js
│ │ │ │ │ │ ├── angular-mock.js
│ │ │ │ │ │ ├── angular-resource-debug.js
│ │ │ │ │ │ ├── angular-resource.js
│ │ │ │ │ │ ├── angular-route-debug.js
│ │ │ │ │ │ ├── angular-route.js
│ │ │ │ │ │ ├── angular-sanitize-debug.js
│ │ │ │ │ │ ├── angular-sanitize.js
│ │ │ │ │ │ ├── angular-touch-debug.js
│ │ │ │ │ │ ├── angular-touch.js
│ │ │ │ │ │ └── angular.js
│ │ │ │ ├── bootstrap
│ │ │ │ │ ├── 0.0.1
│ │ │ │ │ │ ├── index-debug.js
│ │ │ │ │ │ └── index.js
│ │ │ │ │ ├── package.json
│ │ │ │ │ └── src
│ │ │ │ │ │ ├── dropdown-toggle.js
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── modal.js
│ │ │ │ │ │ ├── pagination.js
│ │ │ │ │ │ └── template
│ │ │ │ │ │ ├── modal
│ │ │ │ │ │ ├── backdrop.html
│ │ │ │ │ │ └── window.html
│ │ │ │ │ │ └── pagination
│ │ │ │ │ │ └── pagination.html
│ │ │ │ └── seajs-lazy-angular
│ │ │ │ │ ├── 0.0.1
│ │ │ │ │ ├── seajs-lazy-angular-debug.js
│ │ │ │ │ └── seajs-lazy-angular.js
│ │ │ │ │ ├── package.json
│ │ │ │ │ └── src
│ │ │ │ │ └── seajs-lazy-angular.js
│ │ │ ├── gallery
│ │ │ │ ├── marked
│ │ │ │ │ └── 0.3.0
│ │ │ │ │ │ └── marked.js
│ │ │ │ ├── selection
│ │ │ │ │ └── 0.9.0
│ │ │ │ │ │ ├── package.json
│ │ │ │ │ │ ├── selection-debug.js
│ │ │ │ │ │ └── selection.js
│ │ │ │ └── underscore
│ │ │ │ │ └── 1.4.4
│ │ │ │ │ ├── package.json
│ │ │ │ │ ├── underscore-debug.js
│ │ │ │ │ └── underscore.js
│ │ │ └── seajs
│ │ │ │ ├── seajs-text
│ │ │ │ └── 1.0.2
│ │ │ │ │ ├── package.json
│ │ │ │ │ ├── seajs-text-debug.js
│ │ │ │ │ └── seajs-text.js
│ │ │ │ └── seajs
│ │ │ │ └── 2.1.1
│ │ │ │ ├── package.json
│ │ │ │ ├── sea-debug.js
│ │ │ │ ├── sea.js
│ │ │ │ └── sea.js.map
│ │ └── xhr-shim.js
│ └── stylesheets
│ │ ├── admin.css.scss
│ │ ├── admin
│ │ ├── _blog.css.scss
│ │ ├── _blog_form.css.scss
│ │ ├── _comment.css.scss
│ │ ├── _dashboard.css.scss
│ │ ├── _page.css.scss
│ │ └── _setting.css.scss
│ │ ├── public-bootstrap.css.scss
│ │ └── public.css.scss
├── cells
│ ├── nav
│ │ └── show.html.erb
│ └── nav_cell.rb
├── controllers
│ ├── admin
│ │ ├── application_controller.rb
│ │ ├── attaches_controller.rb
│ │ ├── blogs_controller.rb
│ │ ├── categories_controller.rb
│ │ ├── comments_controller.rb
│ │ ├── dashboard_controller.rb
│ │ ├── disqus_controller.rb
│ │ ├── ga_controller.rb
│ │ ├── home_controller.rb
│ │ ├── pages_controller.rb
│ │ ├── password_controller.rb
│ │ ├── passwords_controller.rb
│ │ ├── sessions_controller.rb
│ │ └── websites_controller.rb
│ ├── application_controller.rb
│ ├── archive_controller.rb
│ ├── blogs_controller.rb
│ ├── categories_controller.rb
│ ├── concerns
│ │ └── .keep
│ ├── feed_controller.rb
│ ├── pages_controller.rb
│ └── tags_controller.rb
├── helpers
│ └── application_helper.rb
├── mailers
│ └── .keep
├── models
│ ├── attach.rb
│ ├── blog.rb
│ ├── category.rb
│ ├── comment.rb
│ ├── concerns
│ │ ├── .keep
│ │ └── has_slug.rb
│ ├── disqus.rb
│ ├── ga.rb
│ ├── page.rb
│ ├── password.rb
│ ├── setting.rb
│ └── website.rb
├── uploaders
│ └── attach_uploader.rb
└── views
│ ├── admin
│ ├── attaches
│ │ ├── _show.json.jbuilder
│ │ ├── create.json.jbuilder
│ │ └── index.json.jbuilder
│ ├── blogs
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── categories
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── comments
│ │ ├── context.json.jbuilder
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── home
│ │ └── show.html.erb
│ ├── pages
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ └── sessions
│ │ └── new.html.erb
│ ├── archive
│ └── show.html.erb
│ ├── blogs
│ ├── index.html.erb
│ └── show.html.erb
│ ├── feed
│ └── show.rss.builder
│ ├── kaminari
│ ├── _first_page.html.erb
│ ├── _gap.html.erb
│ ├── _last_page.html.erb
│ ├── _next_page.html.erb
│ ├── _page.html.erb
│ ├── _paginator.html.erb
│ ├── _prev_page.html.erb
│ └── tiny
│ │ ├── _next_page.html.erb
│ │ ├── _paginator.html.erb
│ │ └── _prev_page.html.erb
│ ├── layouts
│ ├── _ga.html.erb
│ ├── _header.html.erb
│ └── public.html.erb
│ └── pages
│ └── show.html.erb
├── bin
├── bundle
├── rails
└── rake
├── config.ru
├── config
├── application.rb
├── boot.rb
├── compass.rb
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── initializers
│ ├── acts_as_taggable_on.rb
│ ├── backtrace_silencers.rb
│ ├── filter_parameter_logging.rb
│ ├── inflections.rb
│ ├── markdown.rb
│ ├── mime_types.rb
│ ├── rest_client.rb
│ ├── secret_token.rb
│ ├── session_store.rb
│ └── wrap_parameters.rb
├── locales
│ ├── en.yml
│ └── zh-CN.yml
├── routes.rb
├── seajs_config.yml
└── unicorn.rb
├── db
├── migrate
│ ├── 20131216081115_create_blog.rb
│ ├── 20131225085020_create_categories.rb
│ ├── 20131226010542_acts_as_taggable_on_migration.rb
│ ├── 20131231034743_create_settings.rb
│ ├── 20140102062200_create_pages.rb
│ └── 20140108025118_create_attaches.rb
├── schema.rb
└── seeds.rb
├── lib
├── assets
│ └── .keep
├── disqus_client.rb
├── ga_client.rb
└── tasks
│ └── .keep
├── log
└── .keep
├── public
├── 404.html
├── 422.html
├── 500.html
├── favicon.ico
└── robots.txt
├── test
├── controllers
│ └── .keep
├── fixtures
│ ├── .keep
│ ├── attaches.yml
│ ├── categories.yml
│ ├── pages.yml
│ └── settings.yml
├── helpers
│ └── .keep
├── integration
│ └── .keep
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ ├── attach_test.rb
│ ├── category_test.rb
│ ├── page_test.rb
│ └── setting_test.rb
└── test_helper.rb
└── vendor
└── assets
├── javascripts
└── .keep
└── stylesheets
└── .keep
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*.log
16 | /tmp
17 | /.idea
18 | /public/assets
19 | /public/uploads
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "bitwise": true,
4 | "curly": true,
5 | "eqeqeq": true,
6 | "immed": true,
7 | "newcap": true,
8 | "noarg": true,
9 | "noempty": true,
10 | "undef": true,
11 | "indent": 4,
12 | "maxdepth": 4,
13 | "expr": true,
14 | "loopfunc": true,
15 | "browser": true,
16 |
17 | "globals": {
18 | "seajs": true,
19 | "define": true
20 | }
21 | }
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | klog2
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.0.0
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
4 | gem 'rails', '4.0.2'
5 |
6 | # Use mysql as the database for Active Record
7 | gem 'mysql2'
8 |
9 | # Use SCSS for stylesheets
10 | gem 'sass-rails', '~> 4.0.0'
11 | gem 'bootstrap-sass', '~> 3.0.3.0'
12 | gem 'font-awesome-rails'
13 | gem 'compass-rails'
14 |
15 |
16 | # Use Uglifier as compressor for JavaScript assets
17 | gem 'uglifier', '>= 1.3.0'
18 |
19 | # Use CoffeeScript for .js.coffee assets and views
20 | gem 'coffee-rails', '~> 4.0.0'
21 |
22 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes
23 | # gem 'therubyracer', platforms: :ruby
24 |
25 | # Use jquery as the JavaScript library
26 | gem 'jquery-rails'
27 |
28 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
29 | gem 'turbolinks', '~> 2.1'
30 |
31 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
32 | gem 'jbuilder', '~> 1.2'
33 |
34 | group :doc do
35 | # bundle exec rake doc:rails generates the API under doc/api.
36 | gem 'sdoc', require: false
37 | end
38 |
39 | gem 'rest-client', '~>1.6.7'
40 | gem 'enumerize', '~>0.7.0'
41 | gem 'acts-as-taggable-on', '~> 2.4.1'
42 | gem 'carrierwave', '~>0.9.0'
43 | gem 'mini_magick', '~>3.7.0'
44 | gem 'redcarpet', '~>3.0.0'
45 | gem 'coderay', '~>1.1.0'
46 | gem 'truncate_html', '~> 0.9.2'
47 | gem 'kaminari', '~> 0.15.0'
48 | gem 'cells', '~> 3.9.1'
49 | gem 'seajs-rails'
50 |
51 | # GA
52 | gem 'google-api-client', '~> 0.7.1'
53 | gem 'oauth2', '~> 0.9.3'
54 | gem 'legato', '~> 0.3.0'
55 |
56 | # Use ActiveModel has_secure_password
57 | # gem 'bcrypt-ruby', '~> 3.1.2'
58 |
59 | # Use unicorn as the app server
60 | gem 'unicorn'
61 | gem 'unicorn-worker-killer'
62 | gem 'newrelic_rpm'
63 | # Use Capistrano for deployment
64 | # gem 'capistrano', group: :development
65 |
66 | # Use debugger
67 | # gem 'debugger', group: [:development, :test]
68 |
69 | group :development do
70 | gem 'pry-rails'
71 | gem 'pry-nav'
72 | end
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Klog
2 |
3 | > Blog app created with Rails 4.x and Angular.js
4 |
5 | -----
6 |
7 | ## 功能简介
8 |
9 | Klog 是一个简单的个人博客程序,这个项目是它的新版本,[旧版 Klog](https://github.com/edokeh/klog) 不再维护
10 |
11 | Klog2 包含以下功能:
12 |
13 | * 单人博客程序
14 | * 使用 GFM(GitHub Flavored Markdown) 格式撰写文章
15 | * 内置 Markdown 编辑器,支持附件上传
16 | * 使用 Disqus 评论系统,可在 Klog 后台直接操作评论
17 | * 支持添加 Google Analytic 追踪,并可在 Dashboard 中查看简单的 GA 统计报表
18 |
19 | ## Screenshots
20 |
21 | 
22 |
23 | [更多截图](http://edokeh.github.io/klog2/)
24 |
25 | ## 使用技术
26 |
27 | Klog2 使用了以下技术:
28 |
29 | * 选用 Rails 4.x 作为 WEB 框架
30 | * 使用 Turbolink 加速前台博客页面
31 | * 使用 Angular.js 1.2.x 搭建后台
32 | * 使用 Sea.js 作为 Javascript 模块加载器,并与 Angular.js 实现了良好配合
33 | * 使用 [seajs-rails](https://github.com/edokeh/seajs-rails) 将 Sea.js 的打包过程与 Rails 结合
34 |
35 | ## 演示
36 |
37 | 我的 Blog 地址 http://chaoskeh.com
38 |
39 | 演示版地址 http://klog-test.tk/admin
40 |
41 | 演示版密码 password
42 |
43 | ## 安装
44 |
45 | 参考 [How to install](https://github.com/edokeh/klog2/wiki/How-to-install)
46 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Klog2::Application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/app/assets/images/.keep
--------------------------------------------------------------------------------
/app/assets/images/coffee.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/app/assets/images/coffee.gif
--------------------------------------------------------------------------------
/app/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/app/assets/images/logo.png
--------------------------------------------------------------------------------
/app/assets/images/no_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/app/assets/images/no_avatar.png
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/app.js:
--------------------------------------------------------------------------------
1 | /*global ADMIN_PATH: true, CSRF_TOKEN: true, alert: true */
2 | define(function(require, exports, module) {
3 | var angular = require('angularjs');
4 | var common = require('./common/index');
5 | var SeajsLazyAngular = require('seajs-lazy-angular');
6 |
7 | var admin = angular.module('admin', ['ngAll', common.name]);
8 |
9 | admin.config(SeajsLazyAngular.cacheInternals);
10 | SeajsLazyAngular.patchAngular();
11 | SeajsLazyAngular.setResolveCallback(['$rootScope', 'controller', function($rootScope, controller) {
12 | $rootScope.title = controller.title + ' - Klog 后台管理';
13 | $rootScope.nav = controller.nav;
14 | }]);
15 |
16 | admin.config(['$routeProvider', '$httpProvider', function($routeProvider, $httpProvider) {
17 |
18 | $httpProvider.defaults.headers.common['X-CSRF-Token'] = CSRF_TOKEN;
19 | $httpProvider.defaults.headers.common['X-REQUESTED-WITH'] = 'XMLHttpRequest';
20 | $httpProvider.interceptors.push(['$q', function($q) {
21 | return {
22 | 'responseError': function(responseError) {
23 | if (responseError.status >= 500) {
24 | alert('出错啦!刷新一下吧!');
25 | }
26 | return $q.reject(responseError);
27 | }
28 | };
29 | }]);
30 |
31 | var blog = SeajsLazyAngular.createLazyStub(ADMIN_PATH + '/blog/index');
32 | var blogForm = SeajsLazyAngular.createLazyStub(ADMIN_PATH + '/blog-form/index');
33 | var comment = SeajsLazyAngular.createLazyStub(ADMIN_PATH + '/comment/index');
34 | var page = SeajsLazyAngular.createLazyStub(ADMIN_PATH + '/page/index');
35 | var setting = SeajsLazyAngular.createLazyStub(ADMIN_PATH + '/setting/index');
36 | var dashboard = SeajsLazyAngular.createLazyStub(ADMIN_PATH + '/dashboard/index');
37 |
38 | $routeProvider
39 | .when('/blog', blog.createRoute('./controller/index'))
40 |
41 | .when('/blog/new', blogForm.createRoute('./controller/form'))
42 | .when('/blog/:id/edit', blogForm.createRoute('./controller/form'))
43 |
44 | .when('/comment', comment.createRoute('./controller/index'))
45 |
46 | .when('/page', page.createRoute('./controller/index'))
47 |
48 | .when('/setting/website', setting.createRoute('./controller/website'))
49 | .when('/setting/category', setting.createRoute('./controller/category'))
50 | .when('/setting/password', setting.createRoute('./controller/password'))
51 | .when('/setting/disqus', setting.createRoute('./controller/disqus'))
52 | .when('/setting/ga', setting.createRoute('./controller/ga'))
53 | .when('/setting/attach', setting.createRoute('./controller/attach', {reloadOnSearch: false}))
54 |
55 | .when('/dashboard', dashboard.createRoute('./controller/index'))
56 | .otherwise({redirectTo: '/dashboard'});
57 | }]);
58 |
59 | angular.bootstrap(window.document, ['admin']);
60 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/blog-form/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 写文章模块
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 | var editor = require('../editor/index');
7 |
8 | var blogEdit = angular.module('blog', [editor.name]);
9 |
10 | blogEdit.seajsController(require('./controller/form'));
11 |
12 | module.exports = blogEdit;
13 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/blog-form/template/config.html:
--------------------------------------------------------------------------------
1 |
46 |
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/blog/controller/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 文章列表
3 | */
4 | define(function(require, exports, module) {
5 | var _ = require('_');
6 |
7 | var IndexController = ['$scope', '$routeParams', 'Blog', 'Flash', 'Confirm', function($scope, $routeParams, Blog, Flash, Confirm) {
8 |
9 | var getOnce;
10 |
11 | // 根据页数获取 blog 列表
12 | $scope.getBlogs = function(page) {
13 | return Blog.query({
14 | status: $scope.currStatus.value,
15 | page: page
16 | }, function(data) {
17 | $scope.blogs = $scope.blogs.concat(data);
18 | $scope.page = data.$page;
19 | // 自动选中一篇,可能来自新建或修改页面
20 | if ($scope.blogs.length > 0 && !$scope.currBlog) {
21 | var tmpId = Flash.tmp();
22 | var blog = tmpId ? _.findWhere($scope.blogs, {id: tmpId}) : $scope.blogs[0];
23 | $scope.showBlog(blog);
24 | }
25 | getOnce = _.once($scope.getBlogs); // 防止滚动条到底事件重复执行
26 | });
27 | };
28 |
29 | // 显示某一篇 blog 详情
30 | $scope.showBlog = function(blog) {
31 | $scope.currBlog = blog;
32 | if (!blog) {
33 | return;
34 | }
35 | blog.$get({detail: true});
36 | };
37 |
38 | // 删除 blog
39 | $scope.remove = function(blog, e) {
40 | e.stopPropagation();
41 | Confirm.open('确定要删除“' + blog.title + '”?').then(function() {
42 | blog.$remove(function() {
43 | $scope.blogs = _.without($scope.blogs, blog);
44 | if ($scope.currBlog === blog) {
45 | $scope.showBlog($scope.blogs[0]);
46 | }
47 | });
48 | });
49 | };
50 |
51 | // 立即发布
52 | $scope.publish = function(blog) {
53 | Confirm.open('确定要发布“' + blog.title + '”').then(function() {
54 | blog.$publish(function() {
55 | $scope.blogs = _.without($scope.blogs, blog);
56 | $scope.showBlog($scope.blogs[0]);
57 | });
58 | });
59 | };
60 |
61 | // scroll 到底部时载入下一页
62 | $scope.$watch('listScrollTop', function(value) {
63 | if (value >= 0.95 && $scope.page.hasNext) {
64 | getOnce($scope.page.current + 1);
65 | }
66 | });
67 |
68 | $scope.STATUS = Blog.STATUS;
69 | $scope.currStatus = Blog.getStatusText($routeParams.status);
70 | $scope.blogs = [];
71 | $scope.$promise = $scope.getBlogs(1);
72 | }];
73 |
74 | IndexController.title = '文章列表';
75 | IndexController.nav = 'blog';
76 |
77 | module.exports = IndexController;
78 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/blog/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * BLOG 模块
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | var blog = angular.module('blog', []);
8 |
9 | blog.seajsController(require('./controller/index'));
10 |
11 | module.exports = blog;
12 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/comment/directive/focus-if.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 让输入框获得焦点,当条件为真时
3 | *
4 | */
5 | define(function(require, exports, module) {
6 | module.exports = {
7 | 'focusIf': ['$parse', function($parse) {
8 | return {
9 | restrict: 'CA',
10 | link: function(scope, element, attrs) {
11 | var getter = $parse(attrs.focusIf);
12 |
13 | scope.$watch(function() {
14 | return scope.$eval(attrs.focusIf);
15 | }, function(value) {
16 | if (value) {
17 | setTimeout(function() {
18 | element[0].focus();
19 | }, 200);
20 | getter.assign(scope, false);
21 | }
22 | });
23 | }
24 | };
25 | }]
26 | };
27 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/comment/factory/comment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 评论
3 | */
4 | define(function (require, exports, module) {
5 | var _ = require('_');
6 |
7 | module.exports = {
8 | Comment: ['$resource', '$http', function ($resource, $http) {
9 | var URL = '/admin/comments';
10 | var CONTEXT_URL = URL + '/context';
11 |
12 | var Comment = $resource(URL + '/:id', {id: '@id'}, {
13 | query: {
14 | method: 'GET',
15 | isArray: true,
16 | transformResponse: $http.defaults.transformResponse.concat([function (data, header) {
17 | if (data.array && _.isArray(data.array)) {
18 | var array = data.array;
19 | array.$cursor = data.cursor;
20 | return array;
21 | }
22 | else {
23 | return data;
24 | }
25 | }]),
26 | interceptor: {
27 | response: function (response) {
28 | response.resource.$cursor = response.data.$cursor;
29 | return response.resource;
30 | }
31 | }
32 | },
33 | getContext: {
34 | method: 'GET',
35 | isArray: true,
36 | url: CONTEXT_URL
37 | }
38 | });
39 |
40 | return Comment;
41 | }]
42 | };
43 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/comment/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 设置模块
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | var comment = angular.module('comment', []);
8 |
9 | comment.seajsController(require('./controller/index'));
10 |
11 | comment.factory(require('./factory/comment'));
12 |
13 | comment.directive(require('./directive/focus-if'));
14 |
15 | module.exports = comment;
16 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/ajax-spinner.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ajax 指示器
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 | var _ = require('_');
7 |
8 | var ajaxSpinner = angular.module('ajax-spinner', []);
9 |
10 | ajaxSpinner.config(['$httpProvider', function($httpProvider) {
11 | $httpProvider.interceptors.push(['$rootScope', '$q', '$timeout', function($rootScope, $q, $timeout) {
12 | var requestQueue = [];
13 | var SHOW_AFTER = 200;
14 |
15 | // spinner 会在请求一段时间后才显示
16 | function requestHandler(config) {
17 | config.reqTimeout = $timeout(function() {
18 | $rootScope.ajaxing = true;
19 | $rootScope.ajaxingMethod = config.method;
20 | }, SHOW_AFTER);
21 | config.requestStartAt = new Date().getTime();
22 | requestQueue.push(config);
23 |
24 | return config || $q.when(config);
25 | }
26 |
27 | // 响应到达时隐藏
28 | // 速度快的请求不显示 spinner
29 | // 所有响应到达时才隐藏,避免上下反复
30 | function responseHandler(response) {
31 | // config 来自 request 的设置
32 | var config = response.config;
33 | requestQueue = _.without(requestQueue, config);
34 | $timeout.cancel(config.reqTimeout);
35 | $timeout(function() {
36 | if (requestQueue.length === 0) {
37 | $rootScope.ajaxing = false;
38 | }
39 | }, config.requestStartAt + SHOW_AFTER + 500 + 200 - new Date().getTime());
40 |
41 | if (response.status >= 400) {
42 | return $q.reject(response);
43 | }
44 | else {
45 | return response || $q.when(response);
46 | }
47 | }
48 |
49 | return {
50 | 'request': requestHandler,
51 | 'response': responseHandler,
52 | 'responseError': responseHandler
53 | };
54 | }]);
55 | }]);
56 |
57 | ajaxSpinner.directive('ajaxSpinner', function() {
58 | return {
59 | restrict: 'EA',
60 | template: require('./template/ajax-spinner.html')
61 | };
62 | });
63 |
64 | module.exports = ajaxSpinner;
65 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/controller/nav.js:
--------------------------------------------------------------------------------
1 | define(function(require, exports, module) {
2 | var angular = require('angularjs');
3 | var _ = require('_');
4 |
5 | module.exports = {
6 | nav: ['$scope', '$location', 'Confirm', '$http', '$rootScope', function($scope, $location, Confirm, $http, $rootScope) {
7 | $scope.navItems = [
8 | {
9 | name: '文章',
10 | ico: 'fa-files-o',
11 | url: '/blog',
12 | nav: 'blog'
13 | },
14 | {
15 | name: '写文章',
16 | ico: 'fa-pencil',
17 | url: '/blog/new',
18 | nav: 'blog-form'
19 | },
20 | {
21 | name: '评论',
22 | ico: 'fa-comments',
23 | url: '/comment',
24 | nav: 'comment'
25 | },
26 | {
27 | name: '页面',
28 | ico: 'fa-link',
29 | url: '/page',
30 | nav: 'page'
31 | },
32 | {
33 | name: '设置',
34 | ico: 'fa-cogs',
35 | url: '/setting/website',
36 | nav: 'setting'
37 | }
38 | ];
39 |
40 | $scope.$on('$routeChangeSuccess', function() {
41 | _.each($scope.navItems, function(item) {
42 | item.active = item.nav === $rootScope.nav;
43 | });
44 | });
45 |
46 | $scope.logout = function() {
47 | Confirm.open('确定要退出后台?').then(function() {
48 | $http.delete('/admin/session').success(function() {
49 | location.href = '/admin/session/new';
50 | });
51 | });
52 | };
53 | }]
54 | };
55 |
56 | });
57 |
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/directive/file-input.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 为元素提供类似 input[type=file] 的功能,点击后出现上传框
3 | * 示例
4 | */
5 | define(function(require, exports, module) {
6 | var angular = require('angularjs');
7 |
8 | module.exports = {
9 | 'fileInput': [function() {
10 | return {
11 | restrict: 'CA',
12 | scope: {
13 | fileInput: '&',
14 | accept: '@',
15 | multiple: '@'
16 | },
17 | link: function(scope, element, attrs) {
18 |
19 | element.bind('click', function() {
20 | var fileInput = createInput();
21 | element.after(fileInput);
22 | fileInput[0].click();
23 | });
24 |
25 | element.on('$destroy', removeFileInput);
26 |
27 | // 创建 input file
28 | function createInput() {
29 | removeFileInput();
30 | createInput.fileInput = angular.element(' ');
31 | if (scope.multiple) {
32 | createInput.fileInput.attr('multiple', 'multiple');
33 | }
34 | createInput.fileInput.bind('change', function(e) {
35 | var fileList = e.target.files;
36 | scope.$apply(function() {
37 | scope.fileInput({files: fileList});
38 | });
39 | });
40 | return createInput.fileInput;
41 | }
42 |
43 | function removeFileInput() {
44 | if (createInput.fileInput) {
45 | createInput.fileInput.remove();
46 | }
47 | }
48 | }
49 | };
50 | }]
51 | };
52 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/directive/loading-btn.js:
--------------------------------------------------------------------------------
1 | /**
2 | * loading button
3 | * 点击后不可用并显示相应字样,满足 loading-cancel 条件时恢复
4 | *
5 | *
6 | */
7 | define(function(require, exports, module) {
8 | var angular = require('angularjs');
9 |
10 | module.exports = {
11 | loadingBtn: ['$timeout', function($timeout) {
12 | return {
13 | restrict: 'A',
14 | require: '?^form',
15 | link: function(scope, element, attrs, form) {
16 | var originalHTML = element.html();
17 |
18 | element.on('click', function() {
19 |
20 | if (form.$valid) {
21 | $timeout(function() {
22 | attrs.$set('disabled', 'disabled');
23 | element.text(attrs.loadingBtn || '保存中...');
24 | }, 0);
25 | }
26 | });
27 |
28 | scope.$watch(function() {
29 | return scope.$eval(attrs.loadingCancel);
30 | }, function(value) {
31 | if (value) {
32 | attrs.$set('disabled', null);
33 | element.html(originalHTML);
34 | }
35 | });
36 | }
37 | };
38 | }]
39 | };
40 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/directive/scroll-top-percent.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 元素 scrollTop 的双向 bind
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | module.exports = {
8 | scrollTopPercent: function() {
9 | return {
10 | restrict: 'A',
11 | scope: {
12 | scrollTopPercent: '='
13 | },
14 | link: function(scope, element, attrs, ngModel) {
15 | var ignoreEvent;
16 | var ignoreWatch;
17 |
18 | // scope 改变时,修改 scrollTop
19 | scope.$watch('scrollTopPercent', function(value) {
20 | if (angular.isUndefined(value) || ignoreWatch) {
21 | ignoreWatch = false;
22 | return;
23 | }
24 | element[0].scrollTop = value * (element[0].scrollHeight - element[0].clientHeight);
25 | ignoreEvent = true;
26 | });
27 |
28 | // scroll 事件时修改 scope
29 | element.on('scroll', function() {
30 | if (ignoreEvent) {
31 | ignoreEvent = false;
32 | return;
33 | }
34 | scope.$apply(function() {
35 | var scrollTopPercent = element[0].scrollTop / (element[0].scrollHeight - element[0].clientHeight);
36 | scope.scrollTopPercent = scrollTopPercent;
37 | ignoreWatch = true;
38 | });
39 | });
40 | }
41 | };
42 | }
43 | };
44 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/factory/blog.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Blog
3 | */
4 | define(function(require, exports, module) {
5 | var _ = require('_');
6 |
7 | module.exports = {
8 | 'Blog': ['$resource', '$http', 'Attach', function($resource, $http, Attach) {
9 | var Blog = $resource('/admin/blogs/:id', {id: '@id'}, {
10 | create: {method: 'POST'},
11 | update: {method: 'PUT'},
12 | query: {
13 | method: 'GET',
14 | isArray: true,
15 | transformResponse: $http.defaults.transformResponse.concat([function(data, header) {
16 | if (data.array && _.isArray(data.array)) {
17 | var array = data.array;
18 | array.$page = data.page;
19 | return array;
20 | }
21 | else {
22 | return data;
23 | }
24 | }]),
25 | interceptor: {
26 | response: function(response) {
27 | response.resource.$page = response.data.$page;
28 | return response.resource;
29 | }
30 | }
31 | },
32 | get: {
33 | method: 'GET',
34 | interceptor: {
35 | response: function(response) {
36 | if (response.resource.attaches) {
37 | response.resource.attaches = _.map(response.resource.attaches, function(a) {
38 | return new Attach(a);
39 | });
40 | }
41 | }
42 | }
43 | },
44 | publish: {
45 | method: 'POST',
46 | url: '/admin/blogs/:id/publish'
47 | }
48 | });
49 |
50 | Blog.prototype.$save = function() {
51 | if (this.id) {
52 | this.$update.apply(this, arguments);
53 | }
54 | else {
55 | this.$create.apply(this, arguments);
56 | }
57 | };
58 |
59 | Blog.STATUS = [
60 | {value: '1', name: '已发布'},
61 | {value: '0', name: '草稿'},
62 | ];
63 |
64 | Blog.DEFAULT_STATUS = '1';
65 |
66 | Blog.getStatusText = function(value) {
67 | value = value || Blog.DEFAULT_STATUS;
68 | return _.findWhere(Blog.STATUS, {value: value});
69 | };
70 |
71 | return Blog;
72 | }]
73 | };
74 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/factory/category.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 分类
3 | */
4 | define(function(require, exports, module) {
5 |
6 | module.exports = {
7 | 'Category': ['$resource', function($resource) {
8 | var Category = $resource('/admin/categories/:id', {id: '@id'}, {
9 | update: {
10 | method: 'PUT'
11 | }
12 | });
13 |
14 | return Category;
15 | }]
16 | };
17 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/factory/confirm.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 确认框
3 | */
4 | define(function(require, exports, module) {
5 |
6 | module.exports = {
7 | 'Confirm': ['$modal', function($modal) {
8 | return {
9 | open: function(text) {
10 | var modal = $modal.open({
11 | template: require('../template/confirm.html'),
12 | controller: ['$scope', '$modalInstance', function($scope, $modalInstance) {
13 | $scope.text = text;
14 | $scope.modal = $modalInstance;
15 | }]
16 | });
17 | return modal.result;
18 | }
19 | };
20 | }]
21 | };
22 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/factory/flash.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 用于在 route 之间传递消息的组件,类似 Rails 的 flash
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | module.exports = {
8 | 'Flash': function() {
9 | var Flash = {};
10 | var flashMsg = {};
11 |
12 | // getter/setter
13 | angular.forEach(['success', 'error', 'tmp'], function(key) {
14 | Flash[key] = function(value) {
15 | if (value) {
16 | flashMsg[key] = value;
17 | }
18 | else {
19 | var tmp = flashMsg[key];
20 | delete flashMsg[key];
21 | return tmp;
22 | }
23 | };
24 | });
25 | return Flash;
26 | }
27 | };
28 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/factory/page.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 页面
3 | */
4 | define(function(require, exports, module) {
5 | var _ = require('_');
6 |
7 | module.exports = {
8 | 'Page': ['$resource', '$http', 'Attach', '$sce', function($resource, $http, Attach, $sce) {
9 |
10 | var responseInterceptor = {
11 | response: function(response) {
12 | if (response.resource.attaches) {
13 | response.resource.attaches = _.map(response.resource.attaches, function(a) {
14 | return new Attach(a);
15 | });
16 | }
17 | response.resource.html_content = $sce.trustAsHtml(response.resource.html_content);
18 | }
19 | };
20 |
21 | var Page = $resource('/admin/pages/:id', {id: '@id'}, {
22 | create: {
23 | method: 'POST',
24 | interceptor: responseInterceptor
25 | },
26 | update: {
27 | method: 'PUT',
28 | interceptor: responseInterceptor
29 | },
30 | get: {
31 | method: 'GET',
32 | interceptor: responseInterceptor
33 | },
34 | up: {
35 | method: 'POST',
36 | url: '/admin/pages/:id/up'
37 | },
38 | down: {
39 | method: 'POST',
40 | url: '/admin/pages/:id/down'
41 | }
42 | });
43 |
44 | Page.prototype.$save = function() {
45 | if (this.id) {
46 | this.$update.apply(this, arguments);
47 | }
48 | else {
49 | this.$create.apply(this, arguments);
50 | }
51 | };
52 |
53 | return Page;
54 | }]
55 | };
56 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/index.js:
--------------------------------------------------------------------------------
1 | define(function(require, exports, module) {
2 | var angular = require('angularjs');
3 | var bootstrap = require('bootstrap');
4 | var ajaxSpinner = require('./ajax-spinner');
5 | var validation = require('./validation');
6 |
7 | var common = angular.module('common', [
8 | bootstrap.name,
9 | ajaxSpinner.name,
10 | validation.name
11 | ]);
12 |
13 | common.controller(require('./controller/nav'));
14 |
15 | common.factory(require('./factory/attach'));
16 | common.factory(require('./factory/blog'));
17 | common.factory(require('./factory/category'));
18 | common.factory(require('./factory/page'));
19 | common.factory(require('./factory/confirm'));
20 | common.factory(require('./factory/flash'));
21 |
22 | common.directive(require('./directive/popover'));
23 | common.directive(require('./directive/scroll-top-percent'));
24 | common.directive(require('./directive/loading-btn'));
25 | common.directive(require('./directive/file-input'));
26 |
27 | module.exports = common;
28 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/template/ajax-spinner.html:
--------------------------------------------------------------------------------
1 |
2 | {{ajaxingMethod == "GET" ? "载入中" : "处理中"}}...
3 |
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/template/confirm.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ text }}
5 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/common/template/error-for.html:
--------------------------------------------------------------------------------
1 |
2 | {{ field.$errorMessage }}
3 |
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/dashboard/factory/dashboard.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Dashboard HTTP 接口
3 | */
4 | define(function(require, exports, module) {
5 |
6 | module.exports = {
7 | 'Dashboard': ['$http', function($http) {
8 | var Dashboard = {
9 |
10 | };
11 |
12 | return Dashboard;
13 | }]
14 | };
15 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/dashboard/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 设置模块
3 | */
4 | define(function (require, exports, module) {
5 | var angular = require('angularjs');
6 | var angularHighcharts = require('angular-highcharts');
7 |
8 | angularHighcharts.Highcharts.setOptions({
9 | lang: {
10 | shortMonths: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
11 | weekdays: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
12 | }
13 | });
14 |
15 | var dashboard = angular.module('dashboard', [angularHighcharts.name]);
16 |
17 | dashboard.seajsController(require('./controller/index'));
18 |
19 | dashboard.factory(require('./factory/dashboard'));
20 |
21 |
22 | module.exports = dashboard;
23 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/dashboard/template/logs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 | [{{ log.at|date:'yyyy-MM-dd HH:mm' }}]
11 | 同步成功
12 | 同步失败
13 | {{ log.error }}
14 |
15 |
16 |
17 | 立刻同步
18 |
19 |
20 | 同步中...
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/editor/directive/selection.js:
--------------------------------------------------------------------------------
1 | /**
2 | * textarea 的光标位置
3 | * 使用了 selection 库,并将相应的对象输出方便操作
4 | *
5 | */
6 | define(function(require, exports, module) {
7 | var selection = require('selection');
8 |
9 | module.exports = {
10 | 'selection': ['$parse', function($parse) {
11 | return {
12 | restrict: 'CA',
13 | link: function(scope, element, attrs) {
14 | if (!attrs.selection || element[0].tagName.toLowerCase() !== 'textarea') {
15 | return;
16 | }
17 | var sel = selection(element[0]);
18 | var getter = $parse(attrs.selection);
19 |
20 | getter.assign(scope, sel);
21 | }
22 | };
23 | }]
24 | };
25 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/editor/factory/editor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Markdown 编辑器
3 | */
4 | define(function (require, exports, module) {
5 | var marked = require('marked');
6 | var _ = require('_');
7 |
8 | marked.setOptions({
9 | gfm: true,
10 | tables: true,
11 | breaks: true,
12 | pedantic: false,
13 | sanitize: true,
14 | smartLists: true,
15 | smartypants: false,
16 | langPrefix: 'lang-'
17 | });
18 |
19 | // 拖拽事件中是否有文件
20 | function hasFile(event) {
21 | return _.contains(event.originalEvent.dataTransfer.types, 'Files');
22 | }
23 |
24 | var Editor = ['Attach', '$modal', '$parse', function (Attach, $modal, $parse) {
25 | var preview = true;
26 |
27 | return {
28 | /**
29 | * 创建编辑、预览的逻辑
30 | * @param $scope
31 | * @param options {src: 'blog.content', dest: 'htmlContent'}
32 | */
33 | addPreviewFn: function ($scope, options) {
34 | var setter = $parse(options.dest).assign;
35 |
36 | // 监控变化生成预览
37 | $scope.$watch(options.src, function (value) {
38 | if (preview) {
39 | marked(value, function (err, data) {
40 | setter($scope, data);
41 | });
42 | }
43 | });
44 | },
45 |
46 | /**
47 | * 停止监控
48 | */
49 | stopPreview: function () {
50 | preview = false;
51 | },
52 |
53 | startPreview: function () {
54 | preview = true;
55 | },
56 |
57 | /**
58 | * 提供附件上传的功能,对于模板本身有一些要求
59 | * @param $scope
60 | */
61 | addAttachFn: function ($scope, parent) {
62 | // 上传
63 | $scope.doUpload = function (files) {
64 | _.each(files, function (file) {
65 | var attach = Attach.create({originalFile: file});
66 | parent.attaches.push(attach);
67 | });
68 | $scope.attachShow = true;
69 | };
70 |
71 | $scope.removeAttach = function (attach) {
72 | attach.$remove(function () {
73 | parent.attaches = _.without(parent.attaches, attach);
74 | if (parent.attaches.length === 0) {
75 | $scope.attachShow = false;
76 | }
77 | });
78 | };
79 |
80 | $scope.showTip = function () {
81 | $modal.open({
82 | templateUrl: 'editor/tip',
83 | controller: ['$scope', '$modalInstance', function ($scope, $modalInstance) {
84 | $scope.modal = $modalInstance;
85 | }]
86 | });
87 | };
88 | }
89 | };
90 | }];
91 |
92 | module.exports = {Editor: Editor};
93 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/editor/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Markdown 编辑器模块,用于其他模块
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | var editor = angular.module('editor', []);
8 |
9 | editor.run(require('./run/attach-extend'));
10 |
11 | editor.factory(require('./factory/editor'));
12 |
13 | editor.directive(require('./directive/selection'));
14 | editor.directive(require('./directive/file-drop'));
15 |
16 | editor.template({
17 | 'editor/tip': require('./template/tip.html')
18 | });
19 |
20 | module.exports = editor;
21 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/editor/run/attach-extend.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 给现有的 Attach 增加方法
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | module.exports = ['Attach', '$resource', function(Attach, $resource) {
8 |
9 | // attach 对应的 Markdown code
10 | Attach.prototype.getCode = function() {
11 | var url = 'http://' + location.host + this.url;
12 | var code;
13 | if (this.is_image) {
14 | code = '';
15 | }
16 | else {
17 | code = '[' + this.file_name + '](' + url + ')';
18 | }
19 | return code;
20 | };
21 | }];
22 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/editor/template/tip.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 | Header
12 |
13 | # 一级标题
14 | ## 二级标题
15 |
16 |
17 |
18 | 文本样式
19 |
20 | *斜体*
21 | **粗体**
22 |
23 |
24 |
25 | 多行代码
26 |
27 | ```ruby
28 | class Dog
29 | def bark
30 | end
31 | end
32 | ```
33 |
34 |
35 |
36 |
37 |
38 | 无序列表
39 |
40 | * Item 1
41 | * Item 2
42 | * Item 2a
43 | * Item 2b
44 |
45 |
46 |
47 | 有序列表
48 |
49 | 1. Item 1
50 | 2. Item 2
51 | 3. Item 3
52 | * Item 3a
53 | * Item 3b
54 |
55 |
56 |
57 | 单行代码
58 | `rvm install 1.9.2`
59 |
60 |
61 |
62 |
63 |
64 | 图片
65 |
66 | 
67 |
68 |
69 |
70 | 链接
71 |
72 | [链接文字](url)
73 |
74 |
75 |
76 | 引用
77 |
78 | 般若波罗蜜多心经有云
79 |
80 | > 观自在菩萨
81 | > 行深般若波罗蜜多时
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/page/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 页面模块
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 | var editor = require('../editor/index');
7 |
8 | var page = angular.module('page', [editor.name]);
9 |
10 | page.seajsController(require('./controller/index'));
11 |
12 | module.exports = page;
13 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/controller/attach.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 管理附件
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | var Controller = ['$scope', 'Attach', '$http', 'RelativeUrlFactory', 'Confirm', '$location', '$routeParams', '$route', function($scope, Attach, $http, RelativeUrlFactory, Confirm, $location, $routeParams, $route) {
8 | $scope.relativeUrl = RelativeUrlFactory.create(module);
9 | $scope.navClass = 'attach';
10 | var currPage;
11 |
12 | // 获取统计
13 | $scope.getStat = function() {
14 | $http.get('/admin/dashboard/attach').then(function(resp) {
15 | $scope.attachStat = resp.data;
16 | });
17 | };
18 |
19 | $scope.jumpPage = function(page) {
20 | $location.search('page', page);
21 | currPage = page;
22 |
23 | Attach.query({page: page}, function(data) {
24 | $scope.attaches = data;
25 | });
26 | };
27 |
28 | $scope.remove = function(attach) {
29 | Confirm.open('确定要删除“' + attach.file_name + '”?').then(function() {
30 | attach.$remove(function() {
31 | $scope.jumpPage(currPage);
32 | $scope.getStat();
33 | });
34 | });
35 | };
36 |
37 | $scope.$on('$routeUpdate', function() {
38 | if ($routeParams.page !== currPage) {
39 | $scope.jumpPage($routeParams.page);
40 | }
41 | });
42 |
43 | // 初始化
44 | $scope.jumpPage($routeParams.page || 1);
45 | $scope.getStat();
46 | }];
47 |
48 | Controller.title = '附件管理';
49 | Controller.nav = 'setting';
50 |
51 | module.exports = Controller;
52 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/controller/category.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 分类设置
3 | */
4 | define(function(require, exports, module) {
5 | var _ = require('_');
6 | var angular = require('angularjs');
7 |
8 | var Controller = ['$scope', 'Category', 'RelativeUrlFactory', 'Confirm', 'ErrorMessage', function($scope, Category, RelativeUrlFactory, Confirm, ErrorMessage) {
9 | $scope.relativeUrl = RelativeUrlFactory.create(module);
10 | $scope.navClass = 'category';
11 | $scope.categories = Category.query();
12 | $scope.newCategory = new Category();
13 |
14 | ErrorMessage.extend({
15 | category: {
16 | name: {
17 | required: '请填写分类名称',
18 | minlength: '分类名称至少需要2个字符'
19 | }
20 | }
21 | });
22 |
23 | $scope.add = function() {
24 | $scope.newCategory.$resolved = false;
25 |
26 | if ($scope.addForm.$valid) {
27 | $scope.newCategory.$save(function(data) {
28 | $scope.categories.push(data);
29 | $scope.newCategory = new Category();
30 | $scope.newCategory.$resolved = true;
31 | $scope.addForm.$setPristine();
32 | }, function(resp) {
33 | $scope.addServerError = resp.data.errors;
34 | });
35 | }
36 | };
37 |
38 | // 修改
39 | $scope.edit = function(category) {
40 | $scope.cancelEdit($scope.editingCategory);
41 | $scope.editingCategory = category;
42 | $scope.originalCategory = angular.copy(category); // backup
43 | };
44 |
45 | $scope.cancelEdit = function(category) {
46 | angular.copy($scope.originalCategory, category);
47 | $scope.editingCategory = null;
48 | };
49 |
50 | $scope.clearEditError = function() {
51 | $scope.editServerError = null;
52 | };
53 |
54 | // ngif 创建了新的 scope,所以这里需要用参数传递 FormController
55 | $scope.update = function(category, form) {
56 | if (form.$valid) {
57 | category.$update(function() {
58 | $scope.editingCategory = null;
59 | }, function(resp) {
60 | $scope.editServerError = resp.data.errors;
61 | });
62 | }
63 | };
64 |
65 | // 删除
66 | $scope.remove = function(category) {
67 | Confirm.open('确定要删除“' + category.name + '”?').then(function() {
68 | category.$remove(function() {
69 | $scope.categories = _.without($scope.categories, category);
70 | });
71 | });
72 | };
73 | }];
74 |
75 | Controller.title = '分类设置';
76 | Controller.nav = 'setting';
77 |
78 | module.exports = Controller;
79 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/controller/disqus.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 修改 Disqus 设置
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | var Controller = ['$scope', 'Disqus', 'RelativeUrlFactory', 'ErrorMessage', '$timeout', function($scope, Disqus, RelativeUrlFactory, ErrorMessage, $timeout) {
8 | $scope.relativeUrl = RelativeUrlFactory.create(module);
9 | $scope.navClass = 'disqus';
10 | $scope.disqus = Disqus.get();
11 |
12 | ErrorMessage.extend({
13 | disqus: {
14 | shortname: {
15 | required: '请填写 Shortname'
16 | },
17 | api_secret: {
18 | required: '请填写 API Secret'
19 | },
20 | access_token: {
21 | required: '请填写 Access Token'
22 | }
23 | }
24 | });
25 |
26 | $scope.enableDisqus = function(bool) {
27 | $scope.disqus.enable = bool;
28 | $scope.disqus.$updateEnable();
29 | };
30 |
31 | $scope.save = function() {
32 | $scope.disqus.$resolved = false;
33 | if ($scope.form.$valid) {
34 | $scope.disqus.$update(function() {
35 | $scope.saveSuccess = true;
36 | $timeout(function() {
37 | $scope.saveSuccess = false;
38 | }, 3000);
39 | }, function(resp) {
40 | $scope.serverError = resp.data.errors;
41 | });
42 | }
43 | };
44 | }];
45 |
46 | Controller.title = 'Disqus 设置';
47 | Controller.nav = 'setting';
48 |
49 | module.exports = Controller;
50 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/controller/ga.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 修改 Disqus 设置
3 | */
4 | define(function (require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | var Controller = ['$scope', 'GA', 'Attach', 'RelativeUrlFactory', 'ErrorMessage', '$timeout', function ($scope, GA, Attach, RelativeUrlFactory, ErrorMessage, $timeout) {
8 | $scope.relativeUrl = RelativeUrlFactory.create(module);
9 | $scope.navClass = 'ga';
10 | $scope.ga = GA.get();
11 |
12 | ErrorMessage.extend({
13 | ga: {
14 | secret_file: {
15 | required: '请上传秘钥文件'
16 | },
17 | api_email: {
18 | required: '请填写 API Email',
19 | email: '请填写正确的 Email 地址'
20 | }
21 | }
22 | });
23 |
24 | // 无法禁用 email 的校验,this is workaround
25 | $scope.$watch('ga.chart_enable', function (value) {
26 | if (!value && $scope.form.api_email.$error.email) {
27 | $scope.form.api_email.$setViewValue('');
28 | }
29 | });
30 |
31 | $scope.upload = function (files) {
32 | $scope.ga.secret_file = Attach.create({
33 | originalFile: files[0]
34 | });
35 | };
36 |
37 | $scope.save = function () {
38 | $scope.ga.$resolved = false;
39 | if ($scope.form.$valid) {
40 | $scope.ga.$update(function () {
41 | $scope.saveSuccess = true;
42 | $timeout(function () {
43 | $scope.saveSuccess = false;
44 | }, 3000);
45 | }, function (resp) {
46 | $scope.serverError = resp.data.errors;
47 | });
48 | }
49 | };
50 | }];
51 |
52 | Controller.title = 'GA 设置';
53 | Controller.nav = 'setting';
54 |
55 | module.exports = Controller;
56 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/controller/password.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 密码设置
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | var Controller = ['$scope', 'Password', 'RelativeUrlFactory', 'ErrorMessage', function($scope, Password, RelativeUrlFactory, ErrorMessage) {
8 | $scope.relativeUrl = RelativeUrlFactory.create(module);
9 | $scope.navClass = 'password';
10 | $scope.password = new Password();
11 |
12 | ErrorMessage.extend({
13 | password: {
14 | old_pw: {
15 | required: '旧密码不能为空'
16 | },
17 | new_pw: {
18 | required: '新密码不能为空',
19 | minlength: '新密码至少需要6位'
20 | },
21 | new_pw_confirmation: {
22 | equalTo: '新密码两次输入不一致'
23 | }
24 | }
25 | });
26 |
27 | $scope.save = function() {
28 | $scope.saveSuccess = false;
29 | $scope.serverError = null;
30 |
31 | if ($scope.form.$valid) {
32 | $scope.password.$save(function() {
33 | $scope.password = new Password();
34 | $scope.form.$setPristine();
35 | $scope.saveSuccess = true;
36 | }, function(resp) {
37 | $scope.serverError = resp.data.errors;
38 | });
39 | }
40 | };
41 | }];
42 |
43 | Controller.title = '密码设置';
44 | Controller.nav = 'setting';
45 |
46 | module.exports = Controller;
47 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/controller/website.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 修改网站基本设置
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | var Controller = ['$scope', 'Website', 'Attach', 'RelativeUrlFactory', 'ErrorMessage', '$timeout', function($scope, Website, Attach, RelativeUrlFactory, ErrorMessage, $timeout) {
8 | $scope.relativeUrl = RelativeUrlFactory.create(module);
9 | $scope.navClass = 'website';
10 | $scope.website = Website.get();
11 |
12 | ErrorMessage.extend({
13 | website: {
14 | title: {
15 | required: '请填写网站名称'
16 | },
17 | author: {
18 | required: '请填写作者姓名'
19 | },
20 | weibo: {
21 | url: '请填写正确的 URL'
22 | },
23 | donate: {
24 | url: '请填写正确的 URL'
25 | }
26 | }
27 | });
28 |
29 | $scope.uploadAvatar = function(files) {
30 | $scope.avatarAttach = Attach.create({
31 | originalFile: files[0],
32 | max_width: 200
33 | }, function() {
34 | $scope.website.avatar = $scope.avatarAttach.url;
35 | $scope.website.avatar_id = $scope.avatarAttach.id;
36 | });
37 | };
38 |
39 |
40 | $scope.save = function() {
41 | $scope.website.$resolved = false;
42 | if ($scope.form.$valid) {
43 | $scope.website.$update(function() {
44 | $scope.saveSuccess = true;
45 | $timeout(function() {
46 | $scope.saveSuccess = false;
47 | }, 3000);
48 | });
49 | }
50 | };
51 | }];
52 |
53 | Controller.title = '基本信息';
54 | Controller.nav = 'setting';
55 |
56 | module.exports = Controller;
57 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/directive/ng-equal-to.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 校验表单域的值是否等于某个值
3 | */
4 | define(function (require, exports, module) {
5 | module.exports = {
6 | 'ngEqualTo': [function () {
7 | return {
8 | restrict: 'AC',
9 | require: 'ngModel',
10 | link: function (scope, element, attrs, ngModel) {
11 | scope.$watch(function () {
12 | return scope.$eval(attrs.ngEqualTo);
13 | }, function (value) {
14 | ngModel.$setValidity('equalTo', ngModel.$modelValue === value);
15 | });
16 |
17 | scope.$watch(function () {
18 | return ngModel.$modelValue;
19 | }, function (value) {
20 | ngModel.$setValidity('equalTo', scope.$eval(attrs.ngEqualTo) === value);
21 | });
22 | }
23 | };
24 | }]
25 | };
26 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/factory/disqus.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Disqus 设置
3 | */
4 | define(function (require, exports, module) {
5 |
6 | module.exports = {
7 | 'Disqus': ['$resource', function ($resource) {
8 | var URL = '/admin/disqus';
9 | var ENABLE_URL = '/admin/disqus/enable';
10 |
11 | var Disqus = $resource(URL, null, {
12 | update: {
13 | method: 'PUT'
14 | },
15 |
16 | updateEnable: {
17 | method: 'PUT',
18 | url: ENABLE_URL
19 | }
20 | });
21 |
22 | return Disqus;
23 | }]
24 | };
25 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/factory/ga.js:
--------------------------------------------------------------------------------
1 | /**
2 | * GA 设置
3 | */
4 | define(function (require, exports, module) {
5 |
6 | module.exports = {
7 | 'GA': ['$resource', function ($resource) {
8 | var URL = '/admin/ga';
9 |
10 | var GA = $resource(URL, null, {
11 | update: {
12 | method: 'PUT'
13 | }
14 | });
15 |
16 | return GA;
17 | }]
18 | };
19 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/factory/password.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Disqus 设置
3 | */
4 | define(function(require, exports, module) {
5 |
6 | module.exports = {
7 | 'Password': ['$resource', function($resource) {
8 |
9 | var Password = $resource('/admin/password', null, {
10 | save: {
11 | method: 'PUT'
12 | }
13 | });
14 |
15 | return Password;
16 | }]
17 | };
18 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/factory/website.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 网站设置
3 | */
4 | define(function (require, exports, module) {
5 |
6 | module.exports = {
7 | 'Website': ['$resource', function ($resource) {
8 | var Website = $resource('/admin/website', null, {
9 | update: {
10 | method: 'PUT'
11 | }
12 | });
13 |
14 | return Website;
15 | }]
16 | };
17 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 设置模块
3 | */
4 | define(function(require, exports, module) {
5 | var angular = require('angularjs');
6 |
7 | var setting = angular.module('setting', []);
8 |
9 | setting.seajsController(require('./controller/disqus'));
10 | setting.seajsController(require('./controller/category'));
11 | setting.seajsController(require('./controller/password'));
12 | setting.seajsController(require('./controller/website'));
13 | setting.seajsController(require('./controller/attach'));
14 | setting.seajsController(require('./controller/ga'));
15 |
16 | setting.factory(require('./factory/website'));
17 | setting.factory(require('./factory/password'));
18 | setting.factory(require('./factory/disqus'));
19 | setting.factory(require('./factory/ga'));
20 |
21 | setting.directive(require('./directive/ng-equal-to'));
22 |
23 | module.exports = setting;
24 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/template/attach.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 | 共有 {{ attachStat.count }} 个附件,合计 {{ attachStat.size }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 文件名
18 | 大小
19 | 创建时间
20 | 归属
21 |
22 |
23 |
24 |
25 | {{ attach.file_name }}
26 | 删除
27 |
28 | {{ attach.file_size }}
29 | {{ attach.created_at |date:'yyyy-MM-dd HH:mm' }}
30 |
31 | {{attach.parent_type}}
32 | {{ attach.parent.title }}
33 |
34 |
35 |
36 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/template/nav.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/admin/setting/template/password.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require turbolinks
--------------------------------------------------------------------------------
/app/assets/javascripts/html5shiv.js:
--------------------------------------------------------------------------------
1 | (function(g,b){function k(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function l(a){var c={},f=a.createElement,b=a.createDocumentFragment,d=b();a.createElement=function(a){if(!e.shivMethods)return f(a);var b;b=c[a]?c[a].cloneNode():m.test(a)?(c[a]=f(a)).cloneNode():f(a);return b.canHaveChildren&&!n.test(a)?d.appendChild(b):b};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+k().join().replace(/\w+/g,function(a){f(a);
2 | d.createElement(a);return'c("'+a+'")'})+");return n}")(e,d)}function h(a){var c;if(a.documentShived)return a;if(e.shivCSS&&!i){c=a.createElement("p");var b=a.getElementsByTagName("head")[0]||a.documentElement;c.innerHTML="x";c=!!b.insertBefore(c.lastChild,b.firstChild)}j||(c=!l(a));if(c)a.documentShived=c;return a}var d=g.html5||{},n=/^<|^(?:button|form|map|select|textarea|object|iframe|option|optgroup)$/i,
3 | m=/^<|^(?:a|b|button|code|div|fieldset|form|h1|h2|h3|h4|h5|h6|i|iframe|img|input|label|li|link|ol|option|p|param|q|script|select|span|strong|style|table|tbody|td|textarea|tfoot|th|thead|tr|ul)$/i,i,j;(function(){var a=b.createElement("a");a.innerHTML=" ";i="hidden"in a;if(!(a=1==a.childNodes.length))a:{try{b.createElement("a")}catch(c){a=!0;break a}a=b.createDocumentFragment();a="undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}j=
4 | a})();var e={elements:d.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:!1!==d.shivCSS,shivMethods:!1!==d.shivMethods,type:"default",shivDocument:h};g.html5=e;h(b)})(this,document);
5 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/angular-highcharts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "angular",
3 | "name": "angular-highcharts",
4 | "version": "3.0.7",
5 | "spm": {
6 | "output": ["angular-highcharts.js", "highcharts.js"],
7 | "alias": {
8 | "angularjs": "angularjs"
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/angular-highcharts/src/angular-highcharts.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 对 Highcharts 的封装
3 | *
4 | */
5 | define(function(require, exports, module) {
6 | var angular = require('angularjs');
7 | var Highcharts = require('./highcharts');
8 |
9 | module.exports = angular.module('angular-highcharts', []).directive('highChart', function() {
10 | return {
11 | restrict: 'EA',
12 | template: '
',
13 | scope: {
14 | options: "="
15 | },
16 | transclude: true,
17 | replace: true,
18 |
19 | link: function(scope, element, attrs) {
20 | var chartsDefaults = {
21 | chart: {
22 | renderTo: element[0],
23 | type: attrs.type || null,
24 | height: attrs.height || null,
25 | width: attrs.width || null
26 | }
27 | };
28 |
29 | //Update when charts data changes
30 | scope.$watch('options', function(value) {
31 | if (!value) {
32 | return;
33 | }
34 | var deepCopy = true;
35 | var newSettings = {};
36 | angular.extend(newSettings, chartsDefaults, value);
37 | var chart = new Highcharts.Chart(newSettings);
38 | });
39 | }
40 | };
41 | });
42 |
43 | module.exports.Highcharts = Highcharts;
44 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/angularjs-all/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "angular",
3 | "name": "angularjs-all",
4 | "version": "1.2.7",
5 | "spm": {
6 | "output": ["index.js"],
7 | "include": "all",
8 | "alias": {
9 | "angularjs": "angular/angularjs/1.2.7/angular",
10 | "angular-animate": "angular/angularjs/1.2.7/angular-animate",
11 | "angular-resource": "angular/angularjs/1.2.7/angular-resource",
12 | "angular-route": "angular/angularjs/1.2.7/angular-route",
13 | "angular-sanitize": "angular/angularjs/1.2.7/angular-sanitize"
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/angularjs-all/src/index.js:
--------------------------------------------------------------------------------
1 | define(function(require) {
2 | var angular = require('angularjs');
3 | var ngAnimate = require('angular-animate');
4 | var ngResource = require('angular-resource');
5 | var ngRoute = require('angular-route');
6 | var ngSanitize = require('angular-sanitize');
7 |
8 | var angularjsAll = angular.module('ngAll', [
9 | ngAnimate.name,
10 | ngRoute.name,
11 | ngResource.name,
12 | ngSanitize.name
13 | ]);
14 |
15 | return angular;
16 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/angularjs/1.2.7/angular-cookies.js:
--------------------------------------------------------------------------------
1 | /*
2 | AngularJS v1.2.7
3 | (c) 2010-2014 Google, Inc. http://angularjs.org
4 | License: MIT
5 | */
6 | define("angular/angularjs/1.2.7/angular-cookies",["angular/angularjs/1.2.7/angular"],function(require){ var angular = require("angular/angularjs/1.2.7/angular");(function(p,f,n){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(d,b){var c={},g={},h,k=!1,l=f.copy,m=f.isUndefined;b.addPollFn(function(){var a=b.cookies();h!=a&&(h=a,l(a,g),l(a,c),k&&d.$apply())})();k=!0;d.$watch(function(){var a,e,d;for(a in g)m(c[a])&&b.cookies(a,n);for(a in c)(e=c[a],f.isString(e))?e!==g[a]&&(b.cookies(a,e),d=!0):f.isDefined(g[a])?c[a]=g[a]:delete c[a];if(d)for(a in e=b.cookies(),c)c[a]!==e[a]&&(m(e[a])?delete c[a]:c[a]=e[a])});
7 | return c}]).factory("$cookieStore",["$cookies",function(d){return{get:function(b){return(b=d[b])?f.fromJson(b):b},put:function(b,c){d[b]=f.toJson(c)},remove:function(b){delete d[b]}}}])})(window,angular);return angular.module("ngCookies");})
8 | //# sourceMappingURL=angular-cookies.min.js.map
9 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/angularjs/1.2.7/angular-touch.js:
--------------------------------------------------------------------------------
1 | /*
2 | AngularJS v1.2.7
3 | (c) 2010-2014 Google, Inc. http://angularjs.org
4 | License: MIT
5 | */
6 | define("angular/angularjs/1.2.7/angular-touch",["angular/angularjs/1.2.7/angular"],function(require){ var angular = require("angular/angularjs/1.2.7/angular");(function(y,v,z){'use strict';function t(g,a,b){q.directive(g,["$parse","$swipe",function(l,n){var r=75,h=0.3,d=30;return function(p,m,k){function e(e){if(!u)return!1;var c=Math.abs(e.y-u.y);e=(e.x-u.x)*a;return f&&cd&&c/el&&10>n||
8 | (n>l?(d=!1,b.cancel&&b.cancel(a)):(a.preventDefault(),b.move&&b.move(m,a)))}});a.on("touchend mouseup",function(a){d&&(d=!1,b.end&&b.end(g(a),a))})}}}]);q.config(["$provide",function(g){g.decorator("ngClickDirective",["$delegate",function(a){a.shift();return a}])}]);q.directive("ngClick",["$parse","$timeout","$rootElement",function(g,a,b){function l(a,c,b){for(var f=0;fh)){var c=
9 | a.touches&&a.touches.length?a.touches:[a],b=c[0].clientX,c=c[0].clientY;1>b&&1>c||l(k,b,c)||(a.stopPropagation(),a.preventDefault(),a.target&&a.target.blur())}}function r(b){b=b.touches&&b.touches.length?b.touches:[b];var c=b[0].clientX,d=b[0].clientY;k.push(c,d);a(function(){for(var a=0;ah&&12>p)&&(k||(b[0].addEventListener("click",n,!0),b[0].addEventListener("touchstart",r,!0),k=[]),m=Date.now(),l(k,e,g),s&&s.blur(),v.isDefined(d.disabled)&&!1!==d.disabled||c.triggerHandler("click",[a]));f()});c.onclick=function(a){};c.on("click",function(b,c){a.$apply(function(){h(a,{$event:c||b})})});c.on("mousedown",function(a){c.addClass(p)});c.on("mousemove mouseup",function(a){c.removeClass(p)})}}]);t("ngSwipeLeft",-1,"swipeleft");t("ngSwipeRight",1,"swiperight")})(window,angular);return angular.module("ngTouch");})
12 | //# sourceMappingURL=angular-touch.min.js.map
13 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/bootstrap/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "angular",
3 | "name": "bootstrap",
4 | "version": "0.0.1",
5 | "spm": {
6 | "output": ["index.js"],
7 | "alias": {
8 | "angularjs": "angularjs",
9 | "_": "_"
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/bootstrap/src/dropdown-toggle.js:
--------------------------------------------------------------------------------
1 | /*
2 | * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js
3 | * @restrict class or attribute
4 | * @example:
5 |
6 | My Dropdown Menu
7 |
12 |
13 | */
14 | define(function(require, exports, module) {
15 | var angular = require('angularjs');
16 | var _ = require('_');
17 |
18 | angular.module('bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function($document, $location) {
19 | var openElement = null,
20 | closeMenu = angular.noop;
21 | return {
22 | restrict: 'CA',
23 | link: function(scope, element, attrs) {
24 | scope.$watch('$location.path', function() {
25 | closeMenu();
26 | });
27 | element.parent().bind('click', function() {
28 | closeMenu();
29 | });
30 | element.bind('click', function(event) {
31 |
32 | var elementWasOpen = (element === openElement);
33 |
34 | event.preventDefault();
35 | event.stopPropagation();
36 |
37 | if (!!openElement) {
38 | closeMenu();
39 | }
40 |
41 | if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
42 | element.parent().addClass('open');
43 | openElement = element;
44 | closeMenu = function(event) {
45 | if (event) {
46 | event.preventDefault();
47 | event.stopPropagation();
48 | }
49 | $document.unbind('click', closeMenu);
50 | element.parent().removeClass('open');
51 | closeMenu = angular.noop;
52 | openElement = null;
53 | };
54 | $document.bind('click', closeMenu);
55 | }
56 | });
57 | }
58 | };
59 | }]);
60 |
61 | module.exports = angular.module('bootstrap.dropdownToggle');
62 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/bootstrap/src/index.js:
--------------------------------------------------------------------------------
1 | define(function(require, exports, module) {
2 | var modal = require('./modal');
3 | var dropdownToggle = require('./dropdown-toggle');
4 | var pagination = require('./pagination');
5 |
6 | module.exports = angular.module('bootstrap', [modal.name, dropdownToggle.name, pagination.name]);
7 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/bootstrap/src/pagination.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 分页组件
3 | *
4 | */
5 | define(function(require, exports, module) {
6 | var angular = require('angularjs');
7 | var _ = require('_');
8 |
9 | angular.module('bootstrap.pagination', [])
10 | .directive({
11 | 'pagination': function() {
12 | var WINDOW_SIZE = 2;
13 | var OMIT_STR = '...';
14 |
15 | return {
16 | restrict: 'EA',
17 | replace: true,
18 | transclude: true,
19 | scope: {
20 | page: '=',
21 | pagerClick: '&'
22 | },
23 | templateUrl: 'template/pagination/pagination.html',
24 |
25 | link: function(scope, element, attrs) {
26 | scope.omit = OMIT_STR;
27 |
28 | scope.$watchCollection('page', function(page) {
29 | scope.isShow = page && page.total && page.total > 1;
30 | if (scope.isShow) {
31 | scope.currPage = page.current;
32 | scope.totalPage = page.total;
33 |
34 | // 不需要显示所有页数,只需要在当前页附近开一个“窗口”
35 | scope.pages = [1];
36 | if (scope.currPage > 1 + WINDOW_SIZE + 1) {
37 | scope.pages.push(OMIT_STR);
38 | }
39 |
40 | var leftPage = Math.max(2, scope.currPage - WINDOW_SIZE);
41 | var rightPage = Math.min(scope.totalPage - 1, scope.currPage + WINDOW_SIZE);
42 | scope.pages = scope.pages.concat(_.range(leftPage, rightPage + 1));
43 |
44 | if (scope.currPage < scope.totalPage - WINDOW_SIZE - 1) {
45 | scope.pages.push(OMIT_STR);
46 | }
47 | scope.pages.push(scope.totalPage);
48 | }
49 | });
50 |
51 | // 跳转
52 | scope.jump = function(page) {
53 | if (page > scope.totalPage || page < 1 || page === OMIT_STR) {
54 | return;
55 | }
56 | scope.pagerClick({page: page});
57 | };
58 | }
59 | };
60 | }
61 | });
62 |
63 | var m = angular.module('bootstrap.pagination');
64 |
65 | m.run(['$templateCache', function($templateCache) {
66 | $templateCache.put('template/pagination/pagination.html', require('./template/pagination/pagination.html'));
67 | }]);
68 |
69 | module.exports = m;
70 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/bootstrap/src/template/modal/backdrop.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/bootstrap/src/template/modal/window.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/bootstrap/src/template/pagination/pagination.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/seajs-lazy-angular/0.0.1/seajs-lazy-angular.js:
--------------------------------------------------------------------------------
1 | define("angular/seajs-lazy-angular/0.0.1/seajs-lazy-angular",["angularjs"],function(a){function b(a,b){this.url=a,this.resolveCallback=b||e.noop}function c(a,b){var c={name:a,requires:b,realModule:null,__runBlocks:[],__controllers:{},factory:function(){return f.$provide.factory.apply(null,arguments),c},directive:function(){return f.$compileProvider.directive.apply(null,arguments),c},filter:function(){return f.$filterProvider.register.apply(null,arguments),c},controller:function(a,b){return e.isObject(a)?(f.$controllerProvider.register.apply(null,arguments),e.extend(this.__controllers,a)):(f.$controllerProvider.register.apply(null,arguments),this.__controllers[a]=b),c},provider:function(){return f.$provide.provider.apply(null,arguments),c},service:function(){return f.$provide.service.apply(null,arguments),c},constant:function(){return f.$provide.constant.apply(null,arguments),c},run:function(a){return this.__runBlocks.push(a),c},seajsController:function(a){this.controller(a.__moduleUri,a)},retrieveController:function(a){return this.__controllers.hasOwnProperty(a)?this.__controllers[a]:null},template:function(a){this.run(["$templateCache",function(b){e.forEach(a,function(a,c){b.put(c,a)})}])},resolveRun:function(a){e.forEach(this.__runBlocks,function(b){a.invoke(b)}),e.forEach(this.requires,function(b){e.module(b).resolveRun(a)}),this.__runBlocks.length=0}};return c}function d(a){var b=a.split("/"),c=b[b.length-1].replace(/\.js$/,""),d="../template/"+c+".html";return seajs.resolve(d+"#",a)}var e=a("angularjs"),f={},g={},h=e.module;seajs.on("exec",function(a){a.exports&&(a.exports.__moduleUri=a.uri)});var i={cacheInternals:["$provide","$compileProvider","$filterProvider","$controllerProvider","$templateCacheProvider",function(a,b,c,e,g){f.$provide=a,f.$compileProvider=b,f.$filterProvider=c,f.$controllerProvider=e,f.$provide.factory({RelativeUrlFactory:function(){return{create:function(a){return function(b){var c=d(a.uri);return seajs.resolve(b+"#",c)}}}}}),g.$get=["$cacheFactory",function(a){var b=a("templates"),c=b.get;return b.get=function(a){var b=seajs.cache[a];return b?b.exec():c(a)},b}]}],patchAngular:function(){e.module=function(a,b){var d;return"undefined"==typeof b?d=g.hasOwnProperty(a)?g[a]:h.call(e,a):(d=c(a,b),g[a]=d,d.realModule=h.call(e,a,b)),d}},setResolveCallback:function(a){this.resolveCallback=a},createLazyStub:function(a,c){return new b(a,c||this.resolveCallback)}};return b.prototype.createRoute=function(b,c){var e=this.url,f=this.resolveCallback,g=seajs.resolve(b,seajs.resolve(this.url));return c=c||{},c.controller=g,c.resolve={module:["$q","$route","$templateCache","$exceptionHandler","$injector",function(b,c,h,i,j){var k=b.defer(),l=b.defer();return c.current.template||c.current.templateUrl||(c.current.template=function(){return l.promise}),a.async(e,function(b){b.resolveRun(j);var c=b.retrieveController(g);if(c.template){var e=c.template;l.resolve(e)}else{var h;h=c.templateUrl?seajs.resolve(c.templateUrl+"#",g):d(g),a.async(h+"#",function(a){l.resolve(a)})}j.invoke(f,null,{controller:c}),k.resolve(b)}),k.promise}]},c},i});
2 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/angular/seajs-lazy-angular/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "angular",
3 | "name": "seajs-lazy-angular",
4 | "version": "0.0.1",
5 | "spm": {
6 | "output": ["seajs-lazy-angular.js"],
7 | "alias": {
8 | "angularjs": "angularjs"
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/gallery/selection/0.9.0/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "gallery",
3 | "name": "selection",
4 | "version": "0.9.0",
5 | "description": "selection manipulating for textarea.",
6 | "homepage": "http://lab.lepture.com/selection.js/",
7 | "author": "Hsiaoming Yang ",
8 | "keywords": [
9 | "selection",
10 | "textarea",
11 | "editor"
12 | ],
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/lepture/selection.js.git"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/lepture/selection.js/issues"
19 | },
20 | "licenses": [{
21 | "type": "BSD"
22 | }],
23 | "spm": {
24 | "output": ["selection.js"]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/gallery/selection/0.9.0/selection.js:
--------------------------------------------------------------------------------
1 | define("gallery/selection/0.9.0/selection",[],function(e,t,r){function n(e,t){return this.element=e,this.cursor=function(e,r){var n=this.element;if(e===void 0)return t?i(n):[n.selectionStart,n.selectionEnd];if(v(e)){var o=e;e=o[0],r=o[1]}return r===void 0&&(r=e),t?a(n,e,r):n.setSelectionRange(e,r),this},this}function o(e){if(e)this.text=function(){return document.selection.createRange().text};else{var t=window.getSelection();this.element=u(t),this.text=function(){return""+t}}return this}function i(e){var t=document.selection.createRange();if(t&&t.parentElement()===e){var r,n,o=e.value.replace(/\r\n/g,"\n"),i=o.length,a=e.createTextRange();a.moveToBookmark(t.getBookmark());var c=e.createTextRange();return c.collapse(!1),a.compareEndPoints("StartToEnd",c)>-1?r=n=i:(r=-a.moveStart("character",-i),n=-a.moveEnd("character",-i)),r>n&&(n=i),[r,n]}return[0,0]}function a(e,t,r){var n=e.createTextRange();n.move("character",t),n.moveEnd("character",r-t),n.select()}function c(e,t,r,n,o){t===void 0&&(t="");var i=e.element.value;return e.element.value=[i.slice(0,r),t,i.slice(n)].join(""),n=r+t.length,"left"===o?e.cursor(r):"right"===o?e.cursor(n):e.cursor(r,n),e}function u(e){for(var t=null,r=e.anchorNode,n=e.focusNode;!t;){if(r.parentElement===n.parentElement){t=n.parentElement;break}r=r.parentElement,n=n.parentElement}return t}var s=function(e){if(e&&e.length&&(e=e[0]),e){if(e.selectionStart!==void 0)return new n(e);var t=e.tagName.toLowerCase()}if(t&&("textarea"===t||"input"===t))return new n(e,!0);if(window.getSelection)return new o;if(document.selection)return new o(!0);throw Error("your browser is very weird")};s.version="<%= pkg.version %>",r.exports=s,n.prototype.text=function(e,t){var r=this.element,n=this.cursor();return e===void 0?r.value.slice(n[0],n[1]):c(this,e,n[0],n[1],t)},n.prototype.append=function(e,t){var r=this.cursor()[1];return c(this,e,r,r,t)},n.prototype.prepend=function(e,t){var r=this.cursor()[0];return c(this,e,r,r,t)},n.prototype.surround=function(e){e===void 0&&(e=1);var t=this.element.value,r=this.cursor(),n=t.slice(Math.max(0,r[0]-e),r[0]),o=t.slice(r[1],r[1]+e);return[n,o]},n.prototype.line=function(){var e=this.element.value,t=this.cursor(),r=e.slice(0,t[0]).lastIndexOf("\n"),n=e.slice(t[1]).indexOf("\n"),o=r+1;if(-1===n)return e.slice(o);var i=t[1]+n;return e.slice(o,i)};var l=Object.prototype.toString,v=Array.isArray;v||(v=function(e){return"[object Array]"===l.call(e)})});
2 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/gallery/underscore/1.4.4/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "underscore",
3 | "family": "gallery",
4 | "version": "1.4.4",
5 | "package": "https://raw.github.com/documentcloud/underscore/master/package.json",
6 | "description": "JavaScript's functional programming helper library.",
7 | "homepage": "http://underscorejs.org",
8 | "keywords": ["util", "functional", "server", "client", "browser"],
9 | "author": "Jeremy Ashkenas ",
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/documentcloud/underscore.git"
13 | },
14 | "spm": {
15 | "output": ["underscore.js"]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/seajs/seajs-text/1.0.2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "seajs",
3 | "name": "seajs-text",
4 | "version": "1.0.2",
5 | "description": "A Sea.js plugin for loading text resources such as template, json etc",
6 | "keywords": ["seajs", "plugin", "text"],
7 | "author": "Frank Wang ",
8 | "repository": {
9 | "type": "git",
10 | "url": "git://github.com/seajs/seajs-text.git"
11 | },
12 | "devDependencies": {
13 | "seatools": "*"
14 | },
15 | "config": {
16 | "plugin-concat": [
17 | "seajs-text.js"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/seajs/seajs-text/1.0.2/seajs-text.js:
--------------------------------------------------------------------------------
1 | !function(){function a(a){h[a.name]=a}function b(a){return a&&h.hasOwnProperty(a)}function c(a){for(var c in h)if(b(c)){var d=","+h[c].ext.join(",")+",";if(d.indexOf(","+a+",")>-1)return c}}function d(a,b){var c=g.ActiveXObject?new g.ActiveXObject("Microsoft.XMLHTTP"):new g.XMLHttpRequest;return c.open("GET",a,!0),c.onreadystatechange=function(){if(4===c.readyState){if(c.status>399&&c.status<600)throw new Error("Could not load: "+a+", status = "+c.status);b(c.responseText)}},c.send(null)}function e(a){a&&/\S/.test(a)&&(g.execScript||function(a){(g.eval||eval).call(g,a)})(a)}function f(a){return a.replace(/(["\\])/g,"\\$1").replace(/[\f]/g,"\\f").replace(/[\b]/g,"\\b").replace(/[\n]/g,"\\n").replace(/[\t]/g,"\\t").replace(/[\r]/g,"\\r").replace(/[\u2028]/g,"\\u2028").replace(/[\u2029]/g,"\\u2029")}var g=window,h={},i={};a({name:"text",ext:[".tpl",".html"],exec:function(a,b){e('define("'+a+'#", [], "'+f(b)+'")')}}),a({name:"json",ext:[".json"],exec:function(a,b){e('define("'+a+'#", [], '+b+")")}}),a({name:"handlebars",ext:[".handlebars"],exec:function(a,b){var c=['define("'+a+'#", ["handlebars"], function(require, exports, module) {',' var source = "'+f(b)+'"',' var Handlebars = require("handlebars")'," module.exports = function(data, options) {"," options || (options = {})"," options.helpers || (options.helpers = {})"," for (var key in Handlebars.helpers) {"," options.helpers[key] = options.helpers[key] || Handlebars.helpers[key]"," }"," return Handlebars.compile(source)(data, options)"," }","})"].join("\n");e(c)}}),seajs.on("resolve",function(a){var d=a.id;if(!d)return"";var e,f;(f=d.match(/^(\w+)!(.+)$/))&&b(f[1])?(e=f[1],d=f[2]):(f=d.match(/[^?]+(\.\w+)(?:\?|#|$)/))&&(e=c(f[1])),e&&-1===d.indexOf("#")&&(d+="#");var g=seajs.resolve(d,a.refUri);e&&(i[g]=e),a.uri=g}),seajs.on("request",function(a){var b=i[a.uri];b&&(d(a.requestUri,function(c){h[b].exec(a.uri,c),a.onRequest()}),a.requested=!0)}),define("seajs/seajs-text/1.0.2/seajs-text",[],{})}();
--------------------------------------------------------------------------------
/app/assets/javascripts/sea-modules/seajs/seajs/2.1.1/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "family": "seajs",
3 | "name": "seajs",
4 | "version": "2.1.1",
5 | "description": "A Module Loader for the Web",
6 | "homepage": "http://seajs.org/",
7 | "keywords": ["module", "loader"],
8 | "author": "Frank Wang ",
9 | "engines": {
10 | "node": ">= 0.6.0"
11 | },
12 | "dependencies": {},
13 | "devDependencies": {
14 | "grunt": "~0.4.0",
15 | "node-static": "~0.6.9",
16 | "grunt-contrib-concat": "~0.2.0",
17 | "gcc": "~0.2.0"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git://github.com/seajs/seajs.git"
22 | },
23 | "main": "./lib/sea.js",
24 | "licenses": [
25 | {
26 | "type": "MIT",
27 | "url": "http://seajs.org/LICENSE.md"
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/app/assets/javascripts/xhr-shim.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 由于 angular 不支持从 $http 中获取 xhr 对象,导致上传进度事件无法绑定,只能在这里做一个楔子
3 | */
4 | if (window.XMLHttpRequest && window.FormData) {
5 |
6 | window.XMLHttpRequest = (function(origXHR) {
7 | return function() {
8 | var xhr = new origXHR();
9 | xhr.send = (function(orig) {
10 | return function() {
11 | if (arguments[0] instanceof FormData && arguments[0].setXHR) {
12 | var formData = arguments[0];
13 | formData.setXHR(xhr);
14 | }
15 | orig.apply(xhr, arguments);
16 | };
17 | })(xhr.send);
18 | return xhr;
19 | };
20 | })(window.XMLHttpRequest);
21 | }
--------------------------------------------------------------------------------
/app/assets/stylesheets/admin/_blog.css.scss:
--------------------------------------------------------------------------------
1 | // blog 列表页
2 |
3 | .blog-list {
4 |
5 | .panel-heading {
6 | h2 {
7 | float: left;
8 | height: 40px;
9 | line-height: 40px;
10 | font-size: 20px;
11 | padding-left: 20px;
12 | margin: 0;
13 |
14 | small {
15 | color: white;
16 | font-size: 12px;
17 | }
18 | }
19 |
20 | .dropdown-menu {
21 | right: 0;
22 | left: auto;
23 | top: calc(100% - 2px);
24 | }
25 | }
26 |
27 | .list-group {
28 | overflow: auto;
29 | height: calc(100% - 41px);
30 | }
31 |
32 | .list-group-item {
33 | padding-left: 19px;
34 | cursor: pointer;
35 |
36 | &:hover {
37 | background-color: whitesmoke;
38 |
39 | .tools {
40 | opacity: 1;
41 | }
42 | }
43 | }
44 |
45 | .list-group-item.ng-leave {
46 | -webkit-transition: all 1s ease-in all; /* Safari/Chrome */
47 | -moz-transition: all 1s linear all; /* Firefox */
48 | -o-transition: all 1s linear all; /* Opera */
49 | height: 0;
50 | }
51 |
52 | .list-group-item.active {
53 | .tools {
54 | opacity: 1;
55 | }
56 | }
57 |
58 | .list-group-item-text {
59 | color: $gray-light;
60 | span {
61 | margin-right: 10px;
62 | }
63 | }
64 |
65 | .empty {
66 | margin-top: 30%;
67 | text-align: center;
68 |
69 | i {
70 | font-size: 0.9em;
71 | }
72 |
73 | a:hover {
74 | text-decoration: none;
75 | }
76 | }
77 |
78 | .tools {
79 | opacity: 0;
80 | -webkit-transition: opacity 500ms;
81 |
82 | a {
83 | font-size: 16px;
84 |
85 | &:hover {
86 | text-decoration: none;
87 | }
88 | }
89 | }
90 | }
91 |
92 | .blog-preview {
93 | padding-left: 0;
94 |
95 | .panel-heading {
96 | background: white;
97 | border-bottom: 0;
98 | padding-left: 30px;
99 |
100 | h2 {
101 | margin: 0;
102 | a {
103 | i {
104 | font-size: 15px;
105 | display: none;
106 | }
107 | }
108 |
109 | a:hover {
110 | color: $link-color;
111 | i {
112 | display: inline;
113 | }
114 | }
115 | }
116 |
117 | .actions {
118 | margin-top: 3px;
119 | }
120 | }
121 |
122 | .misc {
123 | margin-bottom: 10px;
124 |
125 | span {
126 | margin-right: 3px;
127 | color: $gray-light;
128 | }
129 | }
130 |
131 | .blog-preview-content {
132 | overflow: auto;
133 | height: calc(100% - 53px);
134 | padding-left: 30px;
135 |
136 | > div {
137 | max-width: 800px;
138 | }
139 | }
140 | }
--------------------------------------------------------------------------------
/app/assets/stylesheets/admin/_blog_form.css.scss:
--------------------------------------------------------------------------------
1 | // 编辑器
2 | .blog-editor {
3 | input, textarea, select {
4 | @include no-border-input();
5 | }
6 |
7 | // override error
8 | input.ng-invalid {
9 | &, &:focus {
10 | border-color: transparent;
11 | @include box-shadow(none);
12 | }
13 | }
14 |
15 | // 标题 + 内容
16 | .panel-body {
17 | padding: 0 30px;
18 | height: calc(100% - 55px);
19 | }
20 |
21 | .title {
22 | input {
23 | &, &:focus {
24 | font-size: 34px;
25 | font-weight: bolder;
26 | font-family: Helvetica, Tahoma, Arial, "Microsoft YaHei", "微软雅黑", SimSun, "宋体", Heiti, "黑体", sans-serif;
27 | line-height: 57px;
28 | height: 70px;
29 | border-bottom: 1px solid #ccc;
30 | padding: 6px;
31 | }
32 | }
33 | }
34 |
35 | .content-wrap {
36 | height: calc(100% - 70px);
37 | margin: 0;
38 | padding: 20px 5px 5px 20px;
39 | }
40 |
41 | // 内容多行文本框
42 | .content {
43 | height: 100%;
44 | padding: 0;
45 | }
46 |
47 | // 拖拽文件
48 | .upload-drop {
49 | display: none;
50 | position: absolute;
51 | z-index: 1000;
52 | height: calc(100% - 25px);
53 | width: calc(100% - 30px);
54 | top: 20px;
55 | border: 3px dashed $gray-light;
56 | text-align: center;
57 | font-size: 20px;
58 | font-weight: bold;
59 | color: $gray-light;
60 |
61 | .tip {
62 | position: absolute;
63 | top: 50%;
64 | left: 50%;
65 | margin-top: -10px;
66 | margin-left: -70px;
67 | }
68 | }
69 |
70 | .upload-drop.dragover {
71 | border-color: lighten(#0000FF, 30%);
72 | background: rgba($gray-lighter, 0.8);
73 | }
74 |
75 | // 上传、提示等按钮
76 | .tools {
77 | position: absolute;
78 | right: 10px;
79 | top: 3px;
80 |
81 | > a {
82 | margin-right: 5px;
83 | text-decoration: none;
84 | color: #000;
85 | opacity: 0.3;
86 |
87 | &:hover {
88 | opacity: 0.8;
89 | }
90 |
91 | i {
92 | font-size: 1.2em;
93 | }
94 | }
95 | }
96 |
97 | // 预览
98 | .preview-wrap {
99 | height: calc(100% - 70px);
100 | padding: 20px 2px 5px 20px;
101 | background: #F7F7F9;
102 | background-clip: padding-box;
103 |
104 | .tip {
105 | color: #999;
106 | position: absolute;
107 | top: 3px;
108 | right: 10px;
109 | font-size: 12px;
110 | font-style: normal;
111 | }
112 | }
113 |
114 | .preview {
115 | height: 100%;
116 | overflow: auto;
117 | padding-right: 3px;
118 | }
119 |
120 | // 提交按钮
121 | .btn-submit {
122 | width: 150px;
123 | margin-right: 10px;
124 | }
125 |
126 | // 选项按钮
127 | .btn-config {
128 | font-size: 1.5em;
129 | vertical-align: middle
130 | }
131 |
132 | // 选项 popover
133 | .config-wrap {
134 | padding: 0px 5px;
135 |
136 | table {
137 | width: 300px;
138 | margin-bottom: 5px;
139 | }
140 |
141 | td {
142 | padding: 0;
143 | vertical-align: middle;
144 |
145 | &:first-child {
146 | padding-left: 5px;
147 | }
148 | }
149 |
150 | tr:first-of-type td {
151 | border-top: none;
152 | }
153 |
154 | .input-wrap {
155 | padding: 5px 0 5px 12px;
156 | }
157 | }
158 | }
--------------------------------------------------------------------------------
/app/assets/stylesheets/admin/_comment.css.scss:
--------------------------------------------------------------------------------
1 | // 评论列表
2 | .comment-list {
3 | @include single-transition(width, 2s, ease-in, 1s);
4 |
5 | .panel-heading {
6 | @include clearfix();
7 |
8 | h2 {
9 | float: left;
10 | height: 40px;
11 | line-height: 40px;
12 | font-size: 20px;
13 | padding-left: 20px;
14 | margin: 0;
15 | }
16 | }
17 |
18 | .list-group {
19 | overflow: auto;
20 | height: calc(100% - 40px);
21 | }
22 |
23 | .content {
24 | float: left;
25 | width: calc(100% - 350px);
26 | padding: 0 15px;
27 |
28 | .title {
29 | display: inline-block;
30 | margin-bottom: 5px;
31 | }
32 |
33 | article {
34 | margin-bottom: 30px;
35 | cursor: pointer;
36 | }
37 |
38 | footer {
39 | position: absolute;
40 | bottom: 5px;
41 | }
42 |
43 | .date {
44 | color: $gray-light;
45 | }
46 |
47 | .action {
48 | margin-left: 10px;
49 | }
50 | }
51 |
52 | .author {
53 | margin-right: 5px;
54 | width: 280px;
55 | float: right;
56 |
57 | img {
58 | height: 92px;
59 | width: 92px;
60 | border-radius: 5px;
61 | float: right;
62 | margin-left: 5px;
63 | }
64 |
65 | .detail {
66 | float: left;
67 | width: 180px;
68 | overflow: hidden;
69 |
70 | h3 {
71 | font-size: 18px;
72 | font-weight: bold;
73 | margin-top: 5px;
74 | margin-bottom: 20px;
75 | }
76 |
77 | small {
78 | display: block;
79 | color: $gray-light;
80 | margin-bottom: 5px;
81 | word-break: break-all;
82 | }
83 | }
84 | }
85 | }
86 |
87 | .comment-context {
88 | .panel-heading {
89 | padding: 10px 15px;
90 |
91 | a {
92 | color: white;
93 | opacity: 0.8;
94 | &:hover {
95 | opacity: 1;
96 | }
97 | }
98 | }
99 |
100 | .panel-body {
101 | height: calc(100% - 40px - 101px - 30px); // header + reply + margin
102 | overflow: auto;
103 | padding: 0px 20px;
104 | margin: 15px 0;
105 | }
106 |
107 | article {
108 | padding: 5px 0;
109 | margin-bottom: 20px;
110 |
111 | header {
112 | margin-bottom: 5px;
113 | }
114 |
115 | p {
116 | margin-bottom: 0;
117 | }
118 | }
119 |
120 | .quoted {
121 | color: $gray-light;
122 | border-left: 2px solid $gray-light;
123 | padding-left: 10px;
124 | }
125 |
126 | // 回复区域
127 | .reply {
128 | padding: 10px 8px;
129 | border-top: 1px solid $gray-lighter;
130 | @include clearfix();
131 |
132 | textarea {
133 | height: 80px;
134 | float: left;
135 | width: calc(100% - 60px);
136 | @include no-border-input();
137 | }
138 |
139 | button {
140 | height: 80px;
141 | width: 60px;
142 | float: left;
143 | }
144 | }
145 | }
--------------------------------------------------------------------------------
/app/assets/stylesheets/admin/_dashboard.css.scss:
--------------------------------------------------------------------------------
1 | .dashboard-widget {
2 | height: 250px;
3 | margin-bottom: 30px;
4 |
5 | .panel {
6 | padding: 10px 20px;
7 |
8 | h2 {
9 | margin: 5px 0 10px 0;
10 | font-size: 20px;
11 | }
12 |
13 | small {
14 | color: $gray-light;
15 | font-size: 14px;
16 | margin-left: 5px;
17 |
18 | a {
19 | color: $gray-light;
20 | opacity: 0.6;
21 |
22 | &:hover {
23 | opacity: 1;
24 | }
25 | }
26 | }
27 |
28 | // 其他说明
29 | .others {
30 | font-size: 12px;
31 | color: $gray-light;
32 | position: absolute;
33 | bottom: 5px;
34 | right: 20px;
35 | }
36 | }
37 |
38 | // 载入中
39 | .panel-pending {
40 | background: asset-url("coffee.gif") white no-repeat center;
41 | }
42 |
43 | // 载入失败
44 | .panel-failed {
45 | .panel-error {
46 | display: table;
47 | }
48 | }
49 |
50 | .panel-error {
51 | display: none;
52 | text-align: center;
53 | width: 100%;
54 | height: 100%;
55 |
56 | .panel-error-inner {
57 | display: table-cell;
58 | vertical-align: middle;
59 | }
60 | }
61 |
62 | // 载入成功
63 | .panel-success {
64 | .panel-body {
65 | display: block;
66 | }
67 | }
68 |
69 | .panel-body {
70 | display: none;
71 | padding: 0;
72 | }
73 |
74 | // 第一个统计面板
75 | .statistics {
76 | text-align: center;
77 |
78 | .total-visits {
79 | margin-top: 10px;
80 |
81 | strong {
82 | font-size: 80px;
83 | }
84 | }
85 |
86 | .stat-count {
87 | span {
88 | margin-right: 20px;
89 |
90 | &:last-of-type {
91 | margin-right: 0;
92 | }
93 | }
94 |
95 | strong {
96 | font-size: 50px;
97 | }
98 | }
99 | }
100 | }
101 |
102 | // 同步日志
103 | .sync-modal {
104 | width: 400px;
105 |
106 | ul {
107 | padding-left: 0;
108 | list-style: none;
109 | }
110 | }
--------------------------------------------------------------------------------
/app/assets/stylesheets/admin/_setting.css.scss:
--------------------------------------------------------------------------------
1 | // 设置
2 | .setting-wrap {
3 | .nav-wrap {
4 | padding-right: 0;
5 | padding-top: 25px;
6 | margin-right: -3px;
7 | z-index: 1000;
8 | width: 130px;
9 | }
10 |
11 | .panel-wrap {
12 | padding-left: 0;
13 | width: calc(100% - 130px);
14 | }
15 |
16 | .panel-body {
17 | padding: 30px 50px;
18 | overflow: auto;
19 | }
20 |
21 | legend {
22 | padding-left: 10px;
23 | padding-bottom: 5px;
24 | font-weight: bolder;
25 | }
26 |
27 | .form-group {
28 | margin-bottom: 20px;
29 |
30 | .form-control {
31 | width: 400px;
32 | }
33 |
34 | .help-block {
35 | margin-bottom: 0;
36 |
37 | a {
38 | color: lighten($link-color, 10%);
39 | }
40 | }
41 | }
42 |
43 | .website-form {
44 | .control-label {
45 | width: 120px;
46 | }
47 |
48 | // 头像
49 | .avatar {
50 | position: relative;
51 | width: 200px;
52 |
53 | img {
54 | border-radius: 10px;
55 | }
56 |
57 | a {
58 | position: absolute;
59 | bottom: 0;
60 | width: 100%;
61 | text-align: center;
62 | background: #333;
63 | padding: 10px 0;
64 | opacity: 0.9;
65 | display: none;
66 | border-bottom-left-radius: 10px;
67 | border-bottom-right-radius: 10px;
68 |
69 | &:hover {
70 | color: white;
71 | }
72 | }
73 |
74 | .mask {
75 | position: absolute;
76 | top: 0;
77 | left: 0;
78 | right: 0;
79 | bottom: 0;
80 | background: #ccc;
81 | opacity: 0.9;
82 | color: white;
83 | text-align: center;
84 | padding-top: 40%;
85 | font-size: 16px;
86 | border-radius: 10px;
87 | }
88 |
89 | &:hover {
90 | a {
91 | display: block;
92 | }
93 | }
94 | }
95 | }
96 |
97 | .intro {
98 | border-bottom: 1px solid $gray-lighter;
99 | margin-bottom: 40px;
100 | padding: 20px;
101 |
102 | p {
103 | margin-bottom: 20px;
104 | line-height: 2;
105 | }
106 | }
107 |
108 | // 分类
109 | .category-wrap {
110 | > table {
111 | max-width: 800px;
112 |
113 | td {
114 | border-top-style: dashed;
115 | }
116 | }
117 |
118 | // 添加的表单
119 | .form-add {
120 | width: 350px;
121 | margin-bottom: 30px;
122 | margin-left: 8px;
123 | }
124 |
125 | // 修改的表单
126 | .form-edit {
127 | margin: 10px 0;
128 |
129 | .form-group {
130 | margin-bottom: 0;
131 | width: 350px;
132 |
133 | .form-control {
134 | width: 100%;
135 | }
136 | }
137 |
138 | a {
139 | margin-left: 10px;
140 | }
141 | }
142 | }
143 |
144 | .attach-wrap {
145 | a.action {
146 | margin-left: 20px;
147 | color: $gray-light;
148 | opacity: 0;
149 | @include transition(opacity 500ms)
150 | }
151 |
152 | tr:hover {
153 | a.action {
154 | opacity: 1;
155 | }
156 | }
157 | }
158 |
159 | .ga-form {
160 | .control-label {
161 | width: 180px;
162 | }
163 | }
164 | }
--------------------------------------------------------------------------------
/app/assets/stylesheets/public-bootstrap.css.scss:
--------------------------------------------------------------------------------
1 | // Core variables and mixins
2 | @import "bootstrap/variables";
3 | @import "bootstrap/mixins";
4 |
5 | // Reset
6 | @import "bootstrap/normalize";
7 | @import "bootstrap/print";
8 |
9 | // Core CSS
10 | @import "bootstrap/scaffolding";
11 | @import "bootstrap/type";
12 | @import "bootstrap/code";
13 | @import "bootstrap/grid";
14 | //@import "bootstrap/tables";
15 | //@import "bootstrap/forms";
16 | //@import "bootstrap/buttons";
17 |
18 | // Components
19 | @import "bootstrap/component-animations";
20 | //@import "bootstrap/glyphicons";
21 | //@import "bootstrap/dropdowns";
22 | //@import "bootstrap/button-groups";
23 | //@import "bootstrap/input-groups";
24 | //@import "bootstrap/navs";
25 | //@import "bootstrap/navbar";
26 | //@import "bootstrap/breadcrumbs";
27 | //@import "bootstrap/pagination";
28 | @import "bootstrap/pager";
29 | //@import "bootstrap/labels";
30 | //@import "bootstrap/badges";
31 | //@import "bootstrap/jumbotron";
32 | //@import "bootstrap/thumbnails";
33 | //@import "bootstrap/alerts";
34 | //@import "bootstrap/progress-bars";
35 | //@import "bootstrap/media";
36 | //@import "bootstrap/list-group";
37 | //@import "bootstrap/panels";
38 | @import "bootstrap/wells";
39 | @import "bootstrap/close";
40 |
41 | // Components w/ JavaScript
42 | @import "bootstrap/modals";
43 | //@import "bootstrap/tooltip";
44 | //@import "bootstrap/popovers";
45 | //@import "bootstrap/carousel";
46 |
47 | // Utility classes
48 | @import "bootstrap/utilities";
49 | @import "bootstrap/responsive-utilities";
50 |
--------------------------------------------------------------------------------
/app/cells/nav/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link_to "Blog", '/', :class => nav_class('blog') %>
5 |
6 |
7 | <%= link_to "Archives", archive_path, :class => nav_class('archive') %>
8 |
9 | <% @pages.each do |page| %>
10 |
11 | <%= link_to page.title, page_path(page.slug), :class => nav_class("page_#{page.slug}") %>
12 |
13 | <% end %>
14 |
15 |
--------------------------------------------------------------------------------
/app/cells/nav_cell.rb:
--------------------------------------------------------------------------------
1 | class NavCell < Cell::Rails
2 | helper FontAwesome::Rails::IconHelper, ApplicationHelper
3 |
4 | # 显示导航栏
5 | def show(curr_nav)
6 | @curr_nav = curr_nav
7 | @pages = Page.order('sid ASC')
8 | render
9 | end
10 |
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/admin/application_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::ApplicationController < ApplicationController
2 | layout false
3 | add_flash_types :error
4 |
5 | before_action :check_admin
6 |
7 | private
8 | # 检查是否为admin
9 | def check_admin
10 | redirect_to new_admin_session_path unless is_admin?
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/admin/attaches_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::AttachesController < Admin::ApplicationController
2 |
3 | def index
4 | @attaches = Attach.order('created_at DESC').includes(:parent).page(params[:page]).per(15)
5 | end
6 |
7 | #上传附件
8 | def create
9 | @attach = Attach.new_by_params(params)
10 |
11 | if @attach.save
12 | render
13 | else
14 | render :status => 422
15 | end
16 | end
17 |
18 | def destroy
19 | @attach = Attach.find(params[:id])
20 | @attach.destroy
21 |
22 | head :no_content
23 | end
24 | end
--------------------------------------------------------------------------------
/app/controllers/admin/blogs_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::BlogsController < Admin::ApplicationController
2 |
3 | def index
4 | @blogs = Blog.order("created_at DESC").includes(:category).page(params[:page]).per(10)
5 | @blogs = @blogs.where(:status => params[:status]) if params[:status].present?
6 | end
7 |
8 | def show
9 | @blog = Blog.find(params[:id])
10 | end
11 |
12 | def create
13 | @blog = Blog.new(blog_params)
14 | if @blog.save
15 | # 更新附件的归属
16 | Attach.update_parent((params[:attaches] || []).map { |a| a[:id] }, @blog)
17 | render :show
18 | else
19 | render :show, :status => 422
20 | end
21 | end
22 |
23 | def update
24 | @blog = Blog.find(params[:id])
25 | if @blog.update_attributes(blog_params)
26 | # 更新附件的归属
27 | Attach.update_parent((params[:attaches] || []).map { |a| a[:id] }, @blog)
28 | render :show
29 | else
30 | render :show, :status => 422
31 | end
32 | end
33 |
34 | def destroy
35 | @blog = Blog.find(params[:id])
36 | @blog.destroy
37 |
38 | head :no_content
39 | end
40 |
41 | def publish
42 | @blog = Blog.find(params[:id])
43 | @blog.publish!
44 |
45 | head :no_content
46 | end
47 |
48 | private
49 | def blog_params
50 | params.require(:blog).permit(:title, :content, :slug, :category_id, :status, :attaches)
51 | end
52 | end
--------------------------------------------------------------------------------
/app/controllers/admin/categories_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::CategoriesController < Admin::ApplicationController
2 |
3 | def index
4 | @categories = Category.all
5 | end
6 |
7 | def create
8 | @category = Category.new(category_params)
9 |
10 | if @category.save
11 | render :show
12 | else
13 | render :show, :status => 422
14 | end
15 | end
16 |
17 | def update
18 | @category = Category.find(params[:id])
19 |
20 | if @category.update_attributes(category_params)
21 | render :show
22 | else
23 | render :show, :status => 422
24 | end
25 | end
26 |
27 | def destroy
28 | @category = Category.find(params[:id])
29 | @category.destroy
30 |
31 | head :no_content
32 | end
33 |
34 | private
35 | def category_params
36 | params.require(:category).permit(:name)
37 | end
38 | end
--------------------------------------------------------------------------------
/app/controllers/admin/comments_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::CommentsController < Admin::ApplicationController
2 |
3 | def index
4 | @comments = Comment.all(params[:cursor])
5 | end
6 |
7 | def create
8 | @comment = Comment.new(params[:comment])
9 | @comment.save
10 | render :show
11 | end
12 |
13 | def destroy
14 | Comment.remove(params[:id])
15 | head :no_content
16 | end
17 |
18 | # 获取详情,其实是上下文
19 | def context
20 | @comments = Comment.showContext(params[:comment_id])
21 | @comments.reverse!
22 | end
23 |
24 | end
--------------------------------------------------------------------------------
/app/controllers/admin/dashboard_controller.rb:
--------------------------------------------------------------------------------
1 | # 统计
2 | class Admin::DashboardController < Admin::ApplicationController
3 | rescue_from Faraday::ClientError, OAuth2::Error, :with => :ga_request_error
4 |
5 | def show
6 | @blog_publish_count = Blog.with_status(:publish).count
7 | @blog_draft_count = Blog.with_status(:draft).count
8 | @comment_count = Blog.sum(:comment_count)
9 | @total_visits = GaClient.get_total_visits if Setting.ga.chart_enable
10 |
11 | render :json => {
12 | :blog => {:publish => @blog_publish_count, :draft => @blog_draft_count},
13 | :comment => @comment_count,
14 | :disqus_enable => Setting.disqus.enable,
15 | :ga_enable => Setting.ga.chart_enable,
16 | :total_visits => @total_visits
17 | }
18 | end
19 |
20 | # 评论的统计
21 | def comment
22 | @comment_count = Blog.sum(:comment_count)
23 | @disqus_enable = Setting.disqus.enable
24 |
25 | render :json => {
26 | :total => @comment_count,
27 | :disqus_enable => Setting.disqus.enable
28 | }
29 | end
30 |
31 | # 每日访问量
32 | def daily_visits
33 | visits = GaClient.get_daily_visits
34 | visits = visits.map do |visit|
35 | [DateTime.parse(visit.date).to_i * 1000, visit.visits.to_i]
36 | end
37 | render :json => visits
38 | end
39 |
40 | # 页面访问排行
41 | def top_pages
42 | results = GaClient.get_top_pages
43 | results = results.map(&:marshal_dump)
44 | render :json => results
45 | end
46 |
47 | def browser
48 | results = GaClient.get_browser_with_version
49 | results = results.map(&:marshal_dump)
50 | render :json => results
51 | end
52 |
53 | def hot_blogs
54 | @hot_blogs = Blog.select(:id, :title, :status, :slug, :comment_count).order('comment_count DESC').limit(5)
55 |
56 | render :json => @hot_blogs
57 | end
58 |
59 | # 附件统计
60 | def attach
61 | @attach_count = Attach.count
62 | @attach_size = Attach.sum('file_size')
63 |
64 | render :json => {
65 | :count => @attach_count,
66 | :size => view_context.number_to_human_size(@attach_size),
67 | }
68 | end
69 |
70 | # 评论数同步日志
71 | def sync_comment_logs
72 | @logs = Setting.sync_comment_logs || []
73 | @logs.map!(&:marshal_dump)
74 |
75 | render :json => @logs
76 | end
77 |
78 | # 同步评论数
79 | def sync_comment
80 | # 防御 get
81 | render :text => '', :status => 404 and return if request.get?
82 | Comment.sync_count
83 | head :no_content
84 | end
85 |
86 | private
87 | # GA api 请求中发生错误的处理,错误原因如 timeout 等
88 | def ga_request_error(e)
89 | render :json => {:error => e.message}, :status => e.try(:response).try(:status) || 408 # timeout code default
90 | GaClient.clear_service_account_user
91 |
92 | # log error
93 | message = "\n#{e.class} (#{e.message}):\n"
94 | message << " " << Rails.backtrace_cleaner.clean(e.backtrace).join("\n ")
95 | logger.fatal("#{message}\n\n")
96 | end
97 | end
--------------------------------------------------------------------------------
/app/controllers/admin/disqus_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::DisqusController < Admin::ApplicationController
2 |
3 | def show
4 | @disqus = Disqus.find
5 |
6 | render :json => @disqus
7 | end
8 |
9 | def update
10 | @disqus = Disqus.find
11 |
12 | if @disqus.update_attributes(disqus_params)
13 | render :json => @disqus
14 | else
15 | render :json => @disqus, :status => 422
16 | end
17 | end
18 |
19 | def enable
20 | @disqus = Disqus.find
21 | @disqus.update_enable(params[:enable])
22 |
23 | render :json => @disqus
24 | end
25 |
26 | private
27 |
28 | def disqus_params
29 | params.require(:disqus).permit(:enable, :shortname, :api_secret, :access_token)
30 | end
31 | end
--------------------------------------------------------------------------------
/app/controllers/admin/ga_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::GaController < Admin::ApplicationController
2 |
3 | def show
4 | @ga = Ga.find
5 |
6 | render :json => @ga.to_json(:methods => [:secret_file])
7 | end
8 |
9 | def update
10 | @ga = Ga.find
11 |
12 | if @ga.update_attributes(ga_params)
13 | # 更新附件的归属
14 | Attach.update_parent(ga_params[:secret_file_id], @ga)
15 | render :json => @ga
16 | else
17 | render :json => @ga, :status => 422
18 | end
19 | end
20 |
21 | private
22 |
23 | def ga_params
24 | params.require(:ga).permit(:account, :chart_enable, :api_email).tap do |whitelisted|
25 | whitelisted[:secret_file_id] = params[:secret_file][:id] if params[:secret_file]
26 | end
27 | end
28 | end
--------------------------------------------------------------------------------
/app/controllers/admin/home_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::HomeController < Admin::ApplicationController
2 | #管理首页
3 | def show
4 | end
5 | end
--------------------------------------------------------------------------------
/app/controllers/admin/pages_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::PagesController < Admin::ApplicationController
2 |
3 | def index
4 | @pages = Page.order('sid ASC').all
5 | end
6 |
7 | def show
8 | @page = Page.find(params[:id])
9 | end
10 |
11 | def create
12 | @page = Page.new(page_params)
13 | params[:detail] = true
14 |
15 | if @page.save
16 | # 更新附件的归属
17 | Attach.update_parent((params[:attaches] || []).map { |a| a[:id] }, @page)
18 | render :show
19 | else
20 | render :show, :status => 422
21 | end
22 | end
23 |
24 | def update
25 | @page = Page.find(params[:id])
26 | params[:detail] = true
27 |
28 | if @page.update_attributes(page_params)
29 | # 更新附件的归属
30 | Attach.update_parent((params[:attaches] || []).map { |a| a[:id] }, @page)
31 | render :show
32 | else
33 | render :show, :status => 422
34 | end
35 | end
36 |
37 | def destroy
38 | @page = Page.find(params[:id])
39 | @page.destroy
40 |
41 | head :no_content
42 | end
43 |
44 | def up
45 | @page = Page.find(params[:id])
46 | @page.up
47 |
48 | head :no_content
49 | end
50 |
51 | def down
52 | @page = Page.find(params[:id])
53 | @page.down
54 |
55 | head :no_content
56 | end
57 |
58 | private
59 |
60 | def page_params
61 | params.require(:page).permit(:title, :content, :slug, :attaches)
62 | end
63 | end
--------------------------------------------------------------------------------
/app/controllers/admin/password_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::PasswordController < Admin::ApplicationController
2 |
3 | def update
4 | @admin_pass = Password.new(password_params)
5 |
6 | if @admin_pass.save
7 | render :json => @admin_pass
8 | else
9 | render :json => @admin_pass, :status => 422
10 | end
11 | end
12 |
13 | private
14 | def password_params
15 | params.require(:password).permit(:old, :new, :new_confirmation)
16 | end
17 | end
--------------------------------------------------------------------------------
/app/controllers/admin/passwords_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::PasswordsController < Admin::ApplicationController
2 |
3 | def update
4 | @admin_pass = Password.new(password_params)
5 |
6 | if @admin_pass.save
7 | render :json => @admin_pass
8 | else
9 | render :json => @admin_pass, :status => 422
10 | end
11 | end
12 |
13 | private
14 | def password_params
15 | params.require(:password).permit(:old_pw, :new_pw, :new_pw_confirmation)
16 | end
17 | end
--------------------------------------------------------------------------------
/app/controllers/admin/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::SessionsController < Admin::ApplicationController
2 | skip_action_callback :check_admin
3 |
4 | # 登录页面
5 | def new
6 | # 如果忘记密码,请删除 admin_pass 记录,这里会初始化为 password
7 | Password.reset("password") if Setting.admin_pass.nil?
8 | redirect_to '/admin' if is_admin?
9 | end
10 |
11 | def create
12 | if Password.valid? params[:password]
13 | session[:admin] = true
14 | redirect_to '/admin'
15 | else
16 | redirect_to new_admin_session_path, :error => '密码错误!'
17 | end
18 | end
19 |
20 | def destroy
21 | session[:admin] = nil
22 | respond_to do |format|
23 | format.html { redirect_to :action => :new }
24 | format.json { head :no_content }
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/controllers/admin/websites_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::WebsitesController < Admin::ApplicationController
2 | helper
3 |
4 | def show
5 | @website = Website.find
6 | @website.avatar = view_context.image_path('no_avatar.png') if @website.avatar.nil?
7 | render :json => @website
8 | end
9 |
10 | def update
11 | @website = Website.find
12 | if @website.update_attributes(website_params)
13 | # 更新附件的归属
14 | Attach.update_parent(website_params[:avatar_id], @website)
15 | render :json => @website
16 | else
17 | render :json => @website, :status => 422
18 | end
19 | end
20 |
21 | private
22 |
23 | def website_params
24 | params.require(:website).permit(:title, :sub_title, :author, :avatar, :avatar_id, :github, :weibo, :donate, :ga)
25 | end
26 | end
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 |
6 | layout 'public'
7 |
8 | private
9 |
10 | # 是否是管理员
11 | def is_admin?
12 | return session[:admin] == true
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/controllers/archive_controller.rb:
--------------------------------------------------------------------------------
1 | class ArchiveController < ApplicationController
2 |
3 | def show
4 | expires_in 1.hours
5 | @blogs = Blog.select(:title, :created_at, :status, :slug).with_status(:publish).order('created_at DESC')
6 |
7 | # 按年分组
8 | @blogs_by_year = {}
9 |
10 | @blogs.each do |blog|
11 | @blogs_by_year[blog.created_at.year] ||= []
12 | @blogs_by_year[blog.created_at.year] << blog
13 | end
14 |
15 | @curr_nav = "archive"
16 | end
17 |
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/blogs_controller.rb:
--------------------------------------------------------------------------------
1 | class BlogsController < ApplicationController
2 |
3 | def index
4 | expires_in 2.minutes, :public=>true
5 | @blogs = Blog.with_status(:publish).includes([:category, :tags]).order('created_at DESC').page(params[:page])
6 | @curr_nav = "blog"
7 | end
8 |
9 | def show
10 | @blog = Blog.where(:slug=>params[:id]).first
11 | raise ActiveRecord::RecordNotFound if @blog.nil? || @blog.draft?
12 |
13 | @prev_blog = Blog.with_status(:publish).where('id < ?', @blog.id).order('id DESC').first
14 | @next_blog = Blog.with_status(:publish).where('id > ?', @blog.id).order('id ASC').first
15 | end
16 |
17 | # 根据 post 过来的数据提供预览页面
18 | def preview
19 | @blog = Blog.new_preview(blog_params)
20 | @preview = true
21 | render :show
22 | end
23 |
24 | private
25 | def blog_params
26 | params.permit(:title, :content, :category_id)
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/app/controllers/categories_controller.rb:
--------------------------------------------------------------------------------
1 | class CategoriesController < ApplicationController
2 |
3 | def show
4 | @category = Category.where(:name=>params[:id]).first
5 | raise ActiveRecord::RecordNotFound if @category.nil?
6 |
7 | @blogs = @category.blogs.with_status(:publish).order("created_at DESC").page(params[:page])
8 | render 'blogs/index'
9 | end
10 |
11 | end
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/feed_controller.rb:
--------------------------------------------------------------------------------
1 | class FeedController < ApplicationController
2 |
3 | def show
4 | expires_in 1.hours, :public=>true
5 | @blogs = Blog.with_status(:publish).order('created_at DESC')
6 | render :layout => false
7 | end
8 |
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/pages_controller.rb:
--------------------------------------------------------------------------------
1 | class PagesController < ApplicationController
2 |
3 | def show
4 | @page = Page.where(:slug => params[:id]).first
5 | raise ActiveRecord::RecordNotFound if @page.nil?
6 |
7 | @curr_nav = "page_#{@page.slug}"
8 | end
9 |
10 | end
--------------------------------------------------------------------------------
/app/controllers/tags_controller.rb:
--------------------------------------------------------------------------------
1 | class TagsController < ApplicationController
2 |
3 | def show
4 | @tag = ActsAsTaggableOn::Tag.where(:name => params[:id])
5 | @blogs = Blog.with_status(:publish).tagged_with(params[:id]).order("created_at DESC").page(params[:page])
6 | render 'blogs/index'
7 | end
8 |
9 | end
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | # 获取操作系统
3 | # os.mac?
4 | def os
5 | os = case (request.user_agent || "").downcase
6 | when /iphone|ipad|itouch/
7 | "ios"
8 | when /android/
9 | "android"
10 | when /mac/
11 | "mac"
12 | when /windows/
13 | "win"
14 | else
15 | "unknown"
16 | end
17 | ActiveSupport::StringInquirer.new(os)
18 | end
19 |
20 | # 导航项是否高亮,手工设置 @curr_nav 变量来实现
21 | def nav_class(name)
22 | "active" if @curr_nav == name
23 | end
24 |
25 | # 页面标题
26 | def title(text)
27 | subhead = "#{Setting.website.title} - #{Setting.website.author}的Blog"
28 | if text.present?
29 | "#{text} - #{subhead}"
30 | else
31 | subhead
32 | end
33 | end
34 |
35 | # 针对网站设置的一些链接,Github,微博等等
36 | # use Ruby 2.0 keywords argument
37 | def website_link(setting_key, icon: "", url: "", title: "")
38 | setting = Setting.website[setting_key]
39 | if setting.present?
40 | url = setting if url.blank?
41 | link_to fa_icon(icon + ' fa-fw'), url, :target => '_blank', :rel => 'nofollow', :title => title
42 | end
43 | end
44 | end
--------------------------------------------------------------------------------
/app/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/app/mailers/.keep
--------------------------------------------------------------------------------
/app/models/attach.rb:
--------------------------------------------------------------------------------
1 | class Attach < ActiveRecord::Base
2 | attr_accessor :max_width, :max_height
3 |
4 | before_create :fill_attributes
5 | after_destroy :delete_file
6 |
7 | belongs_to :parent, :polymorphic => true
8 |
9 | mount_uploader :file, AttachUploader
10 |
11 | def self.new_by_params(params)
12 | attach = Attach.new
13 | attach.max_width = params[:max_width]
14 | attach.file = params[:file]
15 | attach.file_name = params[:file].original_filename
16 | attach
17 | end
18 |
19 | def self.update_parent(ids, parent)
20 | return if ids.blank?
21 | attaches = self.where(:id => ids)
22 | attaches.update_all(:parent_id => parent.id, :parent_type => parent.class.to_s)
23 | end
24 |
25 | # 填充content_type,file_size字段
26 | def fill_attributes
27 | self.content_type = file.file.content_type
28 | self.file_size = file.file.size
29 | end
30 |
31 | # 删除对应的文件
32 | def delete_file
33 | self.remove_file!
34 | end
35 |
36 | def image?
37 | self.content_type.include?('image')
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/app/models/blog.rb:
--------------------------------------------------------------------------------
1 | class Blog < ActiveRecord::Base
2 | include TruncateHtmlHelper
3 | include HasSlug
4 | extend Enumerize
5 | enumerize :status, :in => {:draft => 0, :publish => 1}, :predicates => true, :scope => true
6 |
7 | acts_as_ordered_taggable
8 | paginates_per 5
9 |
10 | validates :title, :length => {:in => 2..100}
11 | validates :content, :length => {:in => 3..100000}
12 |
13 | before_validation :clean_slug
14 | before_save :fill_slug
15 | before_save :fill_html_content
16 | after_save :update_blog_count
17 |
18 | belongs_to :category
19 | has_many :attaches, :as=>:parent, :dependent => :destroy
20 |
21 | # 创建一个预览对象
22 | def self.new_preview(params)
23 | blog = Blog.new(params)
24 | blog.fill_html_content
25 | blog.created_at = Time.now
26 |
27 | blog
28 | end
29 |
30 | def publish!
31 | self.status = :publish
32 | self.save
33 | end
34 |
35 | # 将 Markdown 转为 HTML 保存,并保存摘要
36 | def fill_html_content
37 | self.html_content = Klog2::Markdown.render(self.content)
38 | self.html_content_summary = truncate_html(self.html_content, :length => 250, :omission => '', :break_token => '')
39 | end
40 |
41 | # 更新分类的 blog_count
42 | def update_blog_count
43 | return if category.nil?
44 | # 如果状态变动或者分类变动,重算当前分类
45 | category.update_blog_count if status_changed? or category_id_changed?
46 | # 如果分类变动,重算之前的分类
47 | Category.find(category_id_was).update_blog_count if category_id_changed? and !category_id_was.nil?
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/app/models/category.rb:
--------------------------------------------------------------------------------
1 | class Category < ActiveRecord::Base
2 | validates :name,
3 | :length => {:in => 2..20},
4 | :uniqueness => true
5 |
6 | before_destroy :clear_blogs_category
7 |
8 | has_many :blogs
9 |
10 | # 将分类下所有blog的分类属性置为空
11 | def clear_blogs_category
12 | self.blogs.update_all(:category_id => nil)
13 | end
14 |
15 | # 重新计算 blog count 并保存
16 | def update_blog_count
17 | update_attributes(:blog_count => blogs.with_status(:publish).count)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/models/comment.rb:
--------------------------------------------------------------------------------
1 | class Comment
2 | include ActiveModel::Model
3 |
4 | attr_accessor :id, :content, :author_name, :author_email, :author_avatar, :ip, :created_at, :blog, :blog_id, :parent
5 |
6 | def self.all(cursor="", options={})
7 | hash, cursor = DisqusClient.all_post(cursor, options)
8 | arr = CommentArray.new(hash)
9 | arr.cursor = cursor
10 | arr
11 | end
12 |
13 | # 获取评论的上下文
14 | def self.showContext(id)
15 | hash = DisqusClient.get_context(id)
16 | # 此时获取的数据中不含 thread 详情,所以 with_blog: false
17 | CommentArray.new(hash, :with_blog => false)
18 | end
19 |
20 | # 删除
21 | def self.remove(id)
22 | DisqusClient.remove_post(id)
23 | end
24 |
25 | # 从 disqus api 同步 BLOG 评论数
26 | def self.sync_count
27 | return unless Disqus.find.enable?
28 | latest_log = {:at => Time.now}
29 |
30 | begin
31 | DisqusClient.all_thread.each do |th|
32 | blog = Blog.where(:id => th["identifiers"][0]).first
33 | blog.update_columns(:comment_count => th["posts"]) if blog
34 | end
35 | latest_log[:status] = "success"
36 | rescue Exception => e
37 | latest_log[:error] = e.message
38 | latest_log[:status] = "failed"
39 | ensure
40 | sync_logs = Setting.sync_comment_logs || []
41 | sync_logs = sync_logs.push(latest_log).last(5)
42 | Setting.sync_comment_logs = sync_logs
43 | end
44 | end
45 |
46 | # 保存(仅用于创建)
47 | def save
48 | hash = DisqusClient.create_post(self)
49 | fill_by_api(hash, :with_blog => false)
50 |
51 | # 关联的 BLOG
52 | threadHash = DisqusClient.get_thread(hash["thread"])
53 | blog_id = threadHash["identifiers"][0]
54 | self.blog = Blog.where(:id => blog_id).first if blog_id
55 | end
56 |
57 | # 根据 API 获取的数据填充自身字段
58 | def fill_by_api(hash, with_blog: true)
59 | self.id = hash["id"]
60 | self.content = hash["raw_message"]
61 | self.author_name = hash["author"]["name"]
62 | self.author_email = hash["author"]["email"]
63 | self.author_avatar = hash["author"]["avatar"]["large"]["permalink"]
64 | self.ip = hash["ipAddress"]
65 | self.created_at = hash["createdAt"].to_datetime.in_time_zone
66 | self.blog_id = hash["thread"]["identifiers"][0] if with_blog
67 | end
68 |
69 | ###################
70 | # Comment 数组类
71 | ###################
72 | class CommentArray < Array
73 | attr_accessor :cursor
74 |
75 | def initialize(api_response, with_blog: true)
76 | @comments = api_response.map! do |cm|
77 | c = Comment.new
78 | c.fill_by_api(cm, :with_blog => with_blog)
79 | c
80 | end
81 |
82 | # 填充 blog 关联字段
83 | if with_blog
84 | blog_ids = @comments.map(&:blog_id)
85 | Blog.where(:id => blog_ids).each do |blog|
86 | arr = @comments.select { |c| c.blog_id == blog.id.to_s }
87 | arr.each { |c| c.blog = blog }
88 | end
89 | end
90 | super @comments
91 | end
92 |
93 | # 下一页
94 | def next
95 | Comment.all(cursor["next"])
96 | end
97 |
98 | def prev
99 | Comment.all(cursor["prev"])
100 | end
101 |
102 | def has_next?
103 | @cursor["hasNext"]
104 | end
105 |
106 | def has_prev?
107 | cursor["hasPrev"]
108 | end
109 | end
110 | end
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/concerns/has_slug.rb:
--------------------------------------------------------------------------------
1 | # 拥有 Slug 的 Model ,如 Blog/Page
2 | module HasSlug
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | before_validation :clean_slug
7 | before_save :fill_slug
8 |
9 | validates :slug, :uniqueness => true
10 | end
11 |
12 | # 将slug中的非法字符过滤掉
13 | def clean_slug
14 | self.slug = self.slug.gsub(/[^a-zA-Z\-0-9]/, '-').downcase if self.slug.present?
15 | end
16 |
17 | # 如果没有slug则用时间戳代替
18 | def fill_slug
19 | self.slug = Time.now.to_i.to_s if self.slug.blank?
20 | end
21 | end
--------------------------------------------------------------------------------
/app/models/disqus.rb:
--------------------------------------------------------------------------------
1 | class Disqus
2 | include ActiveModel::Model
3 |
4 | PUBLIC_FIELDS = [:enable, :shortname, :api_secret, :access_token]
5 |
6 | attr_accessor *PUBLIC_FIELDS
7 |
8 | with_options :if => :enable? do |disqus|
9 | disqus.validates :shortname, :presence => true
10 | disqus.validates :api_secret, :presence => true
11 | disqus.validates :access_token, :presence => true
12 | disqus.validate :validate_legality
13 | end
14 |
15 | # 获取实例
16 | def self.find
17 | Disqus.new Setting.disqus.marshal_dump
18 | end
19 |
20 | # 更新字段
21 | def update_attributes(attributes={})
22 | attributes.each do |name, value|
23 | send("#{name}=", value)
24 | end
25 | if valid?
26 | Setting.disqus = as_hash
27 | return true
28 | else
29 | return false
30 | end
31 | end
32 |
33 | def update_enable(bool)
34 | self.enable = bool
35 | Setting.disqus = as_hash
36 | end
37 |
38 | def enable?
39 | enable
40 | end
41 |
42 | # 校验合法性,发起请求来验证
43 | def validate_legality
44 | return if !enable? or shortname.blank? or api_secret.blank? or access_token.blank?
45 | begin
46 | Comment.all("", :shortname => shortname, :api_secret => api_secret, :access_token => access_token)
47 | rescue
48 | errors.add(:shortname, 'Disqus 校验失败!')
49 | end
50 | end
51 |
52 | def as_hash
53 | hash = {}
54 | PUBLIC_FIELDS.each { |f| hash[f] = send(f) }
55 | hash
56 | end
57 | end
--------------------------------------------------------------------------------
/app/models/ga.rb:
--------------------------------------------------------------------------------
1 | # google analytic 的设置
2 | class Ga
3 | include ActiveModel::Model
4 |
5 | PUBLIC_FIELDS = [:account, :chart_enable, :secret_file_id, :api_email]
6 |
7 | attr_accessor *PUBLIC_FIELDS
8 | attr_accessor :secret_file
9 |
10 | with_options :if => :chart_enable do |ga|
11 | ga.validates :secret_file_id, :presence => true
12 | ga.validates :api_email, :presence => true
13 | ga.validate :validate_legality
14 | end
15 |
16 | # 获取实例
17 | def self.find
18 | ga = Ga.new(Setting.ga.marshal_dump)
19 | ga.fill_secret_file
20 | ga
21 | end
22 |
23 | # 更新字段
24 | def update_attributes(attributes={})
25 | attributes.each do |name, value|
26 | send("#{name}=", value)
27 | end
28 | fill_secret_file
29 | if valid?
30 | Setting.ga = as_hash
31 | return true
32 | else
33 | return false
34 | end
35 | end
36 |
37 | def as_hash
38 | hash = {}
39 | PUBLIC_FIELDS.each { |f| hash[f] = send(f) }
40 | hash
41 | end
42 |
43 | def id
44 | nil
45 | end
46 |
47 | def fill_secret_file
48 | self.secret_file = Attach.find(secret_file_id) if secret_file_id.present?
49 | end
50 |
51 | # 校验合法性,发起请求来验证
52 | def validate_legality
53 | return if !chart_enable or secret_file.nil? or api_email.blank?
54 | begin
55 | GaClient.clear_service_account_user
56 | GaClient.service_account_user(self)
57 | GaClient.get_daily_visits
58 | rescue
59 | GaClient.clear_service_account_user
60 | errors.add(:api_email, 'GA API 身份校验失败!')
61 | end
62 | end
63 | end
--------------------------------------------------------------------------------
/app/models/page.rb:
--------------------------------------------------------------------------------
1 | class Page < ActiveRecord::Base
2 | include HasSlug
3 |
4 | before_save :fill_html_content
5 | after_create :set_sid
6 |
7 | validates :title, :length => {:in => 2..10}
8 | validates :content, :length => {:in => 3..100000}
9 |
10 | has_many :attaches, :as => :parent, :dependent => :destroy
11 |
12 | #将markup的content转换为html并写入字段
13 | def fill_html_content
14 | self.html_content = Klog2::Markdown.render(self.content)
15 | end
16 |
17 | def set_sid
18 | self.update_column(:sid, id)
19 | end
20 |
21 | #向上
22 | def up
23 | above_record = Page.where("sid < ?", self.sid).order('sid ASC').last
24 | return if above_record.nil?
25 | tmp_id = self.sid
26 | self.update_column(:sid, above_record.sid)
27 | above_record.update_column(:sid, tmp_id)
28 | end
29 |
30 | #向下
31 | def down
32 | under_record = Page.where("sid > ?", self.sid).order('sid ASC').first
33 | return if under_record.nil?
34 | tmp_id = self.sid
35 | self.update_column(:sid, under_record.sid)
36 | under_record.update_column(:sid, tmp_id)
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/app/models/password.rb:
--------------------------------------------------------------------------------
1 | class Password
2 | include ActiveModel::Model
3 |
4 | attr_accessor :old_pw, :new_pw, :new_pw_confirmation
5 |
6 | validate :valid_old
7 | validates :new_pw, :length => {:minimum => 6}, :confirmation => true
8 | validates :new_pw_confirmation, :presence => true
9 |
10 | # 保存
11 | def save
12 | if valid?
13 | Password.reset(new_pw)
14 | return true
15 | else
16 | return false
17 | end
18 | end
19 |
20 | # 校验旧密码
21 | def valid_old
22 | errors.add(:old_pw, "旧密码错误!") unless Password.valid? old_pw
23 | end
24 |
25 | # 判断密码是否正确
26 | def self.valid?(pass)
27 | Digest::SHA1.hexdigest(Setting.admin_pass_salt + pass) == Setting.admin_pass
28 | end
29 |
30 | # salt
31 | def self.reset(pass)
32 | Setting.admin_pass_salt = SecureRandom.hex(10)
33 | Setting.admin_pass = Digest::SHA1.hexdigest(Setting.admin_pass_salt + pass)
34 | end
35 | end
--------------------------------------------------------------------------------
/app/models/setting.rb:
--------------------------------------------------------------------------------
1 | class Setting < ActiveRecord::Base
2 | class SettingNotFound < RuntimeError;
3 | end
4 |
5 | class_attribute :defaults
6 |
7 | #get or set a variable with the variable as the called method
8 | def self.method_missing(method, *args)
9 | method_name = method.to_s
10 | super(method, *args)
11 |
12 | rescue NoMethodError
13 | #set a value for a variable
14 | if method_name =~ /=$/
15 | key = method_name.gsub('=', '')
16 | value = args.first
17 | self[key] = value
18 |
19 | #retrieve a value
20 | else
21 | self[method_name]
22 | end
23 | end
24 |
25 | def self.all_vars(*keys)
26 | vars = Setting.all
27 | vars = vars.where(:key => keys) if keys.present?
28 |
29 | result = {}
30 | vars.each do |var|
31 | result[var.key] = var.value
32 | end
33 | result.with_indifferent_access
34 | end
35 |
36 | #destroy the specified settings record
37 | def self.destroy(key)
38 | if var = Setting.find_by(:key => key)
39 | var.destroy
40 | true
41 | else
42 | raise SettingNotFound, "Setting variable \"#{key}\" not found"
43 | end
44 | end
45 |
46 | #retrieve a setting value by [] notation
47 | def self.[](key)
48 | if var = Setting.find_by(:key => key)
49 | var.value
50 | elsif Setting.defaults and Setting.defaults[key.to_s]
51 | defaults[key.to_s]
52 | else
53 | nil
54 | end
55 | end
56 |
57 | #set a setting value by [] notation
58 | def self.[]=(key, value)
59 | record = Setting.find_or_create_by(:key => key.to_s)
60 | record.value = value
61 | record.save
62 | end
63 |
64 | def value
65 | # JSON.parse 无法处理 JSON 原始类型,hack it!
66 | JSON.parse("[#{self[:value]}]", :object_class => OpenStruct).first
67 | end
68 |
69 | # 使用 JSON 保存值
70 | def value=(new_value)
71 | new_value = new_value.marshal_dump if new_value.is_a? OpenStruct
72 | new_value.map! { |o| (o.is_a? OpenStruct) ? o.marshal_dump : o } if new_value.is_a? Array
73 | self[:value] = new_value.to_json
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/app/models/website.rb:
--------------------------------------------------------------------------------
1 | class Website
2 | include ActiveModel::Model
3 |
4 | PUBLIC_FIELDS = [:title, :sub_title, :author, :avatar, :avatar_id, :github, :weibo, :donate, :ga]
5 |
6 | attr_accessor *PUBLIC_FIELDS
7 |
8 | validates :title, :presence => true
9 | validates :author, :presence => true
10 |
11 | # 获取实例
12 | def self.find
13 | Website.new Setting.website.marshal_dump
14 | end
15 |
16 | # 保存
17 | def update_attributes(attributes={})
18 | attributes.each do |name, value|
19 | send("#{name}=", value)
20 | end
21 | if valid?
22 | Setting.website = as_hash
23 | return true
24 | else
25 | return false
26 | end
27 | end
28 |
29 | def as_hash
30 | hash = {}
31 | PUBLIC_FIELDS.each { |f| hash[f] = send(f) }
32 | hash
33 | end
34 |
35 | def id
36 | nil
37 | end
38 | end
--------------------------------------------------------------------------------
/app/uploaders/attach_uploader.rb:
--------------------------------------------------------------------------------
1 | class AttachUploader < CarrierWave::Uploader::Base
2 | include CarrierWave::MimeTypes
3 | include CarrierWave::MiniMagick
4 |
5 | # 扩展名限制
6 | IMAGE_EXTENSIONS = %w(jpg jpeg gif png)
7 | DOCUMENT_EXTENSIONS = %w(pdf ppt pptx rar zip txt p12)
8 |
9 | # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility:
10 | # include Sprockets::Helpers::RailsHelper
11 | # include Sprockets::Helpers::IsolatedHelper
12 |
13 | # Choose what kind of storage to use for this uploader:
14 | storage :file
15 |
16 | # Override the directory where uploaded files will be stored.
17 | # This is a sensible default for uploaders that are meant to be mounted:
18 | def store_dir
19 | #"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
20 | "uploads/#{model.class.to_s.underscore}"
21 | end
22 |
23 | # Provide a default URL as a default if there hasn't been a file uploaded:
24 | # def default_url
25 | # # For Rails 3.1+ asset pipeline compatibility:
26 | # # asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
27 | #
28 | # "/images/fallback/" + [version_name, "default.png"].compact.join('_')
29 | # end
30 |
31 | # Process files as they are uploaded:
32 | # process :scale => [200, 300]
33 | #
34 | # def scale(width, height)
35 | # # do something
36 | # end
37 |
38 | process :set_content_type
39 |
40 | version :thumb, :if=>:image? do |file|
41 | process :resize
42 | end
43 |
44 | # 根据model对应的max_width和max_height属性调整图片尺寸
45 | # 默认情况下max_width为700
46 | def resize
47 | width = model.max_width || 700
48 | height = model.max_height || nil
49 | manipulate! do |img|
50 | img.resize "#{width}x#{height}>"
51 | img = yield(img) if block_given?
52 | img
53 | end
54 | end
55 |
56 |
57 | # Create different versions of your uploaded files:
58 | #version :thumb, :if=>:is_image? do
59 | # process :resize_to_limit => [200, 200]
60 | #end
61 |
62 | # Add a white list of extensions which are allowed to be uploaded.
63 | # For images you might use something like this:
64 | def extension_white_list
65 | IMAGE_EXTENSIONS + DOCUMENT_EXTENSIONS
66 | end
67 |
68 | # Override the filename of the uploaded files:
69 | # Avoid using model.id or version_name here, see uploader/store.rb for details.
70 | def filename
71 | if super.present?
72 | # current_path 是 Carrierwave 上传过程临时创建的一个文件,有时间标记,所以它将是唯一的
73 | @name ||= Digest::MD5.hexdigest(File.dirname(current_path))
74 | "#{@name}.#{file.extension.downcase}"
75 | end
76 | end
77 |
78 | protected
79 |
80 | def image?(file)
81 | return file.content_type.include?('image')
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/app/views/admin/attaches/_show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! attach, :id, :file_name, :created_at, :parent_type
2 | json.file_size number_to_human_size(attach.file_size)
3 | json.parent do
4 | json.title attach.parent.title
5 | if attach.parent.is_a? Blog
6 | json.url blog_path(attach.parent.slug)
7 | else
8 | json.url page_path(attach.parent.slug)
9 | end
10 | end if attach.parent
11 | json.is_image attach.image?
12 | json.file attach.file.serializable_hash
13 | json.url attach.image? ? attach.file.thumb.url : attach.file.url
--------------------------------------------------------------------------------
/app/views/admin/attaches/create.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! 'admin/attaches/show', :attach => @attach
--------------------------------------------------------------------------------
/app/views/admin/attaches/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.page do
2 | json.current @attaches.current_page
3 | json.total @attaches.total_pages
4 | json.hasNext !@attaches.last_page?
5 | json.totalCount @attaches.total_count
6 | end
7 | json.array do
8 | json.array! @attaches do |attach|
9 | json.partial! 'admin/attaches/show', :attach => attach
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/admin/blogs/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.page do
2 | json.current @blogs.current_page
3 | json.total @blogs.total_pages
4 | json.hasNext !@blogs.last_page?
5 | json.totalCount @blogs.total_count
6 | end
7 | json.array do
8 | json.array! @blogs do |blog|
9 | json.extract! blog, :id, :title, :comment_count, :created_at, :slug
10 | json.publish blog.publish?
11 | json.category blog.category, :name if blog.category.present?
12 | end
13 | end
--------------------------------------------------------------------------------
/app/views/admin/blogs/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! @blog, :id, :title, :comment_count, :created_at, :slug, :tag_list, :category_id
2 | json.status @blog.status.value
3 | json.publish @blog.publish?
4 | json.attaches @blog.attaches.each do |attach|
5 | json.partial! 'admin/attaches/show', :attach => attach
6 | end
7 | json.category @blog.category, :name if @blog.category.present?
8 | json.extract! @blog, :content, :html_content if params[:detail]
9 | json.errors @blog.errors if @blog.errors
--------------------------------------------------------------------------------
/app/views/admin/categories/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.array! @categories do |category|
2 | json.extract! category, :id, :name, :blog_count, :created_at
3 | end
--------------------------------------------------------------------------------
/app/views/admin/categories/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! @category, :id, :name, :blog_count, :created_at
2 | json.errors @category.errors if @category.errors
--------------------------------------------------------------------------------
/app/views/admin/comments/context.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.array! @comments do |comment|
2 | json.content simple_format(comment.content)
3 | json.extract! comment, :id, :author_name, :author_email, :author_avatar, :ip, :created_at
4 | end
--------------------------------------------------------------------------------
/app/views/admin/comments/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.cursor @comments.cursor
2 | json.array do
3 | json.array! @comments do |comment|
4 | json.content simple_format(comment.content)
5 | json.extract! comment, :id, :author_name, :author_email, :author_avatar, :ip, :created_at
6 | json.blog comment.blog, :id, :title, :slug if comment.blog
7 | end
8 | end
--------------------------------------------------------------------------------
/app/views/admin/comments/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.content simple_format(@comment.content)
2 | json.extract! @comment, :id, :author_name, :author_email, :author_avatar, :ip, :created_at
3 | json.blog @comment.blog, :id, :title, :slug if @comment.blog
--------------------------------------------------------------------------------
/app/views/admin/home/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | <%= stylesheet_link_tag "admin", media: "all" %>
11 |
12 | <%= javascript_include_tag "xhr-shim.js" %>
13 | <%= seajs_tag %>
14 |
15 |
29 |
34 |
35 |
36 |
37 |
38 |
41 |
49 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
69 | <%= seajs_use 'admin/app' %>
70 |
71 |
72 |
--------------------------------------------------------------------------------
/app/views/admin/pages/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.array! @pages do |page|
2 | json.extract! page, :id, :title, :created_at, :slug, :sid
3 | end
--------------------------------------------------------------------------------
/app/views/admin/pages/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! @page, :id, :title, :created_at, :slug
2 | json.attaches @page.attaches.each do |attach|
3 | json.partial! 'admin/attaches/show', :attach => attach
4 | end
5 | json.extract! @page, :html_content, :content if params[:detail]
6 | json.errors @page.errors if @page.errors
--------------------------------------------------------------------------------
/app/views/admin/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Klog管理后台
5 |
6 | <%= stylesheet_link_tag 'admin' %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 | <%= form_tag admin_session_path, :class => "form-inline session-form" do -%>
11 | <% if flash[:error] %>
12 |
13 | <%= flash[:error] %>
14 |
15 | <% end %>
16 |
17 | <%= password_field_tag 'password', nil, :class => 'form-control input-lg', :autofocus => true, :required => true, :placeholder=>"Enter password..." %>
18 |
19 | Login
20 |
21 |
22 | <% end -%>
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/views/archive/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% @blogs_by_year.each do |year, blogs| %>
3 |
4 |
<%= year %>
5 |
6 | <% blogs.each do |blog| %>
7 |
8 | <%= link_to blog.title, blog_path(blog.slug) %>
9 | <%= l blog.created_at, :format=>'%m-%d' %>
10 |
11 | <% end %>
12 |
13 |
14 | <% end %>
15 |
16 |
17 | <% content_for :title do %>Archives<% end %>
--------------------------------------------------------------------------------
/app/views/blogs/index.html.erb:
--------------------------------------------------------------------------------
1 | <% if controller_name == 'tags' %>
2 |
3 | <%= fa_icon "tag" %> <%= params[:id] %>
4 |
5 | <% content_for :title do %><%= "Tag - #{params[:id]}" %><% end %>
6 | <% end %>
7 |
8 | <% if controller_name == 'categories' %>
9 |
10 | <%= fa_icon "book" %> <%= params[:id] %>
11 |
12 | <% content_for :title do %><%= "分类 - #{params[:id]}" %><% end %>
13 | <% end %>
14 |
15 | <% @blogs.each do |blog| %>
16 |
17 |
18 |
19 | <%= fa_icon "comments" %>
20 | <%= blog.comment_count %>条评论
21 |
22 |
23 | <%= fa_icon "calendar-o" %>
24 | <%= l blog.created_at %>
25 |
26 |
27 | <%= link_to blog.title, blog_path(blog.slug) %>
28 |
29 |
30 | <% if blog.category.present? %>
31 | <%= link_to fa_icon("book") + ' ' + blog.category.name, category_path(blog.category.name), :class => 'item' %>
32 | <% end %>
33 | <% if blog.tags.present? %>
34 | <% blog.tags.each do |tag| %>
35 | <%= link_to fa_icon("tag") + ' ' +tag.name, tag_path(tag.name), :class => 'item' %>
36 | <% end %>
37 | <% end %>
38 |
39 |
40 | <%= raw blog.html_content_summary %>
41 |
42 |
43 | <%= link_to fa_icon("arrow-right") + " 阅读全文...", blog_path(blog.slug), :class => 'readmore' %>
44 |
45 |
46 | <% end %>
47 |
48 | <% if @blogs.num_pages > 1 %>
49 |
50 | <%= paginate @blogs, :theme=>'tiny' %>
51 |
52 | <% end %>
--------------------------------------------------------------------------------
/app/views/blogs/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= fa_icon "comments" %>
5 | <%= @blog.comment_count %>条评论
6 |
7 |
8 | <%= fa_icon "calendar-o" %>
9 | <%= l @blog.created_at %>
10 |
11 |
12 | <%= @blog.title %>
13 |
14 |
15 | <% if @blog.category.present? %>
16 | <%= link_to fa_icon("book") + ' ' + @blog.category.name, category_path(@blog.category.name), :class => 'pull-left item' %>
17 | <% end %>
18 | <% if @blog.tag_list.present? %>
19 | <% @blog.tags.each do |tag| %>
20 | <%= link_to fa_icon("tag") + ' ' + tag.name, tag_path(tag.name), :class => 'pull-left item' %>
21 | <% end %>
22 | <% end %>
23 |
24 |
25 | <%= raw @blog.html_content %>
26 |
27 |
28 |
29 | <%= link_to '« '+@prev_blog.title, blog_path(@prev_blog.slug), :class => 'pull-left' if @prev_blog.present? %>
30 | <%= link_to @next_blog.title + ' »', blog_path(@next_blog.slug), :class => 'pull-right' if @next_blog.present? %>
31 |
32 |
33 | <% if Setting.disqus.try(:enable) and Setting.disqus.try(:shortname).try(:present?) and !@preview %>
34 |
57 | <% end %>
58 |
59 |
60 | <% content_for :title do %><%= @blog.title %><% end %>
61 |
62 | <% content_for :seo do %>
63 |
64 |
65 | <% end %>
--------------------------------------------------------------------------------
/app/views/feed/show.rss.builder:
--------------------------------------------------------------------------------
1 | xml.instruct! :xml, :version => "1.0"
2 | xml.rss :version => "2.0" do
3 | xml.channel do
4 | xml.title # TODO Setting.website_title
5 | xml.description #Setting.website_descr
6 | xml.link root_url
7 |
8 | @blogs.each do |blog|
9 | xml.item do
10 | xml.title blog.title
11 | xml.description do
12 | xml.cdata! blog.html_content
13 | end
14 | xml.pubDate blog.created_at.to_s(:rfc822)
15 | xml.link blog_url(blog.slug)
16 | xml.guid blog_url(blog.slug)
17 | end
18 | end
19 | end
20 | end
--------------------------------------------------------------------------------
/app/views/kaminari/_first_page.html.erb:
--------------------------------------------------------------------------------
1 | <%# Link to the "First" page
2 | - available local variables
3 | url: url to the first page
4 | current_page: a page object for the currently displayed page
5 | total_pages: total number of pages
6 | per_page: number of items to fetch per page
7 | remote: data-remote
8 | -%>
9 |
10 | <%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, :remote => remote %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/kaminari/_gap.html.erb:
--------------------------------------------------------------------------------
1 | <%# Non-link tag that stands for skipped pages...
2 | - available local variables
3 | current_page: a page object for the currently displayed page
4 | total_pages: total number of pages
5 | per_page: number of items to fetch per page
6 | remote: data-remote
7 | -%>
8 | <%= t('views.pagination.truncate').html_safe %>
9 |
--------------------------------------------------------------------------------
/app/views/kaminari/_last_page.html.erb:
--------------------------------------------------------------------------------
1 | <%# Link to the "Last" page
2 | - available local variables
3 | url: url to the last page
4 | current_page: a page object for the currently displayed page
5 | total_pages: total number of pages
6 | per_page: number of items to fetch per page
7 | remote: data-remote
8 | -%>
9 |
10 | <%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, :remote => remote %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/kaminari/_next_page.html.erb:
--------------------------------------------------------------------------------
1 | <%# Link to the "Next" page
2 | - available local variables
3 | url: url to the next page
4 | current_page: a page object for the currently displayed page
5 | total_pages: total number of pages
6 | per_page: number of items to fetch per page
7 | remote: data-remote
8 | -%>
9 |
10 | <%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, :rel => 'next', :remote => remote %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/kaminari/_page.html.erb:
--------------------------------------------------------------------------------
1 | <%# Link showing page number
2 | - available local variables
3 | page: a page object for "this" page
4 | url: url to this page
5 | current_page: a page object for the currently displayed page
6 | total_pages: total number of pages
7 | per_page: number of items to fetch per page
8 | remote: data-remote
9 | -%>
10 |
11 | <%= link_to_unless page.current?, page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} %>
12 |
13 |
--------------------------------------------------------------------------------
/app/views/kaminari/_paginator.html.erb:
--------------------------------------------------------------------------------
1 | <%# The container tag
2 | - available local variables
3 | current_page: a page object for the currently displayed page
4 | total_pages: total number of pages
5 | per_page: number of items to fetch per page
6 | remote: data-remote
7 | paginator: the paginator that renders the pagination tags inside
8 | -%>
9 | <%= paginator.render do -%>
10 |
23 | <% end -%>
24 |
--------------------------------------------------------------------------------
/app/views/kaminari/_prev_page.html.erb:
--------------------------------------------------------------------------------
1 | <%# Link to the "Previous" page
2 | - available local variables
3 | url: url to the previous page
4 | current_page: a page object for the currently displayed page
5 | total_pages: total number of pages
6 | per_page: number of items to fetch per page
7 | remote: data-remote
8 | -%>
9 |
10 | <%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, :rel => 'prev', :remote => remote %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/kaminari/tiny/_next_page.html.erb:
--------------------------------------------------------------------------------
1 | <%# Link to the "Next" page
2 | - available local variables
3 | url: url to the next page
4 | current_page: a page object for the currently displayed page
5 | num_pages: total number of pages
6 | per_page: number of items to fetch per page
7 | remote: data-remote
8 | -%>
9 |
10 | <%= link_to t('views.pagination.next').html_safe, url, :rel => 'next', :remote => remote %>
11 |
--------------------------------------------------------------------------------
/app/views/kaminari/tiny/_paginator.html.erb:
--------------------------------------------------------------------------------
1 | <%# The container tag
2 | - available local variables
3 | current_page: a page object for the currently displayed page
4 | num_pages: total number of pages
5 | per_page: number of items to fetch per page
6 | remote: data-remote
7 | paginator: the paginator that renders the pagination tags inside
8 | -%>
9 | <%= paginator.render do -%>
10 |
14 | <% end -%>
15 |
--------------------------------------------------------------------------------
/app/views/kaminari/tiny/_prev_page.html.erb:
--------------------------------------------------------------------------------
1 | <%# Link to the "Previous" page
2 | - available local variables
3 | url: url to the previous page
4 | current_page: a page object for the currently displayed page
5 | num_pages: total number of pages
6 | per_page: number of items to fetch per page
7 | remote: data-remote
8 | -%>
9 |
10 | <%= link_to t('views.pagination.previous').html_safe, url, :rel => 'prev', :remote => remote %>
11 |
--------------------------------------------------------------------------------
/app/views/layouts/_ga.html.erb:
--------------------------------------------------------------------------------
1 | <% if Setting.ga.account and !@preview %>
2 | <% if ga == :head %>
3 |
15 | <% elsif ga == :foot %>
16 |
19 | <% end %>
20 | <% end %>
--------------------------------------------------------------------------------
/app/views/layouts/_header.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if Setting.website.avatar.present? %>
3 |
4 |
5 |
6 | <% end %>
7 |
8 | <%= Setting.website.title %>
9 |
10 | <%= simple_format Setting.website.sub_title %>
11 |
12 |
13 | <%= website_link :weibo, :icon=>"weibo", :title => "新浪微博" %>
14 | <%= website_link :github, :url=> "https://github.com/#{Setting.website.github}", :icon=>"github", :title => "Github" %>
15 | <%= website_link :donate, :icon=>"coffee", :title => "打赏咖啡一杯" %>
16 | <%= link_to fa_icon("rss-square"), feed_path(:format=>:rss), :target => '_blank', :title => 'RSS' %>
17 |
18 |
--------------------------------------------------------------------------------
/app/views/layouts/public.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= title (yield :title) %>
5 |
6 |
7 | <%= yield :seo %>
8 |
9 |
10 |
11 | <%= auto_discovery_link_tag(:rss, feed_path(:format => :rss), :title => Setting.website.title) %>
12 |
13 | <%= stylesheet_link_tag "public", media: "all", "data-turbolinks-track" => true %>
14 | <% if os.mac? %>
15 |
20 | <% end %>
21 | <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
22 |
23 |
27 |
28 | <%= render :partial => 'layouts/ga', :object => :head %>
29 |
30 |
31 |
32 |
33 | <%= render "layouts/header" %>
34 |
35 | <%= render_cell :nav, :show, @curr_nav %>
36 |
37 |
38 | <%= yield %>
39 |
40 | Copyright © 2014 - Chaos - Powered by Klog2
41 |
42 |
43 |
44 | <%= render :partial => 'layouts/ga', :object => :foot %>
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/views/pages/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= raw @page.html_content %>
3 |
4 |
5 | <% content_for :title do %><%= @page.title %><% end %>
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 | require 'unicorn/oob_gc'
3 | require 'unicorn/worker_killer'
4 | # 每10次请求,才执行一次GC
5 | use Unicorn::OobGC, 10
6 | # 设定最大请求次数后自杀,避免禁止GC带来的内存泄漏(3072~4096之间随机,避免同时多个进程同时自杀,可以和下面的设定任选)
7 | use Unicorn::WorkerKiller::MaxRequests, 3072, 4096
8 | # 设定达到最大内存后自杀,避免禁止GC带来的内存泄漏(80~128MB之间随机,避免同时多个进程同时自杀)
9 | use Unicorn::WorkerKiller::Oom, (80*(1024**2)), (128*(1024**2))
10 |
11 | require ::File.expand_path('../config/environment', __FILE__)
12 | run Rails.application
13 |
14 | memory_usage = (`ps -o rss= -p #{$$}`.to_i / 1024.00).round(2)
15 | puts "=> Memory usage: #{memory_usage} Mb"
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(:default, Rails.env)
8 |
9 | module Klog2
10 | class Application < Rails::Application
11 | # Settings in config/environments/* take precedence over those specified here.
12 | # Application configuration should go into files in config/initializers
13 | # -- all .rb files in that directory are automatically loaded.
14 |
15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
17 | # config.time_zone = 'Central Time (US & Canada)'
18 | config.time_zone = 'Beijing'
19 |
20 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
21 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
22 | # config.i18n.default_locale = :de
23 | config.i18n.enforce_available_locales = true
24 | config.i18n.default_locale = "zh-CN"
25 |
26 | config.autoload_paths += %W(#{config.root}/lib)
27 |
28 | config.assets.precompile += %w(admin.css public.css)
29 | config.assets.precompile += %w(html5shiv.js respond.js xhr-shim.js)
30 |
31 | config.after_initialize do
32 | Setting.defaults = {
33 | "website" => OpenStruct.new(
34 | :title => "网站名称",
35 | :sub_title => "一点点简介写在这里",
36 | :author => "蛇精病"
37 | ),
38 | "disqus" => OpenStruct.new(:enable => false),
39 | "ga" => OpenStruct.new(:chart_enable => false)
40 | }
41 |
42 | RestClient.log = Rails.logger
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 |
4 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
5 |
--------------------------------------------------------------------------------
/config/compass.rb:
--------------------------------------------------------------------------------
1 | # Require any additional compass plugins here.
2 | project_type = :rails
3 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # MySQL. Versions 4.1 and 5.0 are recommended.
2 | #
3 | # Install the MYSQL driver
4 | # gem install mysql2
5 | #
6 | # Ensure the MySQL gem is defined in your Gemfile
7 | # gem 'mysql2'
8 | #
9 | # And be sure to use new-style password hashing:
10 | # http://dev.mysql.com/doc/refman/5.0/en/old-client.html
11 | development:
12 | adapter: mysql2
13 | encoding: utf8
14 | database: klog2_development
15 | pool: 5
16 | username: root
17 | password: root
18 |
19 | # Warning: The database defined as "test" will be erased and
20 | # re-generated from your development database when you run "rake".
21 | # Do not set this db to the same as development or production.
22 | test:
23 | adapter: mysql2
24 | encoding: utf8
25 | database: klog2_test
26 | pool: 5
27 | username: root
28 | password: root
29 |
30 | production:
31 | adapter: mysql2
32 | encoding: utf8
33 | database: klog2_production
34 | pool: 5
35 | username: root
36 | password: root
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Klog2::Application.initialize!
6 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Klog2::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 | config.reload_classes_only_on_change = true
9 |
10 | # Do not eager load code on boot.
11 | config.eager_load = false
12 |
13 | # Show full error reports and disable caching.
14 | config.consider_all_requests_local = true
15 | config.action_controller.perform_caching = false
16 |
17 | # Don't care if the mailer can't send.
18 | config.action_mailer.raise_delivery_errors = false
19 |
20 | # Print deprecation notices to the Rails logger.
21 | config.active_support.deprecation = :log
22 |
23 | # Raise an error on page load if there are pending migrations
24 | config.active_record.migration_error = :page_load
25 |
26 | # Debug mode disables concatenation and preprocessing of assets.
27 | # This option may cause significant delays in view rendering with a large
28 | # number of complex assets.
29 | config.assets.debug = true
30 | end
31 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Klog2::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static asset server for tests with Cache-Control for performance.
16 | config.serve_static_assets = true
17 | config.static_cache_control = "public, max-age=3600"
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Print deprecation notices to the stderr.
35 | config.active_support.deprecation = :stderr
36 | end
37 |
--------------------------------------------------------------------------------
/config/initializers/acts_as_taggable_on.rb:
--------------------------------------------------------------------------------
1 | ActsAsTaggableOn.remove_unused_tags = true
2 | ActsAsTaggableOn.delimiter = " "
3 | ActsAsTaggableOn.force_lowercase = true
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | #inflect.plural /^(ox)$/i, '\1en'
8 | #inflect.singular /^(ox)en/i, '\1'
9 | #inflect.irregular 'person', 'people'
10 | inflect.uncountable %w(disqus ga)
11 | end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
--------------------------------------------------------------------------------
/config/initializers/markdown.rb:
--------------------------------------------------------------------------------
1 | # markdown语法支持
2 | module Klog2
3 | # 格式化markdown语法为html的类
4 | class Markdown
5 |
6 | def self.render(text)
7 | return self.instance.render(text)
8 | end
9 |
10 | def self.instance
11 | @markdown ||= Redcarpet::Markdown.new(Klog2::Render.new,
12 | :autolink => true,
13 | :fenced_code_blocks => true,
14 | :disable_indented_code_blocks => true,
15 | :strikethrough => true,
16 | :space_after_headers => true,
17 | :superscript => true,
18 | :footnotes => true,
19 | :no_intra_emphasis => true,
20 | :tables => true)
21 | return @markdown
22 | end
23 |
24 | end
25 |
26 | # 格式化定制
27 | class Render < Redcarpet::Render::HTML
28 | def initialize(extensions={})
29 | super(extensions.merge(:xhtml => true,
30 | :with_toc_data => true,
31 | :no_styles => true,
32 | :filter_html => true,
33 | :link_attributes => {:target => "_blank", :rel => "nofollow"},
34 | :hard_wrap => true))
35 | end
36 |
37 | def header(text, header_level)
38 | tag = 'h' + (header_level + 2).to_s
39 | tag_start = "<#{tag}>"
40 | tag_end = "#{tag}>"
41 | return tag_start + text + tag_end
42 | end
43 |
44 | def block_code(code, language)
45 | language = language || 'unknown'
46 | # 加上 HTML 注释是为了不让 truncate_html 把代码片段切开,参见 :break_token 参数
47 | '' + CodeRay.scan(code, language).div(:tab_width => 2)
48 | end
49 |
50 | def table(header, body)
51 | return ""
52 | end
53 | end
54 | end
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/config/initializers/rest_client.rb:
--------------------------------------------------------------------------------
1 | # RestClient 全局 timeout
2 | class << ::RestClient::Request
3 | def execute_with_timeout(args, &block)
4 | args[:timeout] = 10
5 | args[:open_timeout] = 10
6 | execute_without_timeout(args, &block)
7 | end
8 |
9 | alias_method_chain :execute, :timeout
10 | end
--------------------------------------------------------------------------------
/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure your secret_key_base is kept private
11 | # if you're sharing your code publicly.
12 | Klog2::Application.config.secret_key_base = '13f879a77cb72bc672eb9eb52d6811a46d9ecb33474588754c5552e3333f1e59d193afc197d2123e7b7b2a3be754a6a8adc803e0d6eaa519b9cc0209c9358321'
13 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Klog2::Application.config.session_store :cookie_store, key: '_klog_session'
4 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Klog2::Application.routes.draw do
2 | # The priority is based upon order of creation: first created -> highest priority.
3 | # See how all your routes lay out with "rake routes".
4 |
5 | # You can have the root of your site routed with "root"
6 | # root 'welcome#index'
7 |
8 | root 'blogs#index'
9 |
10 | get 'blog/:id.html' => 'blogs#show', :as => :blog
11 | get 'blog/page/:page' => 'blogs#index'
12 | post 'blog/preview' => 'blogs#preview'
13 |
14 | resources :categories
15 | resources :tags
16 | get 'categories/:id/page/:page' => 'categories#show'
17 | get 'tags/:id/page/:page' => 'tags#show'
18 |
19 | get '/feed' => 'feed#show', :as => :feed, :defaults => {:format => 'rss'}
20 | get '/archive.html' => 'archive#show', :as => :archive
21 | get '/page/:id.html' => 'pages#show', :as => :page
22 |
23 | namespace :admin do
24 | get '/' => 'home#show'
25 | get '/dashboard' => 'dashboard#show'
26 | post '/dashboard/sync_comment' => 'dashboard#sync_comment'
27 | get '/dashboard/:action' => 'dashboard'
28 |
29 | resource :session
30 |
31 | resources :attaches
32 | resources :blogs do
33 | post 'publish', :on => :member
34 | end
35 | resources :categories
36 | resources :comments do
37 | get 'context', :on => :collection
38 | end
39 | resources :pages do
40 | member do
41 | post 'up'
42 | post 'down'
43 | end
44 | end
45 | resource :website
46 | resource :password
47 | resource :ga
48 | resource :disqus do
49 | put 'enable'
50 | end
51 | end
52 |
53 | # Example of regular route:
54 | # get 'products/:id' => 'catalog#view'
55 |
56 | # Example of named route that can be invoked with purchase_url(id: product.id)
57 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
58 |
59 | # Example resource route (maps HTTP verbs to controller actions automatically):
60 | # resources :products
61 |
62 | # Example resource route with options:
63 | # resources :products do
64 | # member do
65 | # get 'short'
66 | # post 'toggle'
67 | # end
68 | #
69 | # collection do
70 | # get 'sold'
71 | # end
72 | # end
73 |
74 | # Example resource route with sub-resources:
75 | # resources :products do
76 | # resources :comments, :sales
77 | # resource :seller
78 | # end
79 |
80 | # Example resource route with more complex sub-resources:
81 | # resources :products do
82 | # resources :comments
83 | # resources :sales do
84 | # get 'recent', on: :collection
85 | # end
86 | # end
87 |
88 | # Example resource route with concerns:
89 | # concern :toggleable do
90 | # post 'toggle'
91 | # end
92 | # resources :posts, concerns: :toggleable
93 | # resources :photos, concerns: :toggleable
94 |
95 | # Example resource route within a namespace:
96 | # namespace :admin do
97 | # # Directs /admin/products/* to Admin::ProductsController
98 | # # (app/controllers/admin/products_controller.rb)
99 | # resources :products
100 | # end
101 | end
102 |
--------------------------------------------------------------------------------
/config/seajs_config.yml:
--------------------------------------------------------------------------------
1 | seajs_path: seajs/seajs/2.1.1/sea.js
2 | family: klog
3 | output:
4 | relative:
5 | - admin/app.js
6 | - admin/blog/index.js:
7 | - admin/blog/index.js
8 | - admin/blog/template/*.html.js
9 | - admin/blog-form/index.js:
10 | - admin/blog-form/index.js
11 | - admin/blog-form/template/*.html.js
12 | - admin/comment/index.js:
13 | - admin/comment/index.js
14 | - admin/comment/template/*.html.js
15 | - admin/editor/index.js:
16 | - admin/editor/index.js
17 | - admin/editor/template/*.html.js
18 | - admin/page/index.js:
19 | - admin/page/index.js
20 | - admin/page/template/*.html.js
21 | - admin/setting/index.js:
22 | - admin/setting/index.js
23 | - admin/setting/template/*.html.js
24 | - admin/dashboard/index.js:
25 | - admin/dashboard/index.js
26 | - admin/dashboard/template/*.html.js
27 | all: []
28 | alias:
29 | angular-all: angular-all
30 | angularjs: angularjs
31 | _: _
32 | selection: selection
33 | seajs-lazy-angular: seajs-lazy-angular
34 | marked: marked
35 | bootstrap: bootstrap
36 | angular-highcharts: angular-highcharts
--------------------------------------------------------------------------------
/config/unicorn.rb:
--------------------------------------------------------------------------------
1 | worker_processes 4
2 | timeout 30
3 |
4 | app_root = File.expand_path("../..", __FILE__)
5 | working_directory app_root
6 | preload_app true
7 |
8 | listen 8080, :tcp_nopush => false
9 | listen "/tmp/unicorn.klog.sock", :backlog => 64
10 |
11 | pid "#{app_root}/tmp/pids/unicorn.pid"
12 | stderr_path "#{app_root}/log/unicorn.log"
13 | stdout_path "#{app_root}/log/unicorn.log"
14 |
15 | if GC.respond_to?(:copy_on_write_friendly=)
16 | GC.copy_on_write_friendly = true
17 | end
18 |
19 | before_exec do |_|
20 | ENV["BUNDLE_GEMFILE"] = File.join(app_root, 'Gemfile')
21 | end
22 |
23 | before_fork do |server, worker|
24 | # 参考 http://unicorn.bogomips.org/SIGNALS.html
25 | # 使用USR2信号,以及在进程完成后用QUIT信号来实现无缝重启
26 | old_pid = "#{app_root}/tmp/pids/unicorn.pid.oldbin"
27 | if File.exists?(old_pid) && server.pid != old_pid
28 | begin
29 | Process.kill("QUIT", File.read(old_pid).to_i)
30 | rescue Errno::ENOENT, Errno::ESRCH
31 | puts "Send 'QUIT' signal to unicorn error!"
32 | end
33 | end
34 |
35 | if defined? ActiveRecord::Base
36 | ActiveRecord::Base.connection.disconnect!
37 | end
38 | end
39 |
40 | after_fork do |server, worker|
41 | # 禁止GC,配合后续的OOB,来减少请求的执行时间
42 | GC.disable
43 | # the following is *required* for Rails + "preload_app true",
44 | if defined?(ActiveRecord::Base)
45 | ActiveRecord::Base.establish_connection
46 | end
47 | end
--------------------------------------------------------------------------------
/db/migrate/20131216081115_create_blog.rb:
--------------------------------------------------------------------------------
1 | class CreateBlog < ActiveRecord::Migration
2 | def change
3 | create_table :blogs do |t|
4 | t.string :title, :null => false
5 | t.text :content, :null => false
6 | t.text :html_content, :null => false
7 | t.text :html_content_summary, :null=>false
8 | t.string :slug, :null => false
9 | t.string :seo_kwd
10 | t.string :seo_desc
11 | t.integer :status
12 | t.integer :category_id
13 | t.integer :comment_count, :default => 0
14 |
15 | t.timestamps
16 | end
17 |
18 | add_index :blogs, :slug, :unique => true
19 | end
20 | end
--------------------------------------------------------------------------------
/db/migrate/20131225085020_create_categories.rb:
--------------------------------------------------------------------------------
1 | class CreateCategories < ActiveRecord::Migration
2 | def change
3 | create_table :categories do |t|
4 | t.string :name, :null => false, :limit => 20
5 | t.integer :blog_count, :default => 0
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20131226010542_acts_as_taggable_on_migration.rb:
--------------------------------------------------------------------------------
1 | class ActsAsTaggableOnMigration < ActiveRecord::Migration
2 | def self.up
3 | create_table :tags do |t|
4 | t.string :name
5 | end
6 |
7 | create_table :taggings do |t|
8 | t.references :tag
9 |
10 | # You should make sure that the column created is
11 | # long enough to store the required class names.
12 | t.references :taggable, :polymorphic => true
13 | t.references :tagger, :polymorphic => true
14 |
15 | # Limit is created to prevent MySQL error on index
16 | # length for MyISAM table type: http://bit.ly/vgW2Ql
17 | t.string :context, :limit => 128
18 |
19 | t.datetime :created_at
20 | end
21 |
22 | add_index :taggings, :tag_id
23 | add_index :taggings, [:taggable_id, :taggable_type, :context]
24 | end
25 |
26 | def self.down
27 | drop_table :taggings
28 | drop_table :tags
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/db/migrate/20131231034743_create_settings.rb:
--------------------------------------------------------------------------------
1 | class CreateSettings < ActiveRecord::Migration
2 | def change
3 | create_table :settings do |t|
4 | t.string :key
5 | t.string :value, :limit=>2000
6 |
7 | t.timestamps
8 | end
9 |
10 | add_index :settings, :key, :unique => true
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20140102062200_create_pages.rb:
--------------------------------------------------------------------------------
1 | class CreatePages < ActiveRecord::Migration
2 | def change
3 | create_table :pages do |t|
4 | t.string :title, :null=>false
5 | t.text :content, :null=>false
6 | t.text :html_content, :null=>false
7 | t.string :slug, :null=>false
8 | t.integer :sid
9 |
10 | t.timestamps
11 | end
12 |
13 | add_index :pages, :slug, :unique => true
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/migrate/20140108025118_create_attaches.rb:
--------------------------------------------------------------------------------
1 | class CreateAttaches < ActiveRecord::Migration
2 | def change
3 | create_table :attaches do |t|
4 | t.string :file, :null=>false
5 | t.string :file_name, :null=>false
6 | t.string :content_type, :null=>false
7 | t.integer :file_size, :null=>false
8 | t.integer :parent_id
9 | t.string :parent_type
10 |
11 | t.timestamps
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/lib/assets/.keep
--------------------------------------------------------------------------------
/lib/disqus_client.rb:
--------------------------------------------------------------------------------
1 | # Disqus API Client
2 | # 返回 Hash 格式数据
3 | class DisqusClient
4 | DSQ_API = "https://disqus.com/api/3.0"
5 | DSQ_API_POSTS = "#{DSQ_API}/forums/listPosts.json"
6 | DSQ_API_THREADS = "#{DSQ_API}/forums/listThreads.json"
7 | DSQ_API_POST_CONTEXT = "#{DSQ_API}/posts/getContext.json"
8 | DSQ_API_REMOVE_POST = "#{DSQ_API}/posts/remove.json"
9 | DSQ_API_CREATE_POST = "#{DSQ_API}/posts/create.json"
10 | DSQ_API_THREAD_DETAIL = "#{DSQ_API}/threads/details.json"
11 |
12 | # 获取评论列表
13 | def self.all_post(cursor="", options={})
14 | resp = RestClient.get DSQ_API_POSTS, {
15 | :params => {
16 | :forum => options[:shortname] || Setting.disqus.shortname,
17 | :related => 'thread',
18 | :limit => 30,
19 | :cursor => cursor,
20 | :api_secret => options[:api_secret] || Setting.disqus.api_secret,
21 | :access_token => options[:access_token] || Setting.disqus.access_token
22 | }
23 | }
24 | hash = JSON.parse(resp.to_s)
25 |
26 | # 返回两个
27 | return hash["response"], hash["cursor"]
28 | end
29 |
30 | # 获取评论上下文
31 | def self.get_context(post_id)
32 | resp = RestClient.get DSQ_API_POST_CONTEXT, {
33 | :params => {
34 | :post => post_id,
35 | :api_secret => Setting.disqus.api_secret,
36 | :access_token => Setting.disqus.access_token
37 | }
38 | }
39 | JSON.parse(resp.to_s)["response"]
40 | end
41 |
42 | # 删除评论
43 | def self.remove_post(post_id)
44 | RestClient.post DSQ_API_REMOVE_POST, {
45 | :post => post_id,
46 | :api_secret => Setting.disqus.api_secret,
47 | :access_token => Setting.disqus.access_token
48 | }
49 | end
50 |
51 | # 创建评论
52 | def self.create_post(comment)
53 | resp = RestClient.post DSQ_API_CREATE_POST, {
54 | :parent => comment.parent,
55 | :message => comment.content,
56 | :api_secret => Setting.disqus.api_secret,
57 | :access_token => Setting.disqus.access_token
58 | }
59 | JSON.parse(resp.to_s)["response"]
60 | end
61 |
62 | # 获取所有的 Thread(blog)
63 | # TODO 等写超过 100 篇就来 Fix
64 | def self.all_thread
65 | resp = RestClient.get DSQ_API_THREADS, {
66 | :params => {
67 | :forum => Setting.disqus.shortname,
68 | :limit => 100,
69 | :api_secret => Setting.disqus.api_secret,
70 | :access_token => Setting.disqus.access_token
71 | }
72 | }
73 | JSON.parse(resp.to_s)["response"]
74 | end
75 |
76 | # 获取某个 Thread 的详情
77 | def self.get_thread(thread_id)
78 | resp = RestClient.get DSQ_API_THREAD_DETAIL, {
79 | :params => {
80 | :thread => thread_id,
81 | :api_secret => Setting.disqus.api_secret,
82 | :access_token => Setting.disqus.access_token
83 | }
84 | }
85 | JSON.parse(resp.to_s)["response"]
86 | end
87 | end
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/lib/tasks/.keep
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/log/.keep
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
48 |
49 |
50 |
51 |
52 |
53 |
The page you were looking for doesn't exist.
54 |
You may have mistyped the address or the page may have moved.
55 |
56 | If you are the application owner check the logs for more information.
57 |
58 |
59 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
48 |
49 |
50 |
51 |
52 |
53 |
The change you wanted was rejected.
54 |
Maybe you tried to change something you didn't have access to.
55 |
56 | If you are the application owner check the logs for more information.
57 |
58 |
59 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
48 |
49 |
50 |
51 |
52 |
53 |
We're sorry, but something went wrong.
54 |
55 | If you are the application owner check the logs for more information.
56 |
57 |
58 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-Agent: *
5 | # Disallow: /
6 | User-agent: *
7 | Disallow: /categories/
8 | Disallow: /tags/
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/test/controllers/.keep
--------------------------------------------------------------------------------
/test/fixtures/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/test/fixtures/.keep
--------------------------------------------------------------------------------
/test/fixtures/attaches.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | file: MyString
5 | file_name: MyString
6 | content_type: MyString
7 | file_size: 1
8 | parent_id: 1
9 | parent_type: MyString
10 |
11 | two:
12 | file: MyString
13 | file_name: MyString
14 | content_type: MyString
15 | file_size: 1
16 | parent_id: 1
17 | parent_type: MyString
18 |
--------------------------------------------------------------------------------
/test/fixtures/categories.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | # This model initially had no columns defined. If you add columns to the
4 | # model remove the '{}' from the fixture names and add the columns immediately
5 | # below each fixture, per the syntax in the comments below
6 | #
7 | one: {}
8 | # column: value
9 | #
10 | two: {}
11 | # column: value
12 |
--------------------------------------------------------------------------------
/test/fixtures/pages.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | title: MyString
5 | content: MyText
6 | :
7 | slug: MyString
8 | sid: 1
9 |
10 | two:
11 | title: MyString
12 | content: MyText
13 | :
14 | slug: MyString
15 | sid: 1
16 |
--------------------------------------------------------------------------------
/test/fixtures/settings.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | key: MyString
5 | value: MyString
6 |
7 | two:
8 | key: MyString
9 | value: MyString
10 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/test/helpers/.keep
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/test/integration/.keep
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/test/mailers/.keep
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/test/models/.keep
--------------------------------------------------------------------------------
/test/models/attach_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class AttachTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/category_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class CategoryTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/page_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class PageTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/setting_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class SettingTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] ||= "test"
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | ActiveRecord::Migration.check_pending!
7 |
8 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
9 | #
10 | # Note: You'll currently still have to declare fixtures explicitly in integration tests
11 | # -- they do not yet inherit this setting
12 | fixtures :all
13 |
14 | # Add more helper methods to be used by all tests here...
15 | end
16 |
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/vendor/assets/javascripts/.keep
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edokeh/klog2/cd935ecc3d12f007d10e3c86ea0948d3744ee925/vendor/assets/stylesheets/.keep
--------------------------------------------------------------------------------