想找一个图片点击放大的插件,看了一圈,感觉都太重了,我只想要实现一个点击可以全屏预览、缩放就可以了,工作量很少,自己写一个吧

前端

确定需求

图片缩放肯定不能所有全站图片都缩放,比如logo,评论人头像等。所以要指定区间,我用的主题是自己修改过的官方主题,看过代码能看到,文章内容是被这个div包起来的
QQ20241013-114111.jpg

所以需求就是,被这个div包裹的所有图片元素,点击之后会增加全屏蒙版,然后把图片贴到蒙版上,并且图片可以缩放、拖动,再点击蒙版进行关闭,图片加载过程中要有loading,并且加载后就缓存起来,重复查看不要再请求网络

代码

直接CHatGPT,这种简单的代码,问问GPT又快又好的给我写好了

<div class="fullscreen-overlay" id="overlay">
    <div class="loading">Loading...</div> <!-- Loading text -->
    <img src="" alt="Fullscreen Image" id="fullscreenImage">
</div>
        img {
            max-width: 100%;
            cursor: pointer;
            transition: transform 0.1s ease;
        }

        /* Fullscreen modal with black overlay */
        .fullscreen-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            background: rgba(0, 0, 0, 0.8);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 1000;
            visibility: hidden;
            opacity: 0;
            transition: opacity 0.3s ease;
        }

        /* When active, overlay is visible */
        .fullscreen-overlay.active {
            visibility: visible;
            opacity: 1;
        }

        /* Disable scrolling */
        body.no-scroll {
            overflow: hidden;
        }

        /* Fullscreen image styling */
        .fullscreen-overlay img {
            max-width: 100%;
            max-height: 100%;
            cursor: grab;
            transform: scale(1) translate(0, 0);
            transition: transform 0.1s ease;
            display: none; /* Initially hide the image until it's loaded */
        }

        .fullscreen-overlay img:active {
            cursor: grabbing;
        }

        /* Loading spinner or text */
        .loading {
            color: white;
            font-size: 24px;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            display: none;
        }

        /* Show loading when image is not ready */
        .fullscreen-overlay.loading-active .loading {
            display: block;
        }
    const postContent = document.querySelector('.post-content');
    const overlay = document.getElementById('overlay');
    const fullscreenImage = document.getElementById('fullscreenImage');
    const loadingText = overlay.querySelector('.loading');
    let scale = 1;
    let isDragging = false;
    let startX, startY, currentX = 0, currentY = 0;
    let cachedImages = {}; // Cache for storing loaded images

    // Add event listeners to all images within the .post-content div
    const images = postContent.querySelectorAll('img');
    images.forEach(image => {
        image.addEventListener('click', () => {
            const imgSrc = image.src;

            // Display loading text
            overlay.classList.add('loading-active');
            loadingText.style.display = 'block';

            // Check if image is already cached
            if (!cachedImages[imgSrc]) {
                // Cache the image if it's not cached yet
                fullscreenImage.src = imgSrc;
                cachedImages[imgSrc] = true;

                // Show overlay but keep image hidden until it's fully loaded
                overlay.classList.add('active');
                document.body.classList.add('no-scroll');

                // When the image loads, hide the loading text and display the image
                fullscreenImage.onload = () => {
                    overlay.classList.remove('loading-active');
                    loadingText.style.display = 'none';
                    fullscreenImage.style.display = 'block';
                    resetImageTransform();
                };
            } else {
                // If image is cached, show it immediately
                overlay.classList.add('active');
                document.body.classList.add('no-scroll');
                overlay.classList.remove('loading-active');
                loadingText.style.display = 'none';
                fullscreenImage.style.display = 'block';
                fullscreenImage.src = imgSrc;
                resetImageTransform();
            }
        });
    });

    // Hide fullscreen when clicking on overlay (outside the image)
    overlay.addEventListener('click', (event) => {
        if (event.target === overlay) {
            overlay.classList.remove('active');
            scale = 1;
            fullscreenImage.style.transform = `scale(${scale}) translate(0, 0)`;
            fullscreenImage.style.display = 'none'; // Hide image when closing
            // Re-enable body scrolling
            document.body.classList.remove('no-scroll');
        }
    });

    // Reset the image scale and position when opened
    function resetImageTransform() {
        scale = 1;
        currentX = 0;
        currentY = 0;
        fullscreenImage.style.transform = `scale(${scale}) translate(0, 0)`;
    }

    // Zoom in/out on mouse wheel
    overlay.addEventListener('wheel', (event) => {
        event.preventDefault();
        const zoomSpeed = 0.1;

        if (event.deltaY < 0) {
            // Zoom in
            scale += zoomSpeed;
        } else {
            // Zoom out
            scale = Math.max(1, scale - zoomSpeed);  // Prevent zooming out beyond original size
        }

        fullscreenImage.style.transform = `scale(${scale}) translate(${currentX}px, ${currentY}px)`;
    });

    // Dragging logic
    fullscreenImage.addEventListener('mousedown', (event) => {
        isDragging = true;
        startX = event.clientX - currentX;
        startY = event.clientY - currentY;
        fullscreenImage.style.cursor = 'grabbing';
    });

    document.addEventListener('mousemove', (event) => {
        if (isDragging) {
            currentX = event.clientX - startX;
            currentY = event.clientY - startY;
            fullscreenImage.style.transform = `scale(${scale}) translate(${currentX}px, ${currentY}px)`;
        }
    });

    document.addEventListener('mouseup', () => {
        isDragging = false;
        fullscreenImage.style.cursor = 'grab';
    });

    // Prevent text/image selection while dragging
    fullscreenImage.addEventListener('dragstart', (event) => {
        event.preventDefault();
    });

