feat(distribution): 公告中心支持分站自定义
公告中心(前台公告列表/详情)此前完全没有站点概念,分站访客只能看到主站公告。 本次按「默认继承主站、站长可切仅本站」的口径支持分站自定义。 数据与口径: - 新增 announcements.distribution_site_id(0=主站),前台按站点隔离 - 新增 distribution_sites.announcement_mode:inherit(默认,主站+本站)/ own_only(仅本站) 后端: - 公告模块新增 ResolvePublicSiteScope:主站 [0]、inherit [0, siteID]、own_only [siteID], 站点编码无效/已停用回落主站;列表与详情都按该范围过滤,详情不再允许跨站按 ID 读取 - 管理端公告列表支持按站点筛选(site_id:不传=全部,0=主站,>0=分站)并带出站点名 - 分销模块新增本站公告 CRUD(distribution/announcement.go):强制 distribution_site_id=本站, 先过 ensureSiteAccess,跨站更新/删除/上下架一律 ErrAnnouncementNotFound - UpdateSiteBranding 支持 announcement_mode 切换(非法值报错,读侧非法历史值回落 inherit) - 新增权限 distribution:announcement_view|manage 并授予 super_admin/ops/finance, 站长开通时继承 ops 角色即自动获得 - 路由:站长侧 /api/distribution/announcements*,平台侧 /api/admin/distribution/announcements* 前端: - 分销后台新增「公告中心」页:来源开关(继承主站+本站 / 仅本站)、本站公告列表与 新建/编辑/发布/下架/归档/恢复/删除,空态按来源模式给出引导 - 平台后台公告页新增「站点」列与筛选,新建公告可选择归属站点(主站或指定分站代发) - 前台 PC/移动无需改动:请求已带 X-Distribution-Site-Code,后端按站点隔离,页面已有空态 测试: - 公告模块:主站/继承/仅本站三种可见范围、跨站详情拦截、无效站点回落、管理端站点筛选 - 分销模块:本站公告 CRUD 与跨站防护、公告来源开关校验与非法值容错
This commit is contained in:
@@ -3,6 +3,8 @@ import type { ApiResponse } from '@/shared/types/types'
|
||||
import type { Announcement, PaginatedResult } from '@/features/announcement/api/announcements'
|
||||
|
||||
export interface CreateAnnouncementRequest {
|
||||
/** 归属站点:0(默认)=主站,>0=分站代发 */
|
||||
distribution_site_id?: number
|
||||
title: string
|
||||
content: string
|
||||
category: 'notice' | 'tutorial' | 'rule' | 'faq'
|
||||
@@ -23,6 +25,8 @@ export interface UpdateAnnouncementRequest {
|
||||
export async function fetchAdminAnnouncements(params: {
|
||||
status?: string
|
||||
category?: string
|
||||
/** 站点筛选:不传=全部,0=主站,>0=指定分站 */
|
||||
site_id?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}): Promise<PaginatedResult<Announcement>> {
|
||||
|
||||
@@ -15,6 +15,10 @@ import type {
|
||||
CreateAnnouncementRequest,
|
||||
UpdateAnnouncementRequest,
|
||||
} from '@/features/admin/api/adminAnnouncements'
|
||||
import {
|
||||
fetchDistributionSites,
|
||||
type DistributionSite,
|
||||
} from '@/features/admin/api/adminDistribution'
|
||||
import { uploadAdminFile } from '@/shared/api/files'
|
||||
import { debugError } from '@/shared/utils/debug'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
@@ -44,6 +48,7 @@ const imageSettingsForm = ref({
|
||||
useCustomWidth: true,
|
||||
})
|
||||
const formData = ref<CreateAnnouncementRequest>({
|
||||
distribution_site_id: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
category: 'notice',
|
||||
@@ -52,6 +57,26 @@ const formData = ref<CreateAnnouncementRequest>({
|
||||
is_important: false,
|
||||
})
|
||||
|
||||
// 站点列表:0=主站(默认),其余为分站;公告归属站点只在新建时可选。
|
||||
const distributionSites = ref<DistributionSite[]>([])
|
||||
const currentSiteLabel = computed(() => {
|
||||
if (props.mode === 'create') return ''
|
||||
if (!props.announcement?.distribution_site_id) return '主站'
|
||||
const matched = distributionSites.value.find(
|
||||
item => item.id === props.announcement?.distribution_site_id
|
||||
)
|
||||
return matched ? `${matched.site_name}(${matched.site_code})` : '分站'
|
||||
})
|
||||
|
||||
async function loadDistributionSites() {
|
||||
if (distributionSites.value.length > 0) return
|
||||
try {
|
||||
distributionSites.value = await fetchDistributionSites()
|
||||
} catch (error) {
|
||||
debugError('加载分销站点失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const categories = [
|
||||
{ value: 'notice', label: '通知公告', icon: Bell },
|
||||
{ value: 'tutorial', label: '使用教程', icon: Document },
|
||||
@@ -86,6 +111,7 @@ watch(
|
||||
if (val) {
|
||||
if (props.mode === 'edit' && props.announcement) {
|
||||
formData.value = {
|
||||
distribution_site_id: props.announcement.distribution_site_id,
|
||||
title: props.announcement.title,
|
||||
content: props.announcement.content,
|
||||
category: props.announcement.category as any,
|
||||
@@ -95,6 +121,7 @@ watch(
|
||||
}
|
||||
} else {
|
||||
formData.value = {
|
||||
distribution_site_id: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
category: 'notice',
|
||||
@@ -103,6 +130,7 @@ watch(
|
||||
is_important: false,
|
||||
}
|
||||
}
|
||||
void loadDistributionSites()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
}
|
||||
@@ -278,6 +306,23 @@ function escapeMarkdownImageText(text: string) {
|
||||
<template>
|
||||
<el-dialog :model-value="visible" :title="dialogTitle" width="800px" @close="handleClose">
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="归属站点">
|
||||
<el-select
|
||||
v-if="mode === 'create'"
|
||||
v-model="formData.distribution_site_id"
|
||||
placeholder="选择公告归属站点"
|
||||
>
|
||||
<el-option :value="0" label="主站" />
|
||||
<el-option
|
||||
v-for="site in distributionSites"
|
||||
:key="site.id"
|
||||
:value="site.id"
|
||||
:label="`${site.site_name}(${site.site_code})`"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tag v-else effect="plain">{{ currentSiteLabel }}</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="公告标题" prop="title">
|
||||
<el-input
|
||||
v-model="formData.title"
|
||||
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
type CreateAnnouncementRequest,
|
||||
type UpdateAnnouncementRequest,
|
||||
} from '@/features/admin/api/adminAnnouncements'
|
||||
import {
|
||||
fetchDistributionSites,
|
||||
type DistributionSite,
|
||||
} from '@/features/admin/api/adminDistribution'
|
||||
import type { Announcement } from '@/features/announcement'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import AnnouncementDialog from '../components/AnnouncementDialog.vue'
|
||||
@@ -35,6 +39,9 @@ const currentPageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const statusFilter = ref('')
|
||||
const categoryFilter = ref('')
|
||||
// 站点筛选:'' = 全部,0 = 主站,>0 = 指定分站
|
||||
const siteFilter = ref<number | ''>('')
|
||||
const distributionSites = ref<DistributionSite[]>([])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogMode = ref<'create' | 'edit'>('create')
|
||||
@@ -54,7 +61,23 @@ const statusOptions = [
|
||||
{ value: 'archived', label: '已归档' },
|
||||
]
|
||||
|
||||
onMounted(loadAnnouncements)
|
||||
onMounted(() => {
|
||||
void loadDistributionSites()
|
||||
void loadAnnouncements()
|
||||
})
|
||||
|
||||
async function loadDistributionSites() {
|
||||
try {
|
||||
distributionSites.value = await fetchDistributionSites()
|
||||
} catch {
|
||||
distributionSites.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function siteLabel(row: Announcement): string {
|
||||
if (!row.distribution_site_id) return '主站'
|
||||
return row.distribution_site_name || `分站 #${row.distribution_site_id}`
|
||||
}
|
||||
|
||||
async function loadAnnouncements() {
|
||||
loading.value = true
|
||||
@@ -62,6 +85,7 @@ async function loadAnnouncements() {
|
||||
const result = await fetchAdminAnnouncements({
|
||||
status: statusFilter.value || undefined,
|
||||
category: categoryFilter.value || undefined,
|
||||
site_id: siteFilter.value === '' ? undefined : Number(siteFilter.value),
|
||||
page: currentPage.value,
|
||||
page_size: currentPageSize.value,
|
||||
})
|
||||
@@ -222,6 +246,22 @@ function getStatusType(status: string): '' | 'success' | 'info' | 'warning' {
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-select
|
||||
v-model="siteFilter"
|
||||
placeholder="筛选站点"
|
||||
clearable
|
||||
@change="handleFilterChange"
|
||||
style="width: 170px"
|
||||
>
|
||||
<el-option label="全部站点" :value="''" />
|
||||
<el-option label="主站" :value="0" />
|
||||
<el-option
|
||||
v-for="site in distributionSites"
|
||||
:key="site.id"
|
||||
:value="site.id"
|
||||
:label="`${site.site_name}(${site.site_code})`"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
placeholder="筛选状态"
|
||||
@@ -272,6 +312,14 @@ function getStatusType(status: string): '' | 'success' | 'info' | 'warning' {
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="站点" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.distribution_site_id" type="success" effect="plain" size="small">
|
||||
{{ siteLabel(row) }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" effect="plain" size="small">主站</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分类" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { ApiResponse } from '@/shared/types/types'
|
||||
|
||||
export interface Announcement {
|
||||
id: number
|
||||
/** 归属站点:0=主站,>0=分站 */
|
||||
distribution_site_id: number
|
||||
/** 管理端列表带出的站点名(前台不返回) */
|
||||
distribution_site_name?: string
|
||||
title: string
|
||||
content: string
|
||||
category: string
|
||||
|
||||
Reference in New Issue
Block a user