阅读时间20分钟 (4093字)

在Joomla 5中使用模态选择示例创建自定义表单字段类型

2024-01-Creating-custom-form

在开发扩展时,使用Joomla 5中的ModalSelect表单字段类型简化从数千个产品中筛选出正确产品的方法,通过在模态窗口中按类别、制造商和搜索进行筛选。

简介

在与客户合作的过程中,有各种级别的任务:有人需要一个简单有5-6页的网站。有人需要一个大型商品目录或与第三方系统(使用REST API)集成的在线商店。另一些人需要非标准功能,而市场上没有现成的解决方案。

Joomla适合开发,并允许您轻松维护代码。如果需求符合CMS核心,那么它对所有这些情况都有答案。

要完成一个大项目,我们需要将其分解成更小的任务,我想在这篇文章中谈谈解决这些任务之一的方法。

初始数据

客户已经在Joomla的流行在线商店组件之一(JoomShopping)上创建了一个产品目录。他们可以选择产品的参数,将其放入购物车并进行购买。一切都很正常。然而,现在您需要添加创建产品图形布局的功能。例如,您的产品是一个杯子或T恤。在购买之前,您可以访问产品设计师,上传您的标志或照片,写上文字,并将此布局附加到在线商店中的订单。付款后,布局直接进入生产,图像和文字应用于您的杯子,并寄送到指定地址。

由于实现此功能需要相当多的时间,因此它作为一个独立的产品设计师组件创建。同时,将创建数据提供者插件,允许您与一个或另一个电子商务组件进行工作。

小而实用的任务之一是创建在线商店组件的商品与产品设计师组件中的商品之间的连接。这对未来将处理内容的编辑人员来说应该既方便又直观。因此,仅仅创建一个用于指示所需产品id的文本字段是不够的。在线商店中可能只有几十种产品,在这种情况下,选择用于交流的产品并不困难。如果有数千种产品,那么按参数搜索和筛选产品的功能就很重要。如果您可以根据类别、制造商过滤产品列表,或者在成百上千的产品中按名称查找,您的工作将会更快、更简单。

这个字段与编辑器按钮插件(editors-xtd组)的工作非常相似,其中选择的数据在模态窗口中显示:文章链接、模块短代码等。

一点理论

在Joomla管理员面板中,有一些需要用其他组件中的数据填写的数据字段:指定文章、菜单项、联系人、产品等。通常这些字段设计为select option下拉列表,也可以设计为带有datalistinput type="text",但也有一些方便的字段可以显示所需实体的列表,带有过滤、搜索和分页功能。不仅可以使用网站内(各种组件、插件)的源作为数据源,还可以使用通过REST API提供的第三方服务:CRM、配送服务、外部数据库、其他Joomla网站等。

我们在选择菜单项中的文章时都看到过这些字段的实际应用,例如在“文章 - 单篇文章”、“联系人 - 单个联系人”或创建菜单项别名时 - “系统链接 - 菜单项别名”。但是,让我们再次提醒一下它们的外观。

模态文章选择窗口。

模态单个联系人选择窗口。

Joomla中模态选择字段的机遇

让我们仔细看看这些字段 - 它们究竟允许我们做什么。在内心深处,我们明白这个字段的主要任务是获取所选实体的id并将其放入文本字段。但我们在屏幕上看到的是其他东西 - 而不是id号码,我们看到的是文章或联系人的标题。这很方便。您不需要记住id为1452704的文章的名称。视频也清楚地表明,如果字段已有值,则会显示“清除”按钮。它将字段值重置,并允许您再次点击“选择”按钮。

在某些情况下,我们有机会在创建菜单项的过程中直接创建所选类型的实体 - 文章、联系人等。此按钮的工作考虑到了Joomla的ACL - 访问权限的分离。

想象一下您正在构建一个网站并创建一个“联系人”页面。如果您没有复杂的公司分支机构,那么这只是一个“未分类”分类中的普通Joomla文章。它已经以文本或变量形式包含了所有联系人。在古代,您必须首先创建文章,然后转到菜单项并为其创建菜单项。现在您不需要这样做。

Joomla 5 menu item single article select

如果字段已有值,则在某些情况下,可以在创建菜单项的过程中直接编辑所选实体(文章、菜单项等)。

