首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >访问https链接时如何通过JS调用wpf函数

访问https链接时如何通过JS调用wpf函数
EN

Stack Overflow用户
提问于 2020-02-25 09:28:16
回答 1查看 954关注 0票数 2

我需要执行wpf项目中定义的函数,该函数是从https网页中的JS调用的。所有代码的演示项目如下:https://github.com/tomxue/WebViewIssueInWpf

JS部件:网页链接是https://cmsdev.lenovo.com.cn/musichtml/leHome/weather/index.html?date=&city=&mark=0&speakerId=&reply=

它包含以下一行:

代码语言:javascript
复制
<script src="js/index.js" type="text/javascript" charset="utf-8"></script>

js/index.js包含以下代码:

代码语言:javascript
复制
setTitle(dataObject.city + weekDay(dataObject.date) +"天气" )

setTitle()的定义如下:window.external.notify()的使用方法

代码语言:javascript
复制
    function setTitle(_str){
        try{
            wtjs.setTitle(_str)
        }catch(e){
            console.log(_str)
            window.external.notify(_str);
        }
    }

函数window.external.notify()将通过ScriptNotify()调用wpf函数。

WPF部分:用于wpf项目中的WebView

代码语言:javascript
复制
        this.wv.IsScriptNotifyAllowed = true;
        this.wv.ScriptNotify += Wv_ScriptNotify;

代码语言:javascript
复制
    private void Wv_ScriptNotify(object sender, Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT.WebViewControlScriptNotifyEventArgs e)
    {
        textBlock.Text = e.Value;
    }

问题:

(1)这里的问题是,如果网页使用https://,那么wpf中的上述函数Wv_ScriptNotify()就不会被触发。但是如果网页链接使用http://,则可以触发wpf中的上述函数Wv_ScriptNotify()。为什么以及如何解决这个问题?

更新: 2020-3-2 17:25:55,刚刚测试过,https有效。我不知道是什么原因导致https以前不起作用。

(2) web页面中的 JS使用一个对象wtjs (由我们自己定义,并很好地使用JSBridge处理项目)。我想使用一个与UWP类似的方法,使用一个桥,这样我就可以添加多个函数/接口供JS调用。ScriptNotify()的缺点是只有一个接口是可用的。为了实现这个目标,我编写了下面的代码,现在注释掉了。

代码语言:javascript
复制
wv.RegisterName("wtjs", new myBridge());

下面定义了更多的函数

代码语言:javascript
复制
    public class myBridge
    {
        public void SetTitle(string title)
        {
            Debug.WriteLine("SetTitle is executing...title = {0}", title);
        }

        public void PlayTTS(string tts)
        {
            Debug.WriteLine("PlayTTS is executing...tts = {0}", tts);
        }
    }

在JS端,将调用相应的函数。

代码语言:javascript
复制
wtjs.playTTS(tts)
wtjs.setTitle(_str)

但是实际上wpf方面没有工作,而使用JSBridge的UWP项目使用了web链接(所以网页和JS脚本是可行的)。如何实现这一目标?

(3)

以上两个问题已经用DK Dhilip的答案解决了。但是发现了一个新的问题。请检查我的GitHub代码,并将其更新为最新提交。https://github.com/tomxue/WebViewIssueInWpf

我将一个TextBlock放到WebView上,希望看到文本在web内容上浮动。但事实上,文本是由WebView覆盖的。为什么以及如何解决这个问题?谢谢!

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2020-02-29 22:16:10

问题(1,2)

HTTPS链接对我来说很好,也许页面加载太慢了?

根据微软(来源)的说法,WebView只支持ScriptNotify

我可以将本机对象插入到WebViewControl内容中吗? 不是的。WebBrower (Internet ) ObjectForScripting属性和WebView (UWP) AddWebAllowedObject方法在WebViewControl中都不受支持。作为一种解决办法,您可以使用window.Foreignal.Notification/ ScriptNotify和JavaScript执行来在层之间进行通信,例如:AddAllowedWebObjectWorkaround

但上述建议的解决方案似乎与您的预期不同,因此我只是实现了自己的解决方案,以模仿您所期望的JSBridge约定。

我的自定义解决方案不是经过战斗测试的,它在某些边缘情况下可能会中断,但在几个简单的测试中似乎很好。

支持的内容:

  • 多桥对象
  • JS到C#方法调用
  • JS到C# get/set属性

C#用法:

代码语言:javascript
复制
// Add
webView.AddWebAllowedObject("wtjs", new MyBridge(this));
webView.AddWebAllowedObject("myBridge", new MyOtherBridge());

