Commit bd21693c by lijiabin

【需求 25171】feat: 后台内容管理各列表新增删除功能,搜索新增标签/创建人/创建时间

parent 2cb5537a
...@@ -445,3 +445,14 @@ export const updateArticleShowLatest = (data: UpdateArticleShowLatestDto) => { ...@@ -445,3 +445,14 @@ export const updateArticleShowLatest = (data: UpdateArticleShowLatestDto) => {
data, data,
}) })
} }
/**
* 后台内容管理删除文章(目前帖子管理在用)
*/
export const deleteArticleByAdmin = (id: number) => {
return service.request<boolean>({
url: '/api/cultureArticle/adminDeleteArticle',
method: 'POST',
data: { id },
})
}
...@@ -18,6 +18,14 @@ export interface ArticleSearchParams extends PageSearchParams { ...@@ -18,6 +18,14 @@ export interface ArticleSearchParams extends PageSearchParams {
status?: BooleanFlag status?: BooleanFlag
// 视频分类id,前台分类栏点击筛选用,只对视频类型生效,不传为全部 // 视频分类id,前台分类栏点击筛选用,只对视频类型生效,不传为全部
categoryId?: number categoryId?: number
// 后台内容管理的搜索条件
/** 标签 id,多选 */
tagIdList?: number[]
/** 创建人,模糊查询 */
createUserName?: string
/** 创建时间范围,秒级时间戳,结束时间要转成当天 23:59:59;清空时设 undefined,不要设 0(0 会变成 create_time <= 0,一条都查不出来) */
createTimeStart?: number
createTimeEnd?: number
} }
/** /**
......
<script setup lang="ts"> <script setup lang="ts">
import { Search, Upload } from '@element-plus/icons-vue' import ContentSearchBar from '../components/ContentSearchBar.vue'
import { useAdminDeleteArticle } from '../components/useAdminDeleteArticle'
import { Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks' import { usePageSearch, useResetData } from '@/hooks'
import { addOrUpdateColumn } from '@/api/backend' import { addOrUpdateColumn } from '@/api/backend'
import { getArticleList, updateArticleRecommendAndSort, recallPush } from '@/api' import { getArticleList, updateArticleRecommendAndSort, recallPush } from '@/api'
...@@ -21,6 +23,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa ...@@ -21,6 +23,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa
}, },
}) })
const handleDelete = useAdminDeleteArticle('专栏', () => refresh())
// 对话框 // 对话框
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增')) const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增'))
...@@ -139,13 +143,7 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -139,13 +143,7 @@ const handleRecallPush = async (row: ArticleItemDto) => {
<div class="official-tag-page"> <div class="official-tag-page">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-section"> <div class="search-section">
<el-input <ContentSearchBar v-model="searchParams" @search="refresh" @reset="reset" />
v-model="searchParams.title"
placeholder="请输入标题"
class="w-200px mr-12px"
></el-input>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div> </div>
<!-- 表格区域 --> <!-- 表格区域 -->
...@@ -241,6 +239,12 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -241,6 +239,12 @@ const handleRecallPush = async (row: ArticleItemDto) => {
<span v-else>{{ pushStatusText(0) }}</span> <span v-else>{{ pushStatusText(0) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
......
<template>
<div class="content-search-bar">
<el-input
v-model="params.title"
placeholder="请输入标题"
clearable
class="filter-item"
style="width: 200px"
@keyup.enter="emit('search')"
/>
<el-select
v-model="params.tagIdList"
multiple
collapse-tags
collapse-tags-tooltip
clearable
placeholder="请选择标签"
class="filter-item"
style="width: 240px"
>
<el-option v-for="tag in tagList" :key="tag.id" :label="tag.title" :value="tag.id" />
</el-select>
<el-input
v-model="params.createUserName"
placeholder="请输入创建人"
clearable
class="filter-item"
style="width: 200px"
@keyup.enter="emit('search')"
/>
<el-date-picker
v-model="createTimeRange"
type="daterange"
value-format="X"
range-separator="至"
start-placeholder="创建开始日期"
end-placeholder="创建结束日期"
class="filter-item"
style="width: 300px"
/>
<div class="filter-item flex items-center">
<el-button type="primary" :icon="Search" @click="emit('search')">搜索</el-button>
<el-button @click="emit('reset')">重置</el-button>
<slot />
</div>
</div>
</template>
<script setup lang="ts">
/**
* 后台内容管理各个列表页(专栏/专访/视频/问吧/实践/帖子)共用的搜索栏:
* 标题、标签(多选)、创建人(模糊)、创建时间范围(年月日)。
* 筛选值直接写进传进来的 searchParams,搜索/重置由页面自己处理;页面特有的按钮放默认插槽。
*/
import { Search } from '@element-plus/icons-vue'
import { storeToRefs } from 'pinia'
import { useTagsStore } from '@/stores/tags'
import { formatSeconds } from '@/utils/app'
import type { ArticleSearchParams } from '@/api'
const params = defineModel<ArticleSearchParams>({ required: true })
const emit = defineEmits<{ search: []; reset: [] }>()
const { tagList } = storeToRefs(useTagsStore())
const createTimeRange = computed<string[] | null>({
get: () => {
const { createTimeStart, createTimeEnd } = params.value
return createTimeStart && createTimeEnd ? [String(createTimeStart), String(createTimeEnd)] : null
},
set: (range) => {
// 后端要秒级时间戳,结束时间转成当天 23:59:59;清空时必须是 undefined,设 0 会一条都查不出来
params.value.createTimeStart = range ? Number(range[0]) : undefined
params.value.createTimeEnd = range ? formatSeconds(Number(range[1])) : undefined
},
})
</script>
<style scoped>
.content-search-bar {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
/* 选择框/日期选择器自带 flex-grow,不禁掉会把一整行撑满 */
.filter-item {
flex: none;
}
</style>
import { deleteArticleByAdmin } from '@/api'
import { useMessageBox } from '@/hooks'
import { push } from 'notivue'
/**
* 后台内容管理各列表页的「删除」:删了前台就看不到了,必须二次确认。
* typeLabel 是页面对应的类型名(帖子/专栏…),用在确认文案里。
*/
export const useAdminDeleteArticle = (typeLabel: string, onDeleted: () => void) => {
const { confirm } = useMessageBox()
return async (row: { id: number; title: string }) => {
await confirm({
title: '删除确认',
message: `确定要删除${typeLabel}《${row.title}》吗?删除后前台将无法查看,且无法恢复。`,
confirmText: '确认删除',
type: 'danger',
})
await deleteArticleByAdmin(row.id)
push.success('删除成功')
onDeleted()
}
}
<script setup lang="ts"> <script setup lang="ts">
import { Search, Upload } from '@element-plus/icons-vue' import ContentSearchBar from '../components/ContentSearchBar.vue'
import { useAdminDeleteArticle } from '../components/useAdminDeleteArticle'
import { Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks' import { usePageSearch, useResetData } from '@/hooks'
import { addOrUpdateColumn } from '@/api/backend' import { addOrUpdateColumn } from '@/api/backend'
import { getArticleList, updateArticleRecommendAndSort, recallPush } from '@/api' import { getArticleList, updateArticleRecommendAndSort, recallPush } from '@/api'
...@@ -21,6 +23,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa ...@@ -21,6 +23,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa
}, },
}) })
const handleDelete = useAdminDeleteArticle('专访', () => refresh())
// 对话框 // 对话框
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增')) const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增'))
...@@ -140,13 +144,7 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -140,13 +144,7 @@ const handleRecallPush = async (row: ArticleItemDto) => {
<div class="official-tag-page"> <div class="official-tag-page">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-section"> <div class="search-section">
<el-input <ContentSearchBar v-model="searchParams" @search="refresh" @reset="reset" />
v-model="searchParams.title"
placeholder="请输入标题"
class="w-200px mr-12px"
></el-input>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div> </div>
<!-- 表格区域 --> <!-- 表格区域 -->
...@@ -242,6 +240,12 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -242,6 +240,12 @@ const handleRecallPush = async (row: ArticleItemDto) => {
<span v-else>{{ pushStatusText(0) }}</span> <span v-else>{{ pushStatusText(0) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { Search, Upload } from '@element-plus/icons-vue' import ContentSearchBar from '../components/ContentSearchBar.vue'
import { useAdminDeleteArticle } from '../components/useAdminDeleteArticle'
import { Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks' import { usePageSearch, useResetData } from '@/hooks'
import { addOrUpdateColumn } from '@/api/backend' import { addOrUpdateColumn } from '@/api/backend'
import { getArticleList, updateArticleRecommendAndSort, recallPush } from '@/api' import {
getArticleList,
updateArticleRecommendAndSort,
recallPush,
} from '@/api'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend' import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend'
import dayjs from 'dayjs' import dayjs from 'dayjs'
...@@ -21,6 +27,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa ...@@ -21,6 +27,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa
}, },
}) })
const handleDelete = useAdminDeleteArticle('帖子', () => refresh())
// 对话框 // 对话框
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增')) const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增'))
...@@ -139,13 +147,7 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -139,13 +147,7 @@ const handleRecallPush = async (row: ArticleItemDto) => {
<div class="official-tag-page"> <div class="official-tag-page">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-section"> <div class="search-section">
<el-input <ContentSearchBar v-model="searchParams" @search="refresh" @reset="reset" />
v-model="searchParams.title"
placeholder="请输入标题"
class="w-200px mr-12px"
></el-input>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div> </div>
<!-- 表格区域 --> <!-- 表格区域 -->
...@@ -241,6 +243,12 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -241,6 +243,12 @@ const handleRecallPush = async (row: ArticleItemDto) => {
<span v-else>{{ pushStatusText(0) }}</span> <span v-else>{{ pushStatusText(0) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { Search, Upload } from '@element-plus/icons-vue' import ContentSearchBar from '../components/ContentSearchBar.vue'
import { useAdminDeleteArticle } from '../components/useAdminDeleteArticle'
import { Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks' import { usePageSearch, useResetData } from '@/hooks'
import { addOrUpdateColumn } from '@/api/backend' import { addOrUpdateColumn } from '@/api/backend'
import { getArticleList, updateArticleRecommendAndSort, getArticleDetail, addOrUpdatePractice } from '@/api' import { getArticleList, updateArticleRecommendAndSort, getArticleDetail, addOrUpdatePractice } from '@/api'
...@@ -24,6 +26,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa ...@@ -24,6 +26,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa
}, },
}) })
const handleDelete = useAdminDeleteArticle('实践', () => refresh())
// 对话框 // 对话框
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增')) const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增'))
...@@ -162,13 +166,7 @@ const handleSubmitPermission = async () => { ...@@ -162,13 +166,7 @@ const handleSubmitPermission = async () => {
<div class="official-tag-page"> <div class="official-tag-page">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-section"> <div class="search-section">
<el-input <ContentSearchBar v-model="searchParams" @search="refresh" @reset="reset" />
v-model="searchParams.title"
placeholder="请输入标题"
class="w-200px mr-12px"
></el-input>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div> </div>
<!-- 表格区域 --> <!-- 表格区域 -->
...@@ -246,6 +244,12 @@ const handleSubmitPermission = async () => { ...@@ -246,6 +244,12 @@ const handleSubmitPermission = async () => {
{{ dayjs(row.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }} {{ dayjs(row.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { Search, Upload } from '@element-plus/icons-vue' import ContentSearchBar from '../components/ContentSearchBar.vue'
import { useAdminDeleteArticle } from '../components/useAdminDeleteArticle'
import { Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks' import { usePageSearch, useResetData } from '@/hooks'
import { addOrUpdateColumn } from '@/api/backend' import { addOrUpdateColumn } from '@/api/backend'
import { getArticleList, updateArticleRecommendAndSort, recallPush } from '@/api' import { getArticleList, updateArticleRecommendAndSort, recallPush } from '@/api'
...@@ -21,6 +23,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa ...@@ -21,6 +23,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa
}, },
}) })
const handleDelete = useAdminDeleteArticle('问吧', () => refresh())
// 对话框 // 对话框
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增')) const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增'))
...@@ -139,13 +143,7 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -139,13 +143,7 @@ const handleRecallPush = async (row: ArticleItemDto) => {
<div class="official-tag-page"> <div class="official-tag-page">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-section"> <div class="search-section">
<el-input <ContentSearchBar v-model="searchParams" @search="refresh" @reset="reset" />
v-model="searchParams.title"
placeholder="请输入标题"
class="w-200px mr-12px"
></el-input>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div> </div>
<!-- 表格区域 --> <!-- 表格区域 -->
...@@ -241,6 +239,12 @@ const handleRecallPush = async (row: ArticleItemDto) => { ...@@ -241,6 +239,12 @@ const handleRecallPush = async (row: ArticleItemDto) => {
<span v-else>{{ pushStatusText(0) }}</span> <span v-else>{{ pushStatusText(0) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { Search, Upload } from '@element-plus/icons-vue' import ContentSearchBar from '../components/ContentSearchBar.vue'
import { useAdminDeleteArticle } from '../components/useAdminDeleteArticle'
import { Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks' import { usePageSearch, useResetData } from '@/hooks'
import { addOrUpdateColumn } from '@/api/backend' import { addOrUpdateColumn } from '@/api/backend'
import { import {
...@@ -31,6 +33,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa ...@@ -31,6 +33,8 @@ const { loading, list, total, reset, goToPage, changePageSize, refresh, searchPa
}, },
}) })
const handleDelete = useAdminDeleteArticle('视频', () => refresh())
// 对话框 // 对话框
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增')) const dialogTitle = computed(() => (form.value.id ? '编辑' : '新增'))
...@@ -158,14 +162,9 @@ const handleEditCategory = (row: ArticleItemDto) => { ...@@ -158,14 +162,9 @@ const handleEditCategory = (row: ArticleItemDto) => {
<div class="official-tag-page"> <div class="official-tag-page">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="search-section"> <div class="search-section">
<el-input <ContentSearchBar v-model="searchParams" @search="refresh" @reset="reset">
v-model="searchParams.title"
placeholder="请输入标题"
class="w-200px mr-12px"
></el-input>
<el-button type="primary" :icon="Search" @click="refresh">搜索</el-button>
<el-button @click="reset">重置</el-button>
<el-button @click="categoryManageDialogRef?.open()">视频分类管理</el-button> <el-button @click="categoryManageDialogRef?.open()">视频分类管理</el-button>
</ContentSearchBar>
</div> </div>
<!-- 表格区域 --> <!-- 表格区域 -->
...@@ -305,6 +304,12 @@ const handleEditCategory = (row: ArticleItemDto) => { ...@@ -305,6 +304,12 @@ const handleEditCategory = (row: ArticleItemDto) => {
<span v-else>{{ pushStatusText(0) }}</span> <span v-else>{{ pushStatusText(0) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment