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:
@@ -141,6 +141,7 @@ export interface Tag {
|
|||||||
id: string
|
id: string
|
||||||
tagName: string
|
tagName: string
|
||||||
tagColor?: string | null
|
tagColor?: string | null
|
||||||
|
tagFontColor?: string | null
|
||||||
timing?: string | null
|
timing?: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
@@ -149,12 +150,14 @@ export interface Tag {
|
|||||||
export interface CreateTagRequest {
|
export interface CreateTagRequest {
|
||||||
tagName: string
|
tagName: string
|
||||||
tagColor?: string
|
tagColor?: string
|
||||||
|
tagFontColor?: string
|
||||||
timing?: string
|
timing?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateTagRequest {
|
export interface UpdateTagRequest {
|
||||||
tagName?: string
|
tagName?: string
|
||||||
tagColor?: string | null
|
tagColor?: string | null
|
||||||
|
tagFontColor?: string | null
|
||||||
timing?: string | null
|
timing?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +206,7 @@ export interface OriginGoodsTreeNode {
|
|||||||
sdsGoodId: string
|
sdsGoodId: string
|
||||||
configuredCount: number
|
configuredCount: number
|
||||||
configuredCountries: string[]
|
configuredCountries: string[]
|
||||||
configuredTags: { tagName: string; tagColor: string | null }[]
|
configuredTags: { tagName: string; tagColor: string | null; tagFontColor: string | null }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OriginGoodsTreeCategoryNode {
|
export interface OriginGoodsTreeCategoryNode {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const loading = ref(false)
|
|||||||
|
|
||||||
const leftTreeRef = ref()
|
const leftTreeRef = ref()
|
||||||
const rightTreeRef = ref()
|
const rightTreeRef = ref()
|
||||||
const leftTreeData = ref<any[]>([])
|
|
||||||
const rightTreeData = ref<any[]>([])
|
const rightTreeData = ref<any[]>([])
|
||||||
const allCategories = ref<CategoryTree[]>([])
|
const allCategories = ref<CategoryTree[]>([])
|
||||||
const allCountries = ref<Country[]>([])
|
const allCountries = ref<Country[]>([])
|
||||||
@@ -99,7 +98,6 @@ async function loadAll() {
|
|||||||
allCountries.value = Array.isArray(countries) ? countries : (countries.items ?? [])
|
allCountries.value = Array.isArray(countries) ? countries : (countries.items ?? [])
|
||||||
allTags.value = Array.isArray(tags) ? tags : (tags.items ?? [])
|
allTags.value = Array.isArray(tags) ? tags : (tags.items ?? [])
|
||||||
allGoods.value = goodsRes?.items ?? []
|
allGoods.value = goodsRes?.items ?? []
|
||||||
buildLeftTree()
|
|
||||||
buildRightTree(ogTree)
|
buildRightTree(ogTree)
|
||||||
} catch (e) { console.error('加载失败', e) }
|
} catch (e) { console.error('加载失败', e) }
|
||||||
finally { loading.value = false }
|
finally { loading.value = false }
|
||||||
@@ -125,24 +123,23 @@ function goodToNode(g: Good): any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLeftTree() {
|
// Computed: goods after applying search + country + tag filters
|
||||||
if (mode.value === 'category') {
|
const filteredGoods = computed(() => {
|
||||||
leftTreeData.value = mapCatNodes(allCategories.value, allGoods.value)
|
let result = allGoods.value
|
||||||
} else {
|
if (searchKeyword.value) {
|
||||||
leftTreeData.value = allCountries.value.map(c => {
|
result = result.filter(g => g.goodName?.includes(searchKeyword.value))
|
||||||
const goods = allGoods.value.filter(g => g.countryId === c.id)
|
}
|
||||||
return {
|
if (selectedCountryIds.value.length) {
|
||||||
id: c.id,
|
result = result.filter(g => selectedCountryIds.value.includes(g.countryId))
|
||||||
label: c.countryName,
|
}
|
||||||
icon: c.countryIcon,
|
if (selectedTagIds.value.length) {
|
||||||
isCat: true,
|
result = result.filter(g => {
|
||||||
raw: c,
|
const ids = (g.tags || []).map(t => t.id)
|
||||||
goodCount: goods.length,
|
return selectedTagIds.value.some(id => ids.includes(id))
|
||||||
children: goods.map(goodToNode),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
function mapCatNodes(nodes: CategoryTree[], goods: Good[]): any[] {
|
function mapCatNodes(nodes: CategoryTree[], goods: Good[]): any[] {
|
||||||
return nodes.map(n => {
|
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 buildRightTree(tree: OriginGoodsTreeResponse) {
|
||||||
function mapCat(node: any): any {
|
function mapCat(node: any): any {
|
||||||
const children = (node.children || []).map(mapCat)
|
const children = (node.children || []).map(mapCat)
|
||||||
@@ -184,26 +202,13 @@ function buildRightTree(tree: OriginGoodsTreeResponse) {
|
|||||||
rightTreeData.value = tree.tree.map(mapCat)
|
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) {
|
function rightFilterNode(value: string, data: any) {
|
||||||
if (!value) return true
|
if (!value) return true
|
||||||
if (data.isOG) return data.label.includes(value)
|
if (data.isOG) return data.label.includes(value)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([searchKeyword, selectedCountryIds, selectedTagIds], () => {
|
// Right tree search filter
|
||||||
leftTreeRef.value?.filter?.('')
|
|
||||||
})
|
|
||||||
watch(searchKeyword, () => {
|
watch(searchKeyword, () => {
|
||||||
rightTreeRef.value?.filter?.('')
|
rightTreeRef.value?.filter?.('')
|
||||||
})
|
})
|
||||||
@@ -211,7 +216,6 @@ watch(searchKeyword, () => {
|
|||||||
function refreshLeftTree() {
|
function refreshLeftTree() {
|
||||||
goodsApi.getGoodsList({ page: 1, pageSize: 200 } as any).then((res: any) => {
|
goodsApi.getGoodsList({ page: 1, pageSize: 200 } as any).then((res: any) => {
|
||||||
allGoods.value = res?.items ?? []
|
allGoods.value = res?.items ?? []
|
||||||
buildLeftTree()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,8 +485,8 @@ async function handleCatDelete(node: any) {
|
|||||||
|
|
||||||
async function reloadCategories() {
|
async function reloadCategories() {
|
||||||
const cats = await categoriesApi.getCategoryTree() as any
|
const cats = await categoriesApi.getCategoryTree() as any
|
||||||
allCategories.value = Array.isArray(cats) ? cats : (cats.items ?? [])
|
allCategories.value = (Array.isArray(cats) ? cats : (cats.items ?? []))
|
||||||
buildLeftTree()
|
.filter((c: any) => !c.sdsCategoryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onLeftTreeDragEnd(dragNode: any, dropNode: any, position: string) {
|
async function onLeftTreeDragEnd(dragNode: any, dropNode: any, position: string) {
|
||||||
@@ -557,20 +561,40 @@ async function handleCountryDelete(node: any) {
|
|||||||
async function reloadCountries() {
|
async function reloadCountries() {
|
||||||
const res = await countriesApi.getCountriesList({ page: 1, pageSize: 200 } as any) as any
|
const res = await countriesApi.getCountriesList({ page: 1, pageSize: 200 } as any) as any
|
||||||
allCountries.value = Array.isArray(res) ? res : (res.items ?? [])
|
allCountries.value = Array.isArray(res) ? res : (res.items ?? [])
|
||||||
buildLeftTree()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tag rename (from filter dropdown) ───
|
// ─── Tag edit modal (from filter dropdown) ───
|
||||||
async function renameTag(t: Tag) {
|
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 {
|
try {
|
||||||
const { value } = await ElMessageBox.prompt('请输入新的标签名称', '重命名标签', {
|
await tagsApi.updateTag(tagEditForm.value.id, { tagName: tagEditForm.value.tagName.trim(), tagColor: tagEditForm.value.tagColor, tagFontColor: tagEditForm.value.tagFontColor } as any)
|
||||||
inputValue: t.tagName, confirmButtonText: '保存', cancelButtonText: '取消',
|
ElMessage.success('保存成功')
|
||||||
})
|
tagEditVisible.value = false
|
||||||
if (!value.trim()) return
|
|
||||||
await tagsApi.updateTag(t.id, { tagName: value.trim(), tagColor: t.tagColor || '#ff6800' } as any)
|
|
||||||
await reloadTags()
|
await reloadTags()
|
||||||
ElMessage.success('已重命名')
|
} catch (e: any) { ElMessage.error(e?.response?.data?.message || '操作失败') }
|
||||||
} catch {}
|
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() {
|
async function reloadTags() {
|
||||||
@@ -578,17 +602,11 @@ async function reloadTags() {
|
|||||||
allTags.value = Array.isArray(res) ? res : (res.items ?? [])
|
allTags.value = Array.isArray(res) ? res : (res.items ?? [])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Country rename (from filter dropdown) ───
|
// ─── Country edit (from filter dropdown) ───
|
||||||
async function renameCountry(c: Country) {
|
function openCountryEditFromFilter(c: Country) {
|
||||||
try {
|
countryEditMode.value = 'edit'
|
||||||
const { value } = await ElMessageBox.prompt('请输入新的国家名称', '重命名国家', {
|
countryEditForm.value = { id: c.id, countryName: c.countryName, countryIcon: c.countryIcon || '' }
|
||||||
inputValue: c.countryName, confirmButtonText: '保存', cancelButtonText: '取消',
|
countryEditVisible.value = true
|
||||||
})
|
|
||||||
if (!value.trim()) return
|
|
||||||
await countriesApi.updateCountry(c.id, { countryName: value.trim() } as any)
|
|
||||||
await reloadCountries()
|
|
||||||
ElMessage.success('已重命名')
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Quick create (inline in edit/config modal) ───
|
// ─── Quick create (inline in edit/config modal) ───
|
||||||
@@ -618,7 +636,7 @@ async function quickCreateTag(targetForm: () => void) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onModeChange() { buildLeftTree() }
|
function onModeChange() {}
|
||||||
|
|
||||||
onMounted(() => loadAll())
|
onMounted(() => loadAll())
|
||||||
</script>
|
</script>
|
||||||
@@ -635,7 +653,7 @@ onMounted(() => loadAll())
|
|||||||
<el-option v-for="c in allCountries" :key="c.id" :label="c.countryName" :value="c.id">
|
<el-option v-for="c in allCountries" :key="c.id" :label="c.countryName" :value="c.id">
|
||||||
<div class="filter-opt">
|
<div class="filter-opt">
|
||||||
<span>{{ c.countryName }}</span>
|
<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>
|
</div>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -644,7 +662,7 @@ onMounted(() => loadAll())
|
|||||||
<el-option v-for="t in allTags" :key="t.id" :label="t.tagName" :value="t.id">
|
<el-option v-for="t in allTags" :key="t.id" :label="t.tagName" :value="t.id">
|
||||||
<div class="filter-opt">
|
<div class="filter-opt">
|
||||||
<span class="filter-opt-tag" :style="{ '--c': t.tagColor || '#ccc' }">{{ t.tagName }}</span>
|
<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>
|
</div>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -673,7 +691,6 @@ onMounted(() => loadAll())
|
|||||||
ref="leftTreeRef"
|
ref="leftTreeRef"
|
||||||
:data="leftTreeData"
|
:data="leftTreeData"
|
||||||
:props="treeProps"
|
:props="treeProps"
|
||||||
:filter-node-method="leftFilterNode"
|
|
||||||
node-key="id"
|
node-key="id"
|
||||||
:draggable="mode === 'category'"
|
:draggable="mode === 'category'"
|
||||||
:expand-on-click-node="true"
|
:expand-on-click-node="true"
|
||||||
@@ -913,6 +930,30 @@ onMounted(() => loadAll())
|
|||||||
<el-button type="primary" :loading="countryEditLoading" @click="handleCountrySubmit">保存</el-button>
|
<el-button type="primary" :loading="countryEditLoading" @click="handleCountrySubmit">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const dialogLoading = ref(false)
|
|||||||
const dialogForm = reactive<CreateTagRequest & { id?: string }>({
|
const dialogForm = reactive<CreateTagRequest & { id?: string }>({
|
||||||
tagName: '',
|
tagName: '',
|
||||||
tagColor: '#ff6800',
|
tagColor: '#ff6800',
|
||||||
|
tagFontColor: '#ffffff',
|
||||||
timing: '',
|
timing: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ const dialogRules = {
|
|||||||
|
|
||||||
function openAddDialog() {
|
function openAddDialog() {
|
||||||
dialogMode.value = 'create'
|
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
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ function openEditDialog(t: Tag) {
|
|||||||
id: t.id,
|
id: t.id,
|
||||||
tagName: t.tagName,
|
tagName: t.tagName,
|
||||||
tagColor: t.tagColor || '#ff6800',
|
tagColor: t.tagColor || '#ff6800',
|
||||||
|
tagFontColor: t.tagFontColor || '#ffffff',
|
||||||
timing: t.timing || '',
|
timing: t.timing || '',
|
||||||
})
|
})
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@@ -85,6 +87,7 @@ async function handleSubmit() {
|
|||||||
const payload: CreateTagRequest = {
|
const payload: CreateTagRequest = {
|
||||||
tagName: dialogForm.tagName,
|
tagName: dialogForm.tagName,
|
||||||
tagColor: dialogForm.tagColor || undefined,
|
tagColor: dialogForm.tagColor || undefined,
|
||||||
|
tagFontColor: dialogForm.tagFontColor || undefined,
|
||||||
timing: dialogForm.timing || undefined,
|
timing: dialogForm.timing || undefined,
|
||||||
}
|
}
|
||||||
if (dialogMode.value === 'create') {
|
if (dialogMode.value === 'create') {
|
||||||
@@ -155,14 +158,16 @@ onMounted(fetchList)
|
|||||||
<el-table v-loading="loading" :data="list" border stripe>
|
<el-table v-loading="loading" :data="list" border stripe>
|
||||||
<el-table-column label="预览" width="120">
|
<el-table-column label="预览" width="120">
|
||||||
<template #default="{ row }: { row: Tag }">
|
<template #default="{ row }: { row: Tag }">
|
||||||
<el-tag v-if="row.tagColor" :color="row.tagColor" effect="dark">
|
<span
|
||||||
{{ row.tagName }}
|
v-if="row.tagColor"
|
||||||
</el-tag>
|
class="tag-preview"
|
||||||
|
:style="{ background: row.tagColor, color: row.tagFontColor || '#fff' }"
|
||||||
|
>{{ row.tagName }}</span>
|
||||||
<el-tag v-else effect="plain">{{ row.tagName }}</el-tag>
|
<el-tag v-else effect="plain">{{ row.tagName }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="tagName" label="名称" min-width="200" />
|
<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 }">
|
<template #default="{ row }: { row: Tag }">
|
||||||
<div class="color-cell">
|
<div class="color-cell">
|
||||||
<span class="color-swatch" :style="{ background: row.tagColor || '#d1d5db' }" />
|
<span class="color-swatch" :style="{ background: row.tagColor || '#d1d5db' }" />
|
||||||
@@ -170,6 +175,14 @@ onMounted(fetchList)
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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 prop="timing" label="定时" min-width="120" />
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
<template #default="{ row }: { row: Tag }">
|
<template #default="{ row }: { row: Tag }">
|
||||||
@@ -212,10 +225,14 @@ onMounted(fetchList)
|
|||||||
<el-form-item label="名称" prop="tagName">
|
<el-form-item label="名称" prop="tagName">
|
||||||
<el-input v-model="dialogForm.tagName" placeholder="请输入标签名称" />
|
<el-input v-model="dialogForm.tagName" placeholder="请输入标签名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="颜色">
|
<el-form-item label="背景色">
|
||||||
<el-color-picker v-model="dialogForm.tagColor" />
|
<el-color-picker v-model="dialogForm.tagColor" />
|
||||||
<span class="color-readout">{{ dialogForm.tagColor }}</span>
|
<span class="color-readout">{{ dialogForm.tagColor }}</span>
|
||||||
</el-form-item>
|
</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-form-item label="定时">
|
||||||
<el-input v-model="dialogForm.timing" placeholder="例如 9:00-12:00" />
|
<el-input v-model="dialogForm.timing" placeholder="例如 9:00-12:00" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -266,4 +283,12 @@ onMounted(fetchList)
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-preview {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -66,12 +66,13 @@ model Category {
|
|||||||
|
|
||||||
// ---------- Tags ----------
|
// ---------- Tags ----------
|
||||||
model Tag {
|
model Tag {
|
||||||
id BigInt @id @default(autoincrement()) @map("tag_id")
|
id BigInt @id @default(autoincrement()) @map("tag_id")
|
||||||
tagName String @unique @map("tag_name")
|
tagName String @unique @map("tag_name")
|
||||||
tagColor String? @map("tag_color")
|
tagColor String? @map("tag_color")
|
||||||
timing String?
|
tagFontColor String? @map("tag_font_color")
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
timing String?
|
||||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
goods Good[]
|
goods Good[]
|
||||||
goodTags GoodTag[]
|
goodTags GoodTag[]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Good as PrismaGood } from '@prisma/client';
|
|||||||
export interface GoodRelations {
|
export interface GoodRelations {
|
||||||
country?: { id: bigint; countryName: string; countryIcon: string | null } | null;
|
country?: { id: bigint; countryName: string; countryIcon: string | null } | null;
|
||||||
category?: { id: bigint; categoryName: string; categoryIcon: 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;
|
position?: { id: bigint; indexVal: number } | null;
|
||||||
originGood?: {
|
originGood?: {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
@@ -13,7 +13,7 @@ export interface GoodRelations {
|
|||||||
goodImage: string | null;
|
goodImage: string | null;
|
||||||
goodPrice: unknown;
|
goodPrice: unknown;
|
||||||
} | null;
|
} | 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 {
|
export class GoodDto {
|
||||||
@@ -57,10 +57,10 @@ export class GoodDto {
|
|||||||
category?: { id: string; categoryName: string; categoryIcon: string | null } | null;
|
category?: { id: string; categoryName: string; categoryIcon: string | null } | null;
|
||||||
|
|
||||||
@ApiProperty({ required: false, nullable: true })
|
@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 })
|
@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 })
|
@ApiProperty({ required: false, nullable: true })
|
||||||
position?: { id: string; indexVal: number } | null;
|
position?: { id: string; indexVal: number } | null;
|
||||||
@@ -109,6 +109,7 @@ export class GoodDto {
|
|||||||
id: rel.tag.id.toString(),
|
id: rel.tag.id.toString(),
|
||||||
tagName: rel.tag.tagName,
|
tagName: rel.tag.tagName,
|
||||||
tagColor: rel.tag.tagColor,
|
tagColor: rel.tag.tagColor,
|
||||||
|
tagFontColor: rel.tag.tagFontColor,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
tags: rel.goodTags
|
tags: rel.goodTags
|
||||||
@@ -116,6 +117,7 @@ export class GoodDto {
|
|||||||
id: gt.tag.id.toString(),
|
id: gt.tag.id.toString(),
|
||||||
tagName: gt.tag.tagName,
|
tagName: gt.tag.tagName,
|
||||||
tagColor: gt.tag.tagColor,
|
tagColor: gt.tag.tagColor,
|
||||||
|
tagFontColor: gt.tag.tagFontColor,
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
position: rel.position
|
position: rel.position
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface OriginGoodsTreeNode {
|
|||||||
sdsGoodId: string;
|
sdsGoodId: string;
|
||||||
configuredCount: number;
|
configuredCount: number;
|
||||||
configuredCountries: string[];
|
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. */
|
/** A category node in the hierarchical tree, with origin goods as leaves. */
|
||||||
@@ -125,7 +125,7 @@ export class OriginGoodsService {
|
|||||||
this.prisma.goodTag.findMany({
|
this.prisma.goodTag.findMany({
|
||||||
select: {
|
select: {
|
||||||
good: { select: { originGoodId: true } },
|
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]);
|
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) => {
|
goodsWithTags.forEach((gt) => {
|
||||||
const key = gt.good.originGoodId.toString();
|
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);
|
const arr = tagMap.get(key);
|
||||||
if (arr) {
|
if (arr) {
|
||||||
if (!arr.some((t) => t.tagName === tagInfo.tagName)) arr.push(tagInfo);
|
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 };
|
category!: { id: string; categoryName: string; categoryIcon: string | null };
|
||||||
|
|
||||||
@ApiProperty({ nullable: true })
|
@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 })
|
@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 })
|
@ApiProperty({ nullable: true })
|
||||||
position!: { id: string; indexVal: number } | null;
|
position!: { id: string; indexVal: number } | null;
|
||||||
|
|||||||
@@ -36,11 +36,10 @@ export class PublicQueryGoodDto {
|
|||||||
@IsInt()
|
@IsInt()
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false, description: 'Comma-separated tag IDs, e.g. "30,34"' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@IsString()
|
||||||
@IsInt()
|
tagIds?: string;
|
||||||
tagId?: number;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
@IsOptional()
|
@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 { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { PublicService } from './public.service';
|
import { PublicService } from './public.service';
|
||||||
import { PublicQueryGoodDto } from './dto/public-query-good.dto';
|
import { PublicQueryGoodDto } from './dto/public-query-good.dto';
|
||||||
|
import { PublicTagDto } from './dto/public-tag.dto';
|
||||||
|
|
||||||
@ApiTags('public')
|
@ApiTags('public')
|
||||||
@Controller('public')
|
@Controller('public')
|
||||||
@@ -26,6 +27,12 @@ export class PublicController {
|
|||||||
return this.service.getCountries();
|
return this.service.getCountries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('tags')
|
||||||
|
@ApiOperation({ summary: 'Public list of tags that have goods' })
|
||||||
|
getTags(): Promise<PublicTagDto[]> {
|
||||||
|
return this.service.getTags();
|
||||||
|
}
|
||||||
|
|
||||||
@Get('goods')
|
@Get('goods')
|
||||||
@ApiOperation({ summary: 'Public paginated goods with filters' })
|
@ApiOperation({ summary: 'Public paginated goods with filters' })
|
||||||
getGoods(@Query() query: PublicQueryGoodDto) {
|
getGoods(@Query() query: PublicQueryGoodDto) {
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
|||||||
import { Category as PrismaCategory, Prisma } from '@prisma/client';
|
import { Category as PrismaCategory, Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { PublicQueryGoodDto } from './dto/public-query-good.dto';
|
import { PublicQueryGoodDto } from './dto/public-query-good.dto';
|
||||||
import {
|
import { PublicCategoryNodeDto } from './dto/public-category.dto';
|
||||||
PublicCategoryNodeDto,
|
|
||||||
} from './dto/public-category.dto';
|
|
||||||
import { PublicCountryDto } from './dto/public-country.dto';
|
import { PublicCountryDto } from './dto/public-country.dto';
|
||||||
|
import { PublicTagDto } from './dto/public-tag.dto';
|
||||||
import { PublicGoodDto } from './dto/public-good.dto';
|
import { PublicGoodDto } from './dto/public-good.dto';
|
||||||
|
|
||||||
export interface PublicPaginatedGoods {
|
export interface PublicPaginatedGoods {
|
||||||
@@ -29,11 +28,34 @@ export class PublicService {
|
|||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async getCategoriesTree(): Promise<PublicCategoryNodeDto[]> {
|
async getCategoriesTree(): Promise<PublicCategoryNodeDto[]> {
|
||||||
const categoriesWithGoods = await this.prisma.category.findMany({
|
const leafCategories = await this.prisma.category.findMany({
|
||||||
where: { goods: { some: {} } },
|
where: { goods: { some: {} } },
|
||||||
orderBy: { id: 'asc' },
|
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[]> {
|
async getCountries(): Promise<PublicCountryDto[]> {
|
||||||
@@ -44,11 +66,26 @@ export class PublicService {
|
|||||||
return rows.map(PublicCountryDto.from);
|
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> {
|
async getGoods(query: PublicQueryGoodDto): Promise<PublicPaginatedGoods> {
|
||||||
const where: Prisma.GoodWhereInput = {};
|
const where: Prisma.GoodWhereInput = {};
|
||||||
if (query.countryId !== undefined) where.countryId = BigInt(query.countryId);
|
if (query.countryId !== undefined) where.countryId = BigInt(query.countryId);
|
||||||
if (query.tagId !== undefined) {
|
if (query.tagIds) {
|
||||||
where.goodTags = { some: { tagId: BigInt(query.tagId) } };
|
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) {
|
if (query.keyword) {
|
||||||
where.goodName = { contains: query.keyword, mode: 'insensitive' };
|
where.goodName = { contains: query.keyword, mode: 'insensitive' };
|
||||||
@@ -99,13 +136,13 @@ export class PublicService {
|
|||||||
goodPriority: number;
|
goodPriority: number;
|
||||||
country: { id: bigint; countryName: string; countryIcon: string | null };
|
country: { id: bigint; countryName: string; countryIcon: string | null };
|
||||||
category: { id: bigint; categoryName: string; categoryIcon: 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;
|
position: { id: bigint; indexVal: number } | null;
|
||||||
originGood: {
|
originGood: {
|
||||||
goodImage: string | null;
|
goodImage: string | null;
|
||||||
goodPrice: { toString(): string } | null;
|
goodPrice: { toString(): string } | null;
|
||||||
} | 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;
|
createdAt: Date;
|
||||||
}): PublicGoodDto {
|
}): PublicGoodDto {
|
||||||
return {
|
return {
|
||||||
@@ -127,12 +164,14 @@ export class PublicService {
|
|||||||
id: good.tag.id.toString(),
|
id: good.tag.id.toString(),
|
||||||
tagName: good.tag.tagName,
|
tagName: good.tag.tagName,
|
||||||
tagColor: good.tag.tagColor,
|
tagColor: good.tag.tagColor,
|
||||||
|
tagFontColor: good.tag.tagFontColor,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
tags: good.goodTags.map((gt) => ({
|
tags: good.goodTags.map((gt) => ({
|
||||||
id: gt.tag.id.toString(),
|
id: gt.tag.id.toString(),
|
||||||
tagName: gt.tag.tagName,
|
tagName: gt.tag.tagName,
|
||||||
tagColor: gt.tag.tagColor,
|
tagColor: gt.tag.tagColor,
|
||||||
|
tagFontColor: gt.tag.tagFontColor,
|
||||||
})),
|
})),
|
||||||
position: good.position
|
position: good.position
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ export class CreateTagDto {
|
|||||||
@IsHexColor()
|
@IsHexColor()
|
||||||
tagColor?: string;
|
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 })
|
@ApiProperty({ required: false, nullable: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ export class UpdateTagDto {
|
|||||||
@IsHexColor()
|
@IsHexColor()
|
||||||
tagColor?: string | null;
|
tagColor?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsHexColor()
|
||||||
|
tagFontColor?: string | null;
|
||||||
|
|
||||||
@ApiProperty({ required: false, nullable: true })
|
@ApiProperty({ required: false, nullable: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export class TagsService {
|
|||||||
data: {
|
data: {
|
||||||
tagName: dto.tagName,
|
tagName: dto.tagName,
|
||||||
tagColor: dto.tagColor ?? null,
|
tagColor: dto.tagColor ?? null,
|
||||||
|
tagFontColor: dto.tagFontColor ?? null,
|
||||||
timing: dto.timing ?? null,
|
timing: dto.timing ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -47,6 +48,7 @@ export class TagsService {
|
|||||||
const data: Prisma.TagUpdateInput = {};
|
const data: Prisma.TagUpdateInput = {};
|
||||||
if (dto.tagName !== undefined) data.tagName = dto.tagName;
|
if (dto.tagName !== undefined) data.tagName = dto.tagName;
|
||||||
if (dto.tagColor !== undefined) data.tagColor = dto.tagColor;
|
if (dto.tagColor !== undefined) data.tagColor = dto.tagColor;
|
||||||
|
if (dto.tagFontColor !== undefined) data.tagFontColor = dto.tagFontColor;
|
||||||
if (dto.timing !== undefined) data.timing = dto.timing;
|
if (dto.timing !== undefined) data.timing = dto.timing;
|
||||||
try {
|
try {
|
||||||
return await this.prisma.tag.update({ where: { id }, data });
|
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
Reference in New Issue
Block a user