This commit is contained in:
1 parent
23026dc371
commit
9ce014e7b3
4 files changed
+217
-20
No files matched your search
@@ -4,6 +4,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { AppIcon } from '@/components/AppIcon'
|
||||
import { AppIcons } from '@/constants/icons'
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
listClassificationDramaCandidatesApi,
|
||||
listClassificationDramasApi,
|
||||
listClassificationsApi,
|
||||
saveRankingItemsApi,
|
||||
unbindClassificationDramaApi,
|
||||
updateClassificationApi
|
||||
} from '@/api/admin/dramaClassification'
|
||||
@@ -43,6 +45,7 @@ const activeId = ref(null)
|
||||
const activeClassification = computed(
|
||||
() => classifications.value.find((item) => item.id === activeId.value) || null
|
||||
)
|
||||
const isRanking = computed(() => activeClassification.value?.layoutType === 'RANKING')
|
||||
|
||||
const loading = ref(false)
|
||||
const dramas = ref([])
|
||||
@@ -50,12 +53,19 @@ const total = ref(0)
|
||||
const pageIndex = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const rankingDirty = ref(false)
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const editingId = ref(null)
|
||||
const languages = ref(['zh'])
|
||||
const emptyForm = () => ({ locales: { zh: '' }, sort: 1, status: 1 })
|
||||
const emptyForm = () => ({
|
||||
locales: { zh: '' },
|
||||
code: '',
|
||||
layoutType: 'GRID',
|
||||
sort: 1,
|
||||
status: 1
|
||||
})
|
||||
const form = reactive(emptyForm())
|
||||
|
||||
const candidateVisible = ref(false)
|
||||
@@ -91,10 +101,11 @@ async function loadDramas() {
|
||||
const res = await listClassificationDramasApi(activeId.value, {
|
||||
keyword: keyword.value || undefined,
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value
|
||||
pageSize: isRanking.value ? 50 : pageSize.value
|
||||
})
|
||||
dramas.value = res.data?.list || []
|
||||
total.value = res.data?.total || 0
|
||||
rankingDirty.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -105,6 +116,7 @@ function selectClassification(row) {
|
||||
activeId.value = row.id
|
||||
pageIndex.value = 1
|
||||
keyword.value = ''
|
||||
rankingDirty.value = false
|
||||
loadDramas()
|
||||
}
|
||||
|
||||
@@ -132,6 +144,8 @@ function openEdit(row) {
|
||||
languages.value = languageCodes(locales)
|
||||
Object.assign(form, {
|
||||
locales,
|
||||
code: row.code || '',
|
||||
layoutType: row.layoutType || 'GRID',
|
||||
sort: row.sort ?? 0,
|
||||
status: row.status ?? 1
|
||||
})
|
||||
@@ -150,6 +164,10 @@ async function saveClassification() {
|
||||
const payload = {
|
||||
name: locales.zh,
|
||||
locales,
|
||||
code:
|
||||
form.code ||
|
||||
(form.layoutType === 'RANKING' && !editingId.value ? 'RECOMMEND_RANKING' : undefined),
|
||||
layoutType: form.layoutType,
|
||||
sort: form.sort,
|
||||
status: form.status
|
||||
}
|
||||
@@ -197,7 +215,12 @@ async function removeDrama(row) {
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await unbindClassificationDramaApi(activeId.value, row.id)
|
||||
if (isRanking.value) {
|
||||
const remaining = dramas.value.filter((item) => item.id !== row.id).map((item) => item.id)
|
||||
await saveRankingItemsApi(activeId.value, remaining)
|
||||
} else {
|
||||
await unbindClassificationDramaApi(activeId.value, row.id)
|
||||
}
|
||||
ElMessage.success('已移出')
|
||||
await Promise.all([loadDramas(), loadClassifications()])
|
||||
}
|
||||
@@ -247,11 +270,23 @@ async function confirmBind() {
|
||||
}
|
||||
binding.value = true
|
||||
try {
|
||||
const res = await bindClassificationDramasApi(
|
||||
activeId.value,
|
||||
selectedDramas.value.map((item) => item.id)
|
||||
)
|
||||
ElMessage.success(`已关联 ${res.data ?? selectedDramas.value.length} 部短剧`)
|
||||
if (isRanking.value) {
|
||||
const existingIds = dramas.value.map((item) => item.id)
|
||||
const selectedIds = selectedDramas.value.map((item) => item.id)
|
||||
const nextIds = [...new Set([...existingIds, ...selectedIds])]
|
||||
if (nextIds.length > 50) {
|
||||
ElMessage.warning('推荐榜最多 50 部短剧')
|
||||
return
|
||||
}
|
||||
await saveRankingItemsApi(activeId.value, nextIds)
|
||||
ElMessage.success(`推荐榜已增加 ${selectedIds.length} 部短剧`)
|
||||
} else {
|
||||
const res = await bindClassificationDramasApi(
|
||||
activeId.value,
|
||||
selectedDramas.value.map((item) => item.id)
|
||||
)
|
||||
ElMessage.success(`已关联 ${res.data ?? selectedDramas.value.length} 部短剧`)
|
||||
}
|
||||
candidateVisible.value = false
|
||||
await Promise.all([loadDramas(), loadClassifications()])
|
||||
} finally {
|
||||
@@ -259,6 +294,22 @@ async function confirmBind() {
|
||||
}
|
||||
}
|
||||
|
||||
function onRankingChange() {
|
||||
dramas.value = dramas.value.map((item, index) => ({ ...item, position: index + 1 }))
|
||||
rankingDirty.value = true
|
||||
}
|
||||
|
||||
async function saveRankingOrder() {
|
||||
if (!activeId.value || !dramas.value.length) return
|
||||
await saveRankingItemsApi(
|
||||
activeId.value,
|
||||
dramas.value.map((item) => item.id)
|
||||
)
|
||||
rankingDirty.value = false
|
||||
ElMessage.success('推荐榜顺序已保存')
|
||||
await loadClassifications()
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
if (!value) return '—'
|
||||
return String(value).replace('T', ' ').slice(0, 19)
|
||||
@@ -271,7 +322,7 @@ onMounted(loadClassifications)
|
||||
<PageHeading
|
||||
eyebrow="内容管理"
|
||||
title="分类管理"
|
||||
description="分类是运营自定义的分组,与「类型」无关;一个分类可以关联任意多部短剧。"
|
||||
description="普通分类使用剧目网格;推荐榜使用独立排行布局,并按后台配置顺序展示。"
|
||||
>
|
||||
<el-button v-if="can('drama:create')" type="primary" class="primary-button" @click="openCreate">
|
||||
<AppIcon :icon="AppIcons.add" />
|
||||
@@ -293,9 +344,11 @@ onMounted(loadClassifications)
|
||||
<div class="classification-toolbar__title">
|
||||
{{ activeClassification?.name || '请选择分类' }}
|
||||
<small>共 {{ total }} 部关联短剧</small>
|
||||
<el-tag v-if="isRanking" size="small" type="warning">推荐榜</el-tag>
|
||||
</div>
|
||||
<div class="classification-toolbar__search">
|
||||
<el-input
|
||||
v-if="!isRanking"
|
||||
v-model="keyword"
|
||||
placeholder="搜索短剧名称"
|
||||
clearable
|
||||
@@ -303,7 +356,15 @@ onMounted(loadClassifications)
|
||||
@keyup.enter="search"
|
||||
@clear="search"
|
||||
/>
|
||||
<el-button type="primary" plain @click="search">查询</el-button>
|
||||
<el-button v-if="!isRanking" type="primary" plain @click="search">查询</el-button>
|
||||
<el-button
|
||||
v-if="isRanking && can('drama:update')"
|
||||
type="primary"
|
||||
:disabled="!rankingDirty"
|
||||
@click="saveRankingOrder"
|
||||
>
|
||||
保存排序
|
||||
</el-button>
|
||||
<el-button v-if="can('drama:update')" :disabled="!activeId" @click="openCandidates">
|
||||
<AppIcon :icon="AppIcons.add" />
|
||||
添加短剧
|
||||
@@ -311,7 +372,41 @@ onMounted(loadClassifications)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VueDraggable
|
||||
v-if="isRanking"
|
||||
v-model="dramas"
|
||||
item-key="id"
|
||||
handle=".ranking-row__drag"
|
||||
class="ranking-list"
|
||||
@end="onRankingChange"
|
||||
>
|
||||
<div v-for="(row, index) in dramas" :key="row.id" class="ranking-row">
|
||||
<button class="ranking-row__drag" type="button" title="拖动调整排名">
|
||||
<AppIcon :icon="AppIcons.drag" />
|
||||
</button>
|
||||
<span class="ranking-row__rank">{{ index + 1 }}</span>
|
||||
<img v-if="row.cover" :src="row.cover" class="ranking-row__cover" alt="封面" />
|
||||
<div v-else class="ranking-row__cover ranking-row__cover--empty">无</div>
|
||||
<div class="ranking-row__content">
|
||||
<strong>{{ row.title }}</strong>
|
||||
<span class="status-dot" :class="statusClass(row.status)">
|
||||
{{ STATUS_TEXT[row.status] || row.status }} · {{ row.totalEpisodes || 0 }} 集
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="can('drama:update')"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="removeDrama(row)"
|
||||
>
|
||||
移出
|
||||
</el-button>
|
||||
</div>
|
||||
<el-empty v-if="!dramas.length" description="推荐榜还没有短剧" />
|
||||
</VueDraggable>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="dramas"
|
||||
row-key="id"
|
||||
@@ -353,7 +448,7 @@ onMounted(loadClassifications)
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="classification-pagination">
|
||||
<div v-if="!isRanking" class="classification-pagination">
|
||||
<el-pagination
|
||||
:current-page="pageIndex"
|
||||
:page-size="pageSize"
|
||||
@@ -434,6 +529,74 @@ onMounted(loadClassifications)
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.ranking-row {
|
||||
display: grid;
|
||||
grid-template-columns: 36px 48px 52px minmax(0, 1fr) auto;
|
||||
min-height: 72px;
|
||||
padding: 8px 12px;
|
||||
background: var(--el-bg-color);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ranking-row + .ranking-row {
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.ranking-row__drag {
|
||||
display: inline-flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: grab;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ranking-row__rank {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--el-text-color-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ranking-row__cover {
|
||||
width: 44px;
|
||||
height: 58px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ranking-row__cover--empty {
|
||||
display: flex;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background: var(--el-fill-color-light);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ranking-row__content {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ranking-row__content strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.classification-cover--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -64,6 +64,23 @@ const emit = defineEmits([
|
||||
@update:languages="emit('update:languages', $event)"
|
||||
@update:locales="emit('update:form', { ...form, locales: $event })"
|
||||
/>
|
||||
<el-form-item label="展示样式">
|
||||
<el-radio-group
|
||||
:model-value="form.layoutType"
|
||||
@update:model-value="emit('update:form', { ...form, layoutType: $event })"
|
||||
>
|
||||
<el-radio-button label="GRID">普通分类</el-radio-button>
|
||||
<el-radio-button label="RANKING">推荐榜</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类编码">
|
||||
<el-input
|
||||
:model-value="form.code"
|
||||
maxlength="64"
|
||||
placeholder="例如 RECOMMEND_RANKING"
|
||||
@update:model-value="emit('update:form', { ...form, code: $event.toUpperCase() })"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="classification-form-row">
|
||||
<el-form-item label="排序">
|
||||
<el-input-number
|
||||
|
||||
@@ -29,6 +29,9 @@ const { can } = useRbac()
|
||||
>
|
||||
<div class="classification-list__main">
|
||||
<span class="classification-list__name">{{ item.name }}</span>
|
||||
<el-tag v-if="item.layoutType === 'RANKING'" size="small" type="warning">
|
||||
推荐榜
|
||||
</el-tag>
|
||||
<span class="classification-list__count">{{ item.dramaCount || 0 }}</span>
|
||||
</div>
|
||||
<div class="classification-list__actions" @click.stop>
|
||||
|
||||
Reference in new issue
Block a user