【问题标题】:Extending console.log without affecting log line在不影响日志行的情况下扩展 console.log
【发布时间】:2012-03-22 12:23:25
【问题描述】:

我想扩展 'console.log' 功能以向其输出添加额外信息 - 但我不想影响浏览器在控制台窗口中生成的脚本名称/行号信息。看看如果我创建自己的实现,我会得到无用的跟踪信息,我是否应该定位该代码区域...(它们都链接到日志实现,而不是导致日志消息的实际脚本)

基本上,我的应用程序是一个非常可插拔的基础架构,任何日志输出都可能出现在任意数量的帧内。 因此,我希望每条日志消息在日志消息的开头都包含一个特殊的唯一标识符。

我尝试用自己的方法替换 console.log 方法,但 chrome 抱怨 Uncaught TypeError: Illegal invocation

这就是我覆盖它的方式

var orig = console.log;
console.log = function( message )
{
    orig( (window == top ? '[root]' : '[' + window.name + ']') + ': ' + message );
}

有什么想法吗?

[编辑] 注意:修复“非法调用”问题后,文件名/行号似乎仍被覆盖“污染”...

[编辑] 看起来一般的答案是 - 不 - 尽管有些令人困惑的鹅追逐,但在当前版本的浏览器中无法实现所需的功能。

【问题讨论】:

标签: javascript google-chrome


【解决方案1】:

是的,可以在不弄乱日志调用的原始行号的情况下添加信息。这里的其他一些答案很接近,但诀窍是让您的自定义日志记录方法返回修改后的记录器。下面是一个使用上下文变体的简单示例,该示例仅经过适度测试。

log = function() {
    var context = "My Descriptive Logger Prefix:";
    return Function.prototype.bind.call(console.log, console, context);
}();

这可以用于:

log("A log message..."); 

这是一个 jsfiddle:http://jsfiddle.net/qprro98v/

人们可以轻松获得创意并传入上下文变量,并从函数定义中删除自动执行的括号。即 log("DEBUG:")("A debug message")、log("INFO:")("Here is some info") 等。

该函数唯一真正重要的部分(关于行号)是它返回记录器。

【讨论】:

  • 你也可以添加多个参数,比如Function.prototype.bind.call(console.log, console, "%ccontext: ", "font-weight:bold;");,如果你想要输出比如:“context: log text”(至少在chrome和firefox中)跨度>
  • @MarcoSacchi 如果我想以绿色记录所有消息,例如不仅前缀,我该怎么办?
【解决方案2】:

如果您的用例可以处理一些限制, 有一种方法可以使其发挥作用。限制是:

  • 额外的日志内容必须在绑定时计算;它不能是时间敏感的,也不能以任何方式依赖于传入的日志消息。

  • 额外的日志内容只能放在日志消息的开头。

有了这些限制,以下可能对您有用:

var context = "ALIASED LOG:"
var logalias;

if (console.log.bind === 'undefined') { // IE < 10
    logalias = Function.prototype.bind.call(console.log, console, context);
}
else {
    logalias = console.log.bind(console, context);
}

logalias('Hello, world!');

http://jsfiddle.net/Wk2mf/

【讨论】:

  • 这真的可以正常工作吗?您的 IE if(console.log.bind === undefined)吗?
  • 我认为你是对的,尽管我没有方便测试的 IE 9 副本。我已经更正了代码和 jsfiddle。
【解决方案3】:

一个可接受的解决方案是制作您自己的日志函数,该函数返回一个与日志参数绑定的console.log 函数。

log = function() {
    // Put your extension code here
    var args = Array.prototype.slice.call(arguments);		
    args.unshift(console);
    return Function.prototype.bind.apply(console.log, args);
}

// Note the extra () to call the original console.log
log("Foo", {bar: 1})();

这样,console.log 调用将从正确的行发出,并且会很好地显示在控制台中,允许您单击它和所有内容。

【讨论】:

  • 我觉得@kylehuff 的版本好一点,但你的版本很相似
  • 但是如果我想要条件登录怎么办? :D 没有 setter i / 调用函数我不知道如何解决(我用 setter 解决了)
  • @Mephiztopheles Kylehuff 的版本不同之处在于它不允许其他行为,例如记录到文件或将日志发送到服务器......或进行条件检查。但如果不需要额外的行为,我同意 Kylehuff 的更好。
