😍 Dự án Laravel bán hàng martfury dùng Botble quá đỉnh (ok)
Api sử dụng
<?php
use Botble\Blog\Models\Category;
use Botble\Blog\Models\Post;
use Botble\Page\Models\Page;
use Botble\Slug\Facades\SlugHelper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Botble\Slug\Models\Slug;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Botble\Contact\Models\Contact;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
Route::get('/pages', function (Request $request) {
$posts = Page::where('status', 'published')
->orderBy('created_at', 'desc')
->select('id', 'name', 'created_at') // tuỳ chọn field
->paginate(10); // hoặc ->get() nếu không phân trang
return response()->json([
'data' => $posts,
]);
});
Route::get('/posts', function (Request $request) {
// Lấy ID chuyên mục 'human' từ bảng slugs
$excludedCategoryId = Slug::where('key', 'human')
->where('reference_type', Category::class)
->value('reference_id');
// Nếu không có chuyên mục "human", trả về bài viết bình thường
if (!$excludedCategoryId) {
$posts = Post::where('status', 'published')
->orderBy('created_at', 'desc')
->select('id', 'name', 'description', 'content', 'is_featured', 'image', 'created_at')
->paginate(10);
$posts->getCollection()->transform(function ($post) {
$slug = DB::table('slugs')
->where('reference_type', Post::class)
->where('reference_id', $post->id)
->value('key');
$post->slug = $slug;
return $post;
});
return response()->json(['data' => $posts]);
}
// Lấy danh sách post_id thuộc chuyên mục human
$excludedPostIds = DB::table('post_categories')
->where('category_id', $excludedCategoryId)
->pluck('post_id');
// Lấy các bài viết KHÔNG thuộc chuyên mục đó
$posts = Post::where('status', 'published')
->whereNotIn('id', $excludedPostIds)
->orderBy('created_at', 'desc')
->select('id', 'name', 'description', 'content', 'is_featured', 'image', 'created_at')
->paginate(10);
// Gắn slug cho bài viết
$posts->getCollection()->transform(function ($post) {
$slug = DB::table('slugs')
->where('reference_type', Post::class)
->where('reference_id', $post->id)
->value('key');
$post->slug = $slug;
return $post;
});
return response()->json(['data' => $posts]);
});
Route::get('/pages/{slug}', function ($slug) {
// Tìm slug từ bảng `slugs`
$slugRecord = Slug::where('key', $slug)
->where('reference_type', Page::class)
->first();
if (! $slugRecord) {
return response()->json(['message' => 'Page not found'], 404);
}
// Tìm page từ reference_id
$page = Page::where('id', $slugRecord->reference_id)
->where('status', 'published')
->first();
if (! $page) {
return response()->json(['message' => 'Page not found'], 404);
}
return response()->json([
'data' => [
'id' => $page->id,
'name' => $page->name,
'description' => $page->description,
'slug' => $slug,
'content' => $page->content,
'custom_fields' => get_custom_field($page),
]
]);
});
Route::get('/posts/{slug}/related', function ($slug) {
$slugRecord = Slug::where('key', $slug)
->where('reference_type', Post::class)
->first();
if (! $slugRecord) {
return response()->json(['message' => 'Post not found'], 404);
}
// Lấy bài viết hiện tại và chuyên mục của nó
$post = Post::with('categories')
->where('id', $slugRecord->reference_id)
->where('status', 'published')
->first();
if (! $post) {
return response()->json(['message' => 'Post not found'], 404);
}
// Lấy các ID chuyên mục của bài viết
$categoryIds = $post->categories->pluck('id');
// Tìm bài viết liên quan (cùng chuyên mục, không trùng chính nó)
$relatedPosts = Post::where('status', 'published')
->where('id', '!=', $post->id)
->whereHas('categories', function ($query) use ($categoryIds) {
$query->whereIn('categories.id', $categoryIds);
})
->orderBy('created_at', 'desc')
->take(6)
// ->select('id', 'name', 'slug', 'description', 'image', 'created_at')
->get();
$relatedPosts->transform(function ($post) {
$slug = DB::table('slugs')
->where('reference_type', Post::class)
->where('reference_id', $post->id)
->value('key');
$post->slug = $slug;
return $post;
});
return response()->json([
'data' => $relatedPosts,
]);
});
Route::get('/posts/{slug}', function ($slug) {
// Tìm slug từ bảng `slugs`
$slugRecord = Slug::where('key', $slug)
->where('reference_type', Post::class)
->first();
if (! $slugRecord) {
return response()->json(['message' => 'Page not found'], 404);
}
// Tìm page từ reference_id
$post = Post::where('id', $slugRecord->reference_id)
->where('status', 'published')
->first();
if (! $post) {
return response()->json(['message' => 'Page not found'], 404);
}
return response()->json([
'data' => [
'id' => $post->id,
'name' => $post->name,
'slug' => $slug,
'description' => $post->description,
'is_featured' => $post->is_featured,
'image' => $post->image,
'created_at' => $post->created_at,
'content' => $post->content,
'categories' => $post->categories->map(function ($category) {
return [
'id' => $category->id,
'name' => $category->name,
'slug' => $category->slug,
];
}),
]
]);
});
Route::get('/categories/{slug}/posts', function ($slug, Request $request) {
// Tìm chuyên mục theo slug
$category = DB::table('slugs')
->where('key', $slug)
->where('reference_type', Category::class)
->first();
$slugRecord = DB::table('slugs')
->where('key', $slug)
->where('reference_type', Category::class)
->first();
$categorys = Category::select('name', 'description')
->where('id', $slugRecord->reference_id)
->first();
// Lấy bài viết thuộc category
$categoryId = $category->reference_id;
$posts = Post::where('status', 'published')
->whereHas('categories', function ($query) use ($categoryId) {
$query->where('categories.id', $categoryId);
})
->orderBy('created_at', 'desc')
->select('id', 'name', 'description', 'content', 'is_featured', 'image', 'created_at')
->paginate(10);
// Map slug cho từng bài viết
$posts->getCollection()->transform(function ($post) {
$slug = DB::table('slugs')
->where('reference_type', Post::class)
->where('reference_id', $post->id)
->value('key');
$post->slug = $slug;
if (!$post->custom_fields) {
$post->custom_fields = get_custom_field($post);
}
return $post;
});
return response()->json([
'data' => $posts,
'category' => $categorys
]);
});
// Route::post('/contactforconsultation', function (\Illuminate\Http\Request $request) {
// \Botble\Contact\Models\Contact::create([
// 'name' => $request->input('yourName') ?? $request->input('position'),
// 'email' => $request->input('email'),
// 'phone' => $request->input('phone'),
// 'content' => $request->input('content') ?? $request->input('position'),
// 'address' => $request->input('address') ?? $request->input('cvfilename'),
// 'subject' => $request->input('subject') ?? $request->input('taxcode')
// ]);
// return response()->json(['message' => 'Gửi thành công.']);
// });
Route::post('/contactforconsultation', function (Request $request) {
// Validate the request data (highly recommended)
$request->validate([
'yourName' => 'nullable|string|max:255',
'email' => 'required|email|max:255',
'phone' => 'required|string|max:20',
'content' => 'nullable|string',
'taxcode' => 'nullable|string|max:50',
'companyProfile' => 'nullable|file|mimes:pdf,ppt,pptx,doc,docx,zip|max:5120', // Max 5MB (5120 KB)
'cvfilename' => 'nullable|string|max:255', // Để lấy tên file từ frontend
]);
$companyProfilePath = null;
if ($request->hasFile('companyProfile')) {
$file = $request->file('companyProfile');
// Lưu file vào thư mục 'public/company_profiles'
// 'public' là một disk mặc định trong Laravel, mapping tới storage/app/public
// Đảm bảo bạn đã chạy `php artisan storage:link` để tạo symlink public/storage
$companyProfilePath = $file->store('company_profiles', 'public');
// $companyProfilePath sẽ chứa đường dẫn tương đối từ thư mục lưu trữ, ví dụ: 'company_profiles/abcxyz.pdf'
}
Contact::create([
'name' => $request->input('yourName') ?? $request->input('companyName'), // Thêm companyName nếu bạn gửi từ frontend
'email' => $request->input('email'),
'phone' => $request->input('phone'),
'content' => $request->input('content'), // Giữ nguyên 'content' từ frontend
// Lưu đường dẫn file vào trường 'address' hoặc tạo một cột mới nếu muốn rõ ràng hơn
'address' => $companyProfilePath ? Storage::url($companyProfilePath) : null, // Lưu URL công khai
'subject' => $request->input('subject') ?? $request->input('taxcode')
]);
return response()->json(['message' => 'Gửi thành công.']);
});
Route::post('/humanresource', function (Request $request) {
// Validate the request data (highly recommended)
$request->validate([
'yourName' => 'required|string|max:255', // Đổi nullable thành required
'email' => 'required|email|max:255',
'phone' => 'required|string|max:20',
'subject' => 'nullable|string|max:255', // Từ form: "Ứng tuyển vị trí: ..."
'taxcode' => 'nullable|string|max:50', // Từ form: "Ứng tuyển"
'position' => 'required|string|max:255', // Thêm trường position
'companyProfile' => 'required|file|mimes:pdf,doc,docx,zip|max:5120', // Đổi nullable thành required, mimes chuẩn hơn
'cvfilename' => 'nullable|string|max:255', // Tên file gốc từ frontend
]);
$companyProfilePath = null;
$publicFilePath = null; // Để lưu URL công khai
if ($request->hasFile('companyProfile')) {
$file = $request->file('companyProfile');
// Lưu file vào thư mục 'public/cv_applications' (thay đổi thư mục cho rõ ràng hơn)
$companyProfilePath = $file->store('cv_applications', 'public');
$publicFilePath = Storage::url($companyProfilePath); // Lấy URL công khai
}
// Tạo nội dung cho trường 'content' để dễ đọc trong Botble
$content = "Họ và tên: " . $request->input('yourName') . "\n" .
"Số điện thoại: " . $request->input('phone') . "\n" .
"Email: " . $request->input('email') . "\n" .
"Vị trí ứng tuyển: " . $request->input('position') . "\n";
if ($publicFilePath) {
$content .= "Link CV: " . $publicFilePath . "\n";
}
Contact::create([
'name' => $request->input('yourName'),
'email' => $request->input('email'),
'phone' => $request->input('phone'),
'content' => $content, // Sử dụng content đã tạo
'address' => $publicFilePath, // Lưu trực tiếp URL của CV vào trường address
'subject' => $request->input('subject'),
// Nếu bạn muốn lưu tên file gốc vào một trường khác, bạn cần thêm cột vào DB
// 'cv_original_name' => $request->input('cvfilename'),
]);
return response()->json(['message' => 'Gửi hồ sơ ứng tuyển thành công.']);
});
Source code 1



