20分钟阅读时间 (4084个单词)

Joomla! 3.0 扩展开发系列:管理员配置与代码整理

Joomla! 3.0 Extension Development Series: Admin Configuration and Code Cleanup

大家好!欢迎来到我们开发系列的最后一部分代码部分。如果你从开头一直跟到这里,你现在已经走过了创建原生Joomla! 3.x系列扩展的代码开发过程。这是一个漫长的过程,但希望也是一个既令人满意又富有信息量的过程。我在过去这些文章中很高兴有机会与大家分享Joomla扩展开发的技巧和窍门。在这篇最后的开发文章中,我们将探讨一些最后的想法,进行一些整理,并讨论未来可以添加的额外功能。现在,请准备好,我们将深入最后一篇关于Joomla 3.x组件开发的文章。

  步骤0:制作咖啡

是的,你说对了。和之前每一篇开发文章一样,我们将会从一杯新鲜的咖啡开始。我们将开始让大脑为编写代码做好准备,并开始专注于手头的任务。希望这篇文章不会像之前的文章那么详细,因此我相信不需要额外的咖啡,并且希望你能在这篇教程结束时还有咖啡喝。


  步骤1:编写管理面板

当前Joomla管理员面板有各种用途,并且有几种不同的方式可以最有效地使用它。可以认为管理员面板是每个组件根据需要使用的独特机会。我们编写了Lendr作为前端应用程序,组件的大部分功能和目的都是由前端用户利用的。由于这种情况,我们将仅在Lendr中使用管理员面板来提供额外功能和一些基本选项设置。这样,我们将在管理员组件模型、视图和控制器中避免存储重复代码,同时我们仍将继续能够演示管理员侧的正确代码结构。希望通过遵循这种方法,您能够理解何时以及如何最有效地使用管理员面板。

为管理员侧代码创建了以下文件:一个帮助文件、一个控制器、一个模型、一个视图、一个访问文件、一个配置文件和我们的语言文件。

我们将逐一处理这些文件,从主要入口点:lendr.php开始。

joomla_root/administrator/components/com_lendr/lendr.php

<?php // No direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
//load classes
JLoader::registerPrefix('Lendr', JPATH_COMPONENT_ADMINISTRATOR);
//Load plugins
JPluginHelper::importPlugin('lendr');
 
//application
$app = JFactory::getApplication();
 
// Require specific controller if requested
$controller = $app->input->get('controller','display');
// Create the controller
$classname  = 'LendrControllers'.ucwords($controller);
$controller = new $classname();
 
// Perform the Request task
$controller->execute();
              
此代码与前端入口点非常相似,主要作用是通过适当的控制器引导流量。

joomla_root/administrator/components/com_lendr/controllers/display.php

<?php defined( '_JEXEC' ) or die( 'Restricted access' ); 
 
class LendrControllersDisplay extends JControllerBase
{
  public function execute()
  {
    // Get the application
    $app = $this->getApplication();
 
    // Get the document object.
    $document     = JFactory::getDocument();
 
    $viewName     = $app->input->getWord('view', 'statistics');
    $viewFormat   = $document->getType();
    $layoutName   = $app->input->getWord('layout', 'default');
    $app->input->set('view', $viewName);
 
    // Register the layout paths for the view
    $paths = new SplPriorityQueue;
    $paths->insert(JPATH_COMPONENT . '/views/' . $viewName . '/tmpl', 'normal');
 
    $viewClass  = 'LendrViews' . ucfirst($viewName) . ucfirst($viewFormat);
    $modelClass = 'LendrModels' . ucfirst($viewName);
    $view = new $viewClass(new $modelClass, $paths);
    $view->setLayout($layoutName);
    // Render our view.
    echo $view->render();
 
    return true;
  }
}

您应该立即注意到此控制器与默认前端控制器之间的相似之处。我们在两个中都执行类似任务。在这种情况下,我们设置的默认视图与前端不同。

接下来,我们将检查位于默认视图(如前所述,默认视图被标记为statistics)内的html.php文件夹。

/joomla_root/administrator/components/com_lendr/views/statistics/html.php

>?php defined( '_JEXEC' ) or die( 'Restricted access' ); 
 
