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',