1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-07-15 21:02:20 +08:00

v1.3.4阶段提交

This commit is contained in:
koogua 2021-05-07 19:34:31 +08:00
parent 73a2e21cd6
commit caa34d2675
190 changed files with 8820 additions and 455 deletions

View File

@ -0,0 +1,70 @@
<?php
namespace App\Builders;
use App\Repos\Question as QuestionRepo;
use App\Repos\User as UserRepo;
class AnswerList extends Builder
{
public function handleQuestions(array $answers)
{
$questions = $this->getQuestions($answers);
foreach ($answers as $key => $answer) {
$answers[$key]['question'] = $questions[$answer['question_id']] ?? new \stdClass();
}
return $answers;
}
public function handleUsers(array $answers)
{
$users = $this->getUsers($answers);
foreach ($answers as $key => $answer) {
$answers[$key]['owner'] = $users[$answer['owner_id']] ?? new \stdClass();
}
return $answers;
}
public function getQuestions(array $answers)
{
$ids = kg_array_column($answers, 'question_id');
$questionRepo = new QuestionRepo();
$questions = $questionRepo->findByIds($ids, ['id', 'title']);
$result = [];
foreach ($questions->toArray() as $question) {
$result[$question['id']] = $question;
}
return $result;
}
public function getUsers(array $answers)
{
$ids = kg_array_column($answers, '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,53 @@
<?php
namespace App\Builders;
use App\Repos\User as UserRepo;
class QuestionList extends Builder
{
public function handleQuestions(array $questions)
{
foreach ($questions as $key => $question) {
$questions[$key]['tags'] = json_decode($question['tags'], true);
}
return $questions;
}
public function handleUsers(array $questions)
{
$users = $this->getUsers($questions);
foreach ($questions as $key => $question) {
$questions[$key]['owner'] = $users[$question['owner_id']] ?? new \stdClass();
$questions[$key]['last_replier'] = $users[$question['last_replier_id']] ?? new \stdClass();
}
return $questions;
}
public function getUsers($questions)
{
$ownerIds = kg_array_column($questions, 'owner_id');
$lastReplierIds = kg_array_column($questions, 'last_replier_id');
$ids = array_merge($ownerIds, $lastReplierIds);
$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,75 @@
<?php
namespace App\Builders;
use App\Repos\Tag as TagRepo;
use App\Repos\User as UserRepo;
class TagFollowList extends Builder
{
public function handleTags(array $relations)
{
$tags = $this->getTags($relations);
foreach ($relations as $key => $value) {
$relations[$key]['tag'] = $tags[$value['tag_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 getTags(array $relations)
{
$ids = kg_array_column($relations, 'tag_id');
$tagRepo = new TagRepo();
$columns = ['id', 'name', 'alias', 'icon', 'follow_count'];
$tags = $tagRepo->findByIds($ids, $columns);
$baseUrl = kg_cos_url();
$result = [];
foreach ($tags->toArray() as $tag) {
$tag['icon'] = $baseUrl . $tag['icon'];
$result[$tag['id']] = $tag;
}
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,114 @@
<?php
namespace App\Caches;
use App\Models\Question as QuestionModel;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class HotQuestionList extends Cache
{
protected $lifetime = 1 * 86400;
public function getLifetime()
{
$tomorrow = strtotime('tomorrow');
return $tomorrow - time();
}
public function getKey($id = null)
{
return 'hot_question_list';
}
public function getContent($id = null)
{
$questions = $this->findWeeklyHotQuestions();
if ($questions->count() > 0) {
return $this->handleQuestions($questions);
}
$questions = $this->findMonthlyHotQuestions();
if ($questions->count() > 0) {
return $this->handleQuestions($questions);
}
$questions = $this->findYearlyHotQuestions();
if ($questions->count() > 0) {
return $this->handleQuestions($questions);
}
return [];
}
/**
* @param QuestionModel[] $questions
* @return array
*/
protected function handleQuestions($questions)
{
$result = [];
foreach ($questions as $question) {
$result[] = [
'id' => $question->id,
'title' => $question->title,
];
}
return $result;
}
/**
* @param int $limit
* @return ResultsetInterface|Resultset|QuestionModel[]
*/
protected function findWeeklyHotQuestions($limit = 10)
{
$createTime = strtotime('monday this week');
return $this->findHotQuestions($createTime, $limit);
}
/**
* @param int $limit
* @return ResultsetInterface|Resultset|QuestionModel[]
*/
protected function findMonthlyHotQuestions($limit = 10)
{
$createTime = strtotime(date('Y-m-01'));
return $this->findHotQuestions($createTime, $limit);
}
/**
* @param int $limit
* @return ResultsetInterface|Resultset|QuestionModel[]
*/
protected function findYearlyHotQuestions($limit = 10)
{
$createTime = strtotime(date('Y-01-01'));
return $this->findHotQuestions($createTime, $limit);
}
/**
* @param int $createTime
* @param int $limit
* @return ResultsetInterface|Resultset|QuestionModel[]
*/
protected function findHotQuestions($createTime, $limit = 10)
{
return QuestionModel::query()
->where('create_time > :create_time:', ['create_time' => $createTime])
->orderBy('score DESC')
->limit($limit)
->execute();
}
}

View File

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

View File

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

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

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

View File

@ -5,11 +5,9 @@ namespace App\Caches;
use App\Models\Article as ArticleModel;
use App\Repos\Article as ArticleRepo;
class ArticleRelatedList extends Cache
class TaggedArticleList extends Cache
{
protected $articleId;
protected $limit = 5;
protected $lifetime = 1 * 86400;
@ -21,25 +19,15 @@ class ArticleRelatedList extends Cache
public function getKey($id = null)
{
return "article_related_list:{$id}";
return "tagged_article_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],
'tag_id' => $id,
'published' => ArticleModel::PUBLISH_APPROVED,
];
@ -61,7 +49,7 @@ class ArticleRelatedList extends Cache
$count = 0;
foreach ($articles as $article) {
if ($article->id != $this->articleId && $count < $this->limit) {
if ($count < $this->limit) {
$result[] = [
'id' => $article->id,
'title' => $article->title,

View File

@ -0,0 +1,69 @@
<?php
namespace App\Caches;
use App\Models\Question as QuestionModel;
use App\Repos\Question as QuestionRepo;
class TaggedQuestionList extends Cache
{
protected $limit = 5;
protected $lifetime = 1 * 86400;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return "tagged_question_list:{$id}";
}
public function getContent($id = null)
{
$questionRepo = new QuestionRepo();
$where = [
'tag_id' => $id,
'published' => QuestionModel::PUBLISH_APPROVED,
];
$pager = $questionRepo->paginate($where);
if ($pager->total_items == 0) return [];
return $this->handleContent($pager->items);
}
/**
* @param QuestionModel[] $questions
* @return array
*/
public function handleContent($questions)
{
$result = [];
$count = 0;
foreach ($questions as $question) {
if ($count < $this->limit) {
$result[] = [
'id' => $question->id,
'title' => $question->title,
'view_count' => $question->view_count,
'like_count' => $question->like_count,
'answer_count' => $question->answer_count,
'favorite_count' => $question->favorite_count,
];
$count++;
}
}
return $result;
}
}

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 TopAnswererList extends Cache
{
protected $lifetime = 1 * 86400;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return 'question_top_answerer_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

@ -118,7 +118,7 @@ class ArticleIndexTask extends Task
protected function findArticles()
{
return ArticleModel::query()
->where('published = 1')
->where('published = :published:', ['published' => ArticleModel::PUBLISH_APPROVED])
->execute();
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\Console\Tasks;
use App\Models\Question as QuestionModel;
use App\Services\Search\QuestionDocument;
use App\Services\Search\QuestionSearcher;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class QuestionIndexTask extends Task
{
/**
* 搜索测试
*
* @command: php console.php question_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->searchQuestions($query);
var_export($result);
}
/**
* 清空索引
*
* @command: php console.php question_index clean
*/
public function cleanAction()
{
$this->cleanQuestionIndex();
}
/**
* 重建索引
*
* @command: php console.php question_index rebuild
*/
public function rebuildAction()
{
$this->rebuildQuestionIndex();
}
/**
* 清空索引
*/
protected function cleanQuestionIndex()
{
$handler = new QuestionSearcher();
$index = $handler->getXS()->getIndex();
echo '------ start clean question index ------' . PHP_EOL;
$index->clean();
echo '------ end clean question index ------' . PHP_EOL;
}
/**
* 重建索引
*/
protected function rebuildQuestionIndex()
{
$questions = $this->findQuestions();
if ($questions->count() == 0) return;
$handler = new QuestionSearcher();
$documenter = new QuestionDocument();
$index = $handler->getXS()->getIndex();
echo '------ start rebuild question index ------' . PHP_EOL;
$index->beginRebuild();
foreach ($questions as $question) {
$document = $documenter->setDocument($question);
$index->add($document);
}
$index->endRebuild();
echo '------ end rebuild question index ------' . PHP_EOL;
}
/**
* 搜索文章
*
* @param string $query
* @return array
* @throws \XSException
*/
protected function searchQuestions($query)
{
$handler = new QuestionSearcher();
return $handler->search($query);
}
/**
* 查找文章
*
* @return ResultsetInterface|Resultset|QuestionModel[]
*/
protected function findQuestions()
{
return QuestionModel::query()
->where('published = :published:', ['published' => QuestionModel::PUBLISH_APPROVED])
->execute();
}
}

View File

@ -2,6 +2,7 @@
namespace App\Console\Tasks;
use App\Models\Article as ArticleModel;
use App\Repos\Article as ArticleRepo;
use App\Services\Search\ArticleDocument;
use App\Services\Search\ArticleSearcher;
@ -38,7 +39,7 @@ class SyncArticleIndexTask extends Task
$doc = $document->setDocument($article);
if ($article->published == 1) {
if ($article->published == ArticleModel::PUBLISH_APPROVED) {
$index->update($doc);
} else {
$index->del($article->id);

View File

@ -0,0 +1,44 @@
<?php
namespace App\Console\Tasks;
use App\Repos\Article as ArticleRepo;
use App\Services\Sync\ArticleScore as ArticleScoreSync;
use App\Services\Utils\ArticleScore as ArticleScoreService;
class SyncArticleScoreTask 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;
$service = new ArticleScoreService();
foreach ($articles as $article) {
$service->handle($article);
}
$redis->sRem($key, ...$articleIds);
}
protected function getSyncKey()
{
$sync = new ArticleScoreSync();
return $sync->getSyncKey();
}
}

View File

@ -3,8 +3,8 @@
namespace App\Console\Tasks;
use App\Repos\Course as CourseRepo;
use App\Services\CourseStat as CourseStatService;
use App\Services\Sync\CourseScore as CourseScoreSync;
use App\Services\Utils\CourseScore as CourseScoreService;
class SyncCourseScoreTask extends Task
{
@ -25,10 +25,10 @@ class SyncCourseScoreTask extends Task
if ($courses->count() == 0) return;
$statService = new CourseStatService();
$service = new CourseScoreService();
foreach ($courses as $course) {
$statService->updateScore($course->id);
$service->handle($course);
}
$redis->sRem($key, ...$courseIds);

View File

@ -0,0 +1,61 @@
<?php
namespace App\Console\Tasks;
use App\Models\Question as QuestionModel;
use App\Repos\Question as QuestionRepo;
use App\Services\Search\QuestionDocument;
use App\Services\Search\QuestionSearcher;
use App\Services\Sync\QuestionIndex as QuestionIndexSync;
class SyncQuestionIndexTask extends Task
{
public function mainAction()
{
$redis = $this->getRedis();
$key = $this->getSyncKey();
$questionIds = $redis->sRandMember($key, 1000);
if (!$questionIds) return;
$questionRepo = new QuestionRepo();
$questions = $questionRepo->findByIds($questionIds);
if ($questions->count() == 0) return;
$document = new QuestionDocument();
$handler = new QuestionSearcher();
$index = $handler->getXS()->getIndex();
$index->openBuffer();
foreach ($questions as $question) {
$doc = $document->setDocument($question);
if ($question->published == QuestionModel::PUBLISH_APPROVED) {
$index->update($doc);
} else {
$index->del($question->id);
}
}
$index->closeBuffer();
$redis->sRem($key, ...$questionIds);
}
protected function getSyncKey()
{
$sync = new QuestionIndexSync();
return $sync->getSyncKey();
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Console\Tasks;
use App\Repos\Question as QuestionRepo;
use App\Services\Sync\QuestionScore as QuestionScoreSync;
use App\Services\Utils\QuestionScore as QuestionScoreService;
class SyncQuestionScoreTask extends Task
{
public function mainAction()
{
$redis = $this->getRedis();
$key = $this->getSyncKey();
$questionIds = $redis->sRandMember($key, 1000);
if (!$questionIds) return;
$questionRepo = new QuestionRepo();
$questions = $questionRepo->findByIds($questionIds);
if ($questions->count() == 0) return;
$service = new QuestionScoreService();
foreach ($questions as $question) {
$service->handle($question);
}
$redis->sRem($key, ...$questionIds);
}
protected function getSyncKey()
{
$sync = new QuestionScoreSync();
return $sync->getSyncKey();
}
}

View File

@ -53,6 +53,27 @@ class UploadController extends Controller
return $this->jsonSuccess(['data' => $data]);
}
/**
* @Post("/icon/img", name="admin.upload.icon_img")
*/
public function uploadIconImageAction()
{
$service = new StorageService();
$file = $service->uploadIconImage();
if (!$file) {
return $this->jsonError(['msg' => '上传文件失败']);
}
$data = [
'src' => $service->getImageUrl($file->path),
'title' => $file->name,
];
return $this->jsonSuccess(['data' => $data]);
}
/**
* @Post("/cover/img", name="admin.upload.cover_img")
*/

View File

@ -542,6 +542,12 @@ class AuthNode extends Service
'type' => 'menu',
'route' => 'admin.slide.list',
],
[
'id' => '2-5-5',
'title' => '搜索轮播',
'type' => 'menu',
'route' => 'admin.slide.search',
],
[
'id' => '2-5-2',
'title' => '添加轮播',
@ -560,12 +566,6 @@ class AuthNode extends Service
'type' => 'button',
'route' => 'admin.slide.delete',
],
[
'id' => '2-5-5',
'title' => '搜索轮播',
'type' => 'menu',
'route' => 'admin.slide.search',
],
],
],
[

View File

@ -19,7 +19,7 @@ class Tag extends Service
$params['deleted'] = $params['deleted'] ?? 0;
$sort = 'priority';
$sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage();
$limit = $pagerQuery->getLimit();
@ -43,7 +43,6 @@ class Tag extends Service
$tag->name = $validator->checkName($post['name']);
$tag->priority = $validator->checkPriority($post['priority']);
$tag->published = $validator->checkPublishStatus($post['published']);
$tag->create();
@ -69,6 +68,10 @@ class Tag extends Service
}
}
if (isset($post['icon'])) {
$data['icon'] = $validator->checkIcon($post['icon']);
}
if (isset($post['priority'])) {
$data['priority'] = $validator->checkPriority($post['priority']);
}

View File

@ -12,19 +12,6 @@
<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">
<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

@ -7,15 +7,19 @@
<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">
<label class="layui-form-label">图标</label>
<div class="layui-input-inline" style="width: 80px;">
<img id="img-icon" class="kg-icon" src="{{ tag2.icon }}">
<input type="hidden" name="icon" value="{{ tag2.icon }}">
</div>
<div class="layui-input-inline" style="padding-top:15px;">
<button id="change-icon" class="layui-btn layui-btn-sm" type="button">更换</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">排序</label>
<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">
<input class="layui-input" type="text" name="name" value="{{ tag2.name }}" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
@ -35,3 +39,9 @@
</form>
{% endblock %}
{% block include_js %}
{{ js_include('admin/js/icon.upload.js') }}
{% endblock %}

View File

@ -29,15 +29,17 @@
<col>
<col>
<col>
<col>
<col width="12%">
</colgroup>
<thead>
<tr>
<th>编号</th>
<th>图标</th>
<th>名称</th>
<th>关注数</th>
<th>创建时间</th>
<th>更新时间</th>
<th>排序</th>
<th>发布</th>
<th>操作</th>
</tr>
@ -49,10 +51,11 @@
{% set delete_url = url({'for':'admin.tag.delete','id':item.id}) %}
<tr>
<td>{{ item.id }}</td>
<td><img class="kg-icon" src="{{ item.icon }}" alt="{{ item.name }}"></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>{{ item.follow_count }}</td>
<td>{{ date('Y-m-d',item.create_time) }}</td>
<td>{{ date('Y-m-d',item.update_time) }}</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">

View File

@ -0,0 +1,126 @@
<?php
namespace App\Http\Home\Controllers;
use App\Http\Home\Services\Answer as AnswerService;
use App\Services\Logic\Answer\AnswerAccept as AnswerAcceptService;
use App\Services\Logic\Answer\AnswerCreate as AnswerCreateService;
use App\Services\Logic\Answer\AnswerDelete as AnswerDeleteService;
use App\Services\Logic\Answer\AnswerInfo as AnswerInfoService;
use App\Services\Logic\Answer\AnswerLike as AnswerLikeService;
use App\Services\Logic\Answer\AnswerUpdate as AnswerUpdateService;
/**
* @RoutePrefix("/answer")
*/
class AnswerController extends Controller
{
/**
* @Get("/add", name="home.answer.add")
*/
public function addAction()
{
}
/**
* @Get("/{id:[0-9]+}/edit", name="home.answer.edit")
*/
public function editAction($id)
{
$service = new AnswerService();
$answer = $service->getAnswer($id);
$this->view->setVar('answer', $answer);
}
/**
* @Get("/{id:[0-9]+}", name="home.answer.show")
*/
public function showAction($id)
{
$service = new AnswerInfoService();
$answer = $service->handle($id);
$this->view->setVar('answer', $answer);
}
/**
* @Post("/create", name="home.answer.create")
*/
public function createAction()
{
$service = new AnswerCreateService();
$service->handle();
$content = ['msg' => '创建答案成功'];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/update", name="home.answer.update")
*/
public function updateAction($id)
{
$service = new AnswerUpdateService();
$service->handle($id);
$content = ['msg' => '更新答案成功'];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/delete", name="home.answer.delete")
*/
public function deleteAction($id)
{
$service = new AnswerDeleteService();
$service->handle($id);
$location = $this->url->get(['for' => 'home.uc.answers']);
$content = [
'location' => $location,
'msg' => '删除答案成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/like", name="home.answer.like")
*/
public function likeAction($id)
{
$service = new AnswerLikeService();
$data = $service->handle($id);
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**
* @Post("/{id:[0-9]+}/accept", name="home.answer.accept")
*/
public function acceptAction($id)
{
$service = new AnswerAcceptService();
$data = $service->handle($id);
$msg = $data['action'] == 'do' ? '采纳成功' : '取消采纳成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -8,9 +8,8 @@ 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 App\Services\Logic\Article\RelatedArticleList as RelatedArticleListService;
use Phalcon\Mvc\View;
/**
@ -78,7 +77,7 @@ class ArticleController extends Controller
$article = $service->getArticleModel();
$xmTags = $service->getXmTags(0);
$this->seo->prependTitle('写文章');
$this->seo->prependTitle('写文章');
$this->view->pick('article/edit');
$this->view->setVar('source_types', $sourceTypes);
@ -132,7 +131,7 @@ class ArticleController extends Controller
*/
public function relatedAction($id)
{
$service = new ArticleRelatedListService();
$service = new RelatedArticleListService();
$articles = $service->handle($id);
@ -140,19 +139,6 @@ class ArticleController extends Controller
$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("/create", name="home.article.create")
*/
@ -200,8 +186,10 @@ class ArticleController extends Controller
$service->deleteArticle($id);
$location = $this->url->get(['for' => 'home.uc.articles']);
$content = [
'location' => $this->request->getHTTPReferer(),
'location' => $location,
'msg' => '删除文章成功',
];
@ -236,5 +224,4 @@ class ArticleController extends Controller
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -6,7 +6,6 @@ use App\Models\ChapterLive as LiveModel;
use App\Models\Course as CourseModel;
use App\Services\Logic\Chapter\ChapterInfo as ChapterInfoService;
use App\Services\Logic\Chapter\ChapterLike as ChapterLikeService;
use App\Services\Logic\Chapter\DanmuList as ChapterDanmuListService;
use App\Services\Logic\Chapter\Learning as ChapterLearningService;
use App\Services\Logic\Chapter\ResourceList as ChapterResourceListService;
use App\Services\Logic\Course\BasicInfo as CourseInfoService;
@ -30,18 +29,6 @@ class ChapterController extends Controller
$this->view->setVar('items', $items);
}
/**
* @Get("/{id:[0-9]+}/danmus", name="home.chapter.danmus")
*/
public function danmusAction($id)
{
$service = new ChapterDanmuListService();
$items = $service->handle($id);
return $this->jsonSuccess(['items' => $items]);
}
/**
* @Get("/{id:[0-9]+}", name="home.chapter.show")
*/

View File

@ -63,26 +63,6 @@ class CommentController extends Controller
$this->view->setVar('comment', $comment);
}
/**
* @Get("/add", name="home.comment.add")
*/
public function addAction()
{
}
/**
* @Get("/{id:[0-9]+}/reply", name="home.comment.reply")
*/
public function replyAction($id)
{
$service = new CommentInfoService();
$comment = $service->handle($id);
$this->view->setVar('comment', $comment);
}
/**
* @Post("/create", name="home.comment.create")
*/
@ -100,9 +80,9 @@ class CommentController extends Controller
}
/**
* @Post("/{id:[0-9]+}/reply", name="home.comment.create_reply")
* @Post("/{id:[0-9]+}/reply", name="home.comment.reply")
*/
public function createReplyAction($id)
public function replyAction($id)
{
$service = new CommentReplyService();

View File

@ -0,0 +1,252 @@
<?php
namespace App\Http\Home\Controllers;
use App\Http\Home\Services\Question as QuestionService;
use App\Http\Home\Services\QuestionQuery as QuestionQueryService;
use App\Services\Logic\Question\AnswerList as AnswerListService;
use App\Services\Logic\Question\HotQuestionList as HotQuestionListService;
use App\Services\Logic\Question\QuestionCreate as QuestionCreateService;
use App\Services\Logic\Question\QuestionDelete as QuestionDeleteService;
use App\Services\Logic\Question\QuestionFavorite as QuestionFavoriteService;
use App\Services\Logic\Question\QuestionInfo as QuestionInfoService;
use App\Services\Logic\Question\QuestionLike as QuestionLikeService;
use App\Services\Logic\Question\QuestionList as QuestionListService;
use App\Services\Logic\Question\QuestionUpdate as QuestionUpdateService;
use App\Services\Logic\Question\RelatedQuestionList as RelatedQuestionListService;
use App\Services\Logic\Question\TopAnswererList as TopAnswererListService;
use Phalcon\Mvc\View;
/**
* @RoutePrefix("/question")
*/
class QuestionController extends Controller
{
/**
* @Get("/hot/questions", name="home.question.hot_questions")
*/
public function hotQuestionsAction()
{
$service = new HotQuestionListService();
$questions = $service->handle();
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('question/hot_questions');
$this->view->setVar('questions', $questions);
}
/**
* @Get("/top/answerers", name="home.question.top_answerers")
*/
public function topAnswerersAction()
{
$service = new TopAnswererListService();
$answerers = $service->handle();
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('question/top_answerers');
$this->view->setVar('answerers', $answerers);
}
/**
* @Get("/list", name="home.question.list")
*/
public function listAction()
{
$service = new QuestionQueryService();
$sorts = $service->handleSorts();
$params = $service->getParams();
$this->seo->prependTitle('问答');
$this->view->setVar('sorts', $sorts);
$this->view->setVar('params', $params);
}
/**
* @Get("/pager", name="home.question.pager")
*/
public function pagerAction()
{
$service = new QuestionListService();
$pager = $service->handle();
$pager->target = 'question-list';
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->setVar('pager', $pager);
}
/**
* @Get("/add", name="home.question.add")
*/
public function addAction()
{
$service = new QuestionService();
$question = $service->getQuestionModel();
$xmTags = $service->getXmTags(0);
$this->seo->prependTitle('提问题');
$this->view->pick('question/edit');
$this->view->setVar('question', $question);
$this->view->setVar('xm_tags', $xmTags);
}
/**
* @Get("/{id:[0-9]+}/edit", name="home.question.edit")
*/
public function editAction($id)
{
$service = new QuestionService();
$question = $service->getQuestion($id);
$xmTags = $service->getXmTags($id);
$this->seo->prependTitle('编辑问题');
$this->view->setVar('question', $question);
$this->view->setVar('xm_tags', $xmTags);
}
/**
* @Get("/{id:[0-9]+}", name="home.question.show")
*/
public function showAction($id)
{
$service = new QuestionInfoService();
$question = $service->handle($id);
$this->seo->prependTitle($question['title']);
$this->view->setVar('question', $question);
}
/**
* @Get("/{id:[0-9]+}/answers", name="home.question.answers")
*/
public function answersAction($id)
{
$service = new QuestionService();
$question = $service->getQuestion($id);
$service = new AnswerListService();
$pager = $service->handle($id);
$pager->target = 'answer-list';
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->setVar('question', $question);
$this->view->setVar('pager', $pager);
}
/**
* @Get("/{id:[0-9]+}/related", name="home.question.related")
*/
public function relatedAction($id)
{
$service = new RelatedQuestionListService();
$questions = $service->handle($id);
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->setVar('questions', $questions);
}
/**
* @Post("/create", name="home.question.create")
*/
public function createAction()
{
$service = new QuestionCreateService();
$service->handle();
$location = $this->url->get(['for' => 'home.uc.questions']);
$content = [
'location' => $location,
'msg' => '创建问题成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/update", name="home.question.update")
*/
public function updateAction($id)
{
$service = new QuestionUpdateService();
$service->handle($id);
$location = $this->url->get(['for' => 'home.uc.questions']);
$content = [
'location' => $location,
'msg' => '更新问题成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/delete", name="home.question.delete")
*/
public function deleteAction($id)
{
$service = new QuestionDeleteService();
$service->handle($id);
$location = $this->url->get(['for' => 'home.uc.questions']);
$content = [
'location' => $location,
'msg' => '删除问题成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/favorite", name="home.question.favorite")
*/
public function favoriteAction($id)
{
$service = new QuestionFavoriteService();
$data = $service->handle($id);
$msg = $data['action'] == 'do' ? '收藏成功' : '取消收藏成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**
* @Post("/{id:[0-9]+}/like", name="home.question.like")
*/
public function likeAction($id)
{
$service = new QuestionLikeService();
$data = $service->handle($id);
$msg = $data['action'] == 'do' ? '点赞成功' : '取消点赞成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

View File

@ -5,6 +5,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\Question as QuestionSearchService;
use App\Services\Logic\Search\User as UserSearchService;
/**
@ -42,7 +43,7 @@ class SearchController extends Controller
/**
* @param string $type
* @return ArticleSearchService|CourseSearchService|GroupSearchService|UserSearchService
* @return ArticleSearchService|QuestionSearchService|CourseSearchService|GroupSearchService|UserSearchService
*/
protected function getSearchService($type)
{
@ -50,6 +51,9 @@ class SearchController extends Controller
case 'article':
$service = new ArticleSearchService();
break;
case 'question':
$service = new QuestionSearchService();
break;
case 'group':
$service = new GroupSearchService();
break;

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Home\Controllers;
use App\Services\Logic\Tag\TagFollow as TagFollowService;
use App\Services\Logic\Tag\TagList as TagListService;
use App\Services\Logic\Tag\FollowList as FollowListService;
use Phalcon\Mvc\View;
/**
* @RoutePrefix("/tag")
*/
class TagController extends Controller
{
/**
* @Get("/list", name="home.tag.list")
*/
public function listAction()
{
$this->seo->prependTitle('标签');
}
/**
* @Get("/list/pager", name="home.tag.list_pager")
*/
public function listPagerAction()
{
$service = new TagListService();
$pager = $service->handle();
$pager->target = 'all-tag-list';
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('tag/list_pager');
$this->view->setVar('pager', $pager);
}
/**
* @Get("/my/pager", name="home.tag.my_pager")
*/
public function myPagerAction()
{
$service = new FollowListService();
$pager = $service->handle();
$pager->target = 'my-tag-list';
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('tag/my_pager');
$this->view->setVar('pager', $pager);
}
/**
* @Post("/{id:[0-9]+}/follow", name="home.tag.follow")
*/
public function followAction($id)
{
$service = new TagFollowService();
$data = $service->handle($id);
$msg = $data['action'] == 'do' ? '关注成功' : '取消关注成功';
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
}

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\AnswerList as AnswerListService;
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;
@ -24,6 +25,7 @@ use App\Services\Logic\User\Console\PointHistory as PointHistoryService;
use App\Services\Logic\User\Console\PointRedeemList as PointRedeemListService;
use App\Services\Logic\User\Console\ProfileInfo as ProfileInfoService;
use App\Services\Logic\User\Console\ProfileUpdate as ProfileUpdateService;
use App\Services\Logic\User\Console\QuestionList as QuestionListService;
use App\Services\Logic\User\Console\RefundList as RefundListService;
use App\Services\Logic\User\Console\ReviewList as ReviewListService;
use Phalcon\Mvc\Dispatcher;
@ -154,6 +156,32 @@ class UserConsoleController extends Controller
$this->view->setVar('pager', $pager);
}
/**
* @Get("/questions", name="home.uc.questions")
*/
public function questionsAction()
{
$service = new QuestionListService();
$pager = $service->handle();
$this->view->pick('user/console/questions');
$this->view->setVar('pager', $pager);
}
/**
* @Get("/answers", name="home.uc.answers")
*/
public function answersAction()
{
$service = new AnswerListService();
$pager = $service->handle();
$this->view->pick('user/console/answers');
$this->view->setVar('pager', $pager);
}
/**
* @Get("/favorites", name="home.uc.favorites")
*/

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Home\Controllers;
use App\Services\Logic\Question\HotQuestionList as HotQuestionListService;
use App\Services\Logic\Question\TopAnswererList as TopAnswererListService;
use App\Services\Logic\Tag\FollowList as FollowListService;
use Phalcon\Mvc\View;
/**
* @RoutePrefix("/widget")
*/
class WidgetController extends Controller
{
/**
* @Get("/my/tags", name="home.widget.my_tags")
*/
public function myTagsAction()
{
$service = new FollowListService();
$pager = $service->handle();
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('widget/my_tags');
$this->view->setVar('tags', $pager->items);
}
/**
* @Get("/hot/questions", name="home.widget.hot_questions")
*/
public function hotQuestionsAction()
{
$service = new HotQuestionListService();
$questions = $service->handle();
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('widget/hot_questions');
$this->view->setVar('questions', $questions);
}
/**
* @Get("/top/answerers", name="home.widget.top_answerers")
*/
public function topAnswerersAction()
{
$service = new TopAnswererListService();
$answerers = $service->handle();
$this->view->setRenderLevel(View::LEVEL_ACTION_VIEW);
$this->view->pick('widget/top_answerers');
$this->view->setVar('answerers', $answerers);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Home\Services;
use App\Models\Answer as AnswerModel;
use App\Services\Logic\AnswerTrait;
use App\Services\Logic\Service as LogicService;
class Answer extends LogicService
{
use AnswerTrait;
public function getAnswerModel()
{
return new AnswerModel();
}
public function getAnswer($id)
{
return $this->checkAnswer($id);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Http\Home\Services;
use App\Models\Question as QuestionModel;
use App\Repos\Tag as TagRepo;
use App\Services\Logic\QuestionTrait;
use App\Services\Logic\Service as LogicService;
class Question extends LogicService
{
use QuestionTrait;
public function getQuestionModel()
{
$question = new QuestionModel();
$question->afterFetch();
return $question;
}
public function getQuestion($id)
{
return $this->checkQuestion($id);
}
public function getXmTags($id)
{
$tagRepo = new TagRepo();
$allTags = $tagRepo->findAll(['published' => 1], 'priority');
if ($allTags->count() == 0) return [];
$questionTagIds = [];
if ($id > 0) {
$question = $this->checkQuestion($id);
if (!empty($question->tags)) {
$questionTagIds = kg_array_column($question->tags, 'id');
}
}
$list = [];
foreach ($allTags as $tag) {
$selected = in_array($tag->id, $questionTagIds);
$list[] = [
'name' => $tag->name,
'value' => $tag->id,
'selected' => $selected,
];
}
return $list;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Http\Home\Services;
use App\Models\Question as QuestionModel;
use App\Validators\QuestionQuery as QuestionQueryValidator;
class QuestionQuery extends Service
{
protected $baseUrl;
public function __construct()
{
$this->baseUrl = $this->url->get(['for' => 'home.question.list']);
}
public function handleSorts()
{
$params = $this->getParams();
$result = [];
$sorts = QuestionModel::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 QuestionQueryValidator();
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

@ -0,0 +1,34 @@
{% extends 'templates/layer.volt' %}
{% block content %}
<form class="layui-form" method="POST" action="{{ url({'for':'home.answer.create'}) }}">
<div class="layui-form-item">
<div class="layui-input-block" style="margin:0;">
<div id="vditor"></div>
<textarea name="content" class="layui-hide" id="vditor-textarea"></textarea>
</div>
</div>
<div class="layui-form-item center">
<div class="layui-input-block" style="margin:0;">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="add_answer">提交回答</button>
<input type="hidden" name="question_id" value="{{ request.get('question_id') }}">
</div>
</div>
</form>
{% 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('home/js/answer.js') }}
{{ js_include('home/js/vditor.js') }}
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends 'templates/layer.volt' %}
{% block content %}
<form class="layui-form" method="POST" action="{{ url({'for':'home.answer.update','id':answer.id}) }}">
<div class="layui-form-item">
<div class="layui-input-block" style="margin:0;">
<div id="vditor"></div>
<textarea name="content" class="layui-hide" id="vditor-textarea">{{ answer.content }}</textarea>
</div>
</div>
<div class="layui-form-item center">
<div class="layui-input-block" style="margin:0;">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="edit_answer">提交回答</button>
</div>
</div>
</form>
{% 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('home/js/answer.js') }}
{{ js_include('home/js/vditor.js') }}
{% endblock %}

View File

@ -9,7 +9,7 @@
<div class="footer" id="comment-footer" style="display:none;">
<div class="toolbar"></div>
<div class="action">
<button class="{{ submit_class }}" lay-submit="true" lay-filter="addComment">发布</button>
<button class="{{ submit_class }}" lay-submit="true" lay-filter="add_comment">发布</button>
<button class="layui-btn layui-btn-sm layui-bg-gray" id="btn-cancel-comment" type="button">取消</button>
</div>
</div>

View File

@ -2,7 +2,7 @@
{% block content %}
{% set title = article.id > 0 ? '编辑文章' : '写文章' %}
{% set title = article.id > 0 ? '编辑文章' : '写文章' %}
{% set action_url = article.id > 0 ? url({'for':'home.article.update','id':article.id}) : url({'for':'home.article.create'}) %}
{% set source_url_display = article.source_type == 1 ? 'display:none' : 'display:block' %}
@ -30,8 +30,7 @@
</div>
<div class="layui-form-item center">
<div class="layui-input-block">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="go">提交</button>
<button class="kg-back layui-btn layui-btn-primary" type="button">返回</button>
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="go">发布文章</button>
</div>
</div>
</div>
@ -41,25 +40,6 @@
<div class="layui-card-header">基本信息</div>
<div class="layui-card-body">
<div class="writer-sidebar">
<div class="layui-form-item cover-wrap">
<label class="layui-form-label">封面</label>
<div class="layui-input-block">
<img id="img-cover" class="cover" src="{{ article.cover }}!cover_270">
<input type="hidden" name="cover" value="{{ article.cover }}">
</div>
<button id="change-cover" class="layui-btn layui-btn-sm btn-change" type="button">更换</button>
</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">

View File

@ -1,4 +1,4 @@
{% if authors %}
{% if authors|length > 0 %}
<div class="layui-card">
<div class="layui-card-header">推荐作者</div>
<div class="layui-card-body">

View File

@ -2,10 +2,10 @@
{% 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'}) %}
{% set hot_authors_url = url({'for':'home.article.hot_authors'}) %}
{% set my_tags_url = url({'for':'home.widget.my_tags'},{'type':'article'}) %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
@ -17,30 +17,26 @@
<div class="layout-main clearfix">
<div class="layout-content">
<div class="content-wrap wrap">
<div class="article-sort">
<div class="layui-tab layui-tab-brief search-tab">
<ul class="layui-tab-title">
{% 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>
{% set class = sort_val == sort.id ? 'layui-this' : 'none' %}
<li class="{{ class }}"><a href="{{ sort.url }}">{{ sort.name }}</a></li>
{% endfor %}
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div id="article-list" data-url="{{ pager_url }}"></div>
</div>
</div>
</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>
{% if auth_user.id > 0 %}
<div class="sidebar" id="sidebar-my-tags" data-url="{{ my_tags_url }}"></div>
{% endif %}
<div class="sidebar" id="sidebar-hot-authors" data-url="{{ hot_authors_url }}"></div>
</div>
</div>

View File

@ -1,16 +1,11 @@
{{ partial('macros/article') }}
{% if pager.total_pages > 0 %}
<div class="article-list clearfix">
<div class="article-list">
{% 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="article-card">
<div class="info">
<div class="title layui-elip">
<a href="{{ article_url }}" target="_blank">{{ item.title }}</a>
@ -18,12 +13,19 @@
<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>
<span class="view">{{ item.view_count }} 浏览</span>
<span class="like">{{ item.like_count }} 点赞</span>
<span class="comment">{{ item.comment_count }} 评论</span>
</div>
</div>
{% if item.cover %}
<div class="cover">
<a href="{{ article_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">
</a>
</div>
{% endif %}
</div>
{% endfor %}
</div>

View File

@ -11,7 +11,7 @@
<div class="meta">
<span class="view">{{ article.view_count }} 浏览</span>
<span class="like">{{ article.like_count }} 点赞</span>
<span class="comment">{{ article.like_count }} 评论</span>
<span class="comment">{{ article.comment_count }} 评论</span>
</div>
{% endfor %}
</div>

View File

@ -5,14 +5,14 @@
{{ partial('macros/article') }}
{% set article_list_url = url({'for':'home.article.list'}) %}
{% set related_article_url = url({'for':'home.article.related','id':article.id}) %}
{% set related_url = url({'for':'home.article.related','id':article.id}) %}
{% set owner_url = url({'for':'home.user.show','id':article.owner.id}) %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/">首页</a>
<a href="{{ article_list_url }}">专栏</a>
<a><cite>文章详情</cite></a>
<a><cite>详情</cite></a>
</span>
<span class="share">
<a href="javascript:" title="分享到微信"><i class="layui-icon layui-icon-login-wechat icon-wechat"></i></a>
@ -82,7 +82,7 @@
</div>
</div>
</div>
<div class="sidebar" id="related-article-list" data-url="{{ related_article_url }}"></div>
<div class="sidebar" id="sidebar-related" data-url="{{ related_url }}"></div>
</div>
</div>

View File

@ -8,7 +8,7 @@
<div class="footer" id="comment-footer" style="display:none;">
<div class="toolbar"></div>
<div class="action">
<button class="layui-btn layui-btn-sm" lay-submit="true" lay-filter="addComment">发布</button>
<button class="layui-btn layui-btn-sm" lay-submit="true" lay-filter="add_comment">发布</button>
<button class="layui-btn layui-btn-sm layui-bg-gray" id="btn-cancel-comment" type="button">取消</button>
</div>
</div>

View File

@ -3,10 +3,10 @@
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set like_url = url({'for':'home.comment.like','id':item.id}) %}
{% set delete_url = url({'for':'home.comment.delete','id':item.id}) %}
{% set reply_create_url = url({'for':'home.comment.create_reply','id':item.id}) %}
{% set reply_url = url({'for':'home.comment.reply','id':item.id}) %}
{% set reply_list_url = url({'for':'home.comment.replies','id':item.id},{'limit':5}) %}
<div class="comment-box" id="comment-{{ item.id }}">
<div class="comment-card clearfix">
<div class="comment-card">
<div class="avatar">
<a href="{{ owner_url }}" title="{{ item.owner.name }}" target="_blank">
<img src="{{ item.owner.avatar }}!avatar_160" alt="{{ item.owner.name }}">
@ -52,12 +52,12 @@
</div>
</div>
<div class="comment-form" id="comment-form-{{ item.id }}" style="display:none;">
<form class="layui-form" method="post" action="{{ reply_create_url }}">
<form class="layui-form" method="post" action="{{ reply_url }}">
<textarea class="layui-textarea" name="content" placeholder="撰写评论..." lay-verify="required"></textarea>
<div class="footer">
<div class="toolbar"></div>
<div class="action">
<button class="layui-btn layui-btn-sm" lay-submit="true" lay-filter="replyComment" data-comment-id="{{ item.id }}" data-parent-id="{{ item.parent_id }}">发布</button>
<button class="layui-btn layui-btn-sm" lay-submit="true" lay-filter="reply_comment" data-comment-id="{{ item.id }}" data-parent-id="{{ item.parent_id }}">发布</button>
<button class="layui-btn layui-btn-sm layui-bg-gray btn-cancel-reply" type="button" data-id="{{ item.id }}">取消</button>
</div>
</div>

View File

@ -5,7 +5,7 @@
{% set delete_url = url({'for':'home.comment.delete','id':item.id}) %}
{% set reply_create_url = url({'for':'home.comment.create_reply','id':item.id}) %}
<div class="comment-box" id="comment-{{ item.id }}">
<div class="comment-card clearfix">
<div class="comment-card">
<div class="avatar">
<a href="{{ owner_url }}" title="{{ item.owner.name }}" target="_blank">
<img src="{{ item.owner.avatar }}!avatar_160" alt="{{ item.owner.name }}">
@ -57,7 +57,7 @@
<div class="footer">
<div class="toolbar"></div>
<div class="action">
<button class="layui-btn layui-btn-sm" lay-submit="true" lay-filter="replyComment" data-comment-id="{{ item.id }}" data-parent-id="{{ item.parent_id }}">发布</button>
<button class="layui-btn layui-btn-sm" lay-submit="true" lay-filter="reply_comment" data-comment-id="{{ item.id }}" data-parent-id="{{ item.parent_id }}">发布</button>
<button class="layui-btn layui-btn-sm layui-bg-gray btn-cancel-reply" type="button" data-id="{{ item.id }}">取消</button>
</div>
</div>

View File

@ -1,19 +0,0 @@
{% extends 'templates/layer.volt' %}
{% block content %}
<form class="layui-form" method="post" action="{{ url({'for':'home.comment.create_reply','id':comment.id}) }}">
<div class="layui-form-item">
<textarea class="layui-textarea" name="content" placeholder="@{{ comment.owner.name }}" lay-verify="required"></textarea>
</div>
<div class="layui-form-item center">
<button class="layui-btn" lay-submit="true" lay-filter="replyComment">提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</form>
{% endblock %}
{% block include_js %}
{{ js_include('home/js/comment.js') }}
{% endblock %}

View File

@ -5,7 +5,7 @@
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set consult_url = url({'for':'home.consult.show','id':item.id}) %}
{% set like_url = url({'for':'home.consult.like','id':item.id}) %}
<div class="comment-card review-card clearfix">
<div class="comment-card consult-card">
<div class="avatar">
<a href="{{ owner_url }}" title="{{ item.owner.name }}" target="_blank">
<img src="{{ item.owner.avatar }}!avatar_160" alt="{{ item.owner.name }}">
@ -17,8 +17,8 @@
<i class="layui-icon layui-icon-more"></i>
</a>
</div>
<div class="title layui-elip">{{ item.question }}</div>
<div class="content">{{ item.answer }}</div>
<div class="question">{{ item.question }}</div>
<div class="answer">{{ item.answer }}</div>
<div class="footer">
<div class="left">
<div class="column">

View File

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

View File

@ -0,0 +1,11 @@
{%- macro publish_status(type) %}
{% if type == 1 %}
审核中
{% elseif type == 2 %}
已发布
{% elseif type == 3 %}
未通过
{% else %}
未知
{% endif %}
{%- endmacro %}

View File

@ -0,0 +1,11 @@
{%- macro publish_status(type) %}
{% if type == 1 %}
审核中
{% elseif type == 2 %}
已发布
{% elseif type == 3 %}
未通过
{% else %}
未知
{% endif %}
{%- endmacro %}

View File

@ -42,7 +42,7 @@
<li class="layui-nav-item">
<a href="javascript:">创建</a>
<dl class="layui-nav-child">
<dd><a href="javascript:">提问题</a></dd>
<dd><a href="{{ url({'for':'home.question.add'}) }}" target="_blank">提问题</a></dd>
<dd><a href="{{ url({'for':'home.article.add'}) }}" target="_blank">写文章</a></dd>
</dl>
</li>

View File

@ -0,0 +1,62 @@
{% if pager.total_pages > 0 %}
{% for item in pager.items %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set accept_url = url({'for':'home.answer.accept','id':item.id}) %}
{% set like_url = url({'for':'home.answer.like','id':item.id}) %}
{% set edit_url = url({'for':'home.answer.edit','id':item.id}) %}
{% set delete_url = url({'for':'home.answer.delete','id':item.id}) %}
{% set report_url = '' %}
<div class="comment-card answer-card" id="answer-{{ item.id }}">
<div class="avatar">
<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">
{% if item.accepted == 1 %}
<div class="accepted">
<span class="layui-badge layui-bg-green">已采纳</span>
</div>
{% endif %}
<div class="user">
<a href="{{ owner_url }}" target="_blank">{{ item.owner.name }}</a>
</div>
<div class="content">{{ item.content }}</div>
<div class="footer">
<div class="left">
<div class="column">
<span class="time" title="{{ date('Y-m-d H:i',item.create_time) }}">{{ item.create_time|time_ago }}</span>
</div>
<div class="column">
<span class="like-count" data-count="{{ item.like_count }}">{{ item.like_count }}</span>
{% if item.me.liked == 1 %}
<span class="action action-like liked" title="取消点赞" data-url="{{ like_url }}">已赞</span>
{% else %}
<span class="action action-like" title="点赞支持" data-url="{{ like_url }}">点赞</span>
{% endif %}
</div>
</div>
<div class="right">
{% if question.solved == 0 and auth_user.id == question.owner_id %}
<div class="column">
<span class="action action-accept" data-url="{{ accept_url }}">采纳</span>
</div>
{% endif %}
<div class="column">
<span class="action action-report" data-url="{{ report_url }}">举报</span>
</div>
{% if auth_user.id == item.owner.id %}
<div class="column">
<span class="action action-edit" data-url="{{ edit_url }}">编辑</span>
</div>
<div class="column">
<span class="action action-delete" data-url="{{ delete_url }}">删除</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{{ partial('partials/pager_ajax') }}
{% endif %}

View File

@ -0,0 +1,71 @@
{% extends 'templates/main.volt' %}
{% block content %}
{% set title = question.id > 0 ? '编辑问题' : '提问题' %}
{% set action_url = question.id > 0 ? url({'for':'home.question.update','id':question.id}) : url({'for':'home.question.create'}) %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/">首页</a>
<a><cite>{{ title }}</cite></a>
</span>
</div>
<form class="layui-form" method="POST" action="{{ action_url }}">
<div class="layout-main clearfix">
<div class="layout-content">
<div class="writer-content wrap">
<div class="layui-form-item">
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" value="{{ question.title }}" placeholder="请输入标题..." lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<div id="vditor"></div>
<textarea name="content" class="layui-hide" id="vditor-textarea">{{ question.content }}</textarea>
</div>
</div>
<div class="layui-form-item center">
<div class="layui-input-block">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="go">提交问题</button>
</div>
</div>
</div>
</div>
<div class="layout-sidebar">
<div class="layui-card">
<div class="layui-card-header">基本信息</div>
<div class="layui-card-body">
<div class="writer-sidebar">
<div class="layui-form-item">
<label class="layui-form-label">标签</label>
<div class="layui-input-block">
<div id="xm-tag-ids"></div>
<input type="hidden" name="xm_tags" value='{{ xm_tags|json_encode }}'>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
{% 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('home/js/question.edit.js') }}
{{ js_include('home/js/vditor.js') }}
{% endblock %}

View File

@ -0,0 +1,27 @@
{% if questions|length > 0 %}
<div class="layui-card">
<div class="layui-card-header">热门问题</div>
<div class="layui-card-body">
{% for item in questions %}
{% set url = url({'for':'home.question.show','id':item.id}) %}
{% set rank = loop.index %}
<div class="sidebar-rank-card">
<div class="rank">
{% if rank == 1 %}
<span class="layui-badge layui-bg-red">{{ rank }}</span>
{% elseif rank == 2 %}
<span class="layui-badge layui-bg-blue">{{ rank }}</span>
{% elseif rank == 3 %}
<span class="layui-badge layui-bg-green">{{ rank }}</span>
{% else %}
<span class="layui-badge layui-bg-gray">{{ rank }}</span>
{% endif %}
</div>
<div class="title">
<a href="{{ url }}">{{ item.title }}</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}

View File

@ -0,0 +1,51 @@
{% extends 'templates/main.volt' %}
{% block content %}
{% set sort_val = request.get('sort','trim','latest') %}
{% set pager_url = url({'for':'home.question.pager'}, params) %}
{% set hot_questions_url = url({'for':'home.question.hot_questions'}) %}
{% set top_answerers_url = url({'for':'home.question.top_answerers'}) %}
{% set my_tags_url = url({'for':'home.widget.my_tags'},{'type':'question'}) %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/">首页</a>
<a><cite>问答</cite></a>
</span>
</div>
<div class="layout-main clearfix">
<div class="layout-content">
<div class="content-wrap wrap">
<div class="layui-tab layui-tab-brief search-tab">
<ul class="layui-tab-title">
{% for sort in sorts %}
{% set class = sort_val == sort.id ? 'layui-this' : 'none' %}
<li class="{{ class }}"><a href="{{ sort.url }}">{{ sort.name }}</a></li>
{% endfor %}
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div id="question-list" data-url="{{ pager_url }}"></div>
</div>
</div>
</div>
</div>
</div>
<div class="layout-sidebar">
{% if auth_user.id > 0 %}
<div class="sidebar" id="sidebar-my-tags" data-url="{{ my_tags_url }}"></div>
{% endif %}
<div class="sidebar" id="sidebar-hot-questions" data-url="{{ hot_questions_url }}"></div>
<div class="sidebar" id="sidebar-top-answerers" data-url="{{ top_answerers_url }}"></div>
</div>
</div>
{% endblock %}
{% block include_js %}
{{ js_include('home/js/question.list.js') }}
{% endblock %}

View File

@ -0,0 +1,43 @@
{{ partial('macros/question') }}
{% if pager.total_pages > 0 %}
<div class="question-list">
{% for item in pager.items %}
{% set question_url = url({'for':'home.question.show','id':item.id}) %}
{% set solved_class = item.solved ? 'column solved' : 'column' %}
<div class="article-card question-card">
<div class="info">
<div class="title layui-elip">
<a href="{{ question_url }}" target="_blank">{{ item.title }}</a>
</div>
<div class="summary">{{ item.summary }}</div>
<div class="meta">
{% if item.last_replier.id is defined %}
{% set last_replier_url = url({'for':'home.user.show','id':item.last_replier.id}) %}
<span class="replier"><a href="{{ last_replier_url }}">{{ item.last_replier.name }}</a></span>
<span class="time">{{ item.last_reply_time|time_ago }}</span>
<span class="view">{{ item.view_count }} 浏览</span>
<span class="like">{{ item.like_count }} 点赞</span>
<span class="answer">{{ item.answer_count }} 回答</span>
{% else %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
<span class="owner"><a href="{{ owner_url }}">{{ item.owner.name }}</a></span>
<span class="time">{{ item.create_time|time_ago }}</span>
<span class="view">{{ item.view_count }} 浏览</span>
<span class="like">{{ item.like_count }} 点赞</span>
<span class="answer">{{ item.answer_count }} 回答</span>
{% endif %}
</div>
</div>
{% if item.cover %}
<div class="cover">
<a href="{{ question_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">
</a>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{{ partial('partials/pager_ajax') }}
{% endif %}

View File

@ -0,0 +1,20 @@
{% if questions %}
<div class="layui-card">
<div class="layui-card-header">相关问题</div>
<div class="layui-card-body">
<div class="sidebar-article-list">
{% for question in questions %}
{% set question_url = url({'for':'home.question.show','id':question.id}) %}
<div class="title">
<a href="{{ question_url }}" target="_blank">{{ question.title }}</a>
</div>
<div class="meta">
<span class="view">{{ question.view_count }} 浏览</span>
<span class="like">{{ question.like_count }} 点赞</span>
<span class="answer">{{ question.answer_count }} 回答</span>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,108 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/question') }}
{% set question_list_url = url({'for':'home.question.list'}) %}
{% set add_answer_url = url({'for':'home.answer.add'},{'question_id':question.id}) %}
{% set answer_list_url = url({'for':'home.question.answers','id':question.id}) %}
{% set related_url = url({'for':'home.question.related','id':question.id}) %}
{% set owner_url = url({'for':'home.user.show','id':question.owner.id}) %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/">首页</a>
<a href="{{ question_list_url }}">问答</a>
<a><cite>详情</cite></a>
</span>
<span class="share">
<a href="javascript:" title="分享到微信"><i class="layui-icon layui-icon-login-wechat icon-wechat"></i></a>
<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>
</span>
</div>
<div class="layout-main clearfix">
<div class="layout-sticky">
{{ partial('question/sticky') }}
</div>
<div class="layout-content">
<div class="article-info wrap">
<div class="title">{{ question.title }}</div>
<div class="meta">
<span class="owner">
<a href="{{ owner_url }}">{{ question.owner.name }}</a>
</span>
<span class="view">{{ question.view_count }} 阅读</span>
<span class="answer">{{ question.answer_count }} 回答</span>
<span class="time" title="{{ date('Y-m-d H:i:s',question.create_time) }}">{{ question.create_time|time_ago }}</span>
</div>
<div class="content markdown-body">{{ question.content }}</div>
{% if question.tags %}
<div class="tags">
{% for item in question.tags %}
{% set url = url({'for':'home.question.list'},{'tag_id':item.id}) %}
<a href="{{ url }}" class="layui-btn layui-btn-xs">{{ item.name }}</a>
{% endfor %}
</div>
{% endif %}
</div>
<div id="answer-anchor"></div>
<div class="answer-wrap wrap">
<div id="answer-list" data-url="{{ answer_list_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">
<div class="sidebar-user-card clearfix">
<div class="avatar">
<img src="{{ question.owner.avatar }}!avatar_160" alt="{{ question.owner.name }}">
</div>
<div class="info">
<div class="name layui-elip">
<a href="{{ owner_url }}" title="{{ question.owner.about }}">{{ question.owner.name }}</a>
</div>
<div class="title layui-elip">{{ question.owner.title|default('初出江湖') }}</div>
</div>
</div>
</div>
</div>
</div>
{% if question.me.answered == 0 %}
<div class="sidebar wrap">
<button class="layui-btn layui-btn-fluid btn-answer" data-url="{{ add_answer_url }}">回答问题</button>
</div>
{% endif %}
<div class="sidebar" id="sidebar-related" data-url="{{ related_url }}"></div>
</div>
</div>
{% set share_url = full_url({'for':'home.share'},{'id':question.id,'type':'question','referer':auth_user.id}) %}
{% set qrcode_url = url({'for':'home.qrcode'},{'text':share_url}) %}
<div class="layui-hide">
<input type="hidden" name="share.title" value="{{ question.title }}">
<input type="hidden" name="share.pic" value="">
<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/question.show.js') }}
{{ js_include('home/js/question.share.js') }}
{{ js_include('home/js/answer.js') }}
{% endblock %}

View File

@ -0,0 +1,27 @@
{% set favorite_url = url({'for':'home.question.favorite','id':question.id}) %}
{% set like_url = url({'for':'home.question.like','id':question.id}) %}
{% set favorite_title = question.me.favorited == 1 ? '取消收藏' : '收藏问题' %}
{% set like_title = question.me.liked == 1 ? '取消点赞' : '点赞支持' %}
{% set favorite_class = question.me.favorited == 1 ? 'layui-icon-star-fill' : 'layui-icon-star' %}
{% set like_class = question.me.liked == 1 ? 'active' : '' %}
<div class="toolbar-sticky">
<div class="item" id="toolbar-like">
<div class="icon" title="{{ like_title }}" data-url="{{ like_url }}">
<i class="layui-icon layui-icon-praise icon-praise {{ like_class }}"></i>
</div>
<div class="text" data-count="{{ question.like_count }}">{{ question.like_count }}</div>
</div>
<div class="item" id="toolbar-answer">
<div class="icon" title="回答问题">
<i class="layui-icon layui-icon-reply-fill icon-reply"></i>
</div>
<div class="text" data-count="{{ question.answer_count }}">{{ question.answer_count }}</div>
</div>
<div class="item" id="toolbar-favorite">
<div class="icon" title="{{ favorite_title }}" data-url="{{ favorite_url }}">
<i class="layui-icon layui-icon-star icon-star {{ favorite_class }}"></i>
</div>
<div class="text" data-count="{{ question.favorite_count }}">{{ question.favorite_count }}</div>
</div>
</div>

View File

@ -0,0 +1,27 @@
{% if questions|length > 0 %}
<div class="layui-card">
<div class="layui-card-header">热门问题</div>
<div class="layui-card-body">
{% for item in questions %}
{% set url = url({'for':'home.question.show','id':item.id}) %}
{% set rank = loop.index %}
<div class="sidebar-rank-card">
<div class="rank">
{% if rank == 1 %}
<span class="layui-badge layui-bg-red">{{ rank }}</span>
{% elseif rank == 2 %}
<span class="layui-badge layui-bg-blue">{{ rank }}</span>
{% elseif rank == 3 %}
<span class="layui-badge layui-bg-green">{{ rank }}</span>
{% else %}
<span class="layui-badge layui-bg-gray">{{ rank }}</span>
{% endif %}
</div>
<div class="title">
<a href="{{ url }}">{{ item.title }}</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}

View File

@ -1,14 +1,9 @@
{% if pager.total_pages > 0 %}
<div class="search-course-list">
<div class="search-article-list">
{% 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="search-course-card clearfix">
<div class="cover">
<a href="{{ article_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">
</a>
</div>
<div class="search-article-card article-card">
<div class="info">
<div class="title layui-elip">
<a href="{{ article_url }}" target="_blank">{{ item.title }}</a>
@ -21,6 +16,13 @@
<span>评论:{{ item.comment_count }}</span>
</div>
</div>
{% if item.cover %}
<div class="cover">
<a href="{{ article_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">
</a>
</div>
{% endif %}
</div>
{% endfor %}
</div>

View File

@ -4,7 +4,7 @@
{{ partial('macros/course') }}
{% set types = {'course':'课程','article':'专栏','group':'群组','user':'用户'} %}
{% set types = {'course':'课程','article':'专栏','question':'问答','group':'群组','user':'用户'} %}
{% set type = request.get('type','trim','course') %}
{% set query = request.get('query','striptags','') %}
@ -35,6 +35,10 @@
<div class="layui-tab-item layui-show">
{{ partial('search/article') }}
</div>
{% elseif type == 'question' %}
<div class="layui-tab-item layui-show">
{{ partial('search/question') }}
</div>
{% elseif type == 'group' %}
<div class="layui-tab-item layui-show">
{{ partial('search/group') }}

View File

@ -0,0 +1,25 @@
{% if pager.total_pages > 0 %}
<div class="search-question-list">
{% for item in pager.items %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set question_url = url({'for':'home.question.show','id':item.id}) %}
{% set solved_class = item.solved ? 'column solved' : 'column' %}
<div class="search-question-card article-card question-card">
<div class="info">
<div class="title layui-elip">
<a href="{{ question_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="like">点赞:{{ item.like_count }}</span>
<span class="answer">回答:{{ item.answer_count }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
{{ partial('search/empty') }}
{% endif %}

View File

@ -0,0 +1,38 @@
{% extends 'templates/main.volt' %}
{% block content %}
{% set list_pager_url = url({'for':'home.tag.list_pager'}) %}
{% set my_pager_url = url({'for':'home.tag.my_pager'}) %}
<div class="layui-breadcrumb breadcrumb">
<a href="/">首页</a>
<a><cite>标签</cite></a>
</div>
<div class="tab-wrap">
<div class="layui-tab layui-tab-brief user-tab" lay-filter="tag">
<ul class="layui-tab-title">
<li class="layui-this">所有标签</li>
<li>我的关注</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div id="all-tag-list" data-url="{{ list_pager_url }}"></div>
</div>
<div class="layui-tab-item">
{% if auth_user.id > 0 %}
<div id="my-tag-list" data-url="{{ my_pager_url }}"></div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block include_js %}
{{ js_include('home/js/tag.list.js') }}
{% endblock %}

View File

@ -0,0 +1,23 @@
{% if pager.total_pages > 0 %}
<div class="tag-list clearfix">
<div class="layui-row layui-col-space20">
{% for item in pager.items %}
{% set follow_url = url({'for':'home.tag.follow','id':item.id}) %}
{% set follow_class = item.me.followed == 1 ? 'layui-btn layui-btn-sm followed btn-follow' : 'layui-btn layui-btn-primary layui-btn-sm btn-follow' %}
{% set follow_text = item.me.followed == 1 ? '已关注' : '关注' %}
<div class="layui-col-md3">
<div class="tag-card">
<div class="icon">
<img src="{{ item.icon }}" alt="{{ item.name }}">
</div>
<div class="name">{{ item.name }}<span class="stats">{{ item.follow_count }} 关注 </span></div>
<div class="action">
<span class="{{ follow_class }}" data-url="{{ follow_url }}">{{ follow_text }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{{ partial('partials/pager_ajax') }}
{% endif %}

View File

@ -0,0 +1,21 @@
{% if pager.total_pages > 0 %}
<div class="tag-list clearfix">
<div class="layui-row layui-col-space20">
{% for item in pager.items %}
{% set follow_url = url({'for':'home.tag.follow','id':item.id}) %}
<div class="layui-col-md3">
<div class="tag-card">
<div class="icon">
<img src="{{ item.icon }}" alt="{{ item.name }}">
</div>
<div class="name">{{ item.name }}<span class="stats">{{ item.follow_count }} 关注 </span></div>
<div class="action">
<span class="layui-btn layui-btn-sm followed btn-follow" data-url="{{ follow_url }}">已关注</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{{ partial('partials/pager_ajax') }}
{% endif %}

View File

@ -0,0 +1,65 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/answer') }}
{% set published_types = {'0':'全部','1':'审核中','2':'已发布','3':'未通过'} %}
{% set published = request.get('published','trim','0') %}
<div class="layout-main clearfix">
<div class="my-sidebar">{{ partial('user/console/menu') }}</div>
<div class="my-content">
<div class="wrap">
<div class="my-nav">
<span class="title">我的回答</span>
{% for key,value in published_types %}
{% set class = (published == key) ? 'layui-btn layui-btn-xs' : 'none' %}
{% set url = (key == '0') ? url({'for':'home.uc.answers'}) : url({'for':'home.uc.answers'},{'published':key}) %}
<a class="{{ class }}" href="{{ url }}">{{ value }}</a>
{% endfor %}
</div>
{% if pager.total_pages > 0 %}
<table class="layui-table review-table">
<colgroup>
<col>
<col>
<col width="15%">
</colgroup>
<thead>
<tr>
<th>内容</th>
<th>点赞</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in pager.items %}
{% set question_url = url({'for':'home.question.show','id':item.question.id}) %}
{% set edit_url = url({'for':'home.review.edit','id':item.id}) %}
{% set delete_url = url({'for':'home.review.delete','id':item.id}) %}
<tr>
<td>
<p>提问:<a href="{{ question_url }}" target="_blank">{{ item.question.title }}</a></p>
<p>回答:{{ substr(item.summary,0,32) }}</p>
<p class="meta">
创建:<span class="layui-badge layui-bg-gray">{{ item.create_time|time_ago }}</span>
状态:<span class="layui-badge layui-bg-gray">{{ publish_status(item.published) }}</span>
</p>
</td>
<td>{{ item.like_count }}</td>
<td>
<button class="layui-btn layui-btn-xs btn-edit-review" data-url="{{ edit_url }}">修改</button>
<button class="layui-btn layui-btn-xs layui-bg-red kg-delete" data-url="{{ delete_url }}">删除</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ partial('partials/pager') }}
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -18,18 +18,28 @@
</div>
<div class="layui-card">
<div class="layui-card-header">内容中心</div>
<div class="layui-card-header">课程中心</div>
<div class="layui-card-body">
<ul class="my-menu">
<li><a href="{{ url({'for':'home.uc.courses'}) }}">我的课程</a></li>
<li><a href="{{ url({'for':'home.uc.articles'}) }}">我的文章</a></li>
<li><a href="{{ url({'for':'home.uc.favorites'}) }}">我的收藏</a></li>
<li><a href="{{ url({'for':'home.uc.reviews'}) }}">我的评价</a></li>
<li><a href="{{ url({'for':'home.uc.consults'}) }}">我的咨询</a></li>
</ul>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">创作中心</div>
<div class="layui-card-body">
<ul class="my-menu">
<li><a href="{{ url({'for':'home.uc.articles'}) }}">我的文章</a></li>
<li><a href="{{ url({'for':'home.uc.questions'}) }}">我的提问</a></li>
<li><a href="{{ url({'for':'home.uc.answers'}) }}">我的回答</a></li>
<li><a href="{{ url({'for':'home.uc.favorites'}) }}">我的收藏</a></li>
</ul>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">订单中心</div>
<div class="layui-card-body">

View File

@ -0,0 +1,73 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/question') }}
{% set published_types = {'0':'全部','1':'审核中','2':'已发布','3':'未通过'} %}
{% set published = request.get('published','trim','0') %}
<div class="layout-main clearfix">
<div class="my-sidebar">{{ partial('user/console/menu') }}</div>
<div class="my-content">
<div class="wrap">
<div class="my-nav">
<span class="title">我的提问</span>
{% for key,value in published_types %}
{% set class = (published == key) ? 'layui-btn layui-btn-xs' : 'none' %}
{% set url = (key == '0') ? url({'for':'home.uc.questions'}) : url({'for':'home.uc.questions'},{'published':key}) %}
<a class="{{ class }}" href="{{ url }}">{{ value }}</a>
{% endfor %}
</div>
{% if pager.total_pages > 0 %}
<table class="layui-table">
<colgroup>
<col>
<col>
<col>
<col>
<col>
<col>
</colgroup>
<thead>
<tr>
<th>问题</th>
<th>回答</th>
<th>浏览</th>
<th>点赞</th>
<th>收藏</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in pager.items %}
{% set show_url = url({'for':'home.question.show','id':item.id}) %}
{% set edit_url = url({'for':'home.question.edit','id':item.id}) %}
{% set delete_url = url({'for':'home.question.delete','id':item.id}) %}
<tr>
<td>
<p>标题:<a href="{{ show_url }}" target="_blank">{{ item.title }}</a></p>
<p class="meta">
创建:<span class="layui-badge layui-bg-gray">{{ item.create_time|time_ago }}</span>
状态:<span class="layui-badge layui-bg-gray">{{ publish_status(item.published) }}</span>
</p>
</td>
<td>{{ item.answer_count }}</td>
<td>{{ item.view_count }}</td>
<td>{{ item.like_count }}</td>
<td>{{ item.favorite_count }}</td>
<td class="center">
<a href="{{ edit_url }}" class="layui-btn layui-btn-xs">编辑</a>
<a href="javascript:" class="layui-btn layui-btn-xs layui-bg-red kg-delete" data-url="{{ delete_url }}">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ partial('partials/pager') }}
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% set type = request.get('type','string','article') %}
{% if type == 'article' %}
{% set base_url = url({'for':'home.article.list'}) %}
{% elseif type == 'question' %}
{% set base_url = url({'for':'home.question.list'}) %}
{% endif %}
<div class="layui-card widget-card">
<div class="more">
<a href="{{ url({'for':'home.tag.list'}) }}">管理</a>
</div>
<div class="layui-card-header">关注标签</div>
<div class="layui-card-body">
{% for item in tags %}
{% set tagged_url = base_url ~ '?tag_id=' ~ item.id %}
<a class="layui-badge-rim tag-badge" href="{{ tagged_url }}">{{ item.name }}</a>
{% endfor %}
</div>
</div>

View File

@ -263,6 +263,16 @@ function kg_default_slide_cover_path()
return '/img/default/course_cover.png';
}
/**
* 获取默认图标路径
*
* @return string
*/
function kg_default_icon_path()
{
return '/img/default/user_avatar.png';
}
/**
* 获取存储基准URL
*
@ -405,6 +415,20 @@ function kg_cos_slide_cover_url($path, $style = null)
return kg_cos_img_url($path, $style);
}
/**
* 获取图标URL
*
* @param string $path
* @param string $style
* @return string
*/
function kg_cos_icon_url($path, $style = null)
{
$path = $path ?: kg_default_icon_path();
return kg_cos_img_url($path, $style);
}
/**
* 清除存储图片处理样式
*
@ -452,6 +476,26 @@ function kg_parse_summary($content, $length = 100)
return kg_substr($content, 0, $length);
}
/**
* 解析内容中上传首图
*
* @param string $content
* @return string
*/
function kg_parse_first_content_image($content)
{
$result = '';
$matched = preg_match('/\((.*?)\/img\/content\/(.*?)\)/', $content, $matches);
if ($matched) {
$url = sprintf('%s/img/content/%s', trim($matches[1]), trim($matches[2]));
$result = kg_cos_img_style_trim($url);
}
return $result;
}
/**
* 隐藏部分字符
*

41
app/Listeners/Answer.php Normal file
View File

@ -0,0 +1,41 @@
<?php
namespace App\Listeners;
use App\Models\Answer as AnswerModel;
use Phalcon\Events\Event as PhEvent;
class Answer extends Listener
{
public function afterCreate(PhEvent $event, $source, AnswerModel $answer)
{
}
public function afterUpdate(PhEvent $event, $source, AnswerModel $answer)
{
}
public function afterDelete(PhEvent $event, $source, AnswerModel $answer)
{
}
public function afterRestore(PhEvent $event, $source, AnswerModel $answer)
{
}
public function afterLike(PhEvent $event, $source, AnswerModel $answer)
{
}
public function afterUndoLike(PhEvent $event, $source, AnswerModel $answer)
{
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Listeners;
use App\Models\Question as QuestionModel;
use Phalcon\Events\Event as PhEvent;
class Question extends Listener
{
public function afterCreate(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterUpdate(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterDelete(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterRestore(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterApprove(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterReject(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterView(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterFavorite(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterUndoFavorite(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterLike(PhEvent $event, $source, QuestionModel $question)
{
}
public function afterUndoLike(PhEvent $event, $source, QuestionModel $question)
{
}
}

View File

@ -66,6 +66,16 @@ class UserDailyCounter extends Listener
$this->counter->hIncrBy($user->id, 'article_like_count');
}
public function incrQuestionLikeCount(PhEvent $event, $source, UserModel $user)
{
$this->counter->hIncrBy($user->id, 'question_like_count');
}
public function incrAnswerLikeCount(PhEvent $event, $source, UserModel $user)
{
$this->counter->hIncrBy($user->id, 'answer_like_count');
}
public function incrCommentLikeCount(PhEvent $event, $source, UserModel $user)
{
$this->counter->hIncrBy($user->id, 'comment_like_count');

181
app/Models/Answer.php Normal file
View File

@ -0,0 +1,181 @@
<?php
namespace App\Models;
use App\Caches\MaxAnswerId as MaxAnswerIdCache;
use Phalcon\Mvc\Model\Behavior\SoftDelete;
class Answer extends Model
{
/**
* 发布状态
*/
const PUBLISH_PENDING = 1; // 审核中
const PUBLISH_APPROVED = 2; // 已发布
const PUBLISH_REJECTED = 3; // 未通过
/**
* 自增编号
*
* @var integer
*/
public $id = 0;
/**
* 用户编号
*
* @var integer
*/
public $owner_id = 0;
/**
* 问题编号
*
* @var integer
*/
public $question_id = 0;
/**
* 封面
*
* @var string
*/
public $cover = '';
/**
* 内容
*
* @var string
*/
public $content = '';
/**
* 匿名标识
*
* @var integer
*/
public $anonymous = 0;
/**
* 采纳标识
*
* @var integer
*/
public $accepted = 0;
/**
* 状态标识
*
* @var integer
*/
public $published = self::PUBLISH_PENDING;
/**
* 删除标识
*
* @var integer
*/
public $deleted = 0;
/**
* 终端类型
*
* @var integer
*/
public $client_type = 0;
/**
* 终端IP
*
* @var integer
*/
public $client_ip = '';
/**
* 评论数
*
* @var integer
*/
public $comment_count = 0;
/**
* 点赞数
*
* @var integer
*/
public $like_count = 0;
/**
* 举报数
*
* @var integer
*/
public $report_count = 0;
/**
* 创建时间
*
* @var integer
*/
public $create_time = 0;
/**
* 更新时间
*
* @var integer
*/
public $update_time = 0;
public function getSource(): string
{
return 'kg_answer';
}
public function initialize()
{
parent::initialize();
$this->addBehavior(
new SoftDelete([
'field' => 'deleted',
'value' => 1,
])
);
}
public function beforeCreate()
{
if (empty($this->cover)) {
$this->cover = kg_parse_first_content_image($this->content);
}
$this->create_time = time();
}
public function beforeUpdate()
{
if (empty($this->cover)) {
$this->cover = kg_parse_first_content_image($this->content);
}
$this->update_time = time();
}
public function afterCreate()
{
$cache = new MaxAnswerIdCache();
$cache->rebuild();
}
public static function publishTypes()
{
return [
self::PUBLISH_PENDING => '审核中',
self::PUBLISH_APPROVED => '已发布',
self::PUBLISH_REJECTED => '未通过',
];
}
}

46
app/Models/AnswerLike.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
class AnswerLike extends Model
{
/**
* 主键编号
*
* @var int
*/
public $id = 0;
/**
* 回答编号
*
* @var int
*/
public $answer_id = 0;
/**
* 用户编号
*
* @var int
*/
public $user_id = 0;
/**
* 创建时间
*
* @var int
*/
public $create_time = 0;
public function getSource(): string
{
return 'kg_answer_like';
}
public function beforeCreate()
{
$this->create_time = time();
}
}

View File

@ -4,8 +4,8 @@ namespace App\Models;
use App\Caches\MaxArticleId as MaxArticleIdCache;
use App\Services\Sync\ArticleIndex as ArticleIndexSync;
use App\Services\Sync\ArticleScore as ArticleScoreSync;
use Phalcon\Mvc\Model\Behavior\SoftDelete;
use Phalcon\Text;
class Article extends Model
{
@ -108,6 +108,13 @@ class Article extends Model
*/
public $client_ip = '';
/**
* 综合得分
*
* @var float
*/
public $score = 0.00;
/**
* 私有标识
*
@ -164,6 +171,13 @@ class Article extends Model
*/
public $comment_count = 0;
/**
* 收藏数
*
* @var int
*/
public $favorite_count = 0;
/**
* 点赞数
*
@ -172,11 +186,11 @@ class Article extends Model
public $like_count = 0;
/**
* 收藏
* 举报
*
* @var int
* @var integer
*/
public $favorite_count = 0;
public $report_count;
/**
* 创建时间
@ -201,8 +215,6 @@ class Article extends Model
{
parent::initialize();
$this->keepSnapshots(true);
$this->addBehavior(
new SoftDelete([
'field' => 'deleted',
@ -214,9 +226,7 @@ class Article extends Model
public function beforeCreate()
{
if (empty($this->cover)) {
$this->cover = kg_default_article_cover_path();
} elseif (Text::startsWith($this->cover, 'http')) {
$this->cover = self::getCoverPath($this->cover);
$this->cover = kg_parse_first_content_image($this->content);
}
if (is_array($this->tags) || is_object($this->tags)) {
@ -231,10 +241,13 @@ class Article extends Model
if (time() - $this->update_time > 3 * 3600) {
$sync = new ArticleIndexSync();
$sync->addItem($this->id);
$sync = new ArticleScoreSync();
$sync->addItem($this->id);
}
if (Text::startsWith($this->cover, 'http')) {
$this->cover = self::getCoverPath($this->cover);
if (empty($this->cover)) {
$this->cover = kg_parse_first_content_image($this->content);
}
if (empty($this->summary)) {
@ -257,24 +270,11 @@ class Article extends Model
public function afterFetch()
{
if (!Text::startsWith($this->cover, 'http')) {
$this->cover = kg_cos_article_cover_url($this->cover);
}
if (is_string($this->tags)) {
$this->tags = json_decode($this->tags, true);
}
}
public static function getCoverPath($url)
{
if (Text::startsWith($url, 'http')) {
return parse_url($url, PHP_URL_PATH);
}
return $url;
}
public static function sourceTypes()
{
return [

View File

@ -14,6 +14,7 @@ class Category extends Model
const TYPE_COURSE = 1; // 课程
const TYPE_HELP = 2; // 帮助
const TYPE_ARTICLE = 3; // 文章
const TYPE_QUESTION = 4; // 问答
/**
* 主键编号
@ -143,6 +144,7 @@ class Category extends Model
self::TYPE_COURSE => '课程',
self::TYPE_HELP => '帮助',
self::TYPE_ARTICLE => '专栏',
self::TYPE_QUESTION => '问答',
];
}

View File

@ -114,6 +114,13 @@ class Comment extends Model
*/
public $like_count = 0;
/**
* 举报数
*
* @var integer
*/
public $report_count;
/**
* 创建时间
*

View File

@ -119,6 +119,13 @@ class Consult extends Model
*/
public $like_count = 0;
/**
* 举报数
*
* @var integer
*/
public $report_count;
/**
* 回复时间
*

View File

@ -287,8 +287,6 @@ class Course extends Model
{
parent::initialize();
$this->keepSnapshots(true);
$this->addBehavior(
new SoftDelete([
'field' => 'deleted',

View File

@ -80,7 +80,7 @@ class Nav extends Model
*
* @var int
*/
public $priority = 100;
public $priority = 99;
/**
* 发布标识

View File

@ -20,6 +20,7 @@ class PointHistory extends Model
const EVENT_ARTICLE_POST = 10; // 发布文章
const EVENT_QUESTION_POST = 11; // 发布问题
const EVENT_ANSWER_POST = 12; // 发布答案
const EVENT_ANSWER_ACCEPT = 13; // 采纳答案
/**
* 主键编号

310
app/Models/Question.php Normal file
View File

@ -0,0 +1,310 @@
<?php
namespace App\Models;
use App\Caches\MaxQuestionId as MaxQuestionIdCache;
use App\Services\Sync\QuestionIndex as QuestionIndexSync;
use App\Services\Sync\QuestionScore as QuestionScoreSync;
use Phalcon\Mvc\Model\Behavior\SoftDelete;
class Question extends Model
{
/**
* 发布状态
*/
const PUBLISH_PENDING = 1; // 审核中
const PUBLISH_APPROVED = 2; // 已发布
const PUBLISH_REJECTED = 3; // 未通过
/**
* 自增编号
*
* @var integer
*/
public $id = 0;
/**
* 分类编号
*
* @var integer
*/
public $category_id = 0;
/**
* 提问者
*
* @var integer
*/
public $owner_id = 0;
/**
* 最后回应用户
*
* @var integer
*/
public $last_replier_id = 0;
/**
* 最后回答编号
*
* @var integer
*/
public $last_answer_id = 0;
/**
* 采纳答案编号
*
* @var integer
*/
public $accept_answer_id = 0;
/**
* 标题
*
* @var string
*/
public $title = '';
/**
* 封面
*
* @var string
*/
public $cover = '';
/**
* 标签
*
* @var array|string
*/
public $tags = [];
/**
* 概要
*
* @var string
*/
public $summary = '';
/**
* 内容
*
* @var string
*/
public $content = '';
/**
* 综合得分
*
* @var float
*/
public $score = 0.00;
/**
* 悬赏积分
*
* @var integer
*/
public $bounty = 0;
/**
* 匿名标识
*
* @var integer
*/
public $anonymous = 0;
/**
* 解决标识
*
* @var integer
*/
public $solved = 0;
/**
* 关闭标识
*
* @var integer
*/
public $closed = 0;
/**
* 状态标识
*
* @var integer
*/
public $published = self::PUBLISH_PENDING;
/**
* 删除标识
*
* @var integer
*/
public $deleted = 0;
/**
* 终端类型
*
* @var integer
*/
public $client_type = 0;
/**
* 终端IP
*
* @var integer
*/
public $client_ip = '';
/**
* 浏览数
*
* @var integer
*/
public $view_count = 0;
/**
* 答案数
*
* @var integer
*/
public $answer_count = 0;
/**
* 评论数
*
* @var integer
*/
public $comment_count = 0;
/**
* 收藏数
*
* @var integer
*/
public $favorite_count = 0;
/**
* 点赞数
*
* @var integer
*/
public $like_count = 0;
/**
* 举报数
*
* @var integer
*/
public $report_count = 0;
/**
* 回应时间
*
* @var integer
*/
public $last_reply_time = 0;
/**
* 创建时间
*
* @var integer
*/
public $create_time = 0;
/**
* 更新时间
*
* @var integer
*/
public $update_time = 0;
public function getSource(): string
{
return 'kg_question';
}
public function initialize()
{
parent::initialize();
$this->addBehavior(
new SoftDelete([
'field' => 'deleted',
'value' => 1,
])
);
}
public function beforeCreate()
{
if (is_array($this->tags) || is_object($this->tags)) {
$this->tags = kg_json_encode($this->tags);
}
if (empty($this->cover)) {
$this->cover = kg_parse_first_content_image($this->content);
}
$this->create_time = time();
}
public function beforeUpdate()
{
if (time() - $this->update_time > 3 * 3600) {
$sync = new QuestionIndexSync();
$sync->addItem($this->id);
$sync = new QuestionScoreSync();
$sync->addItem($this->id);
}
if (is_array($this->tags) || is_object($this->tags)) {
$this->tags = kg_json_encode($this->tags);
}
if (empty($this->cover)) {
$this->cover = kg_parse_first_content_image($this->content);
}
if (empty($this->summary)) {
$this->summary = kg_parse_summary($this->content);
}
$this->update_time = time();
}
public function afterCreate()
{
$cache = new MaxQuestionIdCache();
$cache->rebuild();
}
public function afterFetch()
{
if (is_string($this->tags)) {
$this->tags = json_decode($this->tags, true);
}
}
public static function publishTypes()
{
return [
self::PUBLISH_PENDING => '审核中',
self::PUBLISH_APPROVED => '已发布',
self::PUBLISH_REJECTED => '未通过',
];
}
public static function sortTypes()
{
return [
'latest' => '最新提问',
'active' => '最新回答',
'unanswered' => '尚未回答',
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
class QuestionFavorite extends Model
{
/**
* 主键编号
*
* @var int
*/
public $id = 0;
/**
* 问题编号
*
* @var int
*/
public $question_id = 0;
/**
* 用户编号
*
* @var int
*/
public $user_id = 0;
/**
* 创建时间
*
* @var int
*/
public $create_time = 0;
public function getSource(): string
{
return 'kg_question_favorite';
}
public function beforeCreate()
{
$this->create_time = time();
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
class QuestionLike extends Model
{
/**
* 主键编号
*
* @var int
*/
public $id = 0;
/**
* 问题编号
*
* @var int
*/
public $question_id = 0;
/**
* 用户编号
*
* @var int
*/
public $user_id = 0;
/**
* 创建时间
*
* @var int
*/
public $create_time = 0;
public function getSource(): string
{
return 'kg_question_like';
}
public function beforeCreate()
{
$this->create_time = time();
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
class QuestionTag extends Model
{
/**
* 主键编号
*
* @var int
*/
public $id = 0;
/**
* 问题编号
*
* @var int
*/
public $question_id = 0;
/**
* 标签编号
*
* @var int
*/
public $tag_id = 0;
/**
* 创建时间
*
* @var int
*/
public $create_time = 0;
public function getSource(): string
{
return 'kg_question_tag';
}
public function beforeCreate()
{
$this->create_time = time();
}
}

100
app/Models/Report.php Normal file
View File

@ -0,0 +1,100 @@
<?php
namespace App\Models;
class Report extends Model
{
/**
* 条目类型
*/
const ITEM_USER = 100; // 用户
const ITEM_GROUP = 101; // 小组
const ITEM_COURSE = 102; // 课程
const ITEM_CHAPTER = 103; // 章节
const ITEM_CONSULT = 104; // 咨询
const ITEM_REVIEW = 105; // 评价
const ITEM_ARTICLE = 106; // 文章
const ITEM_QUESTION = 107; // 问题
const ITEM_ANSWER = 108; // 答案
const ITEM_COMMENT = 109; // 评论
/**
* 自增编号
*
* @var integer
*/
public $id;
/**
* 用户编号
*
* @var integer
*/
public $owner_id;
/**
* 条目编号
*
* @var integer
*/
public $item_id;
/**
* 条目类型
*
* @var integer
*/
public $item_type;
/**
* 举报理由
*
* @var string
*/
public $reason;
/**
* 处理状态
*
* @var integer
*/
public $reviewed;
/**
* 采纳标识
*
* @var integer
*/
public $accepted;
/**
* 创建时间
*
* @var integer
*/
public $create_time;
/**
* 更新时间
*
* @var integer
*/
public $update_time;
public function getSource(): string
{
return 'kg_report';
}
public function beforeCreate()
{
$this->create_time = time();
}
public function beforeUpdate()
{
$this->update_time = time();
}
}

View File

@ -112,6 +112,13 @@ class Review extends Model
*/
public $like_count = 0;
/**
* 举报数
*
* @var integer
*/
public $report_count;
/**
* 创建时间
*

View File

@ -4,6 +4,7 @@ namespace App\Models;
use App\Caches\MaxTagId as MaxTagIdCache;
use Phalcon\Mvc\Model\Behavior\SoftDelete;
use Phalcon\Text;
class Tag extends Model
{
@ -97,11 +98,21 @@ class Tag extends Model
public function beforeCreate()
{
if (empty($this->icon)) {
$this->icon = kg_default_icon_path();
} elseif (Text::startsWith($this->icon, 'http')) {
$this->icon = self::getIconPath($this->icon);
}
$this->create_time = time();
}
public function beforeUpdate()
{
if (Text::startsWith($this->icon, 'http')) {
$this->icon = self::getIconPath($this->icon);
}
if ($this->deleted == 1) {
$this->published = 0;
}
@ -116,4 +127,20 @@ class Tag extends Model
$cache->rebuild();
}
public function afterFetch()
{
if (!Text::startsWith($this->icon, 'http')) {
$this->icon = kg_cos_icon_url($this->icon);
}
}
public static function getIconPath($url)
{
if (Text::startsWith($url, 'http')) {
return parse_url($url, PHP_URL_PATH);
}
return $url;
}
}

46
app/Models/TagFollow.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
class TagFollow extends Model
{
/**
* 主键编号
*
* @var int
*/
public $id = 0;
/**
* 标签编号
*
* @var int
*/
public $tag_id = 0;
/**
* 用户编号
*
* @var int
*/
public $user_id = 0;
/**
* 创建时间
*
* @var int
*/
public $create_time = 0;
public function getSource(): string
{
return 'kg_tag_follow';
}
public function beforeCreate()
{
$this->create_time = time();
}
}

View File

@ -122,6 +122,20 @@ class User extends Model
*/
public $article_count;
/**
* 提问数
*
* @var int
*/
public $question_count;
/**
* 回答数
*
* @var int
*/
public $answer_count;
/**
* 收藏数
*

104
app/Repos/Answer.php Normal file
View File

@ -0,0 +1,104 @@
<?php
namespace App\Repos;
use App\Library\Paginator\Adapter\QueryBuilder as PagerQueryBuilder;
use App\Models\Answer as AnswerModel;
use App\Models\AnswerLike as AnswerLikeModel;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class Answer extends Repository
{
public function paginate($where = [], $sort = 'accepted', $page = 1, $limit = 15)
{
$builder = $this->modelsManager->createBuilder();
$builder->from(AnswerModel::class);
$builder->where('1 = 1');
if (!empty($where['id'])) {
$builder->andWhere('id = :id:', ['id' => $where['id']]);
}
if (!empty($where['owner_id'])) {
$builder->andWhere('owner_id = :owner_id:', ['owner_id' => $where['owner_id']]);
}
if (!empty($where['question_id'])) {
$builder->andWhere('question_id = :question_id:', ['question_id' => $where['question_id']]);
}
if (isset($where['published'])) {
$builder->andWhere('published = :published:', ['published' => $where['published']]);
}
if (isset($where['deleted'])) {
$builder->andWhere('deleted = :deleted:', ['deleted' => $where['deleted']]);
}
switch ($sort) {
case 'like':
$orderBy = 'like_count DESC';
break;
case 'latest':
$orderBy = 'id DESC';
break;
default:
$orderBy = 'accepted DESC, like_count DESC';
break;
}
$builder->orderBy($orderBy);
$pager = new PagerQueryBuilder([
'builder' => $builder,
'page' => $page,
'limit' => $limit,
]);
return $pager->paginate();
}
/**
* @param int $id
* @return AnswerModel|Model|bool
*/
public function findById($id)
{
return AnswerModel::findFirst([
'conditions' => 'id = :id:',
'bind' => ['id' => $id],
]);
}
/**
* @param array $ids
* @param array|string $columns
* @return ResultsetInterface|Resultset|AnswerModel[]
*/
public function findByIds($ids, $columns = '*')
{
return AnswerModel::query()
->columns($columns)
->inWhere('id', $ids)
->execute();
}
public function countAnswers()
{
return (int)AnswerModel::count(['conditions' => 'deleted = 0']);
}
public function countLikes($answerId)
{
return (int)AnswerLikeModel::count([
'conditions' => 'answer_id = :answer_id:',
'bind' => ['answer_id' => $answerId],
]);
}
}

37
app/Repos/AnswerLike.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace App\Repos;
use App\Models\AnswerLike as AnswerLikeModel;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class AnswerLike extends Repository
{
/**
* @param int $answerId
* @param int $userId
* @return AnswerLikeModel|Model|bool
*/
public function findAnswerLike($answerId, $userId)
{
return AnswerLikeModel::findFirst([
'conditions' => 'answer_id = :answer_id: AND user_id = :user_id:',
'bind' => ['answer_id' => $answerId, 'user_id' => $userId],
]);
}
/**
* @param int $userId
* @return ResultsetInterface|Resultset|AnswerLikeModel[]
*/
public function findByUserId($userId)
{
return AnswerLikeModel::query()
->where('user_id = :user_id:', ['user_id' => $userId])
->execute();
}
}

202
app/Repos/Question.php Normal file
View File

@ -0,0 +1,202 @@
<?php
namespace App\Repos;
use App\Library\Paginator\Adapter\QueryBuilder as PagerQueryBuilder;
use App\Models\Answer as AnswerModel;
use App\Models\Comment as CommentModel;
use App\Models\Question as QuestionModel;
use App\Models\QuestionFavorite as QuestionFavoriteModel;
use App\Models\QuestionLike as QuestionLikeModel;
use App\Models\QuestionTag as QuestionTagModel;
use App\Models\Tag as TagModel;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class Question extends Repository
{
public function paginate($where = [], $sort = 'latest', $page = 1, $limit = 15)
{
$builder = $this->modelsManager->createBuilder();
$builder->from(QuestionModel::class);
$builder->where('1 = 1');
if (!empty($where['tag_id'])) {
$where['id'] = $this->getTagQuestionIds($where['tag_id']);
}
if (!empty($where['id'])) {
if (is_array($where['id'])) {
$builder->inWhere('id', $where['id']);
} else {
$builder->andWhere('id = :id:', ['id' => $where['id']]);
}
}
if (!empty($where['owner_id'])) {
$builder->andWhere('owner_id = :owner_id:', ['owner_id' => $where['owner_id']]);
}
if (!empty($where['title'])) {
$builder->andWhere('title LIKE :title:', ['title' => "%{$where['title']}%"]);
}
if (isset($where['anonymous'])) {
$builder->andWhere('anonymous = :anonymous:', ['anonymous' => $where['anonymous']]);
}
if (isset($where['closed'])) {
$builder->andWhere('closed = :closed:', ['closed' => $where['closed']]);
}
if (isset($where['solved'])) {
$builder->andWhere('solved = :solved:', ['solved' => $where['solved']]);
}
if (isset($where['published'])) {
$builder->andWhere('published = :published:', ['published' => $where['published']]);
}
if (isset($where['deleted'])) {
$builder->andWhere('deleted = :deleted:', ['deleted' => $where['deleted']]);
}
if ($sort == 'unanswered') {
$builder->andWhere('answer_count = 0');
}
switch ($sort) {
case 'active':
$orderBy = 'last_reply_time DESC';
break;
case 'score':
$orderBy = 'score DESC';
break;
default:
$orderBy = 'id DESC';
break;
}
$builder->orderBy($orderBy);
$pager = new PagerQueryBuilder([
'builder' => $builder,
'page' => $page,
'limit' => $limit,
]);
return $pager->paginate();
}
/**
* @param int $id
* @return QuestionModel|Model|bool
*/
public function findById($id)
{
return QuestionModel::findFirst([
'conditions' => 'id = :id:',
'bind' => ['id' => $id],
]);
}
/**
* @param array $ids
* @param array|string $columns
* @return ResultsetInterface|Resultset|QuestionModel[]
*/
public function findByIds($ids, $columns = '*')
{
return QuestionModel::query()
->columns($columns)
->inWhere('id', $ids)
->execute();
}
/**
* @param int $questionId
* @return ResultsetInterface|Resultset|TagModel[]
*/
public function findTags($questionId)
{
return $this->modelsManager->createBuilder()
->columns('t.*')
->addFrom(TagModel::class, 't')
->join(QuestionTagModel::class, 't.id = qt.tag_id', 'qt')
->where('qt.question_id = :question_id:', ['question_id' => $questionId])
->andWhere('t.published = 1')
->getQuery()->execute();
}
/**
* @param int $questionId
* @param int $userId
* @return ResultsetInterface|Resultset|AnswerModel[]
*/
public function findUserAnswers($questionId, $userId)
{
return AnswerModel::query()
->where('question_id = :question_id:', ['question_id' => $questionId])
->andWhere('owner_id = :owner_id:', ['owner_id' => $userId])
->execute();
}
public function countQuestions()
{
return (int)QuestionModel::count(['conditions' => 'deleted = 0']);
}
public function countAnswers($questionId)
{
return (int)AnswerModel::count([
'conditions' => 'question_id = :question_id: AND deleted = 0',
'bind' => ['question_id' => $questionId],
]);
}
public function countComments($questionId)
{
return (int)CommentModel::count([
'conditions' => 'item_id = ?1 AND item_type = ?2 AND deleted = 0',
'bind' => [1 => $questionId, 2 => CommentModel::ITEM_QUESTION],
]);
}
public function countFavorites($questionId)
{
return (int)QuestionFavoriteModel::count([
'conditions' => 'question_id = :question_id:',
'bind' => ['question_id' => $questionId],
]);
}
public function countLikes($questionId)
{
return (int)QuestionLikeModel::count([
'conditions' => 'question_id = :question_id:',
'bind' => ['question_id' => $questionId],
]);
}
protected function getTagQuestionIds($tagId)
{
$tagIds = is_array($tagId) ? $tagId : [$tagId];
$repo = new QuestionTag();
$rows = $repo->findByTagIds($tagIds);
$result = [];
if ($rows->count() > 0) {
$result = kg_array_column($rows->toArray(), 'question_id');
}
return $result;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Repos;
use App\Library\Paginator\Adapter\QueryBuilder as PagerQueryBuilder;
use App\Models\QuestionFavorite as QuestionFavoriteModel;
use Phalcon\Mvc\Model;
class QuestionFavorite extends Repository
{
public function paginate($where = [], $sort = 'latest', $page = 1, $limit = 15)
{
$builder = $this->modelsManager->createBuilder();
$builder->from(QuestionFavoriteModel::class);
$builder->where('1 = 1');
if (!empty($where['question_id'])) {
$builder->andWhere('question_id = :question_id:', ['question_id' => $where['question_id']]);
}
if (!empty($where['user_id'])) {
$builder->andWhere('user_id = :user_id:', ['user_id' => $where['user_id']]);
}
switch ($sort) {
default:
$orderBy = 'id DESC';
break;
}
$builder->orderBy($orderBy);
$pager = new PagerQueryBuilder([
'builder' => $builder,
'page' => $page,
'limit' => $limit,
]);
return $pager->paginate();
}
/**
* @param int $questionId
* @param int $userId
* @return QuestionFavoriteModel|Model|bool
*/
public function findQuestionFavorite($questionId, $userId)
{
return QuestionFavoriteModel::findFirst([
'conditions' => 'question_id = :question_id: AND user_id = :user_id:',
'bind' => ['question_id' => $questionId, 'user_id' => $userId],
]);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Repos;
use App\Models\QuestionLike as QuestionLikeModel;
use Phalcon\Mvc\Model;
class QuestionLike extends Repository
{
/**
* @param int $questionId
* @param int $userId
* @return QuestionLikeModel|Model|bool
*/
public function findQuestionLike($questionId, $userId)
{
return QuestionLikeModel::findFirst([
'conditions' => 'question_id = :question_id: AND user_id = :user_id:',
'bind' => ['question_id' => $questionId, 'user_id' => $userId],
]);
}
}

48
app/Repos/QuestionTag.php Normal file
View File

@ -0,0 +1,48 @@
<?php
namespace App\Repos;
use App\Models\QuestionTag as QuestionTagModel;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class QuestionTag extends Repository
{
/**
* @param int $questionId
* @param int $tagId
* @return QuestionTagModel|Model|bool
*/
public function findQuestionTag($questionId, $tagId)
{
return QuestionTagModel::findFirst([
'conditions' => 'question_id = :question_id: AND tag_id = :tag_id:',
'bind' => ['question_id' => $questionId, 'tag_id' => $tagId],
]);
}
/**
* @param array $tagIds
* @return ResultsetInterface|Resultset|QuestionTagModel[]
*/
public function findByTagIds($tagIds)
{
return QuestionTagModel::query()
->inWhere('tag_id', $tagIds)
->execute();
}
/**
* @param array $questionIds
* @return ResultsetInterface|Resultset|QuestionTagModel[]
*/
public function findByQuestionIds($questionIds)
{
return QuestionTagModel::query()
->inWhere('question_id', $questionIds)
->execute();
}
}

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