Merge branch 'project_pack' into develop

dev_aliyun
p31729568 6 years ago
commit f3056cfad3

@ -166,6 +166,21 @@ module Mobile
return user if user
nil
end
def paginate(objs)
page = params[:page].to_i <= 0 ? 1 : params[:page].to_i
per_page = params[:per_page].to_i > 0 ? params[:per_page].to_i : 20
Kaminari.paginate_array(objs).page(page).per(per_page)
end
def render_ok(data = {})
{ status: 0, message: 'success' }.merge(data)
end
def render_error(message)
{ status: -1, message: message }
end
end
mount Apis::Auth
@ -197,7 +212,7 @@ module Mobile
mount Apis::Ecloud
mount Apis::Cnmooc
mount Apis::ProjectPackages
# add_swagger_documentation ({host: 'educoder.0bug.info', api_version: 'v1', base_path: '/api'}) if Rails.env.development?
add_swagger_documentation ({api_version: 'v1', base_path: '/api'}) if Rails.env.development?

@ -0,0 +1,153 @@
# encoding=utf-8
module Mobile
module Apis
class ProjectPackages < Grape::API
use Mobile::Middleware::NotFoundHandler
helpers do
def current_package
@_current_package ||= ProjectPackage.find(params[:id])
end
def symbolize_params
params.to_hash.symbolize_keys
end
end
resources :project_packages do
desc 'project packages index'
params do
optional :category, type: String, desc: '类型'
optional :keyword, type: String, desc: '搜索关键字'
optional :sort_by, type: String, desc: '排序'
optional :sort_direction, type: String, desc: '排序方向'
optional :page, type: Integer, desc: '页数'
optional :per_page, type: Integer, desc: '分页大小'
end
get do
packages = ProjectPackage.where(status: %w(published bidding_ended bidding_finished))
packages = packages.where(category: params[:category]) if params[:category].present?
keyword = params[:keyword].to_s.strip
packages = packages.where('title LIKE ?', "%#{keyword}%") if keyword.present?
count = packages.count
direction = params[:sort_direction] == 'asc' ? 'asc' : 'desc'
sort = params[:sort_by] == 'price' ? 'min_price' : 'published_at'
packages = packages.order("#{sort} #{direction}")
packages = paginate packages.includes(:creator, :attachments, bidding_users: :user)
present :count, count
present :project_packages, packages, with: Mobile::Entities::ProjectPackage, type: :index, user: current_user
end
desc 'project package show'
params do
requires :id, type: Integer, desc: 'ID'
end
route_param :id do
get do
authenticate!
user = current_user
unless current_package.visitable? || user.id == current_package.creator_id || user.admin?
error!('403 forbidden', 403)
end
present current_package, with: Mobile::Entities::ProjectPackage, type: :show, user: user
end
resource :bidding_users do
desc 'user bidding'
params {}
post do
authenticate!
begin
::ProjectPackages::BiddingService.new(current_package, current_user).call
render_ok
rescue ::ProjectPackages::BiddingService::Error => ex
render_error(ex.message)
end
end
desc 'select bidding user win'
params do
requires :user_ids, type: Array[Integer], desc: '中标者用户ID数组'
end
post 'win' do
authenticate!
begin
package = current_user.project_packages.find(params[:id])
ProjectPackages::WinBiddingService.new(package, symbolize_params).call
rescue ProjectPackages::WinBiddingService::Error =>ex
render_error(ex.message)
end
end
end
end
desc 'create project packages'
params do
requires :category, type: String, desc: '类型'
requires :title, type: String, desc: '标题'
requires :content, type: String, desc: '描述'
optional :attachment_ids, type: Array[Integer], desc: '附件ID数组'
requires :deadline_at, type: DateTime, desc: '截止日期'
requires :min_price, type: Float, desc: '最小费用'
optional :max_price, type: Float, desc: '最大费用'
requires :contact_name, type: String, desc: '联系人姓名'
requires :contact_phone, type: String, desc: '联系人手机号'
optional :code, type: String, desc: '验证码'
optional :publish, type: Boolean, desc: '是否申请发布'
end
post do
authenticate!
begin
package = current_user.project_packages.new
::ProjectPackages::SaveService.new(package, symbolize_params).call
package.increment_visit_count!
render_ok(id: package.id)
rescue ::ProjectPackages::SaveService::Error => ex
render_error(ex.message)
end
end
desc 'update project package'
params do
requires :id, type: Integer, desc: 'ID'
requires :category, type: String, desc: '类型'
requires :title, type: String, desc: '标题'
requires :content, type: String, desc: '描述'
optional :attachment_ids, type: Array[Integer], desc: '附件ID数组'
requires :deadline_at, type: DateTime, desc: '截止日期'
requires :min_price, type: Float, desc: '最小费用'
optional :max_price, type: Float, desc: '最大费用'
requires :contact_name, type: String, desc: '联系人姓名'
requires :contact_phone, type: String, desc: '联系人手机号'
optional :code, type: String, desc: '验证码'
optional :publish, type: Boolean, desc: '是否申请发布'
end
put ':id' do
authenticate!
begin
package = current_user.project_packages.find(params[:id])
return render_error('该状态下不能编辑') unless package.editable?
::ProjectPackages::SaveService.new(package, symbolize_params).call
package.increment_visit_count!
render_ok(id: package.id)
rescue ::ProjectPackages::SaveService::Error => ex
render_error(ex.message)
end
end
end
end
end
end

@ -0,0 +1,72 @@
module Mobile
module Entities
class ProjectPackage < Grape::Entity
include ApplicationHelper
include ActionView::Helpers::NumberHelper
expose :id
expose :title
expose :category
expose :status
expose :visit_count, if: { type: :index }
expose :bidding_users_count, if: { type: :index }
expose :min_price
expose :max_price
expose :contact_name, if: ->(package, opts){ opts[:user].id == package.creator_id || opts[:user].admin? || opts[:user].business? }
expose :contact_phone, if: ->(package, opts){ opts[:user].id == package.creator_id || opts[:user].admin? || opts[:user].business? }
expose :deadline_at do |package, _|
package.deadline_at.try(:utc).try(:iso8601)
end
expose :published_at do |package, _|
package.published_at.try(:utc).try(:iso8601)
end
expose :creator, if: { type: :show } do |package, _|
{
id: package.creator.id,
login: package.creator.login,
name: package.creator.show_name,
avatar_url: "/images/#{url_to_avatar(package.creator)}"
}
end
expose :attachments, if: { type: :show } do |package, _|
package.attachments.map do |attachment|
{
id: attachment.id,
title: attachment.title,
filesize: number_to_human_size(attachment.filesize),
url: "attachments/download/#{attachment.id}",
}
end
end
expose :bidding_users, if: { type: :show } do |package, _|
package.bidding_users.map do |bidding_user|
user = bidding_user.user
{
id: user.id,
login: user.login,
name: user.show_name,
avatar_url: "/images/#{url_to_avatar(user)}",
status: bidding_user.status
}
end
end
expose :operation, if: { type: :show } do |package, opts|
user = opts[:user]
is_creator = user.id == package.creator_id
is_admin = user.admin? || user.business?
{
can_bidding: package.can_bidding?(user),
can_select_bidding_user: package.bidding_end? && package.end_bidding? && (is_creator || is_admin),
can_edit: package.editable? && (is_creator || is_admin),
can_delete: package.deletable? && (is_creator || is_admin)
}
end
end
end
end

@ -0,0 +1,20 @@
#coding=utf-8
module Mobile
module Middleware
class NotFoundHandler < Grape::Middleware::Base
def call!(env)
@env = env
begin
@app.call(@env)
rescue ActiveRecord::RecordNotFound => e
message = {status: 404, message: e.message }.to_json
status = 200
headers = { 'Content-Type' => content_type }
Rack::Response.new([message], status, headers).finish
end
end
end
end
end

@ -681,7 +681,7 @@ class AccountController < ApplicationController
render :json => req
end
# 发送验证码type 1注册手机验证码 2找回密码手机验证码 3找回密码邮箱验证码 4绑定手机 5绑定邮箱 6手机验证码登录 7邮箱验证码登录 8邮箱注册验证码
# 发送验证码type 1注册手机验证码 2找回密码手机验证码 3找回密码邮箱验证码 4绑定手机 5绑定邮箱 6手机验证码登录 7邮箱验证码登录 8邮箱注册验证码 9验证手机号有效
# 验证码是否有效
def valid_verification_code
req = Hash.new(false)
@ -700,7 +700,7 @@ class AccountController < ApplicationController
render :json => req
end
# 发送验证码type 1注册手机验证码 2找回密码手机验证码 3找回密码邮箱验证码 4绑定手机 5绑定邮箱 6手机验证码登录 7邮箱验证码登录 8邮箱注册验证码
# 发送验证码type 1注册手机验证码 2找回密码手机验证码 3找回密码邮箱验证码 4绑定手机 5绑定邮箱 6手机验证码登录 7邮箱验证码登录 8邮箱注册验证码 9验证手机号有效
def get_verification_code
code = %W(0 1 2 3 4 5 6 7 8 9)
type = params[:type].to_i
@ -787,6 +787,22 @@ class AccountController < ApplicationController
else
req[:status] = 2
end
elsif params[:type] == 9
if params[:value] =~ /^1\d{10}$/
begin
verification_code = code.sample(6).join
status = Trustie::Sms.send(mobile: params[:value], code: verification_code)
if status == 0
VerificationCode.create(:phone => params[:value], :status => 1, :code_type => type, :code => verification_code)
end
req[:msg] = code_msg status
rescue => e
Rails.logger.error "发送验证码出错: #{e}"
end
req[:status] = 1
else
req[:status] = 2
end
else
if params[:value] =~ /^[a-zA-Z0-9]+([._\\]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/
if User.where(:mail => params[:value]).count > 0

@ -895,6 +895,8 @@ class AttachmentsController < ApplicationController
@school = @attachment.container
elsif @attachment.container_type == 'Library'
@library = @attachment.container
elsif @attachment.container_type == 'ProjectPackage'
@package = @attachment.container
else
unless @attachment.container_type == 'Syllabus' || @attachment.container_type == 'Bid' || @attachment.container_type == 'Organization' || @attachment.container_type == 'HomeworkAttach' || @attachment.container_type == 'Memo' || @attachment.container_type == 'Softapplication' || @attachment.container_type == 'PhoneAppVersion' || @attachment.container_type == 'StudentWorksScore' || @attachment.container_type == 'StudentWork' || @attachment.container_type == 'Work'|| @attachment.container_type == 'ContestantWork'|| @attachment.container_type == 'Contest' || @attachment.container_type == 'HomeworkBank' || @attachment.container_type == 'Exercise' || @attachment.container_type == 'ExerciseBank' || @attachment.container_type == 'Career' || @attachment.container_type == 'EcTemplate'
@project = @attachment.project

@ -0,0 +1,46 @@
class Managements::ProjectPackageAppliesController < Managements::BaseController
before_filter :set_menu_type
def index
applies = ProjectPackageApply.order('project_package_applies.updated_at desc')
search = params[:search].to_s.strip
if search.present?
applies = applies.joins(:project_package).where('project_packages.title like :search', search: "%#{search}%")
end
applies = applies.where(status: params[:status].presence || :pending)
@applies = paginateHelper applies.includes(project_package: { creator: :user_extensions })
respond_to do |format|
format.js
format.html
end
end
def agree
ProjectPackages::AgreeApplyService.new(current_apply).call
render json: { status: 0 }
rescue ProjectPackages::AgreeApplyService::Error => e
render json: { status: -1, message: e.message }
end
def refuse
ProjectPackages::RefuseApplyService.new(current_apply, reason: params[:reason]).call
render json: { status: 0 }
rescue ProjectPackages::RefuseApplyService::Error => e
render json: { status: -1, message: e.message }
end
private
def current_apply
@_current_apply ||= ProjectPackageApply.find(params[:id])
end
def set_menu_type
@menu_type = 10
@sub_type = 9
end
end

@ -0,0 +1,55 @@
class Managements::ProjectPackagesController < Managements::BaseController
before_filter :set_menu_type, only: [:index]
def index
packages = ProjectPackage.where(nil)
# 任务标题
keyword = params[:keyword].to_s.strip
packages = packages.where('title LIKE ?', "%#{keyword}%") if keyword.present?
# 发布者姓名
creator_name = params[:creator_name].to_s.strip
if creator_name.present?
sql = 'LOWER(concat(users.lastname, users.firstname)) LIKE ?'
packages = packages.joins(:creator).where(sql, "%#{creator_name}%")
end
# 状态
status =
case params[:status]
when 'pending' then %w(pending refused)
when 'applying' then %w(applying)
when 'published' then %w(published bidding_end)
when 'finished' then %w(bidding_finished)
end
packages = packages.where(status: status) if status.present?
# 发布时间
begin_date = (params[:begin_date].to_time.beginning_of_day rescue nil)
end_date = (params[:end_date].to_time.end_of_day rescue nil)
packages = packages.where('published_at >= ?', begin_date) if begin_date.present?
packages = packages.where('published_at <= ?', end_date) if end_date.present?
@count = packages.count
# 排序
params[:sort_by] ||= 'created_at'
params[:sort_direction] ||= 'desc'
packages = packages.order("#{params[:sort_by]} #{params[:sort_direction]}")
@packages = paginateHelper packages.preload(:creator)
end
def destroy
ProjectPackage.find(params[:id]).destroy
render json: { status: 0 }
end
private
def set_menu_type
@menu_type = 14
@sub_type = 1
end
end

@ -0,0 +1,45 @@
# encoding=utf-8
# For react
class ProjectPackagesController < ApplicationController
before_filter :require_login, :except => [:index]
include ApplicationHelper
def show
render_react
end
def new
render_react
end
def index
render_react
end
def edit
render_react
end
def apply_success
render_react
end
def destroy
package = ProjectPackage.find(params[:id])
return render_403 unless package.deletable? && (admin_or_business? || package.creator_id == current_user.id)
package.destroy
Tiding.create(user_id: package.creator_id, trigger_user_id: 1, container_id: package.id,
container_type: 'ProjectPackage', tiding_type: 'Destroyed', extra: package.title)
render json: { status: 0, message: 'success' }
end
private
def render_react
render file: 'public/react/build/index.html', :layout => false
end
end

@ -2425,6 +2425,8 @@ class UsersController < ApplicationController
when "a_bank", "m_bank", "p_bank"
@sort = params[:sort] || "updated_at"
@p = params[:p] || "Common"
when 'a_package', 'l_package', 'p_package'
@p = params[:p] || 'a'
end
case @type
@ -2558,6 +2560,59 @@ class UsersController < ApplicationController
@tag = params[:tag]
@objects = @objects.where(:course_list_id => params[:tag]).order("#{@sort} #{order}")
end
when 'a_package'
bidding_packages = @user.bidding_project_packages
bidding_packages = bidding_packages.none if @show_all && params[:q] == 0 # 参与的不存在未发布的
packages = @user.project_packages
if @show_all
status =
case params[:p]
when '0' then %w(pending applying refused)
when '1' then %w(published)
when '2' then %w(bidding_ended bidding_finished)
end
if status.present?
packages = packages.where(status: status)
bidding_packages = bidding_packages.where(status: status)
end
else
packages = packages.where(status: %w(published bidding_ended bidding_finished))
end
@per_page = 20
ids = bidding_packages.pluck(:id) + packages.pluck(:id)
@objects = ProjectPackage.where(id: ids).order("published_at #{order}")
when 'p_package'
packages = @user.project_packages
if @show_all
status =
case params[:p]
when '0' then %w(pending applying refused)
when '1' then %w(published)
when '2' then %w(bidding_ended bidding_finished)
end
if status.present?
packages = packages.where(status: status)
end
else
packages = packages.where(status: %w(published bidding_ended bidding_finished))
end
@per_page = 20
@objects = packages.order("published_at #{order}")
when 'l_package'
packages = @user.bidding_project_packages
if @show_all
status =
case params[:p]
when '0' then %w(bidding_lost)
when '1' then %w(bidding_won)
end
packages = packages.where(bidding_users: { status: status }) if status.present?
end
@per_page = 20
@objects = packages.order("published_at #{order}")
end
@objects_count = @objects.size
@ -2575,7 +2630,7 @@ class UsersController < ApplicationController
@objects = @objects.to_a
@objects.unshift("new") if @new_icon
@objects = paginateHelper @objects, 16
@objects = paginateHelper @objects, @per_page || 16
respond_to do |format|
format.js

@ -754,6 +754,7 @@ module ApplicationHelper
when 6 then '实训课程发布'
when 7 then '职业认证'
when 8 then '教学案例发布'
when 9 then '众包需求发布'
else '职业认证'
end
when 11
@ -771,6 +772,10 @@ module ApplicationHelper
)
)
)
when 14
case sub_type
when 1 then '任务列表'
end
end
end
@ -4370,6 +4375,8 @@ module ApplicationHelper
candown = User.current.member_of_course?(attachment.container.courses.first) || (course.is_public == 1 && attachment.is_public == 1)
elsif attachment.container_type == "Library" #教学案例允许下载
candown = true
elsif attachment.container_type == "ProjectPackage" #教学案例允许下载
candown = true
else
candown = (attachment.is_public == 1 || attachment.is_public == true)
end
@ -7453,6 +7460,8 @@ def tiding_url tiding
my_account_path
when 'Library'
tiding.tiding_type == 'Apply' ? library_applies_path : library_path(tiding.container_id)
when 'ProjectPackage'
tiding.container.present? ? "/project_packages/#{tiding.container_id}" : 'javascript:void(0)'
end
end