class LendrViewsStatisticsHtml extends JViewHtml
{
  function render()
  {
    $app = JFactory::getApplication();
   
    //retrieve task list from model
    $model = new LendrModelsStatistics();
    $this->stats = $model->getStats();
    $this->addToolbar();
    //display
    return parent::render();
  } 
    /**
     * Add the page title and toolbar.
     *
     * @since   1.6
     */
    protected function addToolbar()
    {
        $canDo  = LendrHelpersLendr::getActions();
        // Get the toolbar object instance
        $bar = JToolBar::getInstance('toolbar');
        JToolbarHelper::title(JText::_('COM_LENDR_STATISTICS'));
               
        if ($canDo->get('core.admin'))
        {
            JToolbarHelper::preferences('com_lendr');
        }
    }
}
            

此文件有几个新元素,所以我们将对它进行更深入的探讨。首先请注意,我们正在继承JViewHtml类。这为我们提供了一些基本功能,您可以在Joomla库文件夹中直接查看JViewHtml类以找到这些功能。然后我们有对模型的标准调用,以加载我们视图所需的特定数据。

接下来,我们有一行新代码。addToolbar函数位于此文件中,通常用于向组件管理员侧的顶部工具栏添加元素。由于我们只进行基本的行政面板,功能有限,因此我们的工具栏中只有一个项目。

最后,我们渲染视图,就像在前端一样。

如前所述,addToolbar函数添加了按钮到组件中的子导航顶部工具栏。由于我们没有将完整的一组视图和功能集成到我们的行政面板示例中,因此我们在此情况下只有一个按钮。我们首先要做的是创建标准Joomla工具栏类的实例,然后将我们的标题和按钮分配给此实例。

注意,我们的语言文件被用于所有语言字符串。如果我们想在组件标题旁边显示一个图像,我们只需在定义标题行时传递第二个引用。

注意:记得在之前的文章中查看管理员时出现错误?这是因为我们没有设置页面标题,而Isis模板期望一个。

在此函数中,我们还调用了我们的帮助类。现在是一个回顾该帮助类的作用及其使用方法的好时机。这个帮助类包含了一些在组件管理中使用的有用功能。

注意:注意:应注意的是,我们从未直接包含此帮助类,而是Joomla自动加载器(如主lendr.php文件中定义的)根据我们的命名约定自动找到该文件,并在我们调用时加载它。

joomla_root/administrator/components/com_lendr/helpers/lendr.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_lendr
 *
 * @copyright   Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
defined('_JEXEC') or die;
/**
 * Lendr component helper.
 *
 * @package     Joomla.Administrator
 * @subpackage  com_lendr
 * @since       1.6
 */
class LendrHelpersLendr
{
  public static $extension = 'com_lendr';
  /**
   * @return  JObject
   */
  public static function getActions()
  {
    $user = JFactory::getUser();
    $result = new JObject;
    $assetName = 'com_lendr';
    $level = 'component';
    $actions = JAccess::getActions('com_lendr', $level);
    foreach ($actions as $action)
    {
      $result->set($action->name, $user->authorise($action->name, $assetName));
    }
    return $result;
  }
}
          

在这个辅助函数中,我们目前有一个函数。getActions 函数将使用存储在组件管理员根目录下的 access.xml 文件。这是一个很好的功能,它只需加载 XML 中找到的值即可创建一个对象。

让我们花一点时间来看看 access.xml 文件,并了解它如何与组件中将要使用到的功能相关联。

joomla_root/administrator/components/com_lendr/access.xml

<?xml version="1.0" encoding="utf-8"?>
<access component="com_lendr">
  <section name="component">
    <action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" />
  </section>
</access>

为了本教程的目的,我们有一个非常简化的访问文件。基本上,我们只是查找是否有管理员权限。我们将使用管理员级别的访问权限来确定用户是否有能力查看管理员组件子菜单中的选项(我们在本教程之前开始的地方)。这里可以添加更多部分和动作,也可以添加自定义动作。这和 Joomla 2.5 中的内容相同,如果感兴趣,建议查看那些教程的细节。

注意:您可以在相关的 http://docs.joomla.org 教程中查看 Joomla 2.5 的访问信息。

这使我们来到了这一步骤的第二部分。组件选项。

组件选项