直接使用

其实这里就可以直接用了,把对应的代码粘贴到对应的位置就行了,没啥好说的。但这样直接就硬写入到主题中了,所以写个插件

插件

插件基于最新版的typecho 1.3.0编写,插件名就叫做SimplePicView

开发步骤

空插件

先下载代码https://github.com/typecho/typecho/releases/download/v1.3.0-alpha/typecho.zip并解压,然后用编辑器打开

在找到usr/plugins目录,新建一个目录SimplePicView,然后在SimplePicView目录内新建一个Plugin.php

以下是内容

<?php

namespace TypechoPlugin\SimplePicView;

use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;

if (!defined('__TYPECHO_ROOT_DIR__')) {
    exit;
}

/**
 * SimplePicView
 *
 * @package SimplePicView
 * @author Evlan
 * @version 1.0.0
 * @link https://evlan.cc
 */
class Plugin implements PluginInterface
{
    /**
     * 激活插件方法,如果激活失败,直接抛出异常
     */
    public static function activate()
    {
    }

    /**
     * 禁用插件方法,如果禁用失败,直接抛出异常
     */
    public static function deactivate()
    {
    }

    /**
     * 获取插件配置面板
     *
     * @param Form $form 配置面板
     */
    public static function config(Form $form)
    {
    }

    /**
     * 个人用户的配置面板
     *
     * @param Form $form
     */
    public static function personalConfig(Form $form)
    {
    }

}

这样一个空插件就好了

填充代码

下面是我的思路,顺藤摸瓜。

首先我们要填充css样式代码,在页面主题头部可以看到有这个方法执行,那就可以在这里输出

QQ20241013-122209.jpg

最后在var/Widget/Archive.php找到这个header()方法,发现在最末尾有这个插件支持,那我们就接着从这里入手

QQ20241013-123536.jpg

这个pluginHandle()方法是继承var/Typecho/Widget.php的,他把自己的名字传进去,获取一个插件。代码如下

    /**
     * 获取对象插件句柄
     *
     * @return Plugin
     */
    public static function pluginHandle(): Plugin
    {
        return Plugin::factory(static::class);
    }

因为是继承var/Typecho/Widget.php的方法,var/Widget/Archive.php本身并没有实现,所以这里要用static::class才能获取到自己的名字。如果用self获取到的就是var/Typecho/Widget.php的名字
这里简单说一下static和self的区别,self是定义这个方法的类,而static是当前类,比如下面这个例子

class A
{
    public static function echo(): void
    {
        echo 'A';
    }

    public static function do(): void
    {
        self::echo();
    }

