From 69945b87495ab09e1628e6554c3f4e4d22b91c26 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Thu, 25 Jun 2026 16:10:56 +0800 Subject: [PATCH] feat(tags): support separate font color for tag chips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tagFontColor column to tags table (Prisma db push) - Update Create/UpdateTagDto, TagsService, PublicTagDto, GoodDto, OriginGoodsService - Expose tagFontColor in PublicGoodDto and backend product mapping - Admin TagsView: add 背景色/字体色 color pickers, separate table columns, live preview - Admin GoodsView inline tag dialog: add 字体色 picker alongside background color - Add product-category.html design reference --- inkreach-official-admin/src/types/index.ts | 5 +- .../src/views/goods/GoodsView.vue | 161 ++- .../src/views/tags/TagsView.vue | 37 +- inkreach-official-nestjs/prisma/schema.prisma | 13 +- .../src/goods/dto/good.dto.ts | 10 +- .../src/origin-goods/origin-goods.service.ts | 8 +- .../src/public/dto/public-good.dto.ts | 4 +- .../src/public/dto/public-query-good.dto.ts | 7 +- .../src/public/dto/public-tag.dto.ts | 25 + .../src/public/public.controller.ts | 7 + .../src/public/public.service.ts | 57 +- .../src/tags/dto/create-tag.dto.ts | 10 + .../src/tags/dto/update-tag.dto.ts | 5 + .../src/tags/tags.service.ts | 2 + inkreach-official-website | 2 +- product-category.html | 1022 +++++++++++++++++ 16 files changed, 1278 insertions(+), 97 deletions(-) create mode 100644 inkreach-official-nestjs/src/public/dto/public-tag.dto.ts create mode 100644 product-category.html diff --git a/inkreach-official-admin/src/types/index.ts b/inkreach-official-admin/src/types/index.ts index 6973167..36776b8 100644 --- a/inkreach-official-admin/src/types/index.ts +++ b/inkreach-official-admin/src/types/index.ts @@ -141,6 +141,7 @@ export interface Tag { id: string tagName: string tagColor?: string | null + tagFontColor?: string | null timing?: string | null createdAt: string updatedAt: string @@ -149,12 +150,14 @@ export interface Tag { export interface CreateTagRequest { tagName: string tagColor?: string + tagFontColor?: string timing?: string } export interface UpdateTagRequest { tagName?: string tagColor?: string | null + tagFontColor?: string | null timing?: string | null } @@ -203,7 +206,7 @@ export interface OriginGoodsTreeNode { sdsGoodId: string configuredCount: number configuredCountries: string[] - configuredTags: { tagName: string; tagColor: string | null }[] + configuredTags: { tagName: string; tagColor: string | null; tagFontColor: string | null }[] } export interface OriginGoodsTreeCategoryNode { diff --git a/inkreach-official-admin/src/views/goods/GoodsView.vue b/inkreach-official-admin/src/views/goods/GoodsView.vue index ab250fb..6838566 100644 --- a/inkreach-official-admin/src/views/goods/GoodsView.vue +++ b/inkreach-official-admin/src/views/goods/GoodsView.vue @@ -21,7 +21,6 @@ const loading = ref(false) const leftTreeRef = ref() const rightTreeRef = ref() -const leftTreeData = ref([]) const rightTreeData = ref([]) const allCategories = ref([]) const allCountries = ref([]) @@ -99,7 +98,6 @@ async function loadAll() { allCountries.value = Array.isArray(countries) ? countries : (countries.items ?? []) allTags.value = Array.isArray(tags) ? tags : (tags.items ?? []) allGoods.value = goodsRes?.items ?? [] - buildLeftTree() buildRightTree(ogTree) } catch (e) { console.error('加载失败', e) } finally { loading.value = false } @@ -125,24 +123,23 @@ function goodToNode(g: Good): any { } } -function buildLeftTree() { - if (mode.value === 'category') { - leftTreeData.value = mapCatNodes(allCategories.value, allGoods.value) - } else { - leftTreeData.value = allCountries.value.map(c => { - const goods = allGoods.value.filter(g => g.countryId === c.id) - return { - id: c.id, - label: c.countryName, - icon: c.countryIcon, - isCat: true, - raw: c, - goodCount: goods.length, - children: goods.map(goodToNode), - } +// Computed: goods after applying search + country + tag filters +const filteredGoods = computed(() => { + let result = allGoods.value + if (searchKeyword.value) { + result = result.filter(g => g.goodName?.includes(searchKeyword.value)) + } + if (selectedCountryIds.value.length) { + result = result.filter(g => selectedCountryIds.value.includes(g.countryId)) + } + if (selectedTagIds.value.length) { + result = result.filter(g => { + const ids = (g.tags || []).map(t => t.id) + return selectedTagIds.value.some(id => ids.includes(id)) }) } -} + return result +}) function mapCatNodes(nodes: CategoryTree[], goods: Good[]): any[] { return nodes.map(n => { @@ -162,6 +159,27 @@ function mapCatNodes(nodes: CategoryTree[], goods: Good[]): any[] { }) } +// Computed: left tree data — auto-rebuilds when any dependency changes +const leftTreeData = computed(() => { + const goods = filteredGoods.value + if (mode.value === 'category') { + return mapCatNodes(allCategories.value, goods) + } else { + return allCountries.value.map(c => { + const cGoods = goods.filter(g => g.countryId === c.id) + return { + id: c.id, + label: c.countryName, + icon: c.countryIcon, + isCat: true, + raw: c, + goodCount: cGoods.length, + children: cGoods.map(goodToNode), + } + }) + } +}) + function buildRightTree(tree: OriginGoodsTreeResponse) { function mapCat(node: any): any { const children = (node.children || []).map(mapCat) @@ -184,26 +202,13 @@ function buildRightTree(tree: OriginGoodsTreeResponse) { rightTreeData.value = tree.tree.map(mapCat) } -function leftFilterNode(value: string, data: any) { - if (!data.isGood) return true - if (searchKeyword.value && !data.goodName?.includes(searchKeyword.value)) return false - if (selectedCountryIds.value.length && !selectedCountryIds.value.includes(data.raw?.countryId)) return false - if (selectedTagIds.value.length) { - const goodTagIds = (data.tags || []).map((t: any) => t.id) - if (!selectedTagIds.value.some(id => goodTagIds.includes(id))) return false - } - return true -} - function rightFilterNode(value: string, data: any) { if (!value) return true if (data.isOG) return data.label.includes(value) return true } -watch([searchKeyword, selectedCountryIds, selectedTagIds], () => { - leftTreeRef.value?.filter?.('') -}) +// Right tree search filter watch(searchKeyword, () => { rightTreeRef.value?.filter?.('') }) @@ -211,7 +216,6 @@ watch(searchKeyword, () => { function refreshLeftTree() { goodsApi.getGoodsList({ page: 1, pageSize: 200 } as any).then((res: any) => { allGoods.value = res?.items ?? [] - buildLeftTree() }) } @@ -481,8 +485,8 @@ async function handleCatDelete(node: any) { async function reloadCategories() { const cats = await categoriesApi.getCategoryTree() as any - allCategories.value = Array.isArray(cats) ? cats : (cats.items ?? []) - buildLeftTree() + allCategories.value = (Array.isArray(cats) ? cats : (cats.items ?? [])) + .filter((c: any) => !c.sdsCategoryId) } async function onLeftTreeDragEnd(dragNode: any, dropNode: any, position: string) { @@ -557,20 +561,40 @@ async function handleCountryDelete(node: any) { async function reloadCountries() { const res = await countriesApi.getCountriesList({ page: 1, pageSize: 200 } as any) as any allCountries.value = Array.isArray(res) ? res : (res.items ?? []) - buildLeftTree() } -// ─── Tag rename (from filter dropdown) ─── -async function renameTag(t: Tag) { +// ─── Tag edit modal (from filter dropdown) ─── +const tagEditVisible = ref(false) +const tagEditForm = ref({ id: '', tagName: '', tagColor: '#ff6800', tagFontColor: '#ffffff' }) +const tagEditLoading = ref(false) + +function openTagEditFromFilter(t: Tag) { + tagEditForm.value = { id: t.id, tagName: t.tagName, tagColor: t.tagColor || '#ff6800', tagFontColor: t.tagFontColor || '#ffffff' } + tagEditVisible.value = true +} + +async function handleTagEditSubmit() { + if (!tagEditForm.value.tagName.trim()) { ElMessage.warning('请输入标签名称'); return } + tagEditLoading.value = true try { - const { value } = await ElMessageBox.prompt('请输入新的标签名称', '重命名标签', { - inputValue: t.tagName, confirmButtonText: '保存', cancelButtonText: '取消', - }) - if (!value.trim()) return - await tagsApi.updateTag(t.id, { tagName: value.trim(), tagColor: t.tagColor || '#ff6800' } as any) + await tagsApi.updateTag(tagEditForm.value.id, { tagName: tagEditForm.value.tagName.trim(), tagColor: tagEditForm.value.tagColor, tagFontColor: tagEditForm.value.tagFontColor } as any) + ElMessage.success('保存成功') + tagEditVisible.value = false await reloadTags() - ElMessage.success('已重命名') - } catch {} + } catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') } + finally { tagEditLoading.value = false } +} + +async function handleTagEditDelete() { + try { + await ElMessageBox.confirm(`确定删除标签「${tagEditForm.value.tagName}」吗?`, '确认', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }) + } catch { return } + try { + await tagsApi.deleteTag(tagEditForm.value.id) + ElMessage.success('删除成功') + tagEditVisible.value = false + await reloadTags() + } catch { ElMessage.error('删除失败') } } async function reloadTags() { @@ -578,17 +602,11 @@ async function reloadTags() { allTags.value = Array.isArray(res) ? res : (res.items ?? []) } -// ─── Country rename (from filter dropdown) ─── -async function renameCountry(c: Country) { - try { - const { value } = await ElMessageBox.prompt('请输入新的国家名称', '重命名国家', { - inputValue: c.countryName, confirmButtonText: '保存', cancelButtonText: '取消', - }) - if (!value.trim()) return - await countriesApi.updateCountry(c.id, { countryName: value.trim() } as any) - await reloadCountries() - ElMessage.success('已重命名') - } catch {} +// ─── Country edit (from filter dropdown) ─── +function openCountryEditFromFilter(c: Country) { + countryEditMode.value = 'edit' + countryEditForm.value = { id: c.id, countryName: c.countryName, countryIcon: c.countryIcon || '' } + countryEditVisible.value = true } // ─── Quick create (inline in edit/config modal) ─── @@ -618,7 +636,7 @@ async function quickCreateTag(targetForm: () => void) { } catch {} } -function onModeChange() { buildLeftTree() } +function onModeChange() {} onMounted(() => loadAll()) @@ -635,7 +653,7 @@ onMounted(() => loadAll())
{{ c.countryName }} - +
@@ -644,7 +662,7 @@ onMounted(() => loadAll())
{{ t.tagName }} - +
@@ -673,7 +691,6 @@ onMounted(() => loadAll()) ref="leftTreeRef" :data="leftTreeData" :props="treeProps" - :filter-node-method="leftFilterNode" node-key="id" :draggable="mode === 'category'" :expand-on-click-node="true" @@ -913,6 +930,30 @@ onMounted(() => loadAll()) 保存 + + + + + + +
+ + +
+
+ +
+ + +
+
+
+ +
diff --git a/inkreach-official-admin/src/views/tags/TagsView.vue b/inkreach-official-admin/src/views/tags/TagsView.vue index c2cdd15..f1fd920 100644 --- a/inkreach-official-admin/src/views/tags/TagsView.vue +++ b/inkreach-official-admin/src/views/tags/TagsView.vue @@ -52,6 +52,7 @@ const dialogLoading = ref(false) const dialogForm = reactive({ tagName: '', tagColor: '#ff6800', + tagFontColor: '#ffffff', timing: '', }) @@ -61,7 +62,7 @@ const dialogRules = { function openAddDialog() { dialogMode.value = 'create' - Object.assign(dialogForm, { id: undefined, tagName: '', tagColor: '#ff6800', timing: '' }) + Object.assign(dialogForm, { id: undefined, tagName: '', tagColor: '#ff6800', tagFontColor: '#ffffff', timing: '' }) dialogVisible.value = true } @@ -71,6 +72,7 @@ function openEditDialog(t: Tag) { id: t.id, tagName: t.tagName, tagColor: t.tagColor || '#ff6800', + tagFontColor: t.tagFontColor || '#ffffff', timing: t.timing || '', }) dialogVisible.value = true @@ -85,6 +87,7 @@ async function handleSubmit() { const payload: CreateTagRequest = { tagName: dialogForm.tagName, tagColor: dialogForm.tagColor || undefined, + tagFontColor: dialogForm.tagFontColor || undefined, timing: dialogForm.timing || undefined, } if (dialogMode.value === 'create') { @@ -155,14 +158,16 @@ onMounted(fetchList) - + + + +