1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-06-26 12:23:06 +08:00

Merge branch 'koogua/v1.3.1' into demo

This commit is contained in:
koogua 2021-04-10 12:24:18 +08:00
commit a03dc6d626
317 changed files with 8307 additions and 775 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
/vendor
/config/config.php
/config/xs.course.ini
/config/xs.article.ini
/config/xs.group.ini
/config/xs.user.ini
/config/alipay/*.crt

View File

@ -1,3 +1,17 @@
### [v1.3.1](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.1)(2021-04-09)
### 更新
- 后台增加文章功能
- 后台增加标签功能
- 增加文章全文检索
- 整理命名空间别名
- 更新部分链接打开方式
- xm-select搜索忽略大小写
- 补充遗漏的面授模型章节相关迁移文件
- 修正上次字段整理导致的字段不存在问题
- 修正上次整理发布字段导致的添加单页和帮助错误
### [v1.3.0](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.0)(2021-03-26)
### 更新

View File

@ -0,0 +1,78 @@
<?php
namespace App\Builders;
use App\Repos\Article as ArticleRepo;
use App\Repos\User as UserRepo;
class ArticleFavoriteList extends Builder
{
public function handleArticles(array $relations)
{
$articles = $this->getArticles($relations);
foreach ($relations as $key => $value) {
$relations[$key]['article'] = $articles[$value['article_id']] ?? new \stdClass();
}
return $relations;
}
public function handleUsers(array $relations)
{
$users = $this->getUsers($relations);
foreach ($relations as $key => $value) {
$relations[$key]['user'] = $users[$value['user_id']] ?? new \stdClass();
}
return $relations;
}
public function getArticles(array $relations)
{
$ids = kg_array_column($relations, 'article_id');
$articleRepo = new ArticleRepo();
$columns = [
'id', 'title', 'cover',
'view_count', 'like_count', 'comment_count', 'favorite_count',
];
$articles = $articleRepo->findByIds($ids, $columns);
$baseUrl = kg_cos_url();
$result = [];
foreach ($articles->toArray() as $article) {
$article['cover'] = $baseUrl . $article['cover'];
$result[$article['id']] = $article;
}
return $result;
}
public function getUsers(array $relations)
{
$ids = kg_array_column($relations, 'user_id');
$userRepo = new UserRepo();
$users = $userRepo->findByIds($ids, ['id', 'name', 'avatar']);
$baseUrl = kg_cos_url();
$result = [];
foreach ($users->toArray() as $user) {
$user['avatar'] = $baseUrl . $user['avatar'];
$result[$user['id']] = $user;
}
return $result;
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Builders;
use App\Caches\CategoryList as CategoryListCache;
use App\Models\Category as CategoryModel;
use App\Repos\User as UserRepo;
class ArticleList extends Builder
{
public function handleArticles(array $articles)
{
foreach ($articles as $key => $article) {
$articles[$key]['tags'] = json_decode($article['tags'], true);
}
return $articles;
}
public function handleCategories(array $articles)
{
$categories = $this->getCategories();
foreach ($articles as $key => $article) {
$articles[$key]['category'] = $categories[$article['category_id']] ?? new \stdClass();
}
return $articles;
}
public function handleUsers(array $articles)
{
$users = $this->getUsers($articles);
foreach ($articles as $key => $article) {
$articles[$key]['owner'] = $users[$article['owner_id']] ?? new \stdClass();
}
return $articles;
}
public function getCategories()
{
$cache = new CategoryListCache();
$items = $cache->get(CategoryModel::TYPE_ARTICLE);
if (empty($items)) return [];
$result = [];
foreach ($items as $item) {
$result[$item['id']] = [
'id' => $item['id'],
'name' => $item['name'],
];
}
return $result;
}
public function getUsers($articles)
{
$ids = kg_array_column($articles, 'owner_id');
$userRepo = new UserRepo();
$users = $userRepo->findByIds($ids, ['id', 'name', 'avatar']);
$baseUrl = kg_cos_url();
$result = [];
foreach ($users->toArray() as $user) {
$user['avatar'] = $baseUrl . $user['avatar'];
$result[$user['id']] = $user;
}
return $result;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Builders;
use App\Repos\User as UserRepo;
class CommentList extends Builder
{
public function handleUsers(array $comments)
{
$users = $this->getUsers($comments);
foreach ($comments as $key => $comment) {
$comments[$key]['owner'] = $users[$comment['owner_id']] ?? new \stdClass();
$comments[$key]['to_user'] = $users[$comment['to_user_id']] ?? new \stdClass();
}
return $comments;
}
public function getUsers(array $comments)
{
$ownerIds = kg_array_column($comments, 'owner_id');
$toUserIds = kg_array_column($comments, 'to_user_id');
$ids = array_merge($ownerIds, $toUserIds);
$userRepo = new UserRepo();
$users = $userRepo->findByIds($ids, ['id', 'name', 'avatar']);
$baseUrl = kg_cos_url();
$result = [];
foreach ($users->toArray() as $user) {
$user['avatar'] = $baseUrl . $user['avatar'];
$result[$user['id']] = $user;
}
return $result;
}
}

View File

@ -37,9 +37,7 @@ class CourseList extends Builder
$items = $cache->get(CategoryModel::TYPE_COURSE);
if (empty($items)) {
return [];
}
if (empty($items)) return [];
$result = [];

31
app/Caches/Article.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace App\Caches;
use App\Repos\Article as ArticleRepo;
class Article extends Cache
{
protected $lifetime = 1 * 86400;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return "article:{$id}";
}
public function getContent($id = null)
{
$articleRepo = new ArticleRepo();
$article = $articleRepo->findById($id);
return $article ?: null;
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Caches;
use App\Models\Article as ArticleModel;
use App\Models\ArticleLike as ArticleLikeModel;
use App\Repos\User as UserRepo;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class ArticleHotAuthorList extends Cache
{
protected $lifetime = 1 * 86400;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return 'article_hot_author_list';
}
public function getContent($id = null)
{
$rankings = $this->findWeeklyAuthorRankings();
if ($rankings->count() > 0) {
$userIds = kg_array_column($rankings->toArray(), 'author_id');
return $this->handleUsers($userIds);
}
$randOwners = $this->findRandArticleOwners();
if ($randOwners->count() > 0) {
$userIds = kg_array_column($randOwners->toArray(), 'owner_id');
return $this->handleUsers($userIds);
}
return [];
}
protected function handleUsers($userIds)
{
$userRepo = new UserRepo();
$users = $userRepo->findByIds($userIds);
$result = [];
foreach ($users as $user) {
$result[] = [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar,
'title' => $user->title,
'about' => $user->about,
'vip' => $user->vip,
];
}
return $result;
}
/**
* @param int $limit
* @return ResultsetInterface|Resultset
*/
protected function findRandArticleOwners($limit = 10)
{
return ArticleModel::query()
->columns(['owner_id'])
->orderBy('RAND()')
->limit($limit)
->execute();
}
/**
* @param int $limit
* @return ResultsetInterface|Resultset
*/
protected function findWeeklyAuthorRankings($limit = 10)
{
$createTime = strtotime('monday this week');
$columns = [
'author_id' => 'a.owner_id',
'like_count' => 'count(al.user_id)',
];
return $this->modelsManager->createBuilder()
->columns($columns)
->addFrom(ArticleLikeModel::class, 'al')
->join(ArticleModel::class, 'al.article_id = a.id', 'a')
->where('al.create_time > :create_time:', ['create_time' => $createTime])
->groupBy('author_id')
->orderBy('like_count DESC')
->limit($limit)->getQuery()->execute();
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Caches;
use App\Models\Article as ArticleModel;
use App\Repos\Article as ArticleRepo;
class ArticleRelatedList extends Cache
{
protected $articleId;
protected $limit = 5;
protected $lifetime = 1 * 86400;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return "article_related_list:{$id}";
}
public function getContent($id = null)
{
$this->articleId = $id;
$articleRepo = new ArticleRepo();
$article = $articleRepo->findById($id);
if (empty($article->tags)) return [];
$tagIds = kg_array_column($article->tags, 'id');
$randKey = array_rand($tagIds);
$where = [
'tag_id' => $tagIds[$randKey],
'published' => 1,
];
$pager = $articleRepo->paginate($where);
if ($pager->total_items == 0) return [];
return $this->handleContent($pager->items);
}
/**
* @param ArticleModel[] $articles
* @return array
*/
public function handleContent($articles)
{
$result = [];
$count = 0;
foreach ($articles as $article) {
if ($article->id != $this->articleId && $count < $this->limit) {
$result[] = [
'id' => $article->id,
'title' => $article->title,
'cover' => $article->cover,
'view_count' => $article->view_count,
'like_count' => $article->like_count,
'comment_count' => $article->comment_count,
'favorite_count' => $article->favorite_count,
];
$count++;
}
}
return $result;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Caches;
use App\Models\Article as ArticleModel;
class MaxArticleId extends Cache
{
protected $lifetime = 365 * 86400;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return 'max_article_id';
}
public function getContent($id = null)
{
$article = ArticleModel::findFirst(['order' => 'id DESC']);
return $article->id ?? 0;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Caches;
use App\Models\Comment as CommentModel;
class MaxCommentId extends Cache
{
protected $lifetime = 365 * 86400;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return 'max_comment_id';
}
public function getContent($id = null)
{
$comment = CommentModel::findFirst(['order' => 'id DESC']);
return $comment->id ?? 0;
}
}

29
app/Caches/MaxTagId.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\Caches;
use App\Models\Tag as TagModel;
class MaxTagId extends Cache
{
protected $lifetime = 365 * 86400;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return 'max_tag_id';
}
public function getContent($id = null)
{
$tag = TagModel::findFirst(['order' => 'id DESC']);
return $tag->id ?? 0;
}
}

31
app/Caches/Tag.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace App\Caches;
use App\Repos\Tag as TagRepo;
class Tag extends Cache
{
protected $lifetime = 365 * 86400;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return "tag:{$id}";
}
public function getContent($id = null)
{
$tagRepo = new TagRepo();
$tag = $tagRepo->findById($id);
return $tag ?: null;
}
}

View File

@ -28,6 +28,8 @@ class UserDailyCounter extends Counter
'chapter_like_count' => 0,
'consult_like_count' => 0,
'review_like_count' => 0,
'article_like_count' => 0,
'comment_like_count' => 0,
];
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\Console\Tasks;
use App\Models\Article as ArticleModel;
use App\Services\Search\ArticleDocument;
use App\Services\Search\ArticleSearcher;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class ArticleIndexTask extends Task
{
/**
* 搜索测试
*
* @command: php console.php article_index search {query}
* @param array $params
* @throws \XSException
*/
public function searchAction($params)
{
$query = $params[0] ?? null;
if (!$query) {
exit('please special a query word' . PHP_EOL);
}
$result = $this->searchArticles($query);
var_export($result);
}
/**
* 清空索引
*
* @command: php console.php article_index clean
*/
public function cleanAction()
{
$this->cleanArticleIndex();
}
/**
* 重建索引
*
* @command: php console.php article_index rebuild
*/
public function rebuildAction()
{
$this->rebuildArticleIndex();
}
/**
* 清空索引
*/
protected function cleanArticleIndex()
{
$handler = new ArticleSearcher();
$index = $handler->getXS()->getIndex();
echo '------ start clean article index ------' . PHP_EOL;
$index->clean();
echo '------ end clean article index ------' . PHP_EOL;
}
/**
* 重建索引
*/
protected function rebuildArticleIndex()
{
$articles = $this->findArticles();
if ($articles->count() == 0) return;
$handler = new ArticleSearcher();
$documenter = new ArticleDocument();
$index = $handler->getXS()->getIndex();
echo '------ start rebuild article index ------' . PHP_EOL;
$index->beginRebuild();
foreach ($articles as $article) {
$document = $documenter->setDocument($article);
$index->add($document);
}
$index->endRebuild();
echo '------ end rebuild article index ------' . PHP_EOL;
}
/**
* 搜索文章
*
* @param string $query
* @return array
* @throws \XSException
*/
protected function searchArticles($query)
{
$handler = new ArticleSearcher();
return $handler->search($query);
}
/**
* 查找文章
*
* @return ResultsetInterface|Resultset|ArticleModel[]
*/
protected function findArticles()
{
return ArticleModel::query()
->where('published = 1')
->execute();
}
}