// Remove
webView.RemoveWebAllowedObject("wtjs");

JS使用:

代码语言:javascript
复制
// Call C# object method (no return value)
wtjs.hello('hello', 'world', 666);
myBridge.saySomething('天猫精灵,叫爸爸!');

// Call C# object method (return value)
wtjs.add(10, 20).then(function (result) { console.log(result); });

// Get C# object property
wtjs.backgroundColor.then(function (color) { console.log(color); });

// Set C# object property
wtjs.niubility = true;

代码

WebViewExtensions.cs

代码语言:javascript
复制
using Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT;
using Microsoft.Toolkit.Wpf.UI.Controls;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Text;

namespace WpfApp3
{
    // Source: https://github.com/dotnet/orleans/issues/1269#issuecomment-171233788
    public static class JsonHelper
    {
        private static readonly Type[] _specialNumericTypes = { typeof(ulong), typeof(uint), typeof(ushort), typeof(sbyte) };

        public static object ConvertWeaklyTypedValue(object value, Type targetType)
        {
            if (targetType == null)
                throw new ArgumentNullException(nameof(targetType));

            if (value == null)
                return null;

            if (targetType.IsInstanceOfType(value))
                return value;

            var paramType = Nullable.GetUnderlyingType(targetType) ?? targetType;

            if (paramType.IsEnum)
            {
                if (value is string)
                    return Enum.Parse(paramType, (string)value);
                else
                    return Enum.ToObject(paramType, value);
            }

            if (paramType == typeof(Guid))
            {
                return Guid.Parse((string)value);
            }

            if (_specialNumericTypes.Contains(paramType))
            {
                if (value is BigInteger)
                    return (ulong)(BigInteger)value;
                else
                    return Convert.ChangeType(value, paramType);
            }

            if (value is long || value is double)
            {
                return Convert.ChangeType(value, paramType);
            }

            return value;
        }
    }

    public enum WebViewInteropType
    {
        Notify = 0,
        InvokeMethod = 1,
        InvokeMethodWithReturn = 2,
        GetProperty = 3,
        SetProperty = 4
    }

    public class WebAllowedObject
    {
        public WebAllowedObject(WebView webview, string name)
        {
            WebView = webview;
            Name = name;
        }

        public WebView WebView { get; private set; }

        public string Name { get; private set; }

        public ConcurrentDictionary<(string, WebViewInteropType), object> FeaturesMap { get; } = new ConcurrentDictionary<(string, WebViewInteropType), object>();

        public EventHandler<WebViewControlNavigationCompletedEventArgs> NavigationCompletedHandler { get; set; }

        public EventHandler<WebViewControlScriptNotifyEventArgs> ScriptNotifyHandler { get; set; }
    }

    public static class WebViewExtensions
    {
        public static bool IsNotification(this WebViewControlScriptNotifyEventArgs e)
        {
            try
            {
                var message = JsonConvert.DeserializeObject<dynamic>(e.Value);

                if (message["___magic___"] != null)
                {
                    return false;
                }
            }
            catch (Exception) { }

            return true;
        }

        public static void AddWebAllowedObject(this WebView webview, string name, object targetObject)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentNullException(nameof(name));

            if (targetObject == null)
                throw new ArgumentNullException(nameof(targetObject));

            if (webview.Tag == null)
            {
                webview.Tag = new ConcurrentDictionary<string, WebAllowedObject>();
            }
            else if (!(webview.Tag is ConcurrentDictionary<string, WebAllowedObject>))
            {
                throw new InvalidOperationException("WebView.Tag property is already being used for other purpose.");
            }

            var webAllowedObjectsMap = webview.Tag as ConcurrentDictionary<string, WebAllowedObject>;

            var webAllowedObject = new WebAllowedObject(webview, name);

