Merge branch 'dev_hjm_a' into dev_cs_new

dev_cs_new
caishi 6 years ago
commit 093fe2a41d

@ -210,6 +210,7 @@ class FilesController < ApplicationController
is_unified_setting = params[:is_unified_setting]
publish_time = params[:publish_time]
publish_time = format_time(Time.parse(publish_time)) unless publish_time.blank?
is_public = params[:is_public]
course_group_publish_times = params[:course_group_publish_times] || []
@ -221,8 +222,10 @@ class FilesController < ApplicationController
@new_attachment_history = @old_attachment.become_history
@new_attachment_history.save!
old_course_second_category_id = @old_attachment.course_second_category_id
@old_attachment.copy_attributes_from_new_attachment(@new_attachment)
@old_attachment.is_public = is_public == true ? 1 : 0 if is_public
@old_attachment.course_second_category_id = old_course_second_category_id
@old_attachment.save!
@new_attachment.delete
end
@ -260,12 +263,7 @@ class FilesController < ApplicationController
return normal_status(-2, "该课程下没有id为 #{params[:id]}的资源") if @file.nil?
return normal_status(403, "您没有权限进行该操作") if @user != @file.author && !@user.teacher_of_course?(@course) && !@file.public?
@is_pdf = false
file_content_type = @file.content_type
file_ext_type = File.extname(@file.filename).strip.downcase[1..-1]
if (file_content_type.present? && file_content_type.downcase.include?("pdf")) || (file_ext_type.present? && file_ext_type.include?("pdf"))
@is_pdf = true
end
@attachment_histories = @file.attachment_histories
end

@ -498,7 +498,6 @@ class GamesController < ApplicationController
rescue Exception => e
# 思路: 异常首先应该考虑去恢复
# retry为1表示已经轮训完成后还没有解决问题这个时候需要检测异常
if params[:retry].to_i == 1
begin
# 如果模板没有问题,则通过中间层检测实训仓库是否异常
# 监测版本库HEAD是否存在不存在则取最新的HEAD
@ -534,8 +533,6 @@ class GamesController < ApplicationController
tip_exception(-3, "#{e.message}")
end
end
end
# 有异常版本库获取不到代码前端轮训15S后调用retry == 1
tip_exception(0, e.message)
end
end

@ -562,6 +562,17 @@ class ShixunsController < ApplicationController
commit_id = commit["id"]
end
# 如果该实训是金课中的实训,则将当前用户加入到当期开课的课堂
# if StageShixun.exists?(shixun_id: @shixun.id, subject_id: Subject.where(excellent: 1))
# subject = Subject.where(excellent: 1, id: StageShixun.where(shixun_id: @shixun.id).pluck(:subject_id)).take
# course = subject.courses.where("start_date is not null and start_date <= '#{Date.today}' and end_date is not null and end_date >= '#{Date.today}'").take
# if course.present? && !CourseMember.exists?(course_id: course.id, user_id: current_user.id)
# # 为了不影响后续操作用create而不是create!
# CourseMember.create(course_id: course.id, user_id: current_user.id, role: 4)
# CourseAddStudentCreateWorksJob.perform_later(course.id, [current_user.id])
# end
# end
ActiveRecord::Base.transaction do
begin
cloud_bridge = edu_setting('cloud_bridge')

@ -83,21 +83,37 @@ class SubjectsController < ApplicationController
@is_creator = current_user.creator_of_subject?(@subject)
@is_manager = @user.manager_of_subject?(@subject)
# 合作团队
@members = @subject.subject_members.includes(:user)
@shixuns = @subject.shixuns.published.pluck(:id)
challenge_ids = Challenge.where(shixun_id: @shixuns).pluck(:id)
@courses = @subject.courses if @subject.excellent
@members = @subject.subject_members.includes(:user)
shixuns = @subject.shixuns.published.pluck(:id)
challenge_ids = Challenge.where(shixun_id: shixuns).pluck(:id)
# 实训路径中的所有实训标签
@tags = ChallengeTag.where(challenge_id: challenge_ids).pluck(:name).uniq
# 用户获取的实训标签
# @user_tags = @subject.shixuns.map(&:user_tags_name).flatten.uniq
@user_tags = user_shixun_tags challenge_ids, @user.id
@my_subject_progress = @subject.my_subject_progress
@challenge_count = challenge_ids.size
# 访问数变更
@subject.increment!(:visits)
end
def right_banner
@user = current_user
# 合作团队
@members = @subject.subject_members.includes(:user)
shixuns = @subject.shixuns.published.pluck(:id)
challenge_ids = Challenge.where(shixun_id: shixuns).pluck(:id)
# 实训路径中的所有实训标签
@tags = ChallengeTag.where(challenge_id: challenge_ids).pluck(:name).uniq
# 用户获取的实训标签
# @user_tags = @subject.shixuns.map(&:user_tags_name).flatten.uniq
@user_tags = user_shixun_tags challenge_ids, @user.id
@my_subject_progress = @subject.my_subject_progress
end
def new
normal_status("")
end

@ -26,6 +26,12 @@ class Users::BaseController < ApplicationController
render_forbidden
end
def require_auth_teacher!
return if current_user.admin_or_business? || observed_user.certification_teacher?
render_forbidden
end
def page_value
params[:page].to_i <= 0 ? 1 : params[:page].to_i
end

@ -1,5 +1,5 @@
class Users::VideoAuthsController < Users::BaseController
before_action :private_user_resources!
before_action :private_user_resources!, :require_auth_teacher!
def create
result = Videos::CreateAuthService.call(observed_user, create_params)

@ -1,5 +1,5 @@
class Users::VideosController < Users::BaseController
before_action :private_user_resources!
before_action :private_user_resources!, :require_auth_teacher!
helper_method :current_video

@ -380,4 +380,8 @@ module TidingDecorator
I18n.t(locale_format(tiding_type)) % [container.try(:title) || extra]
end
end
def public_course_start_content
I18n.t(locale_format) % [belong_container&.name, belong_container&.start_date&.strftime("%Y-%m-%d")]
end
end

@ -258,7 +258,7 @@ module ApplicationHelper
end
def download_url attachment
attachment_path(attachment)
attachment_path(attachment).gsub("/api","")
end
# 耗时:天、小时、分、秒

@ -163,7 +163,7 @@ module ExportHelper
end
w_15 = w.work_score.nil? ? "--" : w.work_score.round(1)
w_16 = w.update_time ? format_time(w.update_time) : "--" "更新时间"
w_17 = (game_spend_time w.cost_time)
w_17 = w.cost_time ? (game_spend_time w.cost_time) : "--"
teacher_comments = w.student_works_scores
if teacher_comments.present?
w_18 = ""

@ -16,4 +16,19 @@ module SubjectsHelper
ChallengeTag.joins("join games on challenge_tags.challenge_id = games.challenge_id").
where(challenge_id: challenge_ids, games: {status: 2, user_id: user_id}).pluck("challenge_tags.name").uniq
end
# 金课的课堂状态 0未开课1进行中2已结束
def subject_course_status course
if course.is_end
{status: 2, time: ""}
elsif course.start_date && course.start_date > Date.today
{status: 0, time: ""}
elsif course.start_date && course.start_date <= Date.today && course.end_date >= Date.today
sum_week = ((course.end_date - course.start_date).to_i / 7.0).ceil
curr_week = ((Date.today - course.start_date).to_i / 7.0).ceil
{status: 1, time: "进行至第#{curr_week}周,共#{sum_week}"}
else
{status: -1, time: ""}
end
end
end

@ -16,6 +16,15 @@ module AliyunVod::Service::VideoUpload
result = request(:post, params)
if result['Code'].present?
message =
case result['Code']
when 'InvalidFileName.Extension' then '不支持的文件格式'
when 'IllegalCharacters' then '文件名称包含非法字符'
end
raise AliyunVod::Error, message if message.present?
end
raise AliyunVod::Error, '获取上传凭证失败' if result['UploadAddress'].blank?
result

@ -115,4 +115,15 @@ class Attachment < ApplicationRecord
end
end
#判断是否为pdf文件
def is_pdf?
is_pdf = false
file_content_type = content_type
file_ext_type = File.extname(filename).strip.downcase[1..-1]
if (file_content_type.present? && file_content_type.downcase.include?("pdf")) || (file_ext_type.present? && file_ext_type.include?("pdf"))
is_pdf = true
end
is_pdf
end
end

@ -20,4 +20,14 @@ class AttachmentHistory < ApplicationRecord
is_public == 1
end
def is_history_pdf?
is_pdf = false
file_content_type = content_type
file_ext_type = File.extname(filename).strip.downcase[1..-1]
if (file_content_type.present? && file_content_type.downcase.include?("pdf")) || (file_ext_type.present? && file_ext_type.include?("pdf"))
is_pdf = true
end
is_pdf
end
end

@ -25,7 +25,8 @@ module Searchable::Course
author_school_name: teacher&.school_name,
visits_count: visits,
members_count: members_count,
is_public: is_public == 1
is_public: is_public == 1,
first_category_url: ApplicationController.helpers.module_url(none_hidden_course_modules.first, self)
}
end

@ -11,7 +11,7 @@ class DuplicateCourseService < ApplicationService
@course = copy_course!
copy_course_modules!
Rails.logger.info("###########second_category_list#{@second_category_list}")
join_course!
copy_homework_commons!
@ -36,9 +36,17 @@ class DuplicateCourseService < ApplicationService
end
def copy_course_modules!
@second_category_list = {}
origin_course.course_modules.each do |course_module|
attrs = course_module.as_json(only: %i[module_type position hidden module_name])
CourseModule.create!(attrs.merge(course_id: course.id))
new_course_module = CourseModule.create!(attrs.merge(course_id: course.id))
# 复制子目录
course_module.course_second_categories.each do |second_category|
category_attr = second_category.as_json(only: %i[category_type name position])
new_second_category =
CourseSecondCategory.create!(category_attr.merge(course_id: course.id, course_module_id: new_course_module.id))
@second_category_list[second_category.id] = new_second_category.id
end
end
end
@ -50,7 +58,10 @@ class DuplicateCourseService < ApplicationService
origin_course.homework_commons.where(homework_type: %i[normal group practice]).find_each do |origin_homework|
homework_attrs = origin_homework.as_json(only: %i[name description homework_type homework_bank_id reference_answer])
homework = HomeworkCommon.create!(homework_attrs.merge(user_id: user.id, course_id: course.id))
course_second_category_id = @second_category_list[origin_homework.course_second_category_id]
homework = HomeworkCommon.create!(homework_attrs.merge(user_id: user.id, course_id: course.id,
course_second_category_id:course_second_category_id))
origin_homework.attachments.find_each do |origin_attachment|
attachment = origin_attachment.copy
@ -71,6 +82,7 @@ class DuplicateCourseService < ApplicationService
HomeworksService.new.create_shixun_homework_cha_setting(homework, origin_homework.shixuns.first)
end
origin_homework.increment!(:quotes)
origin_homework.homework_bank.increment!(:quotes) if origin_homework.homework_bank
end
@ -138,9 +150,9 @@ class DuplicateCourseService < ApplicationService
attachment.copy_from = origin_attachment.copy_from || origin_attachment.id
attachment.is_publish = 0
attachment.attachtype ||= 4
attachment.course_second_category_id = @second_category_list[origin_attachment.course_second_category_id]
attachment.save!
origin_course.update_quotes(attachment)
end
end

@ -51,8 +51,10 @@ class Users::UpdateAccountService < ApplicationService
if first_full_reward
RewardGradeService.call(user, container_id: user.id, container_type: 'Account', score: 500)
sms_notify_admin(user.lastname) if user.user_extension.teacher?
if user.user_extension.teacher?
join_course(user.id,1309, 2)
sms_notify_admin(user.lastname)
end
end
user
@ -73,4 +75,11 @@ class Users::UpdateAccountService < ApplicationService
rescue => ex
Util.logger_error(ex)
end
def join_course(user_id, course_id, identity)
course = Course.find_by(id: course_id)
return unless course
attr = {course_id: course_id, role: identity, user_id: user_id}
CourseMember.create!(attr)
end
end

@ -35,7 +35,7 @@ class Videos::CreateAuthService < ApplicationService
def upload_video_result
AliyunVod::Service.create_upload_video(title, filename, params)
rescue AliyunVod::Error => _
raise Error, '获取视频上传凭证失败'
rescue AliyunVod::Error => ex
raise Error, ex.message || '获取视频上传凭证失败'
end
end

@ -6,4 +6,5 @@ json.publish_time attachment.publish_time
json.quotes attachment.quotes_count
json.downloads_count attachment.downloads_count
json.created_on attachment.created_on
json.url attachment_path(attachment, type: 'history')
json.url attachment_path(attachment, type: 'history').gsub("/api","")
json.is_pdf attachment.is_history_pdf?

@ -3,3 +3,4 @@ json.title attachment.title
json.filesize number_to_human_size(attachment.filesize)
json.url download_url(attachment)
json.created_on attachment.created_on
json.is_pdf attachment.is_pdf?

@ -1,3 +1,3 @@
json.is_pdf @is_pdf
json.partial! 'attachments/attachment_small', attachment: @file
json.partial! "attachment_histories/list", attachment_histories: @attachment_histories

@ -0,0 +1,21 @@
json.members @members do |member|
json.partial! 'subject_member', locals: { user: member.user }
json.role member.role
end
# 技能标签
json.tags @tags do |tag|
unless tag.blank?
json.tag_name tag
json.status @user_tags.include?(tag)
end
end
# 我的进展
json.progress do
json.my_score @subject.my_subject_score
json.all_score @subject.all_score
json.learned @subject.my_subject_progress
json.time @subject.my_consume_time
end

@ -1,4 +1,4 @@
json.(@subject, :id, :name, :description, :learning_notes, :stages_count, :stage_shixuns_count, :shixuns_count)
json.(@subject, :id, :name, :description, :learning_notes, :stages_count, :stage_shixuns_count, :shixuns_count, :excellent)
json.challenge_choose_count @subject.subject_challenge_choose_count
json.challenges_count @subject.subject_challenge_count
@ -12,6 +12,19 @@ json.allow_send @user.logged?
json.allow_visit @subject.status > 1 || @is_manager
json.allow_add_member @is_manager
if @subject.excellent
json.courses @courses do |course|
json.course_id course.id
json.first_category_url module_url(course.none_hidden_course_modules.first, course)
json.start_date course.start_date
json.end_date course.end_date
json.student_count course.students.count
json.course_identity @user.course_identity(course)
json.course_status subject_course_status course
end
end
json.members @members do |member|
json.partial! 'subject_member', locals: { user: member.user }
json.role member.role

@ -225,3 +225,4 @@
System:
1_end: "你提交的发布视频申请:%s审核已通过"
2_end: "你提交的发布视频申请:%s审核未通过<br/><span>原因:%{reason}</span>"
PublicCourseStart_end: "你报名参与的开放课程:%s将于%s正式开课"

@ -273,6 +273,7 @@ Rails.application.routes.draw do
delete :delete_member
post :up_member_position
post :down_member_position
get :right_banner
end
collection do

@ -0,0 +1,5 @@
class ModifyPathForChallenges < ActiveRecord::Migration[5.2]
def change
change_column :challenges, :path, :text
end
end

Binary file not shown.