我们组件的选项可以在 Lendr 组件中的顶部工具栏上的选项按钮中找到。这是我们在 html.php 文件的前一部分中查看过的按钮。canDo 访问检查中的行将向工具栏添加一个首选项按钮。这个首选项也被称为组件的全局配置选项。正如您可能从其他组件中知道的,所有首选项按钮在被点击时都会将用户导向 com_config 组件和相应的组件视图。

用于此视图的数据(当您导航到 Lendr 组件的 com_config 页面时)是从一个特定文件中拉取的。config.xml 文件也存储在管理员侧的 lendr 组件根目录中。现在让我们来看看这个文件。

joomla_root/administrator/components/com_lendr/config.xml
<?xml version="1.0" encoding="utf-8"?>
<config>
  <fieldset name="component"
    label="COM_LENDR_OPTIONS"
    description="COM_LENDR_OPTIONS_DESC"
  >
    <field name="required_account" type="radio"
      default="0"
      class="btn-group"
      label="COM_LENDR_REQUIRED_ACCOUNT"
      description="COM_LENDR_REQUIRED_ACCOUNT_DESC">
      
      <option value="0">JNO</option>
      <option value="1">JYES</option>
    </field>
    <field name="new_reviews" type="radio"
      default="1"
      class="btn-group"
      label="COM_LENDR_ALLOW_NEW_REVIEWS"
      description="COM_LENDR_ALLOW_NEW_REVIEWS_DESC">
      
      <option value="0">JNO</option>
      <option value="1">JYES</option>
    </field>
    <field name="new_wishlists" type="radio"
      default="1"
      class="btn-group"
      label="COM_LENDR_ALLOW_NEW_WISHLISTS"
      description="COM_LENDR_ALLOW_NEW_WISHLISTS_DESC">
      
      <option value="0">JNO</option>
      <option value="1">JYES</option>
    </field>
    <field name="new_waitlists" type="radio"
      default="1"
      class="btn-group"
      label="COM_LENDR_ALLOW_NEW_WAITLISTS"
      description="COM_LENDR_ALLOW_NEW_WAITLISTS_DESC">
      
      <option value="0">JNO</option>
      <option value="1">JYES</option>
    </field>
  </fieldset>
  <fieldset name="permissions"
    description="JCONFIG_PERMISSIONS_DESC"
    label="JCONFIG_PERMISSIONS_LABEL"
  >
    <field name="rules" type="rules"
      component="com_lendr"
      filter="rules"
      validate="rules"
      label="JCONFIG_PERMISSIONS_LABEL"
      section="component" />
  </fieldset>
</config>

在这个文件中,您将看到我们定义了两个字段集。这些字段集对应于在查看 com_config 选项时管理员侧的标签页。第一个字段集显示我们希望管理员用户可以配置的具体参数,第二个字段集处理查看选项按钮的能力。

注意:您可以在组件中定义尽可能多的唯一字段集(标签页)。

在查看和创建此文件时,以下是一些重要事项需要记住。首先,您应该使用您的语言文件来定义所有适当的字符串。其次,您可以使用并应定义用于字段以便它们使用适当的样式。您可以在 Joomla 的其他部分中定义多种类型的字段,在这个特定实例中,我们主要利用单选按钮来切换是或否的值。

现在我们需要探索添加到前端代码中并使用这些新参数的代码。

我们将查看两个特定的实例。首先是 required_account 选项。此选项允许我们定义用户在能够查看 Lendr 之前是否必须登录。我们在以下文件中实现此选项。

joomla_root/components/com_lendr/controllers/default.php
// Line 11 - 19
 $params = JComponentHelper::getParams('com_lendr');
    if ($params->get('required_account') == 1) 
    {
        $user = JFactory::getUser();
        if ($user->get('guest'))
        {
            $app->redirect('index.php',JText::_('COM_LENDR_ACCOUNT_REQUIRED_MSG'));
        }
    }

我们首先使用 Joomla 组件助手获取我们的参数对象。一旦我们有了这些参数,我们就可以检查是否要求用户拥有账户。如果是的话,我们检查用户是否已登录,如果没有,我们将他们发送到 index.php 并带有一条消息,告诉他们在查看 Lendr 之前登录。

我们将在以下视图布局中查看参数值使用情况的另一个实例。

