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; ?>

在这个检查中,我们遵循与控制器中相同的原理。唯一值得注意的事情是,在html.php渲染器中调用Joomla组件助手设置参数对象,而不是在它被使用的地方内联调用,这可以通过我们调用 $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,然后获取书籍表的一个实例并加载相应的行。一旦加载了行,我们就可以轻松地将发布状态设置为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/之外的第三方外部服务