feat(admin): 内容管理支持多语言和标签
This commit is contained in:
1 parent
eaee765239
commit
2b2c6bd708
14 files changed
+1080
-458
No files matched your search
@@ -5,6 +5,13 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { AppIcons } from '@/constants/icons'
|
||||
import {
|
||||
languageLabel,
|
||||
languageCodes,
|
||||
localesFromColumns,
|
||||
payloadLocales
|
||||
} from '@/constants/contentLanguages'
|
||||
import LocaleTextFields from '@/views/components/LocaleTextFields.vue'
|
||||
import { AppImage } from '@/components/AppImage'
|
||||
import {
|
||||
changeDramaStatusApi,
|
||||
@@ -16,6 +23,7 @@ import {
|
||||
} from '@/api/admin/drama'
|
||||
import { useRbac } from '@/stores/rbac'
|
||||
import { formatCount } from '@/utils/format'
|
||||
import { listContentTagsApi } from '@/api/admin/contentTag'
|
||||
import { statusClass } from '@/utils/status'
|
||||
import { uploadFile } from '@/utils/uploader'
|
||||
|
||||
@@ -30,8 +38,7 @@ const categories = ref([])
|
||||
const keyword = ref('')
|
||||
const statusFilter = ref(undefined)
|
||||
const categoryIds = ref([])
|
||||
const dramaTypes = ref([])
|
||||
const hotFilter = ref(undefined)
|
||||
const tags = ref([])
|
||||
const pageIndex = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
@@ -45,11 +52,14 @@ const editingId = ref(null)
|
||||
const editingCommentCount = ref(0)
|
||||
const editingLikeCount = ref(0)
|
||||
const formRef = ref()
|
||||
const textLanguages = ref(['zh'])
|
||||
const form = reactive({
|
||||
title: '',
|
||||
titleLocales: { zh: '' },
|
||||
categoryIds: [],
|
||||
dramaTypes: ['LIVE'],
|
||||
tagId: null,
|
||||
description: '',
|
||||
synopsisLocales: { zh: '' },
|
||||
freeEpisodes: 3,
|
||||
unlockCoin: 0,
|
||||
favoriteCount: 0,
|
||||
@@ -62,16 +72,6 @@ const form = reactive({
|
||||
const MAX_COVER_SIZE = 5 * 1024 * 1024
|
||||
const COVER_TYPES = ['image/jpeg', 'image/png', 'image/webp']
|
||||
|
||||
const formRules = {
|
||||
title: [{ required: true, message: '请输入短剧名称', trigger: 'blur' }],
|
||||
categoryIds: [
|
||||
{ type: 'array', required: true, min: 1, message: '请至少选择一个类型', trigger: 'change' }
|
||||
],
|
||||
dramaTypes: [
|
||||
{ type: 'array', required: true, min: 1, message: '请至少选择一个内容类型', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
const coverDisplayName = computed(
|
||||
() => coverFileName.value || (form.cover ? coverBasename(form.cover) : '尚未选择封面')
|
||||
)
|
||||
@@ -90,11 +90,6 @@ const statusOptions = [
|
||||
{ code: 'offline', text: '已下线' }
|
||||
]
|
||||
|
||||
const dramaTypeOptions = [
|
||||
{ value: 'ANIME', label: '漫剧' },
|
||||
{ value: 'LIVE', label: '真人剧' }
|
||||
]
|
||||
|
||||
const actionMeta = {
|
||||
submit: { label: '提交审核', done: '已提交审核' },
|
||||
approve: { label: '审核通过', done: '已通过审核' },
|
||||
@@ -111,9 +106,7 @@ async function load() {
|
||||
pageSize: pageSize.value,
|
||||
keyword: keyword.value.trim() || undefined,
|
||||
status: statusFilter.value || undefined,
|
||||
categoryIds: categoryIds.value.length ? categoryIds.value : undefined,
|
||||
dramaTypes: dramaTypes.value.length ? dramaTypes.value : undefined,
|
||||
isHot: hotFilter.value
|
||||
categoryIds: categoryIds.value.length ? categoryIds.value : undefined
|
||||
})
|
||||
list.value = res.data?.list || []
|
||||
total.value = res.data?.total || 0
|
||||
@@ -127,6 +120,11 @@ async function loadCategories() {
|
||||
categories.value = res.data || []
|
||||
}
|
||||
|
||||
async function loadTags() {
|
||||
const res = await listContentTagsApi(true)
|
||||
tags.value = res.data || []
|
||||
}
|
||||
|
||||
function search() {
|
||||
pageIndex.value = 1
|
||||
load()
|
||||
@@ -136,8 +134,6 @@ function resetFilters() {
|
||||
keyword.value = ''
|
||||
statusFilter.value = undefined
|
||||
categoryIds.value = []
|
||||
dramaTypes.value = []
|
||||
hotFilter.value = undefined
|
||||
search()
|
||||
}
|
||||
|
||||
@@ -146,24 +142,50 @@ function handleSizeChange() {
|
||||
load()
|
||||
}
|
||||
|
||||
function selectedCategoryIds(row) {
|
||||
const values =
|
||||
Array.isArray(row.categoryIds) && row.categoryIds.length
|
||||
? row.categoryIds
|
||||
: row.categoryId
|
||||
? [row.categoryId]
|
||||
: []
|
||||
return values.filter((id) => Number(id) > 0)
|
||||
}
|
||||
|
||||
function coverBasename(path) {
|
||||
if (!path) return ''
|
||||
const clean = path.split('?')[0].split('#')[0]
|
||||
return decodeURIComponent(clean.split('/').pop() || clean)
|
||||
}
|
||||
|
||||
/** 长整型计数字段在接口里是字符串,数字输入框只接受 number。 */
|
||||
function toCount(value) {
|
||||
const count = Number(value)
|
||||
return Number.isFinite(count) ? count : 0
|
||||
}
|
||||
|
||||
async function resetValidation() {
|
||||
await nextTick()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function changeTextLanguages(codes) {
|
||||
textLanguages.value = codes
|
||||
codes.forEach((code) => {
|
||||
if (form.synopsisLocales[code] == null) form.synopsisLocales[code] = ''
|
||||
})
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
textLanguages.value = ['zh']
|
||||
Object.assign(form, {
|
||||
title: '',
|
||||
titleLocales: { zh: '' },
|
||||
categoryIds: [],
|
||||
dramaTypes: ['LIVE'],
|
||||
tagId: null,
|
||||
description: '',
|
||||
synopsisLocales: { zh: '' },
|
||||
freeEpisodes: 3,
|
||||
unlockCoin: 0,
|
||||
favoriteCount: 0,
|
||||
@@ -182,41 +204,35 @@ function openCreate() {
|
||||
|
||||
function openEdit(row) {
|
||||
editingId.value = row.id
|
||||
const titleLocales = localesFromColumns(row.titleLocales, row.title)
|
||||
const synopsisLocales = localesFromColumns(row.synopsisLocales, row.description)
|
||||
textLanguages.value = languageCodes(titleLocales)
|
||||
textLanguages.value.forEach((code) => {
|
||||
if (synopsisLocales[code] == null) synopsisLocales[code] = ''
|
||||
})
|
||||
Object.assign(form, {
|
||||
title: row.title,
|
||||
categoryIds:
|
||||
Array.isArray(row.categoryIds) && row.categoryIds.length
|
||||
? row.categoryIds
|
||||
: row.categoryId
|
||||
? [row.categoryId]
|
||||
: [],
|
||||
dramaTypes:
|
||||
Array.isArray(row.dramaTypes) && row.dramaTypes.length
|
||||
? row.dramaTypes
|
||||
: row.dramaType
|
||||
? [row.dramaType]
|
||||
: ['LIVE'],
|
||||
titleLocales,
|
||||
categoryIds: selectedCategoryIds(row),
|
||||
tagId: Number(row.tagId) > 0 ? row.tagId : null,
|
||||
description: row.description || '',
|
||||
freeEpisodes: row.freeEpisodes ?? 3,
|
||||
unlockCoin: row.unlockCoin ?? 0,
|
||||
favoriteCount: row.favoriteCount ?? 0,
|
||||
heat: row.views ?? 0,
|
||||
synopsisLocales,
|
||||
freeEpisodes: toCount(row.freeEpisodes ?? 3),
|
||||
unlockCoin: toCount(row.unlockCoin),
|
||||
favoriteCount: toCount(row.favoriteCount),
|
||||
heat: toCount(row.views),
|
||||
isHot: !!row.isHot,
|
||||
sort: row.sort ?? 0,
|
||||
sort: toCount(row.sort),
|
||||
cover: row.cover || ''
|
||||
})
|
||||
coverFileName.value = row.cover ? coverBasename(row.cover) : ''
|
||||
coverError.value = ''
|
||||
editingCommentCount.value = row.commentCount ?? 0
|
||||
editingLikeCount.value = row.likeCount ?? 0
|
||||
editingCommentCount.value = toCount(row.commentCount)
|
||||
editingLikeCount.value = toCount(row.likeCount)
|
||||
dialogVisible.value = true
|
||||
resetValidation()
|
||||
}
|
||||
|
||||
function dramaTypeLabel(value) {
|
||||
return dramaTypeOptions.find((item) => item.value === value)?.label || '真人剧'
|
||||
}
|
||||
|
||||
function rowCategoryList(row) {
|
||||
if (Array.isArray(row.categoryNames) && row.categoryNames.length) {
|
||||
return row.categoryNames.filter(Boolean)
|
||||
@@ -228,25 +244,26 @@ function rowCategoryList(row) {
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function rowDramaTypes(row) {
|
||||
if (Array.isArray(row.dramaTypes) && row.dramaTypes.length) return row.dramaTypes
|
||||
return row.dramaType ? [row.dramaType] : []
|
||||
}
|
||||
|
||||
async function saveDrama() {
|
||||
if (saving.value || uploadingCover.value) return
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
const titles = payloadLocales(form.titleLocales, textLanguages.value)
|
||||
const missing = textLanguages.value.find((code) => !titles[code])
|
||||
if (!titles.zh || missing) {
|
||||
ElMessage.warning(missing ? `请填写${languageLabel(missing)}名称` : '请输入短剧名称')
|
||||
return
|
||||
}
|
||||
const synopses = payloadLocales(form.synopsisLocales, textLanguages.value)
|
||||
const categoryNames = form.categoryIds
|
||||
.map((id) => categories.value.find((item) => item.id === id)?.name)
|
||||
.filter(Boolean)
|
||||
const payload = {
|
||||
...form,
|
||||
title: form.title.trim(),
|
||||
genre: categoryNames.join(' / ')
|
||||
title: titles.zh,
|
||||
titleLocales: titles,
|
||||
description: synopses.zh || '',
|
||||
synopsisLocales: synopses,
|
||||
genre: categoryNames.join(' / '),
|
||||
tagId: form.tagId || 0
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
@@ -370,7 +387,7 @@ function reviewActions(row) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCategories()
|
||||
await Promise.all([loadCategories(), loadTags()])
|
||||
await load()
|
||||
})
|
||||
</script>
|
||||
@@ -413,22 +430,6 @@ onMounted(async () => {
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id" /></el-select
|
||||
><el-select
|
||||
v-model="dramaTypes"
|
||||
class="drama-multi-select"
|
||||
multiple
|
||||
clearable
|
||||
placeholder="全部内容类型"
|
||||
@change="search"
|
||||
><el-option
|
||||
v-for="item in dramaTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select
|
||||
><el-select v-model="hotFilter" clearable placeholder="全部热度" @change="search"
|
||||
><el-option label="仅热门" :value="true" /><el-option
|
||||
label="非热门"
|
||||
:value="false" /></el-select
|
||||
><el-button @click="resetFilters">重置</el-button></div
|
||||
><span class="table-result-count">共 {{ total }} 部短剧</span></div
|
||||
>
|
||||
@@ -465,24 +466,17 @@ onMounted(async () => {
|
||||
>
|
||||
<el-table-column label="类型" min-width="200"
|
||||
><template #default="{ row }"
|
||||
><div class="tag-wrap"
|
||||
><div v-if="rowCategoryList(row).length" class="tag-wrap"
|
||||
><el-tag v-for="name in rowCategoryList(row)" :key="name" type="info" effect="light">{{
|
||||
name
|
||||
}}</el-tag></div
|
||||
></template
|
||||
><span v-else class="table-empty-text">—</span></template
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column label="内容类型" min-width="180"
|
||||
<el-table-column label="标签" min-width="120"
|
||||
><template #default="{ row }"
|
||||
><div class="tag-wrap"
|
||||
><el-tag
|
||||
v-for="type in rowDramaTypes(row)"
|
||||
:key="type"
|
||||
:type="type === 'ANIME' ? 'warning' : 'info'"
|
||||
effect="light"
|
||||
>{{ dramaTypeLabel(type) }}</el-tag
|
||||
></div
|
||||
></template
|
||||
><span v-if="row.tagName">{{ row.tagName }}</span
|
||||
><span v-else class="table-empty-text">—</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="集数" width="90"
|
||||
@@ -513,12 +507,6 @@ onMounted(async () => {
|
||||
}}</el-tag></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="热门" width="90"
|
||||
><template #default="{ row }"
|
||||
><el-tag v-if="row.isHot" type="danger" effect="light">热门</el-tag
|
||||
><span v-else class="table-empty-text">—</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column prop="updatedAt" label="更新时间" width="160" />
|
||||
<el-table-column label="审核" width="140"
|
||||
><template #default="{ row }"
|
||||
@@ -576,141 +564,12 @@ onMounted(async () => {
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editingId ? '编辑短剧' : '新建短剧'"
|
||||
width="680px"
|
||||
width="760px"
|
||||
class="drama-editor-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="beforeDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="drama-editor-form"
|
||||
>
|
||||
<section class="editor-section">
|
||||
<h3 class="editor-section-title">基础信息</h3>
|
||||
<div class="editor-grid editor-grid--two">
|
||||
<el-form-item label="短剧名称" prop="title">
|
||||
<el-input v-model="form.title" maxlength="80" placeholder="请输入短剧名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="categoryIds">
|
||||
<el-select
|
||||
v-model="form.categoryIds"
|
||||
class="drama-multi-select"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="请选择类型"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categories"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="内容类型" prop="dramaTypes">
|
||||
<el-select
|
||||
v-model="form.dramaTypes"
|
||||
class="drama-multi-select"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="请选择内容类型"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in dramaTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="简介">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="简要介绍剧情"
|
||||
/>
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<section class="editor-section">
|
||||
<h3 class="editor-section-title">付费设置</h3>
|
||||
<div class="editor-grid editor-grid--two">
|
||||
<el-form-item label="免费集数">
|
||||
<el-input-number
|
||||
v-model="form.freeEpisodes"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:max="200"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="单集解锁金币">
|
||||
<el-input-number
|
||||
v-model="form.unlockCoin"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="editor-grid editor-grid--two">
|
||||
<el-form-item label="是否热门">
|
||||
<el-switch v-model="form.isHot" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序权重">
|
||||
<el-input-number
|
||||
v-model="form.sort"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:max="99999"
|
||||
:precision="0"
|
||||
/>
|
||||
<p class="readonly-stat-hint">数值越大越靠前,相同权重按 ID 倒序</p>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-section">
|
||||
<h3 class="editor-section-title">互动数据</h3>
|
||||
<div class="editor-grid editor-grid--two">
|
||||
<el-form-item label="收藏数">
|
||||
<el-input-number
|
||||
v-model="form.favoriteCount"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="点赞数">
|
||||
<div class="readonly-stat">{{ formatCount(editingLikeCount) }}</div>
|
||||
<p class="readonly-stat-hint">全剧分集汇总,只读</p>
|
||||
</el-form-item>
|
||||
<el-form-item label="热度">
|
||||
<el-input-number
|
||||
v-model="form.heat"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="评论数">
|
||||
<div class="readonly-stat">{{ formatCount(editingCommentCount) }}</div>
|
||||
<p class="readonly-stat-hint">全剧汇总,实时统计,只读</p>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-form ref="formRef" :model="form" label-position="top" class="drama-editor-form">
|
||||
<section class="editor-section">
|
||||
<h3 class="editor-section-title">封面</h3>
|
||||
<div class="cover-editor">
|
||||
@@ -776,6 +635,117 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-section">
|
||||
<h3 class="editor-section-title">基础信息</h3>
|
||||
<LocaleTextFields
|
||||
:languages="textLanguages"
|
||||
:locales="form.titleLocales"
|
||||
label="名称"
|
||||
:maxlength="80"
|
||||
@update:languages="changeTextLanguages"
|
||||
/>
|
||||
<div class="editor-grid editor-grid--two">
|
||||
<el-form-item label="类型">
|
||||
<el-select
|
||||
v-model="form.categoryIds"
|
||||
class="drama-multi-select"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
placeholder="可不选"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categories"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签">
|
||||
<el-select v-model="form.tagId" clearable filterable placeholder="可不选">
|
||||
<el-option v-for="item in tags" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<LocaleTextFields
|
||||
:languages="textLanguages"
|
||||
:locales="form.synopsisLocales"
|
||||
label="简介"
|
||||
:maxlength="1000"
|
||||
multiline
|
||||
:show-languages="false"
|
||||
:required="false"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="editor-section">
|
||||
<h3 class="editor-section-title">付费设置</h3>
|
||||
<div class="editor-grid editor-grid--two">
|
||||
<el-form-item label="免费集数">
|
||||
<el-input-number
|
||||
v-model="form.freeEpisodes"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:max="200"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="单集解锁金币">
|
||||
<el-input-number
|
||||
v-model="form.unlockCoin"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number
|
||||
v-model="form.sort"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:max="99999"
|
||||
:precision="0"
|
||||
/>
|
||||
<p class="readonly-stat-hint">数值越大越靠前,相同权重按 ID 倒序</p>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-section">
|
||||
<h3 class="editor-section-title">互动数据</h3>
|
||||
<div class="editor-grid editor-grid--two">
|
||||
<el-form-item label="收藏数">
|
||||
<el-input-number
|
||||
v-model="form.favoriteCount"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="点赞数">
|
||||
<div class="readonly-stat">{{ formatCount(editingLikeCount) }}</div>
|
||||
<p class="readonly-stat-hint">全剧分集汇总,只读</p>
|
||||
</el-form-item>
|
||||
<el-form-item label="热度">
|
||||
<el-input-number
|
||||
v-model="form.heat"
|
||||
class="editor-number"
|
||||
controls-position="right"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="评论数">
|
||||
<div class="readonly-stat">{{ formatCount(editingCommentCount) }}</div>
|
||||
<p class="readonly-stat-hint">全剧汇总,实时统计,只读</p>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@@ -816,6 +786,20 @@ onMounted(async () => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.drama-editor-form :deep(.el-form-item) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.drama-editor-form :deep(.el-form-item__content) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.drama-editor-form :deep(.el-input),
|
||||
.drama-editor-form :deep(.el-textarea),
|
||||
.drama-editor-form :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tag-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in new issue
Block a user