joomla_root/components/com_lendr/views/book/tmpl/_entry.php
// Line 43 - 48
<?php if($this->params->get('new_wishlists') == 1 ): ?>
                      <li><a href="javascript:void(0);" onclick="addToWishlist('<?php echo $this->book->book_id; ?>');"><?php echo JText::_('COM_LENDR_ADD_WISHLIST'); ?></a></li>
                    <?php endif; ?>
                    <?php if($this->params->get('new_reviews') == 1 ): ?>
                      <li><a href="#newReviewModal" data-toggle="modal"><?php echo JText::_('COM_LENDR_WRITE_REVIEW'); ?></a></li>
                    <?php endif; ?>

在这个检查中,我们遵循与控制器中相同的原理。唯一值得注意的一点是,设置params对象的Joomla组件助手调用是在html.php渲染器中完成的,而不是直接在它被使用的地方进行,这一点可以通过我们在视图中调用 $this->params 来看到。


  第2步:代码清理

这里我们将快速审查代码清理的两个方面。首先,我们需要添加删除对象的能力,其次我们需要有一个列表视图来显示系统中的所有书籍。

删除

删除对象可以通过多种方式完成。在一种情况下,我们不会真正从数据库中删除数据(尽管这很容易做到),在第二种情况下,我们将实际上完全删除数据。首先,通常在不实际从网站上删除数据的情况下隐藏信息是有帮助的。这允许我们在不小心删除某些东西时“恢复”一个项目。在Lendr中,快速软删除一个项目的最快和最有效的方法是将已发布变量设置为0(零)。在其他一些组件中,已发布的0可能不表示软删除,在这些情况下,更常见的是将已发布值设置为-1。在我们的特定用例中,我们不使用已发布=0来执行任何特定功能,因此通过将其设置为0,我们实际上是从显示中软删除了项目。

joomla_root/components/com_lendr/controllers/delete.php

<?php defined( '_JEXEC' ) or die( 'Restricted access' ); 
 
class LendrControllersDelete extends JControllerBase
{
  public function execute()
  {
    $app = JFactory::getApplication();
    $return = array("success"=>false);
    
    $type = $app->input->get('type','waitlist');
   
    $modelName = 'LendrModels'.ucfirst($type);    
    $model = new $modelName();
    if ( $row = $model->delete() )
    {
      $return['success'] = true;
      $return['msg'] = JText::_('COM_LENDR_BOOK_DELETE_SUCCESS');
    }else{
      $return['msg'] = JText::_('COM_LENDR_BOOK_DELETE_FAILURE');
    }
    echo json_encode($return);
  }
}

在我们的删除控制器中(因为每个控制器都是一个单一的操作)我们有一个将数据传递给适当的模型进行处理的函数。在这里,我们根据JInput变量中的类型值配置模型名称。模型是我们将实际删除项的地方。

joomla_root/components/com_lendr/models/book.php

 /**
  * Delete a book
  * @param int      ID of the book to delete
  * @return boolean True if successfully deleted
  */
  public function delete($id = null)
  {
    $app  = JFactory::getApplication();
    $id   = $id ? $id : $app->input->get('book_id');
    $book = JTable::getInstance('Book','Table');
    $book->load($id);
    $book->published = 0;
    if($book->store()) 
    {
      return true;
    } else {
      return false;
    }
  }

删除一本书是我们软删除而不是永久删除对象的一个例子。代码非常简单,如上所示。我们将找到我们希望删除的书的ID,然后获取Book表的一个实例并加载相应的行。一旦我们加载了行,我们可以轻松地将发布状态设置为0,然后存储结果。如果行存储成功,我们将向javascript返回一个true值,或者如果存储失败,我们将返回一个false。

我们将查看的第二个删除项目的位置是等待列表模型。

joomla_root/components/com_lendr/models/waitlist.php

/**
  * Delete a book from a waitlist
  * @param int      ID of the book to delete
  * @return boolean True if successfully deleted
  */
  public function delete($id = null)
  {
    $app  = JFactory::getApplication();
    $id   = $id ? $id : $app->input->get('waitlist_id');
    if (!$id)
    {
      if ($book_id = $app->input->get('book_id')) 
      {
        $db = JFactory::getDbo();
        $user = JFactory::getUser();
        $query = $db->getQuery(true);
        $query->delete()
            ->from('#__lendr_waitlists')
            ->where('user_id = ' . $user->id)
            ->where('book_id = ' . $book_id);
        $db->setQuery($query);
        if($db->query()) {
          return true;
        }
      } 
    } else {
      $waitlist = JTable::getInstance('Waitlist','Table');
      $waitlist->load($id);
      if ($waitlist->delete()) 
      {
        return true;
      }      
    }
    return false;
  }