User: phamkyanh8668@gmail.com Password: 12345678
Hoặc đã tải lên google driver https://drive.google.com/file/d/1drNl9H6GY9OebkuNUPovTikMxxpfpB2M/view?usp=sharing
Sử dụng plugin custom-field để thêm nội dung vào bài viết, chuyên mục, trang ...

Cách cài đặt plugin & theme


Download plugin https://github.com/buzz-space/dev3map-backend/tree/prod/platform/plugins/custom-field rồi giải nén ra vào thư mục

Bước 1: Để active plugin chúng ra dùng lệnh php artisan plugin:publish nó gợi ý ở dưới

Bước 2: Dùng lệnh php artisan cms:plugin:activate sau đó nó sẽ hỏi plugin cần actate

👌 Cần thiết sau khi cài sử dụng composer dump-autoload để cài đặt lại

Bước 3: Sau khi cài xong nó xuất hiện trong back-end như sau.


Bước 4: Hướng dẫn sử dụng get_field trong platform\plugins\custom-field\helpers\front.php để lấy dữ liệu
platform\plugins\custom-field\helpers\front.php
<?php
use Botble\Base\Models\BaseModel;
use Botble\CustomField\Facades\CustomField;
if (! function_exists('get_field')) {
/**
* @deprecated since v5.17
*/
function get_field(BaseModel $data, string $key = null, string|array $default = null): string|array|null
{
return CustomField::getField($data, $key, $default);
}
}
if (! function_exists('has_field')) {
/**
* @deprecated since v5.17
*/
function has_field(BaseModel $data, $key = null): bool
{
return ! empty(CustomField::getField($data, $key));
}
}
if (! function_exists('get_sub_field')) {
function get_sub_field(array $parentField, string $key, string|array $default = null): string|array|null
{
return CustomField::getChildField($parentField, $key, $default);
}
}
if (! function_exists('has_sub_field')) {
/**
* @deprecated since v5.17
*/
function has_sub_field(array $parentField, string $key): bool
{
return ! empty(CustomField::getChildField($parentField, $key));
}
}
Ví dụ:
Viết api lấy tất cả các trang và sau đó lấy bài viết chi tiết
routes\api.php
<?php
use Botble\Page\Models\Page;
// use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
// use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
Route::get('/pages', function (Request $request) {
$posts = Page::where('status', 'published')
->orderBy('created_at', 'desc')
->select('id', 'name', 'created_at') // tuỳ chọn field
->paginate(10); // hoặc ->get() nếu không phân trang
return response()->json([
'data' => $posts,
]);
});
Route::get('/pages/{id}', function ($id) {
$page = Page::where('id', $id)
->where('status', 'published')
->first();
if (!$page) {
return response()->json([
'message' => 'Page not found',
], 404);
}
return response()->json([
'data' => [
'id' => $page->id,
'name' => $page->name,
'slug' => $page->slug,
'content' => $page->content,
'created_at' => $page->created_at,
'custom_fields' => get_field($page),
'custom_fields' => get_field($page,'get_custom_fields_name'),
]
]);
});
— Cách lấy get_field
https://github.com/codezin/beeart_shopwise/tree/main
— Viêt thêm hàm get_page_by_id
có source code để tham khảo thêm cách hàm khác
platform\plugins\blog\helpers\helpers.php
<?php
use Botble\Base\Enums\BaseStatusEnum;
use Botble\Base\Models\BaseModel;
use Botble\Base\Supports\SortItemsWithChildrenHelper;
use Botble\Blog\Repositories\Interfaces\CategoryInterface;
use Botble\Blog\Repositories\Interfaces\PostInterface;
use Botble\Blog\Repositories\Interfaces\TagInterface;
use Botble\Blog\Supports\PostFormat;
use Botble\Page\Repositories\Interfaces\PageInterface;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
if (! function_exists('get_featured_posts')) {
function get_featured_posts(int $limit, array $with = []): Collection
{
return app(PostInterface::class)->getFeatured($limit, $with);
}
}
if (! function_exists('get_latest_posts')) {
function get_latest_posts(int $limit, array $excepts = [], array $with = []): Collection
{
return app(PostInterface::class)->getListPostNonInList($excepts, $limit, $with);
}
}
if (! function_exists('get_related_posts')) {
function get_related_posts(int|string $id, int $limit): Collection
{
return app(PostInterface::class)->getRelated($id, $limit);
}
}
if (! function_exists('get_posts_by_category')) {
function get_posts_by_category(int|string $categoryId, int $paginate = 12, int $limit = 0): Collection|LengthAwarePaginator
{
return app(PostInterface::class)->getByCategory($categoryId, $paginate, $limit);
}
}
if (! function_exists('get_posts_by_tag')) {
function get_posts_by_tag(string $slug, int $paginate = 12): Collection|LengthAwarePaginator
{
return app(PostInterface::class)->getByTag($slug, $paginate);
}
}
if (! function_exists('get_posts_by_user')) {
function get_posts_by_user(int|string $authorId, int $paginate = 12): Collection|LengthAwarePaginator
{
return app(PostInterface::class)->getByUserId($authorId, $paginate);
}
}
if (! function_exists('get_all_posts')) {
function get_all_posts(
bool $active = true,
int $perPage = 12,
array $with = ['slugable', 'categories', 'categories.slugable', 'author']
): Collection|LengthAwarePaginator {
return app(PostInterface::class)->getAllPosts($perPage, $active, $with);
}
}
if (! function_exists('get_recent_posts')) {
function get_recent_posts(int $limit): Collection|LengthAwarePaginator
{
return app(PostInterface::class)->getRecentPosts($limit);
}
}
if (! function_exists('get_featured_categories')) {
function get_featured_categories(int $limit, array $with = []): Collection|LengthAwarePaginator
{
return app(CategoryInterface::class)->getFeaturedCategories($limit, $with);
}
}
if (! function_exists('get_all_categories')) {
function get_all_categories(array $condition = [], array $with = []): Collection|LengthAwarePaginator
{
return app(CategoryInterface::class)->getAllCategories($condition, $with);
}
}
if (! function_exists('get_all_tags')) {
function get_all_tags(bool $active = true): Collection|LengthAwarePaginator
{
return app(TagInterface::class)->getAllTags($active);
}
}
if (! function_exists('get_popular_tags')) {
function get_popular_tags(
int $limit = 10,
array $with = ['slugable'],
array $withCount = ['posts']
): Collection|LengthAwarePaginator {
return app(TagInterface::class)->getPopularTags($limit, $with, $withCount);
}
}
if (! function_exists('get_popular_posts')) {
function get_popular_posts(int $limit = 10, array $args = []): Collection|LengthAwarePaginator
{
return app(PostInterface::class)->getPopularPosts($limit, $args);
}
}
if (! function_exists('get_popular_categories')) {
function get_popular_categories(
int $limit = 10,
array $with = ['slugable'],
array $withCount = ['posts']
): Collection|LengthAwarePaginator {
return app(CategoryInterface::class)->getPopularCategories($limit, $with, $withCount);
}
}
if (! function_exists('get_category_by_id')) {
function get_category_by_id(int|string $id): ?BaseModel
{
return app(CategoryInterface::class)->getCategoryById($id);
}
}
if (! function_exists('get_categories')) {
function get_categories(array $args = []): array
{
$indent = Arr::get($args, 'indent', '——');
$repo = app(CategoryInterface::class);
$categories = $repo->getCategories(Arr::get($args, 'select', ['*']), [
'created_at' => 'DESC',
'is_default' => 'DESC',
'order' => 'ASC',
], Arr::get($args, 'condition', ['status' => BaseStatusEnum::PUBLISHED]));
$categories = sort_item_with_children($categories);
foreach ($categories as $category) {
$depth = (int)$category->depth;
$indentText = str_repeat($indent, $depth);
$category->indent_text = $indentText;
}
return $categories;
}
}
if (! function_exists('get_categories_with_children')) {
function get_categories_with_children(): array
{
$categories = app(CategoryInterface::class)
->getAllCategoriesWithChildren(['status' => BaseStatusEnum::PUBLISHED], [], ['id', 'name', 'parent_id']);
return app(SortItemsWithChildrenHelper::class)
->setChildrenProperty('child_cats')
->setItems($categories)
->sort();
}
}
if (! function_exists('register_post_format')) {
function register_post_format(array $formats): void
{
PostFormat::registerPostFormat($formats);
}
}
if (! function_exists('get_post_formats')) {
function get_post_formats(bool $toArray = false): array
{
return PostFormat::getPostFormats($toArray);
}
}
if (! function_exists('get_blog_page_id')) {
function get_blog_page_id(): string|null
{
return theme_option('blog_page_id', setting('blog_page_id'));
}
}
if (! function_exists('get_blog_page_url')) {
function get_blog_page_url(): string
{
$blogPageId = (int)theme_option('blog_page_id', setting('blog_page_id'));
if (! $blogPageId) {
return route('public.index');
}
$blogPage = app(PageInterface::class)->findById($blogPageId);
if (! $blogPage) {
return route('public.index');
}
return $blogPage->url;
}
}
platform\packages\page\helpers\helpers.php
<?php
use Botble\Page\Repositories\Interfaces\PageInterface;
use Botble\Page\Supports\Template;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
if (! function_exists('get_all_pages')) {
function get_all_pages(bool $active = true): Collection
{
return app(PageInterface::class)->getAllPages($active);
}
}
if (! function_exists('register_page_template')) {
function register_page_template(array $templates): void
{
Template::registerPageTemplate($templates);
}
}
if (! function_exists('get_page_templates')) {
function get_page_templates(): array
{
return Template::getPageTemplates();
}
}
if (!function_exists('get_page_by_id')) {
/**
* @param array $condition
* @return array
*/
function get_page_by_id($slug, $lang = "en_US")
{
if ($lang == "en_US") {
$query = new Botble\Page\Models\Page();
$query = $query->where('id', $slug);
} else {
$query = DB::table("pages_translations");
$query = $query->where('pages_id', $slug);
}
// $query = $query->where('id', $slug)->where("lang_code","like", ($lang??"en_US")."%");
// $query->join("pages_translations", 'pages_translations.pages_id', '=', 'pages.id');
$page = $query->first();
return $page;
}
}
Chú ý: mặc định nó sẽ lấy cái đầu tiên

