1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-06-22 03:32:47 +08:00

设计弹幕部分

This commit is contained in:
xiaochong0302 2020-07-09 21:31:19 +08:00
parent f2e862e9ae
commit 923e311f79
21 changed files with 654 additions and 50 deletions

View File

@ -24,6 +24,7 @@ class UserDailyCounter extends Counter
return [ return [
'favorite_count' => 0, 'favorite_count' => 0,
'comment_count' => 0, 'comment_count' => 0,
'danmu_count' => 0,
'consult_count' => 0, 'consult_count' => 0,
'order_count' => 0, 'order_count' => 0,
'chapter_vote_count' => 0, 'chapter_vote_count' => 0,

View File

@ -24,10 +24,23 @@ class ChapterController extends Controller
$chapter = $service->handle($id); $chapter = $service->handle($id);
$owned = $chapter['me']['owned'] ?? false;
if (!$owned) {
$this->response->redirect([
'for' => 'web.course.show',
'id' => $chapter['course']['id'],
]);
}
$service = new CourseChapterListService(); $service = new CourseChapterListService();
$chapters = $service->handle($chapter['course']['id']); $chapters = $service->handle($chapter['course']['id']);
$this->siteSeo->prependTitle([$chapter['title'], $chapter['course']['title']]);
$this->siteSeo->setKeywords($chapter['title']);
$this->siteSeo->setDescription($chapter['summary']);
if ($chapter['model'] == 'vod') { if ($chapter['model'] == 'vod') {
$this->view->pick('chapter/show_vod'); $this->view->pick('chapter/show_vod');
} elseif ($chapter['model'] == 'live') { } elseif ($chapter['model'] == 'live') {

View File

@ -87,6 +87,10 @@ class CourseController extends Controller
$rewardOptions = $service->handle(); $rewardOptions = $service->handle();
$this->siteSeo->prependTitle($course['title']);
$this->siteSeo->setKeywords($course['keywords']);
$this->siteSeo->setDescription($course['summary']);
$this->view->setVar('course', $course); $this->view->setVar('course', $course);
$this->view->setVar('chapters', $chapters); $this->view->setVar('chapters', $chapters);
$this->view->setVar('teachers', $teachers); $this->view->setVar('teachers', $teachers);

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Web\Controllers;
use App\Services\Frontend\Danmu\DanmuCreate as DanmuCreateService;
use App\Services\Frontend\Danmu\DanmuInfo as DanmuInfoService;
/**
* @RoutePrefix("/danmu")
*/
class DanmuController extends Controller
{
/**
* @Post("/create", name="web.danmu.create")
*/
public function createAction()
{
$service = new DanmuCreateService();
$danmu = $service->handle();
$service = new DanmuInfoService();
$danmu = $service->handle($danmu->id);
return $this->jsonSuccess(['danmu' => $danmu]);
}
}

View File

@ -51,9 +51,9 @@ class ImController extends LayerController
} }
/** /**
* @Get("/msg/sys/unread/count", name="web.im.unread_sys_msg_count") * @Get("/msg/sys/unread", name="web.im.unread_sys_msg")
*/ */
public function unreadSystemMessagesCountAction() public function unreadSystemMessagesAction()
{ {
$service = new ImService(); $service = new ImService();

View File

@ -498,7 +498,7 @@ class Im extends Service
/** /**
* 避免频繁推送消息 * 避免频繁推送消息
*/ */
if ($onlinePushTime && time() - $onlinePushTime > 600) { if ($onlinePushTime && time() - $onlinePushTime < 600) {
return; return;
} }

View File

@ -17,7 +17,9 @@
<div class="layout-main"> <div class="layout-main">
<div class="layout-content"> <div class="layout-content">
<div id="player" class="player container"></div> <div class="player-container container">
<div id=player"></div>
</div>
</div> </div>
<div class="layout-sidebar"> <div class="layout-sidebar">
<div class="chat-container"> <div class="chat-container">
@ -32,12 +34,12 @@
{% if auth_user.id > 0 %} {% if auth_user.id > 0 %}
<div class="chat-msg-form"> <div class="chat-msg-form">
<form class="layui-form" method="post" action="{{ send_msg_url }}"> <form class="layui-form" method="post" action="{{ send_msg_url }}">
<input class="layui-input" type="text" name="content" maxlength="80" placeholder="快来和大家一起互动吧~" lay-verType="tips" lay-verify="required"> <input class="layui-input" type="text" name="content" maxlength="50" placeholder="快来一起互动吧" lay-verType="tips" lay-verify="required">
<button class="layui-hide" type="submit" lay-submit="true" lay-filter="chat">发送</button> <button class="layui-hide" type="submit" lay-submit="true" lay-filter="chat">发送</button>
</form> </form>
</div> </div>
{% else %} {% else %}
<div class="chat-login-tips">登录后才可以发言哦</div> <div class="chat-login-tips">登录后才可以发言哦</div>
{% endif %} {% endif %}
</div> </div>
<div class="layui-tab-item" id="tab-stats" data-url="{{ live_stats_url }}"></div> <div class="layui-tab-item" id="tab-stats" data-url="{{ live_stats_url }}"></div>

View File

@ -13,7 +13,25 @@
<div class="layout-main clearfix"> <div class="layout-main clearfix">
<div class="layout-content"> <div class="layout-content">
<div id="player" class="player container"></div> <div class="player-container container">
<div id="player"></div>
<div id="danmu"></div>
</div>
<div class="danmu-action container">
<form class="layui-form" action="{{ url({'for':'web.danmu.create'}) }}">
<div class="layui-input-inline" style="width: 100px;">
<input type="checkbox" name="status" title="弹幕" checked="checked" lay-filter="status">
</div>
<div class="layui-input-inline" style="width: 655px;">
{% if auth_user.id > 0 %}
<input class="layui-input" type="text" name="text" maxlength="50" placeholder="快来发个弹幕吧" lay-verType="tips" lay-verify="required">
{% else %}
<input class="layui-input" type="text" name="text" placeholder="登录后才可以发送弹幕哦" readonly="readonly">
{% endif %}
<button class="layui-hide" type="submit" lay-submit="true" lay-filter="chat">发送</button>
</div>
</form>
</div>
</div> </div>
<div class="layout-sidebar"> <div class="layout-sidebar">
{{ partial('chapter/menu') }} {{ partial('chapter/menu') }}
@ -33,6 +51,8 @@
<script src="//imgcache.qq.com/open/qcloud/video/vcplayer/TcPlayer-2.3.2.js"></script> <script src="//imgcache.qq.com/open/qcloud/video/vcplayer/TcPlayer-2.3.2.js"></script>
{{ js_include('lib/jquery.min.js') }}
{{ js_include('lib/jquery.danmu.min.js') }}
{{ js_include('web/js/vod.player.js') }} {{ js_include('web/js/vod.player.js') }}
{% endblock %} {% endblock %}

View File

@ -47,12 +47,16 @@ class Seo
public function appendTitle($text) public function appendTitle($text)
{ {
$this->title = $this->title . $this->titleSeparator . $text; $append = is_array($text) ? implode($this->titleSeparator, $text) : $text;
$this->title = $this->title . $this->titleSeparator . $append;
} }
public function prependTitle($text) public function prependTitle($text)
{ {
$this->title = $text . $this->titleSeparator . $this->title; $prepend = is_array($text) ? implode($this->titleSeparator, $text) : $text;
$this->title = $prepend . $this->titleSeparator . $this->title;
} }
public function getTitle() public function getTitle()

View File

@ -26,6 +26,11 @@ class UserDailyCounter extends Listener
$this->counter->hIncrBy($user->id, 'comment_count'); $this->counter->hIncrBy($user->id, 'comment_count');
} }
public function incrDanmuCount(Event $event, $source, UserModel $user)
{
$this->counter->hIncrBy($user->id, 'danmu_count');
}
public function incrConsultCount(Event $event, $source, UserModel $user) public function incrConsultCount(Event $event, $source, UserModel $user)
{ {
$this->counter->hIncrBy($user->id, 'consult_count'); $this->counter->hIncrBy($user->id, 'consult_count');

157
app/Models/Danmu.php Normal file
View File

@ -0,0 +1,157 @@
<?php
namespace App\Models;
use Phalcon\Mvc\Model\Behavior\SoftDelete;
class Danmu extends Model
{
/**
* 字号类型
*/
const SIZE_SMALL = 0; // 小号
const SIZE_BIG = 1; // 大号
/**
* 位置类型
*/
const POS_MOVE = 0; // 滚动
const POS_TOP = 1; // 顶部
const POS_BOTTOM = 2; // 底部
/**
* 主键编号
*
* @var int
*/
public $id;
/**
* 课程编号
*
* @var int
*/
public $course_id;
/**
* 章节编号
*
* @var int
*/
public $chapter_id;
/**
* 用户编号
*
* @var int
*/
public $user_id;
/**
* 内容
*
* @var string
*/
public $text;
/**
* 颜色
*
* @var string
*/
public $color;
/**
* 字号
*
* @var int
*/
public $size;
/**
* 位置
*
* @var int
*/
public $position;
/**
* 时间轴
*
* @var int
*/
public $time;
/**
* 发布标识
*
* @var int
*/
public $published;
/**
* 删除标识
*
* @var int
*/
public $deleted;
/**
* 创建时间
*
* @var int
*/
public $create_time;
/**
* 更新时间
*
* @var int
*/
public $update_time;
public function getSource(): string
{
return 'kg_danmu';
}
public function initialize()
{
parent::initialize();
$this->addBehavior(
new SoftDelete([
'field' => 'deleted',
'value' => 1,
])
);
}
public function beforeCreate()
{
$this->create_time = time();
}
public function beforeUpdate()
{
$this->update_time = time();
}
public static function sizeTypes()
{
return [
'0' => '小号',
'1' => '大号',
];
}
public static function positionTypes()
{
return [
'0' => '滚动',
'1' => '顶部',
'' => '底部',
];
}
}

85
app/Repos/Danmu.php Normal file
View File

@ -0,0 +1,85 @@
<?php
namespace App\Repos;
use App\Library\Paginator\Adapter\QueryBuilder as PagerQueryBuilder;
use App\Models\Danmu as DanmuModel;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class Danmu extends Repository
{
public function paginate($where = [], $sort = 'latest', $page = 1, $limit = 15)
{
$builder = $this->modelsManager->createBuilder();
$builder->from(DanmuModel::class);
$builder->where('1 = 1');
if (!empty($where['id'])) {
$builder->andWhere('id = :id:', ['id' => $where['id']]);
}
if (!empty($where['course_id'])) {
$builder->andWhere('course_id = :course_id:', ['course_id' => $where['course_id']]);
}
if (!empty($where['chapter_id'])) {
$builder->andWhere('chapter_id = :chapter_id:', ['chapter_id' => $where['chapter_id']]);
}
if (!empty($where['user_id'])) {
$builder->andWhere('user_id = :user_id:', ['user_id' => $where['user_id']]);
}
if (isset($where['published'])) {
$builder->andWhere('published = :published:', ['published' => $where['published']]);
}
if (isset($where['deleted'])) {
$builder->andWhere('deleted = :deleted:', ['deleted' => $where['deleted']]);
}
switch ($sort) {
default:
$orderBy = 'id DESC';
break;
}
$builder->orderBy($orderBy);
$pager = new PagerQueryBuilder([
'builder' => $builder,
'page' => $page,
'limit' => $limit,
]);
return $pager->paginate();
}
/**
* @param int $id
* @return DanmuModel|Model|bool
*/
public function findById($id)
{
return DanmuModel::findFirst($id);
}
/**
* @param array $ids
* @param string|array $columns
* @return ResultsetInterface|Resultset|DanmuModel[]
*/
public function findByIds($ids, $columns = '*')
{
return DanmuModel::query()
->columns($columns)
->inWhere('id', $ids)
->execute();
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Services\Frontend\Danmu;
use App\Models\Danmu as DanmuModel;
use App\Models\User as UserModel;
use App\Services\Frontend\ChapterTrait;
use App\Services\Frontend\Service as FrontendService;
use App\Validators\Danmu as DanmuValidator;
use App\Validators\UserDailyLimit as UserDailyLimitValidator;
class DanmuCreate extends FrontendService
{
use ChapterTrait;
public function handle()
{
$post = $this->request->getPost();
$user = $this->getLoginUser();
$chapter = $this->checkChapter($post['chapter_id']);
$validator = new UserDailyLimitValidator();
$validator->checkDanmuLimit($user);
$validator = new DanmuValidator();
$danmu = new DanmuModel();
$data = [];
$data['text'] = $validator->checkText($post['text']);
$data['time'] = $validator->checkTime($post['time']);
$data['course_id'] = $chapter->course_id;
$data['chapter_id'] = $chapter->id;
$data['user_id'] = $user->id;
$data['color'] = 'white';
$data['published'] = 1;
$danmu->create($data);
$this->incrUserDailyDanmuCount($user);
return $danmu;
}
protected function incrUserDailyDanmuCount(UserModel $user)
{
$this->eventsManager->fire('userDailyCounter:incrDanmuCount', $this, $user);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Services\Frontend\Danmu;
use App\Models\Danmu as DanmuModel;
use App\Repos\User as UserRepo;
use App\Services\Frontend\DanmuTrait;
use App\Services\Frontend\Service as FrontendService;
class DanmuInfo extends FrontendService
{
use DanmuTrait;
public function handle($id)
{
$danmu = $this->checkDanmu($id);
return $this->handleDanmu($danmu);
}
protected function handleDanmu(DanmuModel $danmu)
{
$result = [
'id' => $danmu->id,
'text' => $danmu->text,
'color' => $danmu->color,
'size' => $danmu->size,
'position' => $danmu->position,
'time' => $danmu->time,
];
$userRepo = new UserRepo();
$user = $userRepo->findById($danmu->user_id);
$result['user'] = [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar,
];
return $result;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Services\Frontend;
use App\Validators\Danmu as DanmuValidator;
trait DanmuTrait
{
public function checkDanmu($id)
{
$validator = new DanmuValidator();
return $validator->checkDanmu($id);
}
}

86
app/Validators/Danmu.php Normal file
View File

@ -0,0 +1,86 @@
<?php
namespace App\Validators;
use App\Exceptions\BadRequest as BadRequestException;
use App\Models\Danmu as DanmuModel;
use App\Repos\Danmu as DanmuRepo;
class Danmu extends Validator
{
public function checkDanmu($id)
{
$danmuRepo = new DanmuRepo();
$danmu = $danmuRepo->findById($id);
if (!$danmu) {
throw new BadRequestException('danmu.not_found');
}
return $danmu;
}
public function checkChapter($id)
{
$validator = new Chapter();
return $validator->checkChapter($id);
}
public function checkText($text)
{
$value = $this->filter->sanitize($text, ['trim', 'string']);
$length = kg_strlen($value);
if ($length < 1) {
throw new BadRequestException('danmu.text_too_short');
}
if ($length > 100) {
throw new BadRequestException('danmu.text_too_long');
}
return $value;
}
public function checkSize($size)
{
$list = DanmuModel::sizeTypes();
if (!isset($list[$size])) {
throw new BadRequestException('danmu.invalid_size');
}
return $size;
}
public function checkPosition($pos)
{
$list = DanmuModel::positionTypes();
if (!isset($list[$pos])) {
throw new BadRequestException('danmu.invalid_position');
}
return $pos;
}
public function checkTime($time)
{
$value = (int)$time;
if ($value < 0) {
throw new BadRequestException('danmu.invalid_time');
}
if ($value > 3 * 3600) {
throw new BadRequestException('danmu.invalid_time');
}
return $value;
}
}

View File

@ -38,6 +38,17 @@ class UserDailyLimit extends Validator
} }
} }
public function checkDanmuLimit(UserModel $user)
{
$count = $this->counter->hGet($user->id, 'danmu_count');
$limit = $user->vip ? 100 : 50;
if ($count > $limit) {
throw new BadRequestException('user_daily_limit.reach_danmu_limit');
}
}
public function checkConsultLimit(UserModel $user) public function checkConsultLimit(UserModel $user)
{ {
$count = $this->counter->hGet($user->id, 'consult_count'); $count = $this->counter->hGet($user->id, 'consult_count');

1
public/static/lib/jquery.danmu.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -778,7 +778,19 @@
margin-bottom: 0; margin-bottom: 0;
} }
.player { .danmu-action {
padding: 15px 20px;
margin-bottom: 0;
}
.danmu-action .layui-input {
height: 32px;
line-height: 32px;
font-size: 12px;
}
.player-container {
position: relative;
width: 760px; width: 760px;
height: 428px; height: 428px;
} }
@ -1425,30 +1437,16 @@
color: #999; color: #999;
} }
.layui-layim-list li .msg-count, .layim-chat-list li .msg-count {
position: absolute;
color: white;
background-color: red;
}
.layui-layim-list li .msg-count { .layui-layim-list li .msg-count {
position: absolute;
top: 10px; top: 10px;
right: 10px; right: 10px;
padding: 2px 5px;
line-height: 16px;
height: 16px;
border-radius: 2px;
font-size: 12px;
} }
.layim-chat-list li .msg-count { .layim-chat-list li .msg-count {
position: absolute;
top: 5px; top: 5px;
right: 30px; right: 30px;
padding: 1px 3px;
line-height: 12px;
height: 12px;
border-radius: 2px;
font-size: 10px;
} }
.layim-chat-status .online { .layim-chat-status .online {

View File

@ -68,7 +68,7 @@ layui.use(['jquery', 'layim'], function () {
layui.each(group.list, function (j, user) { layui.each(group.list, function (j, user) {
var $li = $('.layui-layim-list > .layim-friend' + user.id); var $li = $('.layui-layim-list > .layim-friend' + user.id);
if (user.msg_count > 0) { if (user.msg_count > 0) {
$li.append('<em class="msg-count">' + user.msg_count + '</em>'); $li.append('<em class="msg-count layui-badge">' + user.msg_count + '</em>');
} }
}); });
}); });
@ -140,7 +140,7 @@ layui.use(['jquery', 'layim'], function () {
var count = parseInt($msgCount.text()); var count = parseInt($msgCount.text());
$msgCount.text(count + 1).removeClass('layui-hide'); $msgCount.text(count + 1).removeClass('layui-hide');
} else { } else {
$li.append('<em class="msg-count">1</em>'); $li.append('<em class="msg-count layui-badge">1</em>');
} }
} }
@ -196,7 +196,7 @@ layui.use(['jquery', 'layim'], function () {
function refreshMessageBox() { function refreshMessageBox() {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/im/msg/sys/unread/count', url: '/im/msg/sys/unread',
success: function (res) { success: function (res) {
if (res.count > 0) { if (res.count > 0) {
layim.msgbox(res.count); layim.msgbox(res.count);

View File

@ -1,6 +1,7 @@
layui.use(['jquery', 'helper'], function () { layui.use(['jquery', 'form', 'helper'], function () {
var $ = layui.jquery; var $ = layui.jquery;
var form = layui.form;
var helper = layui.helper; var helper = layui.helper;
var interval = null; var interval = null;
@ -31,17 +32,15 @@ layui.use(['jquery', 'helper'], function () {
options.m3u8_sd = playUrls.sd.url; options.m3u8_sd = playUrls.sd.url;
} }
if (userId !== '0' && planId !== '0') { options.listener = function (msg) {
options.listener = function (msg) { if (msg.type === 'play') {
if (msg.type === 'play') { start();
start(); } else if (msg.type === 'pause') {
} else if (msg.type === 'pause') { stop();
stop(); } else if (msg.type === 'end') {
} else if (msg.type === 'end') { stop();
stop();
}
} }
} };
var player = new TcPlayer('player', options); var player = new TcPlayer('player', options);
@ -49,31 +48,100 @@ layui.use(['jquery', 'helper'], function () {
player.currentTime(position); player.currentTime(position);
} }
$('#danmu').danmu({
left: 20,
top: 20,
width: 750,
height: 380
});
//再添加三个弹幕
$("#danmu").danmu("addDanmu", [
{text: "这是滚动弹幕", color: "white", size: 0, position: 0, time: 120}
, {text: "这是顶部弹幕", color: "yellow", size: 0, position: 1, time: 120}
, {text: "这是底部弹幕", color: "red", size: 0, position: 2, time: 120}
]);
form.on('checkbox(status)', function (data) {
if (data.elem.checked) {
$('#danmu').danmu('setOpacity', 1);
} else {
$('#danmu').danmu('setOpacity', 0);
}
});
form.on('submit(chat)', function (data) {
$.ajax({
type: 'POST',
url: data.form.action,
data: {
text: data.field.text,
time: player.currentTime(),
chapter_id: chapterId,
},
success: function (res) {
showDanmu(res);
}
});
return false;
});
function start() { function start() {
if (interval != null) { if (interval != null) {
clearInterval(interval); clearInterval(interval);
interval = null; interval = null;
} }
interval = setInterval(learning, intervalTime); interval = setInterval(learning, intervalTime);
startDanmu();
} }
function stop() { function stop() {
clearInterval(interval); clearInterval(interval);
interval = null; interval = null;
pauseDanmu();
} }
function learning() { function learning() {
$.ajax({ if (userId !== '0' && planId !== '0') {
type: 'POST', $.ajax({
url: learningUrl, type: 'POST',
data: { url: learningUrl,
request_id: requestId, data: {
chapter_id: chapterId, request_id: requestId,
plan_id: planId, chapter_id: chapterId,
interval: intervalTime, plan_id: planId,
position: player.currentTime(), interval: intervalTime,
} position: player.currentTime(),
}
});
}
}
function startDanmu() {
$('#danmu').danmu('danmuStart');
}
function pauseDanmu() {
$('#danmu').danmu('danmuPause');
}
function showDanmu(res) {
/*
$('#danmu').danmu('addDanmu', {
text: res.danmu.text,
color: res.danmu.color,
size: res.danmu.size,
time: res.danmu.time,
position: res.danmu.position,
isnew: 1
}); });
*/
$("#danmu").danmu("addDanmu", [
{text: "这是滚动弹幕", color: "white", size: 0, position: 0, time: 300}
, {text: "这是顶部弹幕", color: "yellow", size: 0, position: 0, time: 300}
, {text: "这是底部弹幕", color: "red", size: 0, position: 0, time: 300}
]);
$('input[name=text]').val('');
} }
}); });