Files
ReelShortWeb/src/views/operation/NoticeManagementView.vue
T
Vicky db313f4aa6 feat(admin): 新增商品配置与系统公告两个后台页面
- src/api/admin/product.ts、src/api/admin/notice.ts 对齐后端新接口
- ProductManagementView:列表/筛选/新建编辑/上下架/删除,新建默认下架有提示
- NoticeManagementView:列表/筛选/新建编辑/发布下架/删除
- 状态列一律用接口返回的 statusText,不在前端写死中文
- id 按字符串处理(雪花 id 超过 JS 安全整数范围,数字会被四舍五入)
2026-10-16 16:33:22 +08:00

355 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Author: fruit.com.cn -->
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import {
createNoticeApi,
deleteNoticeApi,
pageNoticesApi,
updateNoticeApi,
updateNoticeStatusApi
} from '@/api/admin/notice'
import { useRbac } from '@/stores/rbac'
import { dateCell } from '@/utils/format'
import PageHeading from '@/views/components/PageHeading.vue'
const { can } = useRbac()
const loading = ref(false)
const list = ref([])
const total = ref(0)
const pageIndex = ref(1)
const pageSize = ref(10)
const statusFilter = ref(undefined)
const targetFilter = ref(undefined)
const typeFilter = ref(undefined)
const keyword = ref('')
const dialogVisible = ref(false)
const saving = ref(false)
const editingId = ref(null)
const form = reactive({
title: '',
content: '',
target: 'ALL',
type: 'SYSTEM',
publishAt: null,
expireAt: null
})
/** 取值与后端 ops_notice 的枚举一致;显示文案由接口的 targetText / typeText 给 */
const targetOptions = [
{ value: 'ALL', label: '全部用户' },
{ value: 'USER', label: '普通用户' },
{ value: 'CREATOR', label: '创作者(C 端公告位不展示)' }
]
const typeOptions = [
{ value: 'SYSTEM', label: '系统' },
{ value: 'AUDIT', label: '审核' },
{ value: 'PAYMENT', label: '支付' },
{ value: 'ACTIVITY', label: '活动' }
]
async function load() {
loading.value = true
try {
const res = await pageNoticesApi({
pageIndex: pageIndex.value,
pageSize: pageSize.value,
status: statusFilter.value,
target: targetFilter.value,
type: typeFilter.value,
keyword: keyword.value.trim() || undefined
})
list.value = res?.data?.list || []
total.value = res?.data?.total || 0
} finally {
loading.value = false
}
}
function search() {
pageIndex.value = 1
load()
}
function resetFilters() {
statusFilter.value = undefined
targetFilter.value = undefined
typeFilter.value = undefined
keyword.value = ''
search()
}
function handleSizeChange() {
pageIndex.value = 1
load()
}
function openCreate() {
editingId.value = null
Object.assign(form, {
title: '',
content: '',
target: 'ALL',
type: 'SYSTEM',
publishAt: null,
expireAt: null
})
dialogVisible.value = true
}
function openEdit(row) {
editingId.value = row.id
Object.assign(form, {
title: row.title,
content: row.content || '',
target: row.target,
type: row.type,
publishAt: row.publishAt || null,
expireAt: row.expireAt || null
})
dialogVisible.value = true
}
async function saveNotice() {
if (!form.title.trim()) return ElMessage.warning('请填写公告标题')
if (form.publishAt && form.expireAt && form.expireAt <= form.publishAt) {
return ElMessage.warning('失效时间必须晚于发布时间')
}
const payload = {
title: form.title.trim(),
content: form.content,
target: form.target,
type: form.type,
publishAt: form.publishAt || null,
expireAt: form.expireAt || null
}
saving.value = true
try {
if (editingId.value) {
await updateNoticeApi(editingId.value, payload)
ElMessage.success('公告已更新')
} else {
await createNoticeApi(payload)
ElMessage.success('公告已发布')
pageIndex.value = 1
}
dialogVisible.value = false
await load()
} finally {
saving.value = false
}
}
async function toggleStatus(row) {
const next = row.status === 1 ? 0 : 1
await updateNoticeStatusApi(row.id, next)
ElMessage.success(next === 1 ? '公告已发布' : '公告已下架')
await load()
}
async function confirmDelete(row) {
try {
await ElMessageBox.confirm(`确定删除公告「${row.title}」吗?`, '删除确认', {
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger'
})
} catch {
return
}
await deleteNoticeApi(row.id)
ElMessage.success('公告已删除')
if (list.value.length === 1 && pageIndex.value > 1) pageIndex.value -= 1
await load()
}
onMounted(load)
</script>
<template>
<PageHeading
eyebrow="运营管理"
title="系统公告"
description="发布后客户端公告位立即可见;下架或到期后立即消失。"
>
<el-button
v-if="can('notice:update')"
type="primary"
class="primary-button"
@click="openCreate"
>
<el-icon><Plus /></el-icon>
新建公告
</el-button>
</PageHeading>
<section class="panel library-panel">
<div class="library-toolbar">
<div class="drama-filters">
<el-input
v-model="keyword"
clearable
placeholder="公告标题"
@keyup.enter="search"
@clear="search"
/>
<el-select v-model="targetFilter" clearable placeholder="全部目标" @change="search">
<el-option
v-for="item in targetOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select v-model="typeFilter" clearable placeholder="全部类型" @change="search">
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select v-model="statusFilter" clearable placeholder="全部状态" @change="search">
<el-option label="已发布" :value="1" />
<el-option label="已下架" :value="0" />
</el-select>
<el-button @click="search">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
<span class="table-result-count">共 {{ total }} 条公告</span>
</div>
<el-table
v-loading="loading"
:data="list"
row-key="id"
empty-text="还没有发布公告"
class="drama-el-table"
>
<el-table-column prop="title" label="标题" min-width="220" />
<el-table-column prop="typeText" label="类型" width="100" />
<el-table-column prop="targetText" label="目标" width="120" />
<el-table-column prop="content" label="内容" min-width="240" show-overflow-tooltip />
<el-table-column prop="publishAt" label="发布时间" width="170" :formatter="dateCell" />
<el-table-column prop="expireAt" label="失效时间" width="170" :formatter="dateCell" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-button
v-if="can('notice:update')"
link
:type="row.status === 1 ? 'primary' : 'info'"
@click="toggleStatus(row)"
>{{ row.statusText }}</el-button
>
<el-tag v-else :type="row.status === 1 ? 'success' : 'info'" effect="light">{{
row.statusText
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button
v-if="can('notice:update')"
type="primary"
size="small"
@click="openEdit(row)"
>编辑</el-button
>
<el-button
v-if="can('notice:update')"
type="danger"
size="small"
@click="confirmDelete(row)"
>删除</el-button
>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="pageIndex"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@current-change="load"
@size-change="handleSizeChange"
/>
</div>
</section>
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑公告' : '新建公告'" width="680px">
<el-form label-position="top">
<el-form-item label="公告标题" required>
<el-input v-model="form.title" maxlength="128" show-word-limit />
</el-form-item>
<el-form-item label="公告正文">
<el-input
v-model="form.content"
type="textarea"
:rows="4"
maxlength="2000"
show-word-limit
/>
</el-form-item>
<div class="form-row">
<el-form-item label="投放目标">
<el-select v-model="form.target">
<el-option
v-for="item in targetOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="公告类型">
<el-select v-model="form.type">
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<div class="form-row">
<el-form-item label="发布时间">
<el-date-picker
v-model="form.publishAt"
type="datetime"
value-format="YYYY-MM-DDTHH:mm:ss"
placeholder="留空表示立即发布"
/>
</el-form-item>
<el-form-item label="失效时间">
<el-date-picker
v-model="form.expireAt"
type="datetime"
value-format="YYYY-MM-DDTHH:mm:ss"
placeholder="留空表示长期有效"
/>
</el-form-item>
</div>
<p class="console-hint">
投放目标选「创作者」的公告只在后台留档,客户端公告位不返回;新建即发布,下架后客户端立即不可见。
</p>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="saveNotice">保存</el-button>
</template>
</el-dialog>
</template>