- src/api/admin/product.ts、src/api/admin/notice.ts 对齐后端新接口 - ProductManagementView:列表/筛选/新建编辑/上下架/删除,新建默认下架有提示 - NoticeManagementView:列表/筛选/新建编辑/发布下架/删除 - 状态列一律用接口返回的 statusText,不在前端写死中文 - id 按字符串处理(雪花 id 超过 JS 安全整数范围,数字会被四舍五入)
355 lines
10 KiB
Vue
355 lines
10 KiB
Vue
<!-- 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>
|