feat(tags): support separate font color for tag chips

- 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
This commit is contained in:
yeuimu
2026-06-25 16:10:56 +08:00
parent a903dd4903
commit 69945b8749
16 changed files with 1278 additions and 97 deletions
+4 -1
View File
@@ -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 {
@@ -21,7 +21,6 @@ const loading = ref(false)
const leftTreeRef = ref()
const rightTreeRef = ref()
const leftTreeData = ref<any[]>([])
const rightTreeData = ref<any[]>([])
const allCategories = ref<CategoryTree[]>([])
const allCountries = ref<Country[]>([])
@@ -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())
</script>
@@ -635,7 +653,7 @@ onMounted(() => loadAll())
<el-option v-for="c in allCountries" :key="c.id" :label="c.countryName" :value="c.id">
<div class="filter-opt">
<span>{{ c.countryName }}</span>
<el-button text size="small" :icon="Edit" @click.stop="renameCountry(c)" />
<el-button text size="small" :icon="Edit" @click.stop="openCountryEditFromFilter(c)" />
</div>
</el-option>
</el-select>
@@ -644,7 +662,7 @@ onMounted(() => loadAll())
<el-option v-for="t in allTags" :key="t.id" :label="t.tagName" :value="t.id">
<div class="filter-opt">
<span class="filter-opt-tag" :style="{ '--c': t.tagColor || '#ccc' }">{{ t.tagName }}</span>
<el-button text size="small" :icon="Edit" @click.stop="renameTag(t)" />
<el-button text size="small" :icon="Edit" @click.stop="openTagEditFromFilter(t)" />
</div>
</el-option>
</el-select>
@@ -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())
<el-button type="primary" :loading="countryEditLoading" @click="handleCountrySubmit">保存</el-button>
</template>
</el-dialog>
<!-- Tag Edit Modal -->
<el-dialog v-model="tagEditVisible" title="编辑标签" width="420px" destroy-on-close>
<el-form label-width="80px">
<el-form-item label="名称"><el-input v-model="tagEditForm.tagName" placeholder="标签名称" /></el-form-item>
<el-form-item label="背景色">
<div style="display: flex; align-items: center; gap: 8px;">
<el-color-picker v-model="tagEditForm.tagColor" />
<el-input v-model="tagEditForm.tagColor" placeholder="#ff6800" style="width: 120px" />
</div>
</el-form-item>
<el-form-item label="字体色">
<div style="display: flex; align-items: center; gap: 8px;">
<el-color-picker v-model="tagEditForm.tagFontColor" />
<el-input v-model="tagEditForm.tagFontColor" placeholder="#ffffff" style="width: 120px" />
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button type="danger" :loading="tagEditLoading" @click="handleTagEditDelete">删除</el-button>
<el-button @click="tagEditVisible = false">取消</el-button>
<el-button type="primary" :loading="tagEditLoading" @click="handleTagEditSubmit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -52,6 +52,7 @@ const dialogLoading = ref(false)
const dialogForm = reactive<CreateTagRequest & { id?: string }>({
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)
<el-table v-loading="loading" :data="list" border stripe>
<el-table-column label="预览" width="120">
<template #default="{ row }: { row: Tag }">
<el-tag v-if="row.tagColor" :color="row.tagColor" effect="dark">
{{ row.tagName }}
</el-tag>
<span
v-if="row.tagColor"
class="tag-preview"
:style="{ background: row.tagColor, color: row.tagFontColor || '#fff' }"
>{{ row.tagName }}</span>
<el-tag v-else effect="plain">{{ row.tagName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="tagName" label="名称" min-width="200" />
<el-table-column label="色" width="140">
<el-table-column label="背景色" width="140">
<template #default="{ row }: { row: Tag }">
<div class="color-cell">
<span class="color-swatch" :style="{ background: row.tagColor || '#d1d5db' }" />
@@ -170,6 +175,14 @@ onMounted(fetchList)
</div>
</template>
</el-table-column>
<el-table-column label="字体色" width="140">
<template #default="{ row }: { row: Tag }">
<div class="color-cell">
<span class="color-swatch" :style="{ background: row.tagFontColor || '#d1d5db' }" />
<span class="color-hex">{{ row.tagFontColor || '-' }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="timing" label="定时" min-width="120" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }: { row: Tag }">
@@ -212,10 +225,14 @@ onMounted(fetchList)
<el-form-item label="名称" prop="tagName">
<el-input v-model="dialogForm.tagName" placeholder="请输入标签名称" />
</el-form-item>
<el-form-item label="">
<el-form-item label="背景">
<el-color-picker v-model="dialogForm.tagColor" />
<span class="color-readout">{{ dialogForm.tagColor }}</span>
</el-form-item>
<el-form-item label="字体色">
<el-color-picker v-model="dialogForm.tagFontColor" />
<span class="color-readout">{{ dialogForm.tagFontColor }}</span>
</el-form-item>
<el-form-item label="定时">
<el-input v-model="dialogForm.timing" placeholder="例如 9:00-12:00" />
</el-form-item>
@@ -266,4 +283,12 @@ onMounted(fetchList)
font-size: 12px;
color: #6b7280;
}
.tag-preview {
display: inline-block;
padding: 2px 12px;
border-radius: 4px;
font-size: 13px;
white-space: nowrap;
}
</style>
@@ -69,6 +69,7 @@ model Tag {
id BigInt @id @default(autoincrement()) @map("tag_id")
tagName String @unique @map("tag_name")
tagColor String? @map("tag_color")
tagFontColor String? @map("tag_font_color")
timing String?
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
@@ -4,7 +4,7 @@ import type { Good as PrismaGood } from '@prisma/client';
export interface GoodRelations {
country?: { id: bigint; countryName: string; countryIcon: string | null } | null;
category?: { id: bigint; categoryName: string; categoryIcon: string | null } | null;
tag?: { id: bigint; tagName: string; tagColor: string | null } | null;
tag?: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } | null;
position?: { id: bigint; indexVal: number } | null;
originGood?: {
id: bigint;
@@ -13,7 +13,7 @@ export interface GoodRelations {
goodImage: string | null;
goodPrice: unknown;
} | null;
goodTags?: { tag: { id: bigint; tagName: string; tagColor: string | null } }[];
goodTags?: { tag: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } }[];
}
export class GoodDto {
@@ -57,10 +57,10 @@ export class GoodDto {
category?: { id: string; categoryName: string; categoryIcon: string | null } | null;
@ApiProperty({ required: false, nullable: true })
tag?: { id: string; tagName: string; tagColor: string | null } | null;
tag?: { id: string; tagName: string; tagColor: string | null; tagFontColor: string | null } | null;
@ApiProperty({ required: false, type: Array })
tags!: Array<{ id: string; tagName: string; tagColor: string | null }>;
tags!: Array<{ id: string; tagName: string; tagColor: string | null; tagFontColor: string | null }>;
@ApiProperty({ required: false, nullable: true })
position?: { id: string; indexVal: number } | null;
@@ -109,6 +109,7 @@ export class GoodDto {
id: rel.tag.id.toString(),
tagName: rel.tag.tagName,
tagColor: rel.tag.tagColor,
tagFontColor: rel.tag.tagFontColor,
}
: null,
tags: rel.goodTags
@@ -116,6 +117,7 @@ export class GoodDto {
id: gt.tag.id.toString(),
tagName: gt.tag.tagName,
tagColor: gt.tag.tagColor,
tagFontColor: gt.tag.tagFontColor,
}))
: [],
position: rel.position
@@ -31,7 +31,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 }[];
}
/** A category node in the hierarchical tree, with origin goods as leaves. */
@@ -125,7 +125,7 @@ export class OriginGoodsService {
this.prisma.goodTag.findMany({
select: {
good: { select: { originGoodId: true } },
tag: { select: { tagName: true, tagColor: true } },
tag: { select: { tagName: true, tagColor: true, tagFontColor: true } },
},
}),
]);
@@ -145,10 +145,10 @@ export class OriginGoodsService {
else countryMap.set(key, [name]);
});
const tagMap = new Map<string, { tagName: string; tagColor: string | null }[]>();
const tagMap = new Map<string, { tagName: string; tagColor: string | null; tagFontColor: string | null }[]>();
goodsWithTags.forEach((gt) => {
const key = gt.good.originGoodId.toString();
const tagInfo = { tagName: gt.tag.tagName, tagColor: gt.tag.tagColor };
const tagInfo = { tagName: gt.tag.tagName, tagColor: gt.tag.tagColor, tagFontColor: gt.tag.tagFontColor };
const arr = tagMap.get(key);
if (arr) {
if (!arr.some((t) => t.tagName === tagInfo.tagName)) arr.push(tagInfo);
@@ -17,10 +17,10 @@ export class PublicGoodDto {
category!: { id: string; categoryName: string; categoryIcon: string | null };
@ApiProperty({ nullable: true })
tag!: { id: string; tagName: string; tagColor: string | null } | null;
tag!: { id: string; tagName: string; tagColor: string | null; tagFontColor: string | null } | null;
@ApiProperty({ type: Array })
tags!: Array<{ id: string; tagName: string; tagColor: string | null }>;
tags!: Array<{ id: string; tagName: string; tagColor: string | null; tagFontColor: string | null }>;
@ApiProperty({ nullable: true })
position!: { id: string; indexVal: number } | null;
@@ -36,11 +36,10 @@ export class PublicQueryGoodDto {
@IsInt()
categoryId?: number;
@ApiProperty({ required: false })
@ApiProperty({ required: false, description: 'Comma-separated tag IDs, e.g. "30,34"' })
@IsOptional()
@Type(() => Number)
@IsInt()
tagId?: number;
@IsString()
tagIds?: string;
@ApiProperty({ required: false })
@IsOptional()
@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import type { Tag as PrismaTag } from '@prisma/client';
export class PublicTagDto {
@ApiProperty()
id!: string;
@ApiProperty()
tagName!: string;
@ApiProperty({ nullable: true })
tagColor!: string | null;
@ApiProperty({ nullable: true })
tagFontColor!: string | null;
static from(tag: PrismaTag): PublicTagDto {
return {
id: tag.id.toString(),
tagName: tag.tagName,
tagColor: tag.tagColor,
tagFontColor: tag.tagFontColor,
};
}
}
@@ -8,6 +8,7 @@ import {
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { PublicService } from './public.service';
import { PublicQueryGoodDto } from './dto/public-query-good.dto';
import { PublicTagDto } from './dto/public-tag.dto';
@ApiTags('public')
@Controller('public')
@@ -26,6 +27,12 @@ export class PublicController {
return this.service.getCountries();
}
@Get('tags')
@ApiOperation({ summary: 'Public list of tags that have goods' })
getTags(): Promise<PublicTagDto[]> {
return this.service.getTags();
}
@Get('goods')
@ApiOperation({ summary: 'Public paginated goods with filters' })
getGoods(@Query() query: PublicQueryGoodDto) {
@@ -2,10 +2,9 @@ import { Injectable, NotFoundException } from '@nestjs/common';
import { Category as PrismaCategory, Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { PublicQueryGoodDto } from './dto/public-query-good.dto';
import {
PublicCategoryNodeDto,
} from './dto/public-category.dto';
import { PublicCategoryNodeDto } from './dto/public-category.dto';
import { PublicCountryDto } from './dto/public-country.dto';
import { PublicTagDto } from './dto/public-tag.dto';
import { PublicGoodDto } from './dto/public-good.dto';
export interface PublicPaginatedGoods {
@@ -29,11 +28,34 @@ export class PublicService {
constructor(private readonly prisma: PrismaService) {}
async getCategoriesTree(): Promise<PublicCategoryNodeDto[]> {
const categoriesWithGoods = await this.prisma.category.findMany({
const leafCategories = await this.prisma.category.findMany({
where: { goods: { some: {} } },
orderBy: { id: 'asc' },
});
return this.buildTree(categoriesWithGoods);
const ancestorIds = new Set<bigint>();
for (const leaf of leafCategories) {
let cursor: bigint | null = leaf.parentCategoryId;
while (cursor !== null && !ancestorIds.has(cursor)) {
ancestorIds.add(cursor);
const parent = await this.prisma.category.findUnique({
where: { id: cursor },
select: { id: true, parentCategoryId: true },
});
if (!parent) break;
cursor = parent.parentCategoryId;
}
}
const ancestorRows = ancestorIds.size > 0
? await this.prisma.category.findMany({
where: { id: { in: [...ancestorIds] } },
orderBy: { id: 'asc' },
})
: [];
const allRows = [...leafCategories, ...ancestorRows].filter(
(row, idx, arr) => arr.findIndex((r) => r.id === row.id) === idx,
);
allRows.sort((a, b) => Number(a.id - b.id));
return this.buildTree(allRows);
}
async getCountries(): Promise<PublicCountryDto[]> {
@@ -44,11 +66,26 @@ export class PublicService {
return rows.map(PublicCountryDto.from);
}
async getTags(): Promise<PublicTagDto[]> {
const rows = await this.prisma.tag.findMany({
where: { goodTags: { some: {} } },
orderBy: { id: 'asc' },
});
return rows.map(PublicTagDto.from);
}
async getGoods(query: PublicQueryGoodDto): Promise<PublicPaginatedGoods> {
const where: Prisma.GoodWhereInput = {};
if (query.countryId !== undefined) where.countryId = BigInt(query.countryId);
if (query.tagId !== undefined) {
where.goodTags = { some: { tagId: BigInt(query.tagId) } };
if (query.tagIds) {
const ids = query.tagIds
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((s) => BigInt(s));
if (ids.length > 0) {
where.goodTags = { some: { tagId: { in: ids } } };
}
}
if (query.keyword) {
where.goodName = { contains: query.keyword, mode: 'insensitive' };
@@ -99,13 +136,13 @@ export class PublicService {
goodPriority: number;
country: { id: bigint; countryName: string; countryIcon: string | null };
category: { id: bigint; categoryName: string; categoryIcon: string | null };
tag: { id: bigint; tagName: string; tagColor: string | null } | null;
tag: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } | null;
position: { id: bigint; indexVal: number } | null;
originGood: {
goodImage: string | null;
goodPrice: { toString(): string } | null;
} | null;
goodTags: { tag: { id: bigint; tagName: string; tagColor: string | null } }[];
goodTags: { tag: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } }[];
createdAt: Date;
}): PublicGoodDto {
return {
@@ -127,12 +164,14 @@ export class PublicService {
id: good.tag.id.toString(),
tagName: good.tag.tagName,
tagColor: good.tag.tagColor,
tagFontColor: good.tag.tagFontColor,
}
: null,
tags: good.goodTags.map((gt) => ({
id: gt.tag.id.toString(),
tagName: gt.tag.tagName,
tagColor: gt.tag.tagColor,
tagFontColor: gt.tag.tagFontColor,
})),
position: good.position
? {
@@ -22,6 +22,16 @@ export class CreateTagDto {
@IsHexColor()
tagColor?: string;
@ApiProperty({
required: false,
nullable: true,
description: 'Hex font color, e.g. #FFFFFF',
example: '#FFFFFF',
})
@IsOptional()
@IsHexColor()
tagFontColor?: string;
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
@@ -16,6 +16,11 @@ export class UpdateTagDto {
@IsHexColor()
tagColor?: string | null;
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsHexColor()
tagFontColor?: string | null;
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
@@ -28,6 +28,7 @@ export class TagsService {
data: {
tagName: dto.tagName,
tagColor: dto.tagColor ?? null,
tagFontColor: dto.tagFontColor ?? null,
timing: dto.timing ?? null,
},
});
@@ -47,6 +48,7 @@ export class TagsService {
const data: Prisma.TagUpdateInput = {};
if (dto.tagName !== undefined) data.tagName = dto.tagName;
if (dto.tagColor !== undefined) data.tagColor = dto.tagColor;
if (dto.tagFontColor !== undefined) data.tagFontColor = dto.tagFontColor;
if (dto.timing !== undefined) data.timing = dto.timing;
try {
return await this.prisma.tag.update({ where: { id }, data });
Submodule inkreach-official-website updated: 967cceb099...71650f3b22
File diff suppressed because it is too large Load Diff