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

前端

确定需求

图片缩放肯定不能所有全站图片都缩放,比如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, 插件

已有 6 条评论

  1. 海神雕像 海神雕像

    我一直梦想, 去那么多国家。真的很鼓舞。

  2. 自然安靜 自然安靜

    出色的 旅行项目, 不要停下 保持节奏。致敬!

  3. 海灘度假 海灘度假

    信息丰富的 出行资源! 你们真棒!

  4. 觀景平台 觀景平台

    很棒的 旅行分享! 我准备订票了。

  5. 紅頂古城 紅頂古城

    很稀有, 没有多余矫饰的表达。太棒了。

  6. 城市空間 城市空間

    鼓舞人心的 内容! 这是出色的工作。

添加新评论

Loading...
Fullscreen Image