主站
论坛首页/ LiteLog /帖子

LiteLog 小工具(Widget)开发文档

作者:zhangwei 管理员 · 10 天前 · 9 浏览 · 0 回复
zhangwei
楼主
管理员
1 楼

LiteLog 小工具(Widget)开发文档

版本:1.0 适用范围:LiteLog 内核 ≥ 1.0 更新日期:2026-10-08

目录

  1. 系统概述
  2. 核心概念
  3. 目录结构
  4. Widget 基类 API
  5. 快速开始:第一个小工具
  6. 表单字段规范
  7. 条件显示
  8. 多实例支持
  9. 样式规范
  10. 完整示例
  11. 高级话题
  12. 常见问题

1. 系统概述

LiteLog 小工具(Widget)系统仿照 WordPress 小工具概念,允许用户从后台通过拖拽方式,把可复用的功能块添加到网站的侧栏区域。

核心特性:

  • ✅ 注册即用:定义类 + 一行注册代码,后台自动出现
  • ✅ 多实例:同一小工具可在同一区域重复添加,各自独立配置
  • ✅ 拖拽排序:后台拖拽调整顺序
  • ✅ 配置面板:每个实例独立配置参数
  • ✅ 条件显示:根据其他配置项动态显示/隐藏表单字段
  • ✅ 区域化:支持多个区域(左栏、右栏),可扩展
  • ✅ 零污染:样式与数据完全隔离,不影响主题与其他插件

2. 核心概念

概念说明
Widget一个小工具实例的类定义,继承 Widget 基类
WidgetRegistry全局注册表,收集所有已注册的小工具
WidgetArea小工具区域(如 left_sidebar、right_sidebar)
Instance用户拖拽到区域中的一个小工具"副本",拥有独立 ID 与配置
Settings某个实例的配置数据(键值对数组)

数据流

插件加载
  → 定义 Widget 类
  → Hook::addAction('register_widgets', 回调)
  → WidgetRegistry::init() 触发钩子
  → 注册 Widget 到注册表

后台小工具页
  → WidgetRegistry::all() 获取全部可用小工具
  → 用户拖拽到区域 → 保存到 Option(JSON)

前台渲染
  → WidgetArea::render($area)
  → 读取该区域的实例列表
  → 逐个调用 widget->render($settings, $instanceId)

3. 目录结构

推荐在插件的独立文件中定义 Widget:

content/plugins/your-plugin/
├── your-plugin.php              # 插件主文件
├── widget_your.php              # 小工具定义(推荐)
└── assets/
    └── your-widget.css          # 可选:外部样式

在插件主文件中注册:

Hook::addAction('register_widgets', 'YourPlugin_registerWidgets');

function YourPlugin_registerWidgets() {
    if (!class_exists('WidgetRegistry')) return;
    $file = __DIR__ . '/widget_your.php';
    if (is_file($file)) require_once $file;
    if (class_exists('Your_Widget')) {
        WidgetRegistry::register(new Your_Widget());
    }
}

4. Widget 基类 API

4.1 可覆写的属性

属性类型必填说明
$idstring✅唯一标识,仅允许 [a-z0-9_],建议 插件名_功能名
$namestring✅后台显示名称
$descriptionstring⬜后台显示描述
$authorstring⬜作者名
$versionstring⬜版本号
$sourcestring⬜来源标识,建议 plugin:插件名

4.2 必须实现的方法

abstract public function render($settings, $instanceId);
参数类型说明
$settingsarray当前实例的配置数据
$instanceIdstring当前实例的唯一 ID(用于 DOM 定位、事件绑定)

4.3 可覆写的方法

// 默认配置(后台首次添加时使用)
public function defaults() { return []; }

// 后台配置表单(HTML 输出)
public function form($settings) { echo '<p>无可配置项</p>'; }

// 当前是否可用(如依赖插件未启用则返回 false)
public function isAvailable() { return true; }

4.4 父类提供的辅助方法

方法说明
$this->h($str)HTML 转义
$this->output($instanceId, $title, $content, $extraClass = '')输出标准外壳(含标题 + 内容)
$this->fieldText($name, $value, $label = '', $placeholder = '')生成文本输入字段
$this->fieldNumber($name, $value, $label = '', $min = 1, $max = 100)生成数字输入字段
$this->fieldSelect($name, $value, $options, $label = '')生成下拉选择字段

