diff --git a/src/api/admin/appUser.ts b/src/api/admin/appUser.ts index ae6bfa7..884938d 100644 --- a/src/api/admin/appUser.ts +++ b/src/api/admin/appUser.ts @@ -13,6 +13,20 @@ export interface AppUserPageParams { registerSource?: string } +export interface WatchHistoryPageParams { + pageIndex?: number + pageSize?: number + keyword?: string + userId?: number + dramaTitle?: string + dramaId?: number + episodeId?: number + source?: string + finished?: boolean + startTime?: string + endTime?: string +} + export const pageAppUsersApi = (params: AppUserPageParams) => { return request.get>({ url: '/admin/app-users/page', params }) } @@ -32,6 +46,13 @@ export const listAppUserWatchHistoryApi = (id: number, limit = 50) => { }) } +export const pageWatchHistoryApi = (params: WatchHistoryPageParams) => { + return request.get>({ + url: '/admin/watch-history/page', + params + }) +} + /** normal / limited / banned,封禁与限制会立即吊销该用户全部会话 */ export const updateAppUserStatusApi = (id: number, status: string, reason?: string) => { return request.put({ diff --git a/src/api/admin/drama.ts b/src/api/admin/drama.ts index 753bff4..abe00bf 100644 --- a/src/api/admin/drama.ts +++ b/src/api/admin/drama.ts @@ -15,7 +15,9 @@ export interface DramaPageParams { keyword?: string status?: string categoryId?: number + categoryIds?: number[] isHot?: boolean + dramaTypes?: Array<'ANIME' | 'LIVE'> } export const pageDramasApi = (params: DramaPageParams) => { diff --git a/src/api/admin/types.ts b/src/api/admin/types.ts index 15780cb..4040874 100644 --- a/src/api/admin/types.ts +++ b/src/api/admin/types.ts @@ -20,11 +20,15 @@ export interface DramaItem { title: string subtitle?: string categoryId?: number + categoryIds?: number[] + categoryNames?: string[] category?: string genre?: string description?: string cover?: string language?: string + dramaType?: 'ANIME' | 'LIVE' + dramaTypes?: Array<'ANIME' | 'LIVE'> episodes?: number freeEpisodes?: number unlockCoin?: number @@ -33,6 +37,9 @@ export interface DramaItem { status: string statusText: string views?: number + favoriteCount?: number + likeCount?: number + commentCount?: number sort?: number publishAt?: string createdAt?: string @@ -43,12 +50,18 @@ export interface DramaSaveData { title: string subtitle?: string categoryId?: number + categoryIds?: number[] genre?: string description?: string cover?: string language?: string + dramaType?: 'ANIME' | 'LIVE' + dramaTypes?: Array<'ANIME' | 'LIVE'> freeEpisodes?: number unlockCoin?: number + favoriteCount?: number + likeCount?: number + heat?: number isHot?: boolean sort?: number } @@ -69,6 +82,10 @@ export interface EpisodeItem { status: string statusText: string views?: number + likeCount?: number + commentCount?: number + favoriteCount?: number + heat?: number sort?: number } @@ -81,6 +98,7 @@ export interface EpisodeSaveData { videoUrl?: string isFree?: boolean unlockCoin?: number + likeCount?: number status?: string sort?: number } @@ -294,11 +312,15 @@ export interface AppUserBanRecord { export interface WatchHistoryItem { id: number + userId?: number + uid?: string + nickname?: string dramaId: number dramaTitle?: string dramaPublished?: boolean episodeId: number episodeNo?: number + episodeTitle?: string positionMs: number durationMs: number finished?: boolean @@ -311,7 +333,10 @@ export interface WatchHistoryItem { export interface CommentItem { id: number dramaId: number + dramaTitle?: string episodeId: number + episodeNo?: number + episodeTitle?: string parentId: number rootId: number userId: number @@ -330,9 +355,20 @@ export interface CommentItem { export interface ReportItem { id: number reporterId: number + reporterNickname?: string + reporterUid?: string /** COMMENT / USER / DRAMA */ targetType: string targetId: number + targetSummary?: string + dramaId?: number + dramaTitle?: string + episodeId?: number + episodeNo?: number + episodeTitle?: string + commentContent?: string + targetNickname?: string + targetUid?: string reasonCode: string description: string /** pending / handled / rejected */ diff --git a/src/styles/business/drama-management.css b/src/styles/business/drama-management.css index da70d90..4cb4cf9 100644 --- a/src/styles/business/drama-management.css +++ b/src/styles/business/drama-management.css @@ -147,6 +147,24 @@ width: 180px !important; } +.readonly-stat { + min-width: 180px; + height: 32px; + padding: 0 12px; + color: var(--el-text-color-secondary); + line-height: 32px; + background: var(--el-fill-color-lighter); + border: 1px solid var(--el-border-color-lighter); + border-radius: var(--el-border-radius-base); +} + +.readonly-stat-hint { + margin: 4px 0 0; + color: var(--el-text-color-placeholder); + font-size: 12px; + line-height: 1.4; +} + .cover-editor { display: flex; gap: 18px; diff --git a/src/views/contentManagement/DramaManagementView.vue b/src/views/contentManagement/DramaManagementView.vue index 9941efe..cba1ef5 100644 --- a/src/views/contentManagement/DramaManagementView.vue +++ b/src/views/contentManagement/DramaManagementView.vue @@ -29,7 +29,8 @@ const categories = ref([]) const keyword = ref('') const statusFilter = ref(undefined) -const categoryId = ref(undefined) +const categoryIds = ref([]) +const dramaTypes = ref([]) const hotFilter = ref(undefined) const pageIndex = ref(1) const pageSize = ref(10) @@ -41,13 +42,18 @@ const coverPercent = ref(0) const coverError = ref('') const coverFileName = ref('') const editingId = ref(null) +const editingCommentCount = ref(0) +const editingLikeCount = ref(0) const formRef = ref() const form = reactive({ title: '', - categoryId: undefined, + categoryIds: [], + dramaTypes: ['LIVE'], description: '', freeEpisodes: 3, unlockCoin: 0, + favoriteCount: 0, + heat: 0, isHot: false, cover: '' }) @@ -57,7 +63,12 @@ const COVER_TYPES = ['image/jpeg', 'image/png', 'image/webp'] const formRules = { title: [{ required: true, message: '请输入短剧名称', trigger: 'blur' }], - categoryId: [{ required: true, message: '请选择短剧类型', trigger: 'change' }] + categoryIds: [ + { type: 'array', required: true, min: 1, message: '请至少选择一个类型', trigger: 'change' } + ], + dramaTypes: [ + { type: 'array', required: true, min: 1, message: '请至少选择一个内容类型', trigger: 'change' } + ] } const coverDisplayName = computed( @@ -78,6 +89,11 @@ const statusOptions = [ { code: 'offline', text: '已下线' } ] +const dramaTypeOptions = [ + { value: 'ANIME', label: '漫剧' }, + { value: 'LIVE', label: '真人剧' } +] + const actionMeta = { submit: { label: '提交审核', done: '已提交审核' }, approve: { label: '审核通过', done: '已通过审核' }, @@ -94,7 +110,8 @@ async function load() { pageSize: pageSize.value, keyword: keyword.value.trim() || undefined, status: statusFilter.value || undefined, - categoryId: categoryId.value, + categoryIds: categoryIds.value.length ? categoryIds.value : undefined, + dramaTypes: dramaTypes.value.length ? dramaTypes.value : undefined, isHot: hotFilter.value }) list.value = res.data?.list || [] @@ -117,7 +134,8 @@ function search() { function resetFilters() { keyword.value = '' statusFilter.value = undefined - categoryId.value = undefined + categoryIds.value = [] + dramaTypes.value = [] hotFilter.value = undefined search() } @@ -142,15 +160,20 @@ function openCreate() { editingId.value = null Object.assign(form, { title: '', - categoryId: undefined, + categoryIds: [], + dramaTypes: ['LIVE'], description: '', freeEpisodes: 3, unlockCoin: 0, + favoriteCount: 0, + heat: 0, isHot: false, cover: '' }) coverFileName.value = '' coverError.value = '' + editingCommentCount.value = 0 + editingLikeCount.value = 0 dialogVisible.value = true resetValidation() } @@ -159,19 +182,54 @@ function openEdit(row) { editingId.value = row.id Object.assign(form, { title: row.title, - categoryId: row.categoryId || undefined, + 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'], description: row.description || '', freeEpisodes: row.freeEpisodes ?? 3, unlockCoin: row.unlockCoin ?? 0, + favoriteCount: row.favoriteCount ?? 0, + heat: row.views ?? 0, isHot: !!row.isHot, cover: row.cover || '' }) coverFileName.value = row.cover ? coverBasename(row.cover) : '' coverError.value = '' + editingCommentCount.value = row.commentCount ?? 0 + editingLikeCount.value = row.likeCount ?? 0 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) + } + if (!row.genre) return [] + return row.genre + .split('/') + .map((name) => name.trim()) + .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 { @@ -179,11 +237,13 @@ async function saveDrama() { } catch { return } - const category = categories.value.find((item) => item.id === form.categoryId) + const categoryNames = form.categoryIds + .map((id) => categories.value.find((item) => item.id === id)?.name) + .filter(Boolean) const payload = { ...form, title: form.title.trim(), - genre: category?.name || '' + genre: categoryNames.join(' / ') } saving.value = true try { @@ -338,12 +398,30 @@ onMounted(async () => { :key="item.code" :label="item.text" :value="item.code" /> { >— - + + + - + + + + + diff --git a/src/views/contentManagement/EpisodeManagementView.vue b/src/views/contentManagement/EpisodeManagementView.vue index 6e1b4a9..0a5e5fd 100644 --- a/src/views/contentManagement/EpisodeManagementView.vue +++ b/src/views/contentManagement/EpisodeManagementView.vue @@ -35,6 +35,7 @@ const videoFileName = ref('') const previewVisible = ref(false) const previewEpisode = ref(null) const editingId = ref(null) +const editingCommentCount = ref(0) const formRef = ref() const form = reactive({ number: 1, @@ -43,6 +44,7 @@ const form = reactive({ videoUrl: '', isFree: true, unlockCoin: 0, + likeCount: 0, status: 'draft' }) @@ -127,10 +129,12 @@ function openCreate() { videoUrl: '', isFree: true, unlockCoin: selectedDrama.value?.unlockCoin ?? 0, + likeCount: 0, status: 'draft' }) videoFileName.value = '' videoError.value = '' + editingCommentCount.value = 0 dialogVisible.value = true resetValidation() } @@ -144,10 +148,12 @@ function openEdit(row) { videoUrl: row.videoUrl || '', isFree: !!row.isFree, unlockCoin: row.unlockCoin || 0, + likeCount: row.likeCount ?? 0, status: row.status }) videoFileName.value = row.videoUrl ? episodeBasename(row.videoUrl) : '' videoError.value = '' + editingCommentCount.value = row.commentCount ?? 0 dialogVisible.value = true resetValidation() } @@ -167,6 +173,7 @@ async function saveEpisode() { videoUrl: form.videoUrl.trim(), isFree: form.isFree, unlockCoin: form.isFree ? 0 : form.unlockCoin || 0, + likeCount: form.likeCount, status: form.status } saving.value = true @@ -325,7 +332,9 @@ onMounted(async () => { 当前短剧 {{ selectedDrama?.title || '暂无短剧' }} 共 {{ selectedDrama?.episodes ?? episodes.length }} 集共 {{ selectedDrama?.episodes ?? episodes.length }} 集 · 收藏 + {{ formatCount(selectedDrama?.favoriteCount ?? 0) }} · 热度 + {{ formatCount(selectedDrama?.views ?? 0) }}
@@ -398,6 +407,24 @@ onMounted(async () => { + + + +