    public static function do2()
    {
        static::echo();
    }
}

class B extends A
{
    public static function echo(): void
    {
        echo 'B';
    }
}

B::do();//输出A
B::do2();//输出B

再深入Plugin::factory发现是一个单例,就是有值就直接返回,没有值就new一个自己设置进去然后再返回。

    /**
     * 获取实例化插件对象
     *
     * @param string $handle 插件
     * @return Plugin
     */
    public static function factory(string $handle): Plugin
    {
        return self::$instances[$handle] ?? (self::$instances[$handle] = new self($handle));
    }

再看这个new方法,解释放图中

  /**
     * 插件初始化
     *
     * @param string $handle 插件
     */
    public function __construct(string $handle)
    {
        //'Typecho_Plugin_Interface'    => '\Typecho\Plugin\PluginInterface'把名字做一个转换
        //根据上下文,传入的应该是\Widget\Archive。看__TYPECHO_CLASS_ALIASES__这个定义,此处没做处理
        if (defined('__TYPECHO_CLASS_ALIASES__')) {
            $alias = array_search('\\' . ltrim($handle, '\\'), __TYPECHO_CLASS_ALIASES__);
            $handle = $alias ?: $handle;
        }
        //这里再进行一个转换  出来是 Widget_Archive
        $this->handle = Common::nativeClassName($handle);
    }

所以这个self::pluginHandle()方法就是从Plugin类中获取一个属于var/Widget/Archive.php类的单例Plugin,所以返回的是一个Plugin(插件)

后面调用了call('header', $header, $this),我们看Plugin中的call方法,代码如下

    /**
     * 回调处理函数
     *
     * @param string $component 当前组件
     * @param array $args 参数
     * @return mixed
     */
    public function call(string $component, ...$args)
    {
        //根据上下文,$component = 'Widget_Archive:header'
        $component = $this->handle . ':' . $component;
        $last = count($args);
        $args[$last] = $last > 0 ? $args[0] : false;

        if (isset(self::$plugin['handles'][$component])) {
            $args[$last] = null;
            $this->signal = true;
            foreach (self::$plugin['handles'][$component] as $callback) {
                $args[$last] = call_user_func_array($callback, $args);
            }
        }

        return $args[$last];
    }

可以看到最终调用了Plugin::$plugin['handles']['Widget_Archive:header']这个数组里的回调,也就是我们在的插件把输出css代码的语句放进去就好了。
找一下是在这里赋值的

/**
     * 设置回调函数
     *
     * @param string $component 当前组件
     * @param callable $value 回调函数
     */
    public function __set(string $component, callable $value)
    {
        $weight = 0;

        if (strpos($component, '_') > 0) {
            $parts = explode('_', $component, 2);
            [$component, $weight] = $parts;
            $weight = intval($weight) - 10;
        }

        $component = $this->handle . ':' . $component;

        if (!isset(self::$plugin['handles'][$component])) {
            self::$plugin['handles'][$component] = [];
        }

        if (!isset(self::$tmp['handles'][$component])) {
            self::$tmp['handles'][$component] = [];
        }

        foreach (self::$plugin['handles'][$component] as $key => $val) {
            $key = floatval($key);

            if ($weight > $key) {
                break;
            } elseif ($weight == $key) {
                $weight += 0.001;
            }
        }

        self::$plugin['handles'][$component][strval($weight)] = $value;
        self::$tmp['handles'][$component][] = $value;

        ksort(self::$plugin['handles'][$component], SORT_NUMERIC);
    }

是一个魔术方法,直接给Plugin对象赋值就能走到这里,所以执行这个代码就能走到这里,调用代码是\Typecho\Plugin::factory(\Widget\Archive::class)->header = 回调function,看一下HelloWorld那个插件,在激活方法中正好和我们写的这个差不多。

再往下找就没找到什么东西了,换个方向从激活插件的方法入手找。
首先找到这个方法,看哪里调用了
QQ20241013-140728.jpg