【解决方案4】:

实际上至少在 chrome 中是可能的。这是最相关的。这可能因设置而异,而我得到拆分的方式是只记录整个堆栈,然后找到我需要的信息。

        var stack = new Error().stack;
        var file = stack.split("\n")[2].split("/")[4].split("?")[0]
        var line = stack.split("\n")[2].split(":")[5];

这就是全部内容,保留原生对象日志记录。

var orig = console.log
console.log = function(input) {
    var isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
    if(isChrome){
        var stack = new Error().stack;
        var file = stack.split("\n")[2].split("/")[4].split("?")[0]
        var line = stack.split("\n")[2].split(":")[5];
        var append = file + ":" + line;
    }
    orig.apply(console, [input, append])
}

【讨论】:

  • 这个 hack 背后的概念绝对是合理的,但是stack.split("\n")[2].split(":")[5]?你的错误到底是什么样的?第 5 个指数超出了我的范围;我很难猜测您使用的任何浏览器有什么不同,以至于与我看到的相比,一行中有多个额外的冒号。
  • 我只是通过记录整个事情并将其拆分直到我得到我需要的东西。我不确定为什么存在差异。
  • 差异是由于代码库的堆栈不同造成的。你有一个 LINE1 有一个记录命令,你想得到它的位置。从 LINE1 到新的 Error() 调用有多少函数调用很重要。调用越多,堆栈就越深。
【解决方案5】:

您需要使用正确的上下文 (console) 调用 console.log

orig.call(console, message);

要完成允许多个参数的函数:

var orig = console.log;
console.log = function() {
    var msgs = [],
        prefix = (window== top ? '[root]' : '[' + window.name + ']');
    while(arguments.length) {
        msgs.push(prefix + ': ' + [].shift.call(arguments));
    }
    orig.apply(console, msgs);
};

演示:http://jsfiddle.net/je2wR/

请记住,在使用 + 符号将对象与字符串组合时,控制台中的内置对象/数组浏览器会丢失。

【讨论】:

  • 谢谢大卫 - 不幸的是,尽管你的代码写得很好,但似乎最初的问题仍然存在。在您的 jsfiddle 中,chrome 报告说它是“第 30 行”生成日志输出(orig.apply(console, msgs);),当我需要它实际说出第 30 行时 - 好像 console.log 没有被覆盖......这是什么的症结所在我明白了,覆盖 console.log 似乎破坏了控制台堆栈跟踪。
  • 好吧,我误会了,我查看了您的代码并假设您的自定义日志功能存在编码问题。
  • 是的,我可能有点模棱两可,我认为我有一个狭隘的观点,即覆盖将解决问题。我已经对这个有一段时间了,我认为在调用“console.log”之前必须调用一些“神奇的 API”来从堆栈中弹出一个堆栈,以确保“父”文件使用 /line... 但是找不到任何文档。也许它是内部的,并且对 JS 开发人员不可用...一厢情愿向任何可能正在倾听的 Google Chrome 开发人员大喊大叫??
  • 哈——刚刚注意到我在评论中提到了“第 30 行”两次——我的意思是说 Chrome 将“第 27 行”报告为日志事件的来源,但我需要“第 30 行”来成为源...否则生成的每个日志事件都将指向日志记录脚本中的“第 27 行”。
【解决方案6】:

我刚刚在一篇帮助我回答原始“别名”问题的帖子中回答了这个问题:

