├── .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 | ![](http://chaoskeh.com/uploads/attach/thumb_6cbf819cec6d8d44d7146b1b80373505.jpg) 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 |
2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 35 | 36 | 37 | 38 | 43 | 44 |
6 | 10 |
15 | 17 |
22 | 23 |
28 | 31 | 34 |
39 | 42 |
45 |
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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 = '![](' + url + ')'; 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 | -------------------------------------------------------------------------------- /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 | 28 | 29 | 30 | 34 | 35 |
文件名大小创建时间归属
25 | {{ attach.file_name }} 26 | 删除 27 | {{ attach.file_size }}{{ attach.created_at |date:'yyyy-MM-dd HH:mm' }} 31 | {{attach.parent_type}} 32 | {{ attach.parent.title }} 33 |
36 |
37 | 38 |
39 |
40 |
41 |
42 |
-------------------------------------------------------------------------------- /app/assets/javascripts/admin/setting/template/nav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/admin/setting/template/password.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 |
7 |
8 |
9 |
10 | 11 | 12 |
13 | 15 | 16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 | 35 | 36 |
37 |
38 |
39 |
40 | 41 | 42 |
43 | 44 | 保存成功 45 |
46 |
47 |
48 |
49 |
50 |
51 |
-------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /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 | 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 | 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 |
35 |
36 | 52 | 55 | comments powered by Disqus 56 |
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 | -------------------------------------------------------------------------------- /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 |
    11 | <%= prev_page_tag unless current_page.first? %> 12 | <%= next_page_tag unless current_page.last? %> 13 |
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 | -------------------------------------------------------------------------------- /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 = "" 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 "#{header}#{body}
" 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 --------------------------------------------------------------------------------