在这个例子中,我们实际上是从数据库表中硬删除行。我们这样做的原因是因为在这个数据中没有发布列,因此删除选项被认为是合适的。

与之前提到的书籍删除示例一样,如果有一个特定的等待列表ID,我们可以加载该特定对象并直接删除它。如果没有特定的等待列表ID,我们可以使用书ID和人员的用户ID查找必要的行,然后删除相关行。

在Lendr中,我们通过AJAX调用处理删除书籍,因此当我们点击删除按钮时,我们希望自动从我们正在查看的页面上删除相关的行。以下是处理AJAX调用和随后删除行的javascript。

joomla_root/components/com_lendr/assets/js/lendr.js

function deleteBook(book_id,type) 
{
  jQuery.ajax({
    url:'index.php?option=com_lendr&controller=delete&format=raw&tmpl=component',
    type:'POST',
    data: 'book_id='+book_id+'&type='+type,
    dataType: 'JSON',
    success:function(data)
    {
      alert(data.msg);
      if(data.success)
      {
        jQuery("tr#bookRow"+book_id).hide();
      }
    }
  });
}

在这里,我们传递了book_id和类型 - 类型对于在删除控制器中使用非常重要,正如我们之前所见(记住:这是我们如何将任务路由到适当的模型)。然后我们将显示由删除控制器生成的结果警告消息,如果删除成功,我们将从列表视图中删除相关的行。我们通过仅使用jQuery隐藏行来删除行。表行ID由bookRow定义,而特定书籍的ID(这是一个唯一标识符)。

书籍列表视图

所有书籍的列表视图是我们需要清理的另一个项目。这很简单,不需要太多代码。首先,我们需要修改用于书籍的html.php视图类。可以通过以下方式完成。

