MediaWiki:Common.js/uploader.js

H萌娘,万物皆可H的百科全书!
< MediaWiki:Common.js
BakeWater讨论 | 贡献2022年12月17日 (六) 23:56的版本
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

注意:这类代码页面在保存之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。

// <pre>
"use strict";
$(() => (async () => {
    await mw.loader.using(["mediawiki.api"]);
    const commonsApi = new mw.Api({
        timeout: 7000,
    });
    const cdnUrl = {
        requireJs: "https://cdn.jsdelivr.net/npm/[email protected]/require.min.js",
        jss: "https://cdn.jsdelivr.net/npm/[email protected]/dist/jss.min.js",
        jssPreset: "https://cdn.jsdelivr.net/npm/[email protected]/dist/jss-preset-default.min.js",
        vue: "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js",
    };

    /**
     * @param {string} sourceUrl 
     * @return {Promise<never>}
     */
    const loadScript = (sourceUrl) => new Promise((resolve, reject) => {
        const scriptTag = document.createElement("script");
        scriptTag.src = sourceUrl;
        document.body.appendChild(scriptTag);

        scriptTag.addEventListener("load", resolve);
        scriptTag.addEventListener("error", reject);
    });

    /**
     * @param {{ [moduleName: string]: string }[]} modulePaths
     * @return {Promise<any>}
     */
    const loadModules = (modulePaths) => new Promise((resolve, reject) => {

        const paths = Object.fromEntries(modulePaths.map((m) => Object.entries(m).map(([k, v]) => [k, v.replace(/\.js$/, "")])[0]));
        const loadModuleNames = modulePaths.map(item => Object.keys(item)[0]);

        window.require.config({ paths });
        window.require(loadModuleNames, (...modules) => resolve(modules), reject);
    });

    /**
     * @typedef {object} MwApiError
     * @property {string} code
     * @property {string} info
     */
    /**
     * @typedef {object} JQueryXHRError
     * @property {number} status
     * @property {string} statusText
     */
    /**
     * @param {string} word
     * @return {Promise<object, Error>}
     */
    const getHints = (word) => commonsApi.post({
        action: "query",
        format: "json",
        list: "search",
        srsearch: word,
        srnamespace: "14",
        srlimit: "20",
    });

    /**
     * @param {object} options
     * @param {File} options.body
     * @param {string} options.fileName
     * @param {string} options.comment
     * @param {string} options.pageContent
     * @return {Promise<object, (Error | MwApiError | JQueryXHRError)>}
     */
    async function upload({ body, fileName, comment, pageContent }) {
        const data = {
            filename: fileName,
            comment: comment,
            text: pageContent,
            action: "upload",
            format: "json",
            ignorewarnings: true,
            token: await commonsApi.getToken("csrf"),
        };
        if (typeof body === "string") {
            data.url = body;
        } else {
            data.file = body;
        }
        const formData = new FormData();
        Object.entries(data).forEach(([key, value]) => {
            formData.append(key, value);
        });
        // 点名批评 mediawiki.api 没考虑过跨域传文件!
        return $.ajax({
            url: `${mw.config.get("wgServer") + mw.config.get("wgScriptPath")}/api.php`,
            type: "post",
            timeout: 30000,
            xhrFields: { withCredentials: true },
            contentType: false,
            processData: false,
            data: formData,
        }).promise().then((data) => {
            if ("error" in data) {
                throw data.error;
            }
            return data;
        });
    }
    /**
     * @param {Error | MwApiError | JQueryXHRError} errorObject
     * @return {string}
     */
    function errorInfo(errorObject) {
        if (errorObject instanceof Error) {
            return `${errorObject} ${errorObject.stack.split("\n")[1].trim()}`;
        } else if (typeof errorObject.status === "number") {
            return `[${errorObject.status}] ${errorObject.statusText}`;
        }
        return `${errorObject.code} - ${errorObject.info}`;
    }

    /**
     * @param {string[]} fileNames
     * @return {Promise<object, Error>}
     */
    async function checkFileNames(fileNames) {
        const data = await commonsApi.post({
            action: "query",
            format: "json",
            titles: fileNames.map(item => `File:${item}`).join("|"),
            prop: "",
        });
        return Object.fromEntries(Object.values(data.query.pages).map((item) => [item.title.replace("File:", ""), !("missing" in item)]));
    }

    await loadScript(cdnUrl.requireJs);
    const [
        { "default": jss },
        { "default": jssPreset },
        Vue,
    ] = await loadModules([
        { jss: cdnUrl.jss },
        { jssPreset: cdnUrl.jssPreset },
        { Vue: cdnUrl.vue },
    ]);
    jss.setup(jssPreset());
    $(document.body).append('<div id="widget-fileUploader"></div>');

    const template = `
    <div id="widget-fileUploader" :class="s.container">
      <input 
        ref="fileInput"
        style="display:none"
        type="file" 
        multiple="multiple" 
        :accept="allowedFileTypes.map(item => '.' + item).join(',')"
        @change="addFileByFileSelector"
        @click="clearHint"
      />
      <div :class="s.closeBtn" @click="hideWidget">×</div>
      
      <div :class="s.body">
        <div 
          :class="s.fileList"
          @dragenter.prevent="() => {}"
          @dragover.prevent="() => {}"
          @drop.prevent="addFileByDropping"
        >
          <div 
            v-if="files.length === 0"
            key="hintMask" 
            class="hintMask" 
            @click="$refs.fileInput.click()"
          >
            <div class="hintText">点此添加文件,或将文件拖放至此</div>
          </div>

          <div 
            v-for="(item, index) in files" 
            :key="item.body.lastModified" 
            class="item"
            :data-name="item.fileName"
            :data-selected="index === focusedFileIndex"
            title="单击选中文件,双击复制文件名"
            @click="focusFile(index)"
          >
            <img 
              v-if="isImageFile(item.body)"
              :src="item.objectUrl" 
            />
            <div v-else class="unablePreviewHint">
              <div>不支持预览的文件类型</div>
              <div 
                v-if="typeof item.body !== 'string'" 
                class="type"
              >Mimetype: {{ item.body.type }}</div>
            </div>
            <div class="removeBtn" @click.stop="files.splice(index, 1)">×</div>
          </div>

          <div
            v-if="files.length !== 0"
            class="item addFileBox"
            @click="$refs.fileInput.click()"
          />
        </div>

        <div :class="s.panel">
          <div class="block">
            <div class="input-container" title="上传后使用文件时的名字,要求不能和现有文件重复">
              <span>文件名:</span>
              <input v-model.trim="form.fileName" @click="clearHint" />
            </div>

            <div class="input-container categoryInput" title="所有文件共享分类">
              <span>分<span style="visibility: hidden;">一</span>类:</span>
              <input 
                ref="categoryInput"
                v-model.trim="form.categoryInput" 
                @input="loadCategoryHint" 
                @keydown.enter="addCategory(form.categoryInput)"
                @keydown.up.prevent="handlerFor_categoryInput_wasKeyDowned"
              />
              <div class="inputHint">按下回车添加分类</div>
              <div 
                ref="categoryHints"
                v-if="categoryHints.length !== 0"
                class="categoryHints" 
                tabindex="0"
                @keydown.enter="addCategory(categoryHints[categoryHintFocusedIndex])"
                @keydown.prevent="handlerFor_categoryHints_wasKeyDowned"
              >
                <div 
                  v-for="(item, index) in categoryHints"
                  class="item"
                  :data-selected="index === categoryHintFocusedIndex"
                  @click="addCategory(item)"
                >{{ item }}</div>
              </div>
            </div>

            <div class="categories">
              <div 
                v-for="(item, index) in form.categories"
                class="item"
                title="点击删除分类"
                @click="form.categories.splice(index, 1)"
              >{{ item }}</div>
            </div>
          </div>
          
          <div class="block">
            <div class="input-container">
              <span>角色名:</span>
              <input v-model.trim="form.charaName" @click="clearHint" />
            </div>
            
            <div class="input-container">
              <span>作<span style="visibility: hidden;">一</span>者:</span>
              <input v-model.trim="form.author" @click="clearHint" />
            </div>

            <div class="input-container">
              <span>源地址:</span>
              <input v-model.trim="form.source" @click="clearHint" />
            </div>
          </div>

          <div 
            class="block"
            style="flex-direction:column; justify-content:space-around; align-items:flex-start;"
          >
            <div class="input-container" title="所有文件共享前缀">
              <span>添加前缀:</span>
              <input v-model.trim="form.prefix" style="width:calc(100% - 6em)" @click="clearHint" />
            </div>

            <div 
              class="input-container" 
              style="justify-content:flex-start;"
            >
            </div>

            <div class="buttons" @click="clearHint">
              <button @click="addSourceUrlFile">添加源地址文件</button>
              <button :disabled="status === 2" title="执行上传文件" @click="submit(false)">上传</button>
              <button 
                :disabled="status === 2" 
                title="在发生文件名已存在的情况时,自动滤掉已存在的文件。通常用于在上一次批量上传中一部分失败后,再次尝试将之前没传上去的文件重新上传" 
                @click="submit(true)"
              >差分上传</button>
              <button title="将当前文件除文件名的信息同步到全部文件" @click="asyncCurrentFileInfo">同步已输入信息至全部</button>
              <button @click="showManual">-使用说明-</button>
            </div>
          </div>
        </div>
      </div>
    </div>
    `;

    const createStyles = () => jss.createStyleSheet({
        container: {
            width: "100%",
            height: "100%",
            position: "fixed",
            top: 0,
            left: 0,
            backgroundColor: "rgba(0, 0, 0, 0.3)",
            zIndex: 100,
        },

        closeBtn: {
            fontSize: 30,
            fontWeight: "bold",
            color: "white",
            fontFamily: "Simsun",
            position: "fixed",
            top: 10,
            right: 20,
            transition: "transform 0.3s",
            zIndex: 10001,
            cursor: "pointer",

            "&:hover": {
                transform: "rotate(90deg)",
            },
        },

        body: {
            minWidth: 800,
            maxWidth: 930,
            height: 500,
            backgroundColor: "white",
            borderRadius: 10,
            border: "5px #eee solid",
            position: "absolute",
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            margin: "auto",
        },

        fileList: {
            height: "70%",
            backgroundColor: "white",
            borderRadius: "10px 10px 0 0",
            position: "relative",
            borderBottom: "3px #ccc solid",
            boxSizing: "border-box",
            overflow: "auto",
            cursor: "pointer",
            paddingBottom: 10,

            "& .hintMask": {
                position: "absolute",
                width: "100%",
                height: "100%",
                top: 0,
                left: 0,

                "&::before, &::after": {
                    content: '""',
                    width: 40,
                    height: 150,
                    backgroundColor: "#ddd",
                    position: "absolute",
                    top: 0,
                    left: 0,
                    right: 0,
                    bottom: 0,
                    margin: "auto",
                },

                "&::after": {
                    width: 150,
                    height: 40,
                },

                "& > .hintText": {
                    fontSize: 22,
                    color: "#ddd",
                    position: "absolute",
                    left: "50%",
                    transform: "translateX(-50%)",
                    bottom: 30,
                    whiteSpace: "nowrap",
                },
            },

            "& > .item": {
                width: 200,
                height: 150,
                boxSizing: "border-box",
                backgroundColor: "white",
                marginLeft: 10,
                marginTop: 10,
                border: "1px #ccc solid",
                display: "inline-block",
                position: "relative",
                cursor: "pointer",
                verticalAlign: "middle",

                "&.addFileBox": {
                    "&::before, &::after": {
                        content: '""',
                        width: 15,
                        height: 60,
                        backgroundColor: "#ddd",
                        position: "absolute",
                        top: 0,
                        left: 0,
                        bottom: 0,
                        right: 0,
                        margin: "auto",
                    },

                    "&::after": {
                        width: 60,
                        height: 15,
                    },
                },

                '&[data-selected="true"]': {
                    borderColor: "#4EBE8C",

                    "&::after": {
                        content: '""',
                        display: "block",
                        position: "absolute",
                        width: "100%",
                        height: "100%",
                        top: 0,
                        left: 0,
                        boxSizing: "border-box",
                        border: "3px #4EBE8C solid",
                        pointerEvents: "none",
                    },
                },

                "&::before": {
                    content: "attr(data-name)",
                    display: "block",
                    width: "100%",
                    position: "absolute",
                    bottom: 0,
                    left: 0,
                    backgroundColor: "rgba(0, 0, 0, 0.5)",
                    color: "white",
                    fontSize: 13,
                    textAlign: "center",
                    lineHeight: "25px",
                    overflow: "hidden",
                    height: 25,
                    textOverflow: "ellipsis",
                    whiteSpace: "nowrap",
                    boxSizing: "border-box",
                    padding: "0 10px",
                },

                "& > img": {
                    width: "100%",
                    height: "100%",
                    padding: 5,
                    boxSizing: "border-box",
                    objectFit: "scale-down",
                },

                "& > .unablePreviewHint": {
                    width: "100%",
                    height: "100%",
                    display: "flex",
                    flexDirection: "column",
                    justifyContent: "center",
                    alignItems: "center",
                    color: "#666",
                    fontSize: 13,

                    "& > .type": {
                        width: "80%",
                        overflow: "hidden",
                        textOverflow: "ellipsis",
                        whiteSpace: "nowrap",
                    },
                },

                "& > .removeBtn": {
                    width: 20,
                    height: 20,
                    borderRadius: "50%",
                    textAlign: "center",
                    lineHeight: "20px",
                    fontWeight: "bold",
                    fontFamily: "黑体",
                    position: "absolute",
                    top: 5,
                    right: 5,

                    "&:hover": {
                        backgroundColor: "#666",
                        color: "white",
                    },
                },
            },
        },

        panel: {
            height: "30%",
            padding: 10,
            boxSizing: "border-box",
            display: "flex",

            "& > .block": {
                display: "flex",
                flex: 1,
                flexWrap: "wrap",
                alignItems: "center",
                height: "100%",
                padding: "0 10px",

                "& .input-container": {
                    minWidth: 240,
                    position: "relative",

                    "& > *": {
                        verticalAlign: "middle",
                        fontSize: 14,
                    },

                    "& > input": {
                        boxSizing: "border-box",
                        width: "calc(100% - 5em)",
                        minWidth: 150,
                    },
                },
            },

            "& .categoryInput": {
                position: "relative",

                "& .inputHint": {
                    opacity: 0,
                    transition: "opacity 0.2s",
                    backgroundColor: "#fffeee",
                    border: "1px #ccc solid",
                    padding: "2px 10px",
                    position: "absolute",
                    bottom: "calc(100% - 7px)",
                    left: "calc(100% - 7px)",
                    zIndex: 1,
                    borderRadius: 5,
                    whiteSpace: "nowrap",
                },

                "& > input:focus + .inputHint": {
                    opacity: 1,
                },
            },

            "& .categoryHints": {
                minWidth: 170,
                maxHeight: 140,
                backgroundColor: "white",
                whiteSpace: "nowrap",
                overflow: "auto",
                position: "absolute",
                right: 9,
                bottom: "100%",
                border: "1px #666 solid",
                boxSizing: "border-box",
                borderBottom: "none",
                display: "flex",
                flexDirection: "column-reverse",

                "& > .item": {
                    minHeight: 20,
                    lineHeight: "20px",
                    boxSizing: "border-box",
                    padding: "0 5px",
                    width: "100%",
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                    whiteSpace: "nowrap",
                    cursor: "pointer",

                    '&[data-selected="true"]': {
                        backgroundColor: "#ccc",
                    },
                },
            },

            "& .categories": {
                width: "100%",
                height: 25,
                border: "1px #ccc solid",
                borderRadius: 5,
                overflow: "auto",
                marginRight: 5,
                boxSizing: "border-box",

                "& > .item": {
                    display: "inline-block",
                    lineHeight: "15px",
                    textAlign: "center",
                    border: "1px #666 solid",
                    backgroundColor: "#eee",
                    margin: "2px 3px",
                    padding: "0 5px",
                    fontSize: 14,
                    cursor: "pointer",
                },
            },

            "& .buttons": {
                width: "100%",

                "& > button": {
                    marginTop: 5,
                },
            },
        },
    }).attach().classes;

    new Vue({
        el: "#widget-fileUploader",
        template,

        data() {
            return {
                s: createStyles(), // 样式
                allowedFileTypes: ["ogg", "ogv", "oga", "flac", "opus", "wav", "webm", "mp3", "png", "gif", "jpg", "jpeg", "webp", "svg", "pdf", "ppt", "jp2", "doc", "docx", "xls", "xlsx", "psd", "sai", "swf", "mp4"],

                files: [], // 待上传的文件
                focusedFileIndex: 0,
                categoryHints: [],
                categoryInputDebounceTimeoutKey: 0,
                categoryHintFocusedIndex: -1,
                categoryFocused: false,
                status: 1, // 0:失败,1:初始化,2:提交中,3:成功
                form: {
                    fileName: "",
                    categoryInput: "", // 分类输入栏
                    categories: [], // 实际要提交的分类
                    charaName: "",
                    author: "",
                    source: "",
                    prefix: "",
                    license: "Copyright",
                },
                doubleClickTimeoutKey: 0, // 用于双击复制文件名 
            };
        },

        mounted() {
            $("#loading").html('<a href="javascript:void(0);" id="show">点此重新调出上传界面</a>');
            $("#show").on("click", () => {
                $("#widget-fileUploader").fadeIn(200);
                return false;
            });
        },

        watch: {
            files() {
                this.focusedFileIndex === 0 && this.focusFile(0);
            },

            form: {
                deep: true,
                handler() {
                    if (!this.files[this.focusedFileIndex]) { return; }

                    this.files[this.focusedFileIndex] = {
                        ...this.files[this.focusedFileIndex],
                        fileName: this.form.fileName,
                        author: this.form.author,
                        charaName: this.form.charaName,
                        source: this.form.source,
                        license: this.form.license,
                    };
                },
            },

            license(val) {
                if (val === "none:gotoCommons") {
                    alert("该协议需要手动填写授权证明,请到共享站进行上传");
                    window.open(`${mw.config.get("wgServer").replace("zh.moegirl", "commons.moegirl") + mw.config.get("wgScriptPath")}/Special:上传文件`, "_blank");
                }
            },
        },

        computed: {
            license() {
                return this.form.license;
            },
        },

        methods: {
            createFileItem(fileBody) {
                return {
                    body: fileBody,
                    objectUrl: typeof fileBody === "string" ? fileBody : URL.createObjectURL(fileBody),
                    fileName: typeof fileBody === "string" ? fileBody.replace(/.+\/(.+?)$/, "$1") : fileBody.name,
                    author: "",
                    charaName: "",
                    source: "",
                };
            },

            isImageFile(fileBody) {
                const imageType = ["jpg", "png", "jpeg", "gif", "webp"];
                return imageType.includes((typeof fileBody === "string" ? fileBody : fileBody.name).replace(/.+\.(.+?)$/, "$1"));
            },

            hideWidget() {
                $("#widget-fileUploader").fadeOut(200);
                $("#content").css("position", "relative");
            },

            loadCategoryHint() {
                clearTimeout(this.categoryInputDebounceTimeoutKey);
                this.categoryInputDebounceTimeoutKey = setTimeout(() => {
                    if (this.form.categoryInput === "") { return; }
                    getHints(this.form.categoryInput)
                        .then(data => {
                            const hints = data.query.search.map(item => item.title.split("Category:")[1]);
                            this.categoryHints = hints;
                        });
                }, 500);
            },

            clearHint() {
                this.categoryHints = [];
            },

            resetCategory() {
                this.form.categoryInput = "";
                this.categoryHints = [];
                this.categoryHintFocusedIndex = -1;
            },

            addCategory(categoryName) {
                if (!this.form.categories.includes(categoryName)) {
                    this.form.categories.push(categoryName);
                }
                this.resetCategory();
            },

            // 实现上下键切换分类提示
            handlerFor_categoryHints_wasKeyDowned(e) {
                if (e.code === "ArrowUp") {
                    this.categoryHintFocusedIndex++;
                    if (this.categoryHintFocusedIndex > this.categoryHints.length - 1) {
                        this.categoryHintFocusedIndex = 0;
                    }
                }

                if (e.code === "ArrowDown") {
                    this.categoryHintFocusedIndex--;
                    if (this.categoryHintFocusedIndex < 0) {
                        this.$refs.categoryInput.focus();
                    }
                }

                this.categoryHintFocusedIndex >= 0 && this.$refs.categoryHints.querySelectorAll("div")[this.categoryHintFocusedIndex].scrollIntoView();
            },

            handlerFor_categoryInput_wasKeyDowned() {
                if (this.categoryHints.length === 0 || !this.$refs.categoryHints) { return; }
                this.$refs.categoryHints.focus();
                this.categoryHintFocusedIndex = 0;
            },

            addFileByFileSelector(e) {
                Array.from(e.target.files).forEach(file => {
                    if (this.files.length === 50) { return; }

                    if (file.size / 1024 / 1024 > 20) { return alert(`文件【${file.name}】大小超过20m,无法上传!`); }
                    this.files.push(this.createFileItem(file));
                });

                e.target.value = "";
                if (this.files.length === 50) { mw.notify("一次最多上传50个文件", { type: "wran" }); }
            },

            addFileByDropping(e) {
                Array.from(e.dataTransfer.files).forEach(file => {
                    if (this.files.length === 50) { return; }

                    if (!this.allowedFileTypes.includes(file.name.replace(/.+\.(.+?)$/, "$1"))) { return alert(`【${file.name}】不支持上传这种格式的文件!`); }
                    if (file.size / 1024 / 1024 > 8) { return alert(`【${file.name}】的大小超过8m,无法上传!`); }
                    this.files.push(this.createFileItem(file));
                });

                if (this.files.length === 50) { mw.notify("一次最多上传50个文件", { type: "wran" }); }
            },

            focusFile(index) {
                this.focusedFileIndex = index;
                const file = this.files[index];
                this.form = {
                    ...this.form,
                    fileName: file.fileName,
                    author: file.author,
                    charaName: file.charaName,
                    source: file.source,
                    license: file.license,
                };

                // 实现双击复制文件名
                if (this.doubleClickTimeoutKey === 0) {
                    this.doubleClickTimeoutKey = setTimeout(() => {
                        this.doubleClickTimeoutKey = 0;
                    }, 300);
                } else {
                    mw.notify("已复制文件名");
                    this.copyFileName(this.form.prefix + file.fileName);
                    clearTimeout(this.doubleClickTimeoutKey);
                    this.doubleClickTimeoutKey = 0;
                }
            },

            addSourceUrlFile() {
                const url = (prompt("请输入文件地址:(萌娘百科、哔哩哔哩等网站图片加入了防盗链,你必须把图片下载下来后使用本地上传)") || "").trim();
                if (!url) { return; }
                this.files.push(this.createFileItem(url));
            },

            copyFileName(fileName) {
                const inputTag = document.createElement("input");
                inputTag.value = fileName;
                inputTag.style.cssText = "position: fixed; left: -9999px;";
                document.body.appendChild(inputTag);
                inputTag.focus();
                document.execCommand("selectAll");
                document.execCommand("copy");
                setTimeout(() => document.body.removeChild(inputTag), 1000);
            },

            asyncCurrentFileInfo() {
                if (!confirm("确定要将当前选中的文件信息(不含文件名)同步到所有文件中?")) { return; }
                const currentFile = this.files[this.focusedFileIndex];
                if (!currentFile) { return mw.notify("当前未选中文件"); }

                this.files.forEach(item => {
                    item.author = currentFile.author;
                    item.charaName = currentFile.charaName;
                    item.source = currentFile.source;
                    item.license = currentFile.license;
                });

                mw.notify("已同步");
            },

            showManual() {
                alert([
                    "使用说明",
                    "1. 该插件是一个批量上传图片工具,同时支持拖拽上传,批量上传。",
                    "2. 若文件上传时发生异常,请以本站的最近更改为准。",
                    "3. 每个文件拥有独立的信息,但“分类”和“添加前缀”是共享的。在需要同步每个文件的角色名、作者等信息时可以使用“同步文件信息”的功能。",
                    "4. 差分上传是指在发生文件名已存在的情况时,自动滤掉已存在的文件。通常用于在上一次批量上传中一部分失败后,再次尝试将之前没传上去的文件重新上传。",
                    "5. 双击文件可以自动复制“前缀 + 文件名”。",
                ].join("\n"));
            },

            async submit(diffMode) {
                if (this.files.length === 0) { return mw.notify("您还没有上传任何文件", { type: "warn" }); }
                if (this.files.some(item => item.fileName === "")) { return mw.notify("存在文件名为空的文件", { type: "warn" }); }

                const duplicateFilesName = new Set();
                const filesName = this.files.map(({ fileName }) => fileName);
                for (const n of filesName) {
                    if (filesName.indexOf(n) !== filesName.lastIndexOf(n)) {
                        duplicateFilesName.add(n);
                    }
                }
                if (duplicateFilesName.size > 0) {
                    return alert([
                        "这些文件名发生了重复,请不要给要上传的文件设置相同的名称:",
                        ...Array.from(duplicateFilesName.values()),
                    ].join("\n"));
                }

                const authorizedForMoegirlFiles = this.files.filter(item => item.license === "none:gotoCommons");
                if (authorizedForMoegirlFiles.length > 0) {
                    return alert([
                        "这些文件的授权协议不允许使用上传工具,请在本次上传中删除,并前往共享站填写授权信息后上传:",
                        ...authorizedForMoegirlFiles.map(item => item.fileName),
                    ].join("\n"));
                }

                if (!confirm("确定要开始上传吗?")) { return; }

                let postData = this.files.map(item => {
                    const metaCategories = `${item.charaName ? `[[分类:${item.charaName}]]` : ""}${item.author ? `[[分类:作者:${item.author}]]` : ""}`;
                    const source = item.source ? `源地址:${item.source}` : "";

                    const comment = [];
                    if (item.charaName) {
                        comment.push(`人物:[[分类:${item.charaName}|${item.charaName}]]`);
                    }
                    if (item.author) {
                        comment.push(`作者:[[分类:${item.author}|${item.author}]]`);
                    }
                    if (item.source) {
                        comment.push(`源地址:${item.source}`);
                    }
                    if (this.form.categories.length > 0) {
                        comment.push(`其他分类:[[分类:${this.form.categories.join("]]、[[分类:")}]]`);
                    }
                    const pageContent = [
                        "== 文件说明 ==",
source,
                        "基于批量上传工具上传的文件",
                        metaCategories + this.form.categories.map(item => `[[分类:${item}]]`).join(""),
                    ].join("\n");

                    return {
                        body: item.body,
                        fileName: this.form.prefix + item.fileName,
                        comment: comment.join(","),
                        pageContent,
                    };
                });

                mw.notify(`开始${diffMode ? "差分" : ""}上传,共${postData.length}个文件...`);
                console.log(`---- Moegirl:fileUploader 开始${diffMode ? "差分" : ""}上传,共${postData.length}个文件 ----`);
                this.status = 2;

                const printLogFn = (type = "info") => msg => { mw.notify(msg, { type }); console.log(msg); };
                const printLog = printLogFn();
                printLog.warn = printLogFn("warn");
                printLog.error = printLogFn("error");

                try {
                    const checkedResult = await checkFileNames(postData.map(item => item.fileName));
                    const existedFiles = postData.filter(item => checkedResult[item.fileName.replace(/^./, s => s.toUpperCase())]); // 首字母转大写,因为checkedResult返回的文件名首字母是大写 
                    if (existedFiles.length > 0 && !diffMode) {
                        alert([
                            "这些文件名已被使用,请为对应的文件更换其他名称:",
                            ...existedFiles.map(item => item.fileName),
                        ].join("\n"));
                        this.status = 1;
                        return;
                    }

                    if (diffMode) { postData = postData.filter(item => !checkedResult[item.fileName.replace(/^./, s => s.toUpperCase())]); }
                    if (diffMode && postData.length === 0) {
                        alert("差分模式下没有可以上传的文件");
                        this.status = 1;
                        return;
                    }

                    printLog.warn(`差分上传共需要上传${postData.length}个文件`);

                    let uploadResults = [];
                    if (postData.length <= 3) {
                        uploadResults = await Promise.all(
                            postData.map(item => new Promise(resolve => {
                                upload(item)
                                    .then(() => {
                                        printLog(`【${item.fileName}】上传成功`);
                                        resolve({ fileName: item.fileName, result: true });
                                    })
                                    .catch((e) => {
                                        printLog.error(`【${item.fileName}】上传失败:${errorInfo(e)}`);
                                        console.error(e);
                                        resolve({ fileName: item.fileName, result: false });
                                    });
                            })),
                        );
                    } else {
                        printLog.warn("上传文件超过3个,执行分段上传");

                        // 分段上传
                        const segmentedPostData = [[]];
                        for (const item of postData) {
                            if (segmentedPostData[segmentedPostData.length - 1].length >= 3) {
                                segmentedPostData.push([]);
                            }
                            segmentedPostData[segmentedPostData.length - 1].push(item);
                        }

                        console.log(segmentedPostData);

                        for (let i = 0, len = segmentedPostData.length; i < len; i++) {
                            printLog(`共${len}个分段,现在开始第${i + 1}个`);

                            const segment = segmentedPostData[i];
                            const segmentedUploadResult = await Promise.all(
                                segment.map(item => new Promise(resolve => {
                                    upload(item)
                                        .then(() => {
                                            printLog(`【${item.fileName}】上传成功`);
                                            resolve({ fileName: item.fileName, result: true });
                                        })
                                        .catch((e) => {
                                            printLog.error(`【${item.fileName}】上传失败:${errorInfo(e)}`);
                                            resolve({ fileName: item.fileName, result: false });
                                        });
                                })),
                            );

                            uploadResults.push(...segmentedUploadResult);
                            printLog(`第${i + 1}个分段完成,其中${segmentedUploadResult.filter(item => item.result).length}个成功,${segmentedUploadResult.filter(item => !item.result).length}个失败`);
                        }
                    }

                    const report = [
                        `全部上传结果:共计${uploadResults.length}个文件,其中${uploadResults.filter(item => item.result).length}个成功,${uploadResults.filter(item => !item.result).length}个失败`,
                        ...uploadResults.map((item, index) => `${index + 1}. 【${item.fileName}${item.result ? "成功" : "失败"}`),
                    ].join("\n");

                    console.log(report);
                    alert(report);

                    this.status = 3;
                } catch (e) {
                    console.log("上传流程出现错误", e);
                    mw.notify("网络错误,请重试", { type: "error" });
                    this.status = 0;
                }
            },
        },
    });

})());