动作方法

未匹配的标注
本文档最新版为 2.x,旧版本可能放弃维护,推荐阅读最新版!

Livewire Action 其实就是组件上的方法,可以通过前端交互(例如单击按钮或提交表单)触发。 它们为开发人员提供了能够直接从浏览器调用 PHP 方法的途径,使您能够专注于应用程序的逻辑,而不必编写服务端与客户端连接交互的重复代码。

让我们探讨一下在 “CreatePost” 组件上调用 “save” 操作的基本示例:

<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\Post;

class CreatePost extends Component
{
    public $title = '';

    public $content = '';

    public function save()
    {
        Post::create([
            'title' => $this->title,
            'content' => $this->content,
        ]);

        return redirect()->to('/posts');
    }

    public function render()
    {
        return view('livewire.create-post');
    }
}
<form wire:submit="save"> <!-- [tl! highlight] -->
    <input type="text" wire:model="title">

    <textarea wire:model="content"></textarea>

    <button type="submit">Save</button>
</form>

在上面的示例中,当用户通过单击“保存”提交表单时,wire:submit 会拦截 “submit” 事件并调用服务器上的 “save()” 操作。

从本质上讲,操作可以轻松将用户交互提交到服务器端,而无需手动提交和处理 AJAX 请求。

事件监听器

Livewire 支持各种事件监听器,使您能够响应各种类型的用户交互:

Listener Description
wire:click 点击触发
wire:submit 表单提交触发
wire:keydown 键盘按键按下触发
wire:mouseenter 当鼠标进入元素时触发
wire:* wire: 后面的任何文本都将用作监听器的事件名称

由于 wire: 后面的事件名称可以是任何名称,因此 Livewire 支持您可能需要侦听的任何浏览器事件。 例如,要监听 transitionend,您可以使用 wire:transitionend

监听特定的按键

您可以使用 Livewire 的别名指定监听的事件。

例如,要在用户在搜索框中键入内容后按“Enter”键时执行搜索,您可以使用 wire:keydown.enter

<input wire:model="query" wire:keydown.enter="searchPosts">

您可以在别名之后拼接更多的按键别名来监听按键组合。 如果您想监听 Shift 键和 Enter 键同时按下,则可以编写以下内容:

<input wire:keydown.shift.enter="...">

以下是所有可以使用的按键别名:

Modifier Key
.shift Shift
.enter Enter
.space Space
.ctrl Ctrl
.cmd Cmd
.meta Cmd on Mac, Windows key on Windows
.alt Alt
.up Up arrow
.down Down arrow
.left Left arrow
.right Right arrow
.escape Escape
.tab Tab
.caps-lock Caps Lock
.equal Equal, =
.period Period, .
.slash Forward Slash, /

事件修饰符

Livewire also includes helpful modifiers to make common event-handling tasks trivial.

For example, if you need to call event.preventDefault() from inside an event listener, you can suffix the event name with .prevent:

<input wire:keydown.prevent="...">

以下是所有可用事件监听器修饰符及其功能的完整列表:

Modifier Key
.prevent 等同于 .preventDefault()
.stop 等同于 .stopPropagation()
.window 监听 window 对象上的事件
.outside 只监听元素外部的点击
.document 监听 document 对象上的事件
.once 确保监听器仅被调用一次
.debounce 默认情况下,处理程序防抖延迟 250 毫秒
.debounce.100ms 防抖延迟指定为100毫秒
.throttle 将处理程序限制为至少每 250 毫秒调用一次
.throttle.100ms 将处理程序限制为至少每 100 毫秒调用一次
.self 仅当事件源自此元素而不是子元素时才调用侦听器
.camel 将事件名称转换为驼峰式大小写 (wire:custom-event -> “customEvent”)
.dot 将事件名称转换为点链接 (wire:custom-event -> “custom.event”)
.passive wire:touchstart.passive 不会阻止滚动性能
.capture 在“capturing”阶段监听事件