@ -0,0 +1,24 @@
class BiddingUser < ActiveRecord::Base
include AASM
belongs_to :user
belongs_to :project_package, counter_cache: true
aasm(:status) do
state :pending, initiali: true
state :bidding_won
state :bidding_lost
event :win do
transitions from: [:pending], to: :bid_won
end
event :lose do
transitions from: [:pending], to: :bid_lost
end
end
def status_text
I18n.t("bidding_user.status.#{status}")
end
end

@ -0,0 +1,96 @@
class ProjectPackage < ActiveRecord::Base
include AASM
acts_as_attachable
CATEGORY_VALUES = %w(front backend mobile database cloud_compute_and_big_data devops_and_test ai other)
attr_accessible :title, :content, :category, :deadline_at, :contact_name, :contact_phone, :min_price, :max_price
belongs_to :creator, class_name: 'User'
has_many :project_package_applies, dependent: :destroy
has_one :process_project_package_apply, conditions: { status: :pending }, class_name: 'ProjectPackageApply'
has_many :bidding_users, dependent: :delete_all
has_many :win_bidding_users, conditions: { status: :bidding_won }, class_name: 'BiddingUser'
has_many :lose_bidding_users, conditions: { status: :bidding_lost }, class_name: 'BiddingUser'
has_many :attachments, as: :container, dependent: :destroy
validates :category, presence: true, inclusion: { in: CATEGORY_VALUES }
validates :title, presence: true, length: { maximum: 60 }
validates :content, presence: true
validates :deadline_at, presence: true
validates :contact_name, presence: true, length: { maximum: 20 }
validates :contact_phone, presence: true, format: { with: /1\d{10}/ }
validates :min_price, numericality: { greater_than: 0 }
validates :max_price, numericality: { greater_than: ->(obj){ obj.min_price } }, allow_blank: true
aasm(:status) do
state :pending, initiali: true
state :applying
state :refused
state :published
state :bidding_ended
state :bidding_finished
event :apply do
transitions from: [:pending, :refused], to: :applying
end
event :refuse do
transitions from: :applying, to: :refused
end
event :publish do
transitions from: :applying, to: :published
end
event :end_bidding do
transitions from: :published, to: :bidding_ended
end
event :finish_bidding do
transitions from: [:bidding_ended], to: :bidding_finished
end
end
def visitable?
!editable?
end
def editable?
pending? || applying? || refused?
end
def deletable?
pending? || refused?
end
def deadline?
deadline_at < Time.now
end
def bidding_end?
flag = deadline?
end_bidding! if flag && may_end_bidding?
flag
end
def can_bidding?(user)
published? && !bidding_end? && !bidding_users.exists?(user_id: user.id)
end
def increment_visit_count!
ProjectPackage.connection.execute("update project_packages set visit_count = COALESCE(visit_count, 0) + 1 where id = #{id}")
end
def category_text
I18n.t("project_package.category.#{category}")
end
def status_text
I18n.t("project_package.status.#{status}")
end
end

@ -0,0 +1,21 @@
class ProjectPackageApply < ActiveRecord::Base
include AASM
attr_accessible :project_package_id, :reason
belongs_to :project_package
aasm(:status) do
state :pending, initiali: true
state :refused
state :agreed
event :refuse do
transitions from: :pending, to: :refused
end
event :agree do
transitions from: :pending, to: :agreed
end
end
end

@ -365,6 +365,27 @@ class Tiding < ActiveRecord::Base
elsif tiding_type == 'System'
text = status == 1 ? "审核已通过" : "审核未通过,<br/>原因:#{extra}"
"你提交的发布教学案例申请:#{library.try(:title)}#{text}"
end
when 'ProjectPackage'
package = ProjectPackage.find_by_id(container_id)
case tiding_type
when 'Apply' then
"申请发布众包需求:#{package.try(:title)}"
when 'System' then
text = status == 1 ? "审核已通过" : "审核未通过,<br/>原因:#{extra}"
"你提交的众包需求申请:#{package.try(:title)}#{text}"
when 'Created' then
"你创建了众包需求:#{package.try(:title)}"
when 'Destroyed' then
"你删除了众包需求:#{extra}"
when 'Bidding' then
"应征了你发布的众包任务:#{package.try(:title)}"
when 'BiddingEnd' then
"你发布的众包任务:#{package.try(:title)},已进入选标阶段,请尽快进行选择确认!"
when 'BiddingWon' then
"恭喜,你应征的众包任务:#{package.try(:title)},在评选环节中标了"
when 'BiddingLost' then
"很遗憾,你应征投稿的众包任务:#{package.try(:title)},未中标"
end
else
logger.error "error type: 1"

@ -274,6 +274,11 @@ class User < Principal
has_one :user_source
# 众包
has_many :project_packages, foreign_key: :creator_id, dependent: :destroy
has_many :bidding_users, dependent: :destroy
has_many :bidding_project_packages, through: :bidding_users, source: :project_package
## end
# default_scope -> { includes(:user_extensions, :user_score) }

@ -2,4 +2,8 @@ class VerificationCode < ActiveRecord::Base
#status发送状态
#code_type发送类型type 1注册手机验证码 2找回密码手机验证码 3找回密码邮箱验证码 4绑定手机 5绑定邮箱 6手机验证码登录 7邮箱验证码登录 8邮箱注册验证码
attr_accessible :code, :code_type, :email, :phone, :status
def valid_code?
(Time.now.to_i - created_at.to_i) <= 10*60
end
end

@ -506,7 +506,7 @@ class CareersService
{username: current_user.show_name, login: current_user.login,
user_id: current_user.id, image_url: url_to_avatar(current_user),
admin: current_user.admin?, is_teacher: current_user.user_extensions.try(:identity) == 0,
tidding_count: count}
tidding_count: count, phone: current_user.phone}
end
def find_career id

@ -0,0 +1,36 @@
class ProjectPackages::AgreeApplyService
Error = Class.new(StandardError)
attr_reader :apply, :package
def initialize(apply)
@apply = apply
@package = apply.project_package
end
def call
raise Error, '该状态下不能进行此操作' unless apply.may_agree? && package.may_publish?
ActiveRecord::Base.transaction do
apply.agree!
# 发布
package.publish
package.published_at = Time.now
package.save!
# 消息
send_agree_notify!
end
end
private
def send_agree_notify!
Tiding.where(container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'Apply', status: 0).update_all(status: 1)
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1,
container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'System', status: 1)
end
end

@ -0,0 +1,31 @@
class ProjectPackages::ApplyPublishService
Error = Class.new(StandardError)
attr_reader :package
def initialize(package)
@package = package
end
def call
return if package.applying?
raise Error, '该状态下不能申请发布' unless package.may_apply?
ActiveRecord::Base.transaction do
package.apply!
package.project_package_applies.create!
send_project_package_apply_notify!
end
end
private
def send_project_package_apply_notify!
Tiding.create!(user_id: 1, trigger_user_id: package.creator_id,
container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'Apply', status: 0)
end
end

@ -0,0 +1,29 @@
class ProjectPackages::BiddingService
Error = Class.new(StandardError)
attr_reader :package, :user
def initialize(package, user)
@package = package
@user = user
end
def call
raise Error, '竞标已截止' if package.bidding_end?
raise Error, '不能参与自己发布的竞标' if package.creator_id == user.id
raise Error, '您已参与竞标' if package.bidding_users.exists?(user_id: user.id)
ActiveRecord::Base.transaction do
package.bidding_users.create!(user_id: user.id)
send_bidding_notify!
end
end
private
def send_bidding_notify!
Tiding.create!(user_id: package.creator_id, trigger_user_id: user.id,
container_id: package.id, container_type: 'ProjectPackage', tiding_type: 'Bidding')
end
end

@ -0,0 +1,26 @@
class ProjectPackages::EndBiddingService
attr_reader :package
def initialize(package)
@package = package
end
def call
return unless package_deadline?
package.end_bidding!
send_bidding_end_notify!
end
private
def send_bidding_end_notify!
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1,
container_id: package.id, container_type: 'ProjectPackage', tiding_type: 'BiddingEnd')
end
def package_deadline?
package.may_end_bidding? && package.deadline_at < Time.now
end
end

@ -0,0 +1,38 @@
class ProjectPackages::RefuseApplyService
Error = Class.new(StandardError)
attr_reader :apply, :package, :params
def initialize(apply, params)
@apply = apply
@package = apply.project_package
@params = params
end
def call
raise Error, '该状态下不能进行此操作' unless apply.may_refuse? && package.may_refuse?
ActiveRecord::Base.transaction do
apply.refuse
apply.reason = params[:reason].to_s.strip
apply.save!
# 发布
package.refuse!
# 消息
send_refuse_notify!
end
end
private
def send_refuse_notify!
Tiding.where(container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'Apply', status: 0).update_all(status: 1)
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1,
container_id: package.id, container_type: 'ProjectPackage',
tiding_type: 'System', status: 2, extra: apply.reason)
end
end

@ -0,0 +1,67 @@
class ProjectPackages::SaveService
Error = Class.new(StandardError)
attr_reader :package, :params
def initialize(package, params)
@package = package
@params = params
end
def call
check_code_valid! if need_check_code?
is_create = package.new_record?
ActiveRecord::Base.transaction do
package.assign_attributes(params)
package.save!
# 处理附件
deal_attachments
send_create_notify! if is_create
ProjectPackages::ApplyPublishService.new(package).call if with_publish?
end
package
rescue ProjectPackages::ApplyPublishService::Error => ex
raise Error, ex.message
end
private
def need_check_code?
(package.new_record? && params[:contact_phone] != package.creator.phone) || package.contact_phone != params[:contact_phone]
end
def check_code_valid!
raise Error, '验证码不能为空' if params[:code].blank?
code = VerificationCode.where(phone: params[:contact_phone], code_type: 9, code: params[:code]).last
raise Error, '无效的验证码' if code.blank? || !code.valid_code?
end
def deal_attachments
attachment_ids = Array.wrap(params[:attachment_ids]).compact.map(&:to_i) || []
old_attachment_ids = package.attachments.pluck(:id)
destroy_ids = old_attachment_ids - attachment_ids
package.attachments.where(id: destroy_ids).delete_all
new_ids = attachment_ids - old_attachment_ids
if new_ids.present?
Attachment.where(id: new_ids, container_id: nil).update_all(container_id: package.id, container_type: 'ProjectPackage')
end
end
def send_create_notify!
Tiding.create!(user_id: package.creator_id, trigger_user_id: 1,
container_id: package.id, container_type: 'ProjectPackage', tiding_type: 'Created')
end
def with_publish?
params[:publish].to_s == 'true'
end
end

@ -0,0 +1,47 @@
class ProjectPackages::WinBiddingService
Error = Class.new(StandardError)
attr_reader :package, :params
def initialize(package, params)
@package = package
@params = params
end
def call
raise Error, '竞标报名还未结束' unless package.bidding_end?
raise Error, '该状态下不能选择中标者' unless package.can_finish_bidding?
win_user_ids = Array.wrap(params[:user_ids]).compact.map(&:to_i)
bidding_user_ids = package.bidding_users.pluck(:user_id)
win_user_ids = bidding_user_ids & win_user_ids
raise Error, '请选择中标者' if win_user_ids.blank?
ActiveRecord::Base.transaction do
package.finish_bidding!
# win bidding users
package.bidding_users.where(user_id: win_user_ids).update_all(status: :bidding_won)
# lose bidding users
lost_user_ids = bidding_user_ids - win_user_ids
package.bidding_users.where(user_id: lost_user_ids).update_all(status: :bidding_lost)
send_bidding_result_notify!('BiddingWon', win_user_ids)
send_bidding_result_notify!('BiddingLost', lost_user_ids)
end
package
end
private
def send_bidding_result_notify!(type, user_ids)
sql = 'insert into tidings(user_id, trigger_user_id, container_id, container_type, tiding_type) values'
base_opts = [package.creator_id, package.id, 'ProjectPackage', type]
values = '(' + user_ids.map { |id| base_opts.clone.unshift(id).join(',') }.join('),(') + ')'
sql + values
end
end

@ -0,0 +1,12 @@
class CheckProjectPackageDeadlineTask
def call
ProjectPackage.where(status: :published).where('deadline_at < ?', Time.now).find_each do |package|
begin
ProjectPackages::EndBiddingService.new(package).call
rescue => ex
Rails.logger.error ex.message
Rails.logger.error ex.backtrace.join('\n')
end
end
end
end

@ -77,6 +77,11 @@
<li style="width: 100px"><%= link_to "竞赛列表", competition_managements_path %></li>
</ul>
</li>
<li class="fl edu-admin-nav-li edu-position <%= 'active' if @menu_type == 14 %>" style="width: 100px"><a href="javascript:void(0);" class="edu-admin-nav-a">众包+</a>
<ul class="edu-admin-nav-inner edu-absolute">
<li style="width: 100px"><%= link_to "任务列表", managements_project_packages_path %></li>
</ul>
</li>
<li class="fl edu-admin-nav-li edu-position"><a href="javascript:void(0);" class="edu-admin-nav-a">单位</a>
<ul class="edu-admin-nav-inner edu-absolute">
<li><%= link_to "单位列表", departments_part_managements_path, :class => "edu-admin-nav-a" %></li>
@ -117,6 +122,7 @@
<li><%= link_to '实训发布', shixun_authorization_managements_path %></li>
<li><%= link_to '实践课程发布', subject_authorization_managements_path %></li>
<li><%= link_to '教学案例发布', library_applies_path(status: :pending) %></li>
<li><%= link_to '众包需求发布', project_package_applies_path(status: :pending) %></li>
</ul>
</li>
<li class="fl edu-admin-nav-li edu-position"><a href="javascript:void(0);" class="edu-admin-nav-a">认证+</a>

@ -0,0 +1,76 @@
<% if @applies.present? %>
<% @applies.each do |apply| %>
<% user = apply.project_package.creator %>
<% project_package = apply.project_package %>
<div class="admin-con-box apply-<%= apply.id %> clearfix">
<a href="<%= user_path(user) %>" target="_blank" class="fl with10 edu-ad-user">
<%= image_tag(url_to_avatar(user), :class => "fl with10 edu-ad-user", :alt => "头像", :width => "50", :height => "50" ) %>
</a>
<div class="fl with90">
<ul>
<li class="clearfix mb5">
<a href="<%= user_path(user) %>" class="fl"><%= user.try(:show_real_name) %></a>
<span class="fl ml30 font-12 mt3 color-grey"><%= time_from_now(apply.created_at) %></span>
<% if apply.pending? %>
<a href="javascript:void(0);" class="fr color-orange" onclick="reject_project_package_authentication_reason(this);" >拒绝</a>
<a href="javascript:void(0);" class="fr mr15 color-orange" data-remote="true" onclick="project_package_authorization_gree('<%= apply.id %>');">同意</a>
<% else %>
<a href="javascript:void(0);" class="<%= apply.agreed? ? 'task-btn-green' : '' %> task-btn fr"><%= apply.agreed? ? "已同意" : "已拒绝" %></a>
<% end %>
</li>
<li class="clearfix mb10">
<%= link_to project_package.title, project_package_path(project_package), :target => "_blank" %>
</li>
<% if apply.pending? %>
<div class="undis">
<li class="clearfix edu-form-border mb10">
<label class="edu-form-label fl">原因:</label>
<input type="text" class="task-form-90 task-height-40 panel-box-sizing fl edu-form-noborder" placeholder="我得说点儿什么最多200个字符">
</li>
<li class="clearfix">
<a href="javascript:void(0);" class="task-btn task-btn-orange fr" onclick="project_package_submit_reject_reason('<%= apply.id %>', this);" >确定</a>
<a href="javascript:void(0);" class="task-btn fr mr10" onclick="project_package_hide_reject_reason(this);" >取消</a>
</li>
</div>
<% else %>
<% if apply.refused? %>
<li>原因:<span class="color-orange"><%= apply.reason %></span></li>
<% end %>
<% end %>
</ul>
</div>
</div>
<% end %>
<div class="mt20 mb20" style="text-align:center;">
<div class="pages_user_show" style="width:auto; display:inline-block;">
<ul id="homework_pository_ref_pages">
<%= pagination_links_full @obj_pages, @obj_count, :per_page_links => false, :remote => true, :flag => true, :is_new => true %>
</ul>
<div class="cl"></div>
</div>
</div>
<% else %>
<%= render :partial => "welcome/no_data" %>
<% end %>
<script type="text/javascript">
function project_package_authorization_gree(id){
$.ajax({
url: '/managements/project_package_applies/' + id + '/agree',
type: 'post',
success: function(data){
if (data && data.status != -1) {
$('#authentication_list .admin-con-box.apply-' + id).remove();
if($('#authentication_list .admin-con-box').length == 0){
location.reload();
}
} else {
alert(data.message);
}
}
})
}
</script>

