企业内部网工作组协作应用详解
1. 应用概述
该应用是一款适用于小团队的工作组工具,提供了丰富的办公通信和协作功能,如项目管理、为项目添加评论和文档等。它不像 Google Maps 那样炫酷,但实用且必要。Ajax 的运用相对低调,增强了应用的功能和可用性,却不引人注目,同时也展示了 Ajax 的适用场景。
2. 下载与配置
要下载该应用的源代码,可访问 相关链接 。若文件未列出,则与 Rails 默认框架相同。文件就位后,需通过编辑 config/database.yml 来配置数据库。默认配置期望一个名为 intranet_development 的 MySQL 数据库,可在本地主机使用用户名 root 且无密码访问。具体操作步骤如下:
1. 为应用创建数据库。
2. 根据需要修改 database.yml 。
3. 运行 rake db:schema:load 以根据 schema.rb 创建应用的数据库结构。
3. 数据库表结构
应用使用三张表: users 、 posts 和 attachments 。
| 表名 | 用途 |
| ---- | ---- |
| users | 管理用户信息 |
| posts | 存储各种类型的帖子,如文档、项目计划、消息、评论或联系人信息 |
| attachments | 管理二进制文件上传,如照片、电子表格、文档等 |
以下是创建表的代码:
ActiveRecord::Schema.define( ) do
create_table "users", :force => true do |t|
t.column "email", :string, :limit => 100,
:default => "", :null => false
t.column "password", :string, :limit => 100,
:default => "", :null => false
t.column "name", :string, :limit => 40,
:default => "", :null => false
t.column "phone", :string, :limit => 50,
:default => "", :null => false
t.column "address", :string, :limit => 50,
:default => "", :null => false
t.column "city", :string, :limit => 50,
:default => "", :null => false
t.column "state", :string, :limit => 50,
:default => "", :null => false
t.column "zip", :string, :limit => 50,
:default => "", :null => false
t.column "picture_id", :integer
t.column "created_at", :datetime
t.column "updated_at", :datetime
t.column "status", :string, :limit => 50,
:default => "", :null => false
t.column "last_active", :datetime
t.column "admin", :boolean,
:default => false, :null => false
end
add_index "users", ["email"], :name => "email", :unique => true
add_index "users", ["password"], :name => "password"
create_table "posts", :force => true do |t|
t.column "type", :string, :limit => 20
t.column "post_id", :integer
t.column "created_at", :datetime
t.column "updated_at", :datetime
t.column "created_by", :integer
t.column "updated_by", :integer
t.column "name", :string, :limit => 128,
:default => "Untitled", :null => false
t.column "body", :text, :default => "", :null => false
t.column "email", :string, :limit => 50,
:default => "", :null => false
t.column "phone", :string, :limit => 50,
:default => "", :null => false
t.column "start_date", :date
t.column "end_date", :date
t.column "attachment_id", :integer
t.column "attachment_filename", :string
t.column "attachment_content_type", :string, :limit => 128
t.column "attachment_size", :integer
end
add_index "posts", ["type"], :name => "type"
add_index "posts", ["created_at"], :name => "created_at"
add_index "posts", ["updated_at"], :name => "updated_at"
add_index "posts", ["post_id"], :name => "post_id"
create_table "attachments", :force => true do |t|
t.column "content", :binary
t.column "updated_at", :datetime
end
end
4. 模型定义
- User 模型 :用于记录系统用户,每个用户与他创建的帖子相关联,且每个用户可以有一个用户图片,存储在
Attachment模型中。
class User < ActiveRecord::Base
has_many :posts, :foreign_key => "created_by",
:dependent => :destroy
belongs_to :picture, :class_name =>'Attachment',
:foreign_key =>'picture_id', :dependent => :destroy
validates_length_of :name, :password, :email, :within => 4..100
validates_uniqueness_of :email
validates_format_of :email,
:with => /^(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?$/
def self.authenticate(email, password)
find_by_email_and_password(email, password)
end
def first_name; name.split.first; end
def last_name; name.split.last; end
def short_name
name.blank? ? "" : "#{first_name} #{last_name[0,1]}."
end
def file= file
unless file.size == 0
picture = Attachment.new :content => file.read
picture.save
write_attribute'picture_id', picture.id
end
end
def inactive?
last_active < 1.minute.ago rescue true
end
end
- Post 模型 :是
Plan、Message、Document和Comment的超类,一个帖子有一个创建者(必须是用户),可以有附件和评论。
class Post < ActiveRecord::Base
has_many :comments, :order =>'id', :dependent => :destroy
belongs_to :creator, :class_name =>'User',
:foreign_key => "created_by"
belongs_to :attachment, :dependent => :destroy
validates_presence_of :name
def file=(file)
unless file.size == 0
attachment=Attachment.new :content => file.read
attachment.save
write_attribute('attachment_id', attachment.id)
write_attribute('attachment_filename', file.original_filename)
write_attribute('attachment_content_type', file.content_type)
write_attribute('attachment_size', file.size)
end
end
end
- Contact 模型 :是
Post的一种类型,用于存储人员信息,如销售代表、公关人员或客户。
class Contact < Post
validates_format_of :name, :with => /^.+ .+$/,
:message => "must include full name"
validates_format_of :email,
:with => /^(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?$/
def self.letter letter
Contact.find :all,
:conditions => [ "name like ?", '% '+letter+'%' ]
end
def reversed_name
names = name.split
"#{names.pop}, #{names.join ' '}"
end
end
- Document 模型 :是
Post的子类,可表示几乎任何类型的内容,如电子表格、PDF、Word 文档等。
class Document < Post
end
- Message 模型 :是
Post的另一个子类,代表几乎任何类型的简单文本消息。
class Message < Post
end
- Plan 模型 :代表特定类型的事件,提供按特定日期范围获取计划的方法。
class Plan < Post
def self.this_week
Plan.find :all, :conditions => "start_date >= now( ) and
start_date < '#{Date.today + 7}'",
:order => "start_date asc"
end
def self.next_three_weeks
Plan.find :all, :conditions => "start_date >=
'#{Date.today + 7}' and start_date < '#{Date.today + 28}'",
:order => "start_date asc"
end
def self.beyond
Plan.find :all,
:conditions => "start_date >= '#{Date.today + 28}'",
:order => "start_date asc"
end
end
- Project 模型 :是
Post的另一种类型。
class Project < Post
end
- Comment 模型 :是一种简单的
Post,可以附加到另一个帖子上。
class Comment < Post
belongs_to :post
validates_presence_of :body
end
- Attachment 模型 :用于管理与文档相关的二进制数据以及系统用户的附加图像。
class Attachment < ActiveRecord::Base
end
5. 路由配置
应用的路由较为简单, map.resources 调用设置了对应用的 RESTful 访问。
ActionController::Routing::Routes.draw do |map|
map.resources :messages, :plans, :documents, :projects, :contacts,
:member => { :download => :get }
map.resources :comments, :path_prefix => "/:post_type/:post_id"
map.resources :sessions
map.resources :users, :collection => { :statuses => :get },
:member => { :status => :any }
map.home '', :controller =>'messages', :action =>'home'
map.connect ':controller/:action/:id'
end
6. 环境与辅助工具
环境文件要求应用加载 lib/labeling_form_helper.rb , authentication.rb 提供简单的身份验证服务。
RAILS_GEM_VERSION ='1.1.2'
require File.join(File.dirname(__FILE__ _),'boot')
Rails::Initializer.run do |config|
end
require'labeling_form_helper'
7. 身份验证模块
module Authentication
protected
def logged_in?
return false unless session[:user_id]
begin
@current_user ||= User.find(session[:user_id])
rescue ActiveRecord::RecordNotFound
reset_session
end
end
def current_user
@current_user if logged_in?
end
def require_login
username, passwd = get_auth_data
if username && passwd
self.current_user ||=
User.authenticate(username, passwd) || :false
end
return true if logged_in?
respond_to do |format|
format.html do
session[:return_to] = request.request_uri
redirect_to new_session_url
end
format.xml do
headers["Status"] = "Unauthorized"
headers["WWW-Authenticate"] = %(Basic realm="Web Password")
render :text => "Could't authenticate you",
:status =>'401 Unauthorized'
end
end
false
end
def access_denied
redirect_to new_session_url
end
def store_location
session[:return_to] = request.request_uri
end
def redirect_back_or_default(default)
session[:return_to] ?
redirect_to_url(session[:return_to]) :
redirect_to(default)
session[:return_to] = nil
end
def self.included(base)
base.send :helper_method, :current_user, :logged_in?
end
private
def get_auth_data
user, pass = nil, nil
if request.env.has_key?'X-HTTP_AUTHORIZATION'
authdata = request.env['X-HTTP_AUTHORIZATION'].to_s.split
elsif request.env.has_key?'HTTP_AUTHORIZATION'
authdata = request.env['HTTP_AUTHORIZATION'].to_s.split
end
if authdata && authdata[0] =='Basic'
user, pass = Base64.decode64(authdata[1]).split(':')[0..1]
end
return [user, pass]
end
end
8. 表单构建器
LabelingFormBuilder 重写了 form_for 和 remote_form_for 中的一些方法,扩展它们以自动处理字段名称。
class LabelingFormBuilder < ActionView::Helpers::FormBuilder
(%w(text_field password_field text_area
date_select file_field)).each do |selector|
src = <<-end_src
def #{selector}(method, options = {})
text = options.delete(:label) || method.to_s.humanize
errors = @object.errors.on(method.to_s)
errors = errors.is_a?(Array) ? errors.first : errors.to_s
html = '<label for="' + @object_name.to_s +'_' +
method.to_s + '">'
html << text
unless errors.blank?
html << ' <span class="error">' + errors + '</span>'
end
html << '</label> '
#{selector=='date_select' ? "html << '<span id=\"' +
@object_name.to_s +'_' + method.to_s +
'\"></span>'" : ""}
html << super
html
end
end_src
class_eval src, __FILE__, __LINE_ _
end
end
9. 应用控制器
ApplicationController 提供了一些 before_filter 来确保用户已登录、用户有效并显示每日消息,还提供了访问控制的辅助方法。
class ApplicationController < ActionController::Base
include Authentication
before_filter :require_login
before_filter :set_system_announcement
before_filter :check_for_valid_user
private
def set_system_announcement
flash.now[:system_announcement] =
"This is the <strong>Ajax on Rails Intranet</strong>, <br/>
released as part of <a href=\"http://scottraymond.net/\">
<em>Ajax on Rails</em></a> from O’Reilly Media."
end
def can_edit? record
return true if current_user.admin?
case record.class.to_s
when'User'
record.id == current_user.id
when'Message'
record.created_by == current_user.id
else true
end
end
helper_method :can_edit?
def admin?; current_user.admin?; end
helper_method :admin?
def require_admin
unless admin?
flash[:warning] = "Sorry, only administrators can do that."
redirect_to messages_url
end
end
def check_for_valid_user
if logged_in? and !current_user.valid?
flash[:warning] = "Please create your administrator account"
redirect_to edit_user_url(:id => current_user)
return false
end
end
end
10. 辅助方法模块
application_helper.rb 定义了更多辅助方法,用于返回有关内容类型的信息, page_title 尝试在未明确给出标题时推断页面标题, standard_form 使用 labeling_form_helper 来简化视图模板。
module ApplicationHelper
def icon_for content_type
case content_type.to_s.strip
when "image/jpeg"
"JPG"
when "application/vnd.ms-excel"
"XLS"
when "application/msword"
"DOC"
when "application/pdf"
"PDF"
else "Generic"
end
end
def description_of content_type
case content_type.to_s.strip
when "image/jpeg"
"JPEG graphic"
when "application/vnd.ms-excel"
"Excel worksheet"
when "application/msword"
"Word document"
when "application/pdf"
"PDF file"
else ""
end
end
def site_title
'Intranet'
end
def page_title
return @page_title if @page_title
return @post.name if @post and !@post.new_record?
return @user.name if @user and !@user.new_record?
''
end
def flash_div *keys
divs = keys.select { |k| flash[k] }.collect do |k|
content_tag :div, flash[k], :class => "flash #{k}"
end
divs.join
end
def user_thumb user
img = tag("img",
:src => formatted_user_url(:id => user, :format =>'jpg'),
:class =>'user_picture', :alt => user.name)
img_link = link_to img, user_url(:id => user)
text_link = link_to user.short_name, user_url(:id => user)
content_tag :div, "#{img_link}<br/>#{text_link}",
:class =>'user'
end
def clear_div
'<div class="clear"></div>'
end
def standard_form name, object, &block
url = { :action => object.new_record? ? "index" : "show" }
html = { :class => "standard",
:style => (@edit_on ? '' : "display: none;"),
:multipart => true }
concat form_tag(url, html) + "<fieldset>", block.binding
unless object.new_record?
concat '<input name="_method" type="hidden" value="put" />',
block.binding
end
yield LabelingFormBuilder.new(name, object, self, {}, block)
concat "</fieldset>" + end_form_tag, block.binding
end
def standard_submit name=nil, object=nil
name = post_type unless name
object = @post unless object
delete_link = link_to("Delete", { :action =>'show' },
:method => :delete,
:confirm => "Are you sure?",
:class => "delete")
submit_tag("Save #{name}") +
(object.new_record? ? "" : (" or " + delete_link))
end
end
11. 布局文件
application.rhtml 是一个基本布局,包含用于浏览应用的链接,包括登录和注销链接,以及用于创建一些标签式导航的 CSS。 utility DIV 是一个 Ajax 侧边栏,列出了登录和未登录的用户。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml
<html xmlns="http://www.w3.org/1999/xhtml"
xml:lang="en" lang="en">
<head>
<title>
<%= site_title +
(page_title.blank? ? '' : " - #{page_title}") %>
</title>
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag :defaults %>
</head>
<body class="<%= params[:controller] %>">
<%= flash_div :system_announcement %>
<div id="header">
<h1><%= link_to site_title, home_url %></h1>
<% if logged_in? and current_user.valid? %>
<div id="account">
Signed in as
<%= link_to current_user.first_name,
user_url(:id => current_user),
:class =>'strong stealth' %><br/>
<%= link_to'Settings',
edit_user_url(:id => current_user),
:class =>'small subtle' %>
<%= link_to'Sign Out',
session_url(:id => session.session_id),
:method => :delete,
:class =>'small delete' %>
</div>
<ul id="nav">
<li id="messages">
<%= link_to "Messages", messages_url %>
</li>
<li id="plans">
<%= link_to "Event Plans", plans_url %>
</li>
<li id="documents">
<%= link_to "Documents", documents_url %>
</li>
<li id="projects">
<%= link_to "Projects", projects_url %>
</li>
<li id="contacts">
<%= link_to "Contacts", contacts_url %>
</li>
</ul>
<% end %>
</div>
<div id="utility">
<%= flash_div :notice %>
<% if logged_in? and current_user.valid? %>
<div id="status">
<ul>
<li>
<%= link_to current_user.short_name,
user_url(:id => current_user) %>
<span id="my_status">
<%= current_user.status.blank? ?
"(Click to set status)" :
current_user.status %>
</span>
</li>
</ul>
<%= javascript_tag "new Ajax.InPlaceEditor('my_status',
'#{user_url(current_user)}',
{loadTextURL:'#{status_user_url(current_user)}',
ajaxOptions:{method:'put'},
callback:function(form, value){
return'user[status]='+escape(value);
}});" %>
<%= render :partial => "users/statuses" %>
<%= javascript_tag "new PeriodicalExecuter(function( ){
new Ajax.Updater('statuses', '#{statuses_users_url}',
{method:'get'}); }, 30)" %>
</div>
<% end %>
</div>
<div id="main">
<%= flash_div :warning %>
<%= content_tag :h2, page_title %>
<%= yield %>
</div>
</body>
</html>
12. 控制器
- PostsController :是所有其他控制器的超类,实现了帖子的所有基本 CRUD 操作。
class PostsController < ApplicationController
before_filter :find_post,
:only => [ :show, :download, :edit, :update, :destroy ]
before_filter :check_permissions, :only => [ :update, :destroy ]
def index
@page_title = post_type.pluralize
@post = model.new
@posts = model.find :all
end
def new
@page_title = "New #{post_type}"
@edit_on = true
@post = model.new
end
def create
@post = model.new params[:post]
@post.creator = current_user
@post.updated_by = @post.created_by
if @post.save
flash[:notice] ='Post successfully created.'
redirect_to :action =>'index'
else
@page_title = "New #{post_type}"
@edit_on = true
render :action =>'new'
end
end
def show
end
def download
filename = @post.attachment_filename.split(/\\/).last
send_data @post.attachment.content, :filename => filename,
:type => @post.attachment_content_type,
:disposition =>'attachment'
end
def edit
@edit_on = true
render :action =>'show'
end
def update
post = params[:post].merge(:updated_by => current_user)
if @post.update_attributes post
flash[:notice] ='Your changes were saved.'
redirect_to :action =>'show'
else
@edit_on = true
render :action =>'show'
end
end
def destroy
@post.destroy
flash[:notice] = "The post was deleted."
redirect_to :action =>'index'
end
private
def model_name;'Post'; end
def post_type; model_name; end
helper_method :post_type
def model; eval model_name; end
def find_post
@post = model.find params[:id]
end
def check_permissions
unless can_edit? @post
flash[:warning] = "You can't edit that post."
redirect_to :action =>'show'
return false
end
end
end
- CommentsController :是
Posts控制器的子类,用于处理评论的创建。
class CommentsController < ApplicationController
before_filter :find_post
def index
end
def create
@comment = Comment.new params[:comment]
@comment.post_id = @post.id
@comment.name = "Re: #{@post.name}"
@comment.creator = current_user
@comment.save
respond_to do |format|
format.html {
flash[:notice] = "Comment saved."
redirect_to :back
}
format.js {
render :update do |page|
page[:comments].reload
end
}
end
end
def show
@comment = @post.comments.find params[:id]
end
private
def find_post
@post = Post.find params[:post_id]
end
end
- ContactsController :用于管理联系人记录,继承了
PostsController的大部分行为。
class ContactsController < PostsController
def index
@page_title = post_type.pluralize
@post = model.new
@posts = params[:letter] ?
Contact.letter(params[:letter]) :
Contact.find(:all)
end
private
def model_name;'Contact'; end
end
- DocumentsController :用于管理文档,继承了
PostsController的行为。
class DocumentsController < PostsController
private
def model_name;'Document'; end
end
- MessagesController :用于管理消息,继承了
PostsController的行为。
class MessagesController < PostsController
def home
flash.keep
redirect_to messages_url
end
def index
super
@post_pages, @posts = paginate :messages,
:order_by =>'created_at desc', :per_page => 30
end
private
def model_name;'Message'; end
end
- PlansController :用于管理计划,继承了
PostsController的行为。
class PlansController < PostsController
def index
super
@page_title = "Upcoming Event Plans"
@this_week = Plan.this_week
@next_three_weeks = Plan.next_three_weeks
@beyond = Plan.beyond
end
private
def model_name;'Plan'; end
def post_type;'Event Plan'; end
end
- ProjectsController :用于管理项目,继承了
PostsController的行为。
class ProjectsController < PostsController
private
def model_name;'Project'; end
end
- SessionsController :处理登录和注销。
class SessionsController < ApplicationController
before_filter :create_first_user, :only => :new
skip_before_filter :require_login
filter_parameter_logging :password
def new
redirect_to home_url if logged_in?
@user = User.new
end
def create
if user = User.authenticate(params[:session][:email],
params[:session][:password])
reset_session
session[:user_id] = user.id
redirect_back_or_default home_url
flash[:notice] = "Signed in successfully"
else
flash[:warning] = "There was a problem signing you in.
Please try again."
@user = User.new
render :action =>'new'
end
end
def destroy
reset_session
flash[:notice] = "You have been signed out."
redirect_to new_session_url
end
private
def create_first_user
return true unless User.count == 0
user = User.new :admin => 1
user.save_with_validation false
session[:user_id] = user.id
redirect_to home_url
end
end
- UsersController :支持用户注册和编辑用户资料,
statuses操作是应用存在指示器的关键。
class UsersController < ApplicationController
before_filter :require_admin, :only => [ :new, :create ]
before_filter :find_user,
:only => [ :show, :status, :edit, :update, :destroy ]
before_filter :check_permissions,
:only => [ :edit, :update, :destroy ]
skip_before_filter :check_for_valid_user,
:only => [ :edit, :update ]
filter_parameter_logging :password
def index
@users = User.find :all
@page_title = "Users"
@user = User.new
end
def new
@page_title = "New User"
@user = User.new
@edit_on = true
end
def statuses
current_user.update_attributes :last_active => Time.now
render :partial =>'statuses'
end
def create
if @user = User.create(params[:user])
flash[:notice] ='User was successfully saved.'
redirect_to user_url(:id => @user)
else
render :action =>'index'
end
end
def show
if params[:format]=='jpg'
if @user.has_picture?
send_data @user.picture.content,
:filename => "#{@user.id}.jpg",
:type =>'image/jpeg',
:disposition =>'inline'
else
send_file RAILS_ROOT+'/public/images/default_user.jpg',
:filename => "#{@user.id}.jpg",
:type =>'image/jpeg',
:disposition =>'inline'
end
return
end
end
def status
render :text => @user.status
end
def edit
@edit_on = true
render :action =>'show'
end
def update
success = @user.update_attributes params[:user]
respond_to do |format|
format.html {
if success
flash[:notice] ='User was successfully updated.'
redirect_to user_url
else
@edit_on = true
render :action =>'show'
end
}
format.js {
render :text => @user.status.blank? ?
"(none)" :
@user.status
}
end
end
def destroy
@user.destroy
flash[:notice] = "User deleted."
redirect_to users_url
end
private
def post_type; "User"; end
helper_method :post_type
def find_user
@user = User.find params[:id]
end
def check_permissions
return false unless can_edit? @user
end
end
13. 视图与脚本
- PostForm 类 :提供用户点击编辑链接时的视觉效果,表单会滑过内容。
var PostForm = {
toggle: function( ) {
var container = $('form_container');
var form = $$('#form_container form').first( );
if(container.hasClassName('active')) {
form.visualEffect('blind_up', {
duration: 0.25,
afterFinish: function( ){
container.removeClassName('active');
}
});
} else {
form.visualEffect('blind_down', {
duration: 0.5,
beforeStart: function( ){
container.addClassName('active');
}
});
}
}
}
14. 样式表
/* Basics */
/* --------------------------------------------------------- */
*/* */ {
color: inherit;
font: inherit;
margin: 0;
list-style: none;
padding: 0;
text-decoration: none;
}
body {
background-color: #fff;
background-repeat: repeat-y;
color: #333;
}
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 11px;
line-height: 14px;
}
p { margin-bottom: 8px; }
ul li { list-style-type: disc; }
ul, ol { margin: .5em 0 .5em 2em; }
ol li { list-style-type: decimal; }
fieldset { border: none; }
strong, b { font-weight: bold; }
em { font-style: italic; }
.strong { font-weight: bold; }
.small { font-size: 10px; }
#main {
float: left;
position: relative;
left: -2px;
top: 24px;
padding-right: 30px;
width: 575px;
padding-bottom: 50px;
}
#utility {
width: 170px;
padding: 45px 10px 20px 18px;
float: left;
height: 100%;
}
div.clear {
clear: both;
margin-top: 1px;
display: block;
}
/* Links */
/* ------------------------------------------------------------ */
a { color: #264764; text-decoration: underline; }
a:visited { color: #264764; }
a:hover { color: #fff; background-color: #264764;
text-decoration: none; }
a.stealth { color: #000; text-decoration: none; }
a:hover.stealth { background-color: #000; color: #fff; }
a.subtle { color: #666; text-decoration: underline; }
a:hover.subtle { background-color: #666; color: #fff; }
a.delete { color: #c00; text-decoration: underline; }
a:hover.delete { background-color: #c00; color: #fff; }
a.create { color: #009900; text-decoration: underline; }
a:hover.create { background-color: #009900; color: #fff; }
/* Headers */
/* ---------------------------------------------------------- */
#header {
height: 92px;
background-color: #E0E6EF;
border-bottom: 1px solid #888;
}
#header h1 {
font-family: futura;
font-size: 30px;
float: left;
height: 92px;
width: 181px;
xbackground-image: url('/images/logo.gif');
xtext-indent: -1000px;
/* or */
height: 37px;
padding-top: 55px;
width: 136px;
padding-left: 45px;
}
#header h1 a { text-decoration: none; }
#header #account {
float: right;
text-align: right;
font-family: verdana;
font-size: 11px;
color: #333;
margin-right: 8px;
margin-top: 15px;
line-height: 14px;
}
#main h2 {
font-family: trebuchet ms;
font-size: 18px;
font-weight: normal;
color: #264764;
margin-bottom: 0px;
border-bottom: 1px solid #B8B8B8;
width: 569px;
padding-bottom: 8px;
clear: both;
}
h3 {
font-size: 12px;
font-weight: bold;
margin-top: 10px;
margin-bottom: 0;
background-color: #eee;
padding: 3px 0 3px 5px;
border-bottom: 1px solid #ddd;
}
h4 {
font-size: 11px;
font-weight: bold;
margin-top: 10px;
margin-bottom: 2px;
}
/* Warnings and notices */
/* ----------------------------------------------------------- */
.flash.notice {
background-color: #ffc;
padding: .5em;
border-top: 1px solid #dda;
border-bottom: 1px solid #dda;
margin: 0 30px 1.5em 0;
}
.flash.warning {
background-color: #c22;
padding: .5em;
border-top: 1px solid #600;
border-bottom: 1px solid #600;
margin: 0em 0 2em 0em;
color: #fff;
font-weight: bold;
}
.flash.warning a { color: #fff; }
.flash.system_announcement {
padding: 5px;
background-color: #EFF3AB;
border-bottom: 1px solid #898989;
color: #444;
text-align: center;
height: 30px;
}
/* Navigation */
/* ------------------------------------------------------------- */
ul#nav { margin: 0; position: relative; left: 15px; top: 67px; }
html>body ul#nav { top: 68px; } /* non-iewin */
ul#nav li {
display: inline;
height: 30px;
font-size: 12px;
line-height: 26px;
font-family: helvetica, arial;
margin-right: 5px;
padding: 3px 4px 5px 7px;
}
html>body ul#nav li { padding: 3px 7px 4px 7px; } /* non-iewin */
body.messages li#messages, body.plans li#plans,
body.documents li#documents, body.projects li#projects,
body.contacts li#contacts {
background-color: #fff; border: 1px solid #888;
border-bottom: 1px solid #fff;
}
ul#nav li a { text-decoration: none; color: #555; }
ul#nav li a:hover { background-color: transparent;
text-decoration: none; }
ul#nav li:hover a { text-decoration: none; color: #000; }
ul#nav li:hover { text-decoration: underline; }
/* Statuses */
/* ------------------------------------------------------------ */
#status ul li {
list-style-type: none;
margin-bottom: 5px;
font-weight: bold;
}
#status ul li span {
font-weight: normal;
display: block;
font-style: italic;
}
#status ul { margin-left: 0; }
#status ul li a { text-decoration: none; }
#status ul li.inactive, #status ul li.inactive a { color: #777; }
/* Post container */
/* ------------------------------------------------------------- */
#form_container {
padding: 6px 10px 15px 12px;
width: 545px;
margin-bottom: 0px;
}
#form_container.active {
background: #EEF8ED;
border-left: 1px solid #89B989;
border-right: 1px solid #89B989;
border-bottom: 1px solid #89B989;
}
#form_container #new_link {
float: left;
color: #009900;
font-weight: bold;
margin-left: -12px;
}
#form_container.active #new_link span { visibility: hidden; }
#form_container #cancel_link { visibility: hidden; float: right; }
#form_container.active #cancel_link { visibility: visible; }
#form_container.active #new_link a { text-decoration: none; }
#form_container #meta { float: left; margin-left: 10px; }
#form_container #detail { clear: left; padding-top: 20px; }
#form_container.active #detail { display: none; }
/* Standard form */
/* ------------------------------------------------------------- */
form.standard {
clear: left;
margin-top: 10px;
margin-left: 15px;
padding-top: 10px;
width: 510px;
}
form.standard label {
font-weight: bold;
display: block;
margin-bottom: 3px;
font-size: 12px;
font-family: verdana;
}
form.standard input, form.standard textarea {
width: 100%;
display: block;
margin-bottom: 10px;
}
form.standard input#post_name, form.standard input#user_name {
font-size: 18px; font-weight: bold;
}
form.standard .fieldWithErrors {
border-left: 4px solid #c00; padding-left: 3px;
}
form.standard label span.error { color: #c00; }
form.standard input[type='submit'] { width: 100px; display: inline; }
form.standard textarea { height: 150px; }
form.standard select { margin-bottom: 10px; }
/* Body details */
/* ------------------------------------------------------------ */
.post_detail {
background-color: #eee;
border: 1px solid #ccc;
padding: 12px;
width: 508px;
}
#main div.post {
margin-top: 11px;
margin-bottom: 25px;
margin-left: 1px;
}
#main div.post div.user {
width: 60px;
float: left;
text-align: center;
margin-right: 10px;
margin-top: 4px;
font-size: 10px;
}
img.user_picture {
text-decoration: none;
background-color: #fff;
margin-bottom: -2px;
width: 60px;
height: 60px;
border: 1px solid #666;
padding: 2px;
}
#main div.post h3 {
font-weight: bold;
font-size: 11px;
padding-top: 2px;
padding-left: 1px;
}
#main div.post h3 span {
color: #666;
font-weight: normal;
margin-left: 8px;
}
#main div.post p.meta {
color: #666;
font-size: 10px;
margin-bottom: 5px;
}
#main img.icon {
float: left;
width: 32px;
height: 32px;
padding-right: 8px;
}
#main div.post h3 span a { color: #666; font-weight: normal; }
#main div.post h3 span a:hover { background-color: #666;
color: #fff; }
#main div.post div.body { margin-left: 70px; }
#main div.post div.no_user { margin-left: 0px; }
#main div.post p.meta span.comments {
float: right; font-size: 11px;
}
#main p.meta { color: #666; margin-bottom: 10px; }
#letter_links {
margin-top: 20px;
margin-bottom: 20px;
}
#letter_links a {
background-color: #ffa;
padding: 3px 4px;
margin: 0px 1px;
border: 1px solid #dd9;
text-decoration: none;
}
#letter_links a:hover, #letter_links a.active {
color: #000;
background-color: #dd9;
border: 1px solid #cc7;
}
/* Comments */
/* ------------------------------------------------------------- */
#comments {
background-color: #eee;
width: 500px;
margin: 3em 0 1em 0;
padding: 0 0 1em 0;
width: 100%;
}
#comments h2 {
background-color: #777;
width: 100%;
color: #fff;
font-size: 1em;
font-weight: bold;
padding: 3px 0px 3px 3px;
line-height: 1em;
border-bottom: 1px solid #555;
}
#comments form, #comments div.post {
margin: 1em 0 1em 1em; background-color: #eee;
}
#comments form textarea { width: 90%; height: 80px; }
#comments h3 { font-size: 1em; }
#comments input { float: left; }
#comments p img { margin-top: 1px; margin-left: 10px; }
/* User list */
/* ----------------------------------------------------------- */
table#posts { width: 100%; }
table#posts td { margin: 0; padding: 4px;
border-bottom: 1px solid #ccc; }
/* User#show */
/* ----------------------------------------------------------- */
body.user_show #main img {
float: left;
margin-right: 20px;
border: 1px solid #ccc;
padding: 3px;
width: 80px;
height: 80px;
margin-bottom: 100px;
}
#change_picture {
float: right;
width: 120px;
padding: 15px;
text-align: center;
border: 1px solid #ccc
}
body.user_show #main { width: 600px; }
body.user_show #main h3 { clear: left; margin-bottom: 10px; }
body.user_show #main ul#lookuplinks li {
display: inline; margin-right: 10px;
}
#change_picture img { border: 1px solid black; }
通过以上详细的介绍,我们可以全面了解这个企业内部网工作组协作应用的各个方面,包括其功能、数据库结构、模型定义、控制器实现、视图设计以及样式表设置等。这些组件相互协作,为小团队提供了一个高效的办公通信和协作平台。
企业内部网工作组协作应用详解
15. 各部分功能总结与关联
为了更清晰地理解整个应用的架构和各部分之间的关系,我们可以通过以下表格进行总结:
| 部分 | 功能 | 关联 |
| ---- | ---- | ---- |
| 数据库表 | 存储应用的数据,如用户信息、帖子信息、附件信息等 | 模型通过 ActiveRecord 与数据库表交互 |
| 模型 | 定义数据结构和业务逻辑,如用户、帖子、附件等 | 控制器通过模型进行数据的增删改查操作 |
| 控制器 | 处理用户请求,实现 CRUD 操作 | 视图通过控制器获取数据并展示 |
| 视图 | 展示数据给用户,提供用户交互界面 | 控制器将处理结果传递给视图 |
| 路由 | 映射 URL 到相应的控制器动作 | 连接用户请求和控制器 |
| 辅助方法 | 提供额外的功能,如表单构建、内容类型处理等 | 视图和控制器可以调用辅助方法 |
| 样式表 | 定义应用的外观和布局 | 影响视图的显示效果 |
下面是一个 mermaid 流程图,展示了用户从登录到操作帖子的基本流程:
graph LR
A[用户登录] --> B{登录成功?}
B -- 是 --> C[进入应用主页]
B -- 否 --> A
C --> D[选择帖子类型]
D --> E[查看帖子列表]
E --> F{选择操作}
F -- 创建 --> G[填写帖子信息]
F -- 查看 --> H[查看帖子详情]
F -- 编辑 --> I[修改帖子信息]
F -- 删除 --> J[确认删除]
G --> K[保存帖子]
I --> K
K --> E
J --> E
16. 具体操作流程分析
-
用户登录流程 :
- 用户访问登录页面,输入邮箱和密码。
- 表单提交到
SessionsController的create动作。 -
create动作调用User.authenticate方法验证用户信息。 - 如果验证成功,将用户 ID 存储在会话中,并重定向到主页;否则,显示错误信息并重新渲染登录页面。
-
帖子创建流程 :
- 用户在主页选择创建新帖子。
- 进入创建页面,填写帖子信息。
- 表单提交到相应控制器的
create动作。 -
create动作创建新的帖子对象,设置创建者和更新者信息。 - 尝试保存帖子,如果保存成功,显示成功信息并重定向到帖子列表页面;否则,显示错误信息并重新渲染创建页面。
-
帖子编辑流程 :
- 用户在帖子列表中选择要编辑的帖子。
- 进入编辑页面,显示帖子的当前信息。
- 用户修改帖子信息后提交表单。
- 表单提交到相应控制器的
update动作。 -
update动作更新帖子信息,如果更新成功,显示成功信息并重定向到帖子详情页面;否则,显示错误信息并重新渲染编辑页面。
17. 代码优化建议
-
性能优化 :
- 对于频繁查询的操作,可以考虑使用缓存技术,减少数据库查询次数。例如,在
UsersController的statuses动作中,可以缓存用户状态信息,避免每次都查询数据库。 - 对于大数据量的查询,可以使用分页技术,减少一次性加载的数据量。例如,在
MessagesController的index动作中,使用paginate方法进行分页查询。
- 对于频繁查询的操作,可以考虑使用缓存技术,减少数据库查询次数。例如,在
-
代码复用 :
- 可以将一些通用的方法提取到辅助方法模块中,提高代码的复用性。例如,将文件上传的逻辑提取到一个单独的辅助方法中,在多个控制器中复用。
- 对于一些重复的表单字段,可以创建表单组件,减少代码的重复编写。
-
安全性优化 :
- 对用户输入进行严格的验证和过滤,防止 SQL 注入和 XSS 攻击。例如,在模型中使用
validates方法对用户输入进行验证。 - 对敏感信息进行加密存储,如用户密码。可以使用 Rails 提供的加密机制对密码进行加密。
- 对用户输入进行严格的验证和过滤,防止 SQL 注入和 XSS 攻击。例如,在模型中使用
18. 总结
这个企业内部网工作组协作应用为小团队提供了一个功能丰富的办公通信和协作平台。通过合理的数据库设计、模型定义、控制器实现和视图设计,实现了用户管理、帖子管理、评论管理等功能。同时,通过路由配置、辅助方法和样式表的设置,提高了应用的易用性和美观性。
在实际应用中,可以根据具体需求对应用进行扩展和优化。例如,可以添加更多的帖子类型、增加权限管理功能、优化性能等。通过不断地改进和完善,这个应用可以更好地满足团队的协作需求。
希望以上内容能帮助你更好地理解和使用这个企业内部网工作组协作应用。如果你有任何问题或建议,欢迎随时交流。
通过以上对企业内部网工作组协作应用的深入分析,我们可以看到它在功能实现、架构设计和用户体验等方面都有很多值得学习和借鉴的地方。无论是对于开发者还是团队管理者,都可以从中获取有价值的信息,以提升团队的协作效率和工作质量。
超级会员免费看
1335

被折叠的 条评论
为什么被折叠?