因为 wire: 在底层使用了 Alpine 的 x-on 指令,所以这些修饰符由 Alpine 提供给您。 有关何时应使用这些修饰符的更多背景信息,请参阅 Alpine Events 文档

处理第三方事件

Livewire 还支持监听第三方库触发的自定义事件。

例如,假设您在项目中使用 Trix 富文本编辑器,并且您想要监听 trix-change 事件来捕获编辑器的内容。 您可以使用 wire:trix-change 指令来完成此操作:

<form wire:submit="save">
    <!-- ... -->

    <trix-editor
        wire:trix-change="setPostContent($event.target.value)"
    ></trix-editor>

    <!-- ... -->
</form>

在此示例中,只要触发 trix-change事件,就会调用 setPostContent 操作,从而将 Livewire 组件中的conten 属性的当前值更新为 Trix 编辑器的内容。

您可以使用 $event 访问事件对象
在 Livewire 事件处理程序中,您可以通过 $event 访问事件对象。 这对于调试有关事件的信息很有用。 例如,您可以通过$event.target 访问触发事件的元素。

上面的 Trix 演示代码不完整,仅用作事件侦听器的演示。 如果逐字使用,每次按键都会触发网络请求。 更高效的实现是:

<trix-editor
   x-on:trix-change="$wire.content = $event.target.value"
></trix-editor>

监听发送的自定义事件

也可以使用 Livewire 监听从 Alpine 触发的自定义事件:

<div wire:custom-event="...">

    <!-- Deeply nested within this component: -->
    <button x-on:click="$dispatch('custom-event')">...</button>

</div>

单击示例中的按钮时,将触发 custom-event 事件并向上冒泡到 Livewire 组件的根部,其中 wire:custom-event 将捕获该事件并调用指定操作。

如果您想侦听应用程序中其他地方分派的事件,则需要等待事件冒泡到 window 对象并在那里监听。 幸运的是,Livewire 允许您向任何事件监听器添加简单的 .window 修饰符:

<div wire:custom-event.window="...">
    <!-- ... -->
</div>

<!-- Dispatched somewhere on the page outside the component: -->
<button x-on:click="$dispatch('custom-event')">...</button>

提交表单时禁用输入

还是之前的“CreatePost”示例:

<form wire:submit="save">
    <input wire:model="title">

    <textarea wire:model="content"></textarea>

    <button type="submit">Save</button>
</form>

当用户单击“save”时,网络请求将发送到服务器以调用 Livewire 组件上的“save()”操作。

但当网络高延迟时,提交表单不会有任何反应,用户可能在第一个请求没结束时再次点击保存按钮。

在这种情况下,将同时处理同一操作的两个请求。

为了防止这种情况,Livewire 在处理 wire:submit 操作时会自动禁用 <form> 元素内的提交按钮和所有表单输入。 这可确保表单不会意外提交两次。

为了进一步减少网络速度较慢的用户的困惑,显示一些加载指示器(例如微妙的背景颜色变化或 SVG 动画)通常很有帮助。

Livewire 提供了一个 wire:loading 指令,可以轻松地在页面上的任何位置显示和隐藏加载指示器。 以下是使用 wire:loading 在“保存”按钮下方显示加载消息的简短示例:

<form wire:submit="save">
    <textarea wire:model="content"></textarea>

    <button type="submit">Save</button>

    <span wire:loading>Saving...</span> <!-- [tl! highlight] -->
</form>

wire:loading is a powerful feature with a variety of more powerful features. Check out the full loading documentation for more information.

传递参数

Livewire允许您将参数从Blade模板传递到组件中的操作,使您有机会在调用操作时从前端提供操作附加数据或状态。
例如,假设您有一个允许用户删除帖子的 ShowPosts 组件。您可以将帖子的 ID 作为参数传递给 Livewire 组件中的 delete() 操作。然后,该操作可以获取相关的帖子并将其从数据库中删除:

<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Post;

class ShowPosts extends Component
{
    public function delete($id)
    {
        $post = Post::findOrFail($id);

        $this->authorize('delete', $post);

        $post->delete();
    }

    public function render()
    {
        return view('livewire.show-posts', [
            'posts' => Auth::user()->posts,
        ]);
    }
}
<div>
    @foreach ($posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="delete({{ $post->id }})">Delete</button> <!-- [tl! highlight] -->
        </div>
    @endforeach
</div>

对于ID为2的帖子,上述Blade模板中的“删除”按钮将在浏览器中呈现为:

<button wire:click="delete(2)">Delete</button>

单击此按钮时,将调用delete()方法,并将值“2”传递给$id

[!warning] 不要相信动作参数
动作参数应该像HTTP请求输入一样对待,这意味着动作参数值不应该被信任。在数据库中更新实体之前,你应该始终授权实体的所有权。

有关更多信息,请参阅我们的文档中有关安全问题和最佳实践

作为额外的方法,您可以通过向操作提供相应的模型ID作为参数来自动解析Eloquent模型。这非常类似于路由模型绑定。要开始使用,请在操作参数中键入模型类,相应的模型将自动从数据库中检索并传递给操作,而不是ID:

<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Post;

class ShowPosts extends Component
{
    public function delete(Post $post) // [tl! highlight]
    {
        $this->authorize('delete', $post);

        $post->delete();
    }

    public function render()
    {
        return view('livewire.show-posts', [
            'posts' => Auth::user()->posts,
        ]);
    }
}

依赖注入

您可以通过在方法中使用类型提示参数来利用Laravel的依赖注入系统。Livewire和Laravel将从容器中自动解析动作的依赖关系:

<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Repositories\PostRepository;

class ShowPosts extends Component
{
    public function delete(PostRepository $posts, $postId) // [tl! highlight]
    {
        $posts->deletePost($postId);
    }

    public function render()
    {
        return view('livewire.show-posts', [
            'posts' => Auth::user()->posts,
        ]);
    }
}
<div>
    @foreach ($posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="delete({{ $post->id }})">Delete</button> <!-- [tl! highlight] -->
        </div>
    @endforeach
</div>

在这个例子中,delete() 方法在接收提供的$postId 参数之前,接收一个通过 Laravel 的服务容器 解析的 PostRepository 实例。

从Alpine调用

Livewire 与 Alpine 无缝集成。事实上,每个 Livewire 组件也是一个 Alpine 组件。这意味着您可以在组件中充分利用 Alpine 来添加 JavaScript 驱动的客户端交互。
为了使这种配对更加强大,Livewire 向 Alpine 公开了一个神奇的 wire 对象,该对象可以视为 PHP 组件的 JavaScript 表示。除了通过 wire 访问和修改公共属性外,您还可以调用方法。当在 wire 对象上调用方法时,将在后端 Livewire 组件上调用相应的 PHP 方法:

<button x-on:click="$wire.save()">Save Post</button>

一个更复杂的例子,当页面上出现给定的元素时,您可以使用Alpine的x-intersect实用程序来触发一个incrementViewCount() Livewire操作:

<div x-intersect="$wire.incrementViewCount()">...</div>

传递参数

您传递给$wire方法的任何参数也将传递给PHP类方法。例如,参考以下Livewire操作:

public function addTodo($todo)
{
    $this->todos[] = $todo;
}

在组件的Blade模板中,您可以通过Alpine调用此方法,并提供该方法相应的参数:

<div x-data="{ todo: '' }">
    <input type="text" x-model="todo">

    <button x-on:click="$wire.addTodo(todo)">Add Todo</button>
</div>

如果用户在文本输入中键入了“Take out the trash”,并按下“Add Todo”按钮,则将触发addTodo()方法,其中$todo参数值为“Take out the trash”。

接收返回值

为了获得更好的体验,当网络请求正在处理时,调用的$wire操作会返回一个Promise。当收到服务器响应时,Promise将解析后端操作返回的值。
例如,参考以下Livewire 组件:

use App\Models\Post;

public function getPostCount()
{
    return Post::count();
}

使用$wire,可以调用操作并解析其返回值:

<span x-text="await $wire.getPostCount()"></span>

在这个例子中,如果“getPostCount()”方法返回“10”,则<span>标签也将显示“10”。

使用Livewire时不需要Alpine的经验;然而,它是一个非常强大的工具,了解Apline将增强您Livewire的使用体验和生产力。

Livewire’s “hybrid” JavaScript functions

Sometimes there are actions in your component that don’t need to communicate with the server and can be more efficiently written using only JavaScript.

In these cases, rather than writing the actions inside your Blade template or another file, your component action may return the JavaScript function as a string. If the action is marked with the #[Js] attribute, it will be callable from your application’s frontend:

For example:

<?php

namespace App\Livewire;

use Livewire\Attributes\Js;
use Livewire\Component;
use App\Models\Post;

class SearchPosts extends Component
{
    public $query = '';

    #[Js] // [tl! highlight:6]
    public function reset()
    {
        return <<<'JS'
            $wire.query = '';
        JS;
    }

    public function render()
    {
        return view('livewire.search-posts', [
            'posts' => Post::whereTitle($this->query)->get(),
        ]);
    }
}
<div>
    <input wire:model.live="query">

    <button wire:click="reset">Reset Search</button> <!-- [tl! highlight] -->

    @foreach ($posts as $post)
        <!-- ... -->
    @endforeach
</div>

In the above example, when the “Reset Search” button is pressed, the text input will be cleared without sending any requests to the server.

Evaluating one-off JavaScript expressions

In addition to designating entire methods to be evaluated in JavaScript, you can use the js() method to evaluate smaller, individual expressions.

This is generally useful for performing some kind of client-side follow-up after a server-side action is performed.

For example, here is an example of a CreatePost component that triggers a client-side alert dialog after the post is saved to the database:

<?php

namespace App\Livewire;

use Livewire\Component;

class CreatePost extends Component
{
    public $title = '';

    public function save()
    {
        // ...

        $this->js("alert('Post saved!')"); // [tl! highlight:6]
    }
}

The JavaScript expression alert('Post saved!') will now be executed on the client after the post has been saved to the database on the server.

Just like #[Js] methods, you can access the current component’s $wire object inside the expression.

Magic actions

Livewire provides a set of “magic” actions that allow you to perform common tasks in your components without defining custom methods. These magic actions can be used within event listeners defined in your Blade templates.

$parent

The $parent magic variable allows you to access parent component properties and call parent component actions from a child component:

<button wire:click="$parent.removePost({{ $post->id }})">Remove</button>

In the above example, if a parent component has a removePost() action, a child can call it directly from its Blade template using $parent.removePost().

$set

The $set magic action allows you to update a property in your Livewire component directly from the Blade template. To use $set, provide the property you want to update and the new value as arguments:

<button wire:click="$set('query', '')">Reset Search</button>

In this example, when the button is clicked, a network request is dispatched that sets the $query property in the component to ''.

$refresh

The $refresh action triggers a re-render of your Livewire component. This can be useful when updating the component’s view without changing any property values:

<button wire:click="$refresh">Refresh</button>

When the button is clicked, the component will re-render, allowing you to see the latest changes in the view.

$toggle

The $toggle action is used to toggle the value of a boolean property in your Livewire component:

<button wire:click="$toggle('sortAsc')">
    Sort {{ $sortAsc ? 'Descending' : 'Ascending' }}
</button>

In this example, when the button is clicked, the $sortAsc property in the component will toggle between true and false.

$dispatch

The $dispatch action allows you to dispatch a Livewire event directly in the browser. Below is an example of a button that, when clicked, will dispatch the post-deleted event:

<button type="submit" wire:click="$dispatch('post-deleted')">Delete Post</button>

$event

The $event action may be used within event listeners like wire:click. This action gives you access to the actual JavaScript event that was triggered, allowing you to reference the triggering element and other relevant information:

<input type="text" wire:keydown.enter="search($event.target.value)">

When the enter key is pressed while a user is typing in the input above, the contents of the input will be passed as a parameter to the search() action.

Using magic actions from Alpine

You can also call magic actions from Alpine using the $wire object. For example, you may use the $wire object to invoke the $refresh magic action:

<button x-on:click="$wire.$refresh()">Refresh</button>

Skipping re-renders

Sometimes there might be an action in your component with no side effects that would change the rendered Blade template when the action is invoked. If so, you can skip the render portion of Livewire’s lifecycle by adding the #[Renderless] attribute above the action method.

To demonstrate, in the ShowPost component below, the “view count” is logged when the user has scrolled to the bottom of the post:

<?php

namespace App\Livewire;

use Livewire\Attributes\Renderless;
use Livewire\Component;
use App\Models\Post;

class ShowPost extends Component
{
    public Post $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    #[Renderless] // [tl! highlight]
    public function incrementViewCount()
    {
        $this->post->incrementViewCount();
    }

    public function render()
    {
        return view('livewire.show-post');
    }
}
<div>
    <h1>{{ $post->title }}</h1>
    <p>{{ $post->content }}</p>

    <div x-intersect="$wire.incrementViewCount()"></div>
</div>

The example above uses x-intersect, an Alpine utility that calls the expression when the element enters the viewport (typically used to detect when a user scrolls to an element further down the page).

As you can see, when a user scrolls to the bottom of the post, incrementViewCount() is invoked. Since #[Renderless] was added to the action, the view is logged, but the template doesn’t re-render and no part of the page is affected.

If you prefer to not utilize method attributes or need to conditionally skip rendering, you may invoke the skipRender() method in your component action:

<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\Post;

class ShowPost extends Component
{
    public Post $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    public function incrementViewCount()
    {
        $this->post->incrementViewCount();

        $this->skipRender(); // [tl! highlight]
    }

    public function render()
    {
        return view('livewire.show-post');
    }
}

Security concerns

Remember that any public method in your Livewire component can be called from the client-side, even without an associated wire:click handler that invokes it. In these scenarios, users can still trigger the action from the browser’s DevTools.

Below are three examples of easy-to-miss vulnerabilities in Livewire components. Each will show the vulnerable component first and the secure component after. As an exercise, try spotting the vulnerabilities in the first example before viewing the solution.

If you are having difficulty spotting the vulnerabilities and that makes you concerned about your ability to keep your own applications secure, remember all these vulnerabilities apply to standard web applications that use requests and controllers. If you use a component method as a proxy for a controller method, and its parameters as a proxy for request input, you should be able to apply your existing application security knowledge to your Livewire code.

Always authorize action parameters

Just like controller request input, it’s imperative to authorize action parameters since they are arbitrary user input.

Below is a ShowPosts component where users can view all their posts on one page. They can delete any post they like using one of the post’s “Delete” buttons.

Here is a vulnerable version of component:

<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Post;

class ShowPosts extends Component
{
    public function delete($id)
    {
        $post = Post::find($id);

        $post->delete();
    }

    public function render()
    {
        return view('livewire.show-posts', [
            'posts' => Auth::user()->posts,
        ]);
    }
}
<div>
    @foreach ($posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="delete({{ $post->id }})">Delete</button>
        </div>
    @endforeach
</div>

Remember that a malicious user can call delete() directly from a JavaScript console, passing any parameters they would like to the action. This means that a user viewing one of their posts can delete another user’s post by passing the un-owned post ID to delete().

To protect against this, we need to authorize that the user owns the post about to be deleted:

<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Post;

class ShowPosts extends Component
{
    public function delete($id)
    {
        $post = Post::find($id);

        $this->authorize('delete', $post); // [tl! highlight]

        $post->delete();
    }

    public function render()
    {
        return view('livewire.show-posts', [
            'posts' => Auth::user()->posts,
        ]);
    }
}

Always authorize server-side

Like standard Laravel controllers, Livewire actions can be called by any user, even if there isn’t an affordance for invoking the action in the UI.

Consider the following BrowsePosts component where any user can view all the posts in the application, but only administrators can delete a post:

<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\Post;

class BrowsePosts extends Component
{
    public function deletePost($id)
    {
        $post = Post::find($id);

        $post->delete();
    }

    public function render()
    {
        return view('livewire.browse-posts', [
            'posts' => Post::all(),
        ]);
    }
}
<div>
    @foreach ($posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            @if (Auth::user()->isAdmin())
                <button wire:click="deletePost({{ $post->id }})">Delete</button>
            @endif
        </div>
    @endforeach
</div>

As you can see, only administrators can see the “Delete” button; however, any user can call deletePost() on the component from the browser’s DevTools.

To patch this vulnerability, we need to authorize the action on the server like so:

<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Post;

class BrowsePosts extends Component
{
    public function deletePost($id)
    {
        if (! Auth::user()->isAdmin) { // [tl! highlight:2]
            abort(403);
        }

        $post = Post::find($id);

        $post->delete();
    }

    public function render()
    {
        return view('livewire.browse-posts', [
            'posts' => Post::all(),
        ]);
    }
}

With this change, only administrators can delete a post from this component.

Keep dangerous methods protected or private

Every public method inside your Livewire component is callable from the client. Even methods you haven’t referenced inside a wire:click handler. To prevent a user from calling a method that isn’t intended to be callable client-side, you should mark them as protected or private. By doing so, you restrict the visibility of that sensitive method to the component’s class and its subclasses, ensuring they cannot be called from the client-side.

Consider the BrowsePosts example that we previously discussed, where users can view all posts in your application, but only administrators can delete posts. In the Always authorize server-side section, we made the action secure by adding server-side authorization. Now imagine we refactor the actual deletion of the post into a dedicated method like you might do in order to simplify your code:

// Warning: This snippet demonstrates what NOT to do...
<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Post;

class BrowsePosts extends Component
{
    public function deletePost($id)
    {
        if (! Auth::user()->isAdmin) {
            abort(403);
        }

        $this->delete($id); // [tl! highlight]
    }

    public function delete($postId)  // [tl! highlight:5]
    {
        $post = Post::find($postId);

        $post->delete();
    }

    public function render()
    {
        return view('livewire.browse-posts', [
            'posts' => Post::all(),
        ]);
    }
}
<div>
    @foreach ($posts as $post)
        <div wire:key="{{ $post->id }}">
            <h1>{{ $post->title }}</h1>
            <span>{{ $post->content }}</span>

            <button wire:click="deletePost({{ $post->id }})">Delete</button>
        </div>
    @endforeach
</div>

As you can see, we refactored the post deletion logic into a dedicated method named delete(). Even though this method isn’t referenced anywhere in our template, if a user gained knowledge of its existence, they would be able to call it from the browser’s DevTools because it is public.

To remedy this, we can mark the method as protected or private. Once the method is marked as protected or private, an error will be thrown if a user tries to invoke it:

<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Post;

class BrowsePosts extends Component
{
    public function deletePost($id)
    {
        if (! Auth::user()->isAdmin) {
            abort(403);
        }

        $this->delete($id);
    }

    protected function delete($postId) // [tl! highlight]
    {
        $post = Post::find($postId);

        $post->delete();
    }

    public function render()
    {
        return view('livewire.browse-posts', [
            'posts' => Post::all(),
        ]);
    }
}

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~