View File

@ -3,6 +3,7 @@
namespace App\Console\Tasks;
use App\Library\Sitemap;
use App\Models\Article as ArticleModel;
use App\Models\Course as CourseModel;
use App\Models\Help as HelpModel;
use App\Models\ImGroup as ImGroupModel;
@ -35,6 +36,7 @@ class SitemapTask extends Task
$this->addIndex();
$this->addCourses();
$this->addArticles();
$this->addTeachers();
$this->addTopics();
$this->addImGroups();
@ -74,6 +76,21 @@ class SitemapTask extends Task
}
}
protected function addArticles()
{
/**
* @var Resultset|ArticleModel[] $articles
*/
$articles = ArticleModel::query()->where('published = 1')->execute();
if ($articles->count() == 0) return;
foreach ($articles as $article) {
$loc = sprintf('%s/article/%s', $this->siteUrl, $article->id);
$this->sitemap->addItem($loc, 0.8);
}
}
protected function addTeachers()
{
/**

View File

@ -0,0 +1,60 @@
<?php
namespace App\Console\Tasks;
use App\Repos\Article as ArticleRepo;
use App\Services\Search\ArticleDocument;
use App\Services\Search\ArticleSearcher;
use App\Services\Sync\ArticleIndex as ArticleIndexSync;
class SyncArticleIndexTask extends Task
{
public function mainAction()
{
$redis = $this->getRedis();
$key = $this->getSyncKey();
$articleIds = $redis->sRandMember($key, 1000);
if (!$articleIds) return;
$articleRepo = new ArticleRepo();
$articles = $articleRepo->findByIds($articleIds);
if ($articles->count() == 0) return;
$document = new ArticleDocument();
$handler = new ArticleSearcher();
$index = $handler->getXS()->getIndex();
$index->openBuffer();
foreach ($articles as $article) {
$doc = $document->setDocument($article);
if ($article->published == 1) {
$index->update($doc);
} else {
$index->del($article->id);
}
}
$index->closeBuffer();
$redis->sRem($key, ...$articleIds);
}
protected function getSyncKey()
{
$sync = new ArticleIndexSync();
return $sync->getSyncKey();
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace App\Http\Admin\Controllers;
use App\Http\Admin\Services\Article as ArticleService;
use App\Models\Category as CategoryModel;
/**
* @RoutePrefix("/admin/article")
*/
class ArticleController extends Controller
{
/**
* @Get("/category", name="admin.article.category")
*/
public function categoryAction()
{
$location = $this->url->get(
['for' => 'admin.category.list'],
['type' => CategoryModel::TYPE_ARTICLE]
);
$this->response->redirect($location);
}
/**
* @Get("/search", name="admin.article.search")
*/
public function searchAction()
{
$articleService = new ArticleService();
$sourceTypes = $articleService->getSourceTypes();
$categories = $articleService->getCategories();
$xmTags = $articleService->getXmTags(0);
$this->view->setVar('source_types', $sourceTypes);
$this->view->setVar('categories', $categories);
$this->view->setVar('xm_tags', $xmTags);
}
/**
* @Get("/list", name="admin.article.list")
*/
public function listAction()
{
$articleService = new ArticleService();
$pager = $articleService->getArticles();
$this->view->setVar('pager', $pager);
}
/**
* @Get("/add", name="admin.article.add")
*/
public function addAction()
{
$articleService = new ArticleService();
$categories = $articleService->getCategories();
$this->view->setVar('categories', $categories);
}
/**
* @Post("/create", name="admin.article.create")
*/
public function createAction()
{
$articleService = new ArticleService();
$article = $articleService->createArticle();
$location = $this->url->get([
'for' => 'admin.article.edit',
'id' => $article->id,
]);
$content = [
'location' => $location,
'msg' => '创建文章成功',
];
return $this->jsonSuccess($content);
}
/**
* @Get("/{id:[0-9]+}/edit", name="admin.article.edit")
*/
public function editAction($id)
{
$articleService = new ArticleService();
$sourceTypes = $articleService->getSourceTypes();
$categories = $articleService->getCategories();
$article = $articleService->getArticle($id);
$xmTags = $articleService->getXmTags($id);
$this->view->setVar('source_types', $sourceTypes);
$this->view->setVar('categories', $categories);
$this->view->setVar('article', $article);
$this->view->setVar('xm_tags', $xmTags);
}
/**
* @Post("/{id:[0-9]+}/update", name="admin.article.update")
*/
public function updateAction($id)
{
$articleService = new ArticleService();
$articleService->updateArticle($id);
$content = ['msg' => '更新文章成功'];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/delete", name="admin.article.delete")
*/
public function deleteAction($id)
{
$articleService = new ArticleService();
$articleService->deleteArticle($id);
$content = [
'location' => $this->request->getHTTPReferer(),
'msg' => '删除文章成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/restore", name="admin.article.restore")
*/
public function restoreAction($id)
{
$articleService = new ArticleService();
$articleService->restoreArticle($id);
$content = [
'location' => $this->request->getHTTPReferer(),
'msg' => '还原文章成功',
];
return $this->jsonSuccess($content);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Http\Admin\Controllers;
use App\Http\Admin\Services\Comment as CommentService;
/**
* @RoutePrefix("/admin/comment")
*/
class CommentController extends Controller
{
/**
* @Get("/search", name="admin.comment.search")
*/
public function searchAction()
{
}
/**
* @Get("/list", name="admin.comment.list")
*/
public function listAction()
{
$commentService = new CommentService();
$pager = $commentService->getComments();
$this->view->setVar('pager', $pager);
}
/**
* @Post("/{id:[0-9]+}/update", name="admin.comment.update")
*/
public function updateAction($id)
{
$commentService = new CommentService();
$commentService->updateComment($id);
$content = ['msg' => '更新评论成功'];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/delete", name="admin.comment.delete")
*/
public function deleteAction($id)
{
$commentService = new CommentService();
$commentService->deleteComment($id);
$content = [
'location' => $this->request->getHTTPReferer(),
'msg' => '删除评论成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/restore", name="admin.comment.restore")
*/
public function restoreAction($id)
{
$commentService = new CommentService();
$commentService->restoreComment($id);
$content = [
'location' => $this->request->getHTTPReferer(),
'msg' => '还原评论成功',
];
return $this->jsonSuccess($content);
}
}

View File

@ -58,7 +58,7 @@ class Controller extends \Phalcon\Mvc\Controller
* 特例白名单
*/
$whitelist = [
'controllers' => ['public', 'index', 'vod', 'upload', 'test'],
'controllers' => ['public', 'index', 'upload', 'test'],
'routes' => [],
];

View File

@ -50,4 +50,17 @@ class PublicController extends \Phalcon\Mvc\Controller
$this->view->setVar('region', $region);
}
/**
* @Get("/vod/player", name="admin.vod_player")
*/
public function vodPlayerAction()
{
$chapterId = $this->request->getQuery('chapter_id', 'int');
$playUrl = $this->request->getQuery('play_url', 'string');
$this->view->pick('public/vod_player');
$this->view->setVar('chapter_id', $chapterId);
$this->view->setVar('play_url', $playUrl);
}
}

View File

@ -0,0 +1,132 @@
<?php
namespace App\Http\Admin\Controllers;
use App\Http\Admin\Services\Tag as TagService;
/**
* @RoutePrefix("/admin/tag")
*/
class TagController extends Controller
{
/**
* @Get("/list", name="admin.tag.list")
*/
public function listAction()
{
$tagService = new TagService();
$pager = $tagService->getTags();
$this->view->setVar('pager', $pager);
}
/**
* @Get("/search", name="admin.tag.search")
*/
public function searchAction()
{
}
/**
* @Get("/add", name="admin.tag.add")
*/
public function addAction()
{
}
/**
* @Get("/{id:[0-9]+}/edit", name="admin.tag.edit")
*/
public function editAction($id)
{
$tagService = new TagService;
$tag2 = $tagService->getTag($id);
/**
* 注意:"tag"变量被volt引擎内置占用另取名字避免冲突
*/
$this->view->setVar('tag2', $tag2);
}
/**
* @Post("/create", name="admin.tag.create")
*/
public function createAction()
{
$tagService = new TagService();
$tagService->createTag();
$location = $this->url->get(['for' => 'admin.tag.list']);
$content = [
'location' => $location,
'msg' => '创建标签成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/update", name="admin.tag.update")
*/
public function updateAction($id)
{
$tagService = new TagService();
$tagService->updateTag($id);
$location = $this->url->get(['for' => 'admin.tag.list']);
$content = [
'location' => $location,
'msg' => '更新标签成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/delete", name="admin.tag.delete")
*/
public function deleteAction($id)
{
$tagService = new TagService();
$tagService->deleteTag($id);
$location = $this->request->getHTTPReferer();
$content = [
'location' => $location,
'msg' => '删除标签成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/restore", name="admin.tag.restore")
*/
public function restoreAction($id)
{
$tagService = new TagService();
$tagService->restoreTag($id);
$location = $this->request->getHTTPReferer();
$content = [
'location' => $location,
'msg' => '还原标签成功',
];
return $this->jsonSuccess($content);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Admin\Controllers;
use App\Services\MyStorage as StorageService;
use App\Services\Vod as VodService;
/**
* @RoutePrefix("/admin/upload")
@ -126,8 +127,10 @@ class UploadController extends Controller
$items['user_avatar'] = $service->uploadDefaultUserAvatar();
$items['group_avatar'] = $service->uploadDefaultGroupAvatar();
$items['article_cover'] = $service->uploadDefaultArticleCover();
$items['course_cover'] = $service->uploadDefaultCourseCover();
$items['group_cover'] = $service->uploadDefaultPackageCover();
$items['package_cover'] = $service->uploadDefaultPackageCover();
$items['gift_cover'] = $service->uploadDefaultGiftCover();
$items['vip_cover'] = $service->uploadDefaultVipCover();
foreach ($items as $item) {
@ -138,9 +141,9 @@ class UploadController extends Controller
}
/**
* @Get("/sign", name="admin.upload.sign")
* @Post("/credentials", name="admin.upload.credentials")
*/
public function signatureAction()
public function credentialsAction()
{
$service = new StorageService();
@ -155,4 +158,16 @@ class UploadController extends Controller
return $this->jsonSuccess($data);
}
/**
* @Post("/vod/sign", name="admin.upload.vod_sign")
*/
public function vodSignatureAction()
{
$service = new VodService();
$sign = $service->getUploadSignature();
return $this->jsonSuccess(['sign' => $sign]);
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Http\Admin\Controllers;
use App\Services\Vod as VodService;
/**
* @RoutePrefix("/admin/vod")
*/
class VodController extends Controller
{
/**
* @Get("/upload/sign", name="admin.vod.upload_sign")
*/
public function uploadSignatureAction()
{
$service = new VodService();
$sign = $service->getUploadSignature();
return $this->jsonSuccess(['sign' => $sign]);
}
/**
* @Get("/player", name="admin.vod.player")
*/
public function playerAction()
{
$chapterId = $this->request->getQuery('chapter_id', 'int');
$playUrl = $this->request->getQuery('play_url', 'string');
$this->view->pick('public/vod_player');
$this->view->setVar('chapter_id', $chapterId);
$this->view->setVar('play_url', $playUrl);
}
}

View File

@ -0,0 +1,287 @@
<?php
namespace App\Http\Admin\Services;
use App\Builders\ArticleList as ArticleListBuilder;
use App\Caches\Article as ArticleCache;
use App\Library\Paginator\Query as PagerQuery;
use App\Library\Utils\Word as WordUtil;
use App\Models\Article as ArticleModel;
use App\Models\ArticleTag as ArticleTagModel;
use App\Models\Category as CategoryModel;
use App\Repos\Article as ArticleRepo;
use App\Repos\ArticleTag as ArticleTagRepo;
use App\Repos\Category as CategoryRepo;
use App\Repos\Tag as TagRepo;
use App\Services\Sync\ArticleIndex as ArticleIndexSync;
use App\Validators\Article as ArticleValidator;
class Article extends Service
{
public function getXmTags($id)
{
$tagRepo = new TagRepo();
$allTags = $tagRepo->findAll(['published' => 1], 'priority');
if ($allTags->count() == 0) return [];
$articleTagIds = [];
if ($id > 0) {
$article = $this->findOrFail($id);
if (!empty($article->tags)) {
$articleTagIds = kg_array_column($article->tags, 'id');
}
}
$list = [];
foreach ($allTags as $tag) {
$selected = in_array($tag->id, $articleTagIds);
$list[] = [
'name' => $tag->name,
'value' => $tag->id,
'selected' => $selected,
];
}
return $list;
}
public function getCategories()
{
$categoryRepo = new CategoryRepo();
return $categoryRepo->findAll([
'type' => CategoryModel::TYPE_ARTICLE,
'level' => 1,
'published' => 1,
]);
}
public function getSourceTypes()
{
return ArticleModel::sourceTypes();
}
public function getArticles()
{
$pagerQuery = new PagerQuery();
$params = $pagerQuery->getParams();
if (!empty($params['xm_tag_ids'])) {
$params['tag_id'] = explode(',', $params['xm_tag_ids']);
}
$params['deleted'] = $params['deleted'] ?? 0;
$sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage();
$limit = $pagerQuery->getLimit();
$articleRepo = new ArticleRepo();
$pager = $articleRepo->paginate($params, $sort, $page, $limit);
return $this->handleArticles($pager);
}
public function getArticle($id)
{
return $this->findOrFail($id);
}
public function createArticle()
{
$post = $this->request->getPost();
$loginUser = $this->getLoginUser();
$validator = new ArticleValidator();
$category = $validator->checkCategory($post['category_id']);
$title = $validator->checkTitle($post['title']);
$article = new ArticleModel();
$article->owner_id = $loginUser->id;
$article->category_id = $category->id;
$article->title = $title;
$article->create();
return $article;
}
public function updateArticle($id)
{
$post = $this->request->getPost();
$article = $this->findOrFail($id);
$validator = new ArticleValidator();
$data = [];
if (isset($post['category_id'])) {
$category = $validator->checkCategory($post['category_id']);
$data['category_id'] = $category->id;
}
if (isset($post['title'])) {
$data['title'] = $validator->checkTitle($post['title']);
}
if (isset($post['cover'])) {
$data['cover'] = $validator->checkCover($post['cover']);
}
if (isset($post['summary'])) {
$data['summary'] = $validator->checkSummary($post['summary']);
}
if (isset($post['content'])) {
$data['content'] = $validator->checkContent($post['content']);
$data['word_count'] = WordUtil::getWordCount($data['content']);
}
if (isset($post['source_type'])) {
$data['source_type'] = $validator->checkSourceType($post['source_type']);
if ($post['source_type'] != ArticleModel::SOURCE_ORIGIN) {
$data['source_url'] = $validator->checkSourceUrl($post['source_url']);
}
}
if (isset($post['allow_comment'])) {
$data['allow_comment'] = $post['allow_comment'];
}
if (isset($post['featured'])) {
$data['featured'] = $validator->checkFeatureStatus($post['featured']);
}
if (isset($post['published'])) {
$data['published'] = $validator->checkPublishStatus($post['published']);
}
if (isset($post['xm_tag_ids'])) {
$this->saveTags($article, $post['xm_tag_ids']);
}
$article->update($data);
return $article;
}
public function deleteArticle($id)
{
$article = $this->findOrFail($id);
$article->deleted = 1;
$article->update();
return $article;
}
public function restoreArticle($id)
{
$article = $this->findOrFail($id);
$article->deleted = 0;
$article->update();
return $article;
}
protected function findOrFail($id)
{
$validator = new ArticleValidator();
return $validator->checkArticle($id);
}
protected function rebuildArticleCache(ArticleModel $article)
{
$cache = new ArticleCache();
$cache->rebuild($article->id);
}
protected function rebuildArticleIndex(ArticleModel $article)
{
$sync = new ArticleIndexSync();
$sync->addItem($article->id);
}
protected function saveTags(ArticleModel $article, $tagIds)
{
$originTagIds = [];
if ($article->tags) {
$originTagIds = kg_array_column($article->tags, 'id');
}
$newTagIds = $tagIds ? explode(',', $tagIds) : [];
$addedTagIds = array_diff($newTagIds, $originTagIds);
if ($addedTagIds) {
foreach ($addedTagIds as $tagId) {
$articleTag = new ArticleTagModel();
$articleTag->article_id = $article->id;
$articleTag->tag_id = $tagId;
$articleTag->create();
}
}
$deletedTagIds = array_diff($originTagIds, $newTagIds);
if ($deletedTagIds) {
$articleTagRepo = new ArticleTagRepo();
foreach ($deletedTagIds as $tagId) {
$articleTag = $articleTagRepo->findArticleTag($article->id, $tagId);
if ($articleTag) {
$articleTag->delete();
}
}
}
$articleTags = [];
if ($newTagIds) {
$tagRepo = new TagRepo();
$tags = $tagRepo->findByIds($newTagIds);
if ($tags->count() > 0) {
$articleTags = [];
foreach ($tags as $tag) {
$articleTags[] = ['id' => $tag->id, 'name' => $tag->name];
}
}
}
$article->tags = $articleTags;
$article->update();
}
protected function handleArticles($pager)
{
if ($pager->total_items > 0) {
$builder = new ArticleListBuilder();
$items = $pager->items->toArray();
$pipeA = $builder->handleArticles($items);
$pipeB = $builder->handleCategories($pipeA);
$pipeC = $builder->handleUsers($pipeB);
$pipeD = $builder->objects($pipeC);
$pager->items = $pipeD;
}
return $pager;
}
}

View File

@ -243,6 +243,117 @@ class AuthNode extends Service
],
],
],
[
'id' => '1-7',
'title' => '文章管理',
'type' => 'menu',
'children' => [
[
'id' => '1-7-1',
'title' => '文章列表',
'type' => 'menu',
'route' => 'admin.article.list',
],
[
'id' => '1-7-2',
'title' => '搜索文章',
'type' => 'menu',
'route' => 'admin.article.search',
],
[
'id' => '1-7-3',
'title' => '添加文章',
'type' => 'menu',
'route' => 'admin.article.add',
],
[
'id' => '1-7-4',
'title' => '编辑文章',
'type' => 'button',
'route' => 'admin.article.edit',
],
[
'id' => '1-7-5',
'title' => '删除文章',
'type' => 'button',
'route' => 'admin.article.delete',
],
[
'id' => '1-7-6',
'title' => '文章分类',
'type' => 'menu',
'route' => 'admin.article.category',
],
],
],
[
'id' => '1-8',
'title' => '标签管理',
'type' => 'menu',
'children' => [
[
'id' => '1-8-1',
'title' => '标签列表',
'type' => 'menu',
'route' => 'admin.tag.list',
],
[
'id' => '1-8-2',
'title' => '搜索标签',
'type' => 'menu',
'route' => 'admin.tag.search',
],
[
'id' => '1-8-3',
'title' => '添加标签',
'type' => 'menu',
'route' => 'admin.tag.add',
],
[
'id' => '1-8-4',
'title' => '编辑标签',
'type' => 'button',
'route' => 'admin.tag.edit',
],
[
'id' => '1-8-5',
'title' => '删除标签',
'type' => 'button',
'route' => 'admin.tag.delete',
],
],
],
[
'id' => '1-9',
'title' => '评论管理',
'type' => 'button',
'children' => [
[
'id' => '1-9-1',
'title' => '评论列表',
'type' => 'button',
'route' => 'admin.comment.list',
],
[
'id' => '1-9-2',
'title' => '搜索评论',
'type' => 'button',
'route' => 'admin.comment.search',
],
[
'id' => '1-9-3',
'title' => '编辑评论',
'type' => 'button',
'route' => 'admin.comment.edit',
],
[
'id' => '1-9-4',
'title' => '删除评论',
'type' => 'button',
'route' => 'admin.comment.delete',
],
],
],
],
];
}

View File

@ -74,7 +74,6 @@ class Category extends Service
$data['type'] = $validator->checkType($post['type']);
$data['name'] = $validator->checkName($post['name']);
$data['priority'] = $validator->checkPriority($post['priority']);
$data['published'] = $validator->checkPublishStatus($post['published']);
$category = new CategoryModel();

View File

@ -0,0 +1,105 @@
<?php
namespace App\Http\Admin\Services;
use App\Builders\CommentList as CommentListBuilder;
use App\Library\Paginator\Query as PagerQuery;
use App\Repos\Comment as CommentRepo;
use App\Validators\Comment as CommentValidator;
class Comment extends Service
{
public function getComments()
{
$pagerQuery = new PagerQuery();
$params = $pagerQuery->getParams();
$params['deleted'] = $params['deleted'] ?? 0;
$sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage();
$limit = $pagerQuery->getLimit();
$commentRepo = new CommentRepo();
$pager = $commentRepo->paginate($params, $sort, $page, $limit);
return $this->handleComments($pager);
}
public function getComment($id)
{
return $this->findOrFail($id);
}
public function updateComment($id)
{
$comment = $this->findOrFail($id);
$post = $this->request->getPost();
$validator = new CommentValidator();
$data = [];
if (isset($post['content'])) {
$data['content'] = $validator->checkContent($post['content']);
}
if (isset($post['published'])) {
$data['published'] = $validator->checkPublishStatus($post['published']);
}
$comment->update($data);
return $comment;
}
public function deleteComment($id)
{
$page = $this->findOrFail($id);
$page->deleted = 1;
$page->update();
return $page;
}
public function restoreComment($id)
{
$page = $this->findOrFail($id);
$page->deleted = 0;
$page->update();
return $page;
}
protected function findOrFail($id)
{
$validator = new CommentValidator();
return $validator->checkComment($id);
}
protected function handleComments($pager)
{
if ($pager->total_items > 0) {
$builder = new CommentListBuilder();
$pipeA = $pager->items->toArray();
$pipeB = $builder->handleUsers($pipeA);
$pipeC = $builder->objects($pipeB);
$pager->items = $pipeC;
}
return $pager;
}
}

View File

@ -38,13 +38,11 @@ class Course extends Service
$params = $pagerQuery->getParams();
if (!empty($params['xm_category_ids'])) {
$xmCategoryIds = explode(',', $params['xm_category_ids']);
$params['category_id'] = count($xmCategoryIds) > 1 ? $xmCategoryIds : $xmCategoryIds[0];
$params['category_id'] = explode(',', $params['xm_category_ids']);
}
if (!empty($params['xm_teacher_ids'])) {
$xmTeacherIds = explode(',', $params['xm_teacher_ids']);
$params['teacher_id'] = count($xmTeacherIds) > 1 ? $xmTeacherIds : $xmTeacherIds[0];
$params['teacher_id'] = explode(',', $params['xm_teacher_ids']);
}
$params['deleted'] = $params['deleted'] ?? 0;
@ -288,7 +286,7 @@ class Course extends Service
$allCategories = $categoryRepo->findAll([
'type' => CategoryModel::TYPE_COURSE,
'deleted' => 0,
'published' => 1,
]);
if ($allCategories->count() == 0) return [];
@ -456,12 +454,11 @@ class Course extends Service
if ($addedTeacherIds) {
foreach ($addedTeacherIds as $teacherId) {
$courseTeacher = new CourseUserModel();
$courseTeacher->create([
'course_id' => $course->id,
'user_id' => $teacherId,
'role_type' => CourseUserModel::ROLE_TEACHER,
'source_type' => CourseUserModel::SOURCE_IMPORT,
]);
$courseTeacher->course_id = $course->id;
$courseTeacher->user_id = $teacherId;
$courseTeacher->role_type = CourseUserModel::ROLE_TEACHER;
$courseTeacher->source_type = CourseUserModel::SOURCE_IMPORT;
$courseTeacher->create();
}
}
@ -509,10 +506,9 @@ class Course extends Service
if ($addedCategoryIds) {
foreach ($addedCategoryIds as $categoryId) {
$courseCategory = new CourseCategoryModel();
$courseCategory->create([
'course_id' => $course->id,
'category_id' => $categoryId,
]);
$courseCategory->course_id = $course->id;
$courseCategory->category_id = $categoryId;
$courseCategory->create();
}
}
@ -568,18 +564,16 @@ class Course extends Service
$record = $courseRelatedRepo->findCourseRelated($course->id, $relatedId);
if (!$record) {
$courseRelated = new CourseRelatedModel();
$courseRelated->create([
'course_id' => $course->id,
'related_id' => $relatedId,
]);
$courseRelated->course_id = $course->id;
$courseRelated->related_id = $relatedId;
$courseRelated->create();
}
$record = $courseRelatedRepo->findCourseRelated($relatedId, $course->id);
if (!$record) {
$courseRelated = new CourseRelatedModel();
$courseRelated->create([
'course_id' => $relatedId,
'related_id' => $course->id,
]);
$courseRelated->course_id = $relatedId;
$courseRelated->related_id = $course->id;
$courseRelated->create();
}
}
}

View File

@ -64,7 +64,6 @@ class Help extends Service
$data['title'] = $validator->checkTitle($post['title']);
$data['content'] = $validator->checkContent($post['content']);
$data['priority'] = $validator->checkPriority($post['priority']);
$data['published'] = $validator->checkPublishStatus($post['published']);
$data['category_id'] = $category->id;
$help = new HelpModel();

View File

@ -72,7 +72,6 @@ class Nav extends Service
$data['url'] = $validator->checkUrl($post['url']);
$data['target'] = $validator->checkTarget($post['target']);
$data['position'] = $validator->checkPosition($post['position']);
$data['published'] = $validator->checkPublishStatus($post['published']);
$nav = new NavModel();

View File

@ -43,7 +43,6 @@ class Page extends Service
$data['title'] = $validator->checkTitle($post['title']);
$data['content'] = $validator->checkContent($post['content']);
$data['published'] = $validator->checkPublishStatus($post['published']);
$page = new PageModel();
@ -109,11 +108,11 @@ class Page extends Service
return $page;
}
protected function rebuildPageCache(PageModel $help)
protected function rebuildPageCache(PageModel $page)
{
$cache = new PageCache();
$cache->rebuild($help->id);
$cache->rebuild($page->id);
}
protected function findOrFail($id)

View File

@ -62,7 +62,7 @@ class Setting extends Service
{
$oa = $this->getSettings('wechat.oa');
$oa['notify_url'] = $oa['notify_url'] ?: kg_full_url(['for' => 'home.wechat.oa.notify']);
$oa['notify_url'] = $oa['notify_url'] ?: kg_full_url(['for' => 'home.wechat_oa.notify']);
$oa['menu'] = json_decode($oa['menu'], true);

View File

@ -63,8 +63,6 @@ class Student extends Service
$params = $pagerQuery->getParams();
$params['deleted'] = 0;
$sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage();
$limit = $pagerQuery->getLimit();

View File

@ -0,0 +1,126 @@
<?php
namespace App\Http\Admin\Services;
use App\Caches\Tag as TagCache;
use App\Library\Paginator\Query as PagerQuery;
use App\Models\Tag as TagModel;
use App\Repos\Tag as TagRepo;
use App\Validators\Tag as TagValidator;
class Tag extends Service
{
public function getTags()
{
$pagerQuery = new PagerQuery();
$params = $pagerQuery->getParams();
$params['deleted'] = $params['deleted'] ?? 0;
$sort = 'priority';
$page = $pagerQuery->getPage();
$limit = $pagerQuery->getLimit();
$tagRepo = new TagRepo();
return $tagRepo->paginate($params, $sort, $page, $limit);
}
public function getTag($id)
{
return $this->findOrFail($id);
}
public function createTag()
{
$post = $this->request->getPost();
$validator = new TagValidator();
$tag = new TagModel();
$tag->name = $validator->checkName($post['name']);
$tag->published = $validator->checkPublishStatus($post['published']);
$tag->create();
$this->rebuildTagCache($tag);
return $tag;
}
public function updateTag($id)
{
$tag = $this->findOrFail($id);
$post = $this->request->getPost();
$validator = new TagValidator();
$data = [];
if (isset($post['name'])) {
$data['name'] = $validator->checkName($post['name']);
if ($data['name'] != $tag->name) {
$validator->checkIfNameExists($data['name']);
}
}
if (isset($post['priority'])) {
$data['priority'] = $validator->checkPriority($post['priority']);
}
if (isset($post['published'])) {
$data['published'] = $validator->checkPublishStatus($post['published']);
}
$tag->update($data);
$this->rebuildTagCache($tag);
return $tag;
}
public function deleteTag($id)
{
$tag = $this->findOrFail($id);
$tag->deleted = 1;
$tag->update();
$this->rebuildTagCache($tag);
return $tag;
}
public function restoreTag($id)
{
$tag = $this->findOrFail($id);
$tag->deleted = 0;
$tag->update();
$this->rebuildTagCache($tag);
return $tag;
}
protected function rebuildTagCache(TagModel $tag)
{
$cache = new TagCache();
$cache->rebuild($tag->id);
}
protected function findOrFail($id)
{
$validator = new TagValidator();
return $validator->checkTag($id);
}
}

View File

@ -0,0 +1,35 @@
{% extends 'templates/main.volt' %}
{% block content %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.article.create'}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>添加文章</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">分类</label>
<div class="layui-input-block">
<select name="category_id" lay-verify="required">
<option value="">请选择</option>
{% for item in categories %}
<option value="{{ item.id }}">{{ item.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标题</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button id="kg-submit" class="layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,74 @@
{% extends 'templates/main.volt' %}
{% block content %}
<fieldset class="layui-elem-field layui-field-title">
<legend>编辑文章</legend>
</fieldset>
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title kg-tab-title">
<li class="layui-this">基本信息</li>
<li>文章内容</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
{{ partial('article/edit_basic') }}
</div>
<div class="layui-tab-item">
{{ partial('article/edit_desc') }}
</div>
</div>
</div>
{% endblock %}
{% block link_css %}
{{ css_link('https://cdn.jsdelivr.net/npm/vditor/dist/index.css', false) }}
{% endblock %}
{% block include_js %}
{{ js_include('https://cdn.jsdelivr.net/npm/vditor/dist/index.min.js', false) }}
{{ js_include('lib/xm-select.js') }}
{{ js_include('admin/js/cover.upload.js') }}
{{ js_include('admin/js/vditor.js') }}
{% endblock %}
{% block inline_js %}
<script>
layui.use(['jquery', 'form'], function () {
xmSelect.render({
el: '#xm-tag-ids',
name: 'xm_tag_ids',
max: 3,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_tags|json_encode }}
});
var $ = layui.jquery;
var form = layui.form;
form.on('radio(source_type)', function (data) {
var block = $('#source-url-block');
if (data.value === '1') {
block.hide();
} else {
block.show();
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,67 @@
{% set source_url_display = article.source_type == 1 ? 'display:none' : 'display:block' %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.article.update','id':article.id}) }}">
<div class="layui-form-item">
<label class="layui-form-label">封面</label>
<div class="layui-input-inline">
<img id="img-cover" class="kg-cover" src="{{ article.cover }}">
<input type="hidden" name="cover" value="{{ article.cover }}">
</div>
<div class="layui-input-inline" style="padding-top:35px;">
<button id="change-cover" class="layui-btn layui-btn-sm" type="button">更换</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标题</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" value="{{ article.title }}" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">分类</label>
<div class="layui-input-block">
<select name="category_id" lay-verify="required">
<option value="">请选择</option>
{% for item in categories %}
<option value="{{ item.id }}" {% if article.category_id == item.id %}selected="selected"{% endif %}>{{ item.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标签</label>
<div class="layui-input-block">
<div id="xm-tag-ids"></div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">来源类型</label>
<div class="layui-input-block">
{% for value,title in source_types %}
<input type="radio" name="source_type" value="{{ value }}" title="{{ title }}" {% if article.source_type == value %}checked="checked"{% endif %} lay-filter="source_type">
{% endfor %}
</div>
</div>
<div id="source-url-block" style="{{ source_url_display }}">
<div class="layui-form-item">
<label class="layui-form-label">来源网址</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="source_url" value="{{ article.source_url }}">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">允许评论</label>
<div class="layui-input-block">
<input type="radio" name="allow_comment" value="1" title="是" {% if article.allow_comment == 1 %}checked="checked"{% endif %}>
<input type="radio" name="allow_comment" value="0" title="否" {% if article.allow_comment == 0 %}checked="checked"{% endif %}>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>

View File

@ -0,0 +1,22 @@
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.article.update','id':article.id}) }}">
<div class="layui-form-item">
<label class="layui-form-label">详情</label>
<div class="layui-input-block">
<div id="vditor"></div>
<textarea name="content" class="layui-hide" id="vditor-textarea">{{ article.content }}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">简介</label>
<div class="layui-input-block">
<textarea name="summary" class="layui-textarea">{{ article.summary }}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="kg-submit layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>

View File

@ -0,0 +1,183 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/article') }}
{% set add_url = url({'for':'admin.article.add'}) %}
{% set search_url = url({'for':'admin.article.search'}) %}
<div class="kg-nav">
<div class="kg-nav-left">
<span class="layui-breadcrumb">
<a><cite>文章管理</cite></a>
</span>
</div>
<div class="kg-nav-right">
<a class="layui-btn layui-btn-sm" href="{{ add_url }}">
<i class="layui-icon layui-icon-add-1"></i>添加文章
</a>
<a class="layui-btn layui-btn-sm" href="{{ search_url }}">
<i class="layui-icon layui-icon-search"></i>搜索文章
</a>
</div>
</div>
<table class="layui-table kg-table layui-form">
<colgroup>
<col>
<col>
<col>
<col>
<col>
<col>
<col>
<col width="10%">
</colgroup>
<thead>
<tr>
<th>文章</th>
<th>来源</th>
<th>作者</th>
<th>统计</th>
<th>推荐</th>
<th>评论</th>
<th>发布</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in pager.items %}
{% set preview_url = url({'for':'home.article.show','id':item.id}) %}
{% set edit_url = url({'for':'admin.article.edit','id':item.id}) %}
{% set update_url = url({'for':'admin.article.update','id':item.id}) %}
{% set delete_url = url({'for':'admin.article.delete','id':item.id}) %}
{% set restore_url = url({'for':'admin.article.restore','id':item.id}) %}
{% set comment_url = url({'for':'admin.comment.list'},{'item_id':item.id,'item_type':2}) %}
<tr>
<td>
<p>标题:<a href="{{ edit_url }}">{{ item.title }}</a>{{ item.id }}</p>
<p class="meta">
{% if item.category.id is defined %}
<span>分类:{{ item.category.name }}</span>
{% endif %}
{% if item.tags %}
<span>标签:{{ tags_info(item.tags) }}</span>
{% endif %}
</p>
<p>创建:{{ date('Y-m-d H:i',item.create_time) }},更新:{{ date('Y-m-d H:i',item.update_time) }}</p>
</td>
<td>{{ source_info(item.source_type,item.source_url) }}</td>
<td>
<p>昵称:{{ item.owner.name }}</p>
<p>编号:{{ item.owner.id }}</p>
</td>
<td>
<p>浏览:{{ item.view_count }},评论:{{ item.comment_count }}</p>
<p>点赞:{{ item.like_count }},收藏:{{ item.favorite_count }}</p>
</td>
<td><input type="checkbox" name="featured" value="1" lay-skin="switch" lay-text="是|否" lay-filter="featured" data-url="{{ update_url }}" {% if item.featured == 1 %}checked="checked"{% endif %}></td>
<td><input type="checkbox" name="comment" value="1" lay-skin="switch" lay-text="开|关" lay-filter="comment" data-url="{{ update_url }}" {% if item.allow_comment == 1 %}checked="checked"{% endif %}></td>
<td><input type="checkbox" name="published" value="1" lay-skin="switch" lay-text="是|否" lay-filter="published" data-url="{{ update_url }}" {% if item.published == 1 %}checked="checked"{% endif %}></td>
<td class="center">
<div class="layui-dropdown">
<button class="layui-btn layui-btn-sm">操作 <i class="layui-icon layui-icon-triangle-d"></i></button>
<ul>
<li><a href="{{ preview_url }}" target="_blank">预览文章</a></li>
<li><a href="{{ edit_url }}">编辑文章</a></li>
{% if item.deleted == 0 %}
<li><a href="javascript:" class="kg-delete" data-url="{{ delete_url }}">删除文章</a></li>
{% else %}
<li><a href="javascript:" class="kg-restore" data-url="{{ restore_url }}">还原文章</a></li>
{% endif %}
<hr>
<li><a href="javascript:" class="kg-comment" data-url="{{ comment_url }}">评论管理</a></li>
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ partial('partials/pager') }}
{% endblock %}
{% block inline_js %}
<script>
layui.define(['jquery', 'form', 'layer'], function () {
var $ = layui.jquery;
var form = layui.form;
var layer = layui.layer;
form.on('switch(featured)', function (data) {
var checked = $(this).is(':checked');
var featured = checked ? 1 : 0;
var url = $(this).data('url');
var tips = featured === 1 ? '确定要推荐?' : '确定要取消推荐?';
layer.confirm(tips, function () {
$.ajax({
type: 'POST',
url: url,
data: {featured: featured},
success: function (res) {
layer.msg(res.msg, {icon: 1});
},
error: function (xhr) {
var json = JSON.parse(xhr.responseText);
layer.msg(json.msg, {icon: 2});
data.elem.checked = !checked;
form.render();
}
});
}, function () {
data.elem.checked = !checked;
form.render();
});
});
form.on('switch(comment)', function (data) {
var checked = $(this).is(':checked');
var allowComment = checked ? 1 : 0;
var url = $(this).data('url');
var tips = allowComment === 1 ? '确定要开启评论?' : '确定要关闭评论?';
layer.confirm(tips, function () {
$.ajax({
type: 'POST',
url: url,
data: {allow_comment: allowComment},
success: function (res) {
layer.msg(res.msg, {icon: 1});
},
error: function (xhr) {
var json = JSON.parse(xhr.responseText);
layer.msg(json.msg, {icon: 2});
data.elem.checked = !checked;
form.render();
}
});
}, function () {
data.elem.checked = !checked;
form.render();
});
});
$('.kg-comment').on('click', function () {
var url = $(this).data('url');
layer.open({
type: 2,
title: '评论管理',
area: ['1000px', '600px'],
content: url
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,104 @@
{% extends 'templates/main.volt' %}
{% block content %}
<form class="layui-form kg-form" method="GET" action="{{ url({'for':'admin.article.list'}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>搜索文章</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">文章编号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="id" placeholder="文章编号精确匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">作者编号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="owner_id" placeholder="作者编号精确匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标题</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" placeholder="标题模糊匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">分类</label>
<div class="layui-input-block">
<select name="category_id">
<option value="">请选择</option>
{% for item in categories %}
<option value="{{ item.id }}">{{ item.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标签</label>
<div class="layui-input-block">
<div id="xm-tag-ids"></div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">来源类型</label>
<div class="layui-input-block">
{% for value,title in source_types %}
<input type="radio" name="source_type" value="{{ value }}" title="{{ title }}">
{% endfor %}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">推荐</label>
<div class="layui-input-block">
<input type="radio" name="featured" value="1" title="是">
<input type="radio" name="featured" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
<input type="radio" name="published" value="1" title="是">
<input type="radio" name="published" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">删除</label>
<div class="layui-input-block">
<input type="radio" name="deleted" value="1" title="是">
<input type="radio" name="deleted" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn" lay-submit="true">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}
{% block include_js %}
{{ js_include('lib/xm-select.js') }}
{% endblock %}
{% block inline_js %}
<script>
xmSelect.render({
el: '#xm-tag-ids',
name: 'xm_tag_ids',
max: 5,
filterable: true,
data: {{ xm_tags|json_encode }}
});
</script>
{% endblock %}

View File

@ -31,13 +31,6 @@
<input class="layui-input" type="text" name="priority" value="10" lay-verify="number">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
<input type="radio" name="published" value="1" title="是" checked="checked">
<input type="radio" name="published" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">

View File

@ -4,7 +4,7 @@
{% set back_url = url({'for':'admin.category.list'},{'type':type}) %}
{% set add_url = url({'for':'admin.category.add'},{'type':type,'parent_id':parent.id}) %}
{% set allow_add = (type == 1 and parent.level < 2) or (type == 2 and parent.level < 1) %}
{% set allow_add = (type == 1 and parent.level < 2) or (type == 2 and parent.level < 1) or (type == 3 and parent.level < 1) %}
<div class="kg-nav">
<div class="kg-nav-left">
@ -55,7 +55,9 @@
<td>{{ item.id }}</td>
{% if item.type == 1 and item.level < 2 %}
<td><a href="{{ child_url }}">{{ item.name }}</a></td>
{% else %}
{% elseif item.type == 2 %}
<td><a href="{{ edit_url }}">{{ item.name }}</a></td>
{% elseif item.type == 3 %}
<td><a href="{{ edit_url }}">{{ item.name }}</a></td>
{% endif %}
<td>{{ item.level }}</td>

View File

@ -0,0 +1,98 @@
{% extends 'templates/main.volt' %}
{% block content %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.review.update','id':review.id}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>编辑评价</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">内容实用</label>
<div class="layui-input-block">
<div id="rating1" class="kg-rating"></div>
<input type="hidden" name="rating1" value="{{ review.rating1 }}"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">通俗易懂</label>
<div class="layui-input-block">
<div id="rating2" class="kg-rating"></div>
<input type="hidden" name="rating2" value="{{ review.rating2 }}"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">逻辑清晰</label>
<div class="layui-input-block">
<div id="rating3" class="kg-rating"></div>
<input type="hidden" name="rating3" value="{{ review.rating3 }}"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">课程评价</label>
<div class="layui-input-block">
<div class="layui-form-mid gray">{{ review.content }}</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
<input type="radio" name="published" value="1" title="是" {% if review.published == 1 %}checked="checked"{% endif %}>
<input type="radio" name="published" value="0" title="否" {% if review.published == 0 %}checked="checked"{% endif %}>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}
{% block inline_js %}
<script>
layui.use(['jquery', 'rate'], function () {
var $ = layui.jquery;
var rate = layui.rate;
var $rating1 = $('input[name=rating1]');
var $rating2 = $('input[name=rating2]');
var $rating3 = $('input[name=rating3]');
rate.render({
elem: '#rating1',
value: $rating1.val(),
readonly: true,
choose: function (value) {
$rating1.val(value);
}
});
rate.render({
elem: '#rating2',
value: $rating2.val(),
readonly: true,
choose: function (value) {
$rating2.val(value);
}
});
rate.render({
elem: '#rating3',
value: $rating3.val(),
readonly: true,
choose: function (value) {
$rating3.val(value);
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,63 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/common') }}
<table class="layui-table kg-table layui-form">
<colgroup>
<col>
<col>
<col>
<col>
<col width="10%">
</colgroup>
<thead>
<tr>
<th>评论</th>
<th>用户</th>
<th>终端</th>
<th>发布</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in pager.items %}
{% set update_url = url({'for':'admin.comment.update','id':item.id}) %}
{% set delete_url = url({'for':'admin.comment.delete','id':item.id}) %}
{% set restore_url = url({'for':'admin.comment.restore','id':item.id}) %}
<tr>
<td>
<p>内容:<a href="javascript:" title="{{ item.content }}">{{ substr(item.content,0,30) }}</a></p>
<p>时间:{{ date('Y-m-d H:i',item.create_time) }},点赞:{{ item.like_count }}</p>
</td>
<td>
<p>昵称:{{ item.owner.name }}</p>
<p>编号:{{ item.owner.id }}</p>
</td>
<td>
<p>类型:{{ client_type(item.client_type) }}</p>
<p>地址:<a href="javascript:" class="kg-ip2region" data-ip="{{ item.client_ip }}">查看</a></p>
</td>
<td><input type="checkbox" name="published" value="1" lay-skin="switch" lay-text="是|否" lay-filter="published" data-url="{{ update_url }}" {% if item.published == 1 %}checked="checked"{% endif %}></td>
<td class="center">
{% if item.deleted == 0 %}
<a href="javascript:" class="layui-badge layui-bg-red kg-delete" data-url="{{ delete_url }}">删除</a>
{% else %}
<a href="javascript:" class="layui-badge layui-bg-green kg-restore" data-url="{{ restore_url }}">还原</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ partial('partials/pager') }}
{% endblock %}
{% block include_js %}
{{ js_include('admin/js/ip2region.js') }}
{% endblock %}

View File

@ -0,0 +1,50 @@
{% extends 'templates/main.volt' %}
{% block content %}
<form class="layui-form kg-form" method="GET" action="{{ url({'for':'admin.review.list'}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>搜索评价</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">评价编号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="id" placeholder="评价编号精确匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">课程编号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="course_id" placeholder="课程编号精确匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户编号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="owner_id" placeholder="用户编号精确匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
<input type="radio" name="published" value="1" title="是">
<input type="radio" name="published" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">删除</label>
<div class="layui-input-block">
<input type="radio" name="deleted" value="1" title="是">
<input type="radio" name="deleted" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn" lay-submit="true">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -61,25 +61,34 @@
xmSelect.render({
el: '#xm-category-ids',
name: 'xm_category_ids',
filterable: true,
max: 5,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_categories|json_encode }}
});
xmSelect.render({
el: '#xm-teacher-ids',
name: 'xm_teacher_ids',
filterable: true,
max: 5,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_teachers|json_encode }}
});
xmSelect.render({
el: '#xm-course-ids',
name: 'xm_course_ids',
max: 10,
autoRow: true,
filterable: true,
max: 10,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_courses|json_encode }}
});

View File

@ -100,6 +100,9 @@
name: 'xm_category_ids',
max: 5,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_categories|json_encode }}
});
@ -108,6 +111,9 @@
name: 'xm_teacher_ids',
max: 5,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_teachers|json_encode }}
});

View File

@ -69,6 +69,9 @@
name: 'xm_course_id',
radio: true,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_courses|json_encode }}
});
@ -77,6 +80,9 @@
name: 'xm_package_id',
radio: true,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_packages|json_encode }}
});

View File

@ -0,0 +1,18 @@
{%- macro source_info(type,url) %}
{% if type == 1 %}
<span>原创</span>
{% elseif type == 2 %}
<a href="{{ url }}" target="_blank">转载</a>
{% elseif type == 3 %}
<span>翻译</span>
{% else %}
<span>N/A</span>
{% endif %}
{%- endmacro %}
{%- macro tags_info(items) %}
{% for item in items %}
{% set comma = loop.last ? '' : ',' %}
{{ item.name ~ comma }}
{% endfor %}
{%- endmacro %}

View File

@ -0,0 +1,13 @@
{%- macro client_type(value) %}
{% if value == 1 %}
PC
{% elseif value == 2 %}
H5
{% elseif value == 3 %}
APP
{% elseif value == 4 %}
小程序
{% else %}
N/A
{% endif %}
{%- endmacro %}

View File

@ -4,7 +4,7 @@
{% elseif value == 2 %}
直播
{% elseif value == 3 %}
专栏
图文
{% elseif value == 4 %}
面授
{% else %}

View File

@ -51,13 +51,6 @@
<input type="radio" name="target" value="_self" title="原窗口">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
<input type="radio" name="published" value="1" title="是" checked="checked">
<input type="radio" name="published" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">

View File

@ -79,8 +79,12 @@
xmSelect.render({
el: '#xm-course-ids',
name: 'xm_course_ids',
max: 15,
autoRow: true,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_courses|json_encode }}
});