最终找到这里var/Widget/Plugins/Edit.php
QQ20241013-141133.jpg
执行完激活方法之后,执行这个Plugin::activate($pluginName);
内容如下

    /**
     * 启用插件
     *
     * @param string $pluginName 插件名称
     */
    public static function activate(string $pluginName)
    {
        self::$plugin['activated'][$pluginName] = self::$tmp;
        self::$tmp = [];
    }

就是把这个self::$tmp;静态变量存到self::$plugin['activated'][$pluginName]中,
再往下这个

$this->update(
                    ['value' => json_encode(Plugin::export())],
                    $this->db->sql()->where('name = ?', 'plugins')
                );

一看就是把self::$plugin转成json格式存到数据库中,所以激活的时候self::$tmp变量很关键,而恰好,上面往后找不到后续的__set方法中就有给这个self::$tmp赋值...

破案了,只要在激活插件的方法里给\Widget\Archive::class的Plugin附上我们的函数,他激活插件的时候就会执行,然后在__set方法中就会赋值self::$tmp,接下来就数据入库,激活插件。

所以在SimplePicView/Plugin.php的activate方法写下赋值插件函数回调

    /**
     * 激活插件方法,如果激活失败,直接抛出异常
     */
    public static function activate()
    {
        //因为要json入库数据库,所以回调要这么写,不能直接写方法
        \Typecho\Plugin::factory(\Widget\Archive::class)->header = [self::class, 'setHeader'];
    }

    public static function setHeader()
    {
        echo 666;
    }

激活,然后刷新首页看看。
QQ20241013-144555.jpg
成功。以此类推,找到对应的切入点html、js也切进去

QQ20241013-145145.jpg

使用

最终结果usr/plugins/SimplePicView/Plugin.php:

<?php

namespace TypechoPlugin\SimplePicView;

use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;

if (!defined('__TYPECHO_ROOT_DIR__')) {
    exit;
}

/**
 * 一个简单的图片预览插件,支持全屏预览。只对默认主题进行了适配。有问题可看链接自定义修改
 *
 * @package SimplePicView
 * @author Evlan
 * @version 1.0.0
 * @link https://evlan.cc/archives/typecho-plugins-pic-view.html
 */
class Plugin implements PluginInterface
{
    /**
     * 激活插件方法,如果激活失败,直接抛出异常
     */
    public static function activate()
    {
        \Typecho\Plugin::factory(\Widget\Archive::class)->header = [self::class, 'setHeader'];
        \Typecho\Plugin::factory(\Widget\Archive::class)->footer = [self::class, 'setFooter'];
    }

    public static function setHeader(): void
    {
        echo <<<'EOL'
    <style>
        img {
            max-width: 100%;
            cursor: pointer;
            transition: transform 0.1s ease;
        }

        /* Fullscreen modal with black overlay */
        .fullscreen-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            background: rgba(0, 0, 0, 0.8);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 1000;
            visibility: hidden;
            opacity: 0;
            transition: opacity 0.3s ease;
        }

        /* When active, overlay is visible */
        .fullscreen-overlay.active {
            visibility: visible;
            opacity: 1;
        }

        /* Disable scrolling */
        body.no-scroll {
            overflow: hidden;
        }

        /* Fullscreen image styling */
        .fullscreen-overlay img {
            max-width: 100%;
            max-height: 100%;
            cursor: grab;
            transform: scale(1) translate(0, 0);
            transition: transform 0.1s ease;
            display: none; /* Initially hide the image until it's loaded */
        }

        .fullscreen-overlay img:active {
            cursor: grabbing;
        }

        /* Loading spinner or text */
        .loading {
            color: white;
            font-size: 24px;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            display: none;
        }

