diff --git a/app/controllers/watch_video_histories_controller.rb b/app/controllers/watch_video_histories_controller.rb new file mode 100644 index 000000000..15ee62113 --- /dev/null +++ b/app/controllers/watch_video_histories_controller.rb @@ -0,0 +1,10 @@ +class WatchVideoHistoriesController < ApplicationController + before_action :require_login + + def create + watch_log = CreateWatchVideoService.new(current_user, request, params).call + render_ok(log_id: watch_log&.id) + rescue CreateWatchVideoService::Error => ex + render_error(ex.message) + end +end diff --git a/app/models/course_video.rb b/app/models/course_video.rb index 2cfa151ce..e192cf7f8 100644 --- a/app/models/course_video.rb +++ b/app/models/course_video.rb @@ -5,4 +5,6 @@ class CourseVideo < ApplicationRecord validates :title, length: { maximum: 60, too_long: "不能超过60个字符" }, allow_blank: true validates :link, format: { with: CustomRegexp::URL, message: "必须为网址超链接" }, allow_blank: true + + has_many :watch_course_videos end diff --git a/app/models/user.rb b/app/models/user.rb index b0bd191d2..fb4cc50da 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -162,6 +162,10 @@ class User < ApplicationRecord has_many :teacher_group_records, dependent: :destroy + # 视频观看记录 + has_many :watch_video_histories, dependent: :destroy + has_many :watch_course_video, dependent: :destroy + # Groups and active users scope :active, lambda { where(status: STATUS_ACTIVE) } diff --git a/app/models/watch_course_video.rb b/app/models/watch_course_video.rb new file mode 100644 index 000000000..cf2d67028 --- /dev/null +++ b/app/models/watch_course_video.rb @@ -0,0 +1,9 @@ +class WatchCourseVideo < ApplicationRecord + belongs_to :course_video + belongs_to :user + + has_many :watch_video_histories + + + validates :course_video_id, uniqueness: {scope: :user_id} +end diff --git a/app/models/watch_video_history.rb b/app/models/watch_video_history.rb new file mode 100644 index 000000000..36f7be0f3 --- /dev/null +++ b/app/models/watch_video_history.rb @@ -0,0 +1,7 @@ +class WatchVideoHistory < ApplicationRecord + belongs_to :user + belongs_to :video + belongs_to :watch_course_video, optional: true + + validates :duration, numericality: { greater_than_or_equal_to: 0 } +end diff --git a/app/services/create_watch_video_service.rb b/app/services/create_watch_video_service.rb new file mode 100644 index 000000000..8a9bb7f08 --- /dev/null +++ b/app/services/create_watch_video_service.rb @@ -0,0 +1,76 @@ +class CreateWatchVideoService < ApplicationService + attr_reader :user, :params, :request + + def initialize(user, request, params) + @user = user + @request = request + @params = params + end + + def call + ActiveRecord::Base.transaction do + current_time = Time.now + if params[:log_id].present? + if params[:total_duration].to_f < params[:watch_duration].to_f || params[:watch_duration].to_f < 0 + raise Error, '观看时长错误' + end + # 更新观看时长 + watch_video_history = user.watch_video_histories.find(params[:log_id]) + + if watch_video_history.present? && watch_video_history.watch_duration <= params[:watch_duration].to_f && params[:total_duration].to_f > watch_video_history.total_duration + # 如果观看总时长没变,说明视频没有播放,无需再去记录 + + watch_video_history.end_at = current_time + watch_video_history.total_duration = params[:total_duration] + watch_video_history.watch_duration = params[:watch_duration].to_f > watch_video_history.duration ? watch_video_history.duration : params[:watch_duration] + watch_video_history.is_finished = (watch_video_history.duration <= params[:watch_duration].to_f) + watch_video_history.save! + + watch_course_video = watch_video_history.watch_course_video + + if watch_course_video.present? && !watch_course_video.is_finished && watch_course_video.watch_duration < params[:watch_duration].to_f + # 更新课程视频的时长及是否看完状态 + watch_course_video.watch_duration = params[:watch_duration] + watch_course_video.is_finished = (watch_course_video.duration <= params[:watch_duration].to_f) + watch_course_video.end_at = current_time + watch_course_video.save! + end + end + else + # 开始播放时记录一次 + if params[:course_video_id].present? + # 课堂视频 + course_video = CourseVideo.find(params[:course_video_id]) + watch_course_video = WatchCourseVideo.find_or_initialize_by(course_video_id: course_video.id, user_id: user.id) do |d| + d.start_at = current_time + d.duration = params[:duration] + end + + watch_video_history = build_video_log(current_time, course_video.video_id, watch_course_video.id) + watch_video_history.save! + + watch_course_video.save! unless watch_course_video.persisted? + else + # 非课堂视频 + video = Video.find_by(params[:video_id]) + watch_video_history = build_video_log(current_time, video.id) + watch_video_history.save! + end + end + watch_video_history + end + end + + + def build_video_log(current_time, video_id, watch_course_video_id=nil) + WatchVideoHistory.new( + user_id: user.id, + watch_course_video_id: watch_course_video_id, + start_at: current_time, + duration: params[:duration], + video_id: video_id, + device: params[:device], + ip: request.remote_ip + ) + end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index db9b74bd2..bcab93203 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,6 +29,8 @@ Rails.application.routes.draw do put 'commons/unhidden', to: 'commons#unhidden' delete 'commons/delete', to: 'commons#delete' + resources :watch_video_histories, only: [:create] + resources :jupyters do collection do get :save_with_tpi diff --git a/db/migrate/20200305072442_create_watch_course_videos.rb b/db/migrate/20200305072442_create_watch_course_videos.rb new file mode 100644 index 000000000..5695344fc --- /dev/null +++ b/db/migrate/20200305072442_create_watch_course_videos.rb @@ -0,0 +1,15 @@ +class CreateWatchCourseVideos < ActiveRecord::Migration[5.2] + def change + create_table :watch_course_videos do |t| + t.references :course_video, index: true + t.references :user, index: true + t.boolean :is_finished, default: false + t.float :duration, default: 0 + t.float :watch_duration, default: 0 + t.datetime :start_at + t.datetime :end_at + + t.timestamps + end + end +end diff --git a/db/migrate/20200305074638_create_watch_video_histories.rb b/db/migrate/20200305074638_create_watch_video_histories.rb new file mode 100644 index 000000000..ad9645a6a --- /dev/null +++ b/db/migrate/20200305074638_create_watch_video_histories.rb @@ -0,0 +1,18 @@ +class CreateWatchVideoHistories < ActiveRecord::Migration[5.2] + def change + create_table :watch_video_histories do |t| + t.references :watch_course_video, index: true + t.references :user, index: true + t.references :video, index: true + t.boolean :is_finished, default: false + t.float :duration, default: 0 + t.float :watch_duration, default: 0 + t.datetime :start_at + t.datetime :end_at + t.string :device + t.string :ip + + t.timestamps + end + end +end diff --git a/db/migrate/20200312014100_add_total_duration_to_watch_video_histories.rb b/db/migrate/20200312014100_add_total_duration_to_watch_video_histories.rb new file mode 100644 index 000000000..e595384e0 --- /dev/null +++ b/db/migrate/20200312014100_add_total_duration_to_watch_video_histories.rb @@ -0,0 +1,5 @@ +class AddTotalDurationToWatchVideoHistories < ActiveRecord::Migration[5.2] + def change + add_column :watch_video_histories, :total_duration, :float, default: 0 + end +end diff --git a/public/react/src/modules/paths/ShixunPathSearch.js b/public/react/src/modules/paths/ShixunPathSearch.js index be1d3a33c..844215974 100644 --- a/public/react/src/modules/paths/ShixunPathSearch.js +++ b/public/react/src/modules/paths/ShixunPathSearch.js @@ -8,7 +8,7 @@ import Pagination from '@icedesign/base/lib/pagination'; import '@icedesign/base/lib/pagination/style.js'; import './ShixunPaths.css'; import KeywordList from '../tpm/shixuns/shixun-keyword-list'; -import btnUrl from '../tpm/shixuns/btn-new.png'; +import btnUrl from './btn-new.png'; class ShixunPathSearch extends Component { constructor(props) { @@ -252,7 +252,7 @@ class ShixunPathSearch extends Component { - + { diff --git a/public/react/src/modules/paths/btn-new.png b/public/react/src/modules/paths/btn-new.png new file mode 100644 index 000000000..b900fa1f4 Binary files /dev/null and b/public/react/src/modules/paths/btn-new.png differ diff --git a/public/react/src/modules/tpm/TPMIndex.css b/public/react/src/modules/tpm/TPMIndex.css index 2ec090e7d..325b0fc54 100644 --- a/public/react/src/modules/tpm/TPMIndex.css +++ b/public/react/src/modules/tpm/TPMIndex.css @@ -4,48 +4,58 @@ } */ body { overflow: auto !important; - font-family: "Microsoft YaHei"; + font-family: "Microsoft YaHei"; } #root { - /* ie兼容性 */ - position: relative; - min-height: 100%; + /* ie兼容性 */ + position: relative; + min-height: 100%; } + body>.-task-title { - opacity: 1 !important; + opacity: 1 !important; } + /*�����Ŵ󾵵�����·Ŵ󾵵�λ��*/ #root .search-all { - width: 219px; + width: 219px; } /*Header START*/ .newHeader .logoimg { - margin-top: 16px; - float: left; - width: 97px; + margin-top: 16px; + float: left; + width: 97px; } + .head-right i { - font-size: 20px; - float: none !important; + font-size: 20px; + float: none !important; } -.headIcon, #header_keyword_search { - padding-top: 13px !important; + +.headIcon, +#header_keyword_search { + padding-top: 13px !important; } + .search-icon { - height: 30px !important; + height: 30px !important; } + .search-icon i { - font-size: 20px; + font-size: 20px; } + #header_keyword_search i { - color: #4cacff; + color: #4cacff; } -.ant-select-selection--multiple{ - padding-bottom: 0px!important; - padding-top:3px; + +.ant-select-selection--multiple { + padding-bottom: 0px !important; + padding-top: 3px; } + /* 先注释掉下面2个样式,这样写影响范围太广了,并不是所有的select都需要40px高 */ /* .ant-select-selection--single{ height:40px!important; @@ -53,247 +63,323 @@ body>.-task-title { .ant-select-selection__rendered{ line-height: 40px!important; } */ -.ant-select-selection--multiple .ant-select-selection__rendered>ul>li, .ant-select-selection--multiple>ul>li{ - height: 25px!important; - line-height: 23px!important; - margin-bottom:3px; - margin-top:0px; +.ant-select-selection--multiple .ant-select-selection__rendered>ul>li, +.ant-select-selection--multiple>ul>li { + height: 25px !important; + line-height: 23px !important; + margin-bottom: 3px; + margin-top: 0px; } + /*Main START*/ -.newContainer{ - background: #fafafa!important; +.newContainer { + background: #fafafa !important; } -.ant-modal-title{ - font-size: 16px; - font-weight: bold !important; - color: #333; +.ant-modal-title { + font-size: 16px; + font-weight: bold !important; + color: #333; } -.ant-modal-title{ - text-align: center; +.ant-modal-title { + text-align: center; } + /*.ant-modal{*/ - /*top:10rem !important;*/ +/*top:10rem !important;*/ /*}*/ @-moz-document url-prefix() { - .ant-radio-inner { - width: 17px !important; - height: 17px !important; - } + .ant-radio-inner { + width: 17px !important; + height: 17px !important; + } } + /* IE只能用padding,不能用上下居中 */ -.shixunDetail_top{ - display: block!important; - padding-top: 48px; -} -.totalScore{ - display: block!important; - padding-top: 40px; +.shixunDetail_top { + display: block !important; + padding-top: 48px; } -.head-nav ul#header-nav li{ - /*font-weight: 600;*/ + +.totalScore { + display: block !important; + padding-top: 40px; } + /*.newFooter{*/ - /*position: fixed !important;*/ +/*position: fixed !important;*/ /*}*/ -.edu-menu-panel .edu-menu-listnew:hover .careersiconfont{ - color: #000 !important; +.edu-menu-panel .edu-menu-listnew:hover .careersiconfont { + color: #000 !important; } .newHeader { - background: #24292D !important; - height: 60px !important; + background: #24292D !important; + height: 60px !important; } /*-------------------个人主页:右侧提示区域--------------------------*/ -.-task-sidebar{position:fixed;width:40px;height:180px;right:0;bottom:80px !important;z-index: 10;} -.-task-sidebar>div{height: 40px;line-height: 40px;box-sizing: border-box;width:40px;background:#4CACFF;color:#fff;font-size:20px;text-align:center;margin-bottom:5px;border-radius: 4px;} -.-task-sidebar>div i{ color:#fff;} -.-task-sidebar>div i:hover{color: #fff!important;} -.gotop{background-color: rgba(208,207,207,0.5)!important;padding: 0px!important;} -.-task-desc{background:#494949;width:90px;line-height: 36px;text-align: center; - position: absolute;color: #fff;font-size: 13px;z-index: 999999;opacity: 0;} -.-task-desc div{position: absolute;top:10px;right: -7px;height: 13px;} -.-task-desc div img{float: left} -.-task-sidebar .scan_ewm{ - position: absolute !important; - right: 45px !important; - bottom: 0px !important; - background-color: #494949 !important; - -webkit-box-sizing: border-box !important; - box-sizing: border-box !important; - font-size: 14px !important; - line-height: 16px !important; - display: none; - height: 213px !important; -} -.trangle_right{position: absolute;right: -5px;bottom: 15px;width: 0;height: 0px;border-top: 6px solid transparent;border-left: 5px solid #494949;border-bottom: 6px solid transparent} - -.HeaderSearch{ - margin-top: 18px; - margin-right: 20px; -} -.HeaderSearch .ant-input-search .ant-input{ - /*height:30px;*/ - background: #373e3f !important; - border: 1px solid #373e3f !important; - -} -.ant-input-search .ant-input-affix-wrapper{ - border:transparent; +.-task-sidebar { + position: fixed; + width: 40px; + height: 180px; + right: 0; + bottom: 20px !important; + z-index: 10; +} + +.-task-sidebar>div { + height: 40px; + line-height: 40px; + box-sizing: border-box; + width: 40px; + background: #4CACFF; + color: #fff; + font-size: 20px; + text-align: center; + margin-bottom: 5px; + border-radius: 4px; +} + +.-task-sidebar>div i { + color: #fff; +} + +.-task-sidebar>div i:hover { + color: #fff !important; +} + +.gotop { + background-color: rgba(208, 207, 207, 0.5) !important; + padding: 0px !important; +} + +.-task-desc { + background: #494949; + width: 90px; + line-height: 36px; + text-align: center; + position: absolute; + color: #fff; + font-size: 13px; + z-index: 999999; + opacity: 0; +} + +.-task-desc div { + position: absolute; + top: 10px; + right: -7px; + height: 13px; } + +.-task-desc div img { + float: left +} + +.-task-sidebar .scan_ewm { + position: absolute !important; + right: 45px !important; + bottom: 0px !important; + background-color: #494949 !important; + -webkit-box-sizing: border-box !important; + box-sizing: border-box !important; + font-size: 14px !important; + line-height: 16px !important; + display: none; + height: 213px !important; +} + +.trangle_right { + position: absolute; + right: -5px; + bottom: 15px; + width: 0; + height: 0px; + border-top: 6px solid transparent; + border-left: 5px solid #494949; + border-bottom: 6px solid transparent +} + +.HeaderSearch { + margin-top: 18px; + margin-right: 20px; +} + +.HeaderSearch .ant-input-search .ant-input { + /*height:30px;*/ + background: #373e3f !important; + border: 1px solid #373e3f !important; + +} + +.ant-input-search .ant-input-affix-wrapper { + border: transparent; +} + .ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled) { - /* 比较奇怪的需求,先注释掉了,如果需要启用,麻烦增加class限制,别影响别的地方的使用 */ - /* border-color: transparent; */ + /* 比较奇怪的需求,先注释掉了,如果需要启用,麻烦增加class限制,别影响别的地方的使用 */ + /* border-color: transparent; */ } .ant-input:focus { - /*border-color: transparent;*/ - border-right-width: 1px !important; - outline: 0; - -webkit-box-shadow: 0 0 0 2px transparent; - box-shadow: 0 0 0 2px transparent; - border: 1px solid #d9d9d9; + /*border-color: transparent;*/ + border-right-width: 1px !important; + outline: 0; + -webkit-box-shadow: 0 0 0 2px transparent; + box-shadow: 0 0 0 2px transparent; + border: 1px solid #d9d9d9; } -.HeaderSearch .ant-input-search .ant-input::-webkit-input-placeholder{ - color: #999; - font-size: 14px; +.HeaderSearch .ant-input-search .ant-input::-webkit-input-placeholder { + color: #999; + font-size: 14px; } .HeaderSearch .ant-input-search .ant-input:-moz-placeholder { - color: #999; - font-size: 14px; + color: #999; + font-size: 14px; } -.HeaderSearch .ant-input-search .ant-input::-moz-placeholder{ - color: #999; - font-size: 14px; +.HeaderSearch .ant-input-search .ant-input::-moz-placeholder { + color: #999; + font-size: 14px; } -.HeaderSearch .ant-input-search .ant-input:-ms-input-placeholder{ - color: #999; - font-size: 14px; +.HeaderSearch .ant-input-search .ant-input:-ms-input-placeholder { + color: #999; + font-size: 14px; } .HeaderSearch .ant-input-search .ant-input-suffix .anticon-search { - color: #999; + color: #999; } -.HeaderSearch .ant-input-search .ant-input{ - color: #fff; +.HeaderSearch .ant-input-search .ant-input { + color: #fff; } -.HeaderSearch .ant-input-search .ant-input-suffix{ - background: transparent !important; +.HeaderSearch .ant-input-search .ant-input-suffix { + background: transparent !important; } -.roundedRectangles{ - position: absolute; - top: 10px; - right: -22px; +.roundedRectangles { + position: absolute; + top: 10px; + right: -22px; } -.HeaderSearch{ - width: 325px; - /*right: 20px;*/ +.HeaderSearch { + width: 325px; + /*right: 20px;*/ } -.HeaderSearch .ant-input-search{ - right: 20px; + +.HeaderSearch .ant-input-search { + right: 20px; } -.mainheighs{ - height: 100%; - display: block; + +.mainheighs { + height: 100%; + display: block; } -.ml18a{ - margin-left:18%; +.ml18a { + margin-left: 18%; } -.logoimg{ - float: left; - min-width: 40px; - height:40px; +.logoimg { + float: left; + min-width: 40px; + height: 40px; } -.headwith100b{ - width: 100%; +.headwith100b { + width: 100%; } -.wechatcenter{ - text-align: center; + +.wechatcenter { + text-align: center; } -.myrigthsiderbar{ - right: 9% !important; +.myrigthsiderbar { + right: 9% !important; } -.feedbackdivcolor{ - background: #33BD8C !important; - height: 49px !important; - line-height: 24px !important; +.feedbackdivcolor { + background: #33BD8C !important; + height: 49px !important; + line-height: 24px !important; } -.xiaoshou{ - cursor:pointer + +.xiaoshou { + cursor: pointer } -.questiontypes{ - width:37px; - height:17px; - font-size:12px; - color:rgba(51,51,51,1); - line-height:17px; - cursor:pointer; + +.questiontypes { + width: 37px; + height: 17px; + font-size: 12px; + color: rgba(51, 51, 51, 1); + line-height: 17px; + cursor: pointer; } -.questiontype{ - width: 100%; - font-size: 12px; - color: #333333; - line-height: 17px; - text-align: center; - padding: 11px; - cursor:pointer; + +.questiontype { + width: 100%; + font-size: 12px; + color: #333333; + line-height: 17px; + text-align: center; + padding: 11px; + cursor: pointer; } -.questiontypeheng{ - width:100%; - height:1px; - background: #EEEEEE; + +.questiontypeheng { + width: 100%; + height: 1px; + background: #EEEEEE; } -.mystask-sidebar{ - right: 181px !important; + +.mystask-sidebar { + right: 181px !important; } -.mystask-sidebars{ - right: 20px !important; + +.mystask-sidebars { + right: 20px !important; } -.shitikussmys{ - width:29px !important; - height:20px!important; - background:#FF6601 !important; - border-radius:10px !important; - position: absolute !important; - font-size:11px !important; - color:#ffffff !important; - line-height:20px !important; - top: -13px !important; - right: -10px !important; + +.shitikussmys { + width: 29px !important; + height: 20px !important; + background: #FF6601 !important; + border-radius: 10px !important; + position: absolute !important; + font-size: 11px !important; + color: #ffffff !important; + line-height: 20px !important; + top: -13px !important; + right: -10px !important; } -.maxnamewidth30{ - max-width: 30px; - overflow:hidden; - text-overflow:ellipsis; - white-space:nowrap; - cursor: default; -} -.mystask-sidebarss{ - right: 5px !important; +.maxnamewidth30 { + max-width: 30px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: default; } + +.mystask-sidebarss { + right: 5px !important; +} \ No newline at end of file diff --git a/public/react/src/modules/tpm/shixuns/ShixunsIndex.js b/public/react/src/modules/tpm/shixuns/ShixunsIndex.js index c0fbb2c01..2613fc1c7 100644 --- a/public/react/src/modules/tpm/shixuns/ShixunsIndex.js +++ b/public/react/src/modules/tpm/shixuns/ShixunsIndex.js @@ -392,7 +392,7 @@ class ShixunsIndex extends Component { // console.log(this.state.updata) return ( -
+
{this.state.updata === undefined ? "" : } diff --git a/public/react/src/modules/tpm/shixuns/shixun-keyword-list.scss b/public/react/src/modules/tpm/shixuns/shixun-keyword-list.scss index c8a62bdd8..d74f54e03 100644 --- a/public/react/src/modules/tpm/shixuns/shixun-keyword-list.scss +++ b/public/react/src/modules/tpm/shixuns/shixun-keyword-list.scss @@ -1,3 +1,7 @@ +.shi-xun-index .search-keyword-container { + padding: 20px 0 15px 0; +} + .search-keyword-container { display: flex; flex-flow: row nowrap;