View File

@ -61,6 +61,9 @@
name: 'xm_course_id',
radio: true,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_courses|json_encode }}
});

View File

@ -141,7 +141,6 @@
{% endblock %}
{% block inline_js %}
<script>

View File

@ -121,6 +121,10 @@
<td>群组头像</td>
<td>public/static/admin/img/default/group_cover.png</td>
</tr>
<tr>
<td>文章封面</td>
<td>public/static/admin/img/default/article_cover.png</td>
</tr>
<tr>
<td>课程封面</td>
<td>public/static/admin/img/default/course_cover.png</td>

View File

@ -75,6 +75,9 @@
name: 'xm_course_id',
radio: true,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_courses|json_encode }}
});
@ -83,6 +86,9 @@
name: 'xm_page_id',
radio: true,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_pages|json_encode }}
});

View File

@ -61,13 +61,13 @@
<tbody>
{% for item in pager.items %}
{% set learning_url = url({'for':'admin.student.learning'},{'course_id':item.course_id,'user_id':item.user_id,'plan_id':item.plan_id}) %}
{% set list_by_course_url = url({'for':'admin.student.list'},{'course_id':item.course.id}) %}
{% set list_by_user_url = url({'for':'admin.student.list'},{'user_id':item.user_id}) %}
{% set course_url = url({'for':'home.course.show','id':item.course.id}) %}
{% set user_url = url({'for':'home.user.show','id':item.user_id}) %}
{% set edit_url = url({'for':'admin.student.edit'},{'relation_id':item.id}) %}
<tr>
<td>
<p>课程:<a href="{{ list_by_course_url }}">{{ item.course.title }}</a>{{ item.course.id }}</p>
<p>学员:<a href="{{ list_by_user_url }}">{{ item.user.name }}</a>{{ item.user.id }}</p>
<p>课程:<a href="{{ course_url }}" target="_blank">{{ item.course.title }}</a>{{ item.course.id }}</p>
<p>学员:<a href="{{ user_url }}" target="_blank">{{ item.user.name }}</a>{{ item.user.id }}</p>
</td>
<td>
<p>进度:<a href="javascript:" class="kg-learning" title="学习记录" data-url="{{ learning_url }}">{{ item.progress }}%</a></p>