@ -0,0 +1,120 @@
<div class="edu-class-container mb15">
<div class="edu-con-top clearfix">
<p class="ml15 fl color-grey">众包需求发布</p>
</div>
<div class="edu-con-bg01 mt15">
<div class="edu-tab clearfix mb20">
<ul id="edu-tab-nav" class="border-bottom-orange">
<li id="edu-tab-nav-1" class="new-tab-nav background-orange" onclick="HoverLi(1);">
<%= link_to "待审批", project_package_applies_path(status: :pending), class: 'tab_type', remote: true %>
</li>
<li id="edu-tab-nav-2" class="new-tab-nav" onclick="HoverLi(2);">
<%= link_to "已审批", project_package_applies_path(status: [:refused, :agreed]), class: 'tab_type', remote: true %>
</li>
</ul>
<div class="cl"></div>
<div id="edu-tab-con-1">
<div class="mt10">
<div class="edu-position fr task-form-30 mb10 mr15">
<input class="task-form-100 panel-box-sizing" placeholder="输入需求标题进行检索" type="text" id="search_name">
<a href="javascript:void(0);" class="edu-btn-search font-16 color-grey mt10" id="search"><i class="fa fa-search"></i></a>
</div>
<div class="cl"></div>
<div id="authentication_list" class="auth_table">
<%= render :partial => "managements/project_package_applies/project_package_apply_list"%>
</div>
</div>
</div>
<div id="edu-tab-con-2" class="undis">
<div class="mt10">
<p class="fl task-form-60 mt8 ml15 clearfix">
<%= link_to "全部", project_package_applies_path(status: [:refused, :agreed]), :class => "edu-filter-cir-grey mr5 fl font-12 active", :id => "project_package_all_authentication", :remote => true %>
<%= link_to "同意", project_package_applies_path(status: :agreed), :class => "edu-filter-cir-grey mr5 fl font-12", :id => "project_package_agree_authentication", :remote => true %>
<%= link_to "拒绝", project_package_applies_path(status: :refused), :class => "edu-filter-cir-grey mr5 fl font-12", :id => "project_package_reject_authentication", :remote => true %>
</p>
<div class="edu-position fr task-form-30 mb10 fr mr15">
<input class="task-form-100 panel-box-sizing" placeholder="输入需求标题进行检索" type="text" id="project_package_search_name">
<a href="javascript:void(0);" class="edu-btn-search font-16 color-grey mt10" id="project_package_search"><i class="fa fa-search"></i></a>
</div>
<div class="cl"></div>
<div id="project_package_authentication_list" class="auth_table">
</div>
</div>
</div>
<div class="cl"></div>
</div>
</div>
</div>
<script>
/* -------------------------- 拒绝 ------------------------------------ */
function reject_project_package_authentication_reason(nThis){
var reason = $(nThis).parent().parent().find('div');
reason.find("input").val("");
reason.toggle();
}
/* -------------------------- 取消 ------------------------------------ */
function project_package_hide_reject_reason(nThis){
var reason = $(nThis).parent().parent();
reason.find("input").val("");
reason.hide();
}
/* ------------------------- 提交拒绝原因 --------------------------------- */
function project_package_submit_reject_reason(id, nThis){
var nReason = $(nThis).parent().parent();
var reason = nReason.find("input").val();
if (reason == '') {
alert('请输入原因');
return;
}
$.ajax({
url: '/managements/project_package_applies/' + id + '/refuse',
type: 'post',
data: {reason: reason},
success: function(data){
if (data && data.status != -1) {
$('#authentication_list .admin-con-box.apply-' + id).remove();
if($('#authentication_list .admin-con-box').length == 0){
location.reload();
}
} else {
alert(data.message);
}
}
});
}
/* -------------------------- 按名字进行搜索(未审批) ----------------------------- */
$("#search").live("click", function(){
var iName = $("#search_name").val();
$.ajax({
url: "/managements/project_package_applies",
dataType: 'script',
data: { search: iName, status: 'pending' }
});
});
/* ------------------- 按名字进行搜索(已审批)-------------------- */
$("#project_package_search").live("click", function(){
var iName = $("#project_package_search_name").val();
var id = $("#project_package_all_authentication").parent().find(".active").attr("id");
var status = '';
if(id == "project_package_all_authentication"){
status = ['refused', 'agreed'];
}else if(id=="project_package_agree_authentication"){
status = 'agreed';
}else{
status = 'refused';
}
$.ajax({
url: "/managements/project_package_applies",
dataType: 'script',
data: { search: iName, status: status}
});
});
</script>

@ -0,0 +1,30 @@
var nTabIcon_1 = $("#edu-tab-con-1");
var nTabIcon_2 = $("#edu-tab-con-2");
var nTabNav_1 = $("#edu-tab-nav-1");
var nTabNav_2 = $("#edu-tab-nav-2");
var nAudit = $("#project_package_all_authentication").parent();
<% if params[:status].to_s == 'pending' %>
$("#authentication_list").html("<%= j( render :partial => "managements/project_package_applies/project_package_apply_list" ) %>");
nTabNav_1.addClass("background-orange");
nTabNav_2.removeClass("background-orange");
nTabIcon_1.show();
nTabIcon_2.hide();
<% else %>
$("#project_package_authentication_list").html("<%= j( render :partial => "managements/project_package_applies/project_package_apply_list" ) %>");
nTabNav_1.removeClass("background-orange");
nTabNav_2.addClass("background-orange");
nTabIcon_1.hide();
nTabIcon_2.show();
/* -------------------------- 未审批(全部、同意、拒绝点击时动态样式) ------------------------------ */
if(<%= params[:status].to_s == 'agreed' %>){
nAudit.find(".active").removeClass("active");
$("#project_package_agree_authentication").addClass("active");
}else if(<%= params[:status].to_s == 'refused' %>){
nAudit.find(".active").removeClass("active");
$("#project_package_reject_authentication").addClass("active");
}else{
nAudit.find(".active").removeClass("active");
$("#project_package_all_authentication").addClass("active");
}
<% end %>

@ -0,0 +1,53 @@
<div class="list-count">共<span><%= @obj_count %></span>条搜索结果</div>
<table class="edu-pop-table edu-txt-center" cellpadding="0" cellspacing="0" style="table-layout: fixed">
<thead>
<tr>
<th width="6%">序号</th>
<th width="20%" class="edu-txt-left">任务标题</th>
<th width="8%">状态</th>
<th width="8%">竞标人数</th>
<th width="8%">发布者</th>
<th width="12%"><%= sort_tag('创建时间', name: 'created_at', path: managements_project_packages_path) %></th>
<th width="12%"><%= sort_tag('发布时间', name: 'published_at', path: managements_project_packages_path) %></th>
<th width="12%"><%= sort_tag('竞标截止时间', name: 'deadline_at', path: managements_project_packages_path) %></th>
<th width="14%">操作</th>
</tr>
</thead>
<tbody>
<% if @packages.present? %>
<% @packages.each_with_index do |package, index| %>
<tr class="project-package-item project-package-<%= package.id %>">
<td><%= (@obj_pages.page - 1) * @obj_pages.per_page + index + 1 %></td>
<td class="edu-txt-left">
<%= link_to package.title, project_package_path(package), target: '_blank',
style: "display: block;max-width: 250px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;",
data: { tip_down: package.title } %>
</td>
<td><%= package.status_text %></td>
<td><%= package.bidding_users_count %></td>
<td><%= package.creator.show_real_name %></td>
<td><%= package.created_at.strftime('%Y-%m-%d %H:%M') %></td>
<td><%= package.published_at.try(:strftime, '%Y-%m-%d %H:%M') || '--' %></td>
<td><%= package.deadline_at.try(:strftime, '%Y-%m-%d %H:%M') || '--' %></td>
<td> <%= link_to '删除', 'javascript:void(0)', class: 'delete-btn', data: { id: package.id } %></td>
</tr>
<% end %>
<% else %>
<tr>
<td colspan="100">
<%= render :partial => "welcome/no_data" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<div style="text-align:center;" class="new_expand">
<div class="pages_user_show" style="width:auto; display:inline-block;margin: 18px 0;">
<ul id="school_report_ref_pages">
<%= pagination_links_full @obj_pages, @obj_count, per_page_links: false, remote: true, flag: true, is_new: true, path: managements_project_packages_path(params.except(:page)) %>
</ul>
<div class="cl"></div>
</div>
</div>

@ -0,0 +1,115 @@
<div class="edu-con-top clearfix xmt10 bor-grey-e mt10 padding10-20">
<%= form_tag(managements_project_packages_path,method: :get, remote: true, id: '', class: 'management-project-packages-form' ) do %>
<select name="status" class="fl winput-100-30">
<option value="">全部状态</option>
<option value="pending">草稿</option>
<option value="applying">待审核</option>
<option value="published">竞标中</option>
<option value="finished">已完成</option>
</select>
<%= text_field_tag :keyword, params[:keyword], placeholder: '请输入任务标题进行搜索', class: 'fl winput-200-30 ml10' %>
<%= text_field_tag :creator_name, params[:creator_name], placeholder: '请输入发布者进行搜索',
class: 'fl winput-200-30 ml10' %>
<div class="fl ml20 font-14">
<span>发布时间:</span>
<%= text_field_tag :begin_date, params[:begin_date],
class: 'winput-150-30', placeholder: '请选择开始时间'%>
<%= text_field_tag :end_date, params[:end_date],
class: 'winput-150-30', placeholder: '请选择结束时间'%>
</div>
<%= link_to '清除', 'javascript:clearSearchCondition()', class: 'fr task-btn ml5 reset-btn' %>
<%= link_to '搜索', 'javascript:void(0)', class: 'fr task-btn task-btn-orange ml5 search-btn' %>
<% end %>
</div>
<div class="edu-con-bg01 mt15" id="managements-project-packages-list" style="min-height: 400px;">
<%= render partial: 'managements/project_packages/project_package_list' %>
</div>
<script>
$(function(){
$('.search-btn').on('click', function() {
$(".management-project-packages-form").submit();
});
$('#managements-project-packages-list').on('click', '.delete-btn', function(){
var id = $(this).data("id");
op_confirm_tip_1("是否确认删除?", "deleteProjectPackage(" + id + ");")
});
var locale = {
clearText: '清除',
clearStatus: '清除已选日期',
closeText: '关闭',
closeStatus: '不改变当前选择',
prevText: '< 上月',
prevStatus: '显示上月',
prevBigText: '<<',
prevBigStatus: '显示上一年',
nextText: '下月>',
nextStatus: '显示下月',
nextBigText: '>>',
nextBigStatus: '显示下一年',
currentText: '今天',
currentStatus: '显示本月',
monthNames: ['一月','二月','三月','四月','五月','六月', '七月','八月','九月','十月','十一月','十二月'],
monthNamesShort: ['一月','二月','三月','四月','五月','六月', '七月','八月','九月','十月','十一月','十二月'],
monthStatus: '选择月份',
yearStatus: '选择年份',
weekHeader: '周',
weekStatus: '年内周次',
dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'],
dayNamesMin: ['日','一','二','三','四','五','六'],
dayStatus: '设置 DD 为一周起始',
dateStatus: '选择 m月 d日, DD',
dateFormat: 'yy-mm-dd',
firstDay: 1,
initStatus: '请选择日期',
isRTL: false
};
var baseOptions = {
dateFormat: 'yy-mm-dd',
maxDate: -1
}
var options = $.extend({}, locale, baseOptions)
$('input[name="begin_date"]').datepicker(options);
$('input[name="end_date"]').datepicker(options);
});
function clearSearchCondition(){
var form = $(".management-project-packages-form");
form.find("select[name='status']").val("");
form.find("input[name='keyword']").val("");
form.find("input[name='creator_name']").val("");
form.find("input[name='begin_date']").val("");
form.find("input[name='end_date']").val("");
form.submit();
}
function deleteProjectPackage(id){
$.ajax({
type: "DELETE",
url: "<%= managements_project_packages_path %>/" + id,
success: function (data) {
$('.popupAll').remove();
if(data && data.status == 0){
$('#managements-project-packages-list .project-package-item.project-package-' + id).remove();
if($('#managements-project-packages-list .project-package-item').length == 0){
$.ajax({ dataType: 'script', url: "<%= raw managements_project_packages_path(params.except(:controller, :action)) %>" });
}
} else {
notice_box(data.message);
}
}
});
return true;
}
</script>

@ -0,0 +1 @@
$("#managements-project-packages-list").html("<%= j(render 'managements/project_packages/project_package_list') %>")

@ -0,0 +1,142 @@
<% if @type == 'p_package' && User.current == @user %>
<div class="edu-back-white padding20-30 clearfix secondNav educontent bor-top-greyE">
<li class="<%= @p == 'a' ? 'active' : '' %>"><%= link_to "全部", user_path(@user, :type => @type), :remote => true %></li>
<li class="<%= @p == '0' ? 'active' : '' %>"><%= link_to "未发布", user_path(@user, :type => @type, :p => "0"), :remote => true %></li>
<li class="<%= @p == '1' ? 'active' : '' %>"><%= link_to "竞标中", user_path(@user, :type => @type, :p => "1"), :remote => true %></li>
<li class="<%= @p == '2' ? 'active' : '' %>"><%= link_to "已完成", user_path(@user, :type => @type, :p => "2"), :remote => true %></li>
</div>
<% elsif @type == 'l_package' && User.current == @user %>
<div class="edu-back-white padding20-30 clearfix secondNav educontent bor-top-greyE">
<li class="<%= @p == 'a' ? 'active' : '' %>"><%= link_to "全部", user_path(@user, :type => @type, :p => "a"), :remote => true %></li>
<li class="<%= @p == '0' ? 'active' : '' %>"><%= link_to "未中标", user_path(@user, :type => @type, :p => "0"), :remote => true %></li>
<li class="<%= @p == '1' ? 'active' : '' %>"><%= link_to "已中标", user_path(@user, :type => @type, :p => "1"), :remote => true %></li>
</div>
<% end %>
<div class="pl25 pr25 clearfix font-12 mt20 mb20 educontent">
<span class="fl color-grey-9">共<%= @objects_count %>个</span>
<div class="fr edu-menu-panel">
<%= link_to '发布时间', user_path(@user, :order => @new_order, :sort => @sort, :type => @type), :class => "fl color-grey-9", :remote => true %>
</div>
</div>
<% if @objects_count > 0 %>
<div class="educontent project-packages-list">
<% @objects.each do |object| %>
<% can_manage = @type == 'p_package' && (admin_or_business? || current_user.id == @user.id) %>
<div class="project-package-item <%= can_manage ? 'with-operator' : '' %> project-package-<%= object.id %>">
<div class="item-image">
<%= image_tag("educoder/project_packages/#{object.category}.png") %>
</div>
<div class="item-body">
<div class="item-head">
<div class="item-head-title" data-tip-down="<%= object.title %>">
<%= link_to object.title, project_package_path(object) %>
</div>
<div class="item-head-tags">
<% if object.creator_id != @user.id %>
<% bidding_user = object.bidding_users.find_by_user_id(@user.id) %>
<%= raw content_tag(:span, bidding_user.status_text, class: bidding_user.status) %>
<% end %>
</div>
<div class="item-head-blank"></div>
<div class="item-head-price">
<% if object.max_price && object.max_price != object.min_price %>
<span>¥</span><%= object.min_price %>~<span>¥</span><%= object.max_price %>
<% else %>
<span>¥</span><%= object.min_price %>
<% end %>
</div>
</div>
<div class="item-category">
<div class="item-category-item"><%= object.category_text %></div>
</div>
<div class="item-other">
<div class="item-group item-other-visit">
<span class="item-group-icon"><i class="fa fa-eye"></i></span>
<span class="item-group-text"><%= object.visit_count %>人浏览</span>
</div>
<div class="item-group item-other-deadline">
<% if object.published? %>
<span class="item-group-icon"><i class="fa fa-clock-o"></i></span>
<span class="item-group-text">
<% if Time.now + 10.days > object.deadline_at %>
<%= time_from_future(object.deadline_at) %>内竞标截止
<% else %>
<%= object.deadline_at.strftime('%Y-%m-%d') %> 竞标截止
<% end %>
</span>
<% end %>
</div>
<div class="item-group item-other-bidding">
<% if object.bidding_users_count > 0 %>
<span class="item-group-icon"><i class="fa fa-user-o"></i></span>
<span class="item-group-text"><%= object.bidding_users_count %>人竞标</span>
<% end %>
</div>
<div class="item-other-blank"></div>
<div class="item-group item-other-publish-at">
<% if object.published_at.present? %>
<span class="item-group-text">发布日期:<%= object.published_at.try(:strftime, '%Y-%m-%d') %></span>
<% end %>
</div>
</div>
</div>
<% if can_manage && (object.editable? || object.deletable?) %>
<div class="item-operator">
<% if object.editable? %>
<a href="<%= edit_project_package_path(object) %>" title="编辑"><i class="fa fa-pencil"></i></a>
<% end %>
<% if object.deletable? %>
<a href="javascript:void(0);"
class="delete-project-package-btn"
data-id="<%= object.id %>"
title="删除"><i class="fa fa-trash-o"></i></a>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
<div class="educontent edu-txt-center mb80">
<div class="inline pages_user_show">
<ul>
<%= pagination_links_full @obj_pages, @obj_count, :per_page_links => false, :remote => true, :flag => true, :is_new => true %>
</ul>
<div class="cl"></div>
</div>
</div>
<% else %>
<%= render :partial => "welcome/no_data" %>
<% end %>
<script>
$(function(){
$(".delete-project-package-btn").on("click", function(){
var id = $(this).data("id")
op_confirm_tip_1("是否确认删除?", "deleteProjectPackage(" + id + ");")
});
});
function deleteProjectPackage(id) {
$.ajax({
type: "DELETE",
url: "<%= project_packages_path %>/" + id,
success: function (data) {
$('.popupAll').remove();
if(data && data.status == 0){
$('.project-packages-list .project-package-item.project-package-' + id).remove();
if($('.project-packages-list .project-package-item').length == 0){
$.ajax({ dataType: 'script', url: "<%= raw user_path(@user, params.except(:controller, :action)) %>" });
}
} else {
notice_box(data.message);
}
}
});
return true;
}
</script>

