feat(资讯): InformationListView 加发布功能 — 新增/编辑弹窗(标题/一级分类下拉/封面图上传/内容), 走 POST /other/information/save|update + /sys/file/upload; 分类来自 /other/category/list?type=3&level=1(排除无level脏数据9999); 修复 devbox 构建缺 @types/node; 验证: admin 弹窗分类下拉含行业动态, API 创建行业动态新闻后前端 NewsView 行业动态tab显示图片(Playwright 9/9 PASS)
This commit is contained in:
parent
906a675136
commit
fc7d9f9179
18
package-lock.json
generated
18
package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^26.2.0",
|
||||||
"@types/qs": "^6.15.1",
|
"@types/qs": "^6.15.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.71.1",
|
||||||
@ -1280,6 +1281,16 @@
|
|||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "26.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
|
||||||
|
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~8.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.15.1",
|
"version": "6.15.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.15.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.15.1.tgz",
|
||||||
@ -2913,6 +2924,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unimport": {
|
"node_modules/unimport": {
|
||||||
"version": "3.14.6",
|
"version": "3.14.6",
|
||||||
"resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz",
|
"resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz",
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^26.2.0",
|
||||||
"@types/qs": "^6.15.1",
|
"@types/qs": "^6.15.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.71.1",
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* F006 资讯列表 - P1 真 view
|
* F006 资讯列表 - 真 view + 发布功能 (2026-08-08 增强)
|
||||||
*
|
*
|
||||||
* 走 /other/information/page
|
* 列表: GET /other/information/page
|
||||||
* 真实接口 (2026-08-07 探测: 200, total=7)
|
* 新增: POST /other/information/save (title/categoryId/categoryName/content/images)
|
||||||
* 字段: id/title/categoryId/categoryName/content/createTime
|
* 修改: POST /other/information/update
|
||||||
|
* 删除: POST /other/information/delete (body {id})
|
||||||
|
* 分类: GET /other/category/list?type=3&level=1 (一级分类, 排除无 level 的脏数据)
|
||||||
|
* 图片: POST /sys/file/upload (FormData, 返 data.url) — 参考旧系统 information-add-or-update.vue
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, reactive } from 'vue'
|
import { onMounted, ref, reactive, computed } from 'vue'
|
||||||
import { httpGet } from '@/utils/request'
|
import { httpGet, httpPost, httpDelete } from '@/utils/request'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||||
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
import PageHeader from '@/components/PageHeader/PageHeader.vue'
|
||||||
|
|
||||||
@ -16,6 +20,8 @@ interface Info {
|
|||||||
title: string
|
title: string
|
||||||
categoryId: string
|
categoryId: string
|
||||||
categoryName: string
|
categoryName: string
|
||||||
|
content: string
|
||||||
|
images: string
|
||||||
createTime: string
|
createTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,39 +29,233 @@ const list = ref<Info[]>([])
|
|||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const query = reactive({ page: 1, limit: 10 })
|
const query = reactive({ page: 1, limit: 10 })
|
||||||
|
const search = reactive({ title: '' })
|
||||||
|
|
||||||
|
const categories = ref<{ id: string; name: string }[]>([])
|
||||||
|
const categoryLoading = ref(false)
|
||||||
|
|
||||||
|
// === 弹窗状态 ===
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const editMode = ref<'create' | 'update'>('create')
|
||||||
|
const saving = ref(false)
|
||||||
|
const uploadLoading = ref(false)
|
||||||
|
const editForm = reactive({
|
||||||
|
id: '' as string,
|
||||||
|
title: '',
|
||||||
|
categoryId: '' as string,
|
||||||
|
categoryName: '',
|
||||||
|
content: '',
|
||||||
|
images: '',
|
||||||
|
})
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ prop: 'id', label: 'ID', width: 90 },
|
{ prop: 'id', label: 'ID', width: 90 },
|
||||||
{ prop: 'title', label: '资讯标题', minWidth: 280 },
|
{ prop: 'title', label: '资讯标题', minWidth: 260 },
|
||||||
{ prop: 'categoryName', label: '分类', width: 140, formatter: (r: Info) => r.categoryName || '--' },
|
{ prop: 'categoryName', label: '分类', width: 120, formatter: (r: Info) => r.categoryName || '--' },
|
||||||
{ prop: 'createTime', label: '发布时间', width: 180 }
|
{ prop: 'createTime', label: '发布时间', width: 170 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const imgStyle = computed(() => ({
|
||||||
|
maxWidth: '140px',
|
||||||
|
maxHeight: '48px',
|
||||||
|
objectFit: 'cover' as const,
|
||||||
|
borderRadius: '4px',
|
||||||
|
}))
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const resp: any = await httpGet('/other/information/page', { page: query.page, limit: query.limit })
|
const params: any = { page: query.page, limit: query.limit }
|
||||||
|
if (search.title.trim()) params.title = search.title.trim()
|
||||||
|
const resp: any = await httpGet('/other/information/page', params)
|
||||||
list.value = resp?.list || []
|
list.value = resp?.list || []
|
||||||
total.value = Number(resp?.total || 0)
|
total.value = Number(resp?.total || 0)
|
||||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
}
|
}
|
||||||
function onPageChange(p: number) { query.page = p; load() }
|
function onPageChange(p: number) { query.page = p; load() }
|
||||||
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
function onLimitChange(l: number) { query.limit = l; query.page = 1; load() }
|
||||||
onMounted(load)
|
function onSearch() { query.page = 1; load() }
|
||||||
|
function onReset() { search.title = ''; query.page = 1; load() }
|
||||||
|
|
||||||
|
// === 分类 ===
|
||||||
|
async function loadCategories() {
|
||||||
|
categoryLoading.value = true
|
||||||
|
try {
|
||||||
|
const data: any = await httpGet('/other/category/list', { type: 3, level: 1 })
|
||||||
|
categories.value = (data || [])
|
||||||
|
.filter((c: any) => c.level === 1 && c.id)
|
||||||
|
.map((c: any) => ({ id: String(c.id), name: c.name }))
|
||||||
|
.sort((a: any, b: any) => (a.name > b.name ? 1 : -1))
|
||||||
|
} catch { categories.value = [] } finally { categoryLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 新增/编辑 ===
|
||||||
|
function openCreate() {
|
||||||
|
editMode.value = 'create'
|
||||||
|
editForm.id = ''; editForm.title = ''; editForm.categoryId = ''; editForm.categoryName = ''; editForm.content = ''; editForm.images = ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
loadCategories()
|
||||||
|
}
|
||||||
|
function openEdit(row: Info) {
|
||||||
|
editMode.value = 'update'
|
||||||
|
editForm.id = String(row.id)
|
||||||
|
editForm.title = row.title || ''
|
||||||
|
editForm.categoryId = row.categoryId ? String(row.categoryId) : ''
|
||||||
|
editForm.categoryName = row.categoryName || ''
|
||||||
|
editForm.content = row.content || ''
|
||||||
|
editForm.images = row.images || ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
loadCategories()
|
||||||
|
}
|
||||||
|
function onCategoryChange(id: string) {
|
||||||
|
const c = categories.value.find(x => x.id === id)
|
||||||
|
editForm.categoryName = c?.name || ''
|
||||||
|
}
|
||||||
|
async function submit() {
|
||||||
|
if (!editForm.title.trim()) { ElMessage.warning('请填写标题'); return }
|
||||||
|
if (!editForm.categoryId) { ElMessage.warning('请选择分类'); return }
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
title: editForm.title.trim(),
|
||||||
|
categoryId: editForm.categoryId,
|
||||||
|
categoryName: editForm.categoryName,
|
||||||
|
content: editForm.content,
|
||||||
|
images: editForm.images,
|
||||||
|
}
|
||||||
|
if (editMode.value === 'create') {
|
||||||
|
await httpPost('/other/information/save', payload)
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
} else {
|
||||||
|
payload.id = editForm.id
|
||||||
|
await httpPost('/other/information/update', payload)
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
load()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.msg || e?.message || '保存失败')
|
||||||
|
} finally { saving.value = false }
|
||||||
|
}
|
||||||
|
async function onDelete(row: Info) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除资讯「${row.title}」吗?`, '提示', { type: 'warning' })
|
||||||
|
await httpDelete('/other/information/delete', { id: row.id })
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
load()
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e === 'cancel' || e === 'close') return
|
||||||
|
ElMessage.error(e?.msg || e?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 图片上传 (FormData + Bearer 头由拦截器自动带) ===
|
||||||
|
async function customUpload(opt: any) {
|
||||||
|
const file = opt.file as File
|
||||||
|
if (file.size > 2 * 1024 * 1024) { ElMessage.error('图片大小不能超过 2MB!'); return }
|
||||||
|
uploadLoading.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', file)
|
||||||
|
const { http } = await import('@/utils/request')
|
||||||
|
const res: any = await http.post('/sys/file/upload', fd, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
const body = res?.data
|
||||||
|
if (body?.code === 0 && body?.data?.url) {
|
||||||
|
editForm.images = body.data.url
|
||||||
|
ElMessage.success('图片上传成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('图片上传失败: ' + (body?.msg || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error('图片上传失败: ' + (e?.message || '网络错误'))
|
||||||
|
} finally { uploadLoading.value = false }
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rr-page">
|
<div class="rr-page">
|
||||||
<PageHeader title="资讯列表" :sub="`平台资讯 (共 ${total} 条)`" />
|
<PageHeader title="资讯列表" :sub="`平台资讯 (共 ${total} 条)`">
|
||||||
|
<el-button type="primary" @click="openCreate()">新增资讯</el-button>
|
||||||
|
</PageHeader>
|
||||||
<div class="rr-card">
|
<div class="rr-card">
|
||||||
|
<div class="rr-filters-row" style="padding: 16px 16px 0;">
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input v-model="search.title" placeholder="搜索标题" clearable style="width: 220px" @keyup.enter="onSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||||
|
<el-button @click="onReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
<DataTable :columns="columns" :data="list"
|
<DataTable :columns="columns" :data="list"
|
||||||
:borderless="true" :loading="loading" :total="total" :page="query.page" :limit="query.limit" :show-pagination="true" row-key="id" empty-text="暂无资讯" @page-change="onPageChange"
|
:borderless="true" :loading="loading" :total="total" :page="query.page" :limit="query.limit" :show-pagination="true" row-key="id" empty-text="暂无资讯" @page-change="onPageChange"
|
||||||
@limit-change="onLimitChange" />
|
@limit-change="onLimitChange">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-table-column label="封面" width="160">
|
||||||
|
<template #default="{ row: r }">
|
||||||
|
<img v-if="r.images" :src="r.images" :style="imgStyle" alt="封面" referrerpolicy="no-referrer" />
|
||||||
|
<span v-else class="rr-muted">无图</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
|
<template #default="{ row: r }">
|
||||||
|
<span class="rr-actions">
|
||||||
|
<el-link type="primary" @click="openEdit(r)">编辑</el-link>
|
||||||
|
<span class="rr-actions__sep">|</span>
|
||||||
|
<el-link type="danger" @click="onDelete(r)">删除</el-link>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="editMode === 'create' ? '新增资讯' : '编辑资讯'" width="640px" destroy-on-close>
|
||||||
|
<el-form label-width="90px" label-position="right">
|
||||||
|
<el-form-item label="标题" required>
|
||||||
|
<el-input v-model="editForm.title" placeholder="资讯标题" maxlength="50" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="一级分类" required>
|
||||||
|
<el-select v-model="editForm.categoryId" placeholder="请选择一级分类" :loading="categoryLoading" filterable style="width: 100%" @change="onCategoryChange">
|
||||||
|
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="封面图片">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<el-upload
|
||||||
|
:show-file-list="false"
|
||||||
|
accept="image/jpeg,image/png,image/jpg"
|
||||||
|
:http-request="customUpload"
|
||||||
|
:disabled="uploadLoading">
|
||||||
|
<img v-if="editForm.images" :src="editForm.images" alt="封面" class="upload-preview" referrerpolicy="no-referrer" />
|
||||||
|
<el-button v-else :loading="uploadLoading">上传封面</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<span class="rr-muted">支持 JPG/PNG, 不超过 2MB</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="资讯内容">
|
||||||
|
<el-input v-model="editForm.content" type="textarea" :rows="8" placeholder="资讯内容(支持 HTML)" maxlength="20000" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.rr-page { max-width: 1400px; }
|
.rr-page { max-width: 1400px; }
|
||||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||||
</style>
|
.rr-filters-row { display: flex; }
|
||||||
|
.rr-muted { color: var(--text-muted, #909399); font-size: 12px; }
|
||||||
|
.rr-actions { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
|
.rr-actions__sep { color: var(--border, #e4e7ed); }
|
||||||
|
.upload-preview { max-width: 160px; max-height: 80px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border, #e4e7ed); }
|
||||||
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user