5. 快速开始:第一个小工具

一个「站点公告」小工具,显示一段可配置的公告文字。

<?php
// content/plugins/your-plugin/widget_notice.php
if (!defined('LITELOG_ROOT')) exit('error!');
if (!class_exists('Widget')) return;

class Widget_Notice extends Widget {
    public $id          = 'notice';
    public $name        = '站点公告';
    public $description = '显示一段可自定义的公告文字';
    public $version     = '1.0.0';
    public $source      = 'plugin:your-plugin';

    // 默认配置
    public function defaults() {
        return [
            'title'   => '公告',
            'content' => '欢迎来到本站!',
            'style'   => 'info',
        ];
    }

    // 后台配置表单
    public function form($s) {
        $s = array_merge($this->defaults(), is_array($s) ? $s : []);
        $this->fieldText('title', $s['title'], '标题');
        echo '<p class="wg-field">';
        echo '<label>公告内容</label>';
        echo '<textarea data-setting="content" rows="3" '
           . 'style="width:100%;padding:8px 11px;border:1px solid #dfe3e8;border-radius:7px;'
           . 'font-family:inherit;font-size:13.5px;box-sizing:border-box">'
           . $this->h($s['content']) . '</textarea></p>';
        $this->fieldSelect('style', $s['style'], [
            'info'    => '普通提示',
            'warning' => '警告',
            'success' => '成功',
        ], '样式');
    }

    // 前台渲染
    public function render($s, $instanceId) {
        $s = array_merge($this->defaults(), is_array($s) ? $s : []);

        $cls = 'wg-notice wg-notice-' . preg_replace('/[^a-z]/', '', $s['style']);
        $body = '<div class="' . $cls . '">'
              . nl2br($this->h($s['content']))
              . '</div>';

        $this->output($instanceId, $s['title'], $body);
    }
}

在插件主文件中注册:

Hook::addAction('register_widgets', function () {
    if (!class_exists('WidgetRegistry')) return;
    require_once __DIR__ . '/widget_notice.php';
    WidgetRegistry::register(new Widget_Notice());
});

效果:

  • 后台「小工具」页面出现「站点公告」
  • 拖拽到任意区域 → 可配置标题、内容、样式
  • 前台侧栏按配置渲染

6. 表单字段规范

6.1 data-setting 属性(核心)

所有配置项必须添加 data-setting="键名" 属性。后台 JS 通过这个属性读写配置。

<input type="text" data-setting="title" value="...">
<select data-setting="style">...</select>
<textarea data-setting="content">...</textarea>
<input type="number" data-setting="limit" value="10">
<input type="checkbox" data-setting="show_date">

6.2 使用内置辅助方法

文本输入:

$this->fieldText('title', $s['title'], '标题', '请输入标题');

数字输入:

$this->fieldNumber('limit', $s['limit'], '显示数量', 1, 30);

下拉选择:

$this->fieldSelect('order', $s['order'], [
    'latest' => '最新',
    'hot'    => '热门',
], '排序方式');

多行文本(无内置方法,手写):

echo '<p class="wg-field">';
echo '<label>公告内容</label>';
echo '<textarea data-setting="content" rows="3">' . $this->h($s['content']) . '</textarea>';
echo '</p>';

复选框(多选组):

$opts = ['a' => '选项A', 'b' => '选项B'];
$checked = (array)($s['enabled'] ?? []);
echo '<p class="wg-field"><label>启用项</label>';
foreach ($opts as $k => $label) {
    $ck = in_array($k, $checked) ? ' checked' : '';
    echo '<label style="display:block">';
    echo '<input type="checkbox" data-setting="enabled_' . $k . '"' . $ck . '> ';
    echo $this->h($label) . '</label>';
}
echo '</p>';
⚠️ 由于系统以 键名 → 值 平铺存储,多选组建议拆成多个独立的 enabled_xxx 布尔键。

6.3 字段值类型

表单类型存储类型说明
text / textareastring原样存储
numberintJS 会 parseInt
checkbox0 / 1未勾选为 0,勾选为 1
selectstring选项 value 原样存储

7. 条件显示

当表单字段之间存在依赖关系时(例如"帖子模式"下才需要"排序方式"),使用 data-show-when 属性。

7.1 语法