joomla_root/components/com_lendr/views/book/html.php
// Lines 8 - 19
    $layout = $this->getLayout();
    $this->params = JComponentHelper::getParams('com_lendr');
    //retrieve task list from model
    $model = new LendrModelsBook();
    if($layout == 'list')
    {
        $this->books = $model->listItems();
        $this->_bookListView = LendrHelpersView::load('Book','_entry','phtml');
    } else {
  …
          

在这里,我们添加了对获取布局变量的调用,然后检查我们正在加载哪个视图。如果我们处于列表视图,则希望显示所有可用的项目列表,否则我们将以相同的方式加载视图。

接下来,我们将添加一个新的列表布局来显示书籍。

joomla_root/components/com_lendr/views/book/tmpl/list.php
<table cellpadding="0" cellspacing="0" width="100%" class="table table-striped">
  <thead>
    <tr>
      <th><?php echo JText::_('COM_LENDR_DETAILS'); ?></th>
      <th><?php echo JText::_('COM_LENDR_STATUS'); ?></th>
      <th><?php echo JText::_('COM_LENDR_ACTIONS'); ?></th>
    </tr>
  </thead>
  <tbody id="book-list">
    <?php for($i=0, $n = count($this->books);$i<$n;$i++) { 
            $this->_bookListView->book = $this->books[$i];
            $this->_bookListView->type = 'book';
            echo $this->_bookListView->render();
    } ?>
  </tbody>
</table>
          

你应该注意到这个视图与我们之前编写的图书馆视图之间的相似性。唯一的区别是我们现在正在加载系统中的所有书籍,而不是与特定图书馆相关的书籍。


  第3步:菜单和文件清理

我们将审查这个步骤的两个部分。第一部分涉及创建和定义可能的菜单链接入口点,用于在管理员面板中创建菜单链接。第二部分将包括一些不必要的文件和/或函数的轻微删除。

创建菜单链接

通常需要创建一个菜单链接,可以添加到菜单链接中,在前面显示。通过管理员菜单管理器添加菜单链接。当添加新菜单项时,您选择要添加的菜单链接类型。我们需要定义我们想在菜单类型模态选择中显示的视图。我们希望添加到菜单类型选项的两个布局。首先,我们希望允许用户能够链接到所有配置文件的列表,其次我们希望能够链接到所有书籍的列表。

菜单链接基于位于组件各种视图文件夹中的关联metadata.xml文件。以下是配置文件布局的示例。

joomla_root/components/com_lendr/views/profile/tmpl/list.xml
<?xml version="1.0" encoding="utf-8"?>
<metadata>
  <layout title="COM_LENDR_PROFILE_LIST">
    <message><![CDATA[COM_LENDR_PROFILE_LIST_DESC]]></message>
  </layout>
</metadata>
          
注意:此文件名应与同一目录中布局文件的名字一致。

请注意,因为我们正在链接到一个布局,所以我们定义了一个<layout>对象;如果我们链接到一个视图,那么我们将创建一个<view>对象。语言字符串存储在管理系统的语言文件中。

Lendr的系统语言文件如下。

joomla_root/administrator/languages/en-GB/en-GB.com_lendr.sys.ini
COM_LENDR = Lendr
COM_LENDR_SETTINGS = Lendr Settings
COM_LENDR_PROFILE_LIST = Profile List
COM_LENDR_PROFILE_LIST_DESC = Display list of all profiles
COM_LENDR_BOOK_LIST = Book List
COM_LENDR_BOOK_LIST_DESC = Display list of all books

这些字符串用于可能从Lendr组件外部引用Lendr的区域。这意味着这些字符串始终在Joomla中加载,而不仅仅是在index.php?option=com_lendr时。菜单类型模态是一个例子,当使用这些字符串时。管理员组件菜单是第二个例子。

通过将此metadata.xml文件添加到适当的文件夹,我们现在可以在菜单部分查看此布局。元数据文件还可以包含用于添加附加参数的高级选项。(例如,为菜单链接关联特定书籍或配置文件添加选择能力)。

删除不必要的文件和函数

在整个教程中,我们没有添加不必要的函数或文件,所以需要删除的东西相对较少。有一些控制器在前面需要删除,这些控制器在这个教程的范围之外从未实现,还有一些视图我们在布局方面也进行了清理。大部分情况下,这一步主要列在这里,主要是为了提醒未来的开发工作始终确保发布干净的代码。

对于安全性、软件包分发大小和一般的编码标准来说,确保没有可能在未来造成问题的额外文件、文件夹或函数非常重要。请记住,为他人编写代码总是一个好主意。这意味着正确地记录你的代码,并删除任何不必要且可能在未来造成混淆的东西。


  第4步:额外建议

以下是一些额外的建议,如果对这些建议中的任何一项或全部感兴趣,可以在未来的文章中作为本系列的补充。其中一些想法是全新的、前沿的机会,可以为Joomla 3.x组件提供新的功能。如果你对其中的一些建议感兴趣,请在评论区留言。

标签

标签是Joomla 3.1的新功能。通过标签,可以将标签分配给书籍,然后可以根据这些标签搜索和分组书籍。标签还可以允许书籍被分配到多个标签,以便进行过滤和排序。

分类

分类将提供将书籍添加到特定分类的机会。这允许进行大规模的分组,并展示了如何使用Joomla分类结构和演示使用Joomla分类的正确方法。

网络服务 / JSON

使用基本的网络服务模型,我们可以演示直接从Lendr组件中检索数据的方法,而无需使用标准的组件视图和布局。以JSON格式检索数据,以便在其他系统中使用。


下载

从GitHub仓库下载到此点的组件。

下载
下一篇文章:维护发布

正如我提到的,这篇文章代表了这个系列的最后一部分严肃的编码,然而在下一个版本中,我们将讨论一些维护发布和支持组件的想法。我们将探讨GitHub和分发方法,以及版本号约定、发布时间表等。

在Joomla社区杂志上发表的一些文章代表了作者对特定主题的个人观点或经验,可能不代表Joomla项目的官方立场。

0
Joomla 3. 新特性详解...
LESS工具的使用
 

评论

已经注册? 在此登录
尚无评论。成为第一个发表评论的人

通过接受,您将访问由 https://magazine.joomla.net.cn/ 外部的第三方提供的服务