@ -0,0 +1,186 @@
#coding=utf-8
# 执行示例 bundle exec rake excellent_course_exercise:student_answer args=2933,1042,823
# args 第一个是course_id, 第二个是参与人数, 第三个是通过人数
desc "同步精品课的学生试卷数据"
namespace :excellent_course_exercise do
if ENV['args']
course_id = ENV['args'].split(",")[0] # 对应课堂的id
participant_count = ENV['args'].split(",")[1].to_i # 表示参与人数
pass_count = ENV['args'].split(",")[2].to_i # 表示通过人数
end
task :student_answer => :environment do
course = Course.find_by(id: course_id)
course.exercises.each_with_index do |exercise, index|
# 第一个试卷的参与人数和通过人数都是传的数据,后续的随机
if index == 0
members = course.students.order("id asc").limit(participant_count)
update_exercise_user(exercise, members, pass_count)
else
new_participant_count = rand((participant_count - 423)..participant_count)
new_pass_count = rand((new_participant_count - 113)..new_participant_count)
members = course.students.order("id asc").limit(new_participant_count)
update_exercise_user(exercise, members, new_pass_count)
end
end
end
def update_exercise_user exercise, members, pass_count
exercise_question_ids = exercise.exercise_questions.where(question_type: 0).pluck(:id)
# index < pass_count 之前的学生都是通关的,之后的未通过
members.each_with_index do |member, index|
exercise_user = exercise.exercise_users.where(user_id: member.user_id).take
if exercise_question_ids.length == 20
rand_num = index < pass_count - 1 ? 20 : rand(1..16)
elsif exercise_question_ids.length == 17
rand_num = index < pass_count - 1 ? rand(13..17) : rand(1..11)
else
rand_num = exercise_question_ids.length
end
if exercise_user && exercise_user.commit_status == 0
exercise_question_ids = exercise_question_ids.sample(rand_num)
questions = exercise.exercise_questions.where(id: exercise_question_ids)
create_exercise_answer questions, member.user_id
total_score = calculate_student_score(exercise, member.user)
commit_option = {
:status => 1,
:commit_status => 1,
:start_at => exercise.publish_time,
:end_at => exercise.end_time,
:objective_score => total_score,
:score => total_score,
:subjective_score => 0
}
exercise_user.update_columns(commit_option)
end
end
end
def create_exercise_answer questions, user_id
questions.each do |question|
choice_position = question.exercise_standard_answers.take&.exercise_choice_id
choice = question.exercise_choices.where(choice_position: choice_position).take
answer_option = {
:user_id => user_id,
:exercise_question_id => question.id,
:exercise_choice_id => choice&.id,
:answer_text => ""
}
ex_a = ExerciseAnswer.new(answer_option)
ex_a.save!
end
end
#计算试卷的总分和试卷的答题状态
def calculate_student_score(exercise,user)
score1 = 0.0 #选择题/判断题
score2 = 0.0 #填空题
score5 = 0.0 #实训题
total_score = 0.0
ques_stand = [] #问题是否正确
exercise_questions = exercise.exercise_questions.includes(:exercise_answers,:exercise_shixun_answers,:exercise_standard_answers,:exercise_shixun_challenges)
exercise_questions&.each do |q|
begin
if q.question_type != 5
answers_content = q.exercise_answers.where(user_id: user.id) #学生的答案
else
answers_content = q.exercise_shixun_answers.where(user_id: user.id) #学生的答案
end
if q.question_type <= 2 #为选择题或判断题时
if answers_content.present? #学生有回答时
answer_choice_array = []
answers_content.each do |a|
answer_choice_array.push(a.exercise_choice.choice_position) #学生答案的位置
end
user_answer_content = answer_choice_array.sort
standard_answer = q.exercise_standard_answers.pluck(:exercise_choice_id).sort #该问题的标准答案,可能有多个
if user_answer_content == standard_answer #答案一致,多选或单选才给分,答案不对不给分
if standard_answer.size > 0
q_score_1 = q.question_score
# q_score_1 = (q.question_score.to_f / standard_answer.count) #当多选答案正确时每个answer的分数均摊。
else
q_score_1 = 0.0
end
answers_content.update_all(:score => q_score_1)
score1 = score1 + q.question_score
else
answers_content.update_all(:score => -1.0)
score1 += 0.0
end
else
score1 += 0.0
end
elsif q.question_type == 5 #实训题时,主观题这里不评分
q.exercise_shixun_challenges&.each do |exercise_cha|
game = Game.user_games(user.id,exercise_cha.challenge_id)&.first #当前用户的关卡
if game.present?
exercise_cha_score = 0.0
answer_status = 0
# if game.status == 2 && game.final_score >= 0
if game.final_score > 0
exercise_cha_score = game.real_score(exercise_cha.question_score)
# exercise_cha_score = exercise_cha.question_score #每一关卡的得分
answer_status = 1
end
ex_shixun_answer_content = answers_content&.where(exercise_shixun_challenge_id: exercise_cha.id)
code = nil
if exercise_cha.challenge&.path.present?
cha_path = challenge_path(exercise_cha.challenge&.path)
game_challenge = game.game_codes.search_challenge_path(cha_path)&.first
if game_challenge.present?
game_code = game_challenge
code = game_code.try(:new_code)
else
code = git_fle_content(game.myshixun.repo_path,cha_path)
end
end
if ex_shixun_answer_content.blank? #把关卡的答案存入试卷的实训里
### Todo 实训题的_shixun_details里的代码是不是直接从这里取出就可以了涉及到code的多个版本库的修改
sx_option = {
:exercise_question_id => q.id,
:exercise_shixun_challenge_id => exercise_cha.id,
:user_id => user.id,
:score => exercise_cha_score.round(1),
:answer_text => code,
:status => answer_status
}
ExerciseShixunAnswer.create(sx_option)
else
ex_shixun_answer_content.first.update_attributes(score:exercise_cha_score.round(1),answer_text:code)
end
score5 += exercise_cha_score
else
score5 += 0.0
end
end
end
user_scores = answers_content.blank? ? 0.0 : answers_content.score_reviewed.pluck(:score).sum
if user_scores > 0.0
stand_answer = 1
else
stand_answer = 0
end
ques_option = {
"q_id":q.id, #该问题的id
"q_type":q.question_type,
"q_position":q.question_number, #该问题的位置
"stand_status":stand_answer, #该问题是否正确,1为正确0为错误
"user_score":user_scores.round(1) #每个问题的总得分
}
ques_stand.push(ques_option)
rescue Exception => e
Rails.logger.info("calcuclate_score_have_error____________________________#{e}")
next
end
end
total_score = score1 + score2 + score5
total_score
end
end

