1861 lines
91 KiB
PHP
1861 lines
91 KiB
PHP
|
|
<?php
|
|||
|
|
/**
|
|||
|
|
* 豆瓣图书
|
|||
|
|
*
|
|||
|
|
* @package BookInfo
|
|||
|
|
* @author 石头厝
|
|||
|
|
* @version 3.8.0
|
|||
|
|
* @link http://www.shitoucuo.com
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
|
|||
|
|
|
|||
|
|
class BookInfo_Plugin implements Typecho_Plugin_Interface
|
|||
|
|
{
|
|||
|
|
/**
|
|||
|
|
* 激活插件
|
|||
|
|
*/
|
|||
|
|
public static function activate()
|
|||
|
|
{
|
|||
|
|
Typecho_Plugin::factory('Widget_Abstract_Contents')->contentEx = array('BookInfo_Plugin', 'parse');
|
|||
|
|
Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array('BookInfo_Plugin', 'parse');
|
|||
|
|
Typecho_Plugin::factory('admin/write-post.php')->bottom = array('BookInfo_Plugin', 'renderButton');
|
|||
|
|
Typecho_Plugin::factory('admin/write-page.php')->bottom = array('BookInfo_Plugin', 'renderButton');
|
|||
|
|
|
|||
|
|
$cacheDir = dirname(__FILE__) . '/cache/';
|
|||
|
|
if (!file_exists($cacheDir)) mkdir($cacheDir, 0755, true);
|
|||
|
|
|
|||
|
|
return '插件激活成功!';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 禁用插件
|
|||
|
|
*/
|
|||
|
|
public static function deactivate()
|
|||
|
|
{
|
|||
|
|
return '插件已禁用';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 配置面板
|
|||
|
|
*/
|
|||
|
|
public static function config(Typecho_Widget_Helper_Form $form)
|
|||
|
|
{
|
|||
|
|
$cacheEnable = new Typecho_Widget_Helper_Form_Element_Radio('cacheEnable',
|
|||
|
|
array('1' => '启用', '0' => '禁用'),
|
|||
|
|
'1', '启用缓存', '缓存图书信息,提升访问速度');
|
|||
|
|
$form->addInput($cacheEnable);
|
|||
|
|
|
|||
|
|
$cacheTime = new Typecho_Widget_Helper_Form_Element_Text('cacheTime', NULL, '30',
|
|||
|
|
'缓存时间(天)', '图书信息缓存保留天数');
|
|||
|
|
$cacheTime->addRule('isInteger', '请输入整数');
|
|||
|
|
$form->addInput($cacheTime);
|
|||
|
|
|
|||
|
|
$imageProxy = new Typecho_Widget_Helper_Form_Element_Text('imageProxy', NULL,
|
|||
|
|
'https://images.weserv.nl/?url=', '图片代理', '用于加载豆瓣图片');
|
|||
|
|
$form->addInput($imageProxy);
|
|||
|
|
|
|||
|
|
$defaultCover = new Typecho_Widget_Helper_Form_Element_Text('defaultCover', NULL,
|
|||
|
|
'https://img9.doubanio.com/f/shire/5522dd1f5b742d1e1394a17f44d590646b63871d/pics/book-default-lpic.gif',
|
|||
|
|
'默认封面', '当无法获取封面时显示的图片');
|
|||
|
|
$form->addInput($defaultCover);
|
|||
|
|
|
|||
|
|
$summaryLength = new Typecho_Widget_Helper_Form_Element_Text('summaryLength', NULL, '200',
|
|||
|
|
'简介显示长度', '简介默认显示的最大字符数,超出部分可展开查看');
|
|||
|
|
$summaryLength->addRule('isInteger', '请输入整数');
|
|||
|
|
$form->addInput($summaryLength);
|
|||
|
|
|
|||
|
|
$expandText = new Typecho_Widget_Helper_Form_Element_Text('expandText', NULL, '展开',
|
|||
|
|
'"展开"文字', '点击展开完整简介的文字');
|
|||
|
|
$form->addInput($expandText);
|
|||
|
|
|
|||
|
|
$collapseText = new Typecho_Widget_Helper_Form_Element_Text('collapseText', NULL, '收起',
|
|||
|
|
'"收起"文字', '点击收起简介的文字');
|
|||
|
|
$form->addInput($collapseText);
|
|||
|
|
|
|||
|
|
$expandColor = new Typecho_Widget_Helper_Form_Element_Text('expandColor', NULL, '#0073aa',
|
|||
|
|
'展开按钮颜色', '展开/收起按钮的文字颜色');
|
|||
|
|
$form->addInput($expandColor);
|
|||
|
|
|
|||
|
|
// 新增:独立页面每页显示条数设置
|
|||
|
|
$pageSize = new Typecho_Widget_Helper_Form_Element_Text('pageSize', NULL, '10',
|
|||
|
|
'独立页面每页显示条数', '在独立页面中每页显示的图书数量(1-50)');
|
|||
|
|
$pageSize->addRule('isInteger', '请输入整数');
|
|||
|
|
$pageSize->addRule(array(new BookInfo_Plugin, 'validatePageSize'), '请输入1-50之间的整数');
|
|||
|
|
$form->addInput($pageSize);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证页面显示条数
|
|||
|
|
*/
|
|||
|
|
public static function validatePageSize($value)
|
|||
|
|
{
|
|||
|
|
$value = intval($value);
|
|||
|
|
if ($value < 1 || $value > 50) {
|
|||
|
|
throw new Typecho_Widget_Exception('请输入1-50之间的整数');
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 个人配置面板
|
|||
|
|
*/
|
|||
|
|
public static function personalConfig(Typecho_Widget_Helper_Form $form)
|
|||
|
|
{
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 解析短代码
|
|||
|
|
*/
|
|||
|
|
public static function parse($content, $widget, $lastResult)
|
|||
|
|
{
|
|||
|
|
$content = empty($lastResult) ? $content : $lastResult;
|
|||
|
|
|
|||
|
|
// 如果是独立页面,且内容中包含[all_books]标记,则显示所有图书
|
|||
|
|
if ($widget instanceof Widget_Archive && $widget->is('page')) {
|
|||
|
|
// 支持[all_books]和[all_books:page=1]格式
|
|||
|
|
$pattern = '/\[all_books(?::page=(\d+))?\]/i';
|
|||
|
|
if (preg_match_all($pattern, $content, $matches)) {
|
|||
|
|
foreach ($matches[0] as $key => $match) {
|
|||
|
|
$page = isset($matches[1][$key]) ? intval($matches[1][$key]) : 1;
|
|||
|
|
$allBooksHtml = self::renderAllBooks($page);
|
|||
|
|
$content = str_replace($match, $allBooksHtml, $content);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是单篇文章,解析图书短代码
|
|||
|
|
if ($widget instanceof Widget_Archive && $widget->is('single')) {
|
|||
|
|
// 匹配 [book:数字] 或 [book:数字:短评] 格式
|
|||
|
|
$pattern = '/\[book:(\d+)(?::([^\]]+))?\]/i';
|
|||
|
|
if (preg_match_all($pattern, $content, $matches)) {
|
|||
|
|
foreach ($matches[0] as $key => $match) {
|
|||
|
|
$bookId = $matches[1][$key];
|
|||
|
|
$reviewWithCustom = isset($matches[2][$key]) ? trim($matches[2][$key]) : '';
|
|||
|
|
|
|||
|
|
$review = '';
|
|||
|
|
$customData = array();
|
|||
|
|
|
|||
|
|
// 解码短评和自定义数据
|
|||
|
|
if (!empty($reviewWithCustom)) {
|
|||
|
|
// 分离短评和自定义数据
|
|||
|
|
if (strpos($reviewWithCustom, '|CUSTOM:') !== false) {
|
|||
|
|
list($review, $customJson) = explode('|CUSTOM:', $reviewWithCustom, 2);
|
|||
|
|
|
|||
|
|
// 解码自定义数据
|
|||
|
|
if (!empty($customJson)) {
|
|||
|
|
// URL解码
|
|||
|
|
$decodedJson = urldecode($customJson);
|
|||
|
|
// 解析JSON
|
|||
|
|
$customData = json_decode($decodedJson, true);
|
|||
|
|
if (!is_array($customData)) {
|
|||
|
|
$customData = array();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 检查是否是纯自定义数据(没有短评)
|
|||
|
|
if (preg_match('/^\(自定义:(.*)\)$/', $reviewWithCustom, $customMatch)) {
|
|||
|
|
// 这是旧格式的自定义数据,需要转换
|
|||
|
|
$review = '';
|
|||
|
|
$customData = self::parseLegacyCustomData($customMatch[1]);
|
|||
|
|
} else {
|
|||
|
|
// 重要修复:处理中文自定义数据(如:短评(自定义:开始阅读:2025.12.03))
|
|||
|
|
// 检查是否包含中文括号格式的自定义数据
|
|||
|
|
if (preg_match('/^(.*?)(自定义:(.*))$/u', $reviewWithCustom, $customMatch)) {
|
|||
|
|
// 第一部分是短评
|
|||
|
|
$review = trim($customMatch[1]);
|
|||
|
|
// 第二部分是自定义数据
|
|||
|
|
$customData = self::parseLegacyCustomData($customMatch[2]);
|
|||
|
|
} else {
|
|||
|
|
// 没有自定义数据,只有短评
|
|||
|
|
$review = $reviewWithCustom;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解码短评(处理特殊字符)- 修复:只解码真正的短评部分
|
|||
|
|
if (!empty($review)) {
|
|||
|
|
// 处理HTML实体转义
|
|||
|
|
$review = str_replace(array('[', ']'), array('[', ']'), $review);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取图书数据(包含短评和自定义数据)
|
|||
|
|
$bookHtml = self::renderBook($bookId, $review, $customData);
|
|||
|
|
$content = str_replace($match, $bookHtml, $content);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 解析旧格式的自定义数据
|
|||
|
|
*/
|
|||
|
|
private static function parseLegacyCustomData($customText)
|
|||
|
|
{
|
|||
|
|
$customData = array();
|
|||
|
|
|
|||
|
|
// 解析旧格式:开始阅读:2025.12.03,结束阅读:2025.12.07,阅读方法:速读,图书分类:小说,推荐指数:★★
|
|||
|
|
$pairs = explode(',', $customText);
|
|||
|
|
foreach ($pairs as $pair) {
|
|||
|
|
if (strpos($pair, ':') !== false) {
|
|||
|
|
list($key, $value) = explode(':', $pair, 2);
|
|||
|
|
switch (trim($key)) {
|
|||
|
|
case '开始阅读':
|
|||
|
|
$customData['startDate'] = trim($value);
|
|||
|
|
break;
|
|||
|
|
case '结束阅读':
|
|||
|
|
$customData['readDate'] = trim($value);
|
|||
|
|
break;
|
|||
|
|
case '阅读方法':
|
|||
|
|
$customData['readMethod'] = trim($value);
|
|||
|
|
break;
|
|||
|
|
case '图书分类':
|
|||
|
|
$customData['bookCategory'] = trim($value);
|
|||
|
|
break;
|
|||
|
|
case '推荐指数':
|
|||
|
|
// 计算星星数量
|
|||
|
|
$starCount = substr_count($value, '★');
|
|||
|
|
$customData['recommendation'] = $starCount;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $customData;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 渲染单本图书信息
|
|||
|
|
*/
|
|||
|
|
private static function renderBook($bookId, $review = '', $customData = array())
|
|||
|
|
{
|
|||
|
|
// 获取图书数据(包含短评和自定义数据)
|
|||
|
|
$bookData = self::getBookData($bookId, $review, $customData);
|
|||
|
|
|
|||
|
|
if (!$bookData || empty($bookData['title'])) {
|
|||
|
|
return '<div class="book-error" style="padding:10px; background:var(--book-error-bg); color:var(--book-error-text); border:1px solid var(--book-error-border); border-radius:4px; margin:10px 0;">
|
|||
|
|
获取图书信息失败,ID:' . htmlspecialchars($bookId) . '
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$options = Typecho_Widget::widget('Widget_Options')->plugin('BookInfo');
|
|||
|
|
$imageProxy = isset($options->imageProxy) ? $options->imageProxy : 'https://images.weserv.nl/?url=';
|
|||
|
|
$defaultCover = isset($options->defaultCover) ? $options->defaultCover :
|
|||
|
|
'https://img9.doubanio.com/f/shire/5522dd1f5b742d1e1394a17f44d590646b63871d/pics/book-default-lpic.gif';
|
|||
|
|
$summaryLength = isset($options->summaryLength) ? intval($options->summaryLength) : 200;
|
|||
|
|
$expandText = isset($options->expandText) ? $options->expandText : '展开';
|
|||
|
|
$collapseText = isset($options->collapseText) ? $options->collapseText : '收起';
|
|||
|
|
$expandColor = isset($options->expandColor) ? $options->expandColor : '#0073aa';
|
|||
|
|
|
|||
|
|
$title = htmlspecialchars($bookData['title']);
|
|||
|
|
$author = is_array($bookData['author']) ? implode(', ', $bookData['author']) : htmlspecialchars($bookData['author']);
|
|||
|
|
$summary = isset($bookData['summary']) ? $bookData['summary'] : '';
|
|||
|
|
|
|||
|
|
// 重要修复:直接从传入的$review获取短评,确保与编辑器输入一致
|
|||
|
|
$review = htmlspecialchars($review);
|
|||
|
|
|
|||
|
|
// 豆瓣抓取字段
|
|||
|
|
$publisher = isset($bookData['publisher']) ? htmlspecialchars($bookData['publisher']) : '未知';
|
|||
|
|
$pubdate = isset($bookData['pubdate']) ? htmlspecialchars($bookData['pubdate']) : '未知';
|
|||
|
|
$pages = isset($bookData['pages']) ? htmlspecialchars($bookData['pages']) : '未知';
|
|||
|
|
$rating = isset($bookData['rating']) ? floatval($bookData['rating']) : 0;
|
|||
|
|
$ratingCount = isset($bookData['rating_count']) ? intval($bookData['rating_count']) : 0;
|
|||
|
|
|
|||
|
|
// 自定义字段 - 修复:确保正确处理customData
|
|||
|
|
$startDate = isset($customData['startDate']) ? htmlspecialchars($customData['startDate']) : '';
|
|||
|
|
$readDate = isset($customData['readDate']) ? htmlspecialchars($customData['readDate']) : '';
|
|||
|
|
$readMethod = isset($customData['readMethod']) ? htmlspecialchars($customData['readMethod']) : '';
|
|||
|
|
$bookCategory = isset($customData['bookCategory']) ? htmlspecialchars($customData['bookCategory']) : '';
|
|||
|
|
$recommendation = isset($customData['recommendation']) ? intval($customData['recommendation']) : 0;
|
|||
|
|
|
|||
|
|
// 如果customData中没有,尝试从bookData中获取
|
|||
|
|
if (empty($startDate) && isset($bookData['custom_start_date']) && !empty($bookData['custom_start_date'])) {
|
|||
|
|
$startDate = htmlspecialchars($bookData['custom_start_date']);
|
|||
|
|
}
|
|||
|
|
if (empty($readDate) && isset($bookData['custom_read_date']) && !empty($bookData['custom_read_date'])) {
|
|||
|
|
$readDate = htmlspecialchars($bookData['custom_read_date']);
|
|||
|
|
}
|
|||
|
|
if (empty($readMethod) && isset($bookData['custom_read_method']) && !empty($bookData['custom_read_method'])) {
|
|||
|
|
$readMethod = htmlspecialchars($bookData['custom_read_method']);
|
|||
|
|
}
|
|||
|
|
if (empty($bookCategory) && isset($bookData['custom_book_category']) && !empty($bookData['custom_book_category'])) {
|
|||
|
|
$bookCategory = htmlspecialchars($bookData['custom_book_category']);
|
|||
|
|
}
|
|||
|
|
if ($recommendation == 0 && isset($bookData['custom_recommendation'])) {
|
|||
|
|
$recommendation = intval($bookData['custom_recommendation']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查简介是否超过当前设置的限制长度
|
|||
|
|
$isSummaryLong = false;
|
|||
|
|
$summaryShort = $summary;
|
|||
|
|
|
|||
|
|
// 移除HTML标签来计算纯文本长度
|
|||
|
|
$plainSummary = strip_tags($summary);
|
|||
|
|
if (mb_strlen($plainSummary, 'UTF-8') > $summaryLength) {
|
|||
|
|
$isSummaryLong = true;
|
|||
|
|
// 截取纯文本
|
|||
|
|
$plainShort = mb_substr($plainSummary, 0, $summaryLength, 'UTF-8');
|
|||
|
|
// 尝试保持HTML结构,但这是一个简化的处理
|
|||
|
|
$summaryShort = $plainShort . '...';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$coverUrl = !empty($bookData['image']) ? $bookData['image'] : $defaultCover;
|
|||
|
|
$coverSrc = $imageProxy . urlencode($coverUrl);
|
|||
|
|
|
|||
|
|
// 生成评分显示
|
|||
|
|
$ratingHtml = '';
|
|||
|
|
if ($rating > 0) {
|
|||
|
|
$ratingHtml = '<div class="book-rating" style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span class="book-label" style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">豆瓣评分:</span>
|
|||
|
|
<span class="book-rating-value" style="color:var(--book-rating-color); font-weight:600; font-size:16px;">' . number_format($rating, 1) . '</span>';
|
|||
|
|
|
|||
|
|
if ($ratingCount > 0) {
|
|||
|
|
$ratingHtml .= ' <span class="book-rating-count" style="color:var(--book-text-tertiary); font-size:13px;">(' . number_format($ratingCount) . '人评价)</span>';
|
|||
|
|
}
|
|||
|
|
$ratingHtml .= '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成推荐指数星星
|
|||
|
|
$recommendationHtml = '';
|
|||
|
|
if ($recommendation > 0) {
|
|||
|
|
$recommendationHtml = '<div class="book-recommendation" style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span class="book-label" style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">推荐指数:</span>';
|
|||
|
|
|
|||
|
|
for ($i = 1; $i <= 5; $i++) {
|
|||
|
|
if ($i <= $recommendation) {
|
|||
|
|
$recommendationHtml .= '<span style="color:var(--book-star-color); font-size:16px; margin-right:2px;">★</span>';
|
|||
|
|
} else {
|
|||
|
|
$recommendationHtml .= '<span style="color:var(--book-star-empty-color); font-size:16px; margin-right:2px;">★</span>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$recommendationHtml .= '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成自定义信息HTML
|
|||
|
|
$customInfoHtml = '';
|
|||
|
|
$hasCustomInfo = false;
|
|||
|
|
|
|||
|
|
// 生成右侧栏的自定义信息(无标题,样式与中间栏一致)
|
|||
|
|
if ($startDate || $readDate || $readMethod || $bookCategory) {
|
|||
|
|
$hasCustomInfo = true;
|
|||
|
|
|
|||
|
|
if ($startDate) {
|
|||
|
|
$customInfoHtml .= '<div class="book-custom-item" style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span class="book-label" style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">开始阅读:</span>
|
|||
|
|
<span class="book-custom-value" style="color:var(--book-text-primary);">' . $startDate . '</span>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($readDate) {
|
|||
|
|
$customInfoHtml .= '<div class="book-custom-item" style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span class="book-label" style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">结束阅读:</span>
|
|||
|
|
<span class="book-custom-value" style="color:var(--book-text-primary);">' . $readDate . '</span>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($readMethod) {
|
|||
|
|
$customInfoHtml .= '<div class="book-custom-item" style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span class="book-label" style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">阅读方法:</span>
|
|||
|
|
<span class="book-custom-value" style="color:var(--book-text-primary);">' . $readMethod . '</span>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($bookCategory) {
|
|||
|
|
$customInfoHtml .= '<div class="book-custom-item" style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span class="book-label" style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">图书分类:</span>
|
|||
|
|
<span class="book-custom-value" style="color:var(--book-text-primary);">' . $bookCategory . '</span>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成右侧栏的完整HTML
|
|||
|
|
$rightColumnHtml = '';
|
|||
|
|
|
|||
|
|
if ($recommendationHtml) {
|
|||
|
|
$rightColumnHtml .= $recommendationHtml;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($customInfoHtml) {
|
|||
|
|
$rightColumnHtml .= $customInfoHtml;
|
|||
|
|
} else {
|
|||
|
|
// 如果没有自定义信息,显示占位符
|
|||
|
|
$rightColumnHtml .= '<div class="book-custom-item" style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span class="book-label" style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">阅读记录:</span>
|
|||
|
|
<span class="book-custom-value" style="color:var(--book-text-tertiary);">暂无记录</span>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成简介部分HTML,包含展开/收起功能
|
|||
|
|
$summaryHtml = '<div class="book-summary-container">';
|
|||
|
|
|
|||
|
|
if ($isSummaryLong) {
|
|||
|
|
// 长简介:显示短版本 + 展开按钮
|
|||
|
|
$summaryHtml .= '
|
|||
|
|
<div class="book-summary-short" style="display:block;">
|
|||
|
|
<div class="book-summary-content" style="font-size:14px; line-height:1.8; color:var(--book-text-primary); text-align:justify; font-family:\"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif;">
|
|||
|
|
' . nl2br($summaryShort) . '
|
|||
|
|
<a href="javascript:void(0);" class="book-summary-expand"
|
|||
|
|
onclick="toggleBookSummary(this)"
|
|||
|
|
style="color:' . htmlspecialchars($expandColor) . '; text-decoration:none; font-weight:600; cursor:pointer; margin-left:5px; padding:2px 8px; background:var(--book-button-bg); border-radius:4px; border:1px solid var(--book-border); font-size:13px; transition:all 0.2s ease;"
|
|||
|
|
onmouseover="this.style.backgroundColor=\'var(--book-button-hover-bg)\'; this.style.borderColor=\'' . htmlspecialchars($expandColor) . '\';"
|
|||
|
|
onmouseout="this.style.backgroundColor=\'var(--book-button-bg)\'; this.style.borderColor=\'var(--book-border)\';">'
|
|||
|
|
. htmlspecialchars($expandText) . ' ↓
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="book-summary-full" style="display:none;">
|
|||
|
|
<div class="book-summary-content" style="font-size:14px; line-height:1.8; color:var(--book-text-primary); text-align:justify; font-family:\"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif;">
|
|||
|
|
' . $summary . '
|
|||
|
|
<a href="javascript:void(0);" class="book-summary-collapse"
|
|||
|
|
onclick="toggleBookSummary(this)"
|
|||
|
|
style="color:' . htmlspecialchars($expandColor) . '; text-decoration:none; font-weight:600; cursor:pointer; margin-left:5px; padding:2px 8px; background:var(--book-button-bg); border-radius:4px; border:1px solid var(--book-border); font-size:13px; transition:all 0.2s ease;"
|
|||
|
|
onmouseover="this.style.backgroundColor=\'var(--book-button-hover-bg)\'; this.style.borderColor=\'' . htmlspecialchars($expandColor) . '\';"
|
|||
|
|
onmouseout="this.style.backgroundColor=\'var(--book-button-bg)\'; this.style.borderColor=\'var(--book-border)\';">'
|
|||
|
|
. htmlspecialchars($collapseText) . ' ↑
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
</div>';
|
|||
|
|
} else {
|
|||
|
|
// 短简介:直接显示完整内容
|
|||
|
|
$summaryHtml .= '
|
|||
|
|
<div class="book-summary-content" style="font-size:14px; line-height:1.8; color:var(--book-text-primary); text-align:justify; font-family:\"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif;">
|
|||
|
|
' . $summary . '
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$summaryHtml .= '</div>';
|
|||
|
|
|
|||
|
|
// 生成短评HTML(如果有短评)- 只显示纯粹的短评
|
|||
|
|
$reviewHtml = '';
|
|||
|
|
if (!empty($review)) {
|
|||
|
|
$reviewHtml = '<div class="book-review-section" style="margin-top:15px;">
|
|||
|
|
<div class="book-section-title" style="margin:0 0 8px 0; font-size:14px; color:var(--book-text-secondary); font-weight:600; padding-bottom:8px; border-bottom:1px solid var(--book-border);">💭 我的短评</div>
|
|||
|
|
<div class="book-review-content" style="font-size:14px; line-height:1.8; color:var(--book-text-primary); text-align:justify; background:var(--book-review-bg); padding:15px; border-radius:8px; border-left:3px solid ' . htmlspecialchars($expandColor) . '; font-family:\"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif;">
|
|||
|
|
' . nl2br($review) . '
|
|||
|
|
</div>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html = <<<HTML
|
|||
|
|
<!-- 深色模式CSS变量 -->
|
|||
|
|
<style>
|
|||
|
|
|
|||
|
|
|
|||
|
|
.dark .markdown-body h2{margin-bottom:0px!important;padding-bottom:0px!important;}
|
|||
|
|
:root {
|
|||
|
|
/* 浅色主题变量 */
|
|||
|
|
--book-bg-primary: #ffffff;
|
|||
|
|
--book-bg-secondary: #f9fafb;
|
|||
|
|
--book-bg-tertiary: #fafafa;
|
|||
|
|
--book-bg-header: linear-gradient(135deg, #f9fafb 0%, #f1f3f5 100%);
|
|||
|
|
--book-bg-review: #f9f9f9;
|
|||
|
|
--book-text-primary: #1a1a1a;
|
|||
|
|
--book-text-secondary: #555;
|
|||
|
|
--book-text-tertiary: #666;
|
|||
|
|
--book-border: #eaeaea;
|
|||
|
|
--book-border-light: #f0f0f0;
|
|||
|
|
--book-rating-color: #ffac2d;
|
|||
|
|
--book-star-color: #ff6b35;
|
|||
|
|
--book-star-empty-color: #ddd;
|
|||
|
|
--book-button-bg: #fff;
|
|||
|
|
--book-button-hover-bg: #e9ecef;
|
|||
|
|
--book-error-bg: #f8d7da;
|
|||
|
|
--book-error-text: #721c24;
|
|||
|
|
--book-error-border: #f5c6cb;
|
|||
|
|
--book-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|||
|
|
--book-shadow-light: 0 2px 8px rgba(0,0,0,0.05);
|
|||
|
|
--book-shadow-hover: 0 6px 16px rgba(0,0,0,0.12);
|
|||
|
|
--book-index-bg: linear-gradient(135deg, #0073aa, #0056b3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (prefers-color-scheme: dark) {
|
|||
|
|
:root {
|
|||
|
|
/* 深色主题变量 */
|
|||
|
|
--book-bg-primary: #1e1e1e;
|
|||
|
|
--book-bg-secondary: #2d2d2d;
|
|||
|
|
--book-bg-tertiary: #252525;
|
|||
|
|
--book-bg-header: linear-gradient(135deg, #2d2d2d 0%, #252525 100%);
|
|||
|
|
--book-bg-review: #2a2a2a;
|
|||
|
|
--book-text-primary: #e0e0e0;
|
|||
|
|
--book-text-secondary: #b0b0b0;
|
|||
|
|
--book-text-tertiary: #888;
|
|||
|
|
--book-border: #444;
|
|||
|
|
--book-border-light: #333;
|
|||
|
|
--book-rating-color: #ffac2d;
|
|||
|
|
--book-star-color: #ff8c42;
|
|||
|
|
--book-star-empty-color: #555;
|
|||
|
|
--book-button-bg: #3a3a3a;
|
|||
|
|
--book-button-hover-bg: #4a4a4a;
|
|||
|
|
--book-error-bg: #5c2a2a;
|
|||
|
|
--book-error-text: #ffb8b8;
|
|||
|
|
--book-error-border: #7a3a3a;
|
|||
|
|
--book-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|||
|
|
--book-shadow-light: 0 2px 8px rgba(0,0,0,0.2);
|
|||
|
|
--book-shadow-hover: 0 6px 16px rgba(0,0,0,0.4);
|
|||
|
|
--book-index-bg: linear-gradient(135deg, #0073aa, #0056b3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 保持链接颜色在深色模式下的可读性 */
|
|||
|
|
.book-card a {
|
|||
|
|
color: #66b3ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.book-card a:hover {
|
|||
|
|
color: #99ccff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 深色模式下的卡片样式 */
|
|||
|
|
.book-card {
|
|||
|
|
background: var(--book-bg-primary);
|
|||
|
|
color: var(--book-text-primary);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|
|||
|
|
<div class="book-card-h">
|
|||
|
|
<div class="book-card" style="background:var(--book-bg-primary); margin-top:10px;border-radius:12px; box-shadow:var(--book-shadow); overflow:hidden; border:1px solid var(--book-border);">
|
|||
|
|
<!-- 上栏:标题 -->
|
|||
|
|
<div class="book-header" style="padding:20px 25px; background:var(--book-bg-header); border-bottom:1px solid var(--book-border-light);">
|
|||
|
|
<h2 style="margin:0; font-size:22px; line-height:1.3; color:var(--book-text-primary); font-weight:700; text-align:center;">
|
|||
|
|
<a href="https://book.douban.com/subject/{$bookId}/" target="_blank"
|
|||
|
|
style="color:#0073aa; text-decoration:none; transition:color 0.2s ease;"
|
|||
|
|
onmouseover="this.style.color='#0056b3';"
|
|||
|
|
onmouseout="this.style.color='#0073aa';">
|
|||
|
|
《{$title}》
|
|||
|
|
</a>
|
|||
|
|
</h2>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 中栏:三列布局 -->
|
|||
|
|
<div class="book-main" style="padding:25px; border-bottom:1px solid var(--book-border-light); background:var(--book-bg-primary);">
|
|||
|
|
<div style="display:grid; grid-template-columns:120px 1fr 1fr; gap:25px; align-items:stretch;">
|
|||
|
|
<!-- 左侧:图书封面 - 固定高度容器 -->
|
|||
|
|
<div style="display:flex; align-items:center; justify-content:center;">
|
|||
|
|
<a href="https://book.douban.com/subject/{$bookId}/" target="_blank" style="display:block; text-decoration:none; width:100%;">
|
|||
|
|
<div style="position:relative; width:100%;">
|
|||
|
|
<div style="width:100%; height:0; padding-bottom:140%; position:relative; overflow:hidden; border-radius:8px; border:3px solid var(--book-bg-primary); box-shadow:0 4px 12px rgba(0,0,0,0.12); background:var(--book-bg-tertiary);">
|
|||
|
|
<img src="{$coverSrc}" alt="{$title}"
|
|||
|
|
style="position:absolute; top:0; left:0; width:100%; height:100%; object-fit:cover; transition:transform 0.3s ease;"
|
|||
|
|
onerror="this.src='{$defaultCover}';"
|
|||
|
|
onmouseover="this.style.transform='scale(1.05)';"
|
|||
|
|
onmouseout="this.style.transform='scale(1)';">
|
|||
|
|
</div>
|
|||
|
|
<div style="position:absolute; top:-5px; right:-5px; background:#0073aa; color:white; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:bold; box-shadow:0 2px 4px rgba(0,115,170,0.3);">📚</div>
|
|||
|
|
</div>
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 中间:豆瓣图书信息 -->
|
|||
|
|
<div style="padding-right:15px; border-right:1px solid var(--book-border);">
|
|||
|
|
<div style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">作者:</span>
|
|||
|
|
<span style="color:#0073aa;">{$author}</span>
|
|||
|
|
</div>
|
|||
|
|
<div style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">出版社:</span>
|
|||
|
|
<span style="color:var(--book-text-primary);">{$publisher}</span>
|
|||
|
|
</div>
|
|||
|
|
<div style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">出版年:</span>
|
|||
|
|
<span style="color:var(--book-text-primary);">{$pubdate}</span>
|
|||
|
|
</div>
|
|||
|
|
<div style="margin:0 0 10px 0; font-size:14px; color:var(--book-text-secondary);">
|
|||
|
|
<span style="font-weight:600; color:var(--book-text-primary); display:inline-block; width:70px;">页数:</span>
|
|||
|
|
<span style="color:var(--book-text-primary);">{$pages}</span>
|
|||
|
|
</div>
|
|||
|
|
{$ratingHtml}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 右侧:自定义信息 -->
|
|||
|
|
<div style="padding-left:15px;">
|
|||
|
|
{$rightColumnHtml}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 下栏:内容简介和短评 -->
|
|||
|
|
<div style="padding:20px 25px 25px 25px;">
|
|||
|
|
<!-- 内容简介 -->
|
|||
|
|
<div style="margin-bottom:20px;">
|
|||
|
|
<div style="margin:0 0 12px 0; font-size:16px; color:var(--book-text-primary); font-weight:600; display:flex; align-items:center; gap:8px; padding-bottom:8px; border-bottom:1px solid var(--book-border);">
|
|||
|
|
<span style="background:#28a745; color:white; width:28px; height:28px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:14px;">📖</span>
|
|||
|
|
<span>内容简介</span>
|
|||
|
|
</div>
|
|||
|
|
<div style="background:var(--book-bg-tertiary); padding:20px; border-radius:8px; border:1px solid var(--book-border); box-shadow:0 1px 3px rgba(0,0,0,0.05);">
|
|||
|
|
{$summaryHtml}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 短评 -->
|
|||
|
|
{$reviewHtml}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
HTML;
|
|||
|
|
|
|||
|
|
// 添加JavaScript切换函数
|
|||
|
|
$html .= '
|
|||
|
|
<script>
|
|||
|
|
function toggleBookSummary(element) {
|
|||
|
|
var container = element.closest(".book-summary-container");
|
|||
|
|
var shortDiv = container.querySelector(".book-summary-short");
|
|||
|
|
var fullDiv = container.querySelector(".book-summary-full");
|
|||
|
|
|
|||
|
|
if (shortDiv && fullDiv) {
|
|||
|
|
if (shortDiv.style.display === "none") {
|
|||
|
|
// 收起
|
|||
|
|
shortDiv.style.display = "block";
|
|||
|
|
fullDiv.style.display = "none";
|
|||
|
|
} else {
|
|||
|
|
// 展开
|
|||
|
|
shortDiv.style.display = "none";
|
|||
|
|
fullDiv.style.display = "block";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>';
|
|||
|
|
|
|||
|
|
return $html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有图书数据(支持分页)
|
|||
|
|
*/
|
|||
|
|
private static function getAllBooksData($page = 1, $pageSize = 10)
|
|||
|
|
{
|
|||
|
|
$cacheDir = dirname(__FILE__) . '/cache/';
|
|||
|
|
$allBooks = array();
|
|||
|
|
|
|||
|
|
// 扫描缓存目录
|
|||
|
|
if (file_exists($cacheDir)) {
|
|||
|
|
$files = scandir($cacheDir);
|
|||
|
|
foreach ($files as $file) {
|
|||
|
|
if (pathinfo($file, PATHINFO_EXTENSION) === 'json') {
|
|||
|
|
$filePath = $cacheDir . $file;
|
|||
|
|
$content = file_get_contents($filePath);
|
|||
|
|
if ($content) {
|
|||
|
|
$data = json_decode($content, true);
|
|||
|
|
if ($data && isset($data['fetched_at'])) {
|
|||
|
|
// 添加文件名作为bookId
|
|||
|
|
$bookId = pathinfo($file, PATHINFO_FILENAME);
|
|||
|
|
$data['bookId'] = $bookId;
|
|||
|
|
|
|||
|
|
// 使用文件修改时间作为添加时间(如果文件不存在,使用fetched_at)
|
|||
|
|
if (file_exists($filePath)) {
|
|||
|
|
$data['added_time'] = filemtime($filePath);
|
|||
|
|
} else {
|
|||
|
|
$data['added_time'] = isset($data['fetched_at']) ? $data['fetched_at'] : time();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$allBooks[] = $data;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 按added_time倒序排序(最新的在最前面)
|
|||
|
|
usort($allBooks, function($a, $b) {
|
|||
|
|
return $b['added_time'] - $a['added_time'];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$total = count($allBooks);
|
|||
|
|
$totalPages = ceil($total / $pageSize);
|
|||
|
|
|
|||
|
|
// 限制页码范围
|
|||
|
|
$page = max(1, min($page, $totalPages));
|
|||
|
|
|
|||
|
|
// 分页处理
|
|||
|
|
$startIndex = ($page - 1) * $pageSize;
|
|||
|
|
$paginatedBooks = array_slice($allBooks, $startIndex, $pageSize);
|
|||
|
|
|
|||
|
|
return array(
|
|||
|
|
'books' => $paginatedBooks,
|
|||
|
|
'total' => $total,
|
|||
|
|
'page' => $page,
|
|||
|
|
'pageSize' => $pageSize,
|
|||
|
|
'totalPages' => $totalPages,
|
|||
|
|
'startIndex' => $startIndex + 1,
|
|||
|
|
'endIndex' => min($startIndex + $pageSize, $total)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成分页HTML
|
|||
|
|
*/
|
|||
|
|
private static function generatePagination($currentPage, $totalPages, $baseUrl = '')
|
|||
|
|
{
|
|||
|
|
if ($totalPages <= 1) {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html = '<div class="book-pagination" style="margin:30px 0; text-align:center;">';
|
|||
|
|
$html .= '<div style="display:inline-flex; align-items:center; gap:8px; background:var(--book-bg-primary); padding:12px 20px; border-radius:8px; box-shadow:var(--book-shadow-light); border:1px solid var(--book-border);">';
|
|||
|
|
|
|||
|
|
// 首页
|
|||
|
|
if ($currentPage > 1) {
|
|||
|
|
$html .= '<a href="' . self::getPageUrl(1, $baseUrl) . '" style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:var(--book-button-bg); color:#0073aa; text-decoration:none; font-size:14px; font-weight:600; transition:all 0.2s ease;"
|
|||
|
|
onmouseover="this.style.background=\'#0073aa\'; this.style.color=\'white\';"
|
|||
|
|
onmouseout="this.style.background=\'var(--book-button-bg)\'; this.style.color=\'#0073aa\';">«</a>';
|
|||
|
|
} else {
|
|||
|
|
$html .= '<span style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:var(--book-button-bg); color:var(--book-text-tertiary); font-size:14px; font-weight:600;">«</span>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 上一页
|
|||
|
|
if ($currentPage > 1) {
|
|||
|
|
$html .= '<a href="' . self::getPageUrl($currentPage - 1, $baseUrl) . '" style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:var(--book-button-bg); color:#0073aa; text-decoration:none; font-size:14px; font-weight:600; transition:all 0.2s ease;"
|
|||
|
|
onmouseover="this.style.background=\'#0073aa\'; this.style.color=\'white\';"
|
|||
|
|
onmouseout="this.style.background=\'var(--book-button-bg)\'; this.style.color=\'#0073aa\';">‹</a>';
|
|||
|
|
} else {
|
|||
|
|
$html .= '<span style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:var(--book-button-bg); color:var(--book-text-tertiary); font-size:14px; font-weight:600;">‹</span>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 页码显示
|
|||
|
|
$startPage = max(1, $currentPage - 2);
|
|||
|
|
$endPage = min($totalPages, $currentPage + 2);
|
|||
|
|
|
|||
|
|
for ($i = $startPage; $i <= $endPage; $i++) {
|
|||
|
|
if ($i == $currentPage) {
|
|||
|
|
$html .= '<span style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:#0073aa; color:white; font-size:14px; font-weight:600;">' . $i . '</span>';
|
|||
|
|
} else {
|
|||
|
|
$html .= '<a href="' . self::getPageUrl($i, $baseUrl) . '" style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:var(--book-button-bg); color:#0073aa; text-decoration:none; font-size:14px; font-weight:600; transition:all 0.2s ease;"
|
|||
|
|
onmouseover="this.style.background=\'#e6f3ff\'; this.style.color=\'#0073aa\';"
|
|||
|
|
onmouseout="this.style.background=\'var(--book-button-bg)\'; this.style.color=\'#0073aa\';">' . $i . '</a>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 下一页
|
|||
|
|
if ($currentPage < $totalPages) {
|
|||
|
|
$html .= '<a href="' . self::getPageUrl($currentPage + 1, $baseUrl) . '" style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:var(--book-button-bg); color:#0073aa; text-decoration:none; font-size:14px; font-weight:600; transition:all 0.2s ease;"
|
|||
|
|
onmouseover="this.style.background=\'#0073aa\'; this.style.color=\'white\';"
|
|||
|
|
onmouseout="this.style.background=\'var(--book-button-bg)\'; this.style.color=\'#0073aa\';">›</a>';
|
|||
|
|
} else {
|
|||
|
|
$html .= '<span style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:var(--book-button-bg); color:var(--book-text-tertiary); font-size:14px; font-weight:600;">›</span>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 末页
|
|||
|
|
if ($currentPage < $totalPages) {
|
|||
|
|
$html .= '<a href="' . self::getPageUrl($totalPages, $baseUrl) . '" style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:var(--book-button-bg); color:#0073aa; text-decoration:none; font-size:14px; font-weight:600; transition:all 0.2s ease;"
|
|||
|
|
onmouseover="this.style.background=\'#0073aa\'; this.style.color=\'white\';"
|
|||
|
|
onmouseout="this.style.background=\'var(--book-button-bg)\'; this.style.color=\'#0073aa\';">»</a>';
|
|||
|
|
} else {
|
|||
|
|
$html .= '<span style="display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:var(--book-button-bg); color:var(--book-text-tertiary); font-size:14px; font-weight:600;">»</span>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
// 删除了"共几页"的显示
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
return $html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取页面URL
|
|||
|
|
*/
|
|||
|
|
private static function getPageUrl($page, $baseUrl = '')
|
|||
|
|
{
|
|||
|
|
if (empty($baseUrl)) {
|
|||
|
|
// 获取当前页面URL
|
|||
|
|
$currentUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
|
|||
|
|
|
|||
|
|
// 移除现有的page参数
|
|||
|
|
$currentUrl = preg_replace('/[&?]page=\d+/', '', $currentUrl);
|
|||
|
|
$currentUrl = rtrim($currentUrl, '?&');
|
|||
|
|
|
|||
|
|
// 添加分页参数
|
|||
|
|
$separator = strpos($currentUrl, '?') === false ? '?' : '&';
|
|||
|
|
return $currentUrl . $separator . 'page=' . $page;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $baseUrl . (strpos($baseUrl, '?') === false ? '?' : '&') . 'page=' . $page;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 渲染所有图书列表(支持深色模式和封面显示)
|
|||
|
|
*/
|
|||
|
|
public static function renderAllBooks($page = 1)
|
|||
|
|
{
|
|||
|
|
// 从GET参数获取页码(优先级高于参数)
|
|||
|
|
if (isset($_GET['page']) && is_numeric($_GET['page'])) {
|
|||
|
|
$page = intval($_GET['page']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$options = Typecho_Widget::widget('Widget_Options')->plugin('BookInfo');
|
|||
|
|
$pageSize = isset($options->pageSize) ? intval($options->pageSize) : 10;
|
|||
|
|
$imageProxy = isset($options->imageProxy) ? $options->imageProxy : 'https://images.weserv.nl/?url=';
|
|||
|
|
$defaultCover = isset($options->defaultCover) ? $options->defaultCover :
|
|||
|
|
'https://img9.doubanio.com/f/shire/5522dd1f5b742d1e1394a17f44d590646b63871d/pics/book-default-lpic.gif';
|
|||
|
|
|
|||
|
|
// 获取分页数据
|
|||
|
|
$paginationData = self::getAllBooksData($page, $pageSize);
|
|||
|
|
$allBooks = $paginationData['books'];
|
|||
|
|
$total = $paginationData['total'];
|
|||
|
|
$currentPage = $paginationData['page'];
|
|||
|
|
$totalPages = $paginationData['totalPages'];
|
|||
|
|
$startIndex = $paginationData['startIndex'];
|
|||
|
|
$endIndex = $paginationData['endIndex'];
|
|||
|
|
|
|||
|
|
if (empty($allBooks)) {
|
|||
|
|
return '<div style="padding:20px; text-align:center; color:var(--book-text-tertiary); background:var(--book-bg-tertiary); border-radius:8px; border:1px solid var(--book-border);">
|
|||
|
|
<p>暂无图书数据</p>
|
|||
|
|
<p style="font-size:13px; color:var(--book-text-tertiary);">请先在文章中使用[book:ID]短代码添加图书</p>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html = '<div class="all-books-list" style="margin:20px 0;">';
|
|||
|
|
|
|||
|
|
// 标题模块
|
|||
|
|
$html .= '<div style="margin-bottom:20px; padding-bottom:10px; border-bottom:2px solid #0073aa;text-align:center;">';
|
|||
|
|
$html .= '<h2 style="margin:0; font-size:24px; color:var(--book-text-primary);">我的全部已读图书</h2>';
|
|||
|
|
$html .= '<p style="margin:5px 0 0 0;font-size:14px;color:var(--book-text-secondary);">已读' . $total . '本图书,本数据2025.12.08开始统计</p>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
// 页码信息
|
|||
|
|
$html .= '<div style="margin-bottom:20px; padding:15px; background:var(--book-bg-primary); border-radius:8px; border:1px solid var(--book-border); box-shadow:var(--book-shadow-light);">';
|
|||
|
|
$html .= '<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;">';
|
|||
|
|
$html .= '<div style="font-size:14px; color:var(--book-text-secondary);">';
|
|||
|
|
$html .= '<span style="font-weight:600;">显示:</span>';
|
|||
|
|
$html .= '<span style="color:#0073aa; font-weight:600;">' . $startIndex . '-' . $endIndex . '</span> / ';
|
|||
|
|
$html .= '<span style="color:var(--book-text-primary); font-weight:600;">' . $total . '</span>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
$html .= '<div style="font-size:14px; color:var(--book-text-secondary);">';
|
|||
|
|
$html .= '<span style="font-weight:600;">当前:</span>';
|
|||
|
|
$html .= '<span style="color:#e67e22; font-weight:600;">第 ' . $currentPage . ' 页</span> / ';
|
|||
|
|
$html .= '<span style="color:var(--book-text-primary); font-weight:600;">共 ' . $totalPages . ' 页</span>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
// 计算倒序序号(最大的序号在最前面)
|
|||
|
|
$totalCount = $total;
|
|||
|
|
$currentIndex = $totalCount - (($currentPage - 1) * $pageSize);
|
|||
|
|
|
|||
|
|
foreach ($allBooks as $book) {
|
|||
|
|
$bookId = $book['bookId'];
|
|||
|
|
$title = isset($book['title']) ? htmlspecialchars($book['title']) : '未知图书';
|
|||
|
|
|
|||
|
|
// 封面图片
|
|||
|
|
$coverUrl = !empty($book['image']) ? $book['image'] : $defaultCover;
|
|||
|
|
$coverSrc = $imageProxy . urlencode($coverUrl);
|
|||
|
|
|
|||
|
|
// 自定义字段
|
|||
|
|
$startDate = isset($book['custom_start_date']) ? htmlspecialchars($book['custom_start_date']) : '';
|
|||
|
|
$readDate = isset($book['custom_read_date']) ? htmlspecialchars($book['custom_read_date']) : '';
|
|||
|
|
$readMethod = isset($book['custom_read_method']) ? htmlspecialchars($book['custom_read_method']) : '';
|
|||
|
|
$bookCategory = isset($book['custom_book_category']) ? htmlspecialchars($book['custom_book_category']) : '';
|
|||
|
|
|
|||
|
|
// 作者处理
|
|||
|
|
$author = '未知作者';
|
|||
|
|
if (isset($book['author'])) {
|
|||
|
|
if (is_array($book['author'])) {
|
|||
|
|
$author = implode(', ', $book['author']);
|
|||
|
|
} else {
|
|||
|
|
$author = $book['author'];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$author = htmlspecialchars($author);
|
|||
|
|
|
|||
|
|
// 豆瓣信息
|
|||
|
|
$publisher = isset($book['publisher']) ? htmlspecialchars($book['publisher']) : '未知';
|
|||
|
|
$pubdate = isset($book['pubdate']) ? htmlspecialchars($book['pubdate']) : '未知';
|
|||
|
|
$pages = isset($book['pages']) ? htmlspecialchars($book['pages']) : '未知';
|
|||
|
|
|
|||
|
|
// 豆瓣评分
|
|||
|
|
$rating = isset($book['rating']) ? floatval($book['rating']) : 0;
|
|||
|
|
$ratingDisplay = $rating > 0 ? number_format($rating, 1) . '分' : '暂无评分';
|
|||
|
|
|
|||
|
|
// 短评
|
|||
|
|
$review = isset($book['review']) ? htmlspecialchars($book['review']) : '';
|
|||
|
|
|
|||
|
|
// 构建日期范围显示
|
|||
|
|
$dateRange = '';
|
|||
|
|
if ($startDate && $readDate) {
|
|||
|
|
$dateRange = $startDate . '-' . $readDate;
|
|||
|
|
} elseif ($startDate) {
|
|||
|
|
$dateRange = $startDate . '-至今';
|
|||
|
|
} elseif ($readDate) {
|
|||
|
|
$dateRange = '未知-' . $readDate;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建图书分类显示
|
|||
|
|
$categoryDisplay = $bookCategory ? $bookCategory : '未分类';
|
|||
|
|
|
|||
|
|
// 构建阅读方法显示
|
|||
|
|
$methodDisplay = $readMethod ? $readMethod : '未知';
|
|||
|
|
|
|||
|
|
// 获取序号颜色
|
|||
|
|
$indexColor = self::getIndexColor($currentIndex);
|
|||
|
|
|
|||
|
|
$html .= '<div class="book-list-item" style="margin-bottom:25px; padding:20px; background:var(--book-bg-primary); border-radius:8px; border:1px solid var(--book-border); box-shadow:var(--book-shadow-light); transition:all 0.3s ease;"
|
|||
|
|
onmouseover="this.style.boxShadow=\'var(--book-shadow-hover)\'; this.style.transform=\'translateY(-2px)\';"
|
|||
|
|
onmouseout="this.style.boxShadow=\'var(--book-shadow-light)\'; this.style.transform=\'translateY(0)\';">';
|
|||
|
|
|
|||
|
|
$html .= '<div style="display:flex; align-items:flex-start; gap:20px;">';
|
|||
|
|
|
|||
|
|
// 左侧:封面图片带序号
|
|||
|
|
$html .= '<div style="flex-shrink:0; width:90px; position:relative;">';
|
|||
|
|
$html .= '<div style="position:relative;">';
|
|||
|
|
$html .= '<a href="https://book.douban.com/subject/' . $bookId . '/" target="_blank" style="display:block; text-decoration:none;">';
|
|||
|
|
$html .= '<div style="width:100%; height:0; padding-bottom:140%; position:relative; overflow:hidden; border-radius:8px; border:3px solid var(--book-bg-primary); box-shadow:0 4px 12px rgba(0,0,0,0.12); background:var(--book-bg-tertiary);">';
|
|||
|
|
$html .= '<img src="' . $coverSrc . '" alt="' . $title . '"
|
|||
|
|
style="position:absolute; top:0; left:0; width:100%; height:100%; object-fit:cover; transition:transform 0.3s ease;"
|
|||
|
|
onerror="this.src=\'' . $defaultCover . '\';"
|
|||
|
|
onmouseover="this.style.transform=\'scale(1.05)\';"
|
|||
|
|
onmouseout="this.style.transform=\'scale(1)\';">';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
// 序号显示在封面右上角
|
|||
|
|
$html .= '<div style="position:absolute; top:-8px; right:-8px; width:28px; height:28px; background:' . $indexColor . '; color:white; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:bold; font-size:14px; box-shadow:0 2px 4px rgba(0,0,0,0.3); z-index:10;">' . $currentIndex . '</div>';
|
|||
|
|
|
|||
|
|
$html .= '</a>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
// 右侧:图书信息
|
|||
|
|
$html .= '<div style="flex-grow:1;">';
|
|||
|
|
|
|||
|
|
// 第一行:书名(带豆瓣链接)/分类/日期范围/阅读方法
|
|||
|
|
$html .= '<div style="margin-bottom:10px; font-size:16px; line-height:1.5;">';
|
|||
|
|
$html .= '<a href="https://book.douban.com/subject/' . $bookId . '/" target="_blank"
|
|||
|
|
style="color:#0073aa; text-decoration:none; font-weight:bold; font-size:18px;"
|
|||
|
|
onmouseover="this.style.textDecoration=\'underline\';"
|
|||
|
|
onmouseout="this.style.textDecoration=\'none\';">《' . $title . '》</a>';
|
|||
|
|
|
|||
|
|
if ($categoryDisplay || $dateRange || $methodDisplay) {
|
|||
|
|
$html .= '<span style="color:var(--book-text-tertiary); font-size:14px; margin-left:10px;">/</span>';
|
|||
|
|
$html .= '<span style="color:#0073aa; font-size:14px; margin-left:5px;">' . $categoryDisplay . '</span>';
|
|||
|
|
|
|||
|
|
if ($dateRange) {
|
|||
|
|
$html .= '<span style="color:var(--book-text-tertiary); font-size:14px; margin-left:10px;">/</span>';
|
|||
|
|
$html .= '<span style="color:#e67e22; font-size:14px; margin-left:5px;">' . $dateRange . '</span>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($methodDisplay) {
|
|||
|
|
$html .= '<span style="color:var(--book-text-tertiary); font-size:14px; margin-left:10px;">/</span>';
|
|||
|
|
$html .= '<span style="color:#27ae60; font-size:14px; margin-left:5px;">' . $methodDisplay . '</span>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
// 第二行:作者/出版社/出版年/页数/豆瓣评分
|
|||
|
|
$html .= '<div style="margin-bottom:12px; font-size:14px; color:var(--book-text-secondary); line-height:1.6;">';
|
|||
|
|
$html .= '<span style="color:var(--book-text-primary);"><strong>作者:</strong>' . $author . '</span>';
|
|||
|
|
$html .= '<span style="color:var(--book-text-tertiary); margin:0 10px;">|</span>';
|
|||
|
|
$html .= '<span style="color:var(--book-text-primary);"><strong>出版社:</strong>' . $publisher . '</span>';
|
|||
|
|
$html .= '<span style="color:var(--book-text-tertiary); margin:0 10px;">|</span>';
|
|||
|
|
$html .= '<span style="color:var(--book-text-primary);"><strong>出版年:</strong>' . $pubdate . '</span>';
|
|||
|
|
$html .= '<span style="color:var(--book-text-tertiary); margin:0 10px;">|</span>';
|
|||
|
|
$html .= '<span style="color:var(--book-text-primary);"><strong>页数:</strong>' . $pages . '页</span>';
|
|||
|
|
$html .= '<span style="color:var(--book-text-tertiary); margin:0 10px;">|</span>';
|
|||
|
|
// 豆瓣评分显示,如果是0分则显示"暂无评分",否则显示具体分数
|
|||
|
|
if ($rating > 0) {
|
|||
|
|
$html .= '<span style="color:var(--book-rating-color); font-weight:bold;"><strong>豆瓣评分:</strong>' . $ratingDisplay . '</span>';
|
|||
|
|
} else {
|
|||
|
|
$html .= '<span style="color:var(--book-text-tertiary);"><strong>豆瓣评分:</strong>' . $ratingDisplay . '</span>';
|
|||
|
|
}
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
// 第三行:短评
|
|||
|
|
if ($review) {
|
|||
|
|
$html .= '<div style="margin-top:12px; padding-top:12px; border-top:1px dashed var(--book-border);">';
|
|||
|
|
$html .= '<div style="font-size:13px; color:var(--book-text-secondary); font-weight:600; margin-bottom:5px;">📝 短评:</div>';
|
|||
|
|
$html .= '<div style="font-size:14px; line-height:1.6; color:var(--book-text-primary); background:var(--book-review-bg); padding:12px; border-radius:6px; border-left:3px solid #3498db;">';
|
|||
|
|
$html .= nl2br($review);
|
|||
|
|
$html .= '</div>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
$currentIndex--;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 分页导航
|
|||
|
|
if ($totalPages > 1) {
|
|||
|
|
$html .= self::generatePagination($currentPage, $totalPages);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
return $html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取序号颜色
|
|||
|
|
*/
|
|||
|
|
private static function getIndexColor($index)
|
|||
|
|
{
|
|||
|
|
$colors = [
|
|||
|
|
'linear-gradient(135deg, #0073aa, #0056b3)', // 蓝色
|
|||
|
|
'linear-gradient(135deg, #28a745, #218838)', // 绿色
|
|||
|
|
'linear-gradient(135deg, #e67e22, #d35400)', // 橙色
|
|||
|
|
'linear-gradient(135deg, #9b59b6, #8e44ad)', // 紫色
|
|||
|
|
'linear-gradient(135deg, #e74c3c, #c0392b)', // 红色
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return $colors[($index - 1) % count($colors)];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取图书数据(包含短评和自定义信息)
|
|||
|
|
*/
|
|||
|
|
private static function getBookData($bookId, $review = '', $customData = array())
|
|||
|
|
{
|
|||
|
|
if (!is_numeric($bookId)) return null;
|
|||
|
|
|
|||
|
|
$cacheFile = dirname(__FILE__) . '/cache/' . $bookId . '.json';
|
|||
|
|
$options = Typecho_Widget::widget('Widget_Options')->plugin('BookInfo');
|
|||
|
|
$cacheEnable = isset($options->cacheEnable) ? $options->cacheEnable : '1';
|
|||
|
|
$cacheTime = isset($options->cacheTime) ? intval($options->cacheTime) : 7;
|
|||
|
|
|
|||
|
|
$data = null;
|
|||
|
|
$needUpdate = false;
|
|||
|
|
|
|||
|
|
// 检查缓存
|
|||
|
|
if ($cacheEnable == '1' && file_exists($cacheFile)) {
|
|||
|
|
$fileTime = filemtime($cacheFile);
|
|||
|
|
$expireTime = $cacheTime * 24 * 3600;
|
|||
|
|
if (time() - $fileTime < $expireTime) {
|
|||
|
|
$cacheContent = file_get_contents($cacheFile);
|
|||
|
|
if ($cacheContent) {
|
|||
|
|
$data = json_decode($cacheContent, true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果缓存不存在或已过期,从豆瓣获取基本信息
|
|||
|
|
if (!$data || empty($data['title'])) {
|
|||
|
|
$data = self::fetchFromDouban($bookId);
|
|||
|
|
$needUpdate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重要修复:只在$review不为空且与当前数据不同时更新
|
|||
|
|
if (!empty($review) && (!isset($data['review']) || $data['review'] !== $review)) {
|
|||
|
|
$data['review'] = $review;
|
|||
|
|
$data['review_updated'] = time();
|
|||
|
|
$needUpdate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新自定义数据 - 使用传入的customData更新
|
|||
|
|
if (!empty($customData)) {
|
|||
|
|
if (isset($customData['startDate']) && (!isset($data['custom_start_date']) || $data['custom_start_date'] !== $customData['startDate'])) {
|
|||
|
|
$data['custom_start_date'] = $customData['startDate'];
|
|||
|
|
$needUpdate = true;
|
|||
|
|
}
|
|||
|
|
if (isset($customData['readDate']) && (!isset($data['custom_read_date']) || $data['custom_read_date'] !== $customData['readDate'])) {
|
|||
|
|
$data['custom_read_date'] = $customData['readDate'];
|
|||
|
|
$needUpdate = true;
|
|||
|
|
}
|
|||
|
|
if (isset($customData['readMethod']) && (!isset($data['custom_read_method']) || $data['custom_read_method'] !== $customData['readMethod'])) {
|
|||
|
|
$data['custom_read_method'] = $customData['readMethod'];
|
|||
|
|
$needUpdate = true;
|
|||
|
|
}
|
|||
|
|
if (isset($customData['bookCategory']) && (!isset($data['custom_book_category']) || $data['custom_book_category'] !== $customData['bookCategory'])) {
|
|||
|
|
$data['custom_book_category'] = $customData['bookCategory'];
|
|||
|
|
$needUpdate = true;
|
|||
|
|
}
|
|||
|
|
if (isset($customData['recommendation']) && (!isset($data['custom_recommendation']) || $data['custom_recommendation'] != $customData['recommendation'])) {
|
|||
|
|
$data['custom_recommendation'] = intval($customData['recommendation']);
|
|||
|
|
$needUpdate = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确保自定义字段存在
|
|||
|
|
$customFields = array(
|
|||
|
|
'custom_start_date' => '',
|
|||
|
|
'custom_read_date' => '',
|
|||
|
|
'custom_read_method' => '',
|
|||
|
|
'custom_book_category' => '',
|
|||
|
|
'custom_recommendation' => 0
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
foreach ($customFields as $field => $default) {
|
|||
|
|
if (!isset($data[$field])) {
|
|||
|
|
$data[$field] = $default;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果需要更新缓存,保存到文件
|
|||
|
|
if ($needUpdate && $data && !empty($data['title'])) {
|
|||
|
|
file_put_contents($cacheFile, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从豆瓣获取数据 - 修复:保留HTML标签的摘要抓取
|
|||
|
|
*/
|
|||
|
|
private static function fetchFromDouban($bookId)
|
|||
|
|
{
|
|||
|
|
$url = "https://book.douban.com/subject/{$bookId}/";
|
|||
|
|
$ch = curl_init();
|
|||
|
|
curl_setopt_array($ch, array(
|
|||
|
|
CURLOPT_URL => $url,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|||
|
|
CURLOPT_TIMEOUT => 20,
|
|||
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|||
|
|
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|||
|
|
CURLOPT_ENCODING => 'gzip, deflate',
|
|||
|
|
CURLOPT_REFERER => 'https://book.douban.com/',
|
|||
|
|
CURLOPT_HTTPHEADER => array(
|
|||
|
|
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|||
|
|
'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
|
|||
|
|
'Cache-Control: no-cache',
|
|||
|
|
'Connection: keep-alive'
|
|||
|
|
)
|
|||
|
|
));
|
|||
|
|
|
|||
|
|
$html = curl_exec($ch);
|
|||
|
|
if (curl_errno($ch)) {
|
|||
|
|
curl_close($ch);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
curl_close($ch);
|
|||
|
|
|
|||
|
|
if (empty($html)) return null;
|
|||
|
|
|
|||
|
|
$data = array();
|
|||
|
|
|
|||
|
|
// 1. 提取标题
|
|||
|
|
if (preg_match('/<meta\s+property="og:title"\s+content="([^"]+)"/i', $html, $matches)) {
|
|||
|
|
$data['title'] = trim(strip_tags(html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8')));
|
|||
|
|
} elseif (preg_match('/<h1[^>]*>\s*<span[^>]*>([^<]+)<\/span>/', $html, $matches)) {
|
|||
|
|
$data['title'] = trim(strip_tags(html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8')));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 提取封面
|
|||
|
|
if (preg_match('/<meta\s+property="og:image"\s+content="([^"]+)"/i', $html, $matches)) {
|
|||
|
|
$data['image'] = trim($matches[1]);
|
|||
|
|
} elseif (preg_match('/<img[^>]*src="([^"]+)"[^>]*id="mainpic"/', $html, $matches)) {
|
|||
|
|
$data['image'] = trim($matches[1]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 提取描述(内容简介)- 修复:保留HTML标签
|
|||
|
|
$summary = '';
|
|||
|
|
|
|||
|
|
// 豆瓣页面可能有多个intro,第一个通常是内容简介
|
|||
|
|
if (preg_match_all('/<div\s+class="intro"[^>]*>([\s\S]*?)<\/div>/', $html, $matches)) {
|
|||
|
|
// 尝试获取第一个intro(通常是内容简介)
|
|||
|
|
if (isset($matches[1][0])) {
|
|||
|
|
$intro = $matches[1][0];
|
|||
|
|
|
|||
|
|
// 检查是否有展开全部链接
|
|||
|
|
if (preg_match('/<a[^>]*class="[^"]*a_show_full[^"]*"[^>]*>.*?<\/a>/', $intro)) {
|
|||
|
|
// 如果有展开全部链接,说明这个intro是被截断的
|
|||
|
|
// 我们需要查找完整的intro(可能在后面的div中)
|
|||
|
|
foreach ($matches[1] as $introIndex => $introContent) {
|
|||
|
|
// 查找不包含"a_show_full"的完整intro
|
|||
|
|
if (!preg_match('/<a[^>]*class="[^"]*a_show_full[^"]*"[^>]*>.*?<\/a>/', $introContent)) {
|
|||
|
|
$intro = $introContent;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移除展开链接,但保留其他HTML标签(特别是<p>标签)
|
|||
|
|
$intro = preg_replace('/<a[^>]*>.*?<\/a>/', '', $intro);
|
|||
|
|
|
|||
|
|
// 清理多余的空白字符但保留HTML标签
|
|||
|
|
$intro = preg_replace('/\s+/', ' ', $intro);
|
|||
|
|
$intro = trim($intro);
|
|||
|
|
|
|||
|
|
// 解码HTML实体,但保留HTML标签
|
|||
|
|
$intro = html_entity_decode($intro, ENT_QUOTES, 'UTF-8');
|
|||
|
|
|
|||
|
|
// 确保<p>标签正确闭合
|
|||
|
|
$intro = preg_replace('/<p>\s*<\/p>/', '', $intro);
|
|||
|
|
$intro = preg_replace('/<p>\s+/', '<p>', $intro);
|
|||
|
|
$intro = preg_replace('/\s+<\/p>/', '</p>', $intro);
|
|||
|
|
|
|||
|
|
$summary = $intro;
|
|||
|
|
|
|||
|
|
// 如果获取到的内容很短,可能是作者简介,尝试第二个intro
|
|||
|
|
$plainText = strip_tags($intro);
|
|||
|
|
if (mb_strlen($plainText, 'UTF-8') < 50 && isset($matches[1][1])) {
|
|||
|
|
$intro = $matches[1][1];
|
|||
|
|
$intro = preg_replace('/<a[^>]*>.*?<\/a>/', '', $intro);
|
|||
|
|
$intro = preg_replace('/\s+/', ' ', $intro);
|
|||
|
|
$intro = trim($intro);
|
|||
|
|
$intro = html_entity_decode($intro, ENT_QUOTES, 'UTF-8');
|
|||
|
|
$summary = $intro;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果intro中没有,尝试从meta获取(但meta通常没有HTML标签)
|
|||
|
|
if (empty($summary) && preg_match('/<meta\s+property="og:description"\s+content="([^"]+)"/i', $html, $matches)) {
|
|||
|
|
$summary = trim(html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$data['summary'] = $summary ?: '<p>暂无简介</p>';
|
|||
|
|
|
|||
|
|
// 4. 提取作者信息
|
|||
|
|
$authors = array();
|
|||
|
|
if (preg_match('/<meta\s+property="book:author"\s+content="([^"]+)"/i', $html, $matches)) {
|
|||
|
|
$authors[] = trim(html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'));
|
|||
|
|
}
|
|||
|
|
// 从作者链接提取
|
|||
|
|
if (preg_match_all('/<a[^>]*class="author"[^>]*>([^<]+)<\/a>/', $html, $matches)) {
|
|||
|
|
foreach ($matches[1] as $author) {
|
|||
|
|
$cleanAuthor = trim(html_entity_decode(strip_tags($author), ENT_QUOTES, 'UTF-8'));
|
|||
|
|
if ($cleanAuthor && !in_array($cleanAuthor, $authors)) {
|
|||
|
|
$authors[] = $cleanAuthor;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$data['author'] = !empty($authors) ? $authors : array('未知作者');
|
|||
|
|
|
|||
|
|
// 5. 提取图书详细信息(出版社、出版年、页数等)
|
|||
|
|
$infoHtml = '';
|
|||
|
|
if (preg_match('/<div[^>]*id="info"[^>]*>([\s\S]*?)<\/div>/', $html, $matches)) {
|
|||
|
|
$infoHtml = $matches[1];
|
|||
|
|
|
|||
|
|
// 处理换行和span标签
|
|||
|
|
$infoHtml = preg_replace('/<br\s*\/?>/', "\n", $infoHtml);
|
|||
|
|
$infoHtml = preg_replace('/<\/span>/', "</span>\n", $infoHtml);
|
|||
|
|
$infoHtml = strip_tags($infoHtml);
|
|||
|
|
|
|||
|
|
// 按行处理
|
|||
|
|
$lines = explode("\n", $infoHtml);
|
|||
|
|
foreach ($lines as $line) {
|
|||
|
|
$line = trim($line);
|
|||
|
|
if (empty($line)) continue;
|
|||
|
|
|
|||
|
|
// 使用更灵活的正则匹配各种格式
|
|||
|
|
if (preg_match('/^出版社[::]\s*(.+)$/u', $line, $match)) {
|
|||
|
|
$data['publisher'] = trim($match[1]);
|
|||
|
|
}
|
|||
|
|
elseif (preg_match('/^出版年[::]\s*(.+)$/u', $line, $match)) {
|
|||
|
|
$data['pubdate'] = trim($match[1]);
|
|||
|
|
}
|
|||
|
|
elseif (preg_match('/^页数[::]\s*(.+)$/u', $line, $match)) {
|
|||
|
|
$data['pages'] = trim($match[1]);
|
|||
|
|
}
|
|||
|
|
elseif (preg_match('/^ISBN[::]\s*(.+)$/u', $line, $match)) {
|
|||
|
|
$data['isbn'] = trim($match[1]);
|
|||
|
|
}
|
|||
|
|
elseif (preg_match('/^定价[::]\s*(.+)$/u', $line, $match)) {
|
|||
|
|
$data['price'] = trim($match[1]);
|
|||
|
|
}
|
|||
|
|
// 处理其他可能的格式
|
|||
|
|
elseif (preg_match('/出版社[::]\s*(.+)/u', $line, $match)) {
|
|||
|
|
$data['publisher'] = trim($match[1]);
|
|||
|
|
}
|
|||
|
|
elseif (preg_match('/出版年[::]\s*(.+)/u', $line, $match)) {
|
|||
|
|
$data['pubdate'] = trim($match[1]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果上面没提取到,尝试直接在整个HTML中搜索
|
|||
|
|
if (!isset($data['publisher'])) {
|
|||
|
|
$publisherPatterns = array(
|
|||
|
|
'/出版社[::]\s*<\/span>\s*<a[^>]*>([^<]+)<\/a>/',
|
|||
|
|
'/出版社[::]\s*<\/span>\s*<span[^>]*>([^<]+)<\/span>/',
|
|||
|
|
'/出版社[::]\s*([^<]+)<br\s*\/?>/',
|
|||
|
|
'/<span[^>]*>出版社[::]<\/span>\s*([^<]+)/',
|
|||
|
|
'/出版社[::]\s*([^\n<]+)/'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
foreach ($publisherPatterns as $pattern) {
|
|||
|
|
if (preg_match($pattern, $html, $matches)) {
|
|||
|
|
$data['publisher'] = trim(strip_tags($matches[1]));
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!isset($data['pubdate'])) {
|
|||
|
|
$pubdatePatterns = array(
|
|||
|
|
'/出版年[::]\s*<\/span>\s*<span[^>]*>([^<]+)<\/span>/',
|
|||
|
|
'/出版年[::]\s*([^<]+)<br\s*\/?>/',
|
|||
|
|
'/<span[^>]*>出版年[::]<\/span>\s*([^<]+)/',
|
|||
|
|
'/出版年[::]\s*([^\n<]+)/',
|
|||
|
|
'/出版日期[::]\s*([^<]+)/'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
foreach ($pubdatePatterns as $pattern) {
|
|||
|
|
if (preg_match($pattern, $html, $matches)) {
|
|||
|
|
$data['pubdate'] = trim(strip_tags($matches[1]));
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!isset($data['pages'])) {
|
|||
|
|
$pagesPatterns = array(
|
|||
|
|
'/页数[::]\s*<\/span>\s*<span[^>]*>([^<]+)<\/span>/',
|
|||
|
|
'/页数[::]\s*([^<]+)<br\s*\/?>/',
|
|||
|
|
'/<span[^>]*>页数[::]<\/span>\s*([^<]+)/',
|
|||
|
|
'/页数[::]\s*([^\n<]+)/'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
foreach ($pagesPatterns as $pattern) {
|
|||
|
|
if (preg_match($pattern, $html, $matches)) {
|
|||
|
|
$data['pages'] = trim(strip_tags($matches[1]));
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置默认值
|
|||
|
|
if (!isset($data['publisher'])) $data['publisher'] = '未知';
|
|||
|
|
if (!isset($data['pubdate'])) $data['pubdate'] = '未知';
|
|||
|
|
if (!isset($data['pages'])) $data['pages'] = '未知';
|
|||
|
|
if (!isset($data['isbn'])) $data['isbn'] = '未知';
|
|||
|
|
|
|||
|
|
// 6. 提取豆瓣评分
|
|||
|
|
$rating = 0;
|
|||
|
|
$ratingCount = 0;
|
|||
|
|
|
|||
|
|
// 从评分区域提取
|
|||
|
|
if (preg_match('/<strong[^>]*class="ll rating_num"[^>]*>([^<]+)<\/strong>/', $html, $matches)) {
|
|||
|
|
$rating = floatval(trim($matches[1]));
|
|||
|
|
}
|
|||
|
|
elseif (preg_match('/<strong[^>]*class="[^"]*rating_num[^"]*"[^>]*>([^<]+)<\/strong>/', $html, $matches)) {
|
|||
|
|
$rating = floatval(trim($matches[1]));
|
|||
|
|
}
|
|||
|
|
// 从property属性提取
|
|||
|
|
elseif (preg_match('/property="v:average"[^>]*>([^<]+)<\/strong>/', $html, $matches)) {
|
|||
|
|
$rating = floatval(trim($matches[1]));
|
|||
|
|
}
|
|||
|
|
// 从评分数字提取
|
|||
|
|
elseif (preg_match('/<span[^>]*>(\d+\.?\d*)<\/span>[^<]*<span[^>]*>\((\d+)人评价\)/', $html, $matches)) {
|
|||
|
|
$rating = floatval($matches[1]);
|
|||
|
|
$ratingCount = intval($matches[2]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$data['rating'] = $rating;
|
|||
|
|
|
|||
|
|
// 7. 提取评价人数
|
|||
|
|
if (preg_match('/<span[^>]*property="v:votes"[^>]*>(\d+)<\/span>/', $html, $matches)) {
|
|||
|
|
$data['rating_count'] = intval($matches[1]);
|
|||
|
|
} elseif (preg_match('/\((\d+)人评价\)/', $html, $matches)) {
|
|||
|
|
$data['rating_count'] = intval($matches[1]);
|
|||
|
|
} else {
|
|||
|
|
$data['rating_count'] = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果评分人数为0但评分不为0,尝试其他方式
|
|||
|
|
if ($rating > 0 && $data['rating_count'] == 0) {
|
|||
|
|
if (preg_match('/<a[^>]*class="rating_people"[^>]*>(\d+)人评价<\/a>/', $html, $matches)) {
|
|||
|
|
$data['rating_count'] = intval($matches[1]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 8. 初始化review和自定义字段
|
|||
|
|
$data['review'] = '';
|
|||
|
|
$data['review_updated'] = 0;
|
|||
|
|
|
|||
|
|
// 初始化自定义字段
|
|||
|
|
$data['custom_start_date'] = '';
|
|||
|
|
$data['custom_read_date'] = '';
|
|||
|
|
$data['custom_read_method'] = '';
|
|||
|
|
$data['custom_book_category'] = '';
|
|||
|
|
$data['custom_recommendation'] = 0;
|
|||
|
|
|
|||
|
|
// 9. 添加抓取时间
|
|||
|
|
$data['fetched_at'] = time();
|
|||
|
|
|
|||
|
|
return $data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 渲染编辑器按钮 - 修复:恢复原来的编辑器预览和插入格式
|
|||
|
|
*/
|
|||
|
|
public static function renderButton()
|
|||
|
|
{
|
|||
|
|
echo <<<HTML
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
#bookinfo-button {
|
|||
|
|
padding: 5px!important;
|
|||
|
|
background:#fff;
|
|||
|
|
cursor: pointer;
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 16px;
|
|||
|
|
line-height: 1;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
min-width: 26px;
|
|||
|
|
min-height: 26px;
|
|||
|
|
}
|
|||
|
|
#bookinfo-button:hover{background:#E9E9E6}
|
|||
|
|
.dark #bookinfo-button{background:rgb(16, 25, 40);}
|
|||
|
|
.dark #bookinfo-button:hover{background:#375d85;}
|
|||
|
|
|
|||
|
|
/* 豆瓣插件深色模式适配 - 仅添加CSS */
|
|||
|
|
.dark #bookinfo-id,
|
|||
|
|
.dark #bookinfo-review,
|
|||
|
|
.dark #bookinfo-start-date,
|
|||
|
|
.dark #bookinfo-read-date,
|
|||
|
|
.dark #bookinfo-read-method,
|
|||
|
|
.dark #bookinfo-book-category,
|
|||
|
|
.dark #bookinfo-recommendation {
|
|||
|
|
background: #101928 !important;
|
|||
|
|
border: 1px solid #374151 !important;
|
|||
|
|
color: #ffffff !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark #bookinfo-id::placeholder,
|
|||
|
|
.dark #bookinfo-review::placeholder,
|
|||
|
|
.dark #bookinfo-start-date::placeholder,
|
|||
|
|
.dark #bookinfo-read-date::placeholder,
|
|||
|
|
.dark #bookinfo-read-method::placeholder,
|
|||
|
|
.dark #bookinfo-book-category::placeholder {
|
|||
|
|
color: #9ca3af !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark #bookinfo-id:focus,
|
|||
|
|
.dark #bookinfo-review:focus,
|
|||
|
|
.dark #bookinfo-start-date:focus,
|
|||
|
|
.dark #bookinfo-read-date:focus,
|
|||
|
|
.dark #bookinfo-read-method:focus,
|
|||
|
|
.dark #bookinfo-book-category:focus,
|
|||
|
|
.dark #bookinfo-recommendation:focus {
|
|||
|
|
border-color: #0073aa !important;
|
|||
|
|
outline: none !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark label[for="bookinfo-id"],
|
|||
|
|
.dark label[for="bookinfo-review"],
|
|||
|
|
.dark label[for="bookinfo-start-date"],
|
|||
|
|
.dark label[for="bookinfo-read-date"],
|
|||
|
|
.dark label[for="bookinfo-read-method"],
|
|||
|
|
.dark label[for="bookinfo-book-category"],
|
|||
|
|
.dark label[for="bookinfo-recommendation"],
|
|||
|
|
.dark .bookinfo-modal-title {
|
|||
|
|
color: #ffffff !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-modal-content {
|
|||
|
|
background: #1f2937 !important;
|
|||
|
|
border: 1px solid #374151 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-modal-header {
|
|||
|
|
background: #101928 !important;
|
|||
|
|
border-bottom: 1px solid #374151 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-modal-footer {
|
|||
|
|
background: #101928 !important;
|
|||
|
|
border-top: 1px solid #374151 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-help-text {
|
|||
|
|
color: #9ca3af !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-preview-box {
|
|||
|
|
background: #101928 !important;
|
|||
|
|
border: 1px solid #374151 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-preview {
|
|||
|
|
color: #d1d5db !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-option-group {
|
|||
|
|
background: #1f2937 !important;
|
|||
|
|
border: 1px solid #374151 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-cancel-btn {
|
|||
|
|
background: #374151 !important;
|
|||
|
|
color: #d1d5db !important;
|
|||
|
|
border: 1px solid #4b5563 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-cancel-btn:hover {
|
|||
|
|
background: #4b5563 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-insert-btn {
|
|||
|
|
background: #0073aa !important;
|
|||
|
|
color: #ffffff !important;
|
|||
|
|
border: 1px solid #0056b3 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-insert-btn:hover {
|
|||
|
|
background: #0056b3 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 弹窗样式适配 */
|
|||
|
|
.dark .bookinfo-modal-overlay {
|
|||
|
|
background: rgba(0, 0, 0, 0.7) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 选择框选项 */
|
|||
|
|
.dark #bookinfo-recommendation option {
|
|||
|
|
background: #1f2937 !important;
|
|||
|
|
color: #ffffff !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark #bookinfo-recommendation option:disabled {
|
|||
|
|
color: #9ca3af !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 信息提示框 */
|
|||
|
|
.dark .bookinfo-info-box {
|
|||
|
|
background: #374151 !important;
|
|||
|
|
border: 1px solid #4b5563 !important;
|
|||
|
|
color: #d1d5db !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .bookinfo-info-box strong {
|
|||
|
|
color: #ffffff !important;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
<script>
|
|||
|
|
(function() {
|
|||
|
|
function initBookButton() {
|
|||
|
|
var toolbar = null;
|
|||
|
|
var selectors = ['.wmd-button-row', '.typecho-post-option', '.submit', '#custom-field'];
|
|||
|
|
for (var i = 0; i < selectors.length; i++) {
|
|||
|
|
var el = document.querySelector(selectors[i]);
|
|||
|
|
if (el) { toolbar = el; break; }
|
|||
|
|
}
|
|||
|
|
if (!toolbar) {
|
|||
|
|
setTimeout(initBookButton, 500);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (document.getElementById('bookinfo-button')) return;
|
|||
|
|
|
|||
|
|
var button = document.createElement('button');
|
|||
|
|
button.type = 'button';
|
|||
|
|
button.id = 'bookinfo-button';
|
|||
|
|
button.className = 'btn btn-s';
|
|||
|
|
button.innerHTML = '📚';
|
|||
|
|
button.style.cssText = 'padding:6px 12px;';
|
|||
|
|
toolbar.appendChild(button);
|
|||
|
|
|
|||
|
|
button.addEventListener('click', function() {
|
|||
|
|
showBookDialog();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showBookDialog() {
|
|||
|
|
// 创建遮罩层
|
|||
|
|
var overlay = document.createElement('div');
|
|||
|
|
overlay.style.cssText = 'position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:9999;';
|
|||
|
|
|
|||
|
|
// 创建对话框
|
|||
|
|
var modal = document.createElement('div');
|
|||
|
|
modal.style.cssText = 'position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:white; border:1px solid #ddd; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.15); z-index:10000; width:500px; max-height:90vh; overflow-y:auto; padding:20px;';
|
|||
|
|
|
|||
|
|
modal.innerHTML =
|
|||
|
|
'<!--<h3 style="margin-top:0; color:#000;margin-bottom:15px;">插入豆瓣图书</h3>-->' +
|
|||
|
|
|
|||
|
|
'<div style="margin-bottom:10px;margin-top:-5px;">' +
|
|||
|
|
'<label style="display:block; margin-bottom:5px;color:#000; font-weight:600;">📚 豆瓣图书ID</label>' +
|
|||
|
|
'<input type="text" id="bookinfo-id" placeholder="例如:1007305" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:4px; font-size:14px; box-sizing:border-box;">' +
|
|||
|
|
'<!--<div style="font-size:12px; color:#666; margin-top:5px;">在豆瓣图书页面URL中找到的纯数字ID</div>-->' +
|
|||
|
|
'</div>' +
|
|||
|
|
|
|||
|
|
'<div style="margin-bottom:15px;">' +
|
|||
|
|
'<label style="display:block;color:#000; margin-bottom:5px; font-weight:600;">💭 我的短评/读后感</label>' +
|
|||
|
|
'<textarea id="bookinfo-review" placeholder="输入读后感或简短评论..." style="width:100%; padding:8px; border:1px solid #ddd; border-radius:4px; font-size:14px; box-sizing:border-box; min-height:80px; resize:vertical;color:#666;background-Color:#fff;"></textarea>' +
|
|||
|
|
'</div>' +
|
|||
|
|
|
|||
|
|
'<div style="margin-bottom:15px; padding:15px; background:#f8f9fa; border-radius:6px; border:1px solid #eaeaea;">' +
|
|||
|
|
'<h4 style="margin-top:0; margin-bottom:12px; font-size:15px; color:#333;">📖 我的阅读记录(可选)</h4>' +
|
|||
|
|
|
|||
|
|
'<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">' +
|
|||
|
|
'<div>' +
|
|||
|
|
'<label style="display:block; margin-bottom:5px; font-size:13px; color:#555;">开始阅读</label>' +
|
|||
|
|
'<input type="text" id="bookinfo-start-date" placeholder="例如:2024-03-15" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:4px; font-size:13px; box-sizing:border-box;">' +
|
|||
|
|
'</div>' +
|
|||
|
|
|
|||
|
|
'<div>' +
|
|||
|
|
'<label style="display:block; margin-bottom:5px; font-size:13px; color:#555;">结束阅读</label>' +
|
|||
|
|
'<input type="text" id="bookinfo-read-date" placeholder="例如:2024-03-20" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:4px; font-size:13px; box-sizing:border-box;">' +
|
|||
|
|
'</div>' +
|
|||
|
|
'</div>' +
|
|||
|
|
|
|||
|
|
'<div style="margin-top:12px;">' +
|
|||
|
|
'<label style="display:block; margin-bottom:5px; font-size:13px;color:#666; ">推荐指数</label>' +
|
|||
|
|
'<select id="bookinfo-recommendation" style="width:100%; color:#666;padding:6px; border:1px solid #ddd; border-radius:4px; font-size:13px; box-sizing:border-box;background-Color:#fff;">' +
|
|||
|
|
'<option value="0">请选择推荐指数</option>' +
|
|||
|
|
'<option value="1">★ 1星 - 不推荐</option>' +
|
|||
|
|
'<option value="2">★★ 2星 - 一般</option>' +
|
|||
|
|
'<option value="3">★★★ 3星 - 还行</option>' +
|
|||
|
|
'<option value="4">★★★★ 4星 - 推荐</option>' +
|
|||
|
|
'<option value="5">★★★★★ 5星 - 极力推荐</option>' +
|
|||
|
|
'</select>' +
|
|||
|
|
'</div>' +
|
|||
|
|
|
|||
|
|
|
|||
|
|
'<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;margin:10px 0px;">' +
|
|||
|
|
'<div>' +
|
|||
|
|
'<label style="display:block; margin-bottom:5px; font-size:13px; color:#555;">阅读方法</label>' +
|
|||
|
|
'<input type="text" id="bookinfo-read-method" placeholder="例如:速读" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:4px; font-size:13px; box-sizing:border-box;">' +
|
|||
|
|
'</div>' +
|
|||
|
|
|
|||
|
|
'<div>' +
|
|||
|
|
'<label style="display:block; margin-bottom:5px; font-size:13px; color:#555;">图书分类</label>' +
|
|||
|
|
'<input type="text" id="bookinfo-book-category" placeholder="例如:小说" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:4px; font-size:13px; box-sizing:border-box;">' +
|
|||
|
|
'</div>' +
|
|||
|
|
'</div>' +
|
|||
|
|
|
|||
|
|
'<div style="margin-bottom:15px; background:#e8f4fd; padding:12px; border-radius:6px; font-size:13px; color:#666; border:1px solid #d1e7ff;">' +
|
|||
|
|
'<div style="font-weight:600; color:#000; margin-bottom:5px;">📝 生成格式预览:</div>' +
|
|||
|
|
'<div style="font-family:monospace; background:#fff; padding:8px; margin-top:5px; border-radius:4px; border:1px solid #cfe2ff; max-height:80px; overflow-y:auto;">' +
|
|||
|
|
'<div id="bookinfo-preview" style="color:#333; word-break:break-all; font-size:13px; line-height:1.4;">[book:ID:短评]</div>' +
|
|||
|
|
'</div>' +
|
|||
|
|
'<div style="margin-top:8px; color:#666;">说明:自定义信息将自动保存到JSON缓存中</div>' +
|
|||
|
|
'</div>' +
|
|||
|
|
|
|||
|
|
'<div style="text-align:right;">' +
|
|||
|
|
'<button type="button" id="bookinfo-cancel" style="padding:8px 16px; margin-right:10px; background:#f5f5f5; border:1px solid #ddd; border-radius:4px; cursor:pointer; font-size:14px;">取消</button>' +
|
|||
|
|
'<button type="button" id="bookinfo-insert" style="padding:8px 20px; background:#0073aa; color:white; border:none; border-radius:4px; cursor:pointer; font-size:14px; font-weight:600;">插入</button>' +
|
|||
|
|
'</div>';
|
|||
|
|
|
|||
|
|
// 添加到页面
|
|||
|
|
document.body.appendChild(overlay);
|
|||
|
|
document.body.appendChild(modal);
|
|||
|
|
|
|||
|
|
// 获取DOM元素
|
|||
|
|
var bookIdInput = document.getElementById('bookinfo-id');
|
|||
|
|
var reviewInput = document.getElementById('bookinfo-review');
|
|||
|
|
var startDateInput = document.getElementById('bookinfo-start-date');
|
|||
|
|
var readDateInput = document.getElementById('bookinfo-read-date');
|
|||
|
|
var readMethodInput = document.getElementById('bookinfo-read-method');
|
|||
|
|
var bookCategoryInput = document.getElementById('bookinfo-book-category');
|
|||
|
|
var recommendationSelect = document.getElementById('bookinfo-recommendation');
|
|||
|
|
var previewDiv = document.getElementById('bookinfo-preview');
|
|||
|
|
var insertBtn = document.getElementById('bookinfo-insert');
|
|||
|
|
var cancelBtn = document.getElementById('bookinfo-cancel');
|
|||
|
|
|
|||
|
|
// 更新预览 - 恢复原来的预览显示
|
|||
|
|
function updatePreview() {
|
|||
|
|
var bookId = bookIdInput.value.trim();
|
|||
|
|
var review = reviewInput.value.trim();
|
|||
|
|
var startDate = startDateInput.value.trim();
|
|||
|
|
var readDate = readDateInput.value.trim();
|
|||
|
|
var readMethod = readMethodInput.value.trim();
|
|||
|
|
var bookCategory = bookCategoryInput.value.trim();
|
|||
|
|
var recommendation = parseInt(recommendationSelect.value) || 0;
|
|||
|
|
|
|||
|
|
var preview = '[book:' + (bookId || 'ID');
|
|||
|
|
|
|||
|
|
// 构建自定义数据对象
|
|||
|
|
var customData = {};
|
|||
|
|
if (startDate) customData.startDate = startDate;
|
|||
|
|
if (readDate) customData.readDate = readDate;
|
|||
|
|
if (readMethod) customData.readMethod = readMethod;
|
|||
|
|
if (bookCategory) customData.bookCategory = bookCategory;
|
|||
|
|
if (recommendation > 0) customData.recommendation = recommendation;
|
|||
|
|
|
|||
|
|
// 显示预览(中文显示)
|
|||
|
|
var displayText = '';
|
|||
|
|
|
|||
|
|
if (review) {
|
|||
|
|
displayText = review;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有自定义数据,添加到显示文本
|
|||
|
|
if (Object.keys(customData).length > 0) {
|
|||
|
|
var customText = '(自定义:';
|
|||
|
|
var customParts = [];
|
|||
|
|
if (startDate) customParts.push('开始阅读:' + startDate);
|
|||
|
|
if (readDate) customParts.push('结束阅读:' + readDate);
|
|||
|
|
if (readMethod) customParts.push('阅读方法:' + readMethod);
|
|||
|
|
if (bookCategory) customParts.push('图书分类:' + bookCategory);
|
|||
|
|
if (recommendation > 0) customParts.push('推荐指数:' + '★'.repeat(recommendation));
|
|||
|
|
|
|||
|
|
customText += customParts.join(',') + ')';
|
|||
|
|
|
|||
|
|
if (displayText) {
|
|||
|
|
displayText += customText;
|
|||
|
|
} else {
|
|||
|
|
displayText = customText;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有内容,添加到预览
|
|||
|
|
if (displayText) {
|
|||
|
|
// 不对预览中的方括号进行编码,直接显示中文
|
|||
|
|
preview += ':' + displayText;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
preview += ']';
|
|||
|
|
|
|||
|
|
previewDiv.innerHTML = preview;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 绑定输入事件
|
|||
|
|
bookIdInput.addEventListener('input', updatePreview);
|
|||
|
|
reviewInput.addEventListener('input', updatePreview);
|
|||
|
|
startDateInput.addEventListener('input', updatePreview);
|
|||
|
|
readDateInput.addEventListener('input', updatePreview);
|
|||
|
|
readMethodInput.addEventListener('input', updatePreview);
|
|||
|
|
bookCategoryInput.addEventListener('input', updatePreview);
|
|||
|
|
recommendationSelect.addEventListener('change', updatePreview);
|
|||
|
|
|
|||
|
|
// 聚焦输入框
|
|||
|
|
setTimeout(function() {
|
|||
|
|
bookIdInput.focus();
|
|||
|
|
}, 100);
|
|||
|
|
|
|||
|
|
// 插入按钮点击
|
|||
|
|
insertBtn.addEventListener('click', function() {
|
|||
|
|
var bookId = bookIdInput.value.trim();
|
|||
|
|
var review = reviewInput.value.trim();
|
|||
|
|
var startDate = startDateInput.value.trim();
|
|||
|
|
var readDate = readDateInput.value.trim();
|
|||
|
|
var readMethod = readMethodInput.value.trim();
|
|||
|
|
var bookCategory = bookCategoryInput.value.trim();
|
|||
|
|
var recommendation = parseInt(recommendationSelect.value) || 0;
|
|||
|
|
|
|||
|
|
if (bookId && /^\d+$/.test(bookId)) {
|
|||
|
|
// 构建自定义数据对象
|
|||
|
|
var customData = {};
|
|||
|
|
if (startDate) customData.startDate = startDate;
|
|||
|
|
if (readDate) customData.readDate = readDate;
|
|||
|
|
if (readMethod) customData.readMethod = readMethod;
|
|||
|
|
if (bookCategory) customData.bookCategory = bookCategory;
|
|||
|
|
if (recommendation > 0) customData.recommendation = recommendation;
|
|||
|
|
|
|||
|
|
// 构建短代码
|
|||
|
|
var shortcode = '[book:' + bookId;
|
|||
|
|
|
|||
|
|
// 如果有短评或自定义数据
|
|||
|
|
if (review || Object.keys(customData).length > 0) {
|
|||
|
|
var content = '';
|
|||
|
|
|
|||
|
|
// 处理短评部分 - 需要对短评中的方括号进行编码
|
|||
|
|
if (review) {
|
|||
|
|
// 对短评中的方括号进行编码
|
|||
|
|
var encodedReview = review.replace(/[\[\]]/g, function(match) {
|
|||
|
|
if (match === '[') return '[';
|
|||
|
|
if (match === ']') return ']';
|
|||
|
|
return match;
|
|||
|
|
});
|
|||
|
|
content = encodedReview;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有自定义数据,添加到内容后面
|
|||
|
|
if (Object.keys(customData).length > 0) {
|
|||
|
|
// 将自定义数据转为JSON并编码
|
|||
|
|
var customJson = encodeURIComponent(JSON.stringify(customData));
|
|||
|
|
if (content) {
|
|||
|
|
content += '|CUSTOM:' + customJson;
|
|||
|
|
} else {
|
|||
|
|
content = '|CUSTOM:' + customJson;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
shortcode += ':' + content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
shortcode += ']';
|
|||
|
|
|
|||
|
|
// 在编辑器中显示中文
|
|||
|
|
var displayShortcode = '[book:' + bookId;
|
|||
|
|
if (review || Object.keys(customData).length > 0) {
|
|||
|
|
var displayContent = review;
|
|||
|
|
if (Object.keys(customData).length > 0) {
|
|||
|
|
var displayCustomText = '(自定义:';
|
|||
|
|
var customParts = [];
|
|||
|
|
if (startDate) customParts.push('开始阅读:' + startDate);
|
|||
|
|
if (readDate) customParts.push('结束阅读:' + readDate);
|
|||
|
|
if (readMethod) customParts.push('阅读方法:' + readMethod);
|
|||
|
|
if (bookCategory) customParts.push('图书分类:' + bookCategory);
|
|||
|
|
if (recommendation > 0) customParts.push('推荐指数:' + '★'.repeat(recommendation));
|
|||
|
|
|
|||
|
|
displayCustomText += customParts.join(',') + ')';
|
|||
|
|
|
|||
|
|
if (displayContent) {
|
|||
|
|
displayContent += displayCustomText;
|
|||
|
|
} else {
|
|||
|
|
displayContent = displayCustomText;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
displayShortcode += ':' + displayContent;
|
|||
|
|
}
|
|||
|
|
displayShortcode += ']';
|
|||
|
|
|
|||
|
|
insertBookShortcode(displayShortcode);
|
|||
|
|
closeDialog();
|
|||
|
|
} else if (bookId) {
|
|||
|
|
alert('请输入有效的图书ID(纯数字)');
|
|||
|
|
} else {
|
|||
|
|
alert('请输入豆瓣图书ID');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 取消按钮点击
|
|||
|
|
cancelBtn.addEventListener('click', closeDialog);
|
|||
|
|
|
|||
|
|
// 点击遮罩层关闭
|
|||
|
|
overlay.addEventListener('click', closeDialog);
|
|||
|
|
|
|||
|
|
// 键盘事件
|
|||
|
|
bookIdInput.addEventListener('keydown', function(e) {
|
|||
|
|
if (e.key === 'Enter') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
insertBtn.click();
|
|||
|
|
} else if (e.key === 'Escape') {
|
|||
|
|
closeDialog();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
reviewInput.addEventListener('keydown', function(e) {
|
|||
|
|
if (e.key === 'Enter' && e.ctrlKey) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
insertBtn.click();
|
|||
|
|
} else if (e.key === 'Escape') {
|
|||
|
|
closeDialog();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 关闭对话框
|
|||
|
|
function closeDialog() {
|
|||
|
|
if (overlay.parentNode) document.body.removeChild(overlay);
|
|||
|
|
if (modal.parentNode) document.body.removeChild(modal);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 插入短代码到编辑器 - 显示中文而不是编码
|
|||
|
|
function insertBookShortcode(shortcode) {
|
|||
|
|
var textarea = document.getElementById('text');
|
|||
|
|
if (!textarea) return;
|
|||
|
|
|
|||
|
|
var start = textarea.selectionStart;
|
|||
|
|
var end = textarea.selectionEnd;
|
|||
|
|
var text = textarea.value;
|
|||
|
|
|
|||
|
|
// 直接插入中文短代码到编辑器
|
|||
|
|
textarea.value = text.substring(0, start) + shortcode + text.substring(end);
|
|||
|
|
textarea.selectionStart = textarea.selectionEnd = start + shortcode.length;
|
|||
|
|
textarea.focus();
|
|||
|
|
|
|||
|
|
// 触发输入事件
|
|||
|
|
var event = new Event('input', { bubbles: true });
|
|||
|
|
textarea.dispatchEvent(event);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化
|
|||
|
|
if (document.readyState === 'loading') {
|
|||
|
|
document.addEventListener('DOMContentLoaded', initBookButton);
|
|||
|
|
} else {
|
|||
|
|
initBookButton();
|
|||
|
|
}
|
|||
|
|
})();
|
|||
|
|
</script>
|
|||
|
|
HTML;
|
|||
|
|
}
|
|||
|
|
}
|