            if (webAllowedObjectsMap.TryAdd(name, webAllowedObject))
            {
                var objectType = targetObject.GetType();
                var methods = objectType.GetMethods();
                var properties = objectType.GetProperties();

                var jsStringBuilder = new StringBuilder();

                jsStringBuilder.Append("(function () {");
                jsStringBuilder.Append("window['");
                jsStringBuilder.Append(name);
                jsStringBuilder.Append("'] = {");

                jsStringBuilder.Append("__callback: {},");
                jsStringBuilder.Append("__newUuid: function () { return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function (c) { return (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16); }); },");

                foreach (var method in methods)
                {
                    if (!method.IsSpecialName)
                    {
                        if (method.ReturnType == typeof(void))
                        {
                            webAllowedObject.FeaturesMap.TryAdd((method.Name, WebViewInteropType.InvokeMethod), method);
                        }
                        else
                        {
                            webAllowedObject.FeaturesMap.TryAdd((method.Name, WebViewInteropType.InvokeMethodWithReturn), method);
                        }

                        var parameters = method.GetParameters();
                        var parametersInString = string.Join(",", parameters.Select(x => x.Position).Select(x => "$$" + x.ToString()));

                        jsStringBuilder.Append(method.Name);
                        jsStringBuilder.Append(": function (");
                        jsStringBuilder.Append(parametersInString);
                        jsStringBuilder.Append(") {");

                        if (method.ReturnType != typeof(void))
                        {
                            jsStringBuilder.Append("var callbackId = window['" + name + "'].__newUuid();");
                        }

                        jsStringBuilder.Append("window.external.notify(JSON.stringify({");
                        jsStringBuilder.Append("source: '");
                        jsStringBuilder.Append(name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("target: '");
                        jsStringBuilder.Append(method.Name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("parameters: [");
                        jsStringBuilder.Append(parametersInString);
                        jsStringBuilder.Append("]");

                        if (method.ReturnType != typeof(void))
                        {
                            jsStringBuilder.Append(",");
                            jsStringBuilder.Append("callbackId: callbackId");
                        }

                        jsStringBuilder.Append("}), ");
                        jsStringBuilder.Append((method.ReturnType == typeof(void)) ? (int)WebViewInteropType.InvokeMethod : (int)WebViewInteropType.InvokeMethodWithReturn);
                        jsStringBuilder.Append(");");


                        if (method.ReturnType != typeof(void))
                        {
                            jsStringBuilder.Append("var promise = new Promise(function (resolve, reject) {");
                            jsStringBuilder.Append("window['" + name + "'].__callback[callbackId] = { resolve, reject };");
                            jsStringBuilder.Append("});");

                            jsStringBuilder.Append("return promise;");
                        }

                        jsStringBuilder.Append("},");
                    }
                }

                jsStringBuilder.Append("};");

                foreach (var property in properties)
                {
                    jsStringBuilder.Append("Object.defineProperty(");
                    jsStringBuilder.Append("window['");
                    jsStringBuilder.Append(name);
                    jsStringBuilder.Append("'], '");
                    jsStringBuilder.Append(property.Name);
                    jsStringBuilder.Append("', {");

                    if (property.CanRead)
                    {
                        webAllowedObject.FeaturesMap.TryAdd((property.Name, WebViewInteropType.GetProperty), property);

                        jsStringBuilder.Append("get: function () {");
                        jsStringBuilder.Append("var callbackId = window['" + name + "'].__newUuid();");
                        jsStringBuilder.Append("window.external.notify(JSON.stringify({");
                        jsStringBuilder.Append("source: '");
                        jsStringBuilder.Append(name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("target: '");
                        jsStringBuilder.Append(property.Name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("callbackId: callbackId,");
                        jsStringBuilder.Append("parameters: []");
                        jsStringBuilder.Append("}), ");
                        jsStringBuilder.Append((int)WebViewInteropType.GetProperty);
                        jsStringBuilder.Append(");");

                        jsStringBuilder.Append("var promise = new Promise(function (resolve, reject) {");
                        jsStringBuilder.Append("window['" + name + "'].__callback[callbackId] = { resolve, reject };");
                        jsStringBuilder.Append("});");

                        jsStringBuilder.Append("return promise;");

                        jsStringBuilder.Append("},");
                    }

                    if (property.CanWrite)
                    {
                        webAllowedObject.FeaturesMap.TryAdd((property.Name, WebViewInteropType.SetProperty), property);

                        jsStringBuilder.Append("set: function ($$v) {");
                        jsStringBuilder.Append("window.external.notify(JSON.stringify({");
                        jsStringBuilder.Append("source: '");
                        jsStringBuilder.Append(name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("target: '");
                        jsStringBuilder.Append(property.Name);
                        jsStringBuilder.Append("',");
                        jsStringBuilder.Append("parameters: [$$v]");
                        jsStringBuilder.Append("}), ");
                        jsStringBuilder.Append((int)WebViewInteropType.SetProperty);
                        jsStringBuilder.Append(");");
                        jsStringBuilder.Append("},");
                    }

                    jsStringBuilder.Append("});");
                }

                jsStringBuilder.Append("})();");

                var jsString = jsStringBuilder.ToString();

                webAllowedObject.NavigationCompletedHandler = (sender, e) =>
                {
                    var isExternalObjectCustomized = webview.InvokeScript("eval", new string[] { "window.external.hasOwnProperty('isCustomized').toString();" }).Equals("true");

                    if (!isExternalObjectCustomized)
                    {
                        webview.InvokeScript("eval", new string[] { @"
                            (function () {
                                var originalExternal = window.external;
                                var customExternal = {
                                    notify: function (message, type = 0) {
                                        if (type === 0) {
                                            originalExternal.notify(message);
                                        } else {
                                            originalExternal.notify(JSON.stringify({
                                                ___magic___: true,
                                                type: type,
                                                interop: message
                                            }));
                                        }
                                    },
                                    isCustomized: true
                                };
                                window.external = customExternal;
                            })();" });
                    }

                    webview.InvokeScript("eval", new string[] { jsString });
                };

                webAllowedObject.ScriptNotifyHandler = (sender, e) =>
                {
                    try
                    {
                        var message = JsonConvert.DeserializeObject<dynamic>(e.Value);

                        if (message["___magic___"] != null)
                        {
                            var interopType = (WebViewInteropType)message.type;
                            var interop = JsonConvert.DeserializeObject<dynamic>(message.interop.ToString());
                            var source = (string)interop.source.ToString();
                            var target = (string)interop.target.ToString();
                            var parameters = (object[])interop.parameters.ToObject<object[]>();

                            if (interopType == WebViewInteropType.InvokeMethod)
                            {
                                if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
                                {
                                    if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object methodObject))
                                    {
                                        var method = (MethodInfo)methodObject;

                                        var parameterTypes = method.GetParameters().Select(x => x.ParameterType).ToArray();

                                        var convertedParameters = new object[parameters.Length];

                                        for (var i = 0; i < parameters.Length; i++)
                                        {
                                            convertedParameters[i] = JsonHelper.ConvertWeaklyTypedValue(parameters[i], parameterTypes[i]);
                                        }

                                        method.Invoke(targetObject, convertedParameters);
                                    }
                                }
                            }
                            else if (interopType == WebViewInteropType.InvokeMethodWithReturn)
                            {
                                var callbackId = interop.callbackId.ToString();

                                if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
                                {
                                    if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object methodObject))
                                    {
                                        var method = (MethodInfo)methodObject;

                                        var parameterTypes = method.GetParameters().Select(x => x.ParameterType).ToArray();

                                        var convertedParameters = new object[parameters.Length];

                                        for (var i = 0; i < parameters.Length; i++)
                                        {
                                            convertedParameters[i] = JsonHelper.ConvertWeaklyTypedValue(parameters[i], parameterTypes[i]);
                                        }

                                        var invokeResult = method.Invoke(targetObject, convertedParameters);

                                        webview.InvokeScript("eval", new string[] { string.Format("window['{0}'].__callback['{1}'].resolve({2}); delete window['{0}'].__callback['{1}'];", source, callbackId, JsonConvert.SerializeObject(invokeResult)) });
                                    }
                                }
                            }
                            else if (interopType == WebViewInteropType.GetProperty)
                            {
                                var callbackId = interop.callbackId.ToString();

                                if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
                                {
                                    if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object propertyObject))
                                    {
                                        var property = (PropertyInfo)propertyObject;

                                        var getResult = property.GetValue(targetObject);

                                        webview.InvokeScript("eval", new string[] { string.Format("window['{0}'].__callback['{1}'].resolve({2}); delete window['{0}'].__callback['{1}'];", source, callbackId, JsonConvert.SerializeObject(getResult)) });
                                    }
                                }
                            }
                            else if (interopType == WebViewInteropType.SetProperty)
                            {
                                if (webAllowedObjectsMap.TryGetValue(source, out WebAllowedObject storedWebAllowedObject))
                                {
                                    if (storedWebAllowedObject.FeaturesMap.TryGetValue((target, interopType), out object propertyObject))
                                    {
                                        var property = (PropertyInfo)propertyObject;

                                        property.SetValue(targetObject, JsonHelper.ConvertWeaklyTypedValue(parameters[0], property.PropertyType));
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        // Do nothing
                    }
                };

                webview.NavigationCompleted += webAllowedObject.NavigationCompletedHandler;
                webview.ScriptNotify += webAllowedObject.ScriptNotifyHandler;
            }
            else
            {
                throw new InvalidOperationException("Object with the identical name is already exist.");
            }
        }

        public static void RemoveWebAllowedObject(this WebView webview, string name)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentNullException(nameof(name));

            var allowedWebObjectsMap = webview.Tag as ConcurrentDictionary<string, WebAllowedObject>;

            if (allowedWebObjectsMap != null)
            {
                if (allowedWebObjectsMap.TryRemove(name, out WebAllowedObject webAllowedObject))
                {
                    webview.NavigationCompleted -= webAllowedObject.NavigationCompletedHandler;
                    webview.ScriptNotify -= webAllowedObject.ScriptNotifyHandler;

                    webview.InvokeScript("eval", new string[] { "delete window['" + name + "'];" });
                }
            }
        }
    }
}

MainWindow.xaml.cs

代码语言:javascript
复制
using Microsoft.Toolkit.Win32.UI.Controls.Interop.WinRT;
using System;
using System.Diagnostics;
using System.Windows;

namespace WpfApp3
{
    public partial class MainWindow : Window
    {
        public class MyBridge
        {
            private readonly MainWindow _window;

            public MyBridge(MainWindow window)
            {
                _window = window;
            }

            public void setTitle(string title)
            {
                Debug.WriteLine(string.Format("SetTitle is executing...title = {0}", title));

                _window.setTitle(title);
            }

            public void playTTS(string tts)
            {
                Debug.WriteLine(string.Format("PlayTTS is executing...tts = {0}", tts));
            }
        }

        public MainWindow()
        {
            this.InitializeComponent();

            this.wv.IsScriptNotifyAllowed = true;
            this.wv.ScriptNotify += Wv_ScriptNotify;
            this.wv.AddWebAllowedObject("wtjs", new MyBridge(this));

            this.Loaded += MainPage_Loaded;
        }

        private void Wv_ScriptNotify(object sender, WebViewControlScriptNotifyEventArgs e)
        {
            if (e.IsNotification())
            {
                Debug.WriteLine(e.Value);
            }
        }

        private void setTitle(string str)
        {
            textBlock.Text = str;
        }

        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            this.wv.Source = new Uri("https://cmsdev.lenovo.com.cn/musichtml/leHome/weather/index.html?date=&city=&mark=0&speakerId=&reply=");
        }
    }
}

结果

截图:

问题(3)

根据(123.),不可能在WebView/WebBrowser控件之上覆盖UI元素。

幸运的是,有一个名为CefSharp的替代解决方案,它基于Chromium浏览器,对于您的用例来说已经足够好了,再加上背景动画(在原始的WebView控件中不起作用)。

但是,没有完美的解决方案;WPF设计视图在CefSharp中无法使用(显示无效标记错误),但程序只需编译和运行。此外,该项目只能使用x86x64选项构建,AnyCPU将无法工作。

MainWindow.xaml

代码语言:javascript
复制
<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:cefSharp="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf" 
        x:Class="WpfApp3.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid x:Name="grid">
        <cefSharp:ChromiumWebBrowser x:Name="wv" HorizontalAlignment="Left" Height="405" Margin="50,0,0,0" VerticalAlignment="Top" Width="725" RenderTransformOrigin="-0.45,-0.75" />
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="30,30,0,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top" Height="60" Width="335"/>
    </Grid>
</Window>

MainWindow.xaml.cs

代码语言:javascript
复制
using CefSharp;
using System.Diagnostics;
using System.Windows;

namespace WpfApp3
{
    public partial class MainWindow : Window
    {
        public class MyBridge
        {
            private readonly MainWindow _window;