(http://stackoverflow.com/a/12942764/401735)

my_log_alias = console.log.bind(console)

显然已经设计了执行此操作的能力。经过测试。有效。

此后my_log_alias与console.log相同,调用方式相同;从函数内部调用它会报告该函数调用的行号,包括别名或建议函数内部的行(如果适用)。

具体来说,Chrome 提供的行号会告诉你该行所在的文件,所以你所做的可能是不必要的;考虑将此报告为 chrome 中的错误/功能请求,它在 console.log 中提供此信息。

【讨论】:

  • 很遗憾,调用my_log_alias('test') 仍然会输出别名函数的行号,而不是所希望的原始调用者,导致所有行号都没用。
  • 真的吗?使用上述方法?是否有可能其他插件已经建议了该方法并且您正在调用别名?它是否报告了您实施此操作的行号?你是从你的别名中调用 my_log_alias,还是直接调用它?
  • 我不认为你可以创建一个小提琴来展示这个工作?仅上面的 sn-p 不足以确定我们在谈论相同的场景......
  • 说实话,我认为您的问题的答案是“不,您不能在不丢失行号的情况下将值附加到 console.log 列表。”通常,当您将日志语句添加到代码中时,也许您应该只包含对窗口的引用。
【解决方案7】:

Christopher Currie 提供了一个出色的解决方案。我已经根据我的需要对其进行了扩展。这是 AMD 模块:

define([], function () {

    var enableDebug = true;
    var separator = ">";    

    function bind(f, thisArg, ctx) {
        if (f.bind !== 'undefined') { // IE < 10
            return Function.prototype.bind.call(f, thisArg, ctx);
        }
        else {
            return f.bind(thisArg, ctx);
        }
    }

    function newConsole(context, parentConsole) {
        var log;
        var debug;
        var warn;
        var error;

        if (!parentConsole) {
            parentConsole = console;
        }

        context = context + separator;


        if (enableDebug) {
            debug = bind(console.log, console, context + "DEBUG" + separator);
        } else {
            debug = function () {
                // suppress all debug messages
            };
        }

        log = bind(console.log, console, context);

        warn = bind(console.warn, console, context);

        error = bind(console.error, console, context);

        return {
            debug: debug,
            info: log,
            log: log,
            warn: warn,
            error: error,
            /* access console context information */
            context: context,
            /* create a new console with nested context */
            nest: function (subContext) {
                return newConsole(context + subContext, this);
            },
            parent: parentConsole
        };
    }

    return newConsole("");
});

默认情况下,这将输出&gt; {message}。您还可以在日志记录中添加嵌套上下文,例如console.nest("my").log("test") 将输出&gt;my&gt; test

我还添加了一个debug 函数,它将使用&gt;DEBUG&gt; 缩进消息

希望有人会觉得它有用。

【讨论】:

    【解决方案8】:

    不久前,Chrome 推出了一项功能,无需破解代码即可解决您的问题。它被称为“黑盒”,基本上允许您使用他们的工具标记应该忽略的文件。

    https://gist.github.com/paulirish/c307a5a585ddbcc17242

    是的,此解决方案是特定于浏览器的,但如果您使用的是 Chrome,您确实需要此解决方案。

    针对每个日志抛出错误的大量解决方案可以显示正确的行,但它不会是控制台中的可点击链接。

    基于绑定/别名的解决方案只允许您修改打印的文本。您将无法将参数转发给第三个函数进行进一步处理。

    【讨论】:

    • 这个答案接近 NAA[link-only],除了它确实提到了功能的名称(黑盒)。
    • Chrome 在 Edge 的指导下决定删除黑色这个词的好用法......现在它被重命名为“框架忽略列表”......我使用 /logger\..?(j|t)s$
    【解决方案9】:

    我已经研究了好几次,总是发现这是不可能的。

    如果您有兴趣,我的解决方法是将控制台分配给另一个变量,然后将我的所有日​​志消息包装在一个函数中,该函数允许我修改/样式/消息上的任何内容。

    CoffeeScript 看起来不错,但不知道它在普通 JS 中是否实用。

    我只是养成了在所有内容前加上 x 前缀的习惯。

    logger.debug x 'Foo'
    
    log x 'Bar'
    
    log x('FooBar %o'), obj
    

    【讨论】:

      【解决方案10】:

      不幸的是,目前不可能,将来我们可能可以使用 ECMAScript 6 中的 Proxy 对象来做到这一点。

      我的用例是自动为控制台消息添加有用信息的前缀,例如传递的参数和执行方法。目前我最接近的是使用Function.prototype.apply

      一种简单的方法就是这样编写调试语句:

      console.info('=== LazyLoad.css(', arguments, '): css files are skipped, gives us a clean slate to style within theme\'s CSS.');
      

      一个复杂的方法是使用下面的辅助函数,我个人现在更喜欢简单的方法。

      /* Debug prefixing function
       * ===========================
       * 
       * A helper used to provide useful prefixing information 
       * when calling `console.log`, `console.debug`, `console.error`.
       * But the catch is that to utilize one must leverage the 
       * `.apply` function as shown in the below examples.
       *
       * ```
       * console.debug.apply(console, _fDebugPrefix(arguments)
       *    .concat('your message'));
       *
       * // or if you need to pass non strings
       * console.debug.apply(console, _fDebugPrefix(arguments)
       *    .concat('json response was:', oJson));
       *
       *
       * // if you need to use strict mode ("use strict") one can't
       * // extract the function name but following approach works very
       * // well; updating the name is just a matter of search and replace
       * var aDebugPrefix = ['fYourFunctionName('
       *                     ,Array.prototype.slice.call(arguments, 0), 
       *                     ,')'];
       * console.debug.apply(console, 
       *                     aDebugPrefix.concat(['json response was:', oJson]));
       * ```
       */
      function _fDebugPrefix(oArguments) {
          try {
              return [oArguments.callee.name + '('
                      ,Array.prototype.slice.call(oArguments, 0)
                      , ')'];
          }
          catch(err) { // are we in "use strict" mode ?
              return ['<callee.name unsupported in "use strict">('
                      ,Array.prototype.slice.call(oArguments, 0)
                      , ')'];
          }
      }
      

      【讨论】:

        【解决方案11】:

        TS/JS 中的可重用类

        // File: LogLevel.ts
        enum LogLevel {
           error = 0,
           warn,
           info,
           debug,
           verbose,
         }
        
         export default LogLevel;
        
        // File: Logger.js
        import LogLevel from "./LogLevel";
        
        export default class Logger {
          static id = "App";
          static level = LogLevel.info;
        
          constructor(id) {
            this.id = id;
        
            const commonPrefix = `[${Logger.id}/${this.id}]`;
        
            const verboseContext = `[V]${commonPrefix}`;
            if (console.log.bind === "undefined") {
              // IE < 10
              this.verbose = Function.prototype.bind.call(console.log, console, verboseContext);
            } else {
              this.verbose = console.log.bind(console, verboseContext);
            }
            if (LogLevel.verbose > Logger.level) {
              this.verbose = function() {
                return // Suppress
              };
            }
        
            const debugContext = `[D]${commonPrefix}`;
            if (console.debug.bind === "undefined") {
              // IE < 10
              this.debug = Function.prototype.bind.call(console.debug, console, debugContext);
            } else {
              this.debug = console.debug.bind(console, debugContext);
            }
            if (LogLevel.debug > Logger.level) {
              this.debug = function() {
                return // Suppress
              };
            }
        
            const infoContext = `[I]${commonPrefix}`;
            if (console.info.bind === "undefined") {
              // IE < 10
              this.info = Function.prototype.bind.call(console.info, console, infoContext);
            } else {
              this.info = console.info.bind(console, infoContext);
            }
            if (LogLevel.info > Logger.level) {
              this.info = function() {
                return // Suppress
              };
            }
        
            const warnContext = `[W]${commonPrefix}`;
            if (console.warn.bind === "undefined") {
              // IE < 10
              this.warn = Function.prototype.bind.call(console.warn, console, warnContext);
            } else {
              this.warn = console.warn.bind(console, warnContext);
            }
            if (LogLevel.warn > Logger.level) {
              this.warn = function() {
                return // Suppress
              };
            }
        
            const errorContext = `[E]${commonPrefix}`;
            if (console.error.bind === "undefined") {
              // IE < 10
              this.error = Function.prototype.bind.call(console.error, console, errorContext);
            } else {
              this.error = console.error.bind(console, errorContext);
            }
            if (LogLevel.error > Logger.level) {
              this.error = function() {
                return // Suppress
              };
            }
          }
        }
        

        用法(反应):

        // File: src/index.tsx
        
        // ...
        
        Logger.id = "MCA"
        const env = new Env()
        if (env.env == Environment.dev) {
          Logger.level = LogLevel.verbose
          const log = new Logger("Main")
          log.info("Environment is 'Development'")
        }
        
        ///...
        
        // File: src/App/CookieConsent/index.tsx
        import React, { useEffect } from "react";
        import { useCookies } from "react-cookie";
        import "./index.scss";
        
        import Logger from "@lib/Logger" // @lib is just alias configured in webpack.
        
        const cookieName = "mca-cookie-consent";
        
        // const log = new Logger(CookieConsent.name) // IMPORTANT! Don't put log instance here. It is too early! Put inside function.
        
        export default function CookieConsent(): JSX.Element {
          const log = new Logger(CookieConsent.name) // IMPORTANT! Have to be inside function, not in global scope (after imports)
        
          useEffect(() => {
            log.verbose(`Consent is accepted: ${isAccepted()}`);
          }, []);
        
          const [cookie, setCookie] = useCookies([cookieName]);
        
          function isAccepted(): boolean {
            return cookie[cookieName] != undefined;
          }
        
          function containerStyle(): React.CSSProperties {
            return isAccepted() ? { display: "none" } : {};
          }
        
          function handleClick() {
            const expires = new Date();
            expires.setFullYear(expires.getFullYear() + 1);
            log.verbose(`Accepted cookie consent. Expiration: ${expires}`)
            setCookie(cookieName, true, { path: "/", expires: expires, sameSite: "lax" });
          }
        
          return (
            <div className="cookieContainer" style={containerStyle()}>
              <div className="cookieContent">
                <div>
                  <p className="cookieText">This website uses cookies to enhance the user experience.</p>
                </div>
                <div>
                  <button onClick={handleClick} className="cookieButton">
                    I understand
                  </button>
                </div>
              </div>
            </div>
          );
        }
        

        浏览器控制台输出:

        20:47:48.190 [I][MCA/Main] Environment is 'Development' index.tsx:19
        20:47:48.286 [V][MCA/CookieConsent] Consent is accepted: false index.tsx:13
        20:47:52.250 [V][MCA/CookieConsent] Accepted cookie consent. Expiration: Sun Jan 30 2022 20:47:52 GMT+0100 (Central European Standard Time) index.tsx:29
        

        【讨论】:

          【解决方案12】:

          希望这对您的某些情况有所帮助...

          const log = console.log;
          export default function middleWare(optionalStringExtension = '') {
              console.log = (...args) => {
                  log(...args, optionalStringExtension);
              }
          }
          

          作为中间件、文件顶部或函数的第一行运行。

          【讨论】:

            【解决方案13】:

            我也遇到了关于扩展 console.log() 的问题,以便应用程序可以扩展、控制和做一些花哨的事情,除了将内容记录到控制台。然而,丢失行号信息无异于失败​​。在解决了这个问题之后,我想出了一个冗长的解决方法,但至少它仍然是一个“1-liner”可以使用。

            首先,定义一个全局类以使用或添加一些方法到您现有的主要“app”类:

            /**
             * Log message to our in-app and possibly on-screen console, return args.
             * @param {!string} aMsgLevel - one of "log", "debug", "info", "warn", or "error"
             * @param {any} aArgs - the arguments to log (not used directly, just documentation helper)
             * @returns args so it can be nested within a console.log.apply(console,app.log()) statement.
             */
            MyGlobalClassWithLogMethods.prototype.debugLog = function(aMsgLevel, aArgs) {
                var s = '';
                var args = [];
                for (var i=1; i<arguments.length; i++) {
                    args.push(arguments[i]);
                    if (arguments[i])
                        s += arguments[i].toString()+' ';
                }
                if (typeof this.mLog === 'undefined')
                    this.mLog = [];
                this.mLog.push({level: aMsgLevel, msg: s});
                return args;
            };
            
            MyGlobalClassWithLogMethods.prototype.log = function() {
                var args = ['log'].concat(Array.prototype.slice.call(arguments));
                return this.debugLog.apply(this,args);
            };
            
            MyGlobalClassWithLogMethods.prototype.debug = function() {
                var args = ['debug'].concat(Array.prototype.slice.call(arguments));
                return this.debugLog.apply(this,args);
            };
            
            MyGlobalClassWithLogMethods.prototype.info = function() {
                var args = ['info'].concat(Array.prototype.slice.call(arguments));
                return this.debugLog.apply(this,args);
            };
            
            MyGlobalClassWithLogMethods.prototype.warn = function() {
                var args = ['warn'].concat(Array.prototype.slice.call(arguments));
                return this.debugLog.apply(this,args);
            };
            
            MyGlobalClassWithLogMethods.prototype.error = function() {
                var args = ['error'].concat(Array.prototype.slice.call(arguments));
                return this.debugLog.apply(this,args);
            };
            
            //not necessary, but it is used in my example code, so defining it
            MyGlobalClassWithLogMethods.prototype.toString = function() {
                return "app: " + JSON.stringify(this);
            };
            

            接下来,我们像这样使用这些方法:

            //JS line done as early as possible so rest of app can use logging mechanism
            window.app = new MyGlobalClassWithLogMethods();
            
            //only way to get "line info" reliably as well as log the msg for actual page display;
            //  ugly, but works. Any number of params accepted, and any kind of var will get
            //  converted to str using .toString() method.
            console.log.apply(console,app.log('the log msg'));
            console.debug.apply(console,app.debug('the log msg','(debug)', app));
            console.info.apply(console,app.info('the log msg','(info)'));
            console.warn.apply(console,app.warn('the log msg','(warn)'));
            console.error.apply(console,app.error('the log msg','(error)'));
            

            现在控制台获取带有相应行信息的日志消息,并且我们的应用程序包含一组可以使用的日志消息。例如,要使用 HTML、JQuery 和一些 CSS 显示您的应用内日志,可以使用以下简单示例。

            首先,HTML:

            <div id="debug_area">
                <h4 class="text-center">Debug Log</h4>
                <ul id="log_list">
                    <!-- console log/debug/info/warn/error ('msg') lines will go here -->
                </ul>
            </div>
            

            一些CSS:

            .log_level_log {
                color: black;
                background-color: white;
                font-size: x-small;
            }
            .log_level_debug {
                color: #060;
                background-color: #80FF80;
                font-size: x-small;
            }
            .log_level_info {
                color: #00F;
                background-color: #BEF;
                font-size: x-small;
            }
            .log_level_warn {
                color: #E65C00;
                background-color: #FB8;
                font-size: x-small;
            }
            .log_level_error {
                color: #F00;
                background-color: #FBB;
                font-size: x-small;
            }
            

            还有一些 JQuery:

            var theLog = app.mLog || [];
            if (theLog.length>0) {
                var theLogList = $('#log_list');
                theLogList.empty();
                for (var i=0; i<theLog.length; i++) {
                    theLogList.prepend($('<li class="log_level_'+theLog[i].level+'"></li>').text(theLog[i].msg));
                }
            }
            

            这是一个简单的用法,但是一旦你有了适当的机制,你可以做任何你能想到的事情,包括在代码中保留日志行,但设置一个阈值,以便只有警告和错误能够通过.希望这有助于其他人的项目。

            【讨论】:

              【解决方案14】:

              今天您必须将argsrest operator 一起使用,因为正如Mozilla 文档所说,Function.arguments 已被弃用并且无法在箭头函数中访问。因此,您可以像下面这样简单地扩展它:

              //#1
              const myLog= (...args) =>
                console.log.bind(console, ...args);
              //myLog("this is my new log")();
              
              //#2
              const myNewLog= (...args) =>{
               const prefix = "Prefixed: ";
               return console.log.bind(console, ...[prefix,...args]);
              }
              //myNewLog("test")()
              

              您可以像这样创建beautifulLog

              //#3
              const colorizedLog = (text, color= "#40a7e3", ...args) =>
                console.log.bind(
                  console,
                  `%c ${text}`,
                  `font-weight:bold; color:${color}`,
                  ...args
                );
              //colorizedLog("Title:", "#40a7e3", "This is a working example")();
              

              【讨论】:

                【解决方案15】:

                此 sn-p 将前缀应用于所有级别的日志 (console.log console.debug console.info ...):

                export const makeConsole = (context: string, cons = console): Console =>
                  Object.getOwnPropertyNames(cons).reduce((c, lev) => {
                    if (typeof cons[lev] === "function") {
                      c[lev] = Function.prototype.bind.call(cons[lev], cons, context);
                    }
                    return c;
                  }, {});
                
                
                console.debug("Hello world!")
                // >> Hello world!
                
                console = makeConsole("[logging is fun]")
                // >> [logging is fun] Hello world!
                

                奖励,对于 React 窥视:

                export function useConsole(context: string): Console {
                  return React.useMemo(() => makeConsole(context), [context]);
                }
                

                【讨论】:

                  【解决方案16】:

                  试试setTimeout(console.log.bind(console,'foo'));

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2020-01-14
                    • 1970-01-01
                    • 2011-11-29
                    • 1970-01-01
                    • 1970-01-01
                    • 2018-12-24
                    • 2019-07-16
                    • 1970-01-01
                    相关资源
                    最近更新 更多