From 3d021d08f690675d87f2be34fdc7d3688faf872e Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 11 Jul 2026 18:22:10 +0800 Subject: [PATCH] feat(shop): add product taxonomy write REST for editor Expose POST /shop/products/taxonomy to create category/tag terms, enrich GET term payload with slug/parent/count, and let ProductEditorDrawer create categories and ensure tags without WP native UI. --- .../shop/components/ProductEditorDrawer.vue | 99 ++++++++++++-- ...min-shop-controller-product-operations.php | 125 ++++++++++++++---- .../class-aksrt-admin-shop-controller.php | 7 + 3 files changed, 198 insertions(+), 33 deletions(-) diff --git a/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue b/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue index 23105d4f..d6a36c2c 100644 --- a/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue +++ b/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue @@ -45,14 +45,26 @@ 分类与标签 - + + + + + 新建分类 + + @@ -62,8 +74,9 @@ multiple filterable tag - placeholder="输入标签后回车" + placeholder="选择或输入新标签后回车" :loading="loadingTaxonomy" + @update:value="onTagsUpdate" /> @@ -164,6 +177,9 @@ const shippingModeOptions = [ const categoryOptions = ref([]) const tagOptions = ref([]) +const newCategoryName = ref('') +const creatingCategory = ref(false) +const creatingTagNames = new Set() // Load taxonomy options async function loadTaxonomy() { @@ -175,12 +191,75 @@ async function loadTaxonomy() { tagOptions.value = (res.data.tags || []).map(t => ({ label: t.name, value: t.name })) } } catch { - // Taxonomy endpoint may not exist yet — silent fallback + message.warning('分类标签加载失败,可稍后重试') } finally { loadingTaxonomy.value = false } } +async function createCategory() { + const name = (newCategoryName.value || '').trim() + if (!name) { + message.warning('请输入分类名称') + return + } + creatingCategory.value = true + try { + const res = await api.request('/shop/products/taxonomy', { + method: 'POST', + data: { taxonomy: 'category', name }, + }) + const term = res?.data + if (term?.id) { + if (!categoryOptions.value.some(o => o.value === term.id)) { + categoryOptions.value = [...categoryOptions.value, { label: term.name, value: term.id }] + } + if (!form.categories.includes(term.id)) { + form.categories = [...form.categories, term.id] + } + newCategoryName.value = '' + message.success(term.existing ? '分类已存在,已选中' : '分类已创建') + } + } catch (err) { + message.error(err?.message || '创建分类失败') + } finally { + creatingCategory.value = false + } +} + +async function ensureTagTerm(name) { + const label = String(name || '').trim() + if (!label || creatingTagNames.has(label)) return + const exists = tagOptions.value.some(o => o.value === label || o.label === label) + if (exists) return + + creatingTagNames.add(label) + try { + const res = await api.request('/shop/products/taxonomy', { + method: 'POST', + data: { taxonomy: 'tag', name: label }, + }) + const term = res?.data + if (term?.name && !tagOptions.value.some(o => o.value === term.name)) { + tagOptions.value = [...tagOptions.value, { label: term.name, value: term.name }] + } + } catch { + // Keep typed tag in form; backend product save still accepts string tags. + } finally { + creatingTagNames.delete(label) + } +} + +function onTagsUpdate(values) { + const list = Array.isArray(values) ? values : [] + form.tags = list + list.forEach((name) => { + if (!tagOptions.value.some(o => o.value === name)) { + ensureTagTerm(name) + } + }) +} + // Load product data when editing watch(() => props.show, async (val) => { if (val) { diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php b/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php index 7208a479..3f149a9f 100644 --- a/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php +++ b/inc/classes/admin/class-aksrt-admin-shop-controller-product-operations.php @@ -47,35 +47,114 @@ trait AKSRT_Admin_Shop_Controller_Product_Operations { } public function get_product_taxonomy(): WP_REST_Response { - $format_terms = static function (string $taxonomy): array { - if (!taxonomy_exists($taxonomy)) { - return []; - } - - $terms = get_terms([ - 'taxonomy' => $taxonomy, - 'hide_empty' => false, - ]); - if (is_wp_error($terms)) { - return []; - } - - return array_map(static function (WP_Term $term): array { - return [ - 'id' => $term->term_id, - 'name' => $term->name, - ]; - }, $terms); - }; - return $this->success([ 'data' => [ - 'categories' => $format_terms('aksrt_product_cat'), - 'tags' => $format_terms('aksrt_product_tag'), + 'categories' => $this->format_product_terms('aksrt_product_cat'), + 'tags' => $this->format_product_terms('aksrt_product_tag'), ], ]); } + /** + * POST /shop/products/taxonomy — create category or tag term. + * + * Body: { taxonomy: 'category'|'tag'|'aksrt_product_cat'|'aksrt_product_tag', name: string, parent?: int } + */ + public function create_product_taxonomy_term(WP_REST_Request $request): WP_REST_Response { + $params = $this->get_json_params($request); + $raw_tax = sanitize_key((string) ($params['taxonomy'] ?? 'category')); + $name = sanitize_text_field((string) ($params['name'] ?? '')); + $parent = max(0, (int) ($params['parent'] ?? 0)); + + $taxonomy_map = [ + 'category' => 'aksrt_product_cat', + 'tag' => 'aksrt_product_tag', + 'aksrt_product_cat' => 'aksrt_product_cat', + 'aksrt_product_tag' => 'aksrt_product_tag', + ]; + $taxonomy = $taxonomy_map[$raw_tax] ?? ''; + + if ($taxonomy === '' || !taxonomy_exists($taxonomy)) { + return $this->error(__('无效的分类法', 'aksrt')); + } + if ($name === '') { + return $this->error(__('名称不能为空', 'aksrt')); + } + + $existing = term_exists($name, $taxonomy, $taxonomy === 'aksrt_product_cat' ? $parent : 0); + if (is_array($existing) && !empty($existing['term_id'])) { + $term = get_term((int) $existing['term_id'], $taxonomy); + if ($term && !is_wp_error($term)) { + return $this->success([ + 'message' => __('术语已存在', 'aksrt'), + 'data' => [ + 'id' => (int) $term->term_id, + 'name' => (string) $term->name, + 'slug' => (string) $term->slug, + 'parent' => (int) $term->parent, + 'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag', + 'existing' => true, + ], + ]); + } + } + + $args = []; + if ($taxonomy === 'aksrt_product_cat' && $parent > 0) { + $args['parent'] = $parent; + } + + $result = wp_insert_term($name, $taxonomy, $args); + if (is_wp_error($result)) { + return $this->error($result->get_error_message()); + } + + $term_id = (int) ($result['term_id'] ?? 0); + $term = get_term($term_id, $taxonomy); + if (!$term || is_wp_error($term)) { + return $this->error(__('创建失败', 'aksrt')); + } + + return $this->success([ + 'message' => __('术语已创建', 'aksrt'), + 'data' => [ + 'id' => (int) $term->term_id, + 'name' => (string) $term->name, + 'slug' => (string) $term->slug, + 'parent' => (int) $term->parent, + 'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag', + 'existing' => false, + ], + ], 201); + } + + /** + * @return array + */ + private function format_product_terms(string $taxonomy): array { + if (!taxonomy_exists($taxonomy)) { + return []; + } + + $terms = get_terms([ + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + ]); + if (is_wp_error($terms) || !is_array($terms)) { + return []; + } + + return array_map(static function (WP_Term $term): array { + return [ + 'id' => (int) $term->term_id, + 'name' => (string) $term->name, + 'slug' => (string) $term->slug, + 'parent' => (int) $term->parent, + 'count' => (int) $term->count, + ]; + }, $terms); + } + /** * GET /shop/products — 商品列表,支持 search、status、page、per_page */ diff --git a/inc/classes/admin/class-aksrt-admin-shop-controller.php b/inc/classes/admin/class-aksrt-admin-shop-controller.php index 2232e69f..2e0d9ab8 100644 --- a/inc/classes/admin/class-aksrt-admin-shop-controller.php +++ b/inc/classes/admin/class-aksrt-admin-shop-controller.php @@ -175,6 +175,13 @@ class AKSRT_Admin_Shop_Controller extends AKSRT_Admin_Controller_Base { 'permission_callback' => [$this, 'check_permission'], ]); + // Create product category or tag term for the product editor. + register_rest_route(self::API_NAMESPACE, '/shop/products/taxonomy', [ + 'methods' => 'POST', + 'callback' => [$this, 'create_product_taxonomy_term'], + 'permission_callback' => [$this, 'check_permission'], + ]); + // Product list register_rest_route(self::API_NAMESPACE, '/shop/products', [ 'methods' => 'GET',