@ -0,0 +1,142 @@
#coding=utf-8
# 执行示例 bundle exec rake public_course:student args=149,2903
# args 第一个参数是subject_id第二个参数是课程course_id
# 第一期时间2018-12-16 至2019-03-31
# 第二期时间2019-04-07 至2019-07-28
#
# 这次学习很有收获,感谢老师提供这么好的资源和细心的服务🎉🎉🎉
#
desc "同步精品课数据"
namespace :public_course do
if ENV['args']
subject_id = ENV['args'].split(",")[0] # 对应课程的id
course_id = ENV['args'].split(",")[1] # 对应课堂的id
status = ENV['args'].split(",")[2] # 表示相应的期数
type = ENV['args'].split(",")[3] # 表示课程模块
end
if status.to_i == 1
start_time = '2018-12-16'
end_time = '2019-04-01'
elsif status.to_i == 2
start_time = '2019-04-07'
end_time = '2019-07-28'
else
# 这种情况是取所有的
start_time = '2015-01-01'
end_time = '2022-07-28'
end
task :student => :environment do
puts "subject_id is #{subject_id}"
puts "course_id is #{course_id}"
user_ids = Myshixun.find_by_sql("select distinct(user_id) from myshixuns where shixun_id in (select shixun_id from stage_shixuns
where stage_id in (select id from stages where subject_id=#{subject_id}))").map(&:user_id)
puts user_ids
if user_ids.present?
user_ids.each do |user_id|
puts user_id
begin
CourseMember.create!(course_id: course_id, user_id: user_id, role: 4)
rescue Exception => e
Rails.logger()
end
end
end
end
#
task :message => :environment do
discusses = Discuss.find_by_sql("select content, user_id, created_on, updated_on from discusses where dis_id in (select shixun_id from stage_shixuns where
stage_id in (select id from stages where subject_id=#{subject_id})) and created_at > #{start_time} and
created_at<#{end_time}")
discusses.find_each do |discuss|
puts discuss.user_id
puts discuss.content
# 回复帖子
# 讨论区发布帖子
# Message.create!(board: @message.board, root_id: @message.root_id || @message.id,
# author: current_user, parent: @message,
# message_detail_attributes: {
# content: params[:content]
# })
end
end
# 更新某个课程的某类时间
# 执行示例 bundle exec rake public_course:student args=2903,1
task :time => :environment do
# course_id = ENV['args'].split(",")[0] # 对应课堂的id
# type = ENV['args'].split(",")[1]
course = Course.find(course_id)
case type.to_i
when 1
# 讨论区
when 2
# 作业
course.homework_commons.each do |homework|
created_at = random_time(start_time, end_time)
publish_time = random_larger_time created_at, start_time, end_time
end_time = random_larger_time publish_time, start_time, end_time
updated_at = end_time
homework.update_columns(publish_time: publish_time, end_time: end_time, created_at: created_at, updated_at: updated_at)
homework.homework_detail_manual.update_columns(comment_status: 6, created_at: created_at, updated_at: updated_at)
end
when 3
# 试卷
course.exercises.each do |exercise|
created_at = random_time start_time, end_time
publish_time = random_larger_time created_at, start_time, end_time
end_time = random_larger_time publish_time, start_time, end_time
updated_at = end_time
exercise.update_columns(publish_time: publish_time, end_time: end_time, created_at: created_at, updated_at: updated_at, exercise_status: 3)
end
when 4
# 资源
course.attachments.each do |atta|
created_on = random_time start_time, end_time
atta.update_columns(is_publish: 1, created_on: created_on, publish_time: created_on)
end
end
end
def min_swith(time)
puts time
return time < 9 ? "0#{time}" : time
end
def random_time(start_time, end_time)
hour = (6..23).to_a.sample(1).first
min = rand(60)
sec = rand(60)
start_time = Date.parse(start_time)
end_time = Date.parse(end_time)
date = (start_time..end_time).to_a.sample(1).first
time = "#{date} #{min_swith(hour)}:#{min_swith(min)}:#{min_swith(sec)}"
puts time
time
end
def random_larger_time(time, start_time, end_time)
large_time = random_time(start_time, end_time)
while large_time <= time
large_time = random_time(start_time, end_time)
end
large_time
end
end

@ -0,0 +1,19 @@
desc "开放课程开课前通知报名用户"
namespace :public_course_notice do
task :tiding => :environment do
Course.where(excellent: 1).where("start_date is not null and start_date = '#{Date.today}'").each do |course|
attrs = %i[user_id trigger_user_id status container_id container_type belong_container_id
belong_container_type tiding_type created_at updated_at]
same_attrs = {
trigger_user_id: 0, container_id: course.id, container_type: 'PublicCourseStart',
belong_container_id: course.id, belong_container_type: 'Course', tiding_type: 'System', status: 0
}
Tiding.bulk_insert(*attrs) do |worker|
course.students.each do |student|
worker.add same_attrs.merge(user_id: student.user_id)
end
end
end
end
end

@ -0,0 +1,37 @@
# bundle exec rake sync:public_message args=149,2903
namespace :sync do
task :public_message => :environment do
subject_id = ENV['args'].split(",")[0] # 对应课程的id
board_id = ENV['args'].split(",")[1]
message_id = ENV['args'].split(",")[2]
status = ENV['args'].split(",")[3] # 表示相应的期数
if status.to_i == 1
start_time = '2018-12-16'
end_time = '2019-04-01'
elsif status.to_i == 2
start_time = '2019-04-07'
end_time = '2019-07-28'
else
# 这种情况是取所有的
start_time = '2015-01-01'
end_time = '2022-07-28'
end
shixun_ids = Shixun.find_by_sql("select shixun_id from stage_shixuns where stage_id in (select id from stages where
subject_id=#{subject_id}) ").map(&:shixun_id)
discusses = Discuss.where(dis_id: shixun_ids).where("created_at >? and created_at <?", start_time, end_time)
if discusses.present?
discusses.find_each do |discuss|
puts discuss.user_id
puts board_id
puts message_id
new_message = Message.create!(board_id: board_id.to_i, author_id: discuss.user_id, parent_id: message_id, root_id: message_id)
MessageDetail.create!(message_id: new_message.id, content: discuss.try(:content))
end
end
end
end

@ -1,25 +0,0 @@
#coding=utf-8
# 执行示例 bundle exec rake public_course:student args=149,2903
# args 第一个参数是subject_id第二个参数是课程course_id
desc "同步精品课数据"
namespace :public_course do
task :student => :environment do
subject_id = ENV['args'].split(",").first
course_id = ENV['args'].split(",").last
puts "subject_id is #{subject_id}"
puts "course_id is #{course_id}"
user_ids = Myshixun.find_by_sql("select distinct(user_id) from myshixuns where shixun_id in (select shixun_id from stage_shixuns
where stage_id in (select id from stages where subject_id=#{subject_id}))").map(&:user_id)
puts user_ids
if user_ids.present?
user_ids.each do |user_id|
puts user_id
CourseMember.create!(course_id: course_id, user_id: user_id, role: 4)
end
end
end
end

@ -29,7 +29,7 @@ const env = getClientEnvironment(publicUrl);
module.exports = {
// You may want 'eval' instead if you prefer to see the compiled output in DevTools.
// See the discussion in https://github.com/facebookincubator/create-react-app/issues/343.s
devtool: "cheap-module-eval-source-map",
// devtool: "cheap-module-eval-source-map",
// 开启调试
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -64,3 +64,13 @@ html, body {
/* 某些情况下被cm盖住了 */
z-index: 99;
}
/* antd扩展 */
.formItemInline.ant-form-item {
display: flex;
}
.formItemInline .ant-form-item-control-wrapper {
flex: 1;
}

@ -217,6 +217,10 @@ const UsersInfo = Loadable({
loader: () => import('./modules/user/usersInfo/Infos'),
loading: Loading,
})
const InfosIndex = Loadable({
loader: () => import('./modules/user/usersInfo/InfosIndex'),
loading: Loading,
})
// 教学案例
const MoopCases = Loadable({
@ -327,7 +331,10 @@ class App extends Component {
/>
<Route path="/users/:username"
render={
(props) => (<UsersInfo {...this.props} {...props} {...this.state} />)
(props) => {
return (<InfosIndex {...this.props} {...props} {...this.state} />)
}
}></Route>
<Route

@ -69,7 +69,7 @@ export function initAxiosInterceptors(props) {
// proxy = 'http://localhost:3000'
// }
// ---------------------------------------------
if (config.url.indexOf(proxy) != -1) {
if (config.url.indexOf(proxy) != -1 || config.url.indexOf(':') != -1) {
return config
}
requestProxy(config)

@ -0,0 +1,237 @@
import React, { useState, useEffect, memo } from 'react';
import { getUrl2, isDev } from 'educoder'
import axios from 'axios'
const $ = window.$
let _url_origin = getUrl2()
let _path = isDev() ? 'public' : 'build'
let uploader
let _testHost = '' ; '192.168.2.63:3001/api'
const login = 'innov'
function createUploader () {
uploader = new window.AliyunUpload.Vod({
timeout: $('#timeout').val() || 60000,
partSize: $('#partSize').val() || 1048576,
parallel: $('#parallel').val() || 5,
retryCount: $('#retryCount').val() || 3,
retryDuration: $('#retryDuration').val() || 2,
region: $('#region').val() || 'ap-southeast-1',
userId: $('#userId').val() || 1829848226361863, // 1202060945918292, // 1303984639806000,
// 添加文件成功
addFileSuccess: function (uploadInfo) {
console.log('addFileSuccess')
$('#authUpload').attr('disabled', false)
$('#resumeUpload').attr('disabled', false)
$('#status').text('添加文件成功, 等待上传...')
console.log("addFileSuccess: " + uploadInfo.file.name)
$('#pauseUpload').attr('disabled', false)
uploader.startUpload()
},
// 开始上传
onUploadstarted: function (uploadInfo) {
// 如果是 UploadAuth 上传方式, 需要调用 uploader.setUploadAuthAndAddress 方法
// 如果是 UploadAuth 上传方式, 需要根据 uploadInfo.videoId是否有值调用点播的不同接口获取uploadauth和uploadAddress
// 如果 uploadInfo.videoId 有值,调用刷新视频上传凭证接口,否则调用创建视频上传凭证接口
// 注意: 这里是测试 demo 所以直接调用了获取 UploadAuth 的测试接口, 用户在使用时需要判断 uploadInfo.videoId 存在与否从而调用 openApi
// 如果 uploadInfo.videoId 存在, 调用 刷新视频上传凭证接口(https://help.aliyun.com/document_detail/55408.html)
// 如果 uploadInfo.videoId 不存在,调用 获取视频上传地址和凭证接口(https://help.aliyun.com/document_detail/55407.html)
if (!uploadInfo.videoId) {
// var createUrl = 'https://demo-vod.cn-shanghai.aliyuncs.com/voddemo/CreateUploadVideo?Title=testvod1&FileName=aa.mp4&BusinessType=vodai&TerminalType=pc&DeviceModel=iPhone9,2&UUID=59ECA-4193-4695-94DD-7E1247288&AppVersion=1.0.0&VideoId=5bfcc7864fc14b96972842172207c9e6'
// $.get(createUrl, function (data) {
// var uploadAuth = data.UploadAuth
// var uploadAddress = data.UploadAddress
// var videoId = data.VideoId
// uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress,videoId)
// }, 'json')
var createUrl = `${_testHost}/users/${login}/video_auths.json?debug=true`
axios.post(createUrl, {
title: 'testvod1',
file_name: 'aa.mp4'
}).then((response) => {
// if (response.data.status == )
const data = response.data.data
var uploadAuth = data.UploadAuth
var uploadAddress = data.UploadAddress
var videoId = data.VideoId
uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress, videoId)
}).catch((error) => {
console.log(error)
})
$('#status').text('文件开始上传...')
console.log("onUploadStarted:" + uploadInfo.file.name + ", endpoint:" + uploadInfo.endpoint + ", bucket:" + uploadInfo.bucket + ", object:" + uploadInfo.object)
} else {
// 如果videoId有值根据videoId刷新上传凭证
// https://help.aliyun.com/document_detail/55408.html?spm=a2c4g.11186623.6.630.BoYYcY
// var refreshUrl = 'https://demo-vod.cn-shanghai.aliyuncs.com/voddemo/RefreshUploadVideo?BusinessType=vodai&TerminalType=pc&DeviceModel=iPhone9,2&UUID=59ECA-4193-4695-94DD-7E1247288&AppVersion=1.0.0&Title=haha1&FileName=xxx.mp4&VideoId=' + uploadInfo.videoId
// $.get(refreshUrl, function (data) {
// var uploadAuth = data.UploadAuth
// var uploadAddress = data.UploadAddress
// var videoId = data.VideoId
// uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress,videoId)
// }, 'json')
var refreshUrl = `${_testHost}/users/${login}/video_auths.json?debug=true`
axios.put(refreshUrl, {
video_id: uploadInfo.videoId,
}).then((response) => {
const data = response.data.data
var uploadAuth = data.UploadAuth
var uploadAddress = data.UploadAddress
var videoId = data.VideoId
uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress, videoId)
}).catch((error) => {
console.log(error)
})
}
},
// 文件上传成功
onUploadSucceed: function (uploadInfo) {
console.log("onUploadSucceed: " + uploadInfo.file.name + ", endpoint:" + uploadInfo.endpoint + ", bucket:" + uploadInfo.bucket + ", object:" + uploadInfo.object)
$('#status').text('文件上传成功!')
},
// 文件上传失败
onUploadFailed: function (uploadInfo, code, message) {
console.log("onUploadFailed: file:" + uploadInfo.file.name + ",code:" + code + ", message:" + message)
$('#status').text('文件上传失败!')
},
// 取消文件上传
onUploadCanceled: function (uploadInfo, code, message) {
console.log("Canceled file: " + uploadInfo.file.name + ", code: " + code + ", message:" + message)
$('#status').text('文件上传已暂停!')
},
// 文件上传进度,单位:字节, 可以在这个函数中拿到上传进度并显示在页面上
onUploadProgress: function (uploadInfo, totalSize, progress) {
console.log("onUploadProgress:file:" + uploadInfo.file.name + ", fileSize:" + totalSize + ", percent:" + Math.ceil(progress * 100) + "%")
var progressPercent = Math.ceil(progress * 100)
$('#auth-progress').text(progressPercent)
$('#status').text('文件上传中...')
},
// 上传凭证超时
onUploadTokenExpired: function (uploadInfo) {
// 上传大文件超时, 如果是上传方式一即根据 UploadAuth 上传时
// 需要根据 uploadInfo.videoId 调用刷新视频上传凭证接口(https://help.aliyun.com/document_detail/55408.html)重新获取 UploadAuth
// 然后调用 resumeUploadWithAuth 方法, 这里是测试接口, 所以我直接获取了 UploadAuth
$('#status').text('文件上传超时!')
// let refreshUrl = 'https://demo-vod.cn-shanghai.aliyuncs.com/voddemo/RefreshUploadVideo?BusinessType=vodai&TerminalType=pc&DeviceModel=iPhone9,2&UUID=59ECA-4193-4695-94DD-7E1247288&AppVersion=1.0.0&Title=haha1&FileName=xxx.mp4&VideoId=' + uploadInfo.videoId
// $.get(refreshUrl, function (data) {
// var uploadAuth = data.UploadAuth
// uploader.resumeUploadWithAuth(uploadAuth)
// console.log('upload expired and resume upload with uploadauth ' + uploadAuth)
// }, 'json')
var refreshUrl = `${_testHost}/users/${login}/video_auths.json?debug=true`
axios.put(refreshUrl, {
video_id: uploadInfo.videoId,
}).then((response) => {
const data = response.data.data
var uploadAuth = data.UploadAuth
uploader.resumeUploadWithAuth(uploadAuth)
}).catch((error) => {
console.log(error)
})
},
// 全部文件上传结束
onUploadEnd: function (uploadInfo) {
$('#status').text('文件上传完毕!')
console.log("onUploadEnd: uploaded all the files")
}
})
return uploader
}
function AliyunUploader(props) {
useEffect(() => {
if (window.AliyunUpload && window.AliyunUpload.Vod) {
} else {
$.getScript(
`${_url_origin}/react/${_path}/js/aliyun-upload/lib/es6-promise.min.js`,
(data, textStatus, jqxhr) => {
$.getScript(
`${_url_origin}/react/${_path}/js/aliyun-upload/lib/aliyun-oss-sdk-5.3.1.min.js`,
(data, textStatus, jqxhr) => {
$.getScript(
`${_url_origin}/react/${_path}/js/aliyun-upload/aliyun-upload-sdk-1.5.0.min.js`,
(data, textStatus, jqxhr) => {
});
});
});
}
$('#fileUpload').on('change', function (e) {
var file = e.target.files[0]
if (!file) {
alert("请先选择需要上传的文件!")
return
}
var Title = file.name
var userData = '{"Vod":{}}'
if (uploader) {
uploader.stopUpload()
$('#auth-progress').text('0')
$('#status').text("")
}
if (!uploader) {
uploader = createUploader()
}
// 首先调用 uploader.addFile(event.target.files[i], null, null, null, userData)
console.log(uploader)
let result = uploader.addFile(file, null, null, null, userData)
$('#authUpload').attr('disabled', false)
// $('#pauseUpload').attr('disabled', true)
// $('#resumeUpload').attr('disabled', true)
})
return () => {
$('#fileUpload').off('change')
}
}, [])
let { source, id, className, type } = props;
function onStop() {
$('#resumeUpload').attr('disabled', false)
// $('#pauseUpload').attr('disabled', true)
uploader.stopUpload()
}
function onResume() {
// $('#resumeUpload').attr('disabled', true)
// $('#pauseUpload').attr('disabled', false)
uploader.startUpload()
}
return(
<React.Fragment>
<div>
<input type="file" id="fileUpload"></input>
<label class="status">上传状态: <span id="status"></span></label>
</div>
<div class="upload-type">
上传方式一, 使用 UploadAuth 上传:
{/* <button id="authUpload" disabled="true"></button>
<button id="pauseUpload" disabled="true" onClick={onStop}>暂停</button>
<button id="resumeUpload" disabled="true" onClick={onResume}>恢复上传</button> */}
<button id="authUpload" >开始上传</button>
<button id="pauseUpload" onClick={onStop}>暂停</button>
<button id="resumeUpload" onClick={onResume}>恢复上传</button>
<span class="progress">上传进度: <i id="auth-progress">0</i> %</span>
<span></span>
</div>
</React.Fragment>
)
}
export default AliyunUploader

@ -0,0 +1,188 @@
import React, { useState, useEffect, memo } from 'react';
import { getUrl2, isDev } from 'educoder'
const $ = window.$
let _url_origin = getUrl2()
let _path = _isDev ? 'public' : 'build'
let uploader
function createUploader () {
uploader = new AliyunUpload.Vod({
timeout: $('#timeout').val() || 60000,
partSize: $('#partSize').val() || 1048576,
parallel: $('#parallel').val() || 5,
retryCount: $('#retryCount').val() || 3,
retryDuration: $('#retryDuration').val() || 2,
region: $('#region').val() || 'ap-southeast-1',
userId: $('#userId').val() || 1202060945918292, // 1303984639806000,
// 添加文件成功
addFileSuccess: function (uploadInfo) {
console.log('addFileSuccess')
$('#authUpload').attr('disabled', false)
$('#resumeUpload').attr('disabled', false)
$('#status').text('添加文件成功, 等待上传...')
console.log("addFileSuccess: " + uploadInfo.file.name)
$('#pauseUpload').attr('disabled', false)
uploader.startUpload()
},
// 开始上传
onUploadstarted: function (uploadInfo) {
// 如果是 UploadAuth 上传方式, 需要调用 uploader.setUploadAuthAndAddress 方法
// 如果是 UploadAuth 上传方式, 需要根据 uploadInfo.videoId是否有值调用点播的不同接口获取uploadauth和uploadAddress
// 如果 uploadInfo.videoId 有值,调用刷新视频上传凭证接口,否则调用创建视频上传凭证接口
// 注意: 这里是测试 demo 所以直接调用了获取 UploadAuth 的测试接口, 用户在使用时需要判断 uploadInfo.videoId 存在与否从而调用 openApi
// 如果 uploadInfo.videoId 存在, 调用 刷新视频上传凭证接口(https://help.aliyun.com/document_detail/55408.html)
// 如果 uploadInfo.videoId 不存在,调用 获取视频上传地址和凭证接口(https://help.aliyun.com/document_detail/55407.html)
if (!uploadInfo.videoId) {
var createUrl = 'https://demo-vod.cn-shanghai.aliyuncs.com/voddemo/CreateUploadVideo?Title=testvod1&FileName=aa.mp4&BusinessType=vodai&TerminalType=pc&DeviceModel=iPhone9,2&UUID=59ECA-4193-4695-94DD-7E1247288&AppVersion=1.0.0&VideoId=5bfcc7864fc14b96972842172207c9e6'
$.get(createUrl, function (data) {
var uploadAuth = data.UploadAuth
var uploadAddress = data.UploadAddress
var videoId = data.VideoId
uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress,videoId)
}, 'json')
$('#status').text('文件开始上传...')
console.log("onUploadStarted:" + uploadInfo.file.name + ", endpoint:" + uploadInfo.endpoint + ", bucket:" + uploadInfo.bucket + ", object:" + uploadInfo.object)
} else {
// 如果videoId有值根据videoId刷新上传凭证
// https://help.aliyun.com/document_detail/55408.html?spm=a2c4g.11186623.6.630.BoYYcY
var refreshUrl = 'https://demo-vod.cn-shanghai.aliyuncs.com/voddemo/RefreshUploadVideo?BusinessType=vodai&TerminalType=pc&DeviceModel=iPhone9,2&UUID=59ECA-4193-4695-94DD-7E1247288&AppVersion=1.0.0&Title=haha1&FileName=xxx.mp4&VideoId=' + uploadInfo.videoId
$.get(refreshUrl, function (data) {
var uploadAuth = data.UploadAuth
var uploadAddress = data.UploadAddress
var videoId = data.VideoId
uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress,videoId)
}, 'json')
}
},
// 文件上传成功
onUploadSucceed: function (uploadInfo) {
console.log("onUploadSucceed: " + uploadInfo.file.name + ", endpoint:" + uploadInfo.endpoint + ", bucket:" + uploadInfo.bucket + ", object:" + uploadInfo.object)
$('#status').text('文件上传成功!')
},
// 文件上传失败
onUploadFailed: function (uploadInfo, code, message) {
console.log("onUploadFailed: file:" + uploadInfo.file.name + ",code:" + code + ", message:" + message)
$('#status').text('文件上传失败!')
},
// 取消文件上传
onUploadCanceled: function (uploadInfo, code, message) {
console.log("Canceled file: " + uploadInfo.file.name + ", code: " + code + ", message:" + message)
$('#status').text('文件上传已暂停!')
},
// 文件上传进度,单位:字节, 可以在这个函数中拿到上传进度并显示在页面上
onUploadProgress: function (uploadInfo, totalSize, progress) {
console.log("onUploadProgress:file:" + uploadInfo.file.name + ", fileSize:" + totalSize + ", percent:" + Math.ceil(progress * 100) + "%")
var progressPercent = Math.ceil(progress * 100)
$('#auth-progress').text(progressPercent)
$('#status').text('文件上传中...')
},
// 上传凭证超时
onUploadTokenExpired: function (uploadInfo) {
// 上传大文件超时, 如果是上传方式一即根据 UploadAuth 上传时
// 需要根据 uploadInfo.videoId 调用刷新视频上传凭证接口(https://help.aliyun.com/document_detail/55408.html)重新获取 UploadAuth
// 然后调用 resumeUploadWithAuth 方法, 这里是测试接口, 所以我直接获取了 UploadAuth
$('#status').text('文件上传超时!')
let refreshUrl = 'https://demo-vod.cn-shanghai.aliyuncs.com/voddemo/RefreshUploadVideo?BusinessType=vodai&TerminalType=pc&DeviceModel=iPhone9,2&UUID=59ECA-4193-4695-94DD-7E1247288&AppVersion=1.0.0&Title=haha1&FileName=xxx.mp4&VideoId=' + uploadInfo.videoId
$.get(refreshUrl, function (data) {
var uploadAuth = data.UploadAuth
uploader.resumeUploadWithAuth(uploadAuth)
console.log('upload expired and resume upload with uploadauth ' + uploadAuth)
}, 'json')
},
// 全部文件上传结束
onUploadEnd: function (uploadInfo) {
$('#status').text('文件上传完毕!')
console.log("onUploadEnd: uploaded all the files")
}
})
return uploader
}
function AliyunUploaderDemo(props) {
useEffect(() => {
if (window.AliyunUpload && window.AliyunUpload.Vod) {
} else {
$.getScript(
`${_url_origin}/react/${_path}/js/aliyun-upload/lib/es6-promise.min.js`,
(data, textStatus, jqxhr) => {
$.getScript(
`${_url_origin}/react/${_path}/js/aliyun-upload/lib/aliyun-oss-sdk-5.3.1.min.js`,
(data, textStatus, jqxhr) => {
$.getScript(
`${_url_origin}/react/${_path}/js/aliyun-upload/aliyun-upload-sdk-1.5.0.min.js`,
(data, textStatus, jqxhr) => {
});
});
});
}
$('#fileUpload').on('change', function (e) {
var file = e.target.files[0]
if (!file) {
alert("请先选择需要上传的文件!")
return
}
var Title = file.name
var userData = '{"Vod":{}}'
if (uploader) {
uploader.stopUpload()
$('#auth-progress').text('0')
$('#status').text("")
}
if (!uploader) {
uploader = createUploader()
}
// 首先调用 uploader.addFile(event.target.files[i], null, null, null, userData)
console.log(uploader)
uploader.addFile(file, null, null, null, userData)
$('#authUpload').attr('disabled', false)
// $('#pauseUpload').attr('disabled', true)
// $('#resumeUpload').attr('disabled', true)
})
// return () => {
// }
}, [])
let { source, id, className, type } = props;
function onStop() {
$('#resumeUpload').attr('disabled', false)
// $('#pauseUpload').attr('disabled', true)
uploader.stopUpload()
}
function onResume() {
// $('#resumeUpload').attr('disabled', true)
// $('#pauseUpload').attr('disabled', false)
uploader.startUpload()
}
return(
<React.Fragment>
<div>
<input type="file" id="fileUpload"></input>
<label class="status">上传状态: <span id="status"></span></label>
</div>
<div class="upload-type">
上传方式一, 使用 UploadAuth 上传:
{/* <button id="authUpload" disabled="true"></button>
<button id="pauseUpload" disabled="true" onClick={onStop}>暂停</button>
<button id="resumeUpload" disabled="true" onClick={onResume}>恢复上传</button> */}
<button id="authUpload" >开始上传</button>
<button id="pauseUpload" onClick={onStop}>暂停</button>
<button id="resumeUpload" onClick={onResume}>恢复上传</button>
<span class="progress">上传进度: <i id="auth-progress">0</i> %</span>
<span></span>
</div>
</React.Fragment>
)
}
export default AliyunUploaderDemo

@ -4,7 +4,10 @@ export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
foreground_select: '#4CACFF'
foreground_select: '#4CACFF',
foreground_orange1: '#FF6800',
foreground_tip: '#333',
},
dark: {
foreground: '#ffffff',

@ -17,10 +17,10 @@ class ActionBtn extends Component {
to==undefined ?
<a href="javascript:void(0)" onClick={this.props.onClick}
{...others}
className={"Actionbtn "+`${map[style]} ${this.props.className}`}
className={"Actionbtn "+`${map[style || 'blue']} ${this.props.className}`}
>{children}</a>
:
<Link to={to} className={"btn "+`${map[this.props.style]} ${this.props.className}`}>{this.props.children}</Link>
<Link to={to} className={"btn "+`${map[this.props.style]} ${this.props.className}`} {...others}>{this.props.children}</Link>
}
</React.Fragment>
)

@ -56,12 +56,14 @@ export { default as MarkdownToHtml } from './components/markdown/MarkdownToHtml'
export { default as DMDEditor } from './components/markdown/DMDEditor'
export { default as Clappr } from './components/media/Clappr'
export { default as AliyunUploader } from './components/media/AliyunUploader'
export { default as ImageLayerHook } from './hooks/ImageLayerHook'
// 外部
export { default as CBreadcrumb } from '../modules/courses/common/CBreadcrumb'
export { CNotificationHOC as CNotificationHOC } from '../modules/courses/common/CNotificationHOC'
export { default as ModalWrapper } from '../modules/courses/common/ModalWrapper'
export { default as NoneData } from '../modules/courses/coursesPublic/NoneData'

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

@ -36,22 +36,39 @@ class Fileslistitem extends Component{
course_id:coursesId
},
}).then((result)=>{
if(result.data.attachment_histories.length===0){
if(result.data.is_pdf===true){
//预览pdf
axios({
method:'get',
url:result.data.url,
responseType: 'arraybuffer',
}).then((result)=>{
var binaryData = [];
binaryData.push(result.data);
this.url =window.URL.createObjectURL(new Blob(binaryData, {type:"application/pdf"}));
window.open(this.url);
})
}else{
let link = document.createElement('a');
document.body.appendChild(link);
link.href = result.data.url;
link.href = "/api"+result.data.url;
link.download = result.data.title;
//兼容火狐浏览器
let evt = document.createEvent("MouseEvents");
evt.initEvent("click", false, false);
link.dispatchEvent(evt);
document.body.removeChild(link);
}
}else{
this.setState({
Showoldfiles:true,
allfiles:result.data
})
}
}).catch((error)=>{
console.log(error)
})
@ -144,6 +161,7 @@ class Fileslistitem extends Component{
loadtype={this.state.Loadtype}
/>:""}
<Showoldfiles
{...this.props}
visible={this.state.Showoldfiles}
allfiles={this.state.allfiles}
closaoldfilesprops={this.closaoldfilesprops}

@ -757,6 +757,7 @@ class Fileslists extends Component{
setupdate={(id)=>this.seactall(id)}
Cancel={this.Cancelvisible}
has_course_groups={this.state.has_course_groups}
attachmentId={this.state.coursesecondcategoryid}
/>:""}
<Titlesearchsection

@ -10,7 +10,7 @@ class CBreadcrumb extends Component{
render(){
let { items, className, separator }=this.props;
return(
<p className={`clearfix mb10 ${className}`}>
<p className={`clearfix mb10 ${className} cBreadcrumb`}>
{ items && items.map( (item, index) => {
if (!item.name) {
return ''

@ -16,6 +16,7 @@ class ModalWrapper extends Component{
}
onCancel = () => {
this.setVisible(false)
this.props.onCancel && this.props.onCancel()
}
onOk = () => {
this.props.onOk && this.props.onOk()

@ -496,6 +496,15 @@ class Coursesleftnav extends Component{
saveNavmoda=()=>{
let {Navmodaltypename,setnavid,NavmodalValue}=this.state;
let id =setnavid;
if(Navmodaltypename===5&&NavmodalValue==="未分班"||Navmodaltypename===2&&NavmodalValue==="未分班"){
this.setState({
NavmodalValuetype:true,
NavmodalValues:"名称不能和未分班一样"
})
return
}
if(NavmodalValue===""){
this.setState({
NavmodalValuetype:true,
@ -639,6 +648,7 @@ class Coursesleftnav extends Component{
}
onDragEnd=(result)=>{
debugger
// console.log(result)
// let {course_modules}=this.props;
// let newcourse_modules=course_modules;
@ -672,19 +682,23 @@ class Coursesleftnav extends Component{
if(result.source.droppableId==="shixun_homework"||result.source.droppableId==="graduation"||result.source.droppableId==="attachment"){
let url ="/course_second_categories/"+result.draggableId+"/move_category.json"
let url ="/course_second_categories/"+result.draggableId+"/move_category.json";
if(result.destination.index!=null){
this.droppablepost(url,result.destination.index+1)
}
}else if(result.source.droppableId==="board"){
let url ="/boards/"+result.draggableId+"/move_category.json"
let url ="/boards/"+result.draggableId+"/move_category.json";
if(result.destination.index!=null) {
this.droppablepost(url, result.destination.index + 1)
}
}else if(result.source.droppableId==="course_group"){
let url ="/course_groups/"+result.draggableId+"/move_category.json"
if(result.draggableId!=1){
let url ="/course_groups/"+result.draggableId+"/move_category.json";
if(result.destination.index!=null) {
this.droppablepost(url, result.destination.index + 1)
}
}
}
}
@ -820,8 +834,16 @@ class Coursesleftnav extends Component{
onInput={this.setNavmodalValue}
/>
</div>
{this.state.NavmodalValuetype===true?<span className={"color-red"}>
<style>
{
`
.ml70{
margin-left: 70px;
}
`
}
</style>
{this.state.NavmodalValuetype===true?<span className={"ml70 color-red"}>
{this.state.NavmodalValues}
</span>:""}
<div className={this.state.NavmodalValuetype===true?"clearfix mt20 edu-txt-center":"clearfix mt50 edu-txt-center"}>
@ -928,9 +950,9 @@ class Coursesleftnav extends Component{
<a className="fl pl46 pd0 Draggablelichild">
<span className={this.props.location.pathname===iem.second_category_url?"color-blue fl ml38 maxwidth155 task-hide Draggablelichild":"fl ml38 maxwidth155 task-hide Draggablelichild"}>{iem.category_name}</span>
<span className={twosandiantype===undefined?this.props.location.pathname===iem.second_category_url?"fr mr20 color-blue Draggablelichild font-14":"fr mr20 color999 Draggablelichild font-14":item.type===twosandiantypes&&twosandiantype===index&&iem.category_name!="未分班"?"none":this.props.location.pathname===iem.second_category_url?"fr mr20 color-blue Draggablelichild font-14":"fr mr20 color999 Draggablelichild font-14"} >{iem.category_count===0?"":iem.category_count}</span>
<span className={twosandiantype===undefined?this.props.location.pathname===iem.second_category_url?"fr mr20 color-blue Draggablelichild font-14":"fr mr20 color999 Draggablelichild font-14":item.type===twosandiantypes&&twosandiantype===index&&iem.category_id!=0?"none":this.props.location.pathname===iem.second_category_url?"fr mr20 color-blue Draggablelichild font-14":"fr mr20 color999 Draggablelichild font-14"} >{iem.category_count===0?"":iem.category_count}</span>
{item.type===twosandiantypes&&twosandiantype===index?
iem.category_name==="未分班"?"":
iem.category_id===0?"":
iem.category_type==="graduation_topics"||iem.category_type==="graduation_tasks"?
<span className={"fr mr20 color999 Draggablelichild font-14"} >{iem.category_count===0?"":iem.category_count}</span>
:<Popover placement="right" content={this.content(item,iem,index)} trigger="hover" key={index}>

@ -6,8 +6,9 @@ class NoneData extends Component{
super(props)
}
render(){
const { style } = this.props;
return(
<div className="edu-tab-con-box clearfix edu-txt-center">
<div className="edu-tab-con-box clearfix edu-txt-center" style={style}>
<img className="edu-nodata-img mb20" src={getImageUrl("images/educoder/nodata.png")}/>
<p className="edu-nodata-p mb20">暂时还没有相关数据哦</p>
</div>

@ -220,12 +220,13 @@ class Selectsetting extends Component{
let coursesId=this.props.match.params.coursesId;
let attachmentId=this.props.match.params.attachmentId;
let attachmentId=this.props.attachmentId;
let url="/files/"+this.props.discussMessageid+".json";
//
axios.put(url,{
course_id:coursesId,
new_attachment_id:newfileList.length===0?undefined:newfileList,
course_second_category_id:this.props.coursesidtype===undefined||this.props.coursesidtype==="node"?0:attachmentId,
is_public:is_public,
is_unified_setting:unified_setting,
publish_time:unified_setting===true?datatime===undefined?moment(new Date()).format('YYYY-MM-DD HH'):datatime:undefined,

@ -42,6 +42,18 @@ class Showoldfiles extends Component{
this.props.closaoldfilesprops()
}
showfiless=(url)=>{
axios({
method:'get',
url:url,
responseType: 'arraybuffer',
}).then((result)=>{
var binaryData = [];
binaryData.push(result.data);
this.url =window.URL.createObjectURL(new Blob(binaryData, {type:"application/pdf"}));
window.open(this.url);
})
}
render(){
let {visible,allfiles}=this.props;
@ -175,7 +187,10 @@ class Showoldfiles extends Component{
<div className="clearfix edu-txt-center lineh-40 bor-bottom-greyE" id={allfiles.id}>
<li className="fl fontlefts">
<a className={"isabox"} href={allfiles.url}>{allfiles.title}</a>
{allfiles.is_pdf===false?
<a className={"isabox"} href={"/api"+allfiles.url} >{allfiles.title}</a>:
<a className={"isabox"} onClick={()=>this.showfiless(allfiles.url)} >{allfiles.title}</a>
}
<span className={"newcolor-orange fl"}>当前版本</span>
</li>
@ -191,7 +206,11 @@ class Showoldfiles extends Component{
<div className="clearfix edu-txt-center lineh-40 bor-bottom-greyE" id={item.id} key={key}>
<li className="fl fontlefts">
<a className={"isabox"} href={item.url}>{item.title}</a>
{item.is_pdf===false?
<a className={"isabox"} href={"/api"+item.url}>{item.title}</a>:
<a className={"isabox"} onClick={()=>this.showfiless(item.url)} >{item.title}</a>
}
</li>
<li className="fl filesves ">

@ -1173,6 +1173,7 @@ samp {
border-right: none!important;
box-shadow: none!important;
}
/* 这个加了干嘛的影响到了带addonAfter的input */
.searchViewAfter,.searchViewAfter:focus,.searchViewAfter .ant-input:hover,.ant-input-group .ant-input:focus{
border-right: none!important;
}

@ -760,6 +760,7 @@ class GraduationTasksSubmitedit extends Component{
height: '30px'
}}>
<Checkbox value={item.user_id}
key={item.user_id}
checked={
task_status.map((item,key)=>{
return parseInt(task_status[key])===item.user_id?true:false

@ -187,8 +187,8 @@ class addCollaborators extends Component{
{
partnerList && partnerList.map((item,key)=>{
return(
<li className="clearfix">
<Checkbox value={item.user_id} key={key} className="fl"></Checkbox>
<li className="clearfix" key={key}>
<Checkbox value={item.user_id} key={item.user_id} className="fl"></Checkbox>
<a target="_blank" className="task-hide color-grey3 fl span1 edu-txt-w80">{item.user_name}</a>
<span className="task-hide fl color-grey edu-txt-w80 span2">{item.nickname}</span>
<span className="task-hide fl color-grey edu-txt-w80 span2">{item.identity}</span>

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { SnackbarHOC } from 'educoder';
import {Link} from 'react-router-dom';
import {Tooltip,Menu} from 'antd';
import Loadable from 'react-loadable';
@ -8,8 +8,8 @@ import {BrowserRouter as Router,Route,Switch} from 'react-router-dom';
import UpgradeModals from '../../modals/UpgradeModals';
import axios from 'axios';
import {getImageUrl} from 'educoder';
import { TPMIndexHOC } from '../../tpm/TPMIndexHOC';
import { CNotificationHOC } from '../../courses/common/CNotificationHOC'
import InfosBanner from './InfosBanner'
import "./usersInfo.css"
import "../../courses/css/members.css"
import "../../courses/css/Courses.css"
@ -39,6 +39,11 @@ const InfosProject = Loadable({
loader: () => import('./InfosProject'),
loading:Loading,
})
const InfosVideo = Loadable({
loader: () => import('./video/InfosVideo'),
loading:Loading,
})
const $ = window.$;
class Infos extends Component{
@ -46,7 +51,7 @@ class Infos extends Component{
super(props);
this.state={
data:undefined,
is_current:true,
is_current:undefined,
is_edit:false,
sign:undefined,
type:0,
@ -88,6 +93,7 @@ class Infos extends Component{
}
//获取个人主页信息
getInfo = (user_login) =>{
let url =`/users/${user_login}/homepage_info.json`;
@ -196,12 +202,12 @@ class Infos extends Component{
}
// 试用申请
trialapplications =()=>{
this.setState({
isRenders: true,
showTrial:true
})
}
// trialapplications =()=>{
// this.setState({
// isRenders: true,
// showTrial:true
// })
// }
cancelModulationModels=()=>{
this.setState({
isRenders: false
@ -214,21 +220,25 @@ class Infos extends Component{
render(){
let {
data ,
is_current,
is_edit,
sign,
type,
followed,
id,
login,
isRenders,
moduleName,
next_gold
}=this.state;
let {username}= this.props.match.params;
let {pathname}=this.props.location;
moduleName=pathname.split("/")[3];
let isCurrent = true;
let currentLogin;
if(this.props.current_user && this.props.current_user.login != this.props.match.params.username){
isCurrent=false;
currentLogin = this.props.current_user.login;
}
const _commonProps = {
is_current: isCurrent,
login: currentLogin
}
return(
<div className="newMain">
{this.state.updata===undefined?"":<UpgradeModals
@ -237,165 +247,14 @@ class Infos extends Component{
{
isRenders && <Trialapplication {...this.props} {...this.state} Cancel={() => this.cancelModulationModels()}/>
}
<div className="user-main-half">
<div className="user-headImg"></div>
<div className="user-headCon">
<div className="pr" style={{"min-height": "465px"}}>
<div className="educontent pt80 clearfix edu-txt-center">
<div className="inline">
<div className="fl headtab">
<span>{is_current ? "我":"TA"}的经验值</span>
<a style={{ cursor: 'default' }}
// href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}/user_experience`}
>{data && data.experience}</a>
</div>
<em className="v-h-line fl"></em>
<div className="fl headtab">
<span>{is_current ? "我":"TA"}的金币</span>
<a style={{ cursor: 'default' }}
// href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}/user_grade`}
id="user_code">{data && data.grade}</a>
</div>
<div className="headphoto mt14">
<img alt="头像" id="user_avatar_show" nhname="avatar_image" src={data && `${getImageUrl('images/'+data.avatar_url)}`}/>
</div>
<div className="fl headtab">
<span>{is_current ? "我":"TA"}的粉丝</span>
<a style={{ cursor: 'default' }}
// href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}/user_fanslist`}
id="user_h_fan_count">{data && data.fan_count}</a>
</div>
<em className="v-h-line fl"></em>
<div className="fl headtab">
<span>{is_current ? "我":"TA"}的关注</span>
<a style={{ cursor: 'default' }}
// href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}/user_watchlist`}
>{data && data.follow_count}</a>
</div>
<span className="clearfix"></span>
<span className="myName">{data && data.name}</span>
</div>
</div>
<div className="educontent mt10 clearfix edu-txt-center">
<div className="inline">
{
data && is_current == false && data.identity =="学生" ? "" : <span className="mypost fl mr10">{data && data.identity}</span>
}
<a
// href={is_current ? `${this.props.Headertop && this.props.Headertop.old_url}/account/authentication` :"javascript:void(0)"}
// target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title={ data && data.authentication ?"已实名认证":"未实名认证"}>
<i className={ data && data.authentication ? "iconfont icon-shenfenrenzheng font-13 color-blue":"iconfont icon-shenfenrenzheng font-13 color-grey-9"}></i>
</Tooltip>
</a>
<a
// href={is_current ? `${this.props.Headertop && this.props.Headertop.old_url}/account/professional_certification` :"javascript:void(0)"}
// target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title={ data && data.professional_certification ?"已职业认证":"未职业认证"}>
<i className={ data && data.professional_certification ? "iconfont icon-zhiyerenzheng font-13 color-blue":"iconfont icon-zhiyerenzheng font-13 color-grey-9"}></i>
</Tooltip>
</a>
<a
// href={is_current ? `${this.props.Headertop && this.props.Headertop.old_url}/account/change_or_bind?type=phone` :"javascript:void(0)"}
// target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title={ data && data.phone_binded ?"已手机认证":"未手机认证"}>
<i className={ data && data.phone_binded ? "iconfont icon-shoujirenzheng font-13 color-blue":"iconfont icon-shoujirenzheng font-13 color-grey-9"}></i>
</Tooltip>
</a>
<a
// href={is_current ? `${this.props.Headertop && this.props.Headertop.old_url}/my/account` :"javascript:void(0)"}
// target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title={ data && data.email_binded ?"已邮箱认证":"未邮箱认证"}>
<i className={ data && data.email_binded ? "iconfont icon-youxiangrenzheng font-13 color-blue":"iconfont icon-youxiangrenzheng font-13 color-grey-9"}></i>
</Tooltip>
</a>
{/* <!--学院管理员身份--> */}
{
data && data.college_identifier &&
<a
// href={`${this.props.Headertop && this.props.Headertop.old_url}/colleges/${data.college_identifier}/statistics`} target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title="学院管理员">
<i className="iconfont icon-chengyuanguanli font-12 color-blue" data-tip-down="学院管理员"></i>
</Tooltip>
</a>
}
</div>
</div>
<div className="mt15 educontent clearfix edu-txt-center">
{sign && <p className="mb20" style={{"height": "28px"}}>
{/* 这家伙很懒,什么都没留下~ */}
{
is_edit && is_current ?
<input type="text" id="mysign" class="mysign-input" placeholder="请输入您的个性签名" style={{height:"20px"}} value={sign} onInput={this.inputSign} onBlur={this.savemysign}/>
:
is_current ?
<a className="mysign-span" onClick={this.editmysign} style={{"display": "block"}}>{sign || ""}</a>
:
<span className="mysign-span" style={{"display": "block","cursor":"default"}}>{sign || ""}</span>
}
</p>}
{
is_current ?
<div className="inline">
{
data && data.attendance_signed ?
<React.Fragment>
<span className="user_default_btn user_grey_btn mb5">已签到</span>
<p id="attendance_notice" className="none font-12 color-grey-6" style={{"display":"block"}}>明日签到&nbsp;<font className="color-orange">+{next_gold}</font>&nbsp;</p>
</React.Fragment>
:
<a herf="javascript:void(0);" onClick={this.signFor} id="attendance" className="user_default_btn user_orange_btn fl mb15">签到</a>
// <a herf="javascript:void(0);" onClick={this.trialapplications} id="authentication_apply" className="user_default_btn user_private_btn fl ml15">试用申请</a>
}
</div>
:
<div className="inline">
<a href="javascript:void(0);" onClick={this.followPerson} className="user_default_btn user_watch_btn user_private_btn fl mr20">{followed ? "取消关注":"关注"}</a>
<a href={`${this.props.Headertop && this.props.Headertop.old_url}/messages/${login}/message_detail?target_ids=${id}`} className="user_default_btn user_private_btn fl">私信</a>
</div>
}
</div>
<div className="edu-txt-center navInfo">
<div className="inline">
<li className={`${moduleName == 'courses' ||moduleName == undefined ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'courses'})}
to={`/users/${username}/courses`}>课堂</Link>
</li>
<li className={`${moduleName == 'shixuns' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'shixuns'})}
to={`/users/${username}/shixuns`}>实训</Link>
</li>
<li className={`${moduleName == 'paths' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'paths'})}
to={`/users/${username}/paths`}>实践课程</Link>
</li>
<li className={`${moduleName == 'projects' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'projects'})}
to={`/users/${username}/projects`}>项目</Link>
</li>
<li className={`${moduleName == 'package' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'package'})}
to={`/users/${username}/package`}>众包</Link>
</li>
{/*{ data && data.identity!="学生" && <li> <a href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}?type=m_bank`}>题库</a></li>}*/}
<InfosBanner
{...this.props}
{...this.state}
{..._commonProps}
signFor={this.signFor}
followPerson={this.followPerson}
></InfosBanner>
</div>
</div>
</div>
</div>
</div>
<Switch {...this.props}>
{/* --------------------------------------------------------------------- */}
@ -405,7 +264,7 @@ class Infos extends Component{
{/* http://localhost:3007/courses/1309/homework/9300/setting */}
<Route exact path="/users/:username/package"
render={
(props) => (<InfosPackage {...this.props} {...props} {...this.state} />)
(props) => (<InfosPackage {...this.props} {...props} {...this.state} {..._commonProps}/>)
}
></Route>
@ -413,35 +272,42 @@ class Infos extends Component{
{/* http://localhost:3007/courses/1309/homework/9300/setting */}
<Route exact path="/users/:username/courses"
render={
(props) => (<InfosCourse {...this.props} {...props} {...this.state} />)
(props) => (<InfosCourse {...this.props} {...props} {...this.state} {..._commonProps}/>)
}
></Route>
{/* 实训 */}
<Route exact path="/users/:username/shixuns"
render={
(props) => (<InfosShixun {...this.props} {...props} {...this.state} />)
(props) => (<InfosShixun {...this.props} {...props} {...this.state} {..._commonProps}/>)
}
></Route>
{/* 实训课程 */}
<Route exact path="/users/:username/paths"
render={
(props) => (<InfosPath {...this.props} {...props} {...this.state} />)
(props) => (<InfosPath {...this.props} {...props} {...this.state} {..._commonProps}/>)
}
></Route>
{/* 项目 */}
<Route exact path="/users/:username/projects"
render={
(props) => (<InfosProject {...this.props} {...props} {...this.state} />)
(props) => (<InfosProject {...this.props} {...props} {...this.state} {..._commonProps}/>)
}
></Route>
{/* 项目 */}
<Route exact path="/users/:username/videoes"
render={
(props) => (<InfosVideo {...this.props} {...props} {...this.state} {..._commonProps}/>)
}
></Route>
<Route exact path="/users/:username"
render={
(props) => (<InfosCourse {...this.props} {...props} {...this.state} />)
(props) => (<InfosCourse {...this.props} {...props} {...this.state} {..._commonProps}/>)
}
></Route>
@ -450,4 +316,5 @@ class Infos extends Component{
)
}
}
export default CNotificationHOC() ( SnackbarHOC() ( TPMIndexHOC(Infos) ));
// CNotificationHOC() ( SnackbarHOC() ( TPMIndexHOC))
export default (Infos) ;

@ -161,7 +161,7 @@ class InfosBank extends Component{
modalSave={modalSave}
></Modals>
<Spin size="large" spinning={isSpin}>
<div className="white-panel edu-back-white pt25 pb25 clearfix ">
<div className="white-panel edu-back-white pt20 pb20 clearfix ">
<li className={type=="publicly" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeType("publicly")}>{is_current ? "我":"TA"}的题库</a></li>
<li className={type=="personal" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeType("personal")}>公共题库</a></li>
</div>

@ -0,0 +1,113 @@
import React, { Component } from 'react';
import {Link} from 'react-router-dom';
import {Tooltip,Menu} from 'antd';
import {getImageUrl} from 'educoder';
import "./usersInfo.css"
import "../../courses/css/members.css"
import "../../courses/css/Courses.css"
import banner from '../../../images/account/infobanner.png'
class InfosBanner extends Component{
constructor(props){
super(props);
}
render(){
let {
data ,
id,
login,
moduleName
}=this.props;
let is_current=this.props.is_current;
let {username}= this.props.match.params;
let {pathname}=this.props.location;
moduleName=pathname.split("/")[3];
return(
<div className="bannerPanel mb60">
<div className="educontent">
<div className="clearfix color-white mb25">
<p className="myPhoto mr30 fl"><img alt="头像" src={data && `${getImageUrl('images/'+data.avatar_url)}`}/></p>
<div className="fl">
<p className="clearfix mt20">
<span className="username task-hide" style={{"maxWidth":'370px'}}>{data && data.name}</span>
{
data && is_current == false && data.identity =="学生" ? "" :
<span className="userpost"><label>{data && data.identity}</label></span>
}
</p>
<p className="mt20">
<Tooltip placement='bottom' title={ data && data.professional_certification ?"已职业认证":"未职业认证"}>
<i className={ data && data.professional_certification ? "iconfont icon-shenfenzhenghaomaguizheng font-18 user-colorgrey-green mr30 ml2":"iconfont icon-shenfenzhenghaomaguizheng font-18 user-colorgrey-B8 mr30 ml2"}></i>
</Tooltip>
<Tooltip placement='bottom' title={ data && data.authentication ?"已实名认证":"未实名认证"}>
<i className={ data && data.authentication ? "iconfont icon-renzhengshangjia font-18 user-colorgrey-green":"iconfont icon-renzhengshangjia font-18 user-colorgrey-B8"}></i>
</Tooltip>
</p>
</div>
<div className="fr">
<div class="fl headtab mt20">
<span>{is_current ? "我":"TA"}的经验值</span>
<a style={{"cursor":"default"}}>{data && data.experience}</a>
</div>
<div class="fl headtab mt20 pr leftTransform pl20">
<span>{is_current ? "我":"TA"}的金币</span>
<a style={{"cursor":"default"}}>{data && data.grade}</a>
</div>
{
is_current ?
<span className="fl mt35 ml60">
{
data && data.attendance_signed ?
<span className="user_default_btn user_grey_btn font-18">已签到</span>
:
<a herf="javascript:void(0);" onClick={this.props.signFor} className="user_default_btn user_yellow_btn fl font-18">签到</a>
}
</span>
:
<span className="fl mt35 ml60">
<a href={`${this.props.Headertop && this.props.Headertop.old_url}/messages/${login}/message_detail?target_ids=${id}`} className="user_default_btn user_yellow_btn fl font-18">私信</a>
</span>
}
</div>
</div>
<div className="userNav">
<li className={`${moduleName == 'courses' ||moduleName == undefined ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'courses'})}
to={`/users/${username}/courses`}>翻转课堂</Link>
</li>
<li className={`${moduleName == 'shixuns' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'shixuns'})}
to={`/users/${username}/shixuns`}>开发社区</Link>
</li>
<li className={`${moduleName == 'paths' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'paths'})}
to={`/users/${username}/paths`}>实践课程</Link>
</li>
<li className={`${moduleName == 'projects' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'projects'})}
to={`/users/${username}/projects`}>项目</Link>
</li>
<li className={`${moduleName == 'package' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'package'})}
to={`/users/${username}/package`}>众包</Link>
</li>
{/* <li className={`${moduleName == 'videoes' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'videoes'})}
to={`/users/${username}/videoes`}>视频</Link>
</li> */}
</div>
</div>
</div>
)
}
}
export default InfosBanner;

@ -107,7 +107,7 @@ class InfosCourse extends Component{
return(
<div className="educontent">
<Spin size="large" spinning={isSpin}>
<div className="white-panel edu-back-white pt25 pb25 clearfix ">
<div className="white-panel edu-back-white pt20 pb20 clearfix ">
<li className={category ? "" : "active"}><a href="javascript:void(0)" onClick={()=>this.changeCategory()}>全部</a></li>
<li className={category=="manage" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("manage")}>{is_current ? "我":"TA"}管理的</a></li>
<li className={category=="study" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("study")}>{is_current ? "我":"TA"}学习的</a></li>

@ -0,0 +1,79 @@
import React, { Component } from 'react';
import {Link} from 'react-router-dom';
import {BrowserRouter as Router,Route,Switch} from 'react-router-dom';
import { SnackbarHOC } from 'educoder';
import { TPMIndexHOC } from '../../tpm/TPMIndexHOC';
import { CNotificationHOC } from '../../courses/common/CNotificationHOC'
import Loadable from 'react-loadable';
import Loading from '../../../Loading';
const UsersInfo = Loadable({
loader: () => import('./Infos'),
loading: Loading,
})
const VideoUploadList = Loadable({
loader: () => import('./video/VideoUploadList'),
loading: Loading,
})
const VideoPublishSuccess = Loadable({
loader: () => import('./video/VideoPublishSuccess'),
loading: Loading,
})
const $ = window.$;
class InfosIndex extends Component{
constructor(props){
super(props);
this.state={
data:undefined,
}
}
componentDidMount =()=>{
}
//判断是否看的是当前用户的个人主页
componentDidUpdate =(prevProps)=> {
}
render(){
let {
data ,
}=this.state;
return(
<Switch {...this.props}>
{/* --------------------------------------------------------------------- */}
{/* 视频发布 */}
<Route exact path="/users/:username/videoes/upload"
render={
(props) => (<VideoUploadList {...this.props} {...props} {...this.state} />)
}
></Route>
<Route exact path="/users/:username/videoes/success"
render={
(props) => (<VideoPublishSuccess {...this.props} {...props} {...this.state} />)
}
></Route>
<Route path="/users/:username"
render={
(props) => (<UsersInfo {...this.props} {...props} {...this.state} />)
}
></Route>
</Switch>
)
}
}
export default CNotificationHOC() ( SnackbarHOC() ( TPMIndexHOC(InfosIndex) ));

@ -168,7 +168,7 @@ class InfosPackage extends Component{
modalSave={this.state.ModalSave}
/>
<Spin size="large" spinning={isSpin}>
<div className="white-panel edu-back-white pt25 pb25 clearfix ">
<div className="white-panel edu-back-white pt20 pb20 clearfix ">
<li className={category ? "" : "active"}><a onClick={()=>this.changeCategory()}>全部</a></li>
<li className={category=="manage" ? "active" : ""}><a onClick={()=>this.changeCategory("manage")}>{is_current ? "我":"TA"}管理的</a></li>
<li className={category=="bidden" ? "active" : ""}><a onClick={()=>this.changeCategory("bidden")}>{is_current ? "我":"TA"}参与的</a></li>

@ -120,7 +120,7 @@ class InfosPath extends Component{
return(
<div className="educontent">
<Spin size="large" spinning={isSpin}>
<div className="white-panel edu-back-white pt25 pb25 clearfix ">
<div className="white-panel edu-back-white pt20 pb20 clearfix ">
<li className={category ? "" : "active"}><a href="javascript:void(0)" onClick={()=>this.changeCategory()}>全部</a></li>
<li className={category=="manage" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("manage")}>{is_current ? "我":"TA"}管理的</a></li>
<li className={category=="study" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("study")}>{is_current ? "我":"TA"}学习的</a></li>

@ -102,7 +102,7 @@ class InfosProject extends Component{
return(
<div className="educontent">
<Spin size="large" spinning={isSpin}>
<div className="white-panel edu-back-white pt25 pb25 clearfix ">
<div className="white-panel edu-back-white pt20 pb20 clearfix ">
<li className={category ? "" : "active"}><a href="javascript:void(0)" onClick={()=>this.changeCategory()}>全部</a></li>
<li className={category=="manage" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("manage")}>{is_current ? "我":"TA"}管理的</a></li>
<li className={category=="study" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("study")}>{is_current ? "我":"TA"}学习的</a></li>

@ -120,7 +120,7 @@ class InfosShixun extends Component{
return(
<div className="educontent">
<Spin size="large" spinning={isSpin}>
<div className="white-panel edu-back-white pt25 pb25 clearfix ">
<div className="white-panel edu-back-white pt20 pb20 clearfix ">
<li className={category ? "" : "active"}><a href="javascript:void(0)" onClick={()=>this.changeCategory()}>全部</a></li>
<li className={category=="manage" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("manage")}>{is_current ? "我":"TA"}管理的</a></li>
<li className={category=="study" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("study")}>{is_current ? "我":"TA"}学习的</a></li>

@ -0,0 +1,190 @@
import React, { Component } from 'react';
import {Link} from 'react-router-dom';
import {Tooltip,Menu} from 'antd';
import {getImageUrl} from 'educoder';
import "./usersInfo.css"
import "../../courses/css/members.css"
import "../../courses/css/Courses.css"
class banner_out extends Component{
constructor(props){
super(props);
}
render(){
let {
data ,
is_current,
is_edit,
sign,
type,
followed,
id,
login,
moduleName,
next_gold
}=this.props;
let {username}= this.props.match.params;
return(
<div className="user-main-half">
<div className="user-headImg"></div>
<div className="user-headCon">
<div className="pr" style={{"min-height": "465px"}}>
<div className="educontent pt80 clearfix edu-txt-center">
<div className="inline">
<div className="fl headtab">
<span>{is_current ? "我":"TA"}的经验值</span>
<a style={{ cursor: 'default' }}
// href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}/user_experience`}
>{data && data.experience}</a>
</div>
<em className="v-h-line fl"></em>
<div className="fl headtab">
<span>{is_current ? "我":"TA"}的金币</span>
<a style={{ cursor: 'default' }}
// href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}/user_grade`}
id="user_code">{data && data.grade}</a>
</div>
<div className="headphoto mt14">
<img alt="头像" id="user_avatar_show" nhname="avatar_image" src={data && `${getImageUrl('images/'+data.avatar_url)}`}/>
</div>
<div className="fl headtab">
<span>{is_current ? "我":"TA"}的粉丝</span>
<a style={{ cursor: 'default' }}
// href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}/user_fanslist`}
id="user_h_fan_count">{data && data.fan_count}</a>
</div>
<em className="v-h-line fl"></em>
<div className="fl headtab">
<span>{is_current ? "我":"TA"}的关注</span>
<a style={{ cursor: 'default' }}
// href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}/user_watchlist`}
>{data && data.follow_count}</a>
</div>
<span className="clearfix"></span>
<span className="myName">{data && data.name}</span>
</div>
</div>
<div className="educontent mt10 clearfix edu-txt-center">
<div className="inline">
{
data && is_current == false && data.identity =="学生" ? "" : <span className="mypost fl mr10">{data && data.identity}</span>
}
<a
// href={is_current ? `${this.props.Headertop && this.props.Headertop.old_url}/account/authentication` :"javascript:void(0)"}
// target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title={ data && data.authentication ?"已实名认证":"未实名认证"}>
<i className={ data && data.authentication ? "iconfont icon-shenfenrenzheng font-13 color-blue":"iconfont icon-shenfenrenzheng font-13 color-grey-9"}></i>
</Tooltip>
</a>
<a
// href={is_current ? `${this.props.Headertop && this.props.Headertop.old_url}/account/professional_certification` :"javascript:void(0)"}
// target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title={ data && data.professional_certification ?"已职业认证":"未职业认证"}>
<i className={ data && data.professional_certification ? "iconfont icon-zhiyerenzheng font-13 color-blue":"iconfont icon-zhiyerenzheng font-13 color-grey-9"}></i>
</Tooltip>
</a>
<a
// href={is_current ? `${this.props.Headertop && this.props.Headertop.old_url}/account/change_or_bind?type=phone` :"javascript:void(0)"}
// target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title={ data && data.phone_binded ?"已手机认证":"未手机认证"}>
<i className={ data && data.phone_binded ? "iconfont icon-shoujirenzheng font-13 color-blue":"iconfont icon-shoujirenzheng font-13 color-grey-9"}></i>
</Tooltip>
</a>
<a
// href={is_current ? `${this.props.Headertop && this.props.Headertop.old_url}/my/account` :"javascript:void(0)"}
// target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title={ data && data.email_binded ?"已邮箱认证":"未邮箱认证"}>
<i className={ data && data.email_binded ? "iconfont icon-youxiangrenzheng font-13 color-blue":"iconfont icon-youxiangrenzheng font-13 color-grey-9"}></i>
</Tooltip>
</a>
{/* <!--学院管理员身份--> */}
{
data && data.college_identifier &&
<a
// href={`${this.props.Headertop && this.props.Headertop.old_url}/colleges/${data.college_identifier}/statistics`} target="_blank"
className={is_current ? "ringauto fl" :"ringauto fl cdefault"}>
<Tooltip placement='bottom' title="学院管理员">
<i className="iconfont icon-chengyuanguanli font-12 color-blue" data-tip-down="学院管理员"></i>
</Tooltip>
</a>
}
</div>
</div>
<div className="mt15 educontent clearfix edu-txt-center">
<p className="mb20" style={{"height": "28px"}}>
{
is_edit && is_current ?
<input type="text" id="mysign" class="mysign-input" placeholder="请输入您的个性签名" style={{height:"20px"}} value={sign} onInput={this.inputSign} onBlur={this.savemysign}/>
:
is_current ?
<a className="mysign-span" onClick={this.editmysign} style={{"display": "block"}}>{sign || "这家伙很懒,什么都没留下~"}</a>
:
<span className="mysign-span" style={{"display": "block","cursor":"default"}}>{sign || "这家伙很懒,什么都没留下~"}</span>
}
</p>
{
is_current ?
<div className="inline">
{
data && data.attendance_signed ?
<React.Fragment>
<span className="user_default_btn user_grey_btn mb5">已签到</span>
<p id="attendance_notice" className="none font-12 color-grey-6" style={{"display":"block"}}>明日签到&nbsp;<font className="color-orange">+{next_gold}</font>&nbsp;</p>
</React.Fragment>
:
<a herf="javascript:void(0);" onClick={this.props.signFor} id="attendance" className="user_default_btn user_orange_btn fl mb15">签到</a>
// <a herf="javascript:void(0);" onClick={this.trialapplications} id="authentication_apply" className="user_default_btn user_private_btn fl ml15">试用申请</a>
}
</div>
:
<div className="inline">
<a href="javascript:void(0);" onClick={this.props.followPerson} className="user_default_btn user_watch_btn user_private_btn fl mr20">{followed ? "取消关注":"关注"}</a>
<a href={`${this.props.Headertop && this.props.Headertop.old_url}/messages/${login}/message_detail?target_ids=${id}`} className="user_default_btn user_private_btn fl">私信</a>
</div>
}
</div>
<div className="edu-txt-center navInfo">
<div className="inline">
<li className={`${moduleName == 'courses' ||moduleName == undefined ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'courses'})}
to={`/users/${username}/courses`}>课堂</Link>
</li>
<li className={`${moduleName == 'shixuns' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'shixuns'})}
to={`/users/${username}/shixuns`}>实训</Link>
</li>
<li className={`${moduleName == 'paths' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'paths'})}
to={`/users/${username}/paths`}>实践课程</Link>
</li>
<li className={`${moduleName == 'projects' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'projects'})}
to={`/users/${username}/projects`}>项目</Link>
</li>
<li className={`${moduleName == 'package' ? 'active' : '' }`}>
<Link
onClick={() => this.setState({moduleName: 'package'})}
to={`/users/${username}/package`}>众包</Link>
</li>
{/*{ data && data.identity!="学生" && <li> <a href={`${this.props.Headertop && this.props.Headertop.old_url}/users/${username}?type=m_bank`}>题库</a></li>}*/}
</div>
</div>
</div>
</div>
</div>
)
}
}
export default banner_out;

@ -0,0 +1,96 @@
import React, { useState, useEffect, useContext, useRef, memo } from 'react';
import {Link} from 'react-router-dom';
import { Icon } from 'antd'
import { getUrl2, isDev, ThemeContext } from 'educoder'
import axios from 'axios'
function CRoundSelect (props) {
const [open, setOpen] = useState(false)
const theme = useContext(ThemeContext);
const { category, changeCategory, categories, right, width, items,
sortKey, onSortChange } = props;
const username = props.match.params.username
useEffect(() => {
}, [])
function onToggleOpen(over) {
if (over) {
console.log('over')
setOpen(true)
} else {
console.log('out')
setOpen(false)
}
}
function findIndexByKey(key) {
let _index = -1
items && items.some((item, index) => {
if (item.key == key) {
_index = index
return true;
}
})
return _index
}
function _onSortChange(key, index) {
if (index == 0) {
return;
}
setOpen(false)
onSortChange(key, index)
}
let index = findIndexByKey(sortKey)
return (
<React.Fragment>
<div className="" style={{position: 'relative', lineHeight: '24px'}}>
{/* onMouseOut={onToggleOpen} */}
<div className="trigger" onMouseOver={() => onToggleOpen(true)} >
<style>{`
.trigger, .droplist {
padding: 0px 6px;
border: 1px solid ${theme.foreground_select};
color: ${theme.foreground_select};
border-radius: 6px;
}
.trigger {
width: ${width || 'fit-content'};
cursor: pointer;
}
.droplist {
width: ${width || 'fit-content'};
position: absolute;
z-index: 2;
top: 0px;
background: #fff;
cursor: pointer;
}
`}</style>
<div className="currentItem">
{items[index].name} <Icon type="down" />
</div>
</div>
{true && <ul className="droplist" onMouseLeave={() => onToggleOpen(false)}
style={{display: open ? 'block' : 'none'}}
>
{items.map((item, index) =>
<li key={item.key} className=""
onClick={() => _onSortChange(item.key, index)}>{item.name}</li>
)}
{/* <li className="">AAAAAAAA</li>
<li className="">BBBBBBB</li>
<li className="">CCCCCCC</li> */}
</ul> }
</div>
</React.Fragment>
)
}
export default CRoundSelect

@ -0,0 +1,56 @@
import React, { useState, useEffect, useContext, useRef, memo } from 'react';
import {Link} from 'react-router-dom';
import { getUrl2, isDev, ThemeContext } from 'educoder'
import { Modal } from 'antd'
function HeadlessModal (props) {
// const [ visible, setVisible ] = useState(false)
const theme = useContext(ThemeContext);
const { category, visible, setVisible, className, width } = props;
useEffect(() => {
}, [])
return (
<Modal
visible={visible}
className={`headless ${className}`}
title={null}
footer={null}
width={width}
>
<style>{`
.headless .ant-modal-close {
display:none;
}
.headless .ant-modal-body {
padding: 0px;
}
.headless .closeBtn {
position: absolute;
color: ${theme.foreground_select};
top: -8px;
right: -10px;
font-size: 24px !important;
background: #fff;
width: 14px;
height: 8px;
margin-right: 0px;
}
.headless .icon-htmal5icon19:before {
left: -4px;
position: absolute;
top: -13px;
}
`}</style>
<i className="iconfont icon-htmal5icon19 closeBtn" onClick={ () => setVisible(false) }></i>
{props.children}
</Modal>
)
}
export default HeadlessModal

@ -0,0 +1,35 @@
import React, { useState, useEffect, useContext, useRef, memo } from 'react';
import {Link} from 'react-router-dom';
import { getUrl2, isDev, ThemeContext } from 'educoder'
import axios from 'axios'
function InfoTab (props) {
const theme = useContext(ThemeContext);
const { category, changeCategory, categories, right } = props;
const username = props.match.params.username
useEffect(() => {
}, [])
return (
<div className="white-panel edu-back-white pt20 pb20 clearfix ">
{categories && categories.map(item => {
return (
<li key={item.key} className={category == item.key ? "active" : ''}><a href="javascript:void(0)" onClick={()=>changeCategory(item.key)}>{item.name}</a></li>
)
})}
{/* <li className={category ? "" : "active"}><a href="javascript:void(0)" onClick={()=>this.changeCategory()}></a></li>
<li className={category=="manage" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("manage")}>{is_current ? "我":"TA"}管理的</a></li>
<li className={category=="study" ? "active" : ""}><a href="javascript:void(0)" onClick={()=>this.changeCategory("study")}>{is_current ? "我":"TA"}学习的</a></li> */}
<div className="fr">
{right}
</div>
</div>
)
}
export default InfoTab

@ -120,3 +120,110 @@
display: block;
font-size: 20px;
}
/* 个人主页头部改版 */
.bannerPanel{
width:100%;
top:0px;
left:0px;
height:186px;
padding-top:25px;
box-sizing: border-box;
background: url('../../../images/account/infobanner.png') no-repeat top center;
background-color: #0d4473
}
.username{
font-size:26px;
margin-right:15px;
height: 30px;
line-height: 30px;
float: left;
color:#fff;
}
.myPhoto img{
border-radius: 50%;
width:100%;
height:100%
}
.myPhoto{
border-radius: 50%;
border:2px solid #fff;
width:110px;
height:110px;
}
.userpost{
padding:0px 10px;
border:1px solid #fff;
border-radius: 2px;
color:#fff!important;
display: inline;
margin-top: 6px;
float:left;
}
.userpost label{
display:block;
height: 18px;
line-height:18px;
}
.user-colorgrey-B8{color:#B8B8B8}
.user-colorgrey-green{color:#7ED321}
.user_yellow_btn {
color: #fff!important;
background-color: #FF8E02;
}
.headtab span {
color: #e8e8e8;
font-size: 14px;
}
.headtab span, .headtab a {
display: block;
width: 100%;
text-align: center;
}
.headtab {
width: 140px;
height: 70px;
text-align: center;
}
.leftTransform:before{
position: absolute;
content:'';
left:0px;
top:6px;
width:1px;
height:84%;
background: #fff;
transform: rotate(18deg);
}
.userNav{
height: 54px;
line-height: 54px;
background: #fff;
box-shadow:0px 9px 16px -1px #e6e6e6;
border-radius:7px;
padding:0px 10px;
}
.userNav li{
display: inline-block;
padding:0px 30px;
box-sizing: border-box
}
.userNav li a{
color:#333333;
font-size: 16px;
display: block
}
.userNav li.active a{
position: relative;
color:#4CACFF;
}
.userNav li.active a:after{
position: absolute;
bottom: 0px;
height: 4px;
width:100%;
content: '';
left:0px;
background: #4CACFF;
}

@ -0,0 +1,175 @@
import { getUrl2, isDev } from 'educoder'
import axios from 'axios'
let _url_origin = getUrl2()
let _path = isDev() ? 'public' : 'build'
let _testHost = '' ; // 'http://192.168.2.63:3001/api' ; // '' ;
let login = 'innov'
let uploader;
let $ = window.$
function loadLib(callback) {
$.getScript(
`${_url_origin}/react/${_path}/js/aliyun-upload/lib/es6-promise.min.js`,
(data, textStatus, jqxhr) => {
$.getScript(
`${_url_origin}/react/${_path}/js/aliyun-upload/lib/aliyun-oss-sdk-5.3.1.min.js`,
(data, textStatus, jqxhr) => {
$.getScript(
`${_url_origin}/react/${_path}/js/aliyun-upload/aliyun-upload-sdk-1.5.0.min.js`,
(data, textStatus, jqxhr) => {
callback && callback()
});
});
});
}
function createUploader(options) {
if (window.AliyunUpload && window.AliyunUpload.Vod) {
doCreateUploader(options)
} else {
loadLib(() => {
doCreateUploader(options)
})
}
}
function doCreateUploader (options) {
uploader = new window.AliyunUpload.Vod({
timeout: $('#timeout').val() || 60000,
partSize: $('#partSize').val() || 1048576,
parallel: $('#parallel').val() || 5,
retryCount: $('#retryCount').val() || 3,
retryDuration: $('#retryDuration').val() || 2,
region: $('#region').val() || 'ap-southeast-1',
userId: $('#userId').val() || 1202060945918292, // 1303984639806000,
// 添加文件成功
addFileSuccess: function (uploadInfo) {
console.log("addFileSuccess: " + uploadInfo.file.name)
uploader.startUpload()
},
// 开始上传
onUploadstarted: function (uploadInfo) {
// 如果是 UploadAuth 上传方式, 需要调用 uploader.setUploadAuthAndAddress 方法
// 如果是 UploadAuth 上传方式, 需要根据 uploadInfo.videoId是否有值调用点播的不同接口获取uploadauth和uploadAddress
// 如果 uploadInfo.videoId 有值,调用刷新视频上传凭证接口,否则调用创建视频上传凭证接口
// 注意: 这里是测试 demo 所以直接调用了获取 UploadAuth 的测试接口, 用户在使用时需要判断 uploadInfo.videoId 存在与否从而调用 openApi
// 如果 uploadInfo.videoId 存在, 调用 刷新视频上传凭证接口(https://help.aliyun.com/document_detail/55408.html)
// 如果 uploadInfo.videoId 不存在,调用 获取视频上传地址和凭证接口(https://help.aliyun.com/document_detail/55407.html)
const fileName = uploadInfo.file.name
if (!uploadInfo.videoId) {
var createUrl = `${_testHost}/users/${login}/video_auths.json?debug=true`
axios.post(createUrl, {
title: fileName,
file_name: fileName
}).then((response) => {
// if (response.data.status == )
const data = response.data.data
var uploadAuth = data.UploadAuth
var uploadAddress = data.UploadAddress
var videoId = data.VideoId
uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress, videoId)
options.addFileSuccess && options.addFileSuccess(uploadInfo)
}).catch((error) => {
// 删除当前出错的,并执行下一个任务
uploader.deleteFile(uploader._curIndex)
uploader.nextUpload()
console.log(error)
})
$('#status').text('文件开始上传...')
console.log("onUploadStarted:" + uploadInfo.file.name + ", endpoint:" + uploadInfo.endpoint + ", bucket:" + uploadInfo.bucket + ", object:" + uploadInfo.object)
} else {
// 如果videoId有值根据videoId刷新上传凭证
var refreshUrl = `${_testHost}/users/${login}/video_auths.json?debug=true`
axios.put(refreshUrl, {
video_id: uploadInfo.videoId,
}).then((response) => {
if (response.data.status == -1) {
options.onUploadError && options.onUploadError(uploadInfo)
return;
}
const data = response.data.data
var uploadAuth = data.UploadAuth
var uploadAddress = data.UploadAddress
var videoId = data.VideoId
uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress, videoId)
options.addFileSuccess && options.addFileSuccess(uploadInfo)
}).catch((error) => {
uploader.deleteFile(uploader._curIndex)
uploader.nextUpload()
console.log(error)
})
}
},
// 文件上传成功
onUploadSucceed: function (uploadInfo) {
options.onUploadSucceed && options.onUploadSucceed(uploadInfo)
console.log("onUploadSucceed: " + uploadInfo.file.name + ", endpoint:" + uploadInfo.endpoint + ", bucket:" + uploadInfo.bucket + ", object:" + uploadInfo.object)
$('#status').text('文件上传成功!')
},
// 文件上传失败
onUploadFailed: function (uploadInfo, code, message) {
console.log("onUploadFailed: file:" + uploadInfo.file.name + ",code:" + code + ", message:" + message)
$('#status').text('文件上传失败!')
},
// 取消文件上传
onUploadCanceled: function (uploadInfo, code, message) {
console.log("Canceled file: " + uploadInfo.file.name + ", code: " + code + ", message:" + message)
$('#status').text('文件上传已暂停!')
},
// 文件上传进度,单位:字节, 可以在这个函数中拿到上传进度并显示在页面上
onUploadProgress: function (uploadInfo, totalSize, progress) {
options.onUploadProgress && options.onUploadProgress(uploadInfo, totalSize, progress)
console.log("onUploadProgress:file:" + uploadInfo.file.name + ", fileSize:" + totalSize + ", percent:" + Math.ceil(progress * 100) + "%")
var progressPercent = Math.ceil(progress * 100)
$('#auth-progress').text(progressPercent)
$('#status').text('文件上传中...')
},
// 上传凭证超时
onUploadTokenExpired: function (uploadInfo) {
// 上传大文件超时, 如果是上传方式一即根据 UploadAuth 上传时
// 需要根据 uploadInfo.videoId 调用刷新视频上传凭证接口(https://help.aliyun.com/document_detail/55408.html)重新获取 UploadAuth
// 然后调用 resumeUploadWithAuth 方法, 这里是测试接口, 所以我直接获取了 UploadAuth
$('#status').text('文件上传超时!')
var refreshUrl = `${_testHost}/users/${login}/video_auths.json?debug=true`
axios.put(refreshUrl, {
video_id: uploadInfo.videoId,
}).then((response) => {
const data = response.data.data
var uploadAuth = data.UploadAuth
uploader.resumeUploadWithAuth(uploadAuth)
}).catch((error) => {
console.log(error)
})
},
// 全部文件上传结束
onUploadEnd: function (uploadInfo) {
options.onUploadEnd && options.onUploadEnd(uploadInfo)
$('#status').text('文件上传完毕!')
console.log("onUploadEnd: uploaded all the files")
}
})
if (options.gotUploader) {
options.gotUploader(uploader)
}
}
export function getUploader (_login, options) {
_login && (login = _login)
if (!uploader) {
uploader = createUploader(options)
}
return uploader;
}

@ -0,0 +1,86 @@
import React, { useState, useEffect, useContext, useRef, memo } from 'react';
import { Progress, Input, Tooltip, Form } from 'antd'
import { getUrl2, isDev, CBreadcrumb, ActionBtn, ThemeContext, ModalWrapper } from 'educoder'
import axios from 'axios'
const MAX_LENGTH = 30
function EditVideoModal (props) {
const modalEl = useRef(null);
const theme = useContext(ThemeContext);
const { history, videoId, cover_url, title, created_at, isReview, onEditVideo, visible, setVisible,
form, editSuccess } = props;
const getFieldDecorator = form.getFieldDecorator
const username = props.match.params.username
const _title = form.getFieldsValue().title;
function toList() {
history.push(`/users/${username}/videoes`)
}
function toUpload() {
history.push(`/users/${username}/videoes/upload`)
}
function onOk() {
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const url = `/users/${username}/videos/${videoId}.json`
axios.put(url, {
title: _title
}).then((response) => {
if (response.data) {
onCancel()
editSuccess()
}
}).catch((e) => {
})
} else {
// $("html").animate({ scrollTop: $('html').scrollTop() - 100 })
}
})
// setVisible(false)
}
function onCancel() {
setVisible(false)
}
useEffect(() => {
modalEl.current.setVisible(visible)
}, [visible])
useEffect(() => {
visible && form.setFieldsValue({
title,
})
}, [visible])
return (
<ModalWrapper
ref={modalEl}
width="600px"
title={`视频编辑`}
{ ...props }
onOk={onOk}
onCancel={onCancel}
className="editVideoModal"
>
<Form.Item
label="视频标题"
className="title formItemInline"
>
{getFieldDecorator('title', {
rules: [{
required: true, message: '请输入标题',
}, {
max: MAX_LENGTH, message: '最大限制为30个字符',
}],
})(
<Input placeholder="" className="titleInput" maxLength={MAX_LENGTH}
addonAfter={String(_title ? `${String(_title.length)}/${MAX_LENGTH}` : 0)} />
)}
</Form.Item>
</ModalWrapper>
)
}
const WrappedEditVideoModal = Form.create({ name: 'editVideoModal' })(EditVideoModal);
export default WrappedEditVideoModal

@ -0,0 +1,116 @@
.itemWrap {
display: flex;
flex-wrap: wrap;
}
/* item */
.videoItem {
width: 280px;
margin-right: 26px;
margin-bottom: 26px;
position: relative;
}
.videoItem:nth-child(4n+0) {
margin-right: 0px;
}
.videoItem img.cover {
width: 100%;
border-radius: 6px 6px 0px 0px;
height: 158px;
}
.nItem.videoItem:hover .mask {
display: block;
top: 0px;
width: 100%;
height: 158px;
cursor: pointer;
}
.nItem.videoItem:hover .playWrap {.videoItem img.play
display: inline-block;
}
.nItem .mask {
border-radius: 6px 6px 0px 0px;
display: none;
text-align: center;
position: absolute;
background: #000;
opacity: 0.5;
}
.videoItem .playWrap {
display: none;
width: 100%;
text-align: center;
height: 70px;
position: absolute;
top: 0px;
left: 0px;
}
.videoItem img.play {
margin-top: 20%;
position: relative;
z-index: 99;
}
.videoItem .square-main {
padding: 9px 8px;
background: #fff;
border-radius: 0px 0px 6px 6px;
}
.videoItem .square-main .title{
max-width: 256px;
line-height: 18px;
}
.videoInReviewItem .square-main {
background: #EAEAEA;
}
.videoItem .time {
color: #A0A0A0;
}
.videoItem .square-main .buttonRow {
justify-content: space-between;
line-height: 15px;
}
.nItem.videoItem:hover .square-main {
color: #fff;
background: #333;
}
/* 预览弹框 */
.showVideoModal .ant-modal-body {
display: flex;
flex-direction: column;
}
.showVideoModal video{
width: 800px;
height: 450px;
}
.showVideoModal .copyLine {
justify-content: space-between;
padding: 9px;
background: #000000;
width: 800px;
}
.showVideoModal .copyLine input {
color: #707070;
background-color: #000 !important;
border-color: #707070;
margin-right: 12px;
}
.showVideoModal .copyLine a {
flex: 0 0 106px;
}
.toolbarRow {
justify-content: space-between;
padding: 0 8px;
margin-bottom: 6px;
}
/* 跳转按钮 */
.infoVideo .toUploadBtn {
height: 48px;
margin-right: 20px;
}

@ -0,0 +1,343 @@
import React, { useState, useEffect, useContext, useRef, memo } from 'react';
import {Link} from 'react-router-dom';
import { Pagination, Input, Button } from 'antd'
import { getUrl2, isDev, ThemeContext, ActionBtn, NoneData } from 'educoder'
import axios from 'axios'
import VideoInReviewItem from './VideoInReviewItem'
import EditVideoModal from './EditVideoModal'
import './InfosVideo.css'
import InfoTab from '../common/InfoTab'
import HeadlessModal from '../common/HeadlessModal'
import CRoundSelect from '../common/CRoundSelect'
import ClipboardJS from 'clipboard'
function useModal(initValue) {
const [visible, setVisible] = useState(initValue)
return {
visible,
setVisible
}
}
function useCategory(initValue) {
const [category, setCategory] = useState(initValue)
function changeCategory(key) {
setCategory(key)
}
return {
category,
changeCategory
}
}
function usePagination() {
const [page, setPage] = useState(1)
function onPageChange(page) {
setPage(page)
}
return {
current: page,
onChange: onPageChange
}
}
const PAGE_SIZE = 16
const DEFAULT_VIDEO_WIDTH_IN_MD = "90%" // 400
const DEFAULT_VIDEO_HEIGHT_IN_MD = "55%" // 400
let videoId = {};
let _clipboard = null;
const _items=[
{key: 'published_at-desc', name: '最新上传'},
{key: 'published_at-asc', name: '最早上传'},
]
function InfoVideo (props) {
const [videoes, setVideoes] = useState(undefined)
const [reviewVideoes, setReviewVideoes] = useState(undefined)
const [count, setCount] = useState(0)
const [loading, setLoading] = useState(true)
const [sortKey, setSortKey] = useState(_items[0].key)
const editModalObj = useModal(false)
const videoModalObj = useModal(false)
const categoryObj = useCategory('all')
const pageObj = usePagination()
const theme = useContext(ThemeContext);
const editModalEl = useRef(null);
const videoEl = useRef(null);
const { showNotification, history } = props;
const username = props.match.params.username
function toUpload() {
history.push(`/users/${username}/videoes/upload`)
}
function fetchVideoes() {
const fetchUrl = `/users/${username}/videos.json`
const sorts = sortKey.split('-')
setLoading(true)
axios.get(fetchUrl, {
params: {
page: pageObj.current,
per_page: PAGE_SIZE,
sort_by: sorts[0],
sort_direction: sorts[1],
//
}
})
.then((response) => {
setLoading(false)
if (response.data.videos) {
setVideoes(response.data.videos)
setCount(response.data.count)
}
}).catch(() => {
})
}
function fetchReviewVideoes() {
const fetchUrl = `/users/${username}/videos/review.json`
setLoading(true)
axios.get(fetchUrl, {
params: {
per_page: 200
}
})
.then((response) => {
setLoading(false)
if (response.data.videos) {
setReviewVideoes(response.data.videos)
setCount(response.data.count)
}
}).catch(() => {
})
}
useEffect(() => {
fetchVideoes()
}, [pageObj.current, sortKey])
useEffect(() => {
if (categoryObj.category == 'all') {
fetchVideoes()
} else {
fetchReviewVideoes()
}
}, [categoryObj.category])
useEffect(() => {
if (videoModalObj.visible == false) {
// 关闭视频
videoEl.current && videoEl.current.pause()
if (_clipboard) {
_clipboard.destroy();
_clipboard = null;
}
} else {
setTimeout(() => {
if (!_clipboard) {
_clipboard = new ClipboardJS('.copybtn');
_clipboard.on('success', (e) => {
showNotification('复制成功')
});
}
}, 200)
}
}, [videoModalObj.visible])
useEffect(() => {
}, [])
function editSuccess() {
fetchVideoes()
}
function onEditVideo(item) {
videoId = {
videoId: item.id,
title: item.title
}
editModalObj.setVisible(true)
// editModalEl.current.toList(true, video);
// this.refs['editVideoModal'].setVisible(true, video);
}
function onMaskClick(item) {
videoId = {
videoId: item.id,
title: item.title,
file_url: item.file_url,
cover_url: item.cover_url
}
videoModalObj.setVisible(true)
}
// TODO use封装
function onSortChange(key, index) {
const _item = _items[index]
_items.splice(index, 1)
_items.unshift(_item)
setSortKey(key)
}
function getCopyText (file_url, cover_url) {
return `<video src="${file_url}" controls="true" controlslist="nodownload" width="${DEFAULT_VIDEO_WIDTH_IN_MD}" height="${DEFAULT_VIDEO_HEIGHT_IN_MD}" poster="${cover_url}">您的浏览器不支持 video 标签。</video>`
}
const _inputValue = getCopyText(videoId.file_url, videoId.cover_url)
return (
<div className="educontent infoVideo">
<EditVideoModal {...props} {...editModalObj}
editSuccess={editSuccess}
{...videoId}
></EditVideoModal>
<HeadlessModal
{...videoModalObj}
className="showVideoModal"
width={800 - 1}
>
<video
ref={videoEl}
src={videoId.file_url} controls="true" controlslist="nodownload">
您的浏览器不支持 video 标签
</video>
<div className="df copyLine">
<Input value={_inputValue}
className="dark"
></Input>
<ActionBtn className="copybtn" data-clipboard-text={_inputValue}>复制视频地址</ActionBtn>
</div>
</HeadlessModal>
<style>{`
/* item */
.videoPublishSuccess .section {
background: #fff;
padding: 16px 20px;
padding-top: 0px;
position: relative;
text-align: center;
color: ${theme.foreground_tip};
}
.videoItem .square-main .buttonRow i {
vertical-align: top;
font-size: 16px;
color: ${theme.foreground_select} !important;
margin-left: 6px;
}
/*
(26 - 24) * 3 / 2
*/
.itemWrap {
margin-left: 3px;
}
.videoItem {
margin-right: 24px;
}
`}</style>
<InfoTab
{...props}
categories={[{
key: 'all',
name: '全部视频'
}, {
key: 'review',
name: '待审核视频'
}]}
{...categoryObj}
right={
<Button type="primary" icon="upload"
onClick={() => { toUpload() }}
className="toUploadBtn"
>
上传视频
</Button>
}
></InfoTab>
<div className="toolbarRow mt20 df">
<span>
<span style={{color: theme.foreground_orange1}}> {count} </span>
个视频
</span>
{categoryObj.category == 'all' && <CRoundSelect {...props}
width={'90px'}
items={_items}
onSortChange={onSortChange}
sortKey={sortKey }
></CRoundSelect>}
</div>
{categoryObj.category == 'all' ?
<div className="itemWrap">
{
videoes == undefined ? '' :
videoes.length ?
videoes.map((item, index) => {
return (<VideoInReviewItem
{...props}
{...item}
key={item.id}
onEditVideo={onEditVideo}
onMaskClick={onMaskClick}
getCopyText={getCopyText}
>
</VideoInReviewItem>)
})
: <NoneData style={{width: '100%'}}></NoneData>
}
</div>
:
<div className="itemWrap">
{
reviewVideoes == undefined ? '' :
reviewVideoes.length ?
reviewVideoes.map((item, index) => {
return (<VideoInReviewItem
{...props}
{...item}
key={item.id}
isReview={true}
>
</VideoInReviewItem>)
})
: <NoneData style={{width: '100%'}}></NoneData>
}
</div>
}
{
categoryObj.category == 'all' && count > PAGE_SIZE &&
<div className="mt30 mb50 edu-txt-center">
<Pagination showQuickJumper total={count} pageSize={PAGE_SIZE}
{...pageObj}
/>
</div>
}
</div>
)
}
export default InfoVideo
/**
<video src="http://outin-396971199eed11e991a100163e1c7426.oss-cn-shanghai.aliyuncs.com/sv/52943d8b-16c8dc2a8ca/52943d8b-16c8dc2a8ca.mp4" controls="true" controlslist="nodownload" width="400">
您的浏览器不支持 video 标签
</video>
*/

@ -0,0 +1,77 @@
import React, { useState, useEffect, useContext, memo } from 'react';
import { Progress, Input, Tooltip } from 'antd'
import { getUrl2, isDev, CBreadcrumb, ActionBtn, ThemeContext } from 'educoder'
import axios from 'axios'
import moment from 'moment'
import playIcon from './images/play.png'
import ClipboardJS from 'clipboard'
/**
cover_url: "http://video.educoder.net/f6ba49c3944b43ee98736898e31b7d88/snapshots/12da3f7df07c499b8f0fc6dc410094e9-00005.jpg"
created_at: "2019-08-12 13:48:26"
file_url: "http://video.educoder.net/sv/4c7eb4-16c845ee09c/4c7eb4-16c845ee09c.mp4"
id: 1
published_at: "2019-08-12 15:38:00"
title: "测试标题"
updated_at: "2019-08-12 17:17:09"
*/
let _clipboard = null;
function VideoInReviewItem (props) {
const theme = useContext(ThemeContext);
const { history, file_url, cover_url, title, created_at, published_at, isReview, id
, onEditVideo, onMaskClick, getCopyText, showNotification } = props;
useEffect(()=> {
if (!isReview) {
_clipboard = new ClipboardJS(`.copybtn_item_${id}`);
_clipboard.on('success', (e) => {
showNotification('复制成功')
});
}
return () => {
_clipboard && _clipboard.destroy();
}
}, [])
const username = props.match.params.username
function toList() {
history.push(`/users/${username}/videoes`)
}
function toUpload() {
history.push(`/users/${username}/videoes/upload`)
}
return (
<div className={`${isReview ? 'videoInReviewItem' : 'nItem'} videoItem`}>
<img className="cover" src={cover_url || "http://video.educoder.net/e7d18970482a46d2a6f0e951b504256c/snapshots/491e113950d74f1dab276097dae287dd-00005.jpg"}
></img>
{!isReview && <div className="mask" onClick={() => onMaskClick(props)}>
</div>}
{!isReview &&
<div className="playWrap" onClick={() => onMaskClick(props)}>
<img className="play" src={playIcon}></img>
</div>
}
<div className="square-main">
<div className="title overflowHidden1"
title={title && title.length > 20 ? title : ''}
>{title}</div>
<div className="df buttonRow">
{/* 2019-09-01 10:00:22 */}
<span className="time">{moment(published_at || created_at).format('YYYY-MM-DD HH:mm:ss')}</span>
{ isReview != true && <div>
<Tooltip title="编辑" placement="bottom">
<i className="icon-bianji1 iconfont" onClick={() => onEditVideo(props)}
style={{ marginTop: '1px', display: 'inline-block'}}
></i>
</Tooltip>
<Tooltip title="复制视频地址" placement="bottom">
<i className={`icon-fuzhi iconfont copybtn_item_${id}`} data-clipboard-text={getCopyText(file_url, cover_url)}></i>
</Tooltip>
</div> }
</div>
</div>
</div>
)
}
export default VideoInReviewItem

@ -0,0 +1,73 @@
import React, { useState, useEffect, useContext, memo } from 'react';
import { Progress, Input } from 'antd'
import { getUrl2, isDev, CBreadcrumb, ActionBtn, ThemeContext } from 'educoder'
import axios from 'axios'
import okIcon from './images/ok_border.png'
function VideoUpload (props) {
const theme = useContext(ThemeContext);
const { history } = props;
const username = props.match.params.username
function toList() {
history.push(`/users/${username}/videoes`)
}
function toUpload() {
history.push(`/users/${username}/videoes/upload`)
}
return (
<div className={`videoPublishSuccess educontent`}>
<CBreadcrumb
className="mb26 mt16"
separator=" > "
items={[
{ to: `/users/${username}/videoes`, name: '视频'},
{ name: '上传'}
]}
></CBreadcrumb>
<style>{`
.videoPublishSuccess .section {
background: #fff;
padding: 72px 20px;
position: relative;
text-align: center;
color: ${theme.foreground_tip};
}
.videoPublishSuccess img.ok {
width: 64px;
margin: 16px;
margin-top: 0px;
}
.videoPublishSuccess .tip {
margin-top: 10px;
margin-bottom: 12px;
}
.videoPublishSuccess .toListBtn {
margin-right: 10px;
}
.videoPublishSuccess .toUploadBtn {
width: 112px;
}
`}</style>
<div className="section">
<div>
<img className="ok" src={okIcon}></img>
{/* <i className="icon-wanchenggouxuan iconfont font-36" style={{color: theme.foreground_select}}></i> */}
</div>
<div className="font-16" style={{ 'line-height': '16px'}}>恭喜</div>
<div className="font-16">提交成功</div>
<div className="tip">平台正在审核您的申请审核结果将以平台消息的形式通知您</div>
<div>
<ActionBtn className="toListBtn" onClick={toList}>查看已上传视频</ActionBtn>
<ActionBtn className="toUploadBtn" onClick={toUpload}>继续上传</ActionBtn>
</div>
</div>
</div>
)
}
export default VideoUpload

@ -0,0 +1,64 @@
import update from 'immutability-helper'
function find(state, action) {
let _index = -1
state.videoes.some((item, index) => {
// 同文件不同名字 fileHash也是一样的
if (item.loaded != 100 && (action.uploadInfo.fileHash == item.fileHash && action.uploadInfo.file.name == item.name)) {
_index = index
return true;
}
})
return _index;
}
export function reducer(state, action) {
switch (action.type) {
case 'addVideo':
const uploadInfo = action.uploadInfo
return {videoes: [...state.videoes, {
name: uploadInfo.file.name,
size: uploadInfo.file.size,
type: uploadInfo.file.type,
fileHash: uploadInfo.fileHash, // "ba1bbc53fdecd9eaaae479fbd9518442"
state: uploadInfo.state, // "Uploading" "Ready" "Success"
videoId: uploadInfo.videoId, // "719b82c875c34ac39f94feb145d25ad2"
loaded: 0,
title: ''
}]};
case 'removeVideo':
return {
videoes: update(state.videoes, {$splice: [[action.index, 1]]})
}
case 'updateProgress':
let _index = find(state, action)
let newVideoes = state.videoes
// 删除先执行
if (_index != -1) {
newVideoes = update(state.videoes, {[_index]: {
loaded: {$set: action.progressPercent},
videoId: {$set: action.uploadInfo.videoId},
// addFileSuccess的时候没有fileHash
fileHash: {$set: action.uploadInfo.fileHash}
}})
}
return {videoes: newVideoes};
case 'updateTitle':
let _upadteIndex = action.index
let newVideoes2 = state.videoes
if (_upadteIndex != -1) {
newVideoes2 = update(state.videoes, {[_upadteIndex]: {
title: {$set: action.title},
}})
}
return {videoes: newVideoes2};
default:
throw new Error();
}
}
export const initialState = {videoes: []};

@ -0,0 +1,52 @@
import React, { useState, useEffect, memo } from 'react';
import { Progress, Input } from 'antd'
import { getUrl2, isDev, CBreadcrumb, ActionBtn } from 'educoder'
import axios from 'axios'
const MAX_LENGTH = 30
/**
name: file.name,
size: file.size,
type: file.type,
fileHash: uploadInfo.fileHash, // "ba1bbc53fdecd9eaaae479fbd9518442"
state: uploadInfo.state, // "Uploading"
videoId: uploadInfo.videoId, // "719b82c875c34ac39f94feb145d25ad2"
loaded: 0
*/
function VideoUpload (props) {
const { className, index, name, loaded, state, cancelUpload, onTitleChange, title } = props;
// const [title, setTitle] = useState('')
const username = props.match.params.username
function titleChange (e) {
onTitleChange(e.target.value, index)
}
return (
<div className={`videoUpload ${className}`}>
<div className="filename">{index+1}. {name}</div>
<div className="progress df">
<Progress percent={loaded}
status={loaded == '100' ? "" : 'active'}
/>
<div className="cancelUpload">
<ActionBtn className="" onClick={() => cancelUpload(index, loaded == '100' )}>{loaded == '100' ? "删除" : "取消上传"}</ActionBtn>
</div>
</div>
<div>
<span className="titleLabel">标题</span>
<Input placeholder={`标题支持最多${MAX_LENGTH}个字符`} onInput={titleChange} maxLength={MAX_LENGTH} suffix={
<span className="color-grey-6 font-13">{String(title.length)}/{MAX_LENGTH}</span>
}
className="titleInput"
></Input>
</div>
</div>
)
}
export default VideoUpload

@ -0,0 +1,404 @@
import React, { useState, useEffect, useReducer, useContext, memo } from 'react';
import { getUrl2, isDev, CBreadcrumb, ActionBtn, ThemeContext } from 'educoder'
import axios from 'axios'
import VideoUpload from './VideoUpload'
import { Button } from 'antd'
import { getUploader } from './AliyunUploaderManager'
import { reducer, initialState } from './VideoReducer'
import { deleteVideoInCloud } from './VideoUtil'
import uploadIcon from './images/upload.png'
let uploader
const files = []
const MAX_FILE_COUNT = 3
const MAX_FILE_SIZE = 200
let noUploads = true
function VideoUploadList (props) {
const [videoes, setVideoes] = useState([]);
const [state, dispatch] = useReducer(reducer, initialState);
const theme = useContext(ThemeContext)
useEffect(() => {
window.addEventListener("beforeunload", beforeunload);
return () => {
window.removeEventListener("beforeunload", beforeunload);
}
}, [])
// TODO 闭包!
noUploads = (!state.videoes || state.videoes.length == 0);
function beforeunload(e) {
if (noUploads) {
return true;
}
var confirmationMessage = "确认要离开当前页面,当前数据不可恢复";
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Webkit, Safari, Chrome etc.
}
const _beforeunload = beforeunload // .bind(this, noUploads, state)
const username = props.match.params.username
const { showNotification, history } = props;
const uploaderOptions = {
}
function onUploadChange (e) {
var file = e.target.files[0]
if (!file) {
// alert("请先选择需要上传的文件!")
return
}
if (file.size > 200 * 1024 * 1024) {
// 超过200m TODO
clearInput()
showNotification(`视频大小超过${MAX_FILE_SIZE}M`)
return;
}
let gotTheSameFileName = false;
state.videoes.some((item) => {
if (item.name == file.name) {
gotTheSameFileName = true;
return true;
}
})
if (gotTheSameFileName) {
clearInput()
showNotification(`你不能上传同一个视频文件名称,请重新选择。`)
return;
}
var Title = file.name
var userData = '{"Vod":{}}'
if (!uploader) {
getUploader(username,
// Object.assign(uploaderOptions,
{
addFileSuccess: (uploadInfo) => {
const file = uploadInfo.file
console.log('addFileSuccess', uploadInfo)
// const newVideoes = [...videoes, {
// name: file.name,
// size: file.size,
// type: file.type,
// fileHash: uploadInfo.fileHash, // "ba1bbc53fdecd9eaaae479fbd9518442"
// state: uploadInfo.state, // "Uploading" "Ready"
// videoId: uploadInfo.videoId, // "719b82c875c34ac39f94feb145d25ad2"
// loaded: 0
// }]
// setVideoes(newVideoes)
// files.push(file)
dispatch({type: 'addVideo', uploadInfo})
},
onUploadProgress: (uploadInfo, totalSize, progress) => {
var progressPercent = Math.ceil(progress * 100)
let _index = -1;
videoes.some((item, index) => {
// addFileSuccess的时候没有fileHash
// if (uploadInfo.fileHash == item.fileHash) {
if (uploadInfo.file.name == item.name) {
_index = index
return true;
}
})
// TODO 这里不用reducer会出现state被重置的问题
// if (_index == -1) {
// const newVideoes = [...videoes, {
// name: file.name,
// size: file.size,
// type: file.type,
// fileHash: uploadInfo.fileHash, // "ba1bbc53fdecd9eaaae479fbd9518442"
// state: uploadInfo.state, // "Uploading" "Ready"
// videoId: uploadInfo.videoId, // "719b82c875c34ac39f94feb145d25ad2"
// loaded: progressPercent
// }]
// setVideoes(newVideoes)
// return;
// }
// // exercise_questions : update(prevState.exercise_questions, {[index]: { isNew: {$set: false}}})
// setVideoes(update(videoes, {[_index]: { loaded: {$set: progressPercent}}}))
dispatch({type: 'updateProgress', uploadInfo, progressPercent})
},
onUploadEnd: (uploadInfo) => {
console.log('onUploadEnd', uploadInfo)
},
onUploadSucceed: (uploadInfo) => {
console.log('onUploadSucceed', uploadInfo)
},
onUploadError: (uploadInfo) => {
},
// 可能需要等lib加载完毕才能执行
gotUploader: (_uploader) => {
// 首先调用 uploader.addFile(event.target.files[i], null, null, null, userData)
console.log(_uploader)
let result = _uploader.addFile(file, null, null, null, userData)
uploader = _uploader;
window.uploader = uploader;
}
}
// )
)
} else {
let result = uploader.addFile(file, null, null, null, userData)
}
}
function clearInput() {
const _input = document.getElementById('fileUpload')
_input.value = ''
}
// uploader.deleteFile(index);
function cancelUpload(index, isSuccess) {
// TODO 确定取消?
uploader.deleteFile(index)
if (isSuccess) {
deleteVideoInCloud(username, state.videoes[index].videoId)
}
// else {
// uploader.cancelFile(index)
// }
clearInput()
dispatch({type: 'removeVideo', index})
// setVideoes([...videoes.splice(index, 1)])
}
function onPublish() {
if (state.videoes.length == 0) {
showNotification('请先上传视频')
return;
}
const publishUrl = `/users/${username}/videos/batch_publish.json`
axios.post(publishUrl, {
videos: state.videoes.map(item => {
return {
video_id: item.videoId,
// todo
title: item.title
}
})
}).then((response) => {
// to success page
if (response.data.status == 0) {
history.push(`/users/${username}/videoes/success`)
}
}).catch((error) => {
console.log(error)
})
}
function onTitleChange(title, index) {
dispatch({type: 'updateTitle', title, index})
}
// login
const protocolLine = <div>上传视频即表示您已同意<span style={{color: theme.foreground_select}}>上传内容协议</span></div>
return (
<div className="educontent videoUploadList" style={{ marginBottom: '200px' }}>
<style>{`
.videoUploadList .section {
background: #fff;
padding: 16px 20px;
padding-top: 0px;
position: relative;
padding-bottom: 36px;
}
.videoUploadList .cBreadcrumb {
margin-top: 16px;
}
.videoUploadList .uploadTip {
line-height: 18px;
margin-bottom: 16px;
}
.videoUploadList .title {
margin-bottom: 4px;
}
.videoUploadList .title .head {
display: inline-block;
margin-right: 8px;
}
.videoUploadList .title .titleDescription {
color: #555;
}
.videoUploadList .section .description {
padding-top: 10px;
margin-top: 20px;
margin-bottom: 30px;
color: #777;
}
.videoUploadList .section .description.noUploads {
text-align: 'center';
}
.videoUploadList .publishBtn {
padding: 0 16px
}
.videoUploadList .publishRow .publishBtn {
padding: 6px 24px;
height: auto;
margin-bottom: 24px;
}
.videoUploadList .addVideoBtn {
position: absolute;
right: 30px;
}
.videoUploadList .publishRow {
text-align: center;
margin-top: 42px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.noUploads {
text-align: center;
}
/* item */
.videoUploadList .cancelUpload {
flex: 0 0 200px;
}
.videoUploadList .titleInput {
width: 480px;
margin-top: 16px;
}
.videoUploadList .videoUpload {
padding: 26px 0;
border-bottom: 1px dashed #DCDCDC;
}
.videoUploadList .videoUpload:last-child {
border-bottom: none;
}
`}</style>
<CBreadcrumb
className="mb26"
separator=" > "
items={[
{ to: `/users/${username}/videoes`, name: '视频'},
{ name: '上传'}
]}
></CBreadcrumb>
<div className="title">
<h2 className="head">上传视频</h2>
{/* <span className="titleDescription">单次最多支持{MAX_FILE_COUNT}个视频文件上传,不支持断点续传,单个视频文件最大{MAX_FILE_SIZE}M</span> */}
</div>
<div className="section">
{/* noUploads */}
{noUploads && <div className="noUploads" style={{paddingTop: '72px'}}>
<img src={uploadIcon} onClick={() => document.getElementById('fileUpload').click()}></img>
<div style={{
color: '#000000',
fontSize: '18px',
fontWeight: 'bold',
marginBottom: '20px'
}}>选择您要上传的视频</div>
{protocolLine}
</div>}
<div>
{state.videoes.map((item, vIndex) => {
return (
<VideoUpload {...props} {...item} className=""
cancelUpload={cancelUpload}
onTitleChange={onTitleChange}
key={vIndex}
index={vIndex}
></VideoUpload>
)
})}
</div>
{state.videoes && state.videoes.length === MAX_FILE_COUNT &&
<div className="uploadTip">
<i className="iconfont icon-tishi" style={{color: '#FF6F6F', verticalAlign: 'text-bottom'}}></i>
<span>单次最多支持3个视频文件上传</span>
</div>}
{(!noUploads && state.videoes.length < MAX_FILE_COUNT) && <ActionBtn className="publishBtn" onClick={() => document.getElementById('fileUpload').click()}
>继续添加</ActionBtn>}
<div className={`description ${noUploads ? 'noUploads' : ''}`}>
<div className="">视频大小不支持断点续传单个视频文件最大200M单次最多支持3个视频文件上传 </div>
<div className="">视频规格aviflvf4vm4vmovmp4rmvbswfwebm </div>
<div className="">温馨提示请勿上传违法视频平台将为每一个视频分配一个地址您可以通过引用改地址将视频使用在开发社区等模块</div>
</div>
{!noUploads && <React.Fragment>
{/* {(state.videoes.length < MAX_FILE_COUNT) && <Button type="primary" icon="plus-square"
onClick={() => { document.getElementById('fileUpload').click()}}
className="fr addVideoBtn"
>
添加更多视频
</Button>} */}
<div style={{}} className="publishRow">
<ActionBtn className="publishBtn" onClick={() => onPublish()}
>立即发布</ActionBtn>
{protocolLine}
</div>
</React.Fragment>}
</div>
<input type="file" id="fileUpload" style={{display: 'none'}} onChange={onUploadChange}
accept="video/*"
></input>
</div>
)
}
export default VideoUploadList
/**
bucket: "outin-396971199eed11e991a100163e1c7426"
checkpoint: {file: File, name: "sv/2d0fd065-16c7a62fcc5/2d0fd065-16c7a62fcc5.mp4", fileSize: 491511493, partSize: 1048576, uploadId: "A8DB0663F44C44F58F3F7F45892ED08B", }
endpoint: "https://oss-cn-shanghai.aliyuncs.com"
file: File {name: "[阳光电影-www.ygdy8.com]金秘书为何这样-02.mp4", lastModified: 1532441562000, lastModifiedDate: Tue Jul 24 2018 22:12:42 GMT+0800 (China Standard Time), webkitRelativePath: "", size: 491511493, }
fileHash: "ba1bbc53fdecd9eaaae479fbd9518442"
isImage: false
loaded: 0.5927505330490405
object: "sv/2d0fd065-16c7a62fcc5/2d0fd065-16c7a62fcc5.mp4"
region: "cn-shanghai"
retry: false
ri: "F0FDC11A-9A92-4A50-882A-423C3EA499F3"
state: "Uploading"
userData: "eyJWb2QiOnt9fQ=="
videoId: "719b82c875c34ac39f94feb145d25ad2"
file
lastModified: 1532441562000
lastModifiedDate: Tue Jul 24 2018 22:12:42 GMT+0800 (China Standard Time) {}
name: "[阳光电影-www.ygdy8.com]金秘书为何这样-02.mp4"
size: 491511493
type: "video/mp4"
webkitRelativePath: ""
*/

@ -0,0 +1,13 @@
import axios from 'axios'
export function deleteVideoInCloud(login, video_id) {
const url = `/users/${login}/videos/cancel.json`
axios.post(url, {
video_id
}).then((response) => {
}).catch((error) => {
console.log(error)
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

@ -183,14 +183,14 @@ class SearchPage extends Component{
return (
<a key={key}
href={
item.type==="shixun"?`/shixuns/${item.identifier}/challenges`:item.type==="course"?`/courses/${item.id}/students`:item.type==="subject"?`/paths/${item.id}`:item.type==="memo"?`/forums/${item.id}`:""
item.type==="shixun"?`/shixuns/${item.identifier}/challenges`:item.type==="course"?`${item.first_category_url}`:item.type==="subject"?`/paths/${item.id}`:item.type==="memo"?`/forums/${item.id}`:""
}
target="_blank"
>
<div className="project-package-item">
<div className={"font-16 color-dark fl "} >
<div className={"font-16 color-dark fl "} style={{width:"100%"}} >
{/*标题*/}
<span className={"markdown-body fonttext"}
dangerouslySetInnerHTML={{__html:item.title}}/>

Loading…
Cancel
Save