            public MyBridge(MainWindow window)
            {
                _window = window;
            }

            public void setTitle(string title)
            {
                Debug.WriteLine(string.Format("SetTitle is executing...title = {0}", title));

                _window.setTitle(title);
            }

            public void playTTS(string tts)
            {
                Debug.WriteLine(string.Format("PlayTTS is executing...tts = {0}", tts));
            }
        }

        public MainWindow()
        {
            this.InitializeComponent();

            this.wv.JavascriptObjectRepository.Register("wtjs", new MyBridge(this), true, new BindingOptions() { CamelCaseJavascriptNames = false });
            this.wv.FrameLoadStart += Wv_FrameLoadStart;

            this.Loaded += MainPage_Loaded;
        }

        private void Wv_FrameLoadStart(object sender, FrameLoadStartEventArgs e)
        {
            if (e.Url.StartsWith("https://cmsdev.lenovo.com.cn/musichtml/leHome/weather"))
            {
                e.Browser.MainFrame.ExecuteJavaScriptAsync("CefSharp.BindObjectAsync('wtjs');");
            }
        }

        private void setTitle(string str)
        {
            this.Dispatcher.Invoke(() =>
            {
                textBlock.Text = str;
            });
        }

        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            this.wv.Address = "https://cmsdev.lenovo.com.cn/musichtml/leHome/weather/index.html?date=&city=&mark=0&speakerId=&reply=";
        }
    }
}

截图:

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/60391301

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档