<!-- 当 setting 的 mode 值为 "threads" 时显示 -->
<p class="wg-field" data-show-when="mode=threads">...</p>

<!-- 当 mode 值为 "threads" 或 "boards" 时显示 -->
<p class="wg-field" data-show-when="mode=threads,boards">...</p>

<!-- 同时满足多个条件(用 ; 分隔) -->
<p class="wg-field" data-show-when="mode=threads;show_author=1">...</p>

7.2 工作原理

  1. 后台 JS 监听所有 data-setting 字段的 input / change 事件
  2. 每次变化时,遍历当前卡片内的 data-show-when 字段
  3. 收集当前所有 setting 值,判断条件是否满足
  4. 满足则 display:'',否则 display:none

7.3 完整示例

public function form($s) {
    $s = array_merge($this->defaults(), is_array($s) ? $s : []);
    ?>
    <p class="wg-field">
        <label>显示模式</label>
        <select data-setting="mode">
            <option value="threads" <?= $s['mode'] === 'threads' ? 'selected' : '' ?>>帖子列表</option>
            <option value="boards"  <?= $s['mode'] === 'boards'  ? 'selected' : '' ?>>版块列表</option>
        </select>
    </p>

    <!-- 仅"帖子列表"模式显示 -->
    <p class="wg-field" data-show-when="mode=threads">
        <label>排序方式</label>
        <select data-setting="order">
            <option value="latest">最新</option>
            <option value="hot">热门</option>
        </select>
    </p>

    <!-- 仅"版块列表"模式显示 -->
    <p class="wg-field" data-show-when="mode=boards">
        <label>显示版块数</label>
        <input type="number" min="1" max="30" data-setting="limit" value="<?= (int)$s['limit'] ?>">
    </p>
    <?php
}

8. 多实例支持

核心原则:render() 内不要依赖全局状态,所有数据从 $settings 读取。

❌ 错误做法

public function render($s, $instanceId) {
    static $count = 0;   // ❌ 静态变量在多实例间共享
    $count++;
    echo "第 {$count} 个实例";
}
public function render($s, $instanceId) {
    global $currentLimit;  // ❌ 全局变量可能被其他实例覆盖
}

✅ 正确做法

public function render($s, $instanceId) {
    // 从 $settings 读,不用全局状态
    $limit = max(1, (int)($s['limit'] ?? 5));
    
    // 需要 ID 时用 $instanceId
    echo '<div id="my-widget-' . $this->h($instanceId) . '">';
    echo '...';
    echo '</div>';
}

样式隔离(多实例)

每个实例的 DOM 用 $instanceId 或唯一 class 区分,需要 JS 时使用事件委托:

public function render($s, $instanceId) {
    $uid = 'wgt_' . preg_replace('/[^a-z0-9]/i', '', $instanceId);
    ?>
    <div class="my-widget" data-instance="<?= $this->h($instanceId) ?>">
        <button class="my-widget-btn" data-uid="<?= $this->h($uid) ?>">点击</button>
    </div>
    <script>
    // 使用事件委托,对所有实例都生效
    document.addEventListener('click', function (e) {
        var btn = e.target.closest('.my-widget-btn');
        if (!btn) return;
        // 通过 data-uid 定位到具体实例
        var uid = btn.getAttribute('data-uid');
        var root = document.querySelector('[data-instance="' + uid + '"]');
        // ...
    });
    </script>
    <?php
}
💡 提示:<script> 标签只在首次渲染时输出一次即可。可以像 HomepageMode_Forum 那样用静态标记:
private static $styleRendered = false;

private function outputStyle() {
    if (self::$styleRendered) return;
    self::$styleRendered = true;
    echo '<style>...</style>';
}

9. 样式规范

9.1 命名前缀

为避免与其他插件冲突,所有 class 必须以你的小工具 ID 为前缀:

/* 小工具 id = "my_notice" */
.wg-my_notice       { /* 外壳 */ }
.wg-my_notice-title { /* 标题 */ }
.wg-my_notice-body  { /* 内容 */ }

9.2 复用主题样式

LiteLog 主题已内置通用 Widget 样式:

.widget                 /* 卡片外壳(白底、圆角、阴影) */
.widget-title           /* 标题(带蓝色竖线) */
.widget-content         /* 内容区 */
.wg-list                /* 列表 */
.wg-list li             /* 列表项 */
.wg-list li a           /* 列表链接 */
.wg-tag-cloud           /* 标签云 */
.wg-tag-cloud a         /* 标签 */
.wg-empty               /* 空状态 */