View File

@ -0,0 +1,30 @@
{% extends 'templates/main.volt' %}
{% block content %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.tag.create'}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>添加标签</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">名称</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="name" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">排序</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="priority" lay-verify="number">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="kg-submit layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends 'templates/main.volt' %}
{% block content %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.tag.update','id':tag2.id}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>编辑标签</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">名称</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="name" value="{{ tag2.name }}" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">排序</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="priority" value="{{ tag2.priority }}" lay-verify="number">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
<input type="radio" name="published" value="1" title="是" {% if tag2.published == 1 %}checked="checked"{% endif %}>
<input type="radio" name="published" value="0" title="否" {% if tag2.published == 0 %}checked="checked"{% endif %}>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="kg-submit layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,78 @@
{% extends 'templates/main.volt' %}
{% block content %}
{% set add_url = url({'for':'admin.tag.add'}) %}
{% set search_url = url({'for':'admin.tag.search'}) %}
<div class="kg-nav">
<div class="kg-nav-left">
<span class="layui-breadcrumb">
<a><cite>标签管理</cite></a>
</span>
</div>
<div class="kg-nav-right">
<a class="layui-btn layui-btn-sm" href="{{ add_url }}">
<i class="layui-icon layui-icon-add-1"></i>添加标签
</a>
<a class="layui-btn layui-btn-sm" href="{{ search_url }}">
<i class="layui-icon layui-icon-search"></i>搜索标签
</a>
</div>
</div>
<table class="layui-table kg-table layui-form">
<colgroup>
<col>
<col>
<col>
<col>
<col>
<col>
<col width="12%">
</colgroup>
<thead>
<tr>
<th>编号</th>
<th>名称</th>
<th>创建时间</th>
<th>更新时间</th>
<th>排序</th>
<th>发布</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in pager.items %}
{% set edit_url = url({'for':'admin.tag.edit','id':item.id}) %}
{% set update_url = url({'for':'admin.tag.update','id':item.id}) %}
{% set delete_url = url({'for':'admin.tag.delete','id':item.id}) %}
<tr>
<td>{{ item.id }}</td>
<td><a href="{{ edit_url }}">{{ item.name }}</a></td>
<td>{{ date('Y-m-d H:i',item.create_time) }}</td>
<td>{{ date('Y-m-d H:i',item.update_time) }}</td>
<td class="center"><input class="layui-input kg-priority" type="text" name="priority" title="数值越小排序越靠前" value="{{ item.priority }}" data-url="{{ update_url }}"></td>
<td><input type="checkbox" name="published" value="1" lay-skin="switch" lay-text="是|否" lay-filter="published" data-url="{{ update_url }}" {% if item.published == 1 %}checked="checked"{% endif %}>
</td>
<td class="center">
<div class="layui-dropdown">
<button class="layui-btn layui-btn-sm">操作 <i class="layui-icon layui-icon-triangle-d"></i></button>
<ul>
<li><a href="{{ edit_url }}">编辑</a></li>
{% if item.deleted == 0 %}
<li><a href="javascript:" class="kg-delete" data-url="{{ delete_url }}">删除</a></li>
{% else %}
<li><a href="javascript:" class="kg-restore" data-url="{{ restore_url }}">还原</a></li>
{% endif %}
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ partial('partials/pager') }}
{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends 'templates/main.volt' %}
{% block content %}
<form class="layui-form kg-form" method="GET" action="{{ url({'for':'admin.tag.list'}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>搜索标签</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">编号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="id" placeholder="编号精确匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">名称</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" placeholder="名称模糊匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
<input type="radio" name="published" value="1" title="是">
<input type="radio" name="published" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">删除</label>
<div class="layui-input-block">
<input type="radio" name="deleted" value="1" title="是">
<input type="radio" name="deleted" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn" lay-submit="true">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -50,8 +50,12 @@
xmSelect.render({
el: '#xm-course-ids',
name: 'xm_course_ids',
max: 15,
autoRow: true,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_courses|json_encode }}
});

