图床上传脚本

在右下角添加悬浮球,点击对应图床按钮弹出上传表单对话框(目前支持 imgURL/SMMS 图床)

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         图床上传脚本
// @namespace    http://21zys.com/
// @version      1.3.7
// @description  在右下角添加悬浮球,点击对应图床按钮弹出上传表单对话框(目前支持 imgURL/SMMS 图床)
// @match        *://*/*
// @author       21zys (Optimized)
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.13/dayjs.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/uuid/8.3.2/uuid.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js
// @connect      sm.ms
// @connect      smms.app
// @connect      imgurl.org
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    if (window !== window.top) return;

    // --- 工具函数:DOM 创建与样式设置 ---
    function createEl(tag, styles = {}, props = {}, parent = null) {
        const el = document.createElement(tag);
        Object.assign(el.style, styles);
        for (const key in props) {
            if (key === 'dataset') {
                Object.assign(el.dataset, props[key]);
            } else {
                el[key] = props[key];
            }
        }
        if (parent) parent.appendChild(el);
        return el;
    }

    // --- 工具函数:通用拖拽逻辑 ---
    // 新增 restrictToEdge 参数,默认为 true。悬浮球传入 false 以允许全区域拖拽。
    function makeDraggable(element, storageKey, handle = null, restrictToEdge = true) {
        const target = handle || element;
        let isDragging = false, startX, startY;

        target.addEventListener('mousedown', (e) => {
            if (handle && e.target !== handle) return;

            // 边缘检测逻辑:如果 restrictToEdge 为 true,且点击位置不在边缘,则不开始拖拽
            if (!handle && restrictToEdge) {
                const rect = element.getBoundingClientRect();
                const edgeThreshold = 25;
                const offsetX = e.clientX - rect.left;
                const offsetY = e.clientY - rect.top;
                if (offsetX > edgeThreshold && offsetX < element.clientWidth - edgeThreshold &&
                    offsetY > edgeThreshold && offsetY < element.clientHeight - edgeThreshold) {
                    return;
                }
            }

            startX = e.clientX;
            startY = e.clientY;
            const rect = element.getBoundingClientRect();
            const offsetX = e.clientX - rect.left;
            const offsetY = e.clientY - rect.top;

            const onMouseMove = (e) => {
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                if (!isDragging && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
                    isDragging = true;
                }
                if (isDragging) {
                    element.style.left = (e.clientX - offsetX) + 'px';
                    element.style.top = (e.clientY - offsetY) + 'px';
                    element.style.right = 'auto';
                    element.style.bottom = 'auto';
                    element.style.transform = 'none';

                    if (storageKey) {
                        localStorage.setItem(storageKey, JSON.stringify({
                            left: element.style.left,
                            top: element.style.top
                        }));
                    }
                }
            };

            const onMouseUp = () => {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
                setTimeout(() => isDragging = false, 100);
            };

            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });
    }

    // --- 工具函数:通用对话框 UI 构建 ---
    function createBaseDialog(storageKey) {
        const savedPos = JSON.parse(localStorage.getItem(storageKey)) || null;
        const dialogStyle = {
            position: 'fixed', width: '400px', padding: '20px',
            backgroundColor: 'rgba(255, 255, 255, 0.8)', borderRadius: '12px',
            backdropFilter: 'blur(10px)', boxShadow: '0 8px 32px rgba(0, 0, 0, 0.37)',
            display: 'none', opacity: '0', zIndex: '999999', fontFamily: 'Arial, sans-serif',
            transition: 'opacity 0.3s ease',
            left: savedPos ? savedPos.left : '50%',
            top: savedPos ? savedPos.top : '50%',
            transform: savedPos ? 'none' : 'translate(-50%, -50%)'
        };

        const dialog = createEl('div', dialogStyle, {}, document.body);
        // 对话框保持默认行为:只有边缘可以拖拽
        makeDraggable(dialog, storageKey);

        // 关闭按钮
        createEl('span', {
            position: 'absolute', top: '10px', right: '10px', cursor: 'pointer',
            fontSize: '20px', color: '#333'
        }, { innerHTML: '&times;', onclick: () => closeDialog(dialog) }, dialog);

        return dialog;
    }

    // --- 工具函数:通用表单控件 ---
    const commonStyles = {
        label: { fontWeight: 'bold', color: '#333', display: 'inline-block' },
        input: { padding: '8px', border: '1px solid #ccc', borderRadius: '4px', width: 'auto' },
        btn: { padding: '8px 16px', border: 'none', borderRadius: '4px', cursor: 'pointer', transition: '0.3s' }
    };

    function createInputRow(form, labelText, inputName, value = '', placeholder = '', type = 'text') {
        createEl('label', commonStyles.label, { innerText: labelText }, form);
        return createEl('input', commonStyles.input, {
            type: type, name: inputName, value: value, placeholder: placeholder
        }, form);
    }

    // --- 核心逻辑 ---

    const savedPosition = JSON.parse(localStorage.getItem('floatingBallPosition')) || { right: '20px', bottom: '20px' };
    const containerStyle = {
        position: 'fixed', right: savedPosition.right, bottom: savedPosition.bottom,
        left: savedPosition.left || 'auto', top: savedPosition.top || 'auto',
        padding: '25px', zIndex: '99999'
    };

    const floatingContainer = createEl('div', containerStyle, {}, document.body);
    // 修复:悬浮球传入 false,禁用边缘检测,允许点击任意位置拖拽
    makeDraggable(floatingContainer, 'floatingBallPosition', null, false);

    // 悬浮球
    const ballStyle = {
        width: '50px', height: '50px', borderRadius: '50%', backgroundColor: '#007bff',
        color: '#fff', textAlign: 'center', lineHeight: '50px', cursor: 'pointer',
        fontSize: '24px', userSelect: 'none', backdropFilter: 'blur(6px)',
        boxShadow: 'rgba(90, 90, 90, 1) 2px 2px 9px 0px'
    };
    const floatingBall = createEl('div', ballStyle, { innerHTML: '+' }, floatingContainer);

    // 子按钮生成器
    const createSubBtn = (iconUrl, left, onClick) => {
        const btn = createEl('div', {
            position: 'relative', bottom: '90px', left: left, width: '50px', height: '50px',
            background: `url('${iconUrl}') no-repeat center center`, backgroundSize: '50% 50%',
            backgroundColor: 'rgba(0,0,0,0)', cursor: 'pointer', display: 'none', zIndex: '99999'
        }, {}, floatingBall);
        btn.addEventListener('click', onClick);
        return btn;
    };

    const imgUrlBtn = createSubBtn('https://www.imgurl.org/favicon.ico', '35px', () => openDialog(initImgUrlDialog()));
    const smmsBtn = createSubBtn('https://smms.app/favicon-32x32.png', '50px', () => openDialog(initSmmsDialog()));

    floatingContainer.addEventListener('mouseenter', () => { imgUrlBtn.style.display = 'block'; smmsBtn.style.display = 'block'; });
    floatingContainer.addEventListener('mouseleave', () => { imgUrlBtn.style.display = 'none'; smmsBtn.style.display = 'none'; });

    // 缓存 Dialog 实例,避免重复创建
    let _imgUrlDialogInstance = null;
    let _smmsDialogInstance = null;

    function openDialog(dialog) {
        dialog.style.display = 'block';
        setTimeout(() => dialog.style.opacity = '1', 10);
    }

    function closeDialog(dialog) {
        dialog.style.opacity = '0';
        setTimeout(() => dialog.style.display = 'none', 300);
    }

    // --- 结果回显与 Tab 管理通用逻辑 ---
    function setupResultArea(dialog, initialTab, onTabChange) {
        const tabContainer = createEl('div', { display: 'flex', justifyContent: 'space-around', marginTop: '10px', marginBottom: '3px' }, {}, dialog);
        const resultContainer = createEl('div', { marginTop: '10px', textAlign: 'center' }, {}, dialog);
        const resultInput = createEl('input', {
            width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px', cursor: 'pointer'
        }, { type: 'text', readOnly: true, placeholder: '上传结果' }, resultContainer);

        resultInput.addEventListener('click', () => GM_setClipboard(resultInput.value));

        let currentTab = initialTab;
        const tabs = ['HTML', 'imgURL', 'MarkDown', 'BBCode'];
        const btns = [];

        const updateResultFormat = () => {
            const url = resultInput.dataset.url;
            if (!url) return;
            let text = '';
            switch (currentTab) {
                case 'HTML': text = `<img src="${url}" alt="image">`; break;
                case 'imgURL': text = url; break;
                case 'MarkDown': text = `![image](${url})`; break;
                case 'BBCode': text = `[URL=${url}][IMG]${url}[/IMG][/URL]`; break; // 注意:这里统一了 imgURL 和 smms 的 BBCode 格式差异,以 SMMS 的完整格式为准
            }
            if (resultInput.value !== text) resultInput.value = text;
        };

        tabs.forEach(tab => {
            const btn = createEl('button', {
                flex: '1', padding: '10px', border: '1px solid #ccc', borderRadius: '4px 4px 0 0',
                cursor: 'pointer', backgroundColor: tab === currentTab ? '#007bff' : '#f8f9fa',
                color: tab === currentTab ? '#fff' : '#333'
            }, { textContent: tab }, tabContainer);

            btn.addEventListener('click', () => {
                currentTab = tab;
                onTabChange(tab);
                btns.forEach(b => {
                    const active = b.textContent === tab;
                    Object.assign(b.style, { backgroundColor: active ? '#007bff' : '#f8f9fa', color: active ? '#fff' : '#333' });
                });
                updateResultFormat();
            });
            btns.push(btn);
        });

        return { resultInput, updateResultFormat };
    }

    function createProgress() {
        const container = createEl('div', { marginTop: '10px', display: 'none', width: '100%' });
        const bar = createEl('progress', { width: '100%', height: '20px' }, { value: 0, max: 100 }, container);
        return { container, bar };
    }

    // --- imgURL Dialog ---
    function initImgUrlDialog() {
        if (_imgUrlDialogInstance) return _imgUrlDialogInstance;

        const STORAGE_KEY = 'globalImgUrlUploadData';
        const globalData = JSON.parse(GM_getValue(STORAGE_KEY) || localStorage.getItem(STORAGE_KEY)) || {
            uid: '您的UID', token: '您的Token', uploadDate: dayjs().format('YYYY-MM-DD'),
            uploadCount: 0, selectedTab: 'imgURL', selectedAlbumId: 'default', albumList: []
        };

        if (globalData.uploadDate !== dayjs().format('YYYY-MM-DD')) {
            globalData.uploadDate = dayjs().format('YYYY-MM-DD');
            globalData.uploadCount = 0;
            saveData();
        }

        function saveData() {
            GM_setValue(STORAGE_KEY, JSON.stringify(globalData));
            localStorage.setItem(STORAGE_KEY, JSON.stringify(globalData));
        }

        const dialog = createBaseDialog('imgUrlDialogPosition');
        const form = createEl('form', { display: 'grid', gap: '10px' }, { method: 'post' }, dialog);

        const uidInput = createInputRow(form, 'UID:', 'uid', globalData.uid);
        const tokenInput = createInputRow(form, 'Token:', 'token', globalData.token);

        // 相册部分
        const albumContainer = createEl('div', { marginTop: '10px', textAlign: 'center' }, {}, dialog);
        createEl('button', { ...commonStyles.btn, backgroundColor: '#f8f9fa', border: '1px solid #ccc' },
            { type: 'button', innerText: '刷新相册', onclick: fetchAlbums }, albumContainer);

        const albumSelect = createEl('select', {
            width: '120px', height: '34px', marginTop: '-3px', marginRight: '3px',
            textAlign: 'center', border: '1px solid #ccc', borderRadius: '4px'
        }, {}, albumContainer);

        const waterInput = createEl('input', {
            width: '120px', height: '34px', marginTop: '-3px', textAlign: 'left',
            border: '1px solid #ccc', borderRadius: '4px'
        }, { type: 'text', placeholder: '请输入水印文本', value: globalData.water || '' }, albumContainer);

        albumSelect.addEventListener('change', () => { globalData.selectedAlbumId = albumSelect.value; saveData(); });

        function loadAlbumList() {
            albumSelect.innerHTML = '';
            createEl('option', {}, { value: 'default', textContent: '默认相册' }, albumSelect);
            globalData.albumList.forEach(album => {
                createEl('option', {}, { value: album.album_id, textContent: album.name }, albumSelect);
            });
            albumSelect.value = globalData.selectedAlbumId;
        }
        loadAlbumList();

        function fetchAlbums() {
            const fd = new FormData();
            fd.append('uid', uidInput.value);
            fd.append('token', tokenInput.value);
            GM_xmlhttpRequest({
                method: 'POST', url: 'https://www.imgurl.org/api/v2/albums', data: fd,
                onload: (res) => {
                    const data = JSON.parse(res.responseText);
                    if (data?.data?.length) {
                        globalData.albumList = data.data;
                        saveData();
                        loadAlbumList();
                    } else albumSelect.innerHTML = '<option>未找到相册</option>';
                },
                onerror: () => albumSelect.innerHTML = '<option>获取失败</option>'
            });
        }

        // 文件选择与按钮
        createEl('label', commonStyles.label, { innerText: '选择文件:' }, form);
        const fileInput = createEl('input', commonStyles.input, { type: 'file', name: 'file' }, form);

        const btnContainer = createEl('div', { marginTop: '10px', textAlign: 'right' }, {}, form);
        const countLabel = createEl('label', { fontSize: '1rem', fontWeight: 'bold', marginRight: '10px' },
            { textContent: `今日已上传 ${globalData.uploadCount} 张图片` }, btnContainer);

        const uploadBtn = createEl('input', { ...commonStyles.btn, backgroundColor: '#007bff', color: '#fff', marginRight: '10px' },
            { type: 'submit', value: '开始上传' }, btnContainer);

        const clearBtn = createEl('input', { ...commonStyles.btn, backgroundColor: '#6c757d', color: '#fff' },
            { type: 'button', value: '清空' }, btnContainer);

        // 进度条
        const { container: progressContainer, bar: progressBar } = createProgress();
        dialog.appendChild(progressContainer);

        // 结果与Tab
        const { resultInput, updateResultFormat } = setupResultArea(dialog, globalData.selectedTab, (tab) => {
            globalData.selectedTab = tab; saveData();
        });

        clearBtn.onclick = () => {
            fileInput.value = ''; resultInput.value = ''; resultInput.style.color = ''; delete resultInput.dataset.url;
        };

        form.onsubmit = (e) => {
            e.preventDefault();
            if (!uidInput.value || !tokenInput.value) return alertResult('请填写UID和Token', 'red');

            globalData.uid = uidInput.value.trim();
            globalData.token = tokenInput.value.trim();
            globalData.water = waterInput.value.trim();
            saveData();

            if (!fileInput.files.length) return alertResult('请选择文件', 'red');

            const file = fileInput.files[0];
            if (!['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(file.name.split('.').pop().toLowerCase())) {
                return alertResult('格式不支持', 'red');
            }

            addWatermark(file, globalData.water, (blob) => {
                progressContainer.style.display = 'block';
                const fd = new FormData();
                fd.append('file', blob, file.name);
                fd.append('uid', globalData.uid);
                fd.append('token', globalData.token);
                if (albumSelect.value !== 'default') fd.append('album_id', albumSelect.value);

                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://www.imgurl.org/api/v2/upload',
                    data: fd,
                    upload: {
                        onprogress: (e) => { if(e.lengthComputable) progressBar.value = (e.loaded / e.total) * 100; }
                    },
                    onload: (res) => {
                        progressContainer.style.display = 'none';
                        try {
                            const data = JSON.parse(res.responseText);
                            if (data?.data?.url) {
                                handleSuccess(data.data.url);
                            } else {
                                alertResult('上传失败: ' + (data.msg || '未知错误'), 'red');
                            }
                        } catch (e) { alertResult('解析失败', 'red'); }
                    },
                    onerror: () => { progressContainer.style.display = 'none'; alertResult('网络错误', 'red'); }
                });
            });
        };

        function alertResult(msg, color) {
            resultInput.value = msg;
            resultInput.style.color = color;
        }

        function handleSuccess(url) {
            resultInput.dataset.url = url;
            updateResultFormat();
            resultInput.style.color = 'green';
            globalData.uploadCount++;
            saveData();
            countLabel.textContent = `今日已上传 ${globalData.uploadCount} 张图片`;
        }

        _imgUrlDialogInstance = dialog;
        return dialog;
    }

    // --- SM.MS Dialog ---
    function initSmmsDialog() {
        if (_smmsDialogInstance) return _smmsDialogInstance;

        const STORAGE_KEY = 'globalSmmsUploadData';
        const globalData = JSON.parse(GM_getValue(STORAGE_KEY) || localStorage.getItem(STORAGE_KEY)) || {
            token: '您的Token', uploadDate: dayjs().format('YYYY-MM-DD'), uploadCount: 0,
            selectedTab: 'imgURL', water: '', renamePattern: '', autoIncrement: 0
        };

        if (globalData.uploadDate !== dayjs().format('YYYY-MM-DD')) {
            globalData.uploadDate = dayjs().format('YYYY-MM-DD');
            globalData.uploadCount = 0;
            saveData();
        }

        function saveData() {
            GM_setValue(STORAGE_KEY, JSON.stringify(globalData));
            localStorage.setItem(STORAGE_KEY, JSON.stringify(globalData));
        }

        const dialog = createBaseDialog('smmsDialogPosition');
        const form = createEl('form', { display: 'grid', gap: '10px' }, { method: 'post' }, dialog);

        const waterInput = createInputRow(form, '文本水印:', 'smms-water', globalData.water, '请输入需要添加的文本水印');
        const renameInput = createInputRow(form, '高级文件重命名:', 'smms-rename', globalData.renamePattern, '重命名格式(忽略请留空)');
        const tokenInput = createInputRow(form, 'Token:', 'token', globalData.token);

        createEl('label', commonStyles.label, { innerText: '选择文件:' }, form);
        const fileInput = createEl('input', commonStyles.input, { type: 'file', name: 'file' }, form);

        const btnContainer = createEl('div', { marginTop: '10px', textAlign: 'right' }, {}, form);
        const countLabel = createEl('label', { fontSize: '1rem', fontWeight: 'bold', marginRight: '10px' },
            { textContent: `今日已上传 ${globalData.uploadCount} 张图片` }, btnContainer);

        const uploadBtn = createEl('input', { ...commonStyles.btn, backgroundColor: '#007bff', color: '#fff', marginRight: '10px' },
            { type: 'submit', value: '开始上传' }, btnContainer);
        const clearBtn = createEl('input', { ...commonStyles.btn, backgroundColor: '#6c757d', color: '#fff' },
            { type: 'button', value: '清空' }, btnContainer);

        const { container: progressContainer, bar: progressBar } = createProgress();
        dialog.appendChild(progressContainer);

        const { resultInput, updateResultFormat } = setupResultArea(dialog, globalData.selectedTab, (tab) => {
            globalData.selectedTab = tab; saveData();
        });

        clearBtn.onclick = () => {
            fileInput.value = ''; resultInput.value = ''; resultInput.style.color = ''; delete resultInput.dataset.url;
        };

        form.onsubmit = (e) => {
            e.preventDefault();
            if (!tokenInput.value) return alertResult('请填写Token', 'red');

            globalData.token = tokenInput.value.trim();
            globalData.water = waterInput.value.trim();
            globalData.renamePattern = renameInput.value.trim();
            globalData.autoIncrement++;
            saveData();

            if (!fileInput.files.length) return alertResult('请选择文件', 'red');
            const file = fileInput.files[0];
            if (!['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(file.name.split('.').pop().toLowerCase())) {
                return alertResult('格式不支持', 'red');
            }

            addWatermark(file, globalData.water, (blob) => {
                progressContainer.style.display = 'block';
                progressBar.value = 30;
                const fd = new FormData();
                fd.append('smfile', blob, superPictureRename(file.name, globalData.renamePattern, globalData.autoIncrement));
                fd.append('format', 'json');

                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://sm.ms/api/v2/upload',
                    headers: { 'Authorization': globalData.token },
                    data: fd,
                    upload: {
                        onprogress: (e) => { if(e.lengthComputable) progressBar.value = (e.loaded / e.total) * 100; }
                    },
                    onload: (res) => {
                        progressBar.value = 100;
                        progressContainer.style.display = 'none';
                        try {
                            const data = JSON.parse(res.responseText);
                            if (data.success && data.data?.url) {
                                handleSuccess(data.data.url);
                            } else if (data.code === 'image_repeated') {
                                handleSuccess(data.images); // SM.MS 重复上传会返回 images 字段包含 url
                            } else {
                                alertResult('上传失败: ' + (data.message || '未知错误'), 'red');
                            }
                        } catch (e) { alertResult('解析失败', 'red'); }
                    },
                    onerror: () => { progressContainer.style.display = 'none'; alertResult('网络错误', 'red'); }
                });
            });
        };

        function alertResult(msg, color) { resultInput.value = msg; resultInput.style.color = color; }
        function handleSuccess(url) {
            resultInput.dataset.url = url;
            updateResultFormat();
            resultInput.style.color = 'green';
            globalData.uploadCount++;
            saveData();
            countLabel.textContent = `今日已上传 ${globalData.uploadCount} 张图片`;
        }

        _smmsDialogInstance = dialog;
        return dialog;
    }

    // --- 业务辅助函数 ---
    function addWatermark(file, text, callback) {
        if (!text) return callback(file); // 如果没有水印文本,直接返回原文件
        const reader = new FileReader();
        reader.onload = (e) => {
            const img = new Image();
            img.src = e.target.result;
            img.onload = () => {
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                canvas.width = img.width;
                canvas.height = img.height;
                ctx.drawImage(img, 0, 0);

                const fontSize = img.width * 0.1;
                ctx.font = `${fontSize}px Arial`;
                ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';

                ctx.translate(canvas.width / 2, canvas.height / 2);
                ctx.rotate(-Math.PI / 4);
                ctx.fillText(text, 0, 0);
                ctx.rotate(Math.PI / 4);
                ctx.translate(-canvas.width / 2, -canvas.height / 2);

                canvas.toBlob(callback, file.type);
            };
        };
        reader.readAsDataURL(file);
    }

    function superPictureRename(filename, pattern, autoIncrement) {
        if (!pattern) return filename;
        const extIndex = filename.lastIndexOf('.');
        const ext = extIndex > -1 ? filename.substring(extIndex) : '';
        const baseFilename = extIndex > -1 ? filename.substring(0, extIndex) : filename;
        const now = dayjs();

        const randomString = (len) => {
            const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
            let res = '';
            for (let i = 0; i < len; i++) res += chars.charAt(Math.floor(Math.random() * chars.length));
            return res;
        };

        const formatted = pattern.replace(/{Y}/g, now.format('YYYY'))
            .replace(/{y}/g, now.format('YY'))
            .replace(/{m}/g, now.format('MM'))
            .replace(/{d}/g, now.format('DD'))
            .replace(/{h}/g, now.format('HH'))
            .replace(/{i}/g, now.format('mm'))
            .replace(/{s}/g, now.format('ss'))
            .replace(/{ms}/g, now.format('SSS'))
            .replace(/{timestamp}/g, now.valueOf())
            .replace(/{md5}/g, CryptoJS.MD5(randomString(32)).toString())
            .replace(/{md5-16}/g, CryptoJS.MD5(randomString(16)).toString().substring(0, 16))
            .replace(/{uuid}/g, uuid.v4())
            .replace(/{str-(\d+)}/g, (m, p1) => randomString(parseInt(p1)))
            .replace(/{filename}/g, baseFilename)
            .replace(/{auto}/g, autoIncrement);

        return formatted + ext;
    }
})();