feat(news): 支持新闻中心多站点隔离功能

新增站点编码配置,支持新闻分类与文章按站点隔离。主要变更包括:
- 数据库表增加 site_code 字段及索引
- 后台管理界面支持按站点筛选
- 前台接口支持通过查询参数或请求头指定站点
- 新增站点配置与解析逻辑
This commit is contained in:
2026-05-05 15:09:49 +08:00
parent d129d64ebd
commit 3daa0273a4
76 changed files with 592 additions and 114 deletions

4
client/env.d.ts vendored
View File

@@ -1,5 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_PORTAL_SITE_CODE?: string
}
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>

View File

@@ -1,4 +1,5 @@
import request from '@/utils/request'
import { portalSiteQueryParams } from '@/config/portalSite'
export const portalApi = {
getCompanyInfo: () => request.get('/portal/company'),
@@ -17,9 +18,12 @@ export const portalApi = {
getCaseCategories: () => request.get('/portal/case/category'),
getCases: (params?: any) => request.get('/portal/case', { params }),
getCaseById: (id: number) => request.get(`/portal/case/${id}`),
getNewsCategories: () => request.get('/portal/news/category'),
getNewsList: (params?: any) => request.get('/portal/news', { params }),
getNewsById: (id: number) => request.get(`/portal/news/${id}`),
getNewsCategories: () =>
request.get('/portal/news/category', { params: { ...portalSiteQueryParams() } }),
getNewsList: (params?: any) =>
request.get('/portal/news', { params: { ...portalSiteQueryParams(), ...params } }),
getNewsById: (id: number) =>
request.get(`/portal/news/${id}`, { params: { ...portalSiteQueryParams() } }),
}
export const adminApi = {
@@ -85,11 +89,12 @@ export const adminApi = {
addCaseCategory: (data: any) => request.post('/admin/case/category', data),
deleteCaseCategory: (id: number) => request.delete(`/admin/case/category/${id}`),
getNewsList: (params?: any) => request.get('/admin/news', { params }),
getNewsList: (params?: any) => request.get('/admin/news', { params: { ...params } }),
addNews: (data: any) => request.post('/admin/news', data),
updateNews: (data: any) => request.put('/admin/news', data),
deleteNews: (id: number) => request.delete(`/admin/news/${id}`),
getNewsCategories: () => request.get('/admin/news/category'),
getNewsCategories: (params?: any) =>
request.get('/admin/news/category', { params: { ...params } }),
addNewsCategory: (data: any) => request.post('/admin/news/category', data),
deleteNewsCategory: (id: number) => request.delete(`/admin/news/category/${id}`),

View File

@@ -0,0 +1,8 @@
/** 前台新闻等接口按站点隔离;与后端 app.portal.allowed-site-codes 一致。不设则走后端默认 site-code。 */
export function portalSiteQueryParams(): Record<string, string> {
const code = import.meta.env.VITE_PORTAL_SITE_CODE as string | undefined
if (code && String(code).trim() !== '') {
return { siteCode: String(code).trim().toLowerCase() }
}
return {}
}

View File

@@ -2,10 +2,19 @@
<div class="admin-crud-page">
<div class="page-header">
<h2>新闻管理</h2>
<el-button type="primary" @click="openDialog()">新增新闻</el-button>
<div class="page-header-filters">
<span style="margin-right:8px">站点</span>
<el-select v-model="filterSiteCode" placeholder="全部站点" clearable style="width:180px" @change="loadList">
<el-option label="全部站点" value="" />
<el-option label="主站 wuhansaga" value="wuhansaga" />
<el-option label="第二站点 saga-secondary" value="saga-secondary" />
</el-select>
<el-button type="primary" style="margin-left:12px" @click="openDialog()">新增新闻</el-button>
</div>
</div>
<el-table :data="list" border stripe v-loading="loading">
<el-table-column prop="newsId" label="ID" width="80" />
<el-table-column prop="siteCode" label="站点" width="130" />
<el-table-column label="封面" width="140">
<template #default="{ row }">
<el-image v-if="row.coverImage" :src="uploadPublicUrl(row.coverImage)" style="width:100px;height:50px;" fit="cover" />
@@ -31,7 +40,17 @@
<el-form :model="form" label-width="120px">
<el-form-item label="标题(中)"><el-input v-model="form.titleZh" /></el-form-item>
<el-form-item label="标题(英)"><el-input v-model="form.titleEn" /></el-form-item>
<el-form-item label="分类ID"><el-input-number v-model="form.categoryId" :min="1" /></el-form-item>
<el-form-item label="站点">
<el-select v-model="form.siteCode" placeholder="请选择" style="width:100%" @change="onFormSiteChange">
<el-option label="主站 wuhansaga" value="wuhansaga" />
<el-option label="第二站点 saga-secondary" value="saga-secondary" />
</el-select>
</el-form-item>
<el-form-item label="分类">
<el-select v-model="form.categoryId" placeholder="请选择分类" style="width:100%">
<el-option v-for="cat in categoryOptions" :key="cat.newsCategoryId" :label="cat.nameZh" :value="cat.newsCategoryId" />
</el-select>
</el-form-item>
<el-form-item label="摘要(中)"><el-input v-model="form.excerptZh" type="textarea" :rows="2" /></el-form-item>
<el-form-item label="摘要(英)"><el-input v-model="form.excerptEn" type="textarea" :rows="2" /></el-form-item>
<el-form-item label="内容(中)"><el-input v-model="form.contentZh" type="textarea" :rows="6" /></el-form-item>
@@ -59,14 +78,37 @@ import { uploadPublicUrl } from '@/utils/uploadUrl'
const loading = ref(false)
const saving = ref(false)
const list = ref<any[]>([])
const categoryOptions = ref<any[]>([])
const filterSiteCode = ref('')
const dialogVisible = ref(false)
const editingId = ref<number | null>(null)
const form = ref<any>({})
async function loadCategoriesForForm() {
const site = form.value.siteCode
if (!site) {
categoryOptions.value = []
return
}
try {
const res = await adminApi.getNewsCategories({ siteCode: site })
categoryOptions.value = res.data ?? []
} catch {
categoryOptions.value = []
}
}
function onFormSiteChange() {
form.value.categoryId = undefined
loadCategoriesForForm()
}
async function loadList() {
loading.value = true
try {
const res = await adminApi.getNewsList()
const params: Record<string, string> = {}
if (filterSiteCode.value) params.siteCode = filterSiteCode.value
const res = await adminApi.getNewsList(params)
list.value = res.data ?? []
} catch {
list.value = []
@@ -76,9 +118,16 @@ async function loadList() {
}
function openDialog(row?: any) {
if (row) { editingId.value = row.newsId; form.value = { ...row } }
else { editingId.value = null; form.value = { sortOrder: 0, isPublished: 0, isFeatured: 0, categoryId: 1 } }
if (row) {
editingId.value = row.newsId
form.value = { ...row }
} else {
editingId.value = null
const defaultSite = filterSiteCode.value || 'wuhansaga'
form.value = { sortOrder: 0, isPublished: 0, isFeatured: 0, siteCode: defaultSite, categoryId: undefined }
}
dialogVisible.value = true
loadCategoriesForForm()
}
async function handleSave() {
@@ -100,5 +149,5 @@ onMounted(loadList)
</script>
<style scoped lang="less">
.admin-crud-page { .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; h2 { font-size: 20px; color: @gray-800; } } }
.admin-crud-page { .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; h2 { font-size: 20px; color: @gray-800; } .page-header-filters { display: flex; align-items: center; flex-wrap: wrap; } } }
</style>