推荐直接复用:

public function render($s, $instanceId) {
    $logs = LogModel::latest(5);
    ob_start();
    if (empty($logs)) {
        echo '<p class="wg-empty">暂无内容</p>';
    } else {
        echo '<ul class="wg-list">';
        foreach ($logs as $l) {
            echo '<li><a href="' . $this->h(Url::log($l['gid'], $l['alias'])) . '">'
               . $this->h($l['title']) . '</a></li>';
        }
        echo '</ul>';
    }
    $this->output($instanceId, $s['title'], ob_get_clean());
}

9.3 CSS 输出方式

方式一:内联输出(推荐简单场景)

private static $cssDone = false;
private function outputCss() {
    if (self::$cssDone) return;
    self::$cssDone = true;
    echo '<style>.wg-my_notice { ... }</style>';
}

方式二:外部文件(推荐复杂场景)

// 在 render 中注入
if (!defined('MY_WIDGET_CSS_LOADED')) {
    define('MY_WIDGET_CSS_LOADED', true);
    echo '<link rel="stylesheet" href="' . BLOG_URL . 'content/plugins/your-plugin/assets/widget.css">';
}

10. 完整示例

示例 1:最新文章(带数量配置)

<?php
if (!defined('LITELOG_ROOT')) exit('error!');
if (!class_exists('Widget')) return;

class Widget_RecentPosts extends Widget {
    public $id          = 'recent_posts';
    public $name        = '最新文章';
    public $description = '显示最新发布的文章';
    public $version     = '1.0.0';

    public function defaults() {
        return ['title' => '最新文章', 'limit' => 5, 'show_date' => 0];
    }

    public function form($s) {
        $s = array_merge($this->defaults(), is_array($s) ? $s : []);
        $this->fieldText('title', $s['title'], '标题');
        $this->fieldNumber('limit', $s['limit'], '显示数量', 1, 20);
        $this->fieldSelect('show_date', $s['show_date'], [
            0 => '不显示日期',
            1 => '显示日期',
        ], '日期显示');
    }

    public function render($s, $instanceId) {
        $s = array_merge($this->defaults(), is_array($s) ? $s : []);
        $limit = max(1, min(20, (int)$s['limit']));
        $logs = LogModel::latest($limit);

        ob_start();
        if (empty($logs)) {
            echo '<p class="wg-empty">暂无文章</p>';
        } else {
            echo '<ul class="wg-list">';
            foreach ($logs as $l) {
                echo '<li>';
                echo '<a href="' . $this->h(Url::log($l['gid'], $l['alias'])) . '">'
                   . $this->h($l['title']) . '</a>';
                if (!empty($s['show_date'])) {
                    echo '<span class="wg-date">' . date('m-d', $l['date']) . '</span>';
                }
                echo '</li>';
            }
            echo '</ul>';
        }
        $this->output($instanceId, $s['title'], ob_get_clean());
    }
}

示例 2:带模式切换的通用列表

<?php
if (!defined('LITELOG_ROOT')) exit('error!');
if (!class_exists('Widget')) return;

class Widget_ContentList extends Widget {
    public $id          = 'content_list';
    public $name        = '内容列表';
    public $description = '可选择显示文章 / 分类 / 标签';

    public function defaults() {
        return [
            'title'       => '内容',
            'source'      => 'posts',
            'limit'       => 8,
            'show_count'  => 1,
        ];
    }

    public function form($s) {
        $s = array_merge($this->defaults(), is_array($s) ? $s : []);
        ?>
        <p class="wg-field">
            <label>标题</label>
            <input type="text" data-setting="title" value="<?= $this->h($s['title']) ?>">
        </p>

        <p class="wg-field">
            <label>内容来源</label>
            <select data-setting="source">
                <option value="posts"   <?= $s['source'] === 'posts'   ? 'selected' : '' ?>>文章</option>
                <option value="sorts"   <?= $s['source'] === 'sorts'   ? 'selected' : '' ?>>分类</option>
                <option value="tags"    <?= $s['source'] === 'tags'    ? 'selected' : '' ?>>标签</option>
            </select>
        </p>

        <p class="wg-field" data-show-when="source=posts">
            <label>显示数量</label>
            <input type="number" min="1" max="30" data-setting="limit" value="<?= (int)$s['limit'] ?>">
        </p>

        <p class="wg-field" data-show-when="source=sorts,tags">
            <label>显示数量徽章</label>
            <select data-setting="show_count">
                <option value="1" <?= !empty($s['show_count']) ? 'selected' : '' ?>>显示</option>
                <option value="0" <?= empty($s['show_count'])  ? 'selected' : '' ?>>不显示</option>
            </select>
        </p>
        <?php
    }