        /* Show loading when image is not ready */
        .fullscreen-overlay.loading-active .loading {
            display: block;
        }
    </style>
EOL;

    }

    public static function setFooter(): void
    {
        echo <<<'EOL'
<div class="fullscreen-overlay" id="overlay">
    <div class="loading">Loading...</div> <!-- Loading text -->
    <img src="" alt="Fullscreen Image" id="fullscreenImage">
</div>

<script>
    const postContent = document.querySelector('.post-content');
    const overlay = document.getElementById('overlay');
    const fullscreenImage = document.getElementById('fullscreenImage');
    const loadingText = overlay.querySelector('.loading');
    let scale = 1;
    let isDragging = false;
    let startX, startY, currentX = 0, currentY = 0;
    let cachedImages = {}; // Cache for storing loaded images

    // Add event listeners to all images within the .post-content div
    const images = postContent.querySelectorAll('img');
    images.forEach(image => {
        image.addEventListener('click', () => {
            const imgSrc = image.src;

            // Display loading text
            overlay.classList.add('loading-active');
            loadingText.style.display = 'block';

            // Check if image is already cached
            if (!cachedImages[imgSrc]) {
                // Cache the image if it's not cached yet
                fullscreenImage.src = imgSrc;
                cachedImages[imgSrc] = true;

                // Show overlay but keep image hidden until it's fully loaded
                overlay.classList.add('active');
                document.body.classList.add('no-scroll');

                // When the image loads, hide the loading text and display the image
                fullscreenImage.onload = () => {
                    overlay.classList.remove('loading-active');
                    loadingText.style.display = 'none';
                    fullscreenImage.style.display = 'block';
                    resetImageTransform();
                };
            } else {
                // If image is cached, show it immediately
                overlay.classList.add('active');
                document.body.classList.add('no-scroll');
                overlay.classList.remove('loading-active');
                loadingText.style.display = 'none';
                fullscreenImage.style.display = 'block';
                fullscreenImage.src = imgSrc;
                resetImageTransform();
            }
        });
    });

    // Hide fullscreen when clicking on overlay (outside the image)
    overlay.addEventListener('click', (event) => {
        if (event.target === overlay) {
            overlay.classList.remove('active');
            scale = 1;
            fullscreenImage.style.transform = `scale(${scale}) translate(0, 0)`;
            fullscreenImage.style.display = 'none'; // Hide image when closing
            // Re-enable body scrolling
            document.body.classList.remove('no-scroll');
        }
    });

    // Reset the image scale and position when opened
    function resetImageTransform() {
        scale = 1;
        currentX = 0;
        currentY = 0;
        fullscreenImage.style.transform = `scale(${scale}) translate(0, 0)`;
    }

    // Zoom in/out on mouse wheel
    overlay.addEventListener('wheel', (event) => {
        event.preventDefault();
        const zoomSpeed = 0.1;

        if (event.deltaY < 0) {
            // Zoom in
            scale += zoomSpeed;
        } else {
            // Zoom out
            scale = Math.max(1, scale - zoomSpeed);  // Prevent zooming out beyond original size
        }

        fullscreenImage.style.transform = `scale(${scale}) translate(${currentX}px, ${currentY}px)`;
    });

    // Dragging logic
    fullscreenImage.addEventListener('mousedown', (event) => {
        isDragging = true;
        startX = event.clientX - currentX;
        startY = event.clientY - currentY;
        fullscreenImage.style.cursor = 'grabbing';
    });

    document.addEventListener('mousemove', (event) => {
        if (isDragging) {
            currentX = event.clientX - startX;
            currentY = event.clientY - startY;
            fullscreenImage.style.transform = `scale(${scale}) translate(${currentX}px, ${currentY}px)`;
        }
    });

    document.addEventListener('mouseup', () => {
        isDragging = false;
        fullscreenImage.style.cursor = 'grab';
    });

    // Prevent text/image selection while dragging
    fullscreenImage.addEventListener('dragstart', (event) => {
        event.preventDefault();
    });
</script>
EOL;
    }



    /**
     * 禁用插件方法,如果禁用失败,直接抛出异常
     */
    public static function deactivate()
    {
    }

    /**
     * 获取插件配置面板
     *
     * @param Form $form 配置面板
     */
    public static function config(Form $form)
    {
    }

    /**
     * 个人用户的配置面板
     *
     * @param Form $form
     */
    public static function personalConfig(Form $form)
    {
    }

}

1.手动安装
在插件目录新建SimplePicView目录,然后在里面建一个Plugin.php文件,把上面的代码贴进去,然后后台激活即可
2.安装包
下载之后在插件目录解压即可https://evlan.cc/download/file/TypechoSimplePicView.zip

标签: PHP, JavaScript, Typecho, 插件

添加新评论

Loading...
Fullscreen Image