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;