@ -1,6 +1,8 @@
<% current_user = @user == User.current %>
<% str = current_user ? '我' : 'TA' %>
<% show_package = @user.project_packages.count.nonzero? || @user.bidding_project_packages.count.nonzero? %>
<script>
function showedit_headphoto() {
var html = "<%= escape_javascript(render :partial => "users/upload_avatar") %>";
@ -170,6 +172,9 @@
<% if @user.user_extensions.try(:identity) == 0 && current_user %>
<li class="<%= @type == 'm_bank' ? 'active' : '' %>" id="user_bank"><%= link_to "题库", user_path(@user, :type => "m_bank"), :remote => true %></li>
<% end %>
<% if show_package %>
<li class="<%= %w(a_package p_package l_package).include?(@type) ? 'active' : '' %>" id="user_package"><%= link_to "众包", user_path(@user, :type => "a_package"), :remote => true %></li>
<% end %>
</div>
</div>
</div>
@ -235,6 +240,15 @@
<li class="<%= @type == 'p_bank' ? 'active' : '' %>" id="p_bank"><%= link_to "公共题库", user_path(@user, :type => "p_bank"), :remote => true %></li>
</div>
<% end %>
<% if show_package %>
<!-- 众包 -->
<div class="white-panel edu-back-white pt25 pb25 clearfix <%= %w(a_package p_package l_package).include?(@type) ? '' : 'none' %>">
<li class="active" id="a_package"><%= link_to "全部", user_path(@user, :type => "a_package"), :remote => true %></li>
<li id="p_package"><%= link_to str + "管理的", user_path(@user, :type => "p_package"), :remote => true %></li>
<li id="l_package"><%= link_to str + "参与的", user_path(@user, :type => "l_package"), :remote => true %></li>
</div>
<% end %>
</div>
<div id="user_object_list">
@ -248,6 +262,8 @@
<%= render :partial => "users/l_shixun" %>
<% elsif @type == 'a_path' || @type == 'l_path' || @type == "p_path" %>
<%= render :partial => "users/p_path" %>
<% elsif %w(a_package p_package l_package).include?(@type) %>
<%= render :partial => "users/project_package" %>
<% end %>
</div>

@ -14,6 +14,8 @@
$("#user_project").addClass("active");
<% when "m_bank", "p_bank" %>
$("#user_bank").addClass("active");
<% when "p_package", "l_package" %>
$("#user_package").addClass("active");
<% end %>
$(".white-panel").hide();
$("#<%= @type %>").parent().show();
@ -29,5 +31,7 @@
$("#user_object_list").html("<%= j (render :partial => "p_path") %>");
<% when "m_bank", "p_bank" %>
$("#user_object_list").html("<%= j (render :partial => "m_bank") %>");
<% when "a_package", "p_package", 'l_package' %>
$("#user_object_list").html("<%= j (render :partial => "project_package") %>");
<% end %>
<% end %>

@ -0,0 +1,6 @@
'zh':
bidding_user:
status:
pending: 竞标中
bidding_won: 已中标
bidding_lost: 未中标

@ -0,0 +1,18 @@
zh:
project_package:
category:
front: 前端开发
backend: 后端开发
mobile: 移动开发
database: 数据库
cloud_compute_and_big_data: 云计算与大数据
devops_and_test: 运维与测试
ai: 人工智能
other: 其它
status:
pending: 已创建
applying: 审核中
refused: 已拒绝
published: 竞标中
bidding_ended: 待选标
bidding_finished: 已完成

@ -57,6 +57,10 @@ RedmineApp::Application.routes.draw do ## oauth相关
get :publish_success, on: :collection
end
resources :project_packages, only: [:index, :show, :new, :edit, :destroy] do
get :apply_success, on: :member
end
resources :ec_course_evaluations do
member do
match 'import_score', :via => [:post]
@ -527,6 +531,10 @@ RedmineApp::Application.routes.draw do ## oauth相关
end
end
namespace :managements do
resources :project_packages, only: [:index, :destroy]
end
resources :managements do
collection do
get 'evaluate_simple'
@ -746,9 +754,17 @@ RedmineApp::Application.routes.draw do ## oauth相关
post :refuse
end
end
resources :project_package_applies, only: [:index] do
member do
post :agree
post :refuse
end
end
end
end
end
# Enable Grack support
# mount Trustie::Grack.new, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post]

@ -1,3 +1,4 @@
# encoding=utf-8
class TransferCompetitionPersonalEnrollData < ActiveRecord::Migration
CODES = %W(2 3 4 5 6 7 8 9 a b c f e f g h i j k l m n o p q r s t u v w x y z)

@ -1,3 +1,4 @@
# encoding=utf-8
class CreateLibraryTags < ActiveRecord::Migration
def change
create_table :library_tags do |t|

@ -0,0 +1,28 @@
class CreateProjectPackages < ActiveRecord::Migration
def change
create_table :project_packages do |t|
t.references :creator
t.string :category
t.string :status
t.string :title
t.text :content
t.string :contact_name
t.string :contact_phone
t.decimal :min_price
t.decimal :max_price
t.integer :visit_count, default: 0
t.integer :bidding_users_count, default: 0
t.datetime :deadline_at
t.datetime :published_at
t.datetime :bidding_finished_at
t.timestamps
end
add_index :project_packages, :published_at
end
end

@ -0,0 +1,12 @@
class CreateBiddingUsers < ActiveRecord::Migration
def change
create_table :bidding_users do |t|
t.references :project_package
t.references :user
t.string :status
t.timestamps
end
end
end

@ -0,0 +1,14 @@
class CreateProjectPackageApplies < ActiveRecord::Migration
def change
create_table :project_package_applies do |t|
t.references :project_package
t.string :status
t.string :reason
t.datetime :refused_at
t.timestamps
end
end
end

@ -0,0 +1,8 @@
class AddIndexToProjectPackages < ActiveRecord::Migration
def change
add_index :project_packages, :creator_id
add_index :project_package_applies, :project_package_id
add_index :bidding_users, :project_package_id
add_index :bidding_users, :user_id
end
end

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

File diff suppressed because one or more lines are too long

@ -73,6 +73,10 @@ const ForumsIndexComponent = Loadable({
loading: Loading,
})
const ProjectPackages=Loadable({
loader: () => import('./modules/projectPackages/ProjectPackageIndex'),
loading: Loading,
})
const ECIndexComponent = Loadable({
loader: () => import('./modules/ec'),
@ -220,6 +224,8 @@ class App extends Component {
</Route>
<Route path="/fork_list" component={TPMshixunfork_listIndexComponent}>
</Route> */}
{/*众包*/}
<Route path={"/project_packages"} component={ProjectPackages}/>
<Route path="/forums" component={ForumsIndexComponent}>
</Route>

@ -25,6 +25,12 @@ class NotFoundPage extends Component {
<Link to="/test">test</Link>
|
<Link to="/demo">上海社区</Link>
|
<Link to={"/project_packages"}>zb</Link>
|
<Link to={"/project_packages/new"}>zbne</Link>
</div>
);
}

@ -0,0 +1,46 @@
import moment from "moment";
// 处理整点 半点
// 取传入时间往后的第一个半点
export function handleDateString(dateString) {
if (!dateString) return dateString;
const ar = dateString.split(':')
if (ar[1] == '00' || ar[1] == '30') {
return dateString
}
const miniute = parseInt(ar[1]);
if (miniute < 30 || miniute == 60) {
return [ar[0], '30'].join(':')
}
if (miniute < 60) {
// 加一个小时
const tempStr = [ar[0], '00'].join(':');
const format = "YYYY-MM-DD HH:mm:ss";
const _moment = moment(tempStr, format)
_moment.add(1, 'hours')
return _moment.format(format)
}
return dateString
}
// 给moment对象取下一个半点或整点
export function getNextHalfHourOfMoment(moment) {
if (!moment) {
return moment
}
const minutes = moment.minutes()
if (minutes < 30) {
moment.minutes(30)
} else if (minutes < 60) {
moment.minutes(0).add(1, 'hours')
}
return moment
}
export function formatDuring(mss){
var days = parseInt(mss / (1000 * 60 * 60 * 24));
var hours = parseInt((mss % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
var minutes = parseInt((mss % (1000 * 60 * 60)) / (1000 * 60));
return days + "天" + hours + "小时" + minutes + "分";
}

@ -0,0 +1,36 @@
import { bytesToSize } from 'educoder';
export function isImageExtension(fileName) {
return fileName ? !!(fileName.match(/.(jpg|jpeg|png|gif)$/i)) : false
}
export function markdownToHTML(oldContent, selector) {
window.$('#md_div').html('')
// markdown to html
var markdwonParser = window.editormd.markdownToHTML("md_div", {
markdown: oldContent,
emoji: true,
htmlDecode: "style,script,iframe", // you can filter tags decode
taskList: true,
tex: true, // 默认不解析
flowChart: true, // 默认不解析
sequenceDiagram: true // 默认不解析
});
const content = window.$('#md_div').html()
if (selector) {
window.$(selector).html(content)
}
return content
}
export function appendFileSizeToUploadFile(item) {
return `${item.title}${uploadNameSizeSeperator}${item.filesize}`
}
export function appendFileSizeToUploadFileAll(fileList) {
return fileList.map(item => {
if (item.name.indexOf(uploadNameSizeSeperator) == -1) {
return Object.assign({}, item, {name: `${item.name}${uploadNameSizeSeperator}${bytesToSize(item.size)}`})
}
return item
})
}
export const uploadNameSizeSeperator = '  '

@ -1,6 +1,6 @@
export function bytesToSize(bytes) {
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0 Byte';
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0 Byte';
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return parseFloat(bytes / Math.pow(1024, i), 2).toFixed(1) + ' ' + sizes[i];
}

@ -13,6 +13,10 @@ export { updatePageParams as updatePageParams } from './RouterUti
export { bytesToSize as bytesToSize } from './UnitUtil';
export { markdownToHTML, uploadNameSizeSeperator, appendFileSizeToUploadFile, appendFileSizeToUploadFileAll, isImageExtension } from './TextUtil'
export { handleDateString, getNextHalfHourOfMoment,formatDuring } from './DateUtil'
export { isDev as isDev } from './Env'
export { toStore as toStore, fromStore as fromStore } from './Store'

@ -39,11 +39,9 @@ window.__useKindEditor = false;
// TODO 读取到package.json中的配置
var proxy = "http://localhost:3000"
// proxy = "http://testbdweb.trustie.net"
proxy = "http://testbdweb.educoder.net"
// proxy = 'http://192.168.0.195:3000'
proxy ='https://testbdweb.educoder.net'
proxy ='http://testbdweb.educoder.net'
// proxy='https://www.educoder.net'
// proxy = "http://testbdweb.educoder.net"
// proxy = 'http://192.168.2.63:3001'
// proxy='https://www.educoder.net'
const requestMap={};
// 在这里使用requestMap控制避免用户通过双击等操作发出重复的请求
// 如果需要支持重复的请求考虑config里面自定义一个allowRepeat参考来控制

@ -204,4 +204,9 @@
margin-right: 50px;
position: relative;
bottom: 12px;
}
.attachment .icon-fujian {
margin-top: -3px !important;
font-size: 14px !important;
}

@ -0,0 +1,336 @@
import React, {Component} from 'react';
import {Input, Select, Radio, Checkbox, Popconfirm, message, Modal} from 'antd';
import {BrowserRouter as Router, Route, Link, Switch} from "react-router-dom";
import { getImageUrl, toPath, getUrl } from 'educoder';
require('codemirror/lib/codemirror.css');
let origin = getUrl();
let path = getUrl("/editormd/lib/")
const $ = window.$;
let timeout;
let currentValue;
const Option = Select.Option;
const RadioGroup = Radio.Group;
// 保存数据
function md_add_data(k,mdu,d){
window.sessionStorage.setItem(k+mdu,d);
}
// 清空保存的数据
function md_clear_data(k,mdu,id){
window.sessionStorage.removeItem(k+mdu);
var id1 = "#e_tip_"+id;
var id2 = "#e_tips_"+id;
if(k == 'content'){
$(id2).html(" ");
}else{
$(id1).html(" ");
}
}
window.md_clear_data = md_clear_data
// editor 存在了jquery对象上应用不需要自己写md_rec_data方法了
function md_rec_data(k, mdu, id) {
if (window.sessionStorage.getItem(k + mdu) !== null) {
var editor = $("#e_tips_" + id).data('editor');
editor.setValue(window.sessionStorage.getItem(k + mdu));
// debugger;
// /shixuns/b5hjq9zm/challenges/3977/tab=3 setValue可能导致editor样式问题
md_clear_data(k, mdu, id);
}
}
window.md_rec_data = md_rec_data;
function md_elocalStorage(editor,mdu,id){
if (window.sessionStorage){
var oc = window.sessionStorage.getItem('content'+mdu);
if(oc !== null ){
console.log("#e_tips_"+id)
$("#e_tips_"+id).data('editor', editor);
var h = '您上次有已保存的数据,是否<a style="cursor: pointer;" class="link-color-blue" onclick="md_rec_data(\'content\',\''+ mdu + '\',\'' + id + '\')">恢复</a> ? / <a style="cursor: pointer;" class="link-color-blue" onclick="md_clear_data(\'content\',\''+ mdu + '\',\'' + id + '\')">不恢复</a>';
$("#e_tips_"+id).html(h);
}
setInterval(function() {
var d = new Date();
var h = d.getHours();
var m = d.getMinutes();
var s = d.getSeconds();
h = h < 10 ? '0' + h : h;
m = m < 10 ? '0' + m : m;
s = s < 10 ? '0' + s : s;
if(editor.getValue().trim() != ""){
md_add_data("content",mdu,editor.getValue());
var id1 = "#e_tip_"+id;
var id2 = "#e_tips_"+id;
var textStart = " 数据已于 "
var text = textStart + h + ':' + m + ':' + s +" 保存 ";
// 占位符
var oldHtml = $(id2).html();
if (oldHtml && oldHtml != ' ' && oldHtml.startsWith(textStart) == false) {
$(id2).html( oldHtml.split(' (')[0] + ` (${text})`);
} else {
$(id2).html(text);
}
// $(id2).html("");
}
},10000);
}else{
$("#e_tip_"+id).after('您的浏览器不支持localStorage.无法开启自动保存草稿服务,请升级浏览器!');
}
}
function create_editorMD(id, width, high, placeholder, imageUrl, callback, initValue,
onchange, watch, { noStorage, showNullButton }, that) {
// 还是出现了setting只有一份被共用的问题
var editorName = window.editormd(id, {
width: width,
height: high===undefined?400:high,
path: path, // "/editormd/lib/"
markdown : initValue,
dialogLockScreen: false,
watch:watch===undefined?true:watch,
syncScrolling: "single",
tex: true,
tocm: true,
emoji: true,
taskList: true,
codeFold: true,
searchReplace: true,
htmlDecode: "style,script,iframe",
sequenceDiagram: true,
autoFocus: false,
// mine
toolbarIcons: function (mdEditor) {
//
// let react_id = `react_${id}`;
// const __that = window[react_id]
// Or return editormd.toolbarModes[name]; // full, simple, mini
// Using "||" set icons align right.
const icons = ["bold", "italic", "|", "list-ul", "list-ol", "|", "code", "code-block", "|", "testIcon", "testIcon1", '|', "image", "table", '|', "watch", "clear"];
// if (__that.props.showNullButton) {
// icons.push('nullBtton')
// }
return icons
},
toolbarCustomIcons: {
testIcon: "<a type=\"inline\" class=\"latex\" ><div class='zbg'></div></a>",
testIcon1: "<a type=\"latex\" class=\"latex\" ><div class='zbg_latex'></div></a>",
nullBtton: "<a type=\"nullBtton\" class='pr' title='增加填空'><div class='border-left'><span></span></div><span class='fillTip'>点击插入填空项</span><i class=\"iconfont icon-edit font-16\"></i></a>",
},
//这个配置在simple.html中并没有但是为了能够提交表单使用这个配置可以让构造出来的HTML代码直接在第二个隐藏的textarea域中方便post提交表单。
saveHTMLToTextarea: true,
// 用于增加自定义工具栏的功能可以直接插入HTML标签不使用默认的元素创建图标
dialogMaskOpacity: 0.6,
placeholder: placeholder,
imageUpload: true,
imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp", "JPG", "JPEG", "GIF", "PNG", "BMP", "WEBP"],
imageUploadURL: imageUrl,//url
onchange: onchange,
onload: function() {
let _id = this.id // 如果要使用this这里不能使用箭头函数
let _editorName = this;
let react_id = `react_${_editorName.id}`;
const __that = window[react_id]
// this.previewing();
// let _id = id;
$("#" + _id + " [type=\"latex\"]").bind("click", function () {
_editorName.cm.replaceSelection("```latex");
_editorName.cm.replaceSelection("\n");
_editorName.cm.replaceSelection("\n");
_editorName.cm.replaceSelection("```");
var __Cursor = _editorName.cm.getDoc().getCursor();
_editorName.cm.setCursor(__Cursor.line - 1, 0);
});
$("#" + _id + " [type=\"inline\"]").bind("click", function () {
_editorName.cm.replaceSelection("$$$$");
var __Cursor = _editorName.cm.getDoc().getCursor();
_editorName.cm.setCursor(__Cursor.line, __Cursor.ch - 2);
_editorName.cm.focus();
});
$("[type=\"inline\"]").attr("title", "行内公式");
$("[type=\"latex\"]").attr("title", "多行公式");
if (__that.props.showNullButton) {
const NULL_CH = '▁'
// const NULL_CH = ''
// const NULL_CH = '🈳'
$("#" + _id + " [type=\"nullBtton\"]").bind("click", function () {
_editorName.cm.replaceSelection(NULL_CH);
// var __Cursor = _editorName.cm.getDoc().getCursor();
// _editorName.cm.setCursor(__Cursor.line - 1, 0);
});
}
if (noStorage == true) {
} else {
md_elocalStorage(_editorName, `MDEditor__${_id}`, _id);
}
callback && callback(_editorName)
}
});
return editorName;
}
export default class MDEditors extends Component {
constructor(props) {
super(props)
this.state = {
initValue: ''
}
}
componentDidUpdate(prevProps, prevState) {
// 不能加,影响了试卷填空题
// if (this.props.initValue != prevProps.initValue) {
// this.answers_editormd.setValue(this.props.initValue)
// }
}
// react_mdEditor_
componentDidMount = () => {
const { mdID, initValue, placeholder, showNullButton} = this.props;
let _id = `mdEditor_${mdID}`
this.contentChanged = false;
const _placeholder = placeholder || "";
// amp;
// 编辑时要传memoId
const imageUrl = `/api/attachments.json`;
// 创建editorMd
let react_id = `react_${_id}`;
window[react_id] = this
const answers_editormd = create_editorMD(_id, '100%', this.props.height, _placeholder, imageUrl, (__editorName) => {
react_id = `react_${__editorName.id}`;
const that = window[react_id]
setTimeout(() => {
console.log('timeout', __editorName.id)
__editorName.resize()
__editorName.cm && __editorName.cm.refresh()
}, that.props.refreshTimeout || 500)
if (that.props.initValue != undefined && that.props.initValue != '') {
__editorName.setValue(that.props.initValue)
}
if (that.state.initValue) {
__editorName.setValue(that.state.initValue)
}
__editorName.cm.on("change", (_cm, changeObj) => {
that.contentChanged = true;
if (that.state.showError) {
that.setState({showError: false})
}
that.onEditorChange()
})
that.props.onCMBlur && __editorName.cm.on('blur', () => {
that.props.onCMBlur()
})
that.props.onCMBeforeChange && __editorName.cm.on('beforeChange', (cm,change) => {
that.props.onCMBeforeChange(cm,change)
})
that.answers_editormd = __editorName;
window[_id] = __editorName;
}, initValue, this.onEditorChange,this.props.watch, {
noStorage: this.props.noStorage,
showNullButton: this.props.showNullButton
}, this);
}
showError = () => {
this.setState({showError: true})
}
onEditorChange = () => {
if (!this.answers_editormd) return;
const val = this.answers_editormd.getValue();
try {
this.props.onChange && this.props.onChange(val)
} catch(e) {
// http://localhost:3007/courses/1309/common_homeworks/6566/setting
// 从这个页面,跳转到编辑页面,再在编辑页面点击返回的时候,这里会报错
console.error('出错')
console.error(e)
}
}
resize = () => {
if (!this.answers_editormd) { // 还未初始化
return;
}
this.answers_editormd.resize()
this.answers_editormd.cm && this.answers_editormd.cm.refresh()
this.answers_editormd.cm.focus()
}
getValue = () => {
try {
return this.answers_editormd.getValue()
} catch (e) {
return ''
}
}
setValue = (val) => {
try {
this.answers_editormd.setValue(val)
} catch (e) {
// TODO 这里多实例的时候前一个实例的state会被后面这个覆盖 参考NewWork.js http://localhost:3007/courses/1309/homework/9300/edit/1
// 未初始化
this.setState({ initValue: val })
}
}
render() {
let {
showError
} = this.state;
let { mdID, className, noStorage } = this.props;
let _style = {}
if (showError) {
_style.border = '1px solid red'
}
return (
<React.Fragment>
<div className={`df ${className} mt20`} >
{/* padding10-20 */}
<div className="edu-back-greyf5 radius4" id={`mdEditor_${mdID}`} style={{..._style}}>
<textarea style={{display: 'none'}} id="evaluate_script_show" name="content"></textarea>
<div className="CodeMirror cm-s-defualt">
</div>
</div>
</div>
<div className={"fr rememberTip"}>
{noStorage == true ? ' ' : <p id={`e_tips_mdEditor_${mdID}`} className="edu-txt-right color-grey-cd font-12"> </p>}
{/* {noStorage == true ? ' ' : <p id={`e_tips_mdEditor_${mdID}`} className="edu-txt-right color-grey-cd font-12"> </p>} */}
</div>
</React.Fragment>
)
}
}

@ -0,0 +1,25 @@
import React, {Component} from 'react';
import {Link} from "react-router-dom";
class PackageBanner extends Component {
constructor(props) {
super(props)
this.state = {
}
}
componentDidMount() {
}
render() {
return (
<div className="project_packagesHead" style={{height: '300px'}}></div>
)
}
}
export default PackageBanner;

@ -0,0 +1,282 @@
import React, {Component} from 'react';
import {Link} from "react-router-dom";
import axios from 'axios';
import { Input ,Icon,Button,Pagination} from 'antd';
import moment from 'moment';
import '../packageconcnet.css';
const { Search } = Input;
let categorylist=[
{name:"全部",value:undefined},
{name:"前端开发",value:"front"},
{name:"后端开发",value:"backend"},
{name:"移动开发",value:"mobile"},
{name:"数据库",value:"database"},
{name:"云计算和大数据",value:"cloud_compute_and_big_data"},
{name:"人工智能",value:"ai"},
{name:"其他",value:"other"},
]
function setcategorylist(val){
let vals=""
categorylist.some((item,key)=> {
if (item.value === val) {
vals=item.name
return true
}
}
)
return vals
}
class PackageConcent extends Component {
constructor(props) {
super(props)
this.state = {
data:undefined,
project_packages:undefined,
category:undefined,
keyword:undefined,
sort_by:"recently",
sort_direction:"desc",
page:1,
per_page:20,
}
}
//desc, desc, asc
//否 string 排序,默认最新, recently, price
// 否 string 类型, front,backend,mobile,database, cloud_compute_and_big_data,devops_and_test,ai,other
componentDidMount() {
let {category,keyword,sort_by,sort_direction,page}=this.state
this.setdatas(category,keyword,sort_by,sort_direction,page)
}
setdatas=(category,keyword,sort_by,sort_direction,page)=>{
let Url = `/api/v1/project_packages.json`;
axios.get(Url,{params:{
category:category,
keyword:keyword,
sort_by:sort_by,
sort_direction:sort_direction,
page:page,
per_page:20,
}}
).then((response) => {
this.setState({
data:response.data,
project_packages:response.data.project_packages
})
}).catch((error) => {
console.log(error)
})
}
setdatafuns=(value)=>{
let {category,keyword,sort_by,sort_direction,page}=this.state
this.setState({
keyword:value
})
this.setdatas(category,value,sort_by,sort_direction,page)
}
setcategory=(value)=>{
let {category,keyword,sort_by,sort_direction,page}=this.state
this.setState({
category:value
})
this.setdatas(value,keyword,sort_by,sort_direction,page)
}
setsort_byfun=(value)=>{
let {category,keyword,sort_by,sort_direction,page}=this.state
this.setState({
sort_by:value
})
let sort_directionvalue;
if(value===sort_by){
if(sort_direction==="desc"){
this.setState({
sort_direction:"asc"
})
sort_directionvalue="asc";
}else{
this.setState({
sort_direction:"desc"
})
sort_directionvalue="desc";
}
}else{
this.setState({
sort_direction:"desc"
})
sort_directionvalue="desc";
}
this.setdatas(category,keyword,value,sort_directionvalue,page)
}
render() {
let {data,page,category,sort_by,sort_direction,project_packages}=this.state;
return (
<div className="educontent clearfix" style={{flex: "1 0 auto"}}>
<div className="stud-class-set">
<div className="news">
<div className="edu-class-inner container clearfix">
<div className="member for-content-0 for-content">
<div className="people clearfix mb25">
{/*concent*/}
<div className="mb30">
<div className="clearfix">
<p className="clearfix" >
<p style={{height: '50px'}}>
<Search placeholder="输入标题名称进行检索"
style={{ width: 749}}
className="packinput"
enterButton={<span><Icon type="search" className="mr5"/> 搜索</span>}
onSearch={ (value)=>this.setdatafuns(value)} />
<Button type="primary" className="setissues fr" size={"large"}>
<a href="/project_packages/new" >发布需求</a>
</Button>
</p>
</p>
</div>
</div>
<div className="edu-back-white mb20">
<p className="clearfix padding30">
<p className="clearfix mb30 shaiContent">
<span className="shaiTitle fl mt3">类型</span>
<div className="fl pr shaiAllItem pagetype">
{categorylist.map((item,key)=>{
console.log(category)
console.log(item.value)
return(
<li key={key} className={category===item.value?"shaiItem shixun_repertoire active":"shaiItem shixun_repertoire"} value={item.value} onClick={()=>this.setcategory(item.value)}>{item.name}</li>
)
})}
</div>
</p>
<p className="clearfix shaiContent">
<span className="shaiTitle fl mt3">排序</span>
<div className="fl pr shaiAllItem">
<li className="shaiItem shixun_repertoire" value="recently" onClick={()=>this.setsort_byfun("recently")}>
<span className={sort_by==="recently"?"color-blue":""}>最新</span>
<sapn className="relativef">
<i className={sort_by==="recently"&&sort_direction==="asc"?
"iconfont icon-sanjiaoxing-up font-12 topsj color-blue" :"iconfont icon-sanjiaoxing-up font-12 topsj"}></i>
<i className={sort_by==="recently"&&sort_direction==="desc"?
"iconfont icon-sanjiaoxing-down font-12 bottomsj color-blue":"iconfont icon-sanjiaoxing-down font-12 bottomsj"}></i>
</sapn>
</li>
<li className="shaiItem shixun_repertoire " value="price" onClick={()=>this.setsort_byfun("price")}>
<span className={sort_by==="price"?"color-blue":""}>价格</span>
<sapn className="relativef">
<i className={sort_by==="price"&&sort_direction==="asc"?
"iconfont icon-sanjiaoxing-up font-12 topsj color-blue" :"iconfont icon-sanjiaoxing-up font-12 topsj"}></i>
<i className={sort_by==="price"&&sort_direction==="desc"?
"iconfont icon-sanjiaoxing-down font-12 bottomsj color-blue":"iconfont icon-sanjiaoxing-down font-12 bottomsj"}></i>
</sapn>
</li>
</div>
</p>
</p>
</div>
{project_packages&&project_packages.map((item,key)=>{
return(
<div className="educontent project-packages-list" key={key}>
<div className="project-package-item">
<div className="item-image">
<img src={"/images/educoder/project_packages/"+item.category+".png"}/>
</div>
<div className=" item-body">
<div className=" item-head mbf10">
<div className=" item-head-title">
<a className={"fl mt3 font-20 font-bd color-dark maxwidth700 "}
href={"/project_packages/"+item.id}
title={item.title}
>{item.title}</a>
</div>
<div className=" item-head-blank"></div>
<div className=" item-head-price">
<span>{item.min_price}</span>~<span>{item.max_price}</span>
</div>
</div>
<div className=" item-category">
<div className=" item-category-item">{setcategorylist(item.category)}</div>
</div>
<div className=" item-other">
<div className=" item-group item-other-visit">
<span className=" item-group-icon"><i className="fa fa-eye"></i></span>
<span className=" item-group-text">{item.visit_count}人浏览</span>
</div>
<div className=" item-group item-other-deadline">
<span className=" item-group-icon"><i className="fa fa-clock-o"></i></span>
<span className=" item-group-text">{moment(item.deadline_at).endOf('day').fromNow()}竞标截止</span>
</div>
<div className=" item-group item-other-bidding">
<span className=" item-group-icon"><i className="fa fa-user" ></i></span>
<span className=" item-group-text">{item.bidding_users_count}人竞标</span>
</div>
<div className=" item-other-blank"></div>
<div className=" item-group item-other-publish-at">
<span className=" item-group-text">发布于{moment(item.published_at).format("YYYY-MM-DD HH:mm")} </span>
</div>
</div>
</div>
</div>
</div>
)
})}
{project_packages&&project_packages.length===0?<div className="edu-back-white">
<div className="edu-tab-con-box clearfix edu-txt-center">
<img className="edu-nodata-img mb20" src="https://www.educoder.net/images/educoder/nodata.png" />
<p className="edu-nodata-p mb20">暂无数据哦~</p></div>
</div>:""}
<div className={"mt40"}>
<Pagination className="edu-txt-center" hideOnSinglePage={true} pageSize={20} current={page} total={data&&data.count} />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
}
export default PackageConcent;

@ -0,0 +1,30 @@
import React, { Component } from 'react';
import {BrowserRouter as Router,Route,Switch} from 'react-router-dom';
//业务组件
import PackageBanner from "./PackageBanner";
import PackageConcent from "./PackageConcent";
class PackageIndex extends Component{
constructor(props) {
super(props)
}
componentDidMount(){
}
render() {
return (
<div>
<div className="clearfix">
{/*头部banner*/}
<PackageBanner {...this.props}></PackageBanner>
{/*内容banner*/}
<PackageConcent {...this.props}></PackageConcent>
</div>
</div>
)
}
}
export default PackageIndex;

@ -0,0 +1,41 @@
import React, { Component } from 'react';
import { Spin, Icon , Modal,Input,Button} from 'antd';
class NEITaskDetailsModel extends Component {
constructor(props) {
super(props);
this.state = {
}
}
render() {
return(
<Modal
keyboard={false}
title="提示"
visible={this.props.applytype===undefined?false:this.props.applytype}
closable={false}
footer={null}
destroyOnClose={true}
centered={true}
width="530px"
>
<div className="task-popup-content">
<p className="task-popup-text-center font-16 mb20">
<div>{this.props.applyvalue}</div>
<div>{this.props.applybottom}</div>
</p>
<div className="clearfix mt30 edu-txt-center">
<a className="task-btn mr30 color_white" onClick={this.props.applycancel}>取消</a>
<a className="task-btn task-btn-orange " onClick={this.props.applyconfirm}>确定</a>
</div>
</div>
</Modal>
)
}
}
export default NEITaskDetailsModel;

@ -0,0 +1,450 @@
import React, {Component} from 'react';
import {Link} from "react-router-dom";
import axios from 'axios';
import { Input ,Icon,Button,Pagination,DatePicker,Breadcrumb} from 'antd';
import { handleDateString,markdownToHTML,bytesToSize} from 'educoder';
import NEITaskDetailsModel from './NEITaskDetailsModel';
import moment from 'moment';
import '../packageconcnet.css';
import './pds.css'
import gouxuan from './img/gouxuan.png'
import weigouxuan from './img/weigouxuan.png'
const { Search } = Input;
let categorylist=[
{name:"全部",value:undefined},
{name:"前端开发",value:"front"},
{name:"后端开发",value:"backend"},
{name:"移动开发",value:"mobile"},
{name:"数据库",value:"database"},
{name:"云计算和大数据",value:"cloud_compute_and_big_data"},
{name:"人工智能",value:"ai"},
{name:"其他",value:"other"},
]
function setcategorylist(val){
let vals=""
categorylist.some((item,key)=> {
if (item.value === val) {
vals=item.name
return true
}
}
)
return vals
}
class PackageIndexNEITaskDetails extends Component {
constructor(props) {
super(props)
this.contentMdRef = React.createRef();
this.state = {
data:undefined,
modalCancel: false,
overtype:false,
setbiddingmantype:false,
datalist:[]
}
}
componentDidMount() {
let url =`/api/v1/project_packages/${this.props.match.params.id}.json`;
axios.get(url).then((response) => {
this.setState({
data:response.data
})
}).catch((error) => {
console.log(error);
})
}
setbiddingman=()=>{
this.setState({
setbiddingmantype:true
})
}
notsetbiddingman=()=>{
let {data} =this.state;
let gouxuans2=data.bidding_users
for (var i=0;i<gouxuans2.length;i++){
if(gouxuans2[i].bool === true){
gouxuans2[i].bool=false;
}
}
this.setState({
setbiddingmantype:false,
datalist:[]
})
}
modalCancel=()=>{
this.setState({
modalCancel:false
})
}
setbiddingusers=()=>{
let{datalist}=this.state;
if(datalist.length>0){
this.setState({
applytype:true,
applyvalue:`选择的${datalist.length}个竞标者将被设定为“中标”`,
applybottom:"是否确认执行?",
applycancel:this.setApplycancel,
applyconfirm:this.setApplysumbit
})
}
}
setApplysumbit=()=>{
this.setState({
applytype:false,
})
let{datalist}=this.state;
let newlist=[];
datalist.map((item,key)=>{
newlist.push(item.id)
})
let url=`/api/v1/project_packages/${this.props.match.params.id}/bidding_users/win.json`;
axios.post(url,{
user_ids:newlist
}).then((response) => {
if(response.data.status===0){
this.props.showSnackbar("提交成功");
}
}).catch((error) => {
console.log(error)
})
}
Clickteacher2=(e)=>{
let {data} =this.state;
let newlist=[]
let gouxuans2=data.bidding_users
for (var i=0;i<gouxuans2.length;i++){
if(gouxuans2[i].id === e){
// console.log("51");
// console.log(e);
if(gouxuans2[i].bool === true){
gouxuans2[i].bool=false;
}else{
gouxuans2[i].bool=true;
newlist.push(gouxuans2[i])
}
}else{
if(gouxuans2[i].bool === true){
newlist.push(gouxuans2[i])
}
}
}
console.log(newlist);
this.setState({
datalist:newlist,
})
}
setover=()=>{
this.setState({
overtype:true
})
}
setout=()=>{
this.setState({
overtype:false
})
}
deletePackages=()=>{
this.setState({
applytype:true,
applyvalue:"是否确认删除?",
applycancel:this.setApplycancel,
applyconfirm:this.setApplydelect
})
}
setApplydelect=()=>{
this.setState({
applytype:false,
})
let url=`/project_packages/${this.props.match.params.id}.json`;
axios.delete(url ).then((response) => {
// const status = response.data.status
console.log(response)
this.props.showSnackbar('删除成功');
}).catch((error) => {
console.log(error)
})
}
setBiddingApply=()=>{
this.setState({
applytype:true,
applyvalue:"是否确认报名?",
applycancel:this.setApplycancel,
applyconfirm:this.setApplyconfirm
})
}
setApplycancel=()=>{
this.setState({
applytype:false,
})
}
setApplyconfirm=()=>{
this.setState({
applytype:false,
})
let url=`/api/v1/project_packages/${this.props.match.params.id}/bidding_users.json`;
axios.post(url).then((response) => {
if(response.data.status===0){
this.props.showSnackbar(response.data.message);
}
}).catch((error) => {
console.log(error)
})
}
goback = () => {
window.history.go(-1)
}
render() {
let {overtype,data}=this.state;
console.log(data)
console.log(this.props)
return (
<div>
<div className="clearfix">
<NEITaskDetailsModel
applytype={this.state.applytype}
applyvalue={this.state.applyvalue}
applybottom={this.state.applybottom}
applycancel={this.state.applycancel}
applyconfirm={this.state.applyconfirm}
/>
<div className={"educontent mt20 mb50"}>
<Breadcrumb separator={'>'} className={"fl"}>
<Breadcrumb.Item>{this.props.current_user.login}</Breadcrumb.Item>
<Breadcrumb.Item>
<a href="/project_packages">任务大厅</a>
</Breadcrumb.Item>
<Breadcrumb.Item>详情</Breadcrumb.Item>
</Breadcrumb>
<a className="color-grey-6 fr font-15 mr20" onClick={this.goback}>返回</a>
<div className="mb20">
<p className="clearfix ">
<div className={"stud-class-set coursenavbox edu-back-white mt20"}>
<div className={"ant-row contentbox mdInForm "}>
<div className="educontent project-packages-list">
{data&&data.status==="pending"?<div>
<div className="publicpart orangeBlack "></div>
<span className="smalltrangle"></span>
<span className="publicword publicwords"> 未申请 </span>
</div>:data&&data.status==="applying"?<div>
<div className="publicpart orangeGreen"></div>
<span className="smalltrangle"></span>
<span className="publicword publicwords"> 待发布 </span>
</div>:""}
<div className="project-package-item project-package-items height185">
<div className="item-image">
<div className="fl edu-back-white ">
<img alt="头像" className="radius mt10 ml5" height="70" id="nh_user_logo" name="avatar_image"
src={data&&data.creator.avatar_url}
width="70"/>
<div className=" edu-back-white ml28 mt10 ">
{data&&data.creator.name}
</div>
{this.props.current_user.login!=data&&data.creator.login?<div className=" edu-back-white ml5 mt10 "
onMouseOver={this.setover}
onMouseOut={this.setout}
>
{overtype===false?<a className="ContacttheTA fl" href={`/users/${this.props.current_user.login}/private_messages`}> <img alt="头像" class="mr5" src={require('./newsone.png')} />联系TA</a>:
<a className="ContacttheTAs fl" href={`/users/${this.props.current_user.login}/private_messages`}> <img alt="头像" className="mr5"
src={require('./newstwo.png')}/>联系TA</a>}
</div>:""}
</div>
</div>
<div className=" item-body">
<div className=" item-head mbf10">
<div className=" item-head-title ">
<span className={"fl mt3 font-18 font-bd color-dark maxwidth700 "}
title={data&&data.title}
>{data&&data.title}
</span>
</div>
<div className=" item-head-price mtf7">
<span><span className={"font-24"}>{data&&data.min_price}</span></span>~<span><span className={"font-24"}>{data&&data.max_price}</span></span>
</div>
</div>
<div className="item-category">
<div className="item-category-item">{setcategorylist(data&&data.category)}</div>
</div>
<div className=" item-other">
<div>
<span className="item-group-text">发布时间{moment(data&&data.published_at).format("YYYY-MM-DD HH:mm")}</span>
</div>
<div className=" item-group item-other-deadline ml40">
<span className="item-group-text">竞标截止时间{moment(data&&data.deadline_at).format("YYYY-MM-DD HH:mm")}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/*详情*/}
<div className={"stud-class-set padding30 coursenavbox edu-back-white mt20"}>
<div>
<div className={"longboxs"}>
需求详情
{data&&data.status==="pending"&&data&&data.operation.can_select_bidding_user===true?<div className="fr">
<a className="task-btn-nebules fr" href={`/project_packages/${this.props.match.params.id}/edit`}>编辑</a>
<a className="task-btn-nebules fr" onClick={this.deletePackages}>删除</a>
</div>:""}
</div>
<div className={"padding020"}>
<div className={"markdown-body"} dangerouslySetInnerHTML={{__html: markdownToHTML(data&&data.content).replace(/▁/g,"▁▁▁")}}></div>
</div>
</div>
{data&&data.attachments.length>0?<div>
<div className={"longboxs"}>
需求文件
</div>
{data&&data.attachments.map((item,key)=>{
return(
<div className={"newForm newFormbox mt10 "}>
<i className="color-green iconfont icon-fujian mr5 fl font-14 mt3"></i>
<a className="upload_filename color-grey readonly hidden fl mtf3 mr10 ml5" href={item.url}>{item.title} &nbsp; &nbsp;{bytesToSize(item.filesize)}</a>
</div>
)})}
</div>:""}
</div>
{/*发布者和竞选者状态show*/}
{this.state.setbiddingmantype===false?<div className={"stud-class-set coursenavbox edu-back-white mt20"}>
{/*下面是头像*/}
<div className={"stud-class-set pd30a0 coursenavbox edu-back-white"}>
<div className={"relativef"}>
<div className={"longboxs"}>
报名列表(12)
</div>
<div className="packageabsolute">
{data&&data.operation.can_bidding===true?<Button type="primary" className="defalutSubmitbtn fl ml20 defalutSubmitbtns" onClick={this.setBiddingApply}>竞标报名</Button>:""}
{data&&data.operation.can_select_bidding_user===true?<Button type="primary" className="defalutSubmitbtn fl ml20 defalutSubmitbtns" onClick={this.setbiddingman}>选择中标者</Button>:""}
<Button type="primary" className="defalutSubmitbtn fl ml20 defalutSubmitbtns" onClick={this.setbiddingman}>选择中标者</Button>
</div>
</div>
</div>
<div className={"ysllogin_register_contents edu-back-white "} style={{borderTop: '1px solid rgb(234, 234, 234)'}}>
<div className="ysllogin_section">
<div className="ysldivhome2">
<div style={{height: "20px"}}> </div>
{data&&data.bidding_users.map((item,key)=>{
return(
<div className="ysldivhomediv1 homehove">
<img className="div1img" src={item.avatar_url}/>
<div className="textall mt10" title={item.name}> <p className="ptext">{item.name}</p></div>
{this.props.current_user.login!=item.login?<a className="ContacttheTAs fl none" href={`/users/${item.login}/private_messages`}>
<img alt="头像" className="mr5" src={require('./newstwo.png')}/>联系TA
</a>:""}
</div>
)
})}
</div>
</div>
</div>
</div>:<div className={"stud-class-set coursenavbox edu-back-white mt20"}>
{/*发布人选择状态*/}
{/*下面是头像*/}
<div className={"stud-class-set pd30a0 coursenavbox edu-back-white"}>
<div className={"relativef"}>
<div className={"longboxs"}>
报名列表(12)
</div>
<div className="packageabsolute">
<div className=" fl mt10 mr20">
已选 <span className={"color-orange06"}>({this.state.datalist.length})</span>
</div>
<a className="defalutCancelbtns fl" onClick={this.notsetbiddingman}>取消</ a>
<Button type="primary" className="defalutSubmitbtn fl ml20 defalutSubmitbtns" onClick={this.setbiddingusers}>确定</Button>
</div>
</div>
</div>
<div className={"ysllogin_register_contents edu-back-white "} style={{borderTop: '1px solid rgb(234, 234, 234)'}}>
<div className="ysllogin_section">
<div className="ysldivhome2">
<div style={{height: "20px"}}> </div>
{data&&data.bidding_users.map((item,key)=>{
return(
<div className="ysldivhomediv1" onClick={()=>this.Clickteacher2(item.id)}>
{item.bool===true?<img src={gouxuan} className="yslgouxuanimg"/>:<img src={weigouxuan} className="yslgouxuanimg"/>}
<img className="div1img" src={item.avatar_url}/>
<span className={item.bool===true?"textall mt10 color-blue":"textall mt10"} title={item.name}> <p className="ptext">{item.name}</p></span>
</div>
)
})}
</div>
</div>
</div>
</div>}
</p>
</div>
</div>
</div>
</div>
)
}
}
export default PackageIndexNEITaskDetails;

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

@ -0,0 +1,53 @@
.ysldivhome2{
display: flex;
flex-flow: row wrap;
align-content:stretch;
flex-direction: row;
margin-bottom: 20px;
}
.ysllogin_register_contents{
display: flex;
margin-top: 20px;
/*justify-content: center;*/
background: #fff;
}
.ysldivhomediv1{
width: 80px;
height: 130px;
display: flex;
flex-direction:column;
margin-left: 48px;
margin-top: 20px;
}
.yslgouxuanimg{
width: 20px;
height: 20px;
margin-left: 64px;
}
.yslgouxuanimg2{
height: 20px;
}
.div1img{
display: flex;
justify-content:center;
width: 80px;
height: 80px;
border-radius:50%;
}
.textall{
text-align: center;
font-size: 13px;
color: #4B4B4B;
}
.ptext{
width: 80px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.ysllogin_section {
display: flex;
align-items: center;
flex-direction: column;
}

@ -0,0 +1,41 @@
import React, {Component} from 'react';
import {Link} from "react-router-dom";
import { Steps, Divider } from 'antd';
const { Step } = Steps;
class PackageIndexNEIBanner extends Component {
constructor(props) {
super(props)
this.state = {
current:0
}
}
componentDidMount() {
}
onChange=(current)=>{
debugger
console.log('onChange:', current);
this.setState({ current });
};
render() {
const { current } = this.state;
return (
<div className="edu-back-white mb20 PackageIndexNEIBanner">
<p className="clearfix padding110">
<Steps current={current} >
<Step title="发布需求"/>
<Step title="筛选合适的接包方"/>
<Step title="线下交易,完成实施"/>
</Steps>
</p>
</div>
)
}
}
export default PackageIndexNEIBanner;

@ -0,0 +1,695 @@
import React, {Component} from 'react';
import {Link} from "react-router-dom";
import axios from 'axios';
import { Input , Spin, Icon ,Button,Pagination,DatePicker} from 'antd';
import { handleDateString,getUrl,bytesToSize} from 'educoder';
import locale from 'antd/lib/date-picker/locale/zh_CN';
import MDEditors from '../MDEditors';
import PhoneModel from './PhoneModel';
import moment from 'moment';
import '../packageconcnet.css';
const { Search } = Input;
const $ = window.$;
let origin = getUrl();
// load
if (!window.postUpMsg) {
$.getScript(
`${origin}/javascripts/attachments.js`,
(data, textStatus, jqxhr) => {
});
}
function checkPhone(phone){
if(!(/^1[3456789]\d{9}$/.test(phone))){
// alert("手机号码有误,请重填");
return false;
}
}
class PackageIndexNEIBannerConcent extends Component {
constructor(props) {
super(props)
this.contentMdRef = React.createRef();
this.state = {
modalCancel:false,
getverificationcodes:true,
seconds:35,
springtype:false,
category:undefined,
title:undefined,
content:undefined,
attachment_ids:undefined,
deadline_at:undefined,
min_price:undefined,
max_price:undefined,
contact_name:undefined,
contact_phone:undefined,
code:undefined,
publish:false,
}
}
componentDidMount() {
if(this.props.match.params.id!=undefined){
let url=`/api/v1/project_packages/${this.props.match.params.id}.json`
axios.get((url)).then((response) => {
console.log(response)
let data=response.data
this.setState({
category:data.category,
title:data.title,
content:data.content,
deadline_at:moment(data.deadline_at),
min_price:data.min_price,
max_price:data.max_price,
contact_name:data.contact_name,
contact_phone:data.contact_phone,
attachments:data.attachments,
})
}).catch((error) => {
console.log(error);
})
}
// this.contentMdRef.current.setValue("测试赋值")
}
//获取验证码;
getverificationcode =()=>{
// if (this.state.logins&&this.state.logins.length === 0) {
// // 判断没有输入手机号
// return
// }
if(this.state.getverificationcodes === undefined){
console.log("undefined");
return;
}
if (this.state.getverificationcodes === true) {
this.setState({
getverificationcodes: undefined,
})
let timer = setInterval(() => {
this.setState((preState) => ({
seconds: preState.seconds - 1,
}), () => {
if (this.state.seconds == 0) {
clearInterval(timer);
this.setState({
getverificationcodes: false,
seconds: 35,
})
}
});
}, 1000)
//其他的网络请求也可以
this.SMSverification();
} else {
this.setState({
getverificationcodes: undefined,
})
let timer = setInterval(() => {
this.setState((preState) => ({
seconds: preState.seconds - 1,
}), () => {
if (this.state.seconds == 0) {
clearInterval(timer);
this.setState({
getverificationcodes: false,
seconds: 35,
})
}
});
}, 1000)
//其他的网络请求也可以
this.SMSverification();
}
}
//短信验证
SMSverification = () => {
let {contact_phone,code}=this.state;
var url = `/account/get_verification_code.json`;
axios.get((url), {
params: {
value: contact_phone,
type: 9,
}
}).then((result) => {
//验证有问题{"status":1,"message":"success"}
// console.log(result);
this.openNotification("验证码已发送,请注意查收!",2);
}).catch((error) => {
console.log(error);
})
}
onChangeTimePicker = (value, dateString) => {
this.setState({
deadline_at: moment(handleDateString(dateString))
})
}
setPublication=(type)=>{
const content = this.contentMdRef.current.getValue().trim();
// console.log(content)
// console.log(this.state.deadline_at._i)
this.setState({
publish:type
})
let types=type;
let {category,title,attachment_ids,deadline_at,min_price,max_price,contact_name,contact_phone,code,modalCancel}=this.state;
if(category===undefined||category===null||category===""){
this.setState({
categorytypes:true
})
this.scrollToAnchor("publishtimestart");
return
}
if(title===undefined||title===null||title===""){
this.setState({
titletypes:true
})
this.scrollToAnchor("publishtimestart");
return
}
if(content===undefined||content===null||content===""){
this.setState({
contenttypes:true
})
this.scrollToAnchor("publishtimestart");
return
}
if(deadline_at===undefined||deadline_at===null||deadline_at===""){
this.setState({
deadline_attypes:true
})
this.scrollToAnchor("publishtime");
return
}
if(moment(deadline_at)<moment(new Date())){
this.setState({
deadline_attypexy:true
})
return
}
if(parseInt(min_price)===undefined||parseInt(min_price)===null||parseInt(min_price)===""){
this.setState({
min_pricetype:true
})
return
}
if(parseInt(max_price)===undefined||parseInt(max_price)===null||parseInt(max_price)===""){
this.setState({
min_pricetype:true
})
return
}
if(parseInt(min_price)<=0||parseInt(max_price)<=0){
this.setState({
smallstype:true
})
return
}
if(parseInt(max_price)<parseInt(min_price)){
this.setState({
minmaxtype:true
})
return
}
if(contact_name===undefined||contact_name===""||contact_name===null){
this.setState({
contact_nametype:true
})
return
}
if(modalCancel===false){
if(this.props.current_user.phone===undefined||this.props.current_user.phone===null||this.props.current_user.phone===""){
this.setState({
current_userphonetype:true
})
return
}
}
if(modalCancel===true){
if(contact_phone===undefined||contact_phone===null||contact_phone===""){
this.setState({
contact_phonetype:true
})
return
}
if(checkPhone(contact_phone)===false){
this.setState({
contact_phonetypes:true
})
return
}
if(code===undefined||code===""||code===null){
this.setState({
codeypes:true
})
return
}
}
this.setState({
springtype:true
})
if(this.props.match.params.id===undefined){
const url = `/api/v1/project_packages.json`;
axios.post(url, {
category: category,
title: title,
content: content,
attachment_ids: attachment_ids,
deadline_at:deadline_at._i,
min_price:parseInt(min_price),
max_price:parseInt(max_price),
contact_name: contact_name,
contact_phone: contact_phone,
code:code,
publish:types
}
).then((response) => {
if(response.data.status===0){
if(type===true){
this.props.setPublicationfun(response.data.id)
}else{
window.location.href="/project_packages/"+response.data.id
}
this.setState({
springtype:false
})
}else if(response.data.status===-1){
if(response.data.message==="无效的验证码"){
this.setState({
codeypesno:true,
springtype:false
})
}
}
this.setState({
springtype:false
})
}).catch((error) => {
console.log(error)
this.setState({
springtype:false
})
})
}else{
// edit
const url = `/api/v1/project_packages/${this.props.match.params.id}.json`;
axios.put(url, {
category: category,
title: title,
content: content,
attachment_ids: attachment_ids,
deadline_at:deadline_at._i,
min_price:parseInt(min_price),
max_price:parseInt(max_price),
contact_name: contact_name,
contact_phone: contact_phone,
code:code,
publish:types
}
).then((response) => {
if(response.data.status===0){
if(type===true){
this.props.setPublicationfun(response.data.id)
}else{
window.location.href="/project_packages/"+response.data.id
}
this.setState({
springtype:false
})
}else if(response.data.status===-1){
if(response.data.message==="无效的验证码"){
this.setState({
codeypesno:true,
springtype:false
})
}
}
this.setState({
springtype:false
})
}).catch((error) => {
console.log(error)
this.setState({
springtype:false
})
})
}
}
modalCancel=()=>{
this.setState({
modalCancel:false,
contact_phone:undefined,
code:undefined,
})
}
editmodels=()=>{
this.setState({
modalCancel:true
})
}
setcategory=(value)=>{
this.setState({
category:value
})
}
settitlefun=(e)=>{
this.setState({
title:e.target.value
})
}
onChangemin_prices=(e)=>{
this.setState({
min_price:e.target.value
})
}
onChangemax_prices=(e)=>{
this.setState({
max_price:e.target.value
})
}
onChangeContact_name=(e)=>{
this.setState({
contact_name:e.target.value
})
}
onChangeContact_phone=(e)=>{
this.setState({
contact_phone:e.target.value
})
}
onChangeCode=(e)=>{
this.setState({
code:e.target.value
})
}
//跳转道描点的地方
scrollToAnchor = (anchorName) => {
if (anchorName) {
// 找到锚点
let anchorElement = document.getElementById(anchorName);
// 如果对应id的锚点存在就跳转到锚点
if(anchorElement) { anchorElement.scrollIntoView(); }
}
}
render() {
let {modalCancel,seconds,getverificationcodes,attachments,
category,title}=this.state;
let categorylist=[
{name:"前端开发",value:"front"},
{name:"后端开发",value:"backend"},
{name:"移动开发",value:"mobile"},
{name:"数据库",value:"database"},
{name:"云计算和大数据",value:"cloud_compute_and_big_data"},
{name:"人工智能",value:"ai"},
{name:"其他",value:"other"},
]
return (
<div className="mb20 touchSelect">
{/*<PhoneModel*/}
{/*modalsType={modalCancel}*/}
{/*modalCancel={this.modalCancel}*/}
{/*/>*/}
<Spin size="large" spinning={this.state.springtype} >
<p className="clearfix" id={"publishtimestart"}>
<div className={"stud-class-set pd30a0 coursenavbox edu-back-white pb20"}>
<div className={"ant-row contentbox mdInForm "}>
<div className="ant-form-item-label mb10">
<label htmlFor="coursesNew_description" className="ant-form-item-required font-16">请选择需求类型</label>
</div>
<p className="clearfix mb20 shaiContent" >
<div className="fl pr shaiAllItem pagetype">
{categorylist.map((item,key)=>{
return(
<li key={key} className={category===item.value?"shaiItem shixun_repertoire active borderccc":"shaiItem shixun_repertoire borderccc"} value={item.value} onClick={()=>this.setcategory(item.value)}>{item.name}</li>
)
})}
</div>
</p>
{this.state.categorytypes===true?<div className={"color-red"}>请选择类型</div>:""}
<div className="ant-form-item-label mb10">
<label htmlFor="coursesNew_description" className="ant-form-item-required font-16" >需求标题和详情</label>
</div>
<Input placeholder="请输入需求标题示例美食类APP开发最大限制60个字符" maxLength="60" className="input-100-40s mt5 fafafas"
value={title} onInput={this.settitlefun}/>
{this.state.titletypes===true?<div className={"color-red mt10"}>不能为空</div>:""}
<MDEditors ref={this.contentMdRef} placeholder="请填写清晰完整的需求内容" mdID={'courseContentMD'} refreshTimeout={1500}
watch={false} className="courseMessageMD" initValue={this.state.content}></MDEditors>
{/* 请求status 422 */}
{this.state.contenttypes===true?<div className={"color-red"}>不能为空</div>:""}
<div className="df uploadBtn">
<a href="javascript:void(0);" className="fl" onClick={() => window.$('#_file').click()}
data-tip-down="请选择文件上传">
{/*<i className="fa fa-upload mr5 color-blue"></i>*/}
<span className="color-blue">上传附件</span>
</a>
<span style={{ fontSize: "14px"}}>(最多可添加 <span className={"color-orange06"}>5</span> / <span className={"color-orange06"}>10MB</span>)</span>
</div>
{attachments&&attachments.map((item,key)=>{
return(
<div className={"newForm newFormbox mt10 "}>
<i className="color-green iconfont icon-fujian mr5 fl font-14 mt4"></i>
<a className="upload_filename color-grey readonly hidden fl mtf3 mr10 ml5" href={item.url}>{item.title} &nbsp; &nbsp;{bytesToSize(item.filesize)}</a>
</div>
)})}
<form className="newForm newFormbox mt10 ">
<span id={`attachments_fields`} className="attachments_fields"
xmlns="http://www.w3.org/1999/html">
</span>
<span className="add_attachment">
<input className="file_selector"
data-are-you-sure="您确定要删除吗?"
data-delete-all-files="您确定要删除所有文件吗"
data-description-placeholder="可选的描述"
data-field-is-public="公开"
data-file-count="个文件已上传"
data-lebel-file-uploding="个文件正在上传"
data-max-concurrent-uploads="2"
data-max-file-size-message="该文件无法上传。超过文件大小限制 (10 MB)建议上传到百度云等其他共享工具里然后在txt文档里给出链接以及共享密码并上传"
data-max-file-size="10485760" data-upload-path="/uploads.js"
id="_file"
multiple="multiple" name="attachments[dummy][file]"
onChange={() => {
debugger;
console.log(window.$('.file_selector')[0])
window.addInputFiles(window.$('.file_selector')[0])
}}
style={{'display': 'none'}}
type="file">
</input>
</span>
</form>
</div>
</div>
<div className={"stud-class-set padding30 coursenavbox edu-back-white"} style={{borderTop: '1px solid #EAEAEA'}} id={"publishtime"}>
<div className={"ant-row contentbox mdInForm "}>
<div className="ant-form-item-label mb10">
<label htmlFor="coursesNew_description" className="ant-form-item-required font-16">工期与预算</label>
</div>
<p className="clearfix mb20 shaiContent">
<span className="shaiTitle fl mt5 ml10">竞标截止</span>
<DatePicker
showToday={false}
showTime
locale={locale}
style={{"width": "260px"}}
format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择任务的竞标截止日期"
className={"fafas"}
value={this.state.deadline_at}
onChange={this.onChangeTimePicker}
/>
{this.state.deadline_attypes===true?<div className={"color-red ml100"}>不能为空</div>:""}
{this.state.deadline_attypexy===true?<div className={"color-red ml100"}>不能早于当前时间</div>:""}
</p>
<p className="clearfix mb20 shaiContent">
<span className="shaiTitle fl mt5 ml10">支付费用</span>
<Input
className={"fafas"}
style={{"width": "260px"}}
value={this.state.min_price}
placeholder="支付多少费用(最低)"
onInput={this.onChangemin_prices}
suffix={
<span >¥</span>
}
/>
<span className={"ml10 mr10"}></span>
<Input
className={"fafas"}
style={{"width": "260px"}}
value={this.state.max_price}
placeholder="支付多少费用(最高)"
onInput={this.onChangemax_prices}
suffix={
<span>¥</span>
}
/>
{this.state.min_pricetype===true?<div className={"color-red ml100"}>不能为空</div>:""}
{this.state.smallstype===true?<div className={"color-red ml100"}>不能小于零</div>:""}
{this.state.minmaxtype===true?<div className={"color-red ml100"}>最高费用不能小于最低费用</div>:""}
</p>
<div className="ant-form-item-label mb10">
<label htmlFor="coursesNew_description" className="ant-form-item-required font-16" >联系方式</label>
</div>
<p className="clearfix mb20 shaiContent">
<span className="shaiTitle fl mt5 ml38">姓名</span>
<Input
className={"fafafas"}
style={{"width": "260px"}}
value={this.state.contact_name}
placeholder="请输入姓名"
onInput={this.onChangeContact_name}
/>
{this.state.contact_nametype===true?<div className={"color-red ml100"}>不能为空</div>:""}
</p>
{modalCancel===false?<p className="clearfix mb20 shaiContent">
<span className="shaiTitle fl mt5 ml25">手机号</span>
<Input
className={"fafafas fl"}
style={{"width": "260px"}}
value={this.props.current_user.phone}
placeholder="请输入手机号"
disabled={true}
/>
<a className="fl ml20">
<i className="iconfont icon-bianjidaibeijing font-26 color-blue" onClick={()=>this.editmodels()}></i>
</a>
</p>:""}
{this.state.current_userphonetype===true?<div className={"color-red ml100"}>不能为空</div>:""}
{modalCancel===true?<p className="clearfix mb20 shaiContent">
<span className="shaiTitle mt5 fl">
<span className="shaiTitle fl mt5 ml25">
{/*未注册才显示!*/}
手机号</span>
<Input
className={"fafafas fl"}
style={{"width": "260px"}}
value={this.state.contact_phone}
placeholder="请输入手机号码"
onInput={this.onChangeContact_phone}
/>
{this.state.contact_phonetype===true?<div className={"color-red ml100"}>不能为空</div>:""}
{this.state.contact_phonetypes===true?<div className={"color-red ml100"}>请输入正确的手机号</div>:""}
</span>
<span className="shaiTitle mt5 ml17 fl">
<span>
<Search
style={{ width: 300 }}
value={this.state.code}
className="fafas"
placeholder="请输入验证码"
enterButton={
getverificationcodes === undefined ? <span>重新发送 ({seconds}s)</span>: getverificationcodes === true ?<span ></span> :<span ></span>}
onSearch={()=>this.getverificationcode()}
onInput={this.onChangeCode}
/>
{this.state.codeypes===true?<div className={"color-red"}>验证码不能为空</div>:""}
{this.state.codeypesno===true?<div className={"color-red"}>验证码不正确</div>:""}
</span>
<span>
{/*<Button type="primary" className="defalutSubmitbtn ml10 defalutSubmitbtnmodels">重新发送()</Button>*/}
</span>
</span>
<a className="fl mt13">
<span className="font-18 color-blue" onClick={()=>this.modalCancel()}>X</span>
</a>
</p>:""}
</div>
</div>
</p>
<div className="clearfix mt30 mb30">
<Button type="primary" className="defalutSubmitbtn fl mr20 defalutSubmitbtns" onClick={()=>this.setPublication(true)}>申请发布</Button>
<a className="defalutCancelbtns fl" onClick={()=>this.setPublication(false)}>保存</ a>
</div>
</Spin>
</div>
)
}
}
export default PackageIndexNEIBannerConcent;
// attachments:[
// {
// id: 206525,
// title: "412420b57ed8c141963d4c548bde551f",
// filesize: 18523,
// description: null,
// url: "/api/attachments/206525"
// }
// ]

@ -0,0 +1,50 @@
import React, {Component} from 'react';
import {Link} from "react-router-dom";
import { Icon ,Button} from 'antd';
class PackageIndexNEISubmit extends Component {
constructor(props) {
super(props)
this.state = {
current:0
}
}
componentDidMount() {
}
setageload=(sum)=>{
if(sum===undefined){
window.location.href="/project_packages/new"
}else{
// this.props.history.push("/project_packages/"+sum)
window.location.href="/project_packages/"+sum
}
}
render() {
return (
<div className="mb20">
<p className="clearfix ">
<div className={"stud-class-set padding200 coursenavbox edu-back-white"}>
<div className={"mb20"}><Icon type="check-circle" theme="filled" className={"fontcircle color-green"}/></div>
<div className={"sumbtongs mb5"}>恭喜!</div>
<div className={"sumbtongs mb5"}>提交成功</div>
<div className={"terraces mb5"}>平台正在审核您的申请审核结果将以平台消息的形式通知您</div>
<div className="clearfix mt30 mb30 padding251">
<a className="defalutCancelbtns fl" onClick={()=>this.setageload(this.props.id)}>查看发布需求</ a>
<Button type="primary" className="defalutSubmitbtn fl ml40 defalutSubmitbtns" onClick={()=>this.setageload()}>继续发布</Button>
</div>
</div>
</p>
</div>
)
}
}
export default PackageIndexNEISubmit;

@ -0,0 +1,60 @@
import React, { Component } from 'react';
import {BrowserRouter as Router,Route,Switch} from 'react-router-dom';
//业务组件
import PackageIndexNEIBanner from "./PackageIndexNEIBanner";
import PackageIndexNEIBannerConcent from "./PackageIndexNEIBannerConcent"
import PackageIndexNEISubmit from './PackageIndexNEISubmit'
import '../packageconcnet.css';
class PackageIndexNewandEditIndex extends Component{
constructor(props) {
super(props)
this.state = {
setPublication:false,
id:undefined
}
}
componentDidMount(){
}
setPublicationfun=(ids)=>{
this.setState({
setPublication:true,
id:ids
})
}
render() {
let {setPublication}=this.state;
return (
<div>
<div className="clearfix">
{setPublication===false?<div className={"educontent mt20 mb50"}>
<p className="clearfix mt20 mb20">
<span className="fl font-24 color-grey-3">
{this.props.match.params.id!=undefined?"编辑":"新建"}</span>
</p>
<PackageIndexNEIBanner {...this.props} />
<PackageIndexNEIBannerConcent
{...this.props}
setPublicationfun={this.setPublicationfun}
/>
</div>:
<div className={"educontent mt30 mb50"}>
<PackageIndexNEISubmit
{...this.props}
id={this.state.id}
/>
</div>}
</div>
</div>
)
}
}
export default PackageIndexNewandEditIndex;

@ -0,0 +1,140 @@
import React, { Component } from 'react';
import { Spin, Icon , Modal,Input,Button} from 'antd';
class PhoneModel extends Component {
//getverificationcodes 是否是重新发送或者是获取验证码
//多少秒
constructor(props) {
super(props);
this.state = {
funmodalsType:false,
istype:false,
getverificationcodes:true,
seconds:35,
}
}
//获取验证码
getverificationcode =()=>{
// if (this.state.logins&&this.state.logins.length === 0) {
// 判断没有输入手机号
// return
// }
if (this.state.getverificationcodes === true) {
this.setState({
getverificationcodes: undefined,
})
let timer = setInterval(() => {
this.setState((preState) => ({
seconds: preState.seconds - 1,
}), () => {
if (this.state.seconds == 0) {
clearInterval(timer);
this.setState({
getverificationcodes: false,
seconds: 35,
})
}
});
}, 1000)
//其他的网络请求也可以
this.SMSverification();
} else {
this.setState({
getverificationcodes: undefined,
})
let timer = setInterval(() => {
this.setState((preState) => ({
seconds: preState.seconds - 1,
}), () => {
if (this.state.seconds == 0) {
clearInterval(timer);
this.setState({
getverificationcodes: false,
seconds: 35,
})
}
});
}, 1000)
//其他的网络请求也可以
this.SMSverification();
}
}
//短信验证
SMSverification = () => {
// var url = `/accounts/get_verification_code.json`;
// axios.get((url), {
// params: {
// login: this.state.logins,
// type: 1,
// }
// }).then((result) => {
// //验证有问题{"status":1,"message":"success"}
// // console.log(result);
// this.openNotification("验证码已发送,请注意查收!",2);
//
//
// }).catch((error) => {
// console.log(error);
//
// })
}
render() {
let{getverificationcodes,seconds} =this.state;
const antIcons = <Icon type="loading" style={{ fontSize: 24 }} spin />
return(
<Modal
keyboard={false}
title="修改手机号"
visible={this.props.modalsType===undefined?false:this.props.modalsType}
closable={false}
footer={null}
destroyOnClose={true}
centered={true}
width="530px"
>
<div className="task-popup-content">
<p className="task-popup-text-center font-16 mb20">
<span className="shaiTitle mt5">手机号码<span>
<Input
style={{ width: 337 }}
placeholder="请输入手机号码" />
</span></span>
</p>
<p className="task-popup-text-center font-16 mt5">
<span className="shaiTitle mt5 ml17">
验证码
<span>
<Input
style={{ width: 200 }}
placeholder="请输入验证码" />
</span>
<span>
{
getverificationcodes === undefined ?
<Button type="primary" className="defalutSubmitbtn ml10 defalutSubmitbtnmodels" >重新发送 ({seconds}s)</Button>
: getverificationcodes === true ?
<Button type="primary" className="defalutSubmitbtn ml10 defalutSubmitbtnmodels" onClick={() => this.getverificationcode()} >获取验证码</Button>
:
<Button type="primary" className="defalutSubmitbtn ml10 defalutSubmitbtnmodels" onClick={() => this.getverificationcode()} >重新发送</Button>
}
{/*<Button type="primary" className="defalutSubmitbtn ml10 defalutSubmitbtnmodels">重新发送()</Button>*/}
</span>
</span>
</p>
<div className="clearfix mt30 edu-txt-center">
<a className="task-btn mr30 color_white" onClick={this.props.modalCancel}>取消</a>
<a className="task-btn task-btn-orange " onClick={this.props.modalSave}>确定</a>
</div>
</div>
</Modal>
)
}
}
export default PhoneModel;

@ -0,0 +1,72 @@
import React, { Component } from 'react';
import { Redirect } from 'react-router';
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";
import Loading from '../../Loading'
import Loadable from 'react-loadable';
import { TPMIndexHOC } from '../tpm/TPMIndexHOC'
import { SnackbarHOC } from 'educoder'
const PackageIndex = Loadable({
loader: () => import('./PackageIndex/PackageIndex'),
loading: Loading,
})
const PackageIndexNewandEdit = Loadable({
loader: () => import('./PackageIndexNewandEdit/PackageIndexNewandEditIndex'),
loading: Loading,
})
const PackageIndexNEITaskDetails = Loadable({
loader: () => import('./PackageIndexNEITaskDetails/PackageIndexNEITaskDetails'),
loading: Loading,
})
class ProjectPackageIndex extends Component {
constructor(props) {
super(props)
}
render() {
return (
<div className="newMain clearfix">
<Switch>
{/*众包首页*/}
<Route path="/project_packages/:id/edit"
render={
(props) => (<PackageIndexNewandEdit {...this.props} {...props} {...this.state} />)
}
></Route>
<Route path="/project_packages/new"
render={
(props) => (<PackageIndexNewandEdit {...this.props} {...props} {...this.state} />)
}
></Route>
<Route path="/project_packages/:id"
render={
(props) => (<PackageIndexNEITaskDetails {...this.props} {...props} {...this.state} />)
}
></Route>
<Route path="/project_packages"
render={
(props) => (<PackageIndex {...this.props} {...props} {...this.state} />)
}
></Route>
</Switch>
</div>
);
}
}
export default SnackbarHOC() (TPMIndexHOC (ProjectPackageIndex)) ;

@ -0,0 +1,302 @@
.width1240{
width:1240px;
}
.packinput .ant-input{
height: 50px;
width:749px;
border-color: #E1EDF8 !important;
}
.packinput .ant-input-group-addon .ant-btn{
width:140px !important;
font-size: 18px;
height: 50px;
background:rgba(76,172,255,1);
margin-right:18px;
}
.setissues{
width:280px;
height:50px;
background:rgba(76,172,255,1);
border-radius:4px;
margin-left: 15px;
}
.pagetype li{
color:#8F8F8F !important;
}
.maxwidth700{
max-width: 700px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mbf10{
margin-bottom:-10px;
}
.PackageIndexNEIBanner{
width:1200px;
height:110px;
background:rgba(255,255,255,1);
box-shadow:0px 2px 6px 0px rgba(125,125,125,0.26);
border-radius:8px;
}
.padding110{
padding: 39px 110px 0px;
box-sizing: border-box;
}
.borderccc{
border: 1px solid #ccc;
}
.input-100-40s{
width: 100%;
padding: 5px;
box-sizing: border-box;
}
.fafafas{
background-color: #fafafa!important;
height: 40px;
}
.fafafas:focus{
background-color: #fff!important;
}
.fafas .ant-input{
background-color: #fafafa!important;
height: 40px;
}
.fafas .ant-input:focus{
background-color: #fff!important;
}
.fafas .ant-input-group-addon .ant-btn{
width:140px !important;
font-size: 14px;
height: 40px;
background:rgba(76,172,255,1);
}
.newFormbox .upload_filename{
line-height: 32px;
}
.newFormbox .attachment span{
line-height: 23px;
}
.newFormbox .attachment .remove-upload{
line-height: 28px;
}
.pd30a0{
padding: 30px 30px 0 30px;
}
.newFormbox .attachment .icon-fujian{
font-size: 14px !important;
line-height: 14px;
margin-top: 9px;
}
.newFormbox{
height:20px
}
.ml24{
margin-left:24px;
}
.defalutCancelbtns{
display: block;
border: 1px solid #4CACFF !important;
background-color: #fff;
color: #4CACFF !important;
width:130px;
height:40px;
text-align: center;
line-height: 40px;
border-radius: 4px;
}
.defalutSubmitbtns{
background-color: #4CACFF;
height:40px;
}
.defalutSubmitbtnmodels{
width:127px;
height:30px;
background-color: #4CACFF;
}
.ant-steps-item-process .ant-steps-item-icon{
background-color: #4CACFF !important;
}
.ant-steps-item-process .ant-steps-item-icon{
background-color: #4CACFF !important;
}
.padding200{
padding: 115px 200px 215px 200px;
}
.fontcircle{
font-size: 80px;
display: inherit;
}
.sumbtongs{
font-size: 24px;
display: inherit;
text-align: center;
}
.terraces{
font-size: 16px;
display: inherit;
text-align: center;
color:#999;
}
.padding251{
padding: 0px 251px;
}
.ant-modal-title{
text-align: center;
}
.ml17{
margin-left: 17px;
}
.project-package-items{
display: -webkit-flex;
display: flex;
flex-direction: row;
margin:0px !important;
padding: 20px;
background: white;
margin-bottom:0px !important;
box-shadow: none !important;
}
.mtf7{
margin-top:-7px;
}
.publicpart.orangeGreen {
border-left: 80px solid #29BD8B;
}
.publicwords{
left: 3px;
top: 18px;
}
.project-packages-list .project-package-items .item-image{
width:100px !important;
}
.height185{
height: 185px;
}
.ContacttheTA{
width: 80px;
height: 26px;
font-size: 14px;
line-height: 26px;
display: block;
border: 1px solid #4CACFF !important;
background-color: #fff;
color: #4CACFF !important;
text-align: center;
border-radius: 4px;
}
.ContacttheTAs{
width: 80px;
height: 26px;
font-size: 14px;
line-height: 26px;
/*display: block;*/
border: 1px solid #fff !important;
background-color: #4CACFF;
color: #fff !important;
text-align: center;
border-radius: 4px;
}
.ml28{
margin-left: 28px;
}
.longboxs{
font-size: 16px;
font-family: MicrosoftYaHei-Bold;
font-weight: bold;
color: rgba(5,16,26,1);
border-left: 4px solid rgba(76,172,255,1);
padding-left: 10px;
margin-bottom: 20px;
}
.padding020{
padding: 0px 20px 20px;
}
.mtf3{
margin-top:-3px;
}
.task-btn-nebules{
background: #fff!important;
color: #4CACFF!important;
border: 1px solid #4CACFF!important;
margin-left: 20px;
cursor: pointer;
display: inline-block;
padding: 0 12px;
letter-spacing: 1px;
text-align: center;
font-size: 14px;
height: 30px;
line-height: 30px;
border-radius: 2px;
}
.packageabsolute{
position: absolute;
right: -16px;
top: -11px;
}
.relativef{
position: relative;
}
.homehove:hover .ptext{
color: #4CACFF!important;
}
.homehove:hover .ContacttheTAs{
display: block;
}
.topsj{
position: absolute;
top: -3px;
}
.bottomsj{
position: absolute;
bottom: -4px;
}
.touchSelect .ant-spin-dot-spin{
margin-top: 30% !important;
}

@ -133,6 +133,7 @@ class NewHeader extends Component {
},
ImageUrl:"",
ecUrl:null,
project_packages_url:null,
ImageUrlType:false
}
}
@ -198,7 +199,8 @@ class NewHeader extends Component {
}
).then((response) => {
this.setState({
ecUrl:response.data.ec_url
ecUrl:response.data.ec_url,
project_packages_url:response.data.project_packages_url
})
}).catch((error) => {
console.log(error)
@ -232,7 +234,7 @@ class NewHeader extends Component {
}
render() {
let {careerslist,isLogin,current_user,ImageUrl,ecUrl,ImageUrlType} = this.state;
let {careerslist,isLogin,current_user,ImageUrl,ecUrl,ImageUrlType,project_packages_url} = this.state;
// const isLogin = isLogintype; // 这里不会出现未登录的情况,服务端在服务端路由时发现如果是未登录,则跳转到登录页了。
const { user, match } = this.props;
/*
@ -243,6 +245,7 @@ class NewHeader extends Component {
let activeShixuns = false;
let activeCareers = false;
let activeCourses = false;
let competitions = false;
if (match.path === '/forums') {
activeForums = true;
} else if (match.path.startsWith('/shixuns')) {
@ -253,14 +256,16 @@ class NewHeader extends Component {
activeCourses=true;
}else if(match.path.startsWith('/ec_major_schools')){
activeCourses=true;
}else{
}else if(match.path.startsWith('/competitions')){
competitions=true;
}else{
activeIndex = true;
}
console.log(match.path)
console.log(match.path.startsWith("/ec_courses"))
// console.log(match.path)
// console.log(match.path.startsWith("/ec_courses"))
return (
<div className="newHeader" id="nHeader">
<div className="educontent clearfix">
@ -295,7 +300,13 @@ class NewHeader extends Component {
</div>
</li>
{/* <li><a href="/libraries">教学案例</a></li> */}
<li><a href="/competitions">在线竞赛</a></li>
<li className={`${competitions === true ? 'active' : ''}`} ><a href="/competitions">在线竞赛</a></li>
<li className={`${competitions === true ? 'active' : ''}`} style={{display:project_packages_url===null||project_packages_url===undefined||project_packages_url===""?'none':'block'}} >
<a href="/project_packages">
{project_packages_url===null||project_packages_url===undefined||project_packages_url===""?'':'众包'}
</a>
</li>
<li className={`${activeForums === true ? 'active' : ''}`}><a href="/forums">交流问答</a></li>
<li className={`${activeCourses === true ? 'active' : ''}`} style={{display:ecUrl===null||ecUrl===undefined||ecUrl===""?'none':'block'}}><a href={ecUrl}>{ecUrl===null||ecUrl===undefined||ecUrl===""?'':'工程认证'}</a></li>
</ul>

@ -114,7 +114,7 @@ function addFile(inputEl, file, eagerUpload,btnId) {
fileSpan.append(
$('<i></i>').attr({
'class': 'fa fa-folder mr5 color-blue fl mt8',
'class': 'color-green iconfont icon-fujian mr5 fl mt8',
'aria-hidden': true
}),
$('<input>', {

@ -464,7 +464,8 @@ li.li-width7{width: 7%;text-align: left}
.top-black-trangle{display: block;border-width: 8px;position: absolute;top: -16px;right: 4px;border-style: dashed solid dashed dashed;border-color: transparent transparent rgba(5,16,26,0.6) transparent;font-size: 0;line-height: 0;}
.right-black-trangle{display: block;border-width: 8px;position: absolute;top: 10px;right: -16px;border-style: dashed solid dashed dashed;border-color: transparent transparent transparent rgba(5,16,26,0.6);font-size: 0;line-height: 0;}
.activity-nav.active{color: #4CACFF!important;}
.project_packagesHead{width: 100%;margin-bottom:40px;background-size: 100% 100%;background-image: url("/images/educoder/project_packagesHead.png");height: 240px;
justify-content: center;align-items: center;display: -webkit-flex;}
.courseNewNum{display: block;background: #FF6800;border-radius:30px;padding:0px 2px;color: #fff!important;font-size: 11px;
height: 16px;line-height: 15px;min-width: 12px;text-align: center;margin-top: 17px;}

@ -301,6 +301,7 @@ a.color-red-dd:hover{color: #C61616!important;}
.winput-120-35{width: 120px;height: 35px;padding: 5px;box-sizing: border-box;}
.winput-120-30{width: 120px;height: 30px;padding: 5px;box-sizing: border-box;}
.winput-115-40{width: 115px;height: 40px;padding: 5px;box-sizing: border-box;}
.winput-100-30{width: 100px;height: 30px;padding: 5px;box-sizing: border-box;}
.winput-100-40{width: 100px;height: 40px;padding: 5px;box-sizing: border-box;}
.winput-90-40{width: 90px;height: 40px;padding: 5px;box-sizing: border-box;}
.winput-90-35{width: 90px;height: 35px;padding: 5px;box-sizing: border-box;}
@ -969,3 +970,148 @@ html>body #ajax-indicator { position: fixed; }
.radio-check input[type='checkbox']:checked + label:after {
background: #29BD8B;
}
/* 个人中心-众包 */
.project-packages-list {
}
.project-packages-list .project-package-item {
display: -webkit-flex;
display: flex;
flex-direction: row;
margin-bottom: 20px;
padding: 20px;
background: white;
/*box-shadow:1px 3px 3px 1px rgba(156,156,156,0.16);*/
}
.project-packages-list .project-package-item:hover{
box-shadow: 1px 6px 16px rgba(156,156,156,0.16);
opacity: 1;
border-radius: 2px;
}
.project-packages-list .project-package-item .item-image {
width: 150px;
height: 110px;
}
.project-packages-list .project-package-item .item-body {
margin-left: 20px;
flex: 1;
}
.project-package-item .item-body {
display: -webkit-flex;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.project-package-item .item-body .item-head {
display: -webkit-flex;
display: flex;
justify-content: space-between;
font-size: 20px;
height: 40px;
}
.project-package-item .item-head-title {
max-width: 700px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: max-width .2s;
-moz-transition: max-width .2s; /* Firefox 4 */
-webkit-transition: max-width .2s; /* Safari 和 Chrome */
-o-transition: max-width .2s; /* Opera */
}
.project-package-item .item-head-tags {
display: -webkit-flex;
display: flex;
align-items: center;
}
.project-package-item .item-head-tags span {
margin-left: 20px;
padding: 0 10px;
height: 28px;
font-size: 14px;
color: white;
border-radius: 5px;
}
.project-package-item .item-head-tags span.pending { background: lightcoral; }
.project-package-item .item-head-tags span.bidding_won { background: lightgreen; }
.project-package-item .item-head-tags span.bidding_lost { background: grey; }
.project-package-item .item-head-blank {
flex: 1;
}
.project-package-item .item-head-price {
margin-left: 20px;
color: #F83B2D;
font-size: 28px;
font-weight: bold;
}
.project-package-item .item-head-price span {
font-size: 18px;
}
.project-package-item .item-category {
display: -webkit-flex;
display: flex;
}
.project-package-item .item-category-item {
padding: 0 10px;
color: #FF6800;
font-size: 14px;
background:rgba(255,235,213,1);
border-radius:13px;
}
.project-package-item .item-other {
display: -webkit-flex;
display: flex;
justify-content: space-between;
color: #999999;
}
.project-package-item .item-group {
flex: 2;
}
.project-package-item .item-group.item-other-publish-at {
text-align: right;
}
.project-package-item .item-other-blank {
flex: 3
}
.project-package-item.with-operator .item-operator {
width: 0;
transition: width .2s;
-moz-transition: width .2s; /* Firefox 4 */
-webkit-transition: width .2s; /* Safari 和 Chrome */
-o-transition: width .2s; /* Opera */
}
.project-package-item.with-operator:hover .item-operator {
margin: -20px -20px -20px 20px;
padding: 20px 0;
width: 100px;
display: flex;
justify-content: space-around;
flex-direction: column;
align-items: center;
background: #f0f0f0;
}
.project-package-item.with-operator .item-operator a {
display: none;
}
.project-package-item.with-operator:hover .item-operator a {
display: block;
font-size: 20px;
}
.project-package-item.with-operator:hover .item-head-title {
max-width: 600px;
}
.list-count {
background: #fafafa;
}
.list-count span {
color: coral;
padding: 0 3px;
}
Loading…
Cancel
Save