    public function render($s, $instanceId) {
        $s = array_merge($this->defaults(), is_array($s) ? $s : []);
        $source = $s['source'];
        $limit  = max(1, min(30, (int)$s['limit']));
        $showCount = !empty($s['show_count']);

        ob_start();
        switch ($source) {
            case 'sorts':
                $items = SortModel::all();
                if (empty($items)) {
                    echo '<p class="wg-empty">暂无分类</p>';
                    break;
                }
                echo '<ul class="wg-list">';
                foreach ($items as $it) {
                    echo '<li><a href="' . $this->h(Url::sort($it['sid'], $it['alias'])) . '">'
                       . $this->h($it['sortname']) . '</a>';
                    if ($showCount) {
                        echo '<span class="wg-count">'
                           . SortModel::countInSort($it['sid']) . '</span>';
                    }
                    echo '</li>';
                }
                echo '</ul>';
                break;

            case 'tags':
                $items = TagModel::all();
                if (empty($items)) {
                    echo '<p class="wg-empty">暂无标签</p>';
                    break;
                }
                echo '<div class="wg-tag-cloud">';
                foreach ($items as $it) {
                    echo '<a href="' . $this->h(Url::tag($it['tagname'])) . '">'
                       . $this->h($it['tagname']) . '</a>';
                }
                echo '</div>';
                break;

            case 'posts':
            default:
                $items = LogModel::latest($limit);
                if (empty($items)) {
                    echo '<p class="wg-empty">暂无文章</p>';
                    break;
                }
                echo '<ul class="wg-list">';
                foreach ($items as $it) {
                    echo '<li><a href="' . $this->h(Url::log($it['gid'], $it['alias'])) . '">'
                       . $this->h($it['title']) . '</a></li>';
                }
                echo '</ul>';
        }

        $this->output($instanceId, $s['title'], ob_get_clean());
    }
}

示例 3:纯展示型小工具(无配置)

class Widget_Copyright extends Widget {
    public $id          = 'copyright';
    public $name        = '版权声明';
    public $description = '显示站点版权信息';

    public function form($s) {
        echo '<p class="wg-no-options">此小工具没有可配置项</p>';
    }

    public function render($s, $instanceId) {
        $year = date('Y');
        $site = Option::get('site_title');
        $body = '<div style="font-size:12.5px;color:#94a3b8;line-height:1.8">'
              . '© ' . $year . ' ' . $this->h($site) . '<br>'
              . 'Powered by LiteLog'
              . '</div>';
        $this->output($instanceId, '', $body);
    }
}

11. 高级话题

11.1 依赖检测

如果小工具依赖其他插件,用 isAvailable() 声明:

public function isAvailable() {
    return class_exists('ForumThread')
        && Option::get('forum_enable', 'y') === 'y';
}

后台配置页面会自动禁用不可用的小工具,并提示用户。

11.2 缓存

如果查询较重,可在 render() 内使用 Cache:

public function render($s, $instanceId) {
    $cache = new Cache(CONTENT_PATH . 'cache/');
    $key = 'my_widget_' . md5(serialize($s));
    $data = $cache->read($key);
    if ($data === false) {
        $data = heavyQuery();
        $cache->write($key, $data);  // 默认按需
    }
    // ...
}

11.3 钩子扩展

在自己的 Widget 渲染中支持二级钩子,让其他插件可插入内容:

public function render($s, $instanceId) {
    ob_start();
    // ...
    Hook::doAction('my_widget_before_content', $s);
    echo '主体内容';
    Hook::doAction('my_widget_after_content', $s);
    // ...
}

11.4 动态区域

系统内置两个区域:

WidgetArea::AREA_LEFT   // 'left_sidebar'
WidgetArea::AREA_RIGHT  // 'right_sidebar'