因此,使用模态窗口中的选择字段,我们可以

  • 选择
  • 创建
  • 编辑
  • 清除

这就是我眼前的内容。但在Joomla的深处,还有一个有趣的urlCheckin参数,允许您将选定的值发送到字段中指定的url。值得注意的是,Joomla中这项功能已经逐渐发展了很长时间。然而,一个可以满足您需求的独立通用字段类型,只出现在Joomla 5中。它甚至不在Joomla 4中。

Joomla管理面板界面表单构建器的字段是如何排列的?

之前,这个构建器被称为JForm。我将假设并非所有我的读者都拥有像PHP Storm或VS Code这样的开发工具 - 开发环境。因此,我将尝试提供额外的代码库导航指南。

在Joomla中,逻辑与视图(实际的HTML输出)是分离的,所以我们将在多个地方同时探讨它。

逻辑是表单类

逻辑是表单类。在Joomla 5中,表单类文件位于libraries/src/Form。我们检查这些文件,以理解逻辑本身,了解数据发生了什么,以及如何处理它。

Joomla 5 Form (ex JForm) class file structure

简而言之,表单构建器接收带有字段描述的XML,读取数据(字段类型、来自addfieldprefix属性的定制字段类,如果有的话等),使用FormHelper加载所需的字段类。如果字段有一些过滤输出数据的规则 - 使用FormRule类 - 记住Joomla中文件列表类型的字段,您可以在其中指定过滤参数并选择,例如,只选择php或只选择css文件。

Joomla表单字段类文件位于libraries/src/Form/Field。简而言之,有很多这样的文件。这是管理面板的建筑材料,有时也是前端。

类文件描述了类属性,如$type$layout等,这些属性对于操作是必要的。大多数字段都有getInput()方法 - 实际上返回字段的HTML输出,getLayoutData() - 在发送到渲染器之前对字段数据进行预处理,getLabel() - 处理字段标签等。

我们记得字段类继承了父类FormField。在类文件libraries/src/Form/FormField.php中描述了字段可能具有的属性,可以在XML描述中使用。它们有简短的描述,说明它是什么以及为什么。

joomla 5 form field possible attributes for XML

子类(继承者)有处理父类方法的能力,并在必要时可以覆盖它。

Joomla 5中字段的视图(HTML输出,布局)

每个字段类都有一个HTML输出。在经典MVC中,视图立即处理数据输出,但在Joomla中有一个额外的层 - Layout,这允许您覆盖布局 - 这是该CMS最重要的特性之一。核心布局预计位于站点根目录下的layouts文件夹中。它们传递一个包含从getLayoutData()方法接收到的所有数据的数组$displayData。我们在$layout类属性中指定要使用哪个输出布局。

<?php
/**
 * Name of the layout being used to render the field
 *
 * @var    string
 * @since  3.7
 */
protected $layout = 'joomla.form.field.email';

这种类型的记录相当常见。在Joomla中,布局是到站点根目录下layouts文件夹中布局文件的点分隔路径。也就是说,条目$layout = 'joomla.form.field.email'表示在渲染字段时将使用布局layouts/joomla/form/field/email.php

<?php 
use Joomla\CMS\Layout\LayoutHelper;

$displayData = [
                'src' => $this->item->image,
                'alt' => $this->item->name,
               ];

echo LayoutHelper::render(
                        'joomla.html.image',
                         $displayData
                    );

同样,此示例将使用布局layouts/joomla/html/image.php。一些布局可以在站点模板和行政面板的html文件夹中覆盖。

因此,如果我们想确切地看到最终传递到布局中的数据以及其显示方式,请转到布局文件查看。

在 Joomla 5 的 Modal Select 窗口中创建数据选择字段

现在让我们回到文章的主要任务。

示例对我们进行研究很重要(本文撰写时为 Joomla 5.0.1)

  • 字段的主要类libraries/src/Form/Field/ModalSelectField.php
  • Joomla 文章模态选择字段 - administrator/components/com_content/src/Field/Modal/ArticleField.php
  • 菜单类型模态选择字段 - administrator/components/com_menus/src/Field/MenutypeField.php
  • 菜单项模态选择字段 - administrator/components/com_menus/src/Field/MenutypeField.php
  • 输出布局 - layouts/joomla/form/field/modal-select.php