View File

@ -61,9 +61,11 @@ class ChapterController extends Controller
{
$service = new ChapterLikeService();
$service->handle($id);
$data = $service->handle($id);
return $this->jsonSuccess();
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**

View File

@ -77,9 +77,11 @@ class ConsultController extends Controller
{
$service = new ConsultLikeService();
$service->handle($id);
$data = $service->handle($id);
return $this->jsonSuccess();
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**
@ -89,9 +91,11 @@ class ConsultController extends Controller
{
$service = new ConsultLikeService();
$service->handle($id);
$data = $service->handle($id);
return $this->jsonSuccess();
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -108,9 +108,11 @@ class CourseController extends Controller
{
$service = new CourseFavoriteService();
$service->handle($id);
$data = $service->handle($id);
return $this->jsonSuccess();
$msg = $data['action'] == 'do' ? '收藏成功' : '取消收藏成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**
@ -120,9 +122,11 @@ class CourseController extends Controller
{
$service = new CourseFavoriteService();
$service->handle($id);
$data = $service->handle($id);
return $this->jsonSuccess();
$msg = $data['action'] == 'do' ? '收藏成功' : '取消收藏成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -2,7 +2,7 @@
namespace App\Http\Api\Controllers;
use App\Services\Logic\Page\PageInfo as PageInfoService;
use App\Services\Logic\Page\ArticleInfo as PageInfoService;
/**
* @RoutePrefix("/api/page")

View File

@ -77,9 +77,11 @@ class ReviewController extends Controller
{
$service = new ReviewLikeService();
$service->handle($id);
$data = $service->handle($id);
return $this->jsonSuccess();
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**
@ -89,9 +91,11 @@ class ReviewController extends Controller
{
$service = new ReviewLikeService();
$service->handle($id);
$data = $service->handle($id);
return $this->jsonSuccess();
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -37,6 +37,7 @@ class UploadController extends Controller
*/
public function uploadImImageAction()
{
}
/**

View File

@ -0,0 +1,136 @@
<?php
namespace App\Http\Home\Controllers;
use App\Http\Home\Services\ArticleQuery as ArticleQueryService;
use App\Services\Logic\Article\ArticleFavorite as ArticleFavoriteService;
use App\Services\Logic\Article\ArticleInfo as ArticleInfoService;
use App\Services\Logic\Article\ArticleLike as ArticleLikeService;
use App\Services\Logic\Article\ArticleList as ArticleListService;
use App\Services\Logic\Article\CommentList as ArticleCommentListService;
use App\Services\Logic\Article\HotAuthorList as HotAuthorListService;
use App\Services\Logic\Article\RelatedList as ArticleRelatedListService;
use Phalcon\Mvc\View;
/**
* @RoutePrefix("/article")
*/
class ArticleController extends Controller
{
/**
* @Get("/list", name="home.article.list")
*/
public function listAction()
{
$service = new ArticleQueryService();
$categories = $service->handleCategories();
$sorts = $service->handleSorts();
$params = $service->getParams();
$this->seo->prependTitle('专栏');
$this->view->setVar('categories', $categories);
$this->view->setVar('sorts', $sorts);
$this->view->setVar('params', $params);
}
/**
* @Get("/pager", name="home.article.pager")
*/
public function pagerAction()
{
$service = new ArticleListService();
$pager = $service->handle();
$pager->target = 'article-list';
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->setVar('pager', $pager);
}
/**
* @Get("/hot/authors", name="home.article.hot_authors")
*/
public function hotAuthorsAction()
{
$service = new HotAuthorListService();
$authors = $service->handle();
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('article/hot_authors');
$this->view->setVar('authors', $authors);
}
/**
* @Get("/{id:[0-9]+}", name="home.article.show")
*/
public function showAction($id)
{
$service = new ArticleInfoService();
$article = $service->handle($id);
$this->seo->prependTitle($article['title']);
$this->view->setVar('article', $article);
}
/**
* @Get("/{id:[0-9]+}/related", name="home.article.related")
*/
public function relatedAction($id)
{
$service = new ArticleRelatedListService();
$articles = $service->handle($id);
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->setVar('articles', $articles);
}
/**
* @Get("/{id:[0-9]+}/comments", name="home.article.comments")
*/
public function commentsAction($id)
{
$service = new ArticleCommentListService();
$comments = $service->handle($id);
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->setVar('comments', $comments);
}
/**
* @Post("/{id:[0-9]+}/favorite", name="home.article.favorite")
*/
public function favoriteAction($id)
{
$service = new ArticleFavoriteService();
$data = $service->handle($id);
$msg = $data['action'] == 'do' ? '收藏成功' : '取消收藏成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**
* @Post("/{id:[0-9]+}/like", name="home.article.like")
*/
public function likeAction($id)
{
$service = new ArticleLikeService();
$data = $service->handle($id);
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -100,11 +100,11 @@ class ChapterController extends Controller
{
$service = new ChapterLikeService();
$like = $service->handle($id);
$data = $service->handle($id);
$msg = $like->deleted == 0 ? '点赞成功' : '取消点赞成功';
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['msg' => $msg]);
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**

View File

@ -140,11 +140,11 @@ class ConsultController extends Controller
{
$service = new ConsultLikeService();
$like = $service->handle($id);
$data = $service->handle($id);
$msg = $like->deleted == 0 ? '点赞成功' : '取消点赞成功';
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['msg' => $msg]);
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -178,11 +178,11 @@ class CourseController extends Controller
{
$service = new CourseFavoriteService();
$favorite = $service->handle($id);
$data = $service->handle($id);
$msg = $favorite->deleted == 0 ? '收藏成功' : '取消收藏成功';
$msg = $data['action'] == 'do' ? '收藏成功' : '取消收藏成功';
return $this->jsonSuccess(['msg' => $msg]);
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -3,7 +3,7 @@
namespace App\Http\Home\Controllers;
use App\Http\Home\Services\Index as IndexService;
use App\Services\Logic\Page\PageInfo as PageInfoService;
use App\Services\Logic\Page\ArticleInfo as PageInfoService;
/**
* @RoutePrefix("/page")

View File

@ -109,11 +109,11 @@ class ReviewController extends Controller
{
$service = new ReviewLikeService();
$like = $service->handle($id);
$data = $service->handle($id);
$msg = $like->deleted == 0 ? '点赞成功' : '取消点赞成功';
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['msg' => $msg]);
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Home\Controllers;
use App\Services\Logic\Search\Article as ArticleSearchService;
use App\Services\Logic\Search\Course as CourseSearchService;
use App\Services\Logic\Search\Group as GroupSearchService;
use App\Services\Logic\Search\User as UserSearchService;
@ -41,11 +42,14 @@ class SearchController extends Controller
/**
* @param string $type
* @return CourseSearchService|GroupSearchService|UserSearchService
* @return ArticleSearchService|CourseSearchService|GroupSearchService|UserSearchService
*/
protected function getSearchService($type)
{
switch ($type) {
case 'article':
$service = new ArticleSearchService();
break;
case 'group':
$service = new GroupSearchService();
break;

View File

@ -5,6 +5,7 @@ namespace App\Http\Home\Controllers;
use App\Repos\WeChatSubscribe as WeChatSubscribeRepo;
use App\Services\Logic\Account\OAuthProvider as OAuthProviderService;
use App\Services\Logic\User\Console\AccountInfo as AccountInfoService;
use App\Services\Logic\User\Console\ArticleList as ArticleListService;
use App\Services\Logic\User\Console\ConnectDelete as ConnectDeleteService;
use App\Services\Logic\User\Console\ConnectList as ConnectListService;
use App\Services\Logic\User\Console\ConsultList as ConsultListService;
@ -134,6 +135,19 @@ class UserConsoleController extends Controller
$this->view->setVar('pager', $pager);
}
/**
* @Get("/articles", name="home.uc.articles")
*/
public function articlesAction()
{
$service = new ArticleListService();
$pager = $service->handle();
$this->view->pick('user/console/articles');
$this->view->setVar('pager', $pager);
}
/**
* @Get("/favorites", name="home.uc.favorites")
*/

View File

@ -2,8 +2,8 @@
namespace App\Http\Home\Controllers;
use App\Services\Logic\User\ArticleList as UserArticleListService;
use App\Services\Logic\User\CourseList as UserCourseListService;
use App\Services\Logic\User\FavoriteList as UserFavoriteListService;
use App\Services\Logic\User\FriendList as UserFriendListService;
use App\Services\Logic\User\GroupList as UserGroupListService;
use App\Services\Logic\User\UserInfo as UserInfoService;
@ -46,18 +46,18 @@ class UserController extends Controller
}
/**
* @Get("/{id:[0-9]+}/favorites", name="home.user.favorites")
* @Get("/{id:[0-9]+}/articles", name="home.user.articles")
*/
public function favoritesAction($id)
public function articlesAction($id)
{
$service = new UserFavoriteListService();
$service = new UserArticleListService();
$pager = $service->handle($id);
$pager->target = 'tab-favorites';
$pager->target = 'tab-articles';
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('user/favorites');
$this->view->pick('user/articles');
$this->view->setVar('pager', $pager);
}

View File

@ -15,7 +15,7 @@ class VerifyController extends \Phalcon\Mvc\Controller
use ResponseTrait;
/**
* @Post("/sms/code", name="verify.sms_code")
* @Post("/sms/code", name="home.verify.sms_code")
*/
public function smsCodeAction()
{
@ -27,7 +27,7 @@ class VerifyController extends \Phalcon\Mvc\Controller
}
/**
* @Post("/email/code", name="verify.email_code")
* @Post("/email/code", name="home.verify.email_code")
*/
public function emailCodeAction()
{

View File

@ -0,0 +1,105 @@
<?php
namespace App\Http\Home\Services;
use App\Models\Article as ArticleModel;
use App\Models\Category as CategoryModel;
use App\Services\Category as CategoryService;
use App\Validators\ArticleQuery as ArticleQueryValidator;
class ArticleQuery extends Service
{
protected $baseUrl;
public function __construct()
{
$this->baseUrl = $this->url->get(['for' => 'home.article.list']);
}
public function handleCategories()
{
$params = $this->getParams();
if (isset($params['category_id'])) {
unset($params['category_id']);
}
$defaultItem = [
'id' => 'all',
'name' => '全部',
'url' => $this->baseUrl . $this->buildParams($params),
];
$result = [];
$result[] = $defaultItem;
$categoryService = new CategoryService();
$topCategories = $categoryService->getChildCategories(CategoryModel::TYPE_ARTICLE, 0);
foreach ($topCategories as $key => $category) {
$params['category_id'] = $category['id'];
$result[] = [
'id' => $category['id'],
'name' => $category['name'],
'url' => $this->baseUrl . $this->buildParams($params),
];
}
return $result;
}
public function handleSorts()
{
$params = $this->getParams();
$result = [];
$sorts = ArticleModel::sortTypes();
foreach ($sorts as $key => $value) {
$params['sort'] = $key;
$result[] = [
'id' => $key,
'name' => $value,
'url' => $this->baseUrl . $this->buildParams($params),
];
}
return $result;
}
public function getParams()
{
$query = $this->request->getQuery();
$params = [];
$validator = new ArticleQueryValidator();
if (isset($query['category_id']) && $query['category_id'] != 'all') {
$validator->checkCategory($query['category_id']);
$params['category_id'] = $query['category_id'];
}
if (isset($query['tag_id'])) {
$validator->checkTag($query['tag_id']);
$params['tag_id'] = $query['tag_id'];
}
if (isset($query['sort'])) {
$validator->checkSort($query['sort']);
$params['sort'] = $query['sort'];
}
return $params;
}
protected function buildParams($params)
{
return $params ? '?' . http_build_query($params) : '';
}
}

View File

@ -30,12 +30,10 @@ class CourseQuery extends Service
unset($params['sc']);
}
$baseUrl = $this->url->get(['for' => 'home.course.list']);
$defaultItem = [
'id' => 'all',
'name' => '全部',
'url' => $baseUrl . $this->buildParams($params),
'url' => $this->baseUrl . $this->buildParams($params),
];
$result = [];
@ -51,7 +49,7 @@ class CourseQuery extends Service
$result[] = [
'id' => $category['id'],
'name' => $category['name'],
'url' => $baseUrl . $this->buildParams($params),
'url' => $this->baseUrl . $this->buildParams($params),
];
}
@ -78,12 +76,10 @@ class CourseQuery extends Service
unset($params['sc']);
}
$baseUrl = $this->url->get(['for' => 'home.course.list']);
$defaultItem = [
'id' => 'all',
'name' => '全部',
'url' => $baseUrl . $this->buildParams($params),
'url' => $this->baseUrl . $this->buildParams($params),
];
$result = [];
@ -95,7 +91,7 @@ class CourseQuery extends Service
$result[] = [
'id' => $category['id'],
'name' => $category['name'],
'url' => $baseUrl . $this->buildParams($params),
'url' => $this->baseUrl . $this->buildParams($params),
];
}

View File

@ -0,0 +1,24 @@
{% if authors %}
<div class="layui-card">
<div class="layui-card-header">推荐作者</div>
<div class="layui-card-body">
<div class="sidebar-user-list">
{% for author in authors %}
{% set author.title = author.title ? author.title : '暂露头角' %}
{% set author_url = url({'for':'home.user.show','id':author.id}) %}
<div class="sidebar-user-card clearfix">
<div class="avatar">
<img src="{{ author.avatar }}!avatar_160" alt="{{ author.name }}">
</div>
<div class="info">
<div class="name layui-elip">
<a href="{{ author_url }}" title="{{ author.about }}" target="_blank">{{ author.name }}</a>
</div>
<div class="title layui-elip">{{ author.title }}</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,46 @@
{% extends 'templates/main.volt' %}
{% block content %}
{% set category_val = request.get('category_id','int','all') %}
{% set sort_val = request.get('sort','trim','latest') %}
{% set pager_url = url({'for':'home.article.pager'}, params) %}
{% set hot_author_url = url({'for':'home.article.hot_authors'}) %}
<div class="layout-main clearfix">
<div class="layout-content">
<div class="content-wrap wrap">
<div class="article-sort">
{% for sort in sorts %}
{% set class = sort_val == sort.id ? 'layui-btn layui-btn-xs' : 'none' %}
<a class="{{ class }}" href="{{ sort.url }}">{{ sort.name }}</a>
{% endfor %}
</div>
<div class="article-list" id="article-list" data-url="{{ pager_url }}"></div>
</div>
</div>
<div class="layout-sidebar">
<div class="sidebar">
<div class="layui-card">
<div class="layui-card-header">文章分类</div>
<div class="layui-card-body">
<ul class="article-cate-list">
{% for category in categories %}
{% set class = category_val == category.id ? 'active' : '' %}
<li class="{{ class }}"><a href="{{ category.url }}">{{ category.name }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="sidebar" id="hot-author-list" data-url="{{ hot_author_url }}"></div>
</div>
</div>
{% endblock %}
{% block include_js %}
{{ js_include('home/js/article.list.js') }}
{% endblock %}

View File

@ -0,0 +1,31 @@
{{ partial('macros/article') }}
{% if pager.total_pages > 0 %}
<div class="article-list clearfix">
{% for item in pager.items %}
{% set article_url = url({'for':'home.article.show','id':item.id}) %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
<div class="article-card clearfix">
<div class="cover">
<a href="{{ article_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">
</a>
</div>
<div class="info">
<div class="title layui-elip">
<a href="{{ article_url }}" target="_blank">{{ item.title }}</a>
</div>
<div class="summary">{{ item.summary }}</div>
<div class="meta">
<span class="owner"><a href="{{ owner_url }}">{{ item.owner.name }}</a></span>
<span class="view">{{ item.view_count }} 浏览</span>
<span class="comment">{{ item.comment_count }} 评论</span>
<span class="like">{{ item.like_count }} 点赞</span>
<span class="time">{{ item.create_time|time_ago }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
{{ partial('partials/pager_ajax') }}
{% endif %}

View File

@ -0,0 +1,20 @@
{% if articles %}
<div class="layui-card">
<div class="layui-card-header">相关文章</div>
<div class="layui-card-body">
<div class="sidebar-article-list">
{% for article in articles %}
{% set article_url = url({'for':'home.article.show','id':article.id}) %}
<div class="title">
<a href="{{ article_url }}" target="_blank">{{ article.title }}</a>
</div>
<div class="meta">
<span class="view">{{ article.view_count }} 浏览</span>
<span class="like">{{ article.like_count }} 点赞</span>
<span class="comment">{{ article.like_count }} 评论</span>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,141 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/article') }}
{% set list_url = url({'for':'home.article.list'}) %}
{% set category_url = url({'for':'home.article.list'},{'category_id':article.category.id}) %}
{% set owner_url = url({'for':'home.user.show','id':article.owner.id}) %}
{% set favorite_url = url({'for':'home.article.favorite','id':article.id}) %}
{% set like_url = url({'for':'home.article.like','id':article.id}) %}
{% set favorited_class = article.me.favorited ? 'layui-icon-star-fill' : 'layui-icon-star' %}
{% set liked_class = article.me.liked ? 'active' : '' %}
{% set article.owner.title = article.owner.title ? article.owner.title : '默默无名' %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/">首页</a>
<a href="{{ list_url }}">专栏</a>
<a href="{{ category_url }}">{{ article.category.name }}</a>
<a><cite>文章详情</cite></a>
</span>
<span class="share">
<a href="javascript:" title="分享到QQ空间"><i class="layui-icon layui-icon-login-qq icon-qq"></i></a>
<a href="javascript:" title="分享到微博"><i class="layui-icon layui-icon-login-weibo icon-weibo"></i></a>
<a href="javascript:" title="分享到微信"><i class="layui-icon layui-icon-login-wechat icon-wechat"></i></a>
</span>
</div>
<div class="layout-main clearfix">
<div class="action-sticky">
<div class="item">
<div class="icon" data-url="{{ like_url }}">
<i class="layui-icon layui-icon-praise icon-praise {{ liked_class }}"></i>
</div>
<div class="text">{{ article.like_count }}</div>
</div>
<div class="item">
<div class="icon icon-reply">
<i class="layui-icon layui-icon-reply-fill"></i>
</div>
<div class="text">{{ article.comment_count }}</div>
</div>
<div class="item">
<div class="icon" data-url="{{ favorite_url }}">
<i class="layui-icon layui-icon-star icon-star {{ favorited_class }}"></i>
</div>
<div class="text">{{ article.favorite_count }}</div>
</div>
</div>
<div class="layout-content">
<div class="article-info wrap">
<div class="title">{{ article.title }}</div>
<div class="meta">
<span class="source layui-badge layui-bg-green">{{ source_type(article.source_type) }}</span>
<span class="owner">
<a href="{{ owner_url }}">{{ article.owner.name }}</a>
</span>
<span class="view">{{ article.view_count }} 阅读</span>
<span class="word">{{ article.word_count }} 字数</span>
<span class="time">{{ article.create_time|time_ago }}</span>
</div>
<div class="content markdown-body">{{ article.content }}</div>
{% if article.tags %}
<div class="tags">
{% for item in article.tags %}
{% set url = url({'for':'home.article.list'},{'tag_id':item.id}) %}
<a href="{{ url }}" class="layui-btn layui-btn-xs">{{ item.name }}</a>
{% endfor %}
</div>
{% endif %}
{% if article.source_type == 1 %}
<div class="source-tips">本作品系原创,转载请注明出处</div>
{% elseif article.source_url %}
<div class="source-tips">
<a href="{{ article.source_url }}" target="_blank">阅读原文</a>
</div>
{% endif %}
</div>
<div class="comment-wrap" id="comment-wrap">
<div class="comment-form">
</div>
<div class="comment-list">
</div>
</div>
</div>
{% set related_article_url = url({'for':'home.article.related','id':article.id}) %}
<div class="layout-sidebar">
<div class="sidebar">
<div class="layui-card">
<div class="layui-card-header">关于作者</div>
<div class="layui-card-body">
<div class="sidebar-user-card clearfix">
<div class="avatar">
<img src="{{ article.owner.avatar }}!avatar_160" alt="{{ article.owner.name }}">
</div>
<div class="info">
<div class="name layui-elip">
<a href="{{ owner_url }}" title="{{ article.owner.about }}">{{ article.owner.name }}</a>
</div>
<div class="title layui-elip">{{ article.owner.title }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="sidebar" id="related-article-list" data-url="{{ related_article_url }}"></div>
</div>
</div>
{% set share_url = full_url({'for':'home.article.show','id':article.id}) %}
{% set qrcode_url = url({'for':'home.qrcode'},{'text':share_url}) %}
<div class="layui-hide">
<input type="hidden" name="share.title" value="{{ article.title }}">
<input type="hidden" name="share.pic" value="{{ article.cover }}">
<input type="hidden" name="share.url" value="{{ share_url }}">
<input type="hidden" name="share.qrcode" value="{{ qrcode_url }}">
</div>
{% endblock %}
{% block link_css %}
{{ css_link('home/css/markdown.css') }}
{% endblock %}
{% block include_js %}
{{ js_include('home/js/article.show.js') }}
{{ js_include('home/js/article.share.js') }}
{% endblock %}

View File

@ -7,7 +7,7 @@
{% set like_url = url({'for':'home.consult.like','id':item.id}) %}
<div class="review-card clearfix">
<div class="avatar">
<a href="{{ owner_url }}" title="{{ item.owner.name }}">
<a href="{{ owner_url }}" title="{{ item.owner.name }}" target="_blank">
<img src="{{ item.owner.avatar }}!avatar_160" alt="{{ item.owner.name }}">
</a>
</div>

View File

@ -23,7 +23,7 @@
{% set course_url = url({'for':'home.course.show','id':course.id}) %}
<div class="package-course-card">
<div class="cover"><img src="{{ course.cover }}!cover_270" alt="{{ course.title }}"></div>
<div class="title"><a href="{{ course_url }}">{{ course.title }}</a></div>
<div class="title"><a href="{{ course_url }}" target="_blank">{{ course.title }}</a></div>
</div>
{% if loop.first %}
<div class="separator"><i class="layui-icon layui-icon-add-1"></i></div>

View File

@ -7,14 +7,14 @@
{% set like_url = url({'for':'home.review.like','id':item.id}) %}
<div class="review-card clearfix">
<div class="avatar">
<a href="{{ owner_url }}" title="{{ item.owner.name }}">
<a href="{{ owner_url }}" title="{{ item.owner.name }}" target="_blank">
<img src="{{ item.owner.avatar }}!avatar_160" alt="{{ item.owner.name }}">
</a>
</div>
<div class="info">
<div class="rating">{{ star_info(item.rating) }}</div>
<div class="user">
<a href="{{ owner_url }}">{{ item.owner.name }}</a>
<a href="{{ owner_url }}" target="_blank">{{ item.owner.name }}</a>
</div>
<div class="content">{{ item.content }}</div>
<div class="footer">

View File

@ -12,7 +12,7 @@
</div>
<div class="info">
<div class="name layui-elip">
<a href="{{ teacher_url }}" title="{{ teacher.about }}">{{ teacher.name }}</a>
<a href="{{ teacher_url }}" title="{{ teacher.about }}" target="_blank">{{ teacher.name }}</a>
</div>
<div class="title layui-elip">{{ teacher.title }}</div>
</div>

View File

@ -4,7 +4,7 @@
<div class="layui-card-body">
{% for topic in topics %}
{% set topic_url = url({'for':'home.topic.show','id':topic.id}) %}
<a class="layui-badge-rim topic-badge" href="{{ topic_url }}">{{ topic.title }}</a>
<a class="layui-badge-rim topic-badge" href="{{ topic_url }}" target="_blank">{{ topic.title }}</a>
{% endfor %}
</div>
</div>

View File

@ -20,7 +20,7 @@
<ul class="help-list">
{% for help in item.helps %}
{% set show_url = url({'for':'home.help.show','id':help.id}) %}
<li><a href="{{ show_url }}"><i class="layui-icon layui-icon-right"></i>{{ help.title }}</a></li>
<li><a href="{{ show_url }}" target="_blank"><i class="layui-icon layui-icon-right"></i>{{ help.title }}</a></li>
{% endfor %}
</ul>
</div>

View File

@ -11,7 +11,7 @@
</div>
<div class="info">
<div class="name layui-elip">
<a href="{{ user_url }}" title="{{ user.about }}">{{ user.name }}</a>
<a href="{{ user_url }}" title="{{ user.about }}" target="_blank">{{ user.name }}</a>
</div>
<div class="title layui-elip">{{ user.title }}</div>
</div>

View File

@ -10,12 +10,12 @@
<div class="user-card">
<span class="type layui-badge layui-bg-green">{{ type_info(item.type) }}</span>
<div class="avatar">
<a href="{{ group_url }}" title="{{ item.about }}">
<a href="{{ group_url }}" title="{{ item.about }}" target="_blank">
<img src="{{ item.avatar }}!avatar_160" alt="{{ item.name }}">
</a>
</div>
<div class="name layui-elip">
<a href="{{ group_url }}" title="{{ item.name }}">{{ item.name }}</a>
<a href="{{ group_url }}" title="{{ item.name }}" target="_blank">{{ item.name }}</a>
</div>
<div class="meta layui-elip">
<span>成员:{{ item.user_count }}</span>

View File

@ -10,7 +10,7 @@
</div>
<div class="info">
<div class="name layui-elip">
<a href="{{ owner_url }}" title="{{ group.owner.about }}">{{ group.owner.name }}</a>
<a href="{{ owner_url }}" title="{{ group.owner.about }}" target="_blank">{{ group.owner.name }}</a>
</div>
<div class="title layui-elip">{{ group.owner.title }}</div>
</div>

View File

@ -7,12 +7,12 @@
<div class="layui-col-md3">
<div class="user-card">
<div class="{{ avatar_class }}">
<a href="{{ user_url }}" title="{{ item.user.about }}">
<a href="{{ user_url }}" title="{{ item.user.about }}" target="_blank">
<img src="{{ item.user.avatar }}" alt="{{ item.user.name }}">
</a>
</div>
<div class="name layui-elip">
<a href="{{ user_url }}" title="{{ item.user.about }}">{{ item.user.name }}</a>
<a href="{{ user_url }}" title="{{ item.user.about }}" target="_blank">{{ item.user.name }}</a>
</div>
<div class="title layui-elip">{{ item.user.title }}</div>
<div class="action">

View File

@ -7,12 +7,12 @@
<div class="user-card">
<span class="type layui-badge layui-bg-green">{{ type_info(group.type) }}</span>
<div class="avatar">
<a href="{{ group_url }}" title="{{ group.about }}" target="group">
<a href="{{ group_url }}" title="{{ group.about }}" target="_blank">
<img src="{{ group.avatar }}!avatar_160" alt="{{ group.name }}">
</a>
</div>
<div class="name layui-elip">
<a href="{{ group_url }}" title="{{ group.name }}" target="group">{{ group.name }}</a>
<a href="{{ group_url }}" title="{{ group.name }}" target="_blank">{{ group.name }}</a>
</div>
<div class="meta layui-elip">
<span>成员:{{ group.user_count }}</span>

View File

@ -8,12 +8,12 @@
<div class="layui-col-md3">
<div class="user-card">
<div class="{{ avatar_class }}">
<a href="{{ user_url }}" title="{{ user.about }}" target="user">
<a href="{{ user_url }}" title="{{ user.about }}" target="_blank">
<img src="{{ user.avatar }}!avatar_160" alt="{{ user.name }}">
</a>
</div>
<div class="name layui-elip">
<a href="{{ user_url }}" title="{{ user.about }}" target="user">{{ user.name }}</a>
<a href="{{ user_url }}" title="{{ user.about }}" target="_blank">{{ user.name }}</a>
</div>
<div class="title layui-elip">{{ user.title }}</div>
<div class="action">

View File

@ -0,0 +1,11 @@
{%- macro source_type(type) %}
{% if type == 1 %}
原创
{% elseif type == 2 %}
转载
{% elseif type == 3 %}
翻译
{% else %}
未知
{% endif %}
{%- endmacro %}

Some files were not shown because too many files have changed in this diff Show More