platform\themes\martfury\layouts\coming-soon.blade.php
Nếu viết thế này nó sẽ lấy id = 1
@php
$page = get_page_by_id(12);
$mission = get_field($page);
@endphp
{{
dd($mission);
}}
Nếu viết thế này nó lấy chính sác id = 6
@php
$page = get_page_by_id(12);
$mission = get_field($page,'abc');
@endphp
{{
dd($mission);
}}

— Cách Lấy get_sub_field




Kết quản thật đáng mong đợi

platform\themes\martfury\layouts\coming-soon.blade.php
@php
$page = get_page_by_id(12);
$subfields = get_field($page,'subfield');
if ($subfields) {
foreach ($subfields as $item) {
$subfield_1 = get_sub_field($item, 'subfield_1');
echo "<h3>{$subfield_1}</h3>";
}
}
@endphp
{{dd($subfields)}}
Hàng loạt các theme để nghiên cứu
'get_field' is deprecated.
C:\xampp82\htdocs\lva3\platform\themes
'get_field' is deprecated. được hướng dẫn sử dụng getMetaData để thay thế
vendor\botble\platform\base\src\Models\Concerns\HasMetadata.php
Route::get('/pages/{id}', function ($id) {
$page = Page::where('id', $id)
->where('status', 'published')
->first();
if (!$page) {
return response()->json([
'message' => 'Page not found',
], 404);
}
return response()->json([
'data' => [
'id' => $page->id,
'name' => $page->name,
'slug' => $page->slug,
'content' => $page->content,
'created_at' => $page->created_at,
// 'custom_fields' => get_field($page),
// 'custom_fields' => get_field($page, 'get_custom_fields_name'),
'custom_fields' => [
// Trả về toàn bộ custom fields
'all' => $page->getMetaData('get_custom_fields_name', false),
// Trả về một custom field cụ thể, ví dụ 'get_custom_fields_name'
// 'get_custom_fields_name' => $page->getMetaData('get_custom_fields_name', true),
],
]
]);
});
Có cần thêm HasMetadata vào bộ điều khiển của Trang không?
ChatGPT cho biết:
Không cần thêm HasMetaData
vào c.
Tuy nhiên, bạn cần đảm bảo mô hìnhPage
có sử dụngHasMetaBoxes
hoHasMetaData
đ$page->getMetaData()
.
✅ Kiểm tra cách và bổ sung các đặc điểm đúng
Mở file Page
mod
app/Models/Page.php
(platform/packages/page/models/Page.php
tùy chọn vào
Đảm bảo có dòng sau trong cl
phpSao chépBiên tậpuse Botble\Base\Traits\MetaBoxable;
class Page extends BaseModel
{
use MetaBoxable;
// ...
}
Giải th
MetaBoxable
là tragetMetaData()
vàsetMetaData()
vàoBạn không cần thêm bất cứ điều gì vào bộ điều khiển, vì tiếp tục
$page->getMetaData(...)
nếu
🔎 Kiểm tra n
Nế
phpSao chépBiên tập$page->getMetaData();
Thì bạn đã có đặc điểm đúng. If failed Call to undefined method
, thphải thêm trait MetaBoxable
vào m.
Bạn có cần tôi kiểm tra hoặc viết lại toàn bộ Page
mô hình cấu trúc có các trường tùy chỉnh được hỗ trợ không?
Last updated
Was this helpful?