如需新区域,在插件启动时注册:

Hook::addAction('init_widget_areas', function () {
    WidgetArea::registerArea('footer_widgets', [
        'name' => '页脚区域',
        'desc' => '显示在页面底部',
    ]);
});

然后在主题中调用:

WidgetArea::render('footer_widgets');

12. 常见问题

Q1:小工具在后台不显示?

排查步骤:

  1. 检查插件主文件的 Hook::addAction('register_widgets', ...) 是否执行
  2. 检查 widget_xxx.php 是否被 require_once 加载
  3. 检查类是否继承了 Widget 且 $id 不为空
  4. 检查是否已启用插件

Q2:多个实例配置相互干扰?

原因:通常是在 render() 或 form() 中使用了静态变量或全局状态。

解决:

  • 所有配置从 $settings 读取
  • 需要唯一 ID 时用 $instanceId
  • 不要用 static $x 保存状态

Q3:切换模式时字段没隐藏?

检查:

  1. 字段是否添加了 data-show-when="key=value" 属性?
  2. 被依赖的字段是否有 data-setting="key"?
  3. value 与配置项的实际值是否一致(注意字符串 vs 数字)?

Q4:表单修改没保存?

检查:

  1. 所有可编辑字段是否有 data-setting 属性?
  2. 表单元素是否是 input / select / textarea?
  3. 浏览器控制台是否有 JS 报错?

Q5:前台渲染的样式与主题不一致?

方案:

  • 复用主题通用 class:.widget、.widget-title、.wg-list、.wg-tag-cloud
  • 用小工具 ID 作为前缀(如 .wg-your_id)
  • 避免使用 !important 覆盖主题

Q6:如何让某个小工具支持多语言?

在 form() 和 render() 中统一用 __() 或自定义翻译函数:

$this->fieldText('title', $s['title'], __('Title', 'your-plugin'));

LiteLog 内核不内置 i18n 系统,需要插件自行实现。

Q7:如何在内核更新后保持兼容?

  • 只使用文档中列出的公开 API
  • 避免直接访问 WidgetRegistry / WidgetArea 的私有属性
  • 遵循版本号语义:内核可能新增方法,但不会破坏现有方法签名

附录 A:完整 API 速查表

Widget 属性

public $id;
public $name;
public $description;
public $author;
public $version;
public $source;

Widget 方法

abstract public function render($settings, $instanceId);
public function form($settings);
public function defaults();
public function isAvailable();

// 辅助方法
protected function h($str);
protected function output($instanceId, $title, $content, $extraClass = '');
protected function fieldText($name, $value, $label = '', $placeholder = '');
protected function fieldNumber($name, $value, $label = '', $min = 1, $max = 100);
protected function fieldSelect($name, $value, $options, $label = '');

WidgetRegistry 静态方法

WidgetRegistry::register($widget);
WidgetRegistry::all();           // 返回 [id => Widget]
WidgetRegistry::get($id);        // 返回 Widget 或 null
WidgetRegistry::exists($id);     // 返回 bool

WidgetArea 静态方法

WidgetArea::render($area);              // 渲染某个区域
WidgetArea::hasVisibleWidgets($area);   // 该区域是否有可见小工具
WidgetArea::getData($area);             // 获取原始数据(数组或 null)
WidgetArea::setData($area, $list);      // 保存数据
WidgetArea::getEffectiveData($area);    // 获取生效数据(含默认值)

相关钩子

钩子时机
register_widgets注册表初始化时触发,插件注册小工具的入口

附录 B:命名建议

场景推荐避免
Widget IDforum_latest、novel_rankingmy_widget_1、w1
类名Widget_ForumLatestLatest、WL
CSS 前缀.wg-forum_latest.latest、.box
数据键title、limit、show_countt、n、flag

结语

小工具系统是 LiteLog 生态的扩展基石。遵循本规范,你的小工具将:

  • 与内核、其他插件、主题完全兼容
  • 支持任意数量的实例,互不干扰
  • 拥有开箱即用的后台配置体验
  • 大幅降低用户使用门槛

如遇文档未覆盖的问题,欢迎提交 Issue 或查阅内核源码 include/lib/widget.php。

版本历史

版本日期变更
1.02026-10-08首次发布
登录 或 后台登录 后参与讨论