diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd4f0bf..cb5bebab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### [v1.3.0](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.0)(2021-03-26) + +### 更新 + +- 课程增加面授模型 +- 重构前台群组成员管理 +- 后台增加群组成员管理 +- 重构订单存储商品详情数据结构 +- 调整用户和群组列表等UI + ### [v1.2.9](https://gitee.com/koogua/course-tencent-cloud/releases/v1.2.9)(2021-03-22) ### 更新 diff --git a/README.md b/README.md index b8019549..acae1239 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,12 @@ ### 系统功能 -实现了点播、直播、专栏、会员、微聊等,是一个完整的产品,具体功能我也不想写一大堆,自己体验吧! +实现了点播、直播、专栏、面授、会员、群组、积分商城、秒杀等,全功能无阉割,100%真开源在线教育解决方案。具体功能我也不想写一大堆,自己体验吧! 友情提示: -- 系统配置低(1核 1G 1M 跑多个容器),切莫压测 -- 课程数据来源于网络(无实质内容),切莫购买 +- 演示系统配置低(1Core,1G,1M 跑多个容器)切莫压测 +- 课程数据来源于网络(无实质内容)切莫购买 - 管理后台已禁止数据提交,私密配置已过滤 桌面端演示: @@ -27,13 +27,13 @@ - [前台演示](https://ctc.koogua.com) - [后台演示](https://ctc.koogua.com/admin) -演示帐号:100015@163.com / 123456 (前后台通用) +演示账号:100015@163.com / 123456 (前后台通用) 移动端演示:  -演示帐号:13507083515 / 123456 +演示账号:13507083515 / 123456 支付流程演示: @@ -42,10 +42,10 @@ - [数据库与中间件的基础必修课(0.02元)](https://ctc.koogua.com/order/confirm?item_id=80&item_type=2) Tips: 测试支付请用手机号注册一个新账户,以便接收订单通知,以及避免课程无法购买 - + 即时通讯演示: -请使用以下两个帐号在不同终端或者浏览器登录,打开微聊界面 +请使用以下两个账号在不同终端或者浏览器登录,打开微聊界面 - 帐号A:100015@163.com / 123456 - 帐号B:100065@163.com / 123456 diff --git a/app/Builders/ImGroupList.php b/app/Builders/ImGroupList.php index a21b9648..f0bdd8d9 100644 --- a/app/Builders/ImGroupList.php +++ b/app/Builders/ImGroupList.php @@ -8,6 +8,17 @@ use App\Repos\User as UserRepo; class ImGroupList extends Builder { + public function handleGroups(array $groups) + { + $baseUrl = kg_cos_url(); + + foreach ($groups as $key => $group) { + $groups[$key]['avatar'] = $baseUrl . $group['avatar']; + } + + return $groups; + } + public function handleCourses(array $groups) { $courses = $this->getCourses($groups); diff --git a/app/Console/Tasks/DeliverTask.php b/app/Console/Tasks/DeliverTask.php index f37a12d0..7c57b30d 100644 --- a/app/Console/Tasks/DeliverTask.php +++ b/app/Console/Tasks/DeliverTask.php @@ -2,6 +2,7 @@ namespace App\Console\Tasks; +use App\Models\Course as CourseModel; use App\Models\CourseUser as CourseUserModel; use App\Models\ImGroupUser as ImGroupUserModel; use App\Models\Order as OrderModel; @@ -105,13 +106,19 @@ class DeliverTask extends Task protected function handleCourseOrder(OrderModel $order) { - $itemInfo = $order->item_info; + $course = $order->item_info['course']; + + if ($course['model'] == CourseModel::MODEL_OFFLINE) { + $expiryTime = strtotime($course['attrs']['end_date']); + } else { + $expiryTime = $course['study_expiry_time']; + } $courseUser = new CourseUserModel(); $courseUser->user_id = $order->owner_id; $courseUser->course_id = $order->item_id; - $courseUser->expiry_time = $itemInfo['course']['study_expiry_time']; + $courseUser->expiry_time = $expiryTime; $courseUser->role_type = CourseUserModel::ROLE_STUDENT; $courseUser->source_type = CourseUserModel::SOURCE_CHARGE; diff --git a/app/Console/Tasks/UpgradeTask.php b/app/Console/Tasks/UpgradeTask.php index 2667186d..521289f1 100644 --- a/app/Console/Tasks/UpgradeTask.php +++ b/app/Console/Tasks/UpgradeTask.php @@ -80,7 +80,7 @@ class UpgradeTask extends Task $redis->del($statsKey); } - echo "end reset metadata..." . PHP_EOL; + echo "------ end reset metadata ------" . PHP_EOL; } /** diff --git a/app/Http/Admin/Controllers/ChapterController.php b/app/Http/Admin/Controllers/ChapterController.php index 1c81ebbc..f1a40e5d 100644 --- a/app/Http/Admin/Controllers/ChapterController.php +++ b/app/Http/Admin/Controllers/ChapterController.php @@ -79,10 +79,17 @@ class ChapterController extends Controller $chapter = $chapterService->createChapter(); - $location = $this->url->get([ - 'for' => 'admin.course.chapters', - 'id' => $chapter->course_id, - ]); + if ($chapter->parent_id > 0) { + $location = $this->url->get([ + 'for' => 'admin.chapter.lessons', + 'id' => $chapter->parent_id, + ]); + } else { + $location = $this->url->get([ + 'for' => 'admin.course.chapters', + 'id' => $chapter->course_id, + ]); + } $content = [ 'location' => $location, @@ -131,6 +138,10 @@ class ChapterController extends Controller $read = $contentService->getChapterRead($chapter->id); $this->view->setVar('read', $read); break; + case CourseModel::MODEL_OFFLINE: + $offline = $contentService->getChapterOffline($chapter->id); + $this->view->setVar('offline', $offline); + break; } } diff --git a/app/Http/Admin/Controllers/ImGroupController.php b/app/Http/Admin/Controllers/ImGroupController.php index e5659778..fc715e73 100644 --- a/app/Http/Admin/Controllers/ImGroupController.php +++ b/app/Http/Admin/Controllers/ImGroupController.php @@ -10,6 +10,22 @@ use App\Http\Admin\Services\ImGroup as ImGroupService; class ImGroupController extends Controller { + /** + * @Get("/{id:[0-9]+}/users", name="admin.im_group.users") + */ + public function usersAction($id) + { + $service = new ImGroupService(); + + $group = $service->getGroup($id); + $pager = $service->getGroupUsers($id); + + $this->view->pick('im/group/users'); + + $this->view->setVar('group', $group); + $this->view->setVar('pager', $pager); + } + /** * @Get("/list", name="admin.im_group.list") */ diff --git a/app/Http/Admin/Controllers/ImGroupUserController.php b/app/Http/Admin/Controllers/ImGroupUserController.php new file mode 100644 index 00000000..f5de83f1 --- /dev/null +++ b/app/Http/Admin/Controllers/ImGroupUserController.php @@ -0,0 +1,32 @@ +deleteGroupUser(); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除成员成功', + ]; + + return $this->jsonSuccess($content); + } + +} diff --git a/app/Http/Admin/Services/AuthNode.php b/app/Http/Admin/Services/AuthNode.php index 236ae11b..646c0dec 100644 --- a/app/Http/Admin/Services/AuthNode.php +++ b/app/Http/Admin/Services/AuthNode.php @@ -381,6 +381,18 @@ class AuthNode extends Service 'type' => 'button', 'route' => 'admin.im_group.delete', ], + [ + 'id' => '2-4-6', + 'title' => '群员列表', + 'type' => 'button', + 'route' => 'admin.im_group.users', + ], + [ + 'id' => '2-4-7', + 'title' => '删除群员', + 'type' => 'button', + 'route' => 'admin.im_group_user.delete', + ], ], ], [ diff --git a/app/Http/Admin/Services/Chapter.php b/app/Http/Admin/Services/Chapter.php index 5a387d95..4d9b4efa 100644 --- a/app/Http/Admin/Services/Chapter.php +++ b/app/Http/Admin/Services/Chapter.php @@ -7,6 +7,7 @@ use App\Caches\Chapter as ChapterCache; use App\Caches\CourseChapterList as CatalogCache; use App\Models\Chapter as ChapterModel; use App\Models\ChapterLive as ChapterLiveModel; +use App\Models\ChapterOffline as ChapterOfflineModel; use App\Models\ChapterRead as ChapterReadModel; use App\Models\ChapterVod as ChapterVodModel; use App\Models\Course as CourseModel; @@ -25,9 +26,7 @@ class Chapter extends Service $resources = $resourceRepo->findByChapterId($id); - if ($resources->count() == 0) { - return []; - } + if ($resources->count() == 0) return []; $builder = new ResourceListBuilder(); @@ -118,6 +117,10 @@ class Chapter extends Service $chapterRead = new ChapterReadModel(); $attrs = $chapterRead->create($data); break; + case CourseModel::MODEL_OFFLINE: + $chapterOffline = new ChapterOfflineModel(); + $attrs = $chapterOffline->create($data); + break; } if ($attrs === false) { @@ -137,10 +140,11 @@ class Chapter extends Service $this->db->rollback(); - $logger = $this->getLogger(); + $logger = $this->getLogger('http'); $logger->error('Create Chapter Error ' . kg_json_encode([ - 'code' => $e->getCode(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), 'message' => $e->getMessage(), ])); @@ -176,7 +180,7 @@ class Chapter extends Service if (isset($post['published'])) { $data['published'] = $validator->checkPublishStatus($post['published']); - if ($chapter->published == 0 && $post['published'] == 1) { + if ($post['published'] == 1) { $validator->checkPublishAbility($chapter); } } @@ -259,6 +263,8 @@ class Chapter extends Service $courseStats->updateLiveAttrs($course->id); } elseif ($course->model == CourseModel::MODEL_READ) { $courseStats->updateReadAttrs($course->id); + } elseif ($course->model == CourseModel::MODEL_OFFLINE) { + $courseStats->updateOfflineAttrs($course->id); } } diff --git a/app/Http/Admin/Services/ChapterContent.php b/app/Http/Admin/Services/ChapterContent.php index 09fccf37..a4bb6235 100644 --- a/app/Http/Admin/Services/ChapterContent.php +++ b/app/Http/Admin/Services/ChapterContent.php @@ -12,6 +12,7 @@ use App\Services\ChapterVod as ChapterVodService; use App\Services\CourseStat as CourseStatService; use App\Services\Vod as VodService; use App\Validators\ChapterLive as ChapterLiveValidator; +use App\Validators\ChapterOffline as ChapterOfflineValidator; use App\Validators\ChapterRead as ChapterReadValidator; use App\Validators\ChapterVod as ChapterVodValidator; @@ -39,6 +40,13 @@ class ChapterContent extends Service return $chapterRepo->findChapterRead($chapterId); } + public function getChapterOffline($chapterId) + { + $chapterRepo = new ChapterRepo(); + + return $chapterRepo->findChapterOffline($chapterId); + } + public function getPlayUrls($chapterId) { $service = new ChapterVodService(); @@ -64,6 +72,9 @@ class ChapterContent extends Service case CourseModel::MODEL_READ: $this->updateChapterRead($chapter); break; + case CourseModel::MODEL_OFFLINE: + $this->updateChapterOffline($chapter); + break; } $this->rebuildCatalogCache($chapter); @@ -84,9 +95,7 @@ class ChapterContent extends Service /** * 无新文件上传 */ - if ($fileId == $vod->file_id) { - return; - } + if ($fileId == $vod->file_id) return; /** * 删除旧文件 @@ -95,21 +104,17 @@ class ChapterContent extends Service $this->deleteVodFile($vod->file_id); } - $vod->update([ - 'file_id' => $fileId, - 'file_transcode' => '', - ]); + $vod->file_id = $fileId; + $vod->file_transcode = []; + + $vod->update(); - /** - * @var array $attrs - */ $attrs = $chapter->attrs; - $attrs['duration'] = 0; - $attrs['file']['status'] = ChapterModel::FS_UPLOADED; + $chapter->attrs = $attrs; - $chapter->update(['attrs' => $attrs]); + $chapter->update(); $this->updateCourseVodAttrs($vod->course_id); } @@ -129,20 +134,17 @@ class ChapterContent extends Service $validator->checkTimeRange($startTime, $endTime); - $live->update([ - 'start_time' => $startTime, - 'end_time' => $endTime, - ]); + $live->start_time = $startTime; + $live->end_time = $endTime; + + $live->update(); - /** - * @var array $attrs - */ $attrs = $chapter->attrs; - $attrs['start_time'] = $startTime; $attrs['end_time'] = $endTime; + $chapter->attrs = $attrs; - $chapter->update(['attrs' => $attrs]); + $chapter->update(); $this->updateCourseLiveAttrs($live->course_id); } @@ -161,9 +163,6 @@ class ChapterContent extends Service $read->update(['content' => $content]); - /** - * @var array $attrs - */ $attrs = $chapter->attrs; $attrs['word_count'] = WordUtil::getWordCount($content); @@ -174,6 +173,36 @@ class ChapterContent extends Service $this->updateCourseReadAttrs($read->course_id); } + protected function updateChapterOffline(ChapterModel $chapter) + { + $post = $this->request->getPost(); + + $chapterRepo = new ChapterRepo(); + + $offline = $chapterRepo->findChapterOffline($chapter->id); + + $validator = new ChapterOfflineValidator(); + + $startTime = $validator->checkStartTime($post['start_time']); + $endTime = $validator->checkEndTime($post['end_time']); + + $validator->checkTimeRange($startTime, $endTime); + + $offline->start_time = $startTime; + $offline->end_time = $endTime; + + $offline->update(); + + $attrs = $chapter->attrs; + $attrs['start_time'] = $startTime; + $attrs['end_time'] = $endTime; + $chapter->attrs = $attrs; + + $chapter->update(); + + $this->updateCourseOfflineAttrs($offline->course_id); + } + protected function updateCourseVodAttrs($courseId) { $statService = new CourseStatService(); @@ -195,6 +224,13 @@ class ChapterContent extends Service $statService->updateReadAttrs($courseId); } + protected function updateCourseOfflineAttrs($courseId) + { + $statService = new CourseStatService(); + + $statService->updateOfflineAttrs($courseId); + } + protected function deleteVodFile($fileId) { $vodService = new VodService(); diff --git a/app/Http/Admin/Services/Course.php b/app/Http/Admin/Services/Course.php index 7ee7204d..12c858f4 100644 --- a/app/Http/Admin/Services/Course.php +++ b/app/Http/Admin/Services/Course.php @@ -26,6 +26,7 @@ use App\Repos\ImGroup as ImGroupRepo; use App\Repos\User as UserRepo; use App\Services\Sync\CourseIndex as CourseIndexSync; use App\Validators\Course as CourseValidator; +use App\Validators\CourseOffline as CourseOfflineValidator; class Course extends Service { @@ -114,7 +115,7 @@ class Course extends Service $this->db->rollback(); - $logger = $this->getLogger(); + $logger = $this->getLogger('http'); $logger->error('Create Course Error ' . kg_json_encode([ 'code' => $e->getCode(), @@ -159,17 +160,24 @@ class Course extends Service $data['level'] = $validator->checkLevel($post['level']); } - if (isset($post['price_mode'])) { - if ($post['price_mode'] == 'free') { - $data['market_price'] = 0; - $data['vip_price'] = 0; - } else { - $data['origin_price'] = $validator->checkOriginPrice($post['origin_price']); - $data['market_price'] = $validator->checkMarketPrice($post['market_price']); - $data['vip_price'] = $validator->checkVipPrice($post['vip_price']); - $data['study_expiry'] = $validator->checkStudyExpiry($post['study_expiry']); - $data['refund_expiry'] = $validator->checkRefundExpiry($post['refund_expiry']); - } + if (isset($post['study_expiry'])) { + $data['study_expiry'] = $validator->checkStudyExpiry($post['study_expiry']); + } + + if (isset($post['refund_expiry'])) { + $data['refund_expiry'] = $validator->checkRefundExpiry($post['refund_expiry']); + } + + if (isset($post['origin_price'])) { + $data['origin_price'] = $validator->checkOriginPrice($post['origin_price']); + } + + if (isset($post['market_price'])) { + $data['market_price'] = $validator->checkMarketPrice($post['market_price']); + } + + if (isset($post['vip_price'])) { + $data['vip_price'] = $validator->checkVipPrice($post['vip_price']); } if (isset($post['featured'])) { @@ -195,6 +203,28 @@ class Course extends Service $this->saveRelatedCourses($course, $post['xm_course_ids']); } + if ($course->model == CourseModel::MODEL_OFFLINE) { + + $validator = new CourseOfflineValidator(); + + $data['study_expiry'] = 0; + $data['refund_expiry'] = 0; + + if (isset($post['attrs']['start_date']) && isset($post['attrs']['end_date'])) { + $data['attrs']['start_date'] = $validator->checkStartDate($post['attrs']['start_date']); + $data['attrs']['end_date'] = $validator->checkEndDate($post['attrs']['end_date']); + $validator->checkDateRange($data['attrs']['start_date'], $data['attrs']['end_date']); + } + + if (isset($post['attrs']['user_limit'])) { + $data['attrs']['user_limit'] = $validator->checkUserLimit($post['attrs']['user_limit']); + } + + if (isset($post['attrs']['location'])) { + $data['attrs']['location'] = $validator->checkLocation($post['attrs']['location']); + } + } + $course->update($data); $this->updateImGroup($course); diff --git a/app/Http/Admin/Services/ImGroup.php b/app/Http/Admin/Services/ImGroup.php index 9f4af997..8cad2a93 100644 --- a/app/Http/Admin/Services/ImGroup.php +++ b/app/Http/Admin/Services/ImGroup.php @@ -3,13 +3,15 @@ namespace App\Http\Admin\Services; use App\Builders\ImGroupList as ImGroupListBuilder; +use App\Builders\ImGroupUserList as ImGroupUserListBuilder; use App\Library\Paginator\Query as PagerQuery; use App\Models\ImGroup as ImGroupModel; use App\Models\ImGroupUser as ImGroupUserModel; -use App\Models\User as UserModel; +use App\Models\ImUser as ImUserModel; use App\Repos\ImGroup as ImGroupRepo; use App\Repos\ImGroupUser as ImGroupUserRepo; use App\Validators\ImGroup as ImGroupValidator; +use App\Validators\ImGroupUser as ImGroupUserValidator; class ImGroup extends Service { @@ -89,9 +91,10 @@ class ImGroup extends Service } if (isset($post['owner_id'])) { - $owner = $validator->checkGroupOwner($post['owner_id']); - $data['owner_id'] = $owner->id; - $this->handleGroupOwner($group, $owner); + $validator = new ImGroupUserValidator(); + $user = $validator->checkUser($post['owner_id']); + $data['owner_id'] = $user->id; + $this->handleGroupOwner($group, $user); } $group->update($data); @@ -121,7 +124,42 @@ class ImGroup extends Service return $group; } - protected function handleGroupOwner(ImGroupModel $group, UserModel $user) + public function getGroupUsers($id) + { + $pagerQuery = new PagerQuery(); + + $params = $pagerQuery->getParams(); + + $params['group_id'] = $id; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $groupUserRepo = new ImGroupUserRepo(); + + $pager = $groupUserRepo->paginate($params, $sort, $page, $limit); + + return $this->handleGroupUsers($pager); + } + + protected function handleGroupUsers($pager) + { + if ($pager->total_items == 0) { + return $pager; + } + + $builder = new ImGroupUserListBuilder(); + + $stepA = $pager->items->toArray(); + $stepB = $builder->handleUsers($stepA); + + $pager->items = $stepB; + + return $pager; + } + + protected function handleGroupOwner(ImGroupModel $group, ImUserModel $user) { $repo = new ImGroupUserRepo(); @@ -130,21 +168,40 @@ class ImGroup extends Service if ($groupUser) return; $groupUser = new ImGroupUserModel(); + $groupUser->group_id = $group->id; $groupUser->user_id = $user->id; + $groupUser->create(); + $this->incrGroupUserCount($group); + + $this->incrUserGroupCount($user); + } + + protected function incrGroupUserCount(ImGroupModel $group) + { $group->user_count += 1; + $group->update(); } + protected function incrUserGroupCount(ImUserModel $user) + { + $user->group_count += 1; + + $user->update(); + } + protected function handleGroups($pager) { if ($pager->total_items > 0) { $builder = new ImGroupListBuilder(); - $pipeA = $pager->items->toArray(); + $items = $pager->items->toArray(); + + $pipeA = $builder->handleGroups($items); $pipeB = $builder->handleUsers($pipeA); $pipeC = $builder->objects($pipeB); diff --git a/app/Http/Admin/Services/ImGroupUser.php b/app/Http/Admin/Services/ImGroupUser.php new file mode 100644 index 00000000..22305ece --- /dev/null +++ b/app/Http/Admin/Services/ImGroupUser.php @@ -0,0 +1,55 @@ +request->getQuery('group_id', 'int', 0); + $userId = $this->request->getQuery('user_id', 'int', 0); + + $validator = new ImGroupUserValidator(); + + $group = $validator->checkGroup($groupId); + $user = $validator->checkUser($userId); + + $validator->checkIfAllowDelete($groupId, $userId); + + $groupUser = $this->findOrFail($groupId, $userId); + + $groupUser->delete(); + + $this->decrGroupUserCount($group); + $this->decrUserGroupCount($user); + } + + protected function decrGroupUserCount(ImGroupModel $group) + { + if ($group->user_count > 0) { + $group->user_count -= 1; + $group->update(); + } + } + + protected function decrUserGroupCount(ImUserModel $user) + { + if ($user->group_count > 0) { + $user->group_count -= 1; + $user->update(); + } + } + + protected function findOrFail($groupId, $userId) + { + $validator = new ImGroupUserValidator(); + + return $validator->checkGroupUser($groupId, $userId); + } + +} diff --git a/app/Http/Admin/Services/Package.php b/app/Http/Admin/Services/Package.php index f89a13ce..07171e7b 100644 --- a/app/Http/Admin/Services/Package.php +++ b/app/Http/Admin/Services/Package.php @@ -6,6 +6,7 @@ use App\Caches\CoursePackageList as CoursePackageListCache; use App\Caches\Package as PackageCache; use App\Caches\PackageCourseList as PackageCourseListCache; use App\Library\Paginator\Query as PagerQuery; +use App\Models\Course as CourseModel; use App\Models\CoursePackage as CoursePackageModel; use App\Models\Package as PackageModel; use App\Repos\Course as CourseRepo; @@ -32,7 +33,20 @@ class Package extends Service $courseRepo = new CourseRepo(); - $items = $courseRepo->findAll(['free' => 0, 'published' => 1]); + /** + * 面授课程不参与套餐计划,因为无法进行退款计算 + */ + $model = [ + CourseModel::MODEL_VOD, + CourseModel::MODEL_LIVE, + CourseModel::MODEL_READ, + ]; + + $items = $courseRepo->findAll([ + 'model' => $model, + 'free' => 0, + 'published' => 1, + ]); if ($items->count() == 0) return []; diff --git a/app/Http/Admin/Services/User.php b/app/Http/Admin/Services/User.php index 539fcb32..7ebf83c9 100644 --- a/app/Http/Admin/Services/User.php +++ b/app/Http/Admin/Services/User.php @@ -267,7 +267,9 @@ class User extends Service $builder = new UserListBuilder(); - $pipeA = $pager->items->toArray(); + $items = $pager->items->toArray(); + + $pipeA = $builder->handleUsers($items); $pipeB = $builder->handleAdminRoles($pipeA); $pipeC = $builder->handleEduRoles($pipeB); $pipeD = $builder->objects($pipeC); diff --git a/app/Http/Admin/Views/chapter/edit_lesson.volt b/app/Http/Admin/Views/chapter/edit_lesson.volt index 4f0c7dc6..9f4e98ab 100644 --- a/app/Http/Admin/Views/chapter/edit_lesson.volt +++ b/app/Http/Admin/Views/chapter/edit_lesson.volt @@ -9,6 +9,8 @@ 直播信息 {% elseif model == '3' %} 图文信息 + {% elseif model == '4' %} + 面授信息 {% endif %} {%- endmacro %} @@ -33,6 +35,8 @@ {{ partial('chapter/edit_lesson_live') }} {% elseif course.model == 3 %} {{ partial('chapter/edit_lesson_read') }} + {% elseif course.model == 4 %} + {{ partial('chapter/edit_lesson_offline') }} {% endif %}
开始:{{ date('Y-m-d H:i',attrs['start_time']) }}
+结束:{{ date('Y-m-d H:i',attrs['end_time']) }}
+ {% else %} + N/A + {% endif %} +{%- endmacro %} + +编号 | +名称 | +时间 | +排序 | +免费 | +发布 | +操作 | +
---|---|---|---|---|---|---|
{{ item.id }} | ++ {{ item.title }} + 课 + | +{{ offline_time_info(item.attrs) }} | ++ | + | + | + + | +