├── .gitattributes
├── .gitignore
├── 404.php
├── LICENSE
├── README.md
├── archive.php
├── assets
├── main.css
├── main.js
└── normalize.css
├── functions.php
├── includes
├── footer.php
├── head.php
└── header.php
├── index.php
├── libs
├── Contents.php
└── Utils.php
├── page.php
└── post.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.js linguist-language=PHP
2 | *.css linguist-language=PHP
3 | *.html linguist-language=PHP
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | temp/*
3 | .DS_Store
4 | package-lock.json
--------------------------------------------------------------------------------
/404.php:
--------------------------------------------------------------------------------
1 | need('includes/head.php');
11 | $this->need('includes/header.php');
12 | ?>
13 |
14 |
15 |
16 | need('includes/footer.php'); ?>
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 AlanDecode
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 一个 Typecho 主题开发起步框架
2 |
3 | 我在为 Typecho 写主题时发现了几个问题:
4 |
5 | - 某些代码可以复用,没必要每次都写一遍
6 | - Typecho 文档不充分,某些方法每次用到都要现查
7 | - 某些方法 Typecho 的接口太复杂或者压根就没有实现
8 | - 某些方法在不同的 Typecho 版本行为不一致
9 | - 我的主题都有比较相似的目录结构
10 |
11 | 因此我准备把自己的最佳实践打包一下,美其名曰一个「框架」,其实只是为了减少一些无谓的工作量。
12 |
13 | ~~是不是闻到了 JQuery 的味道~~
14 |
15 | 使用时,直接下载本框架,并在其中删改、添加代码。Utils.php 与 Contents.php 中提供了一些常用的代码,可以看看。
16 |
17 | **非常欢迎各种 pull request**
18 |
19 | 目录结构:
20 |
21 | ```
22 | index.php
23 | post.php
24 | page.php
25 | archive.php
26 | 404.php
27 | functions.php
28 | includes
29 | |-- head.php
30 | |-- header.php
31 | |-- footer.php
32 | libs
33 | |-- Utils.php
34 | |-- Contents.php
35 | assets
36 | |-- main.css
37 | |-- main.js
38 | ```
39 |
40 |
41 | ## License
42 |
43 | MIT © [AlanDecode](https://github.com/AlanDecode)
--------------------------------------------------------------------------------
/archive.php:
--------------------------------------------------------------------------------
1 | need('includes/head.php');
11 | $this->need('includes/header.php');
12 | ?>
13 |
14 |
15 |
16 | need('includes/footer.php'); ?>
--------------------------------------------------------------------------------
/assets/main.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlanDecode/typecho-theme-dev-framework/620fae9f54cc589327f55f38b16112677c484b09/assets/main.css
--------------------------------------------------------------------------------
/assets/main.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlanDecode/typecho-theme-dev-framework/620fae9f54cc589327f55f38b16112677c484b09/assets/main.js
--------------------------------------------------------------------------------
/assets/normalize.css:
--------------------------------------------------------------------------------
1 | html{line-height:1.15;-webkit-text-size-adjust:100%;}body{margin:0;}main{display:block;}h1{font-size:2em;margin:0.67em 0;}hr{box-sizing:content-box;height:0;overflow:visible;}pre{font-family:monospace,monospace;font-size:1em;}a{background-color:transparent;}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted;}b,strong{font-weight:bolder;}code,kbd,samp{font-family:monospace,monospace;font-size:1em;}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sub{bottom:-0.25em;}sup{top:-0.5em;}img{border-style:none;}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0;}button,input{overflow:visible;}button,select{text-transform:none;}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button;}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0;}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText;}fieldset{padding:0.35em 0.75em 0.625em;}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal;}progress{vertical-align:baseline;}textarea{overflow:auto;}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0;}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto;}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px;}[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit;}details{display:block;}summary{display:list-item;}template{display:none;}[hidden]{display:none;}
--------------------------------------------------------------------------------
/functions.php:
--------------------------------------------------------------------------------
1 |
11 |
12 | contentEx = array('Contents','parseContent');
23 | Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array('Contents','parseContent');
24 |
25 | /**
26 | * 主题启用时执行的方法
27 | */
28 | function themeInit() {
29 | /**
30 | * 重置某些设置项,采用数据库查询方式完成
31 | */
32 | $db = Typecho_Db::get();
33 |
34 | /* 关闭评论反垃圾保护,使用 PJAX 时可能需要取消注释以下 4 行 */
35 | // $query = $db->update('table.options')->rows(array('value'=>'0'))->where('name=?', 'commentsAntiSpam');
36 | // $db->query($query);
37 | // $query = $db->update('table.options')->rows(array('value'=>'0'))->where('name=?', 'commentsCheckReferer');
38 | // $db->query($query);
39 |
40 | /* 设置评论最大嵌套层数 */
41 | $query = $db->update('table.options')->rows(array('value'=>'999'))->where('name=?', 'commentsMaxNestingLevels');
42 | $db->query($query);
43 |
44 | /* 强制新评论在前 */
45 | $query = $db->update('table.options')->rows(array('value'=>'DESC'))->where('name=?', 'commentsOrder');
46 | $db->query($query);
47 |
48 | /* 默认显示第一页评论 */
49 | $query = $db->update('table.options')->rows(array('value'=>'first'))->where('name=?', 'commentsPageDisplay');
50 | $db->query($query);
51 | }
52 |
53 | /**
54 | * 主题后台设置
55 | */
56 | function themeConfig($form) {
57 |
58 | }
59 |
60 | /**
61 | * 文章与独立页自定义字段
62 | */
63 | function themeFields(Typecho_Widget_Helper_Layout $layout) {
64 |
65 | }
--------------------------------------------------------------------------------
/includes/footer.php:
--------------------------------------------------------------------------------
1 | 标签,并关闭
、 标签
6 | *
7 | * @author 熊猫小A
8 | */
9 | if (!defined('__TYPECHO_ROOT_DIR__')) exit;
10 | ?>
11 |
12 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/includes/head.php:
--------------------------------------------------------------------------------
1 | 标签,并开始 、 标签
6 | *
7 | * @author 熊猫小A
8 | */
9 | if (!defined('__TYPECHO_ROOT_DIR__')) exit;
10 | ?>
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | is('post') || $this->is('page')){
24 | if(isset($this->fields->banner))
25 | $banner=$this->fields->banner;
26 | if(isset($this->fields->excerpt))
27 | $description = $this->fields->excerpt;
28 | }else{
29 | $description = Helper::options()->description;
30 | }
31 | ?>
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | " />
47 | header('description=&'); ?>
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/includes/header.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | 熊猫小A
6 | *
7 | * @package Typecho-Theme-X
8 | * @author 熊猫小A
9 | * @version 1.0
10 | * @link https://blog.imalan.cn
11 | */
12 | if (!defined('__TYPECHO_ROOT_DIR__')) exit;
13 | $this->need('includes/head.php');
14 | $this->need('includes/header.php');
15 | ?>
16 |
17 |
18 |
19 | need('includes/footer.php'); ?>
--------------------------------------------------------------------------------
/libs/Contents.php:
--------------------------------------------------------------------------------
1 | 'cid',
43 | 'Comments' => 'coid',
44 | 'Metas' => 'mid',
45 | 'Users' => 'uid'
46 | );
47 | $className = "Widget_Abstract_{$table}";
48 | $key = $keys[$table];
49 | $db = Typecho_Db::get();
50 | $widget = new $className(Typecho_Request::getInstance(), Typecho_Widget_Helper_Empty::getInstance());
51 |
52 | $db->fetchRow(
53 | $widget->select()->where("{$key} = ?", $pkId)->limit(1),
54 | array($widget, 'push'));
55 | return $widget;
56 | }
57 |
58 | /**
59 | * 输出完备的标题
60 | */
61 | public static function title(Widget_Archive $archive)
62 | {
63 | $archive->archiveTitle(array(
64 | 'category' => '分类 %s 下的文章',
65 | 'search' => '包含关键字 %s 的文章',
66 | 'tag' => '标签 %s 下的文章',
67 | 'author' => '%s 发布的文章'
68 | ), '', ' - ');
69 | Helper::options()->title();
70 | }
71 |
72 | /**
73 | * 返回上一篇文章
74 | */
75 | public static function getPrev($archive)
76 | {
77 | $db = Typecho_Db::get();
78 | $content = $db->fetchRow($db->select()->from('table.contents')->where('table.contents.created < ?', $archive->created)
79 | ->where('table.contents.status = ?', 'publish')
80 | ->where('table.contents.type = ?', $archive->type)
81 | ->where('table.contents.password IS NULL')
82 | ->order('table.contents.created', Typecho_Db::SORT_DESC)
83 | ->limit(1));
84 |
85 | if($content) {
86 | return self::widgetById('Contents', $content['cid']);
87 | }else{
88 | return NULL;
89 | }
90 | }
91 |
92 | /**
93 | * 返回下一篇文章
94 | */
95 | public static function getNext($archive)
96 | {
97 | $db = Typecho_Db::get();
98 | $content = $db->fetchRow($db->select()->from('table.contents')->where('table.contents.created > ? AND table.contents.created < ?',
99 | $archive->created, Helper::options()->gmtTime)
100 | ->where('table.contents.status = ?', 'publish')
101 | ->where('table.contents.type = ?', $archive->type)
102 | ->where('table.contents.password IS NULL')
103 | ->order('table.contents.created', Typecho_Db::SORT_ASC)
104 | ->limit(1));
105 |
106 | if($content) {
107 | return self::widgetById('Contents', $content['cid']);
108 | }else{
109 | return NULL;
110 | }
111 | }
112 |
113 | /**
114 | * 最近评论,过滤引用通告,过滤博主评论
115 | */
116 | public static function getRecentComments($num = 10)
117 | {
118 | $comments = array();
119 |
120 | $db = Typecho_Db::get();
121 | $rows = $db->fetchAll($db->select()->from('table.comments')->where('table.comments.status = ?', 'approved')
122 | ->where('type = ?', 'comment')
123 | ->where('ownerId <> authorId')
124 | ->order('table.comments.created', Typecho_Db::SORT_DESC)
125 | ->limit($num));
126 |
127 | foreach ($rows as $row) {
128 | $comment = self::widgetById('Comments', $row['coid']);
129 | $comments[] = $comment;
130 | }
131 |
132 | return $comments;
133 | }
134 | }
--------------------------------------------------------------------------------
/libs/Utils.php:
--------------------------------------------------------------------------------
1 | index($path);
19 | }
20 |
21 | /**
22 | * 输出相对首页路径,本方法用于静态文件
23 | */
24 | public static function indexHome($path = '')
25 | {
26 | Helper::options()->siteUrl($path);
27 | }
28 |
29 | /**
30 | * 输出相对主题目录路径,用于静态文件
31 | */
32 | public static function indexTheme($path = '')
33 | {
34 | Helper::options()->themeUrl($path);
35 | }
36 |
37 | /**
38 | * 根据邮箱返回 Gravatar 头像链接
39 | */
40 | public static function gravatar($mail, $size = 64, $d = '')
41 | {
42 | return Typecho_Common::gravatarUrl($mail, $size, '', urlencode($d), true);
43 | }
44 |
45 | /**
46 | * 判断插件是否可用(存在且已激活)
47 | */
48 | public static function hasPlugin($name)
49 | {
50 | $plugins = Typecho_Plugin::export();
51 | $plugins = $plugins['activated'];
52 | return is_array($plugins) && array_key_exists($name, $plugins);
53 | }
54 |
55 | /**
56 | * 判断移动端请求
57 | */
58 | public static function isMobile()
59 | {
60 | if (isset ($_SERVER['HTTP_X_WAP_PROFILE'])){
61 | return TRUE;
62 | }
63 |
64 | if (isset ($_SERVER['HTTP_USER_AGENT'])) {
65 | $clientkeywords = array ('mobile','nokia','sony','ericsson','mot','samsung','htc','sgh','lg','sharp','sie-','philips','panasonic','alcatel','lenovo','iphone','ipod','blackberry','meizu','android','netfront','symbian','ucweb','windowsce','palm','operamini','operamobi','openwave','nexusone','cldc','midp','wap'
66 | );
67 | if (preg_match("/(" . implode('|', $clientkeywords) . ")/i", strtolower($_SERVER['HTTP_USER_AGENT']))){
68 | return TRUE;
69 | }
70 | }
71 | if (isset ($_SERVER['HTTP_ACCEPT'])){
72 | if ((strpos($_SERVER['HTTP_ACCEPT'], 'vnd.wap.wml') !== FALSE) && (strpos($_SERVER['HTTP_ACCEPT'], 'text/html') === FALSE || (strpos($_SERVER['HTTP_ACCEPT'], 'vnd.wap.wml') < strpos($_SERVER['HTTP_ACCEPT'], 'text/html')))){
73 | return TRUE;
74 | }
75 | }
76 | return FALSE;
77 | }
78 |
79 | /**
80 | * 判定电脑端微信内置浏览器
81 | */
82 | public static function isWeixin()
83 | {
84 | return !self::isMobile() && strpos($_SERVER['HTTP_USER_AGENT'], 'MicroMessenger') !== false;
85 | }
86 |
87 | /**
88 | * 判定内容是否过时
89 | * $timeout:过期时间,单位为天
90 | */
91 | public static function isOutdated($archive, $timeout)
92 | {
93 | date_default_timezone_set("Asia/Shanghai");
94 | return round((time()- $archive->created) / 3600 / 24) > $timeout;
95 | }
96 |
97 | /**
98 | * 输出建站时间(最早一篇文章的写作时间)
99 | */
100 | public static function getBuildTime()
101 | {
102 | date_default_timezone_set("Asia/Shanghai");
103 | $db = Typecho_Db::get();
104 | $content = $db->fetchRow($db->select()->from('table.contents')
105 | ->where('table.contents.status = ?', 'publish')
106 | ->where('table.contents.password IS NULL')
107 | ->order('table.contents.created', Typecho_Db::SORT_ASC)
108 | ->limit(1));
109 | return $content['created'];
110 | }
111 |
112 | /**
113 | * 已发布文章数量
114 | */
115 | public static function getPostNum()
116 | {
117 | $db = Typecho_Db::get();
118 | return $db->fetchObject($db->select(array('COUNT(cid)' => 'num'))
119 | ->from('table.contents')
120 | ->where('table.contents.type = ?', 'post')
121 | ->where('table.contents.status = ?', 'publish'))->num;
122 | }
123 |
124 | /**
125 | * 分类数量
126 | */
127 | public static function getCatNum()
128 | {
129 | $db = Typecho_Db::get();
130 | return $db->fetchObject($db->select(array('COUNT(mid)' => 'num'))
131 | ->from('table.metas')
132 | ->where('table.metas.type = ?', 'category'))->num;
133 | }
134 |
135 | /**
136 | * 标签数量
137 | */
138 | public static function getTagNum()
139 | {
140 | $db = Typecho_Db::get();
141 | return $db->fetchObject($db->select(array('COUNT(mid)' => 'num'))
142 | ->from('table.metas')
143 | ->where('table.metas.type = ?', 'tag'))->num;
144 | }
145 |
146 | /**
147 | * 总字数
148 | *
149 | * @return int
150 | */
151 | public static function getWordCount()
152 | {
153 | $db = Typecho_Db::get();
154 | $posts = $db->fetchAll($db->select('table.contents.text')
155 | ->from('table.contents')
156 | ->where('table.contents.type = ?', 'post')
157 | ->where('table.contents.status = ?', 'publish'));
158 | $total = 0;
159 | foreach ($posts as $post) {
160 | $total = $total + mb_strlen(preg_replace("/[^\x{4e00}-\x{9fa5}]/u", "", $post['text']), 'UTF-8');
161 | }
162 | return $total;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/page.php:
--------------------------------------------------------------------------------
1 | need('includes/head.php');
11 | $this->need('includes/header.php');
12 | ?>
13 |
14 |
15 |
16 | need('includes/footer.php'); ?>
--------------------------------------------------------------------------------
/post.php:
--------------------------------------------------------------------------------
1 | need('includes/head.php');
11 | $this->need('includes/header.php');
12 | ?>
13 |
14 |
15 |
16 | need('includes/footer.php'); ?>
--------------------------------------------------------------------------------