在撰写本文时,com_contacts 的单个联系模态选择字段尚未转换为通用字段类型,仅位于 administrator/components/com_contact/src/Field/Modal/ContactField.php。它直接继承自 FormField,而不是 ModalSelectField

添加自定义字段的动作算法如下

  • 创建一个包含我们的字段的 XML 表单,在 xml 文件中或使用 \SimpleXMLElement 程序化地。
  • 如果我们现场工作,则使用 onContentPrepareForm 事件的插件,将 XML 表单添加到所需的表单(在那之前检查 $form->getName()
  • 创建字段类。
  • 如有必要,我们创建字段自己的 HTML 输出(布局)。我们将不在此文章中涉及这一点。

它就这样工作了。

字段 XML

在此代码中,最重要的是 addfieldprefix 属性,它表示 字段类的命名空间。类名由 addfieldprefix + "\" + type + "Field" 形成。在这种情况下,字段类将是 Joomla\Plugin\Wtproductbuilder\Providerjoomshopping\Field\ProductlistField

<field
      type="productlist"
      name="product_id"
      addfieldprefix="Joomla\Plugin\Wtproductbuilder\Providerjoomshopping\Field"
      label="Field label"
      hint="Field placeholder"
      />

字段的 HTML 输出(布局)

为了使 PHP 中发生的一切都清楚,首先需要查看字段输出的布局。它在文件 layouts/joomla/form/field/modal-select.php 中。实际上,输出 2 个输入字段 - 一个可见,另一个不可见。选定的文章、联系或产品的标题以占位符的形式输入到可见字段中,即 $valueTitle 参数。另一个是它的 id - $value。如果我们还没有选择任何内容,字段中应该有“选择文章”或“选择产品”这样的短语。这是一个语言常量,我们将其放入 XML 字段的提示属性或字段的 setup() 方法中。

所有可用于输出布局的参数(这意味着可以在程序中或 XML 文件中使用)

<?php
extract($displayData);

/**
 * Layout variables
 * -----------------
 * @var   string   $autocomplete    Autocomplete attribute for the field.
 * @var   boolean  $autofocus       Is autofocus enabled?
 * @var   string   $class           Classes for the input.
 * @var   string   $description     Description of the field.
 * @var   boolean  $disabled        Is this field disabled?
 * @var   string   $group           Group the field belongs to. <fields> section in form XML.
 * @var   boolean  $hidden          Is this field hidden in the form?
 * @var   string   $hint            Placeholder for the field.
 * @var   string   $id              DOM id of the field.
 * @var   string   $label           Label of the field.
 * @var   string   $labelclass      Classes to apply to the label.
 * @var   boolean  $multiple        Does this field support multiple values?
 * @var   string   $name            Name of the input field.
 * @var   string   $onchange        Onchange attribute for the field.
 * @var   string   $onclick         Onclick attribute for the field.
 * @var   string   $pattern         Pattern (Reg Ex) of value of the form field.
 * @var   boolean  $readonly        Is this field read only?
 * @var   boolean  $repeat          Allows extensions to duplicate elements.
 * @var   boolean  $required        Is this field required?
 * @var   integer  $size            Size attribute of the input.
 * @var   boolean  $spellcheck      Spellcheck state for the form field.
 * @var   string   $validate        Validation rules to apply.
 * @var   string   $value           Value attribute of the field.
 * @var   string   $dataAttribute   Miscellaneous data attributes preprocessed for HTML output
 * @var   array    $dataAttributes  Miscellaneous data attribute for eg, data-*
 * @var   string   $valueTitle
 * @var   array    $canDo
 * @var   string[] $urls
 * @var   string[] $modalTitles
 * @var   string[] $buttonIcons
 */

PHP 字段类

正如你可能猜到的,字段类在我的插件中。到达它的方式 plugins/wtproductbuilder/providerjoomshopping/src/Field/ProductlistField.php。我以单个模态文章选择字段为基础,并重新设计它以满足我的需求 - 从 JoomShopping 在线商店中选择产品。我们通过我们的类扩展了父类 ModalSelectField

我的任务仅限于产品选择,编辑和创建不在其中,因此我们在文章中只谈论产品选择。PHP 类很小,我将给出它的全部内容并对其进行注释。

<?php

namespace Joomla\Plugin\Wtproductbuilder\Providerjoomshopping\Field;

use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\ModalSelectField;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Supports a modal article picker.
 *
 * @since  1.6
 */
class ProductlistField extends ModalSelectField
{
	/**
	 * The form field type.
	 *
	 * @var    string
	 * @since  1.6
	 */
	protected $type = 'Productlist';

	/**
	 * Method to attach a Form object to the field.
	 *
	 * @param   \SimpleXMLElement  $element  The SimpleXMLElement object representing the `<field>` tag for the form field object.
	 * @param   mixed              $value    The form field value to validate.
	 * @param   string             $group    The field name group control value.
	 *
	 * @return  boolean  True on success.
	 *
	 * @see     FormField::setup()
	 * @since   5.0.0
	 */
	public function setup(\SimpleXMLElement $element, $value, $group = null)
	{
		
       // Get the field
       $result = parent::setup($element, $value, $group);

		if (!$result)
		{
			return $result;
		}

		$app = Factory::getApplication();

		// We need the Url to get the list of products, 
		// getting an editing form, a creation form
		// entities. We indicate them here.
		// The result of accessing these URLs should return HTML,
		// which will include a small javascript,
		// transmitting the selected values - product id and product name.

      
		$urlSelect = (new Uri())->setPath(Uri::base(true) . '/index.php');
		$urlSelect->setQuery([
			'option'                => 'com_ajax',
			'plugin'                => 'providerjoomshopping',
			'group'                 => 'wtproductbuilder',
			'format'                => 'html',
			'tmpl'                  => 'component',
			Session::getFormToken() => 1,
		]);

		$modalTitle = Text::_('PLG_WTPRODUCTBUILDER_PROVIDERJOOMSHOPPING_MODAL_SELECT_CHOOSE_PRODUCT');
		$this->urls['select'] = (string) $urlSelect;

		// We comment on these lines, they are not needed. In the articles about JavaScript section
		// I’ll tell you why.
		// $wa = $app->getDocument()->getWebAssetManager();
		// $wa->useScript('field.modal-fields')->useScript('core');
		
		// Modal window title
		// To create and edit, respectively, you also need
		// individual headings
		$this->modalTitles['select'] = $modalTitle;

		// hint - the field placeholder in HTML.
		$this->hint = $this->hint ?: Text::_('PLG_WTPRODUCTBUILDER_PROVIDERJOOMSHOPPING_MODAL_SELECT_CHOOSE_PRODUCT');

		return $result;
	}
}

单独引入了 getValueTitle() 方法,用于在已选择并保存所选实体(产品名称、文章标题等)的情况下显示所选实体的名称。也就是说,我们去编辑菜单项,我们不触碰字段,但我们想看到文章标题或产品名称对人们来说是可理解的,而不仅仅是 id。该方法显示所需的标题。

<?php 
    /**
	 * The method shows the name of the selected product in the placeholder field.
	 *
	 * @return string
	 *
	 * @since   5.0.0
	 */
	protected function getValueTitle()
	{
		$value = (int) $this->value ?: ''; // This is a product id or article id or ...
		$title = '';

		if ($value)
		{
			try
			{
				// To get the necessary data, it is best to use
				// methods of the Joomla core API and/or components,
				// and not direct requests to the database.
				$lang = \JSFactory::getLang();
				$name             = $lang->get('name');
				$jshop_product = \JSFactory::getTable('product', 'jshop');
				$jshop_product->load($value);
				$title = $jshop_product->$name;
			}
			catch (\Throwable $e)
			{
				Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');
			}
		}
		return $title ?: $value;
	}

在一些需要更复杂功能(如多语言关联)的字段中,字段类中还有其他方法覆盖了 FormField 类的基本方法

  • setLayoutData() 是一个在实际上渲染字段之前预处理数据的方法
  • getRenderer() - 渲染的附加参数

等等。

在我们的案例中,没有这样的需求,所以我们不使用它们。

模态窗口内容的HTML输出

当您点击“选择”按钮时,会打开一个模态Bootstrap窗口,其中在<iframe>中打开产品列表。通过点击产品名称或JavaScript图片,获取产品ID和名称用于我们的字段。但为了获取该列表本身的HTML,我们必须在某个地方实现它。也就是说,这种结论和功能必须支持组件,然后我们将访问组件URL。或者通过com_ajax访问插件并从那里获取HTML。这就是我们将要做的事情。

Modal window iframe content

在我的插件中,onAjaxProviderjoomshopping()方法返回产品列表的HTML输出。在那里我们遍历包含它们的数组,获取图片、名称并输出。代码量很大,所以我会发布最重要的片段。

简化循环代码示例

<table class="table table-striped table-bordered">
   <thead>
   <tr>
      <th><?php echo Text::_('JSHOP_PRODUCT'); ?></th>
   </tr>
   </thead>
   <tbody>
   <?php foreach ($products as $product): ?>
      <tr>
         <td>
            <b> <?php
               $link_attribs = [
                  'class' => 'select-link',
                  'data-product-id' => $product->product_id,
                  'data-product-title' => htmlspecialchars($product->name),
               ];
               echo HTMLHelper::link(
                  '#',
                  $product->name,
                  $link_attribs
               );?></b>
            <div><?php echo $product->short_description; ?></div>
         </td>
      </tr>
      <?php endforeach; ?>
   </tbody>
</table>

首先,我们在<iframe>中添加我们的javascript,它将监听图片和产品名称的点击。我们将它作为媒体资产添加到插件中,并通过Web资产管理器连接它。

<?php
$app = $this->getApplication();
$doc = $app->getDocument();
$doc->getWebAssetManager()
    ->useScript('core')
    ->registerAndUseScript(
        'wtproductbuilder.providerjoomshopping.modal', 'plg_wtproductbuilder_providerjoomshopping/providerjoomshopping.modal.js'
    );

第二。 链接标签代码必须包含我们需要的数据属性。我们在商品输出循环的示例代码中看到了这个片段。

<?php
use Joomla\CMS\HTML\HTMLHelper;

// this code is executed internally in foreach($products as $product)

$link_attribs = [
    'class' => 'select-link',
    'data-product-id' => $row->product_id,
    'data-product-title' => htmlspecialchars($row->name),
];
echo HTMLHelper::link(
    '#', // url
    $row->name, // Link text
    $link_attribs // link attributes
);

JavaScript处理。从<iframe>发送数据到父窗口中的字段

现在让我们开始使用JavaScript。在撰写文章的过程中,出现了一些细节,允许我们谈论旧的和新的工作方式。

我们还记得,在工作的过程中,我们连接了以下js脚本

  • media/system/js/fields/modal-fields.min.js - 这个文件是在文章选择字段类中连接的。然而,我们现在可以谈论说,这是过时的工作方法。这个文件不再需要。我们在PHP类中将其注释掉了。
  • media/plg_wtproductbuilder_providerjoomshopping/js/providerjoomshopping.modal.js - 我们自己的js文件。

让我们从我们的javascript开始。在这里,使用select-link类,我们获取所有选择器并将点击事件监听器挂载到它们上。

(() => {
    document.addEventListener('DOMContentLoaded', () => {
        // Get the elements

        const product_links = document.querySelectorAll('.select-link');
        // Listen for click event
        product_links.forEach((element) => {
            element.addEventListener('click', event => {
                event.preventDefault();
                const {
                    target
                } = event;

                let data = {
                    'messageType' : 'joomla:content-select',
                    'id' : target.getAttribute('data-product-id'),
                    'title' : target.getAttribute('data-product-title')
                };
                window.parent.postMessage(data);
            });
        });
    });
})();

对于熟悉Joomla的人来说,与idtitle相关的内容是直观的,但对于习惯于使用Joomla的人来说,与数据对象和postMessage相关的内容可能不明显。

就像在Joomla 3和Joomla 4中一样

以前,在Joomla 2.5、3.x甚至在4.x中,使用了以下方法:在字段输出布局中,我们使用内联脚本挂载一个处理函数到窗口上,并从<iframe>调用它,如window.parent[functionName]。看看这个代码

element.addEventListener('click', event => {
      event.preventDefault();
  
      const functionName = event.target.getAttribute('data-function');
  
      if (functionName === 'jSelectMenuItem' && window[functionName]) {
        // Used in xtd_contacts
        window[functionName](event.target.getAttribute('data-id'), event.target.getAttribute('data-title'), event.target.getAttribute('data-uri'), null, null, event.target.getAttribute('data-language'));
      } else if (window.parent[functionName]) {
        // Used in com_menus
        window.parent[functionName](event.target.getAttribute('data-id'), event.target.getAttribute('data-title'), null, null, event.target.getAttribute('data-uri'), event.target.getAttribute('data-language'), null);
      }

在这种情况下,函数名称在文章/联系人/菜单项列表中的每个链接的data-function属性中指定。函数本身内联放置,有时将其名称与额外的id统一。例如,"jSelectArticle_".$this->id。

jSelectArticle()函数或类似的函数(我们会有jSelectProduct())是来自文件modal-fields.min.js的标准processModalSelect()函数的包装器。它反过来调用processModalParent()函数并在执行后关闭模态窗口。

Old joomla processModalSelect method on javascript

该函数需要指定大量参数才能工作:实体的类型(文章、联系人等)、字段的名称前缀(在实践中变成了HTML字段选择器的id)、实际的idtitle——我们需要的参数等。

Old joomla processModalSelect method

一个函数中收集了所有场合所需的一切。这就是我们的字段中放置数据的方式。

在Joomla 5中是如何实现的

然而,现在在Joomla 5中,这个文件不再需要。如果我们使用标准的字段输出布局,那么将连接到它的modal-content-select-field资产,以新的方式工作。

modal content select field asset in joomla 5

现在Joomla 5前端正在切换到使用JavaScript postMessages。由于并非所有旧扩展都准备好切换到新的rails,因此实现了JoomlaExpectingPostMessage标志,这使得您能够区分过时的事件调用方法。它与描述的工作方法间接相关,但可能对某些人有用。这个标志将在完全过渡到postMessages后删除。

JoomlaExpectingPostMessage flag in javascript in Joomla 5

因此,我们现在不需要具有调用函数名称的链接的额外属性。相反,我们使用postMessage机制。为此,在数据对象中,我们需要指定messageType参数等于joomla:content-select。为什么?从JavaScript的角度来看,在Joomla中工作如下

  • 点击链接并获取链接属性
  • 向父窗口发送消息 window.parent.postMessage(data)
  • media/system/js/fields/modal-content-select-field.js 文件在父窗口中连接,该窗口具有消息事件的监听器。
  • 它检查消息类型,如果它是joomla:content-select,则将值放入所需的字段,并关闭模态窗口

joomla javascript postmessages message type joomla content select

在研究Joomla核心代码并寻找解决方案的过程中,我自然遇到了函数jSelectArticle()等。然后我遇到了postMessage并决定通过给它一个长且唯一的名称来创建自己的MessageType。为了使其工作,我为它编写了自己的处理程序,调用(证明是过时的)processModalSelect()函数。我遇到了模态窗口无论如何都不愿意关闭的情况,尽管数据已正确插入到字段中。进一步的搜索首先找到了正确的事件类型,然后是删除不必要的脚本和简化代码的整体。

总结

Joomla为开发者提供了一套丰富的工具,用于处理和从第三方源获取数据,并将其用于您的代码中。当开发人员创建自己的扩展时,与JForm字段一起工作非常重要,尤其是在需要解决超出典型范围的任务时。当然,这样的模态窗口和数据选择是一个相当特殊的情况,但通过这种方式,您可以覆盖任何其他JForm字段,并创建具有您自己的UX逻辑的自定义类型。

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

1
您的Joomla年指南
接近Joomla领导层选举!
 

评论 2

已经注册?在此登录
Harald Leithner于2024年2月7日星期三 14:33
看起来是一篇很好的文章

但为什么它不在https://manual.joomla.net.cn 上?

1
但为什么它不在 https://manual.joomla.net.cn 上?
Sergey Tolkachyov于2024年2月10日星期六 12:14
也许

为了成为官方文档的一部分,我认为这篇文章似乎还需要经过一个额外的适应阶段。

0
为了成为官方文档的一部分,我认为这篇文章似乎还需要经过一个额外的适应阶段。

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