Files
Yi.Admin/Yi.Ai.Vue3/src/pages/console/announcement/index.vue
2026-01-24 22:08:54 +08:00

405 lines
11 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.
<script setup lang="ts">
import type { AnnouncementDto } from '@/api/announcement/types';
import { Delete, Edit, Plus, Refresh } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { onMounted, ref, watch } from 'vue';
import {
create,
deleteById,
getList,
update,
} from '@/api/announcement';
import { AnnouncementTypeEnum } from '@/api/announcement/types';
// ==================== Tab 切换 ====================
const activeTab = ref<'activity' | 'system'>('system');
// Tab 切换时重新加载数据
function handleTabChange() {
currentPage.value = 1;
fetchList();
}
// 获取当前 Tab 对应的类型枚举值
function getCurrentTypeEnum(): AnnouncementTypeEnum {
return activeTab.value === 'activity' ? AnnouncementTypeEnum.Activity : AnnouncementTypeEnum.System;
}
// ==================== 公告列表管理 ====================
const announcementList = ref<AnnouncementDto[]>([]);
const loading = ref(false);
const searchKey = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
// 公告对话框
const dialogVisible = ref(false);
const dialogTitle = ref('');
const form = ref<Partial<AnnouncementDto>>({});
// 获取公告列表
async function fetchList() {
loading.value = true;
try {
const res = await getList({
searchKey: searchKey.value,
skipCount: (currentPage.value - 1) * pageSize.value,
maxResultCount: pageSize.value,
type: getCurrentTypeEnum(),
});
announcementList.value = res.data.items;
total.value = res.data.totalCount;
}
catch (error: any) {
ElMessage.error(error.message || '获取公告列表失败');
}
finally {
loading.value = false;
}
}
// 打开对话框
function openDialog(type: 'create' | 'edit', row?: AnnouncementDto) {
dialogTitle.value = type === 'create' ? '创建公告' : '编辑公告';
if (type === 'create') {
form.value = {
title: '',
content: [''],
remark: '',
imageUrl: '',
startTime: new Date().toISOString().slice(0, 19),
endTime: '',
type: getCurrentTypeEnum(),
url: '',
};
}
else {
form.value = {
...row,
startTime: row.startTime ? new Date(row.startTime).toISOString().slice(0, 19) : '',
endTime: row.endTime ? new Date(row.endTime).toISOString().slice(0, 19) : '',
};
}
dialogVisible.value = true;
}
// 添加内容项
function addContentItem() {
if (form.value.content && form.value.content.length < 10) {
form.value.content.push('');
}
else {
ElMessage.warning('最多只能添加10条内容');
}
}
// 删除内容项
function removeContentItem(index: number) {
if (form.value.content && form.value.content.length > 1) {
form.value.content.splice(index, 1);
}
else {
ElMessage.warning('至少需要保留一条内容');
}
}
// 保存
async function save() {
if (!form.value.title || !form.value.content || form.value.content.some(c => !c)) {
ElMessage.warning('请填写标题和所有内容项');
return;
}
try {
const data = {
...form.value,
content: form.value.content?.filter(c => c.trim()) || [],
remark: form.value.remark || null,
imageUrl: form.value.imageUrl || null,
endTime: form.value.endTime || null,
url: form.value.url || null,
};
if (form.value.id) {
await update(data as any);
ElMessage.success('更新成功');
}
else {
await create(data as any);
ElMessage.success('创建成功');
}
dialogVisible.value = false;
fetchList();
}
catch (error: any) {
ElMessage.error(error.message || '保存失败');
}
}
// 删除
async function handleDelete(row: AnnouncementDto) {
try {
await ElMessageBox.confirm('确定要删除该公告吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await deleteById(row.id);
ElMessage.success('删除成功');
fetchList();
}
catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败');
}
}
}
// 分页改变
function handleCurrentChange(page: number) {
currentPage.value = page;
fetchList();
}
function handleSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1;
fetchList();
}
// 初始化
onMounted(() => {
fetchList();
});
</script>
<template>
<div class="announcement-management">
<div class="management-container">
<!-- Tab 切换 -->
<el-tabs v-model="activeTab" class="announcement-tabs" @tab-change="handleTabChange">
<el-tab-pane label="系统公告" name="system" />
<el-tab-pane label="活动公告" name="activity" />
</el-tabs>
<!-- 顶部操作栏 -->
<div class="action-bar">
<el-input
v-model="searchKey"
placeholder="搜索标题或备注"
clearable
style="width: 250px; margin-right: 10px"
@keyup.enter="fetchList"
>
<template #append>
<el-button :icon="Refresh" @click="fetchList" />
</template>
</el-input>
<el-button type="primary" :icon="Plus" @click="openDialog('create')">
新建公告
</el-button>
</div>
<!-- 表格 -->
<div class="table-wrapper">
<el-table
v-loading="loading"
:data="announcementList"
border
stripe
style="width: 100%"
height="100%"
>
<el-table-column prop="title" label="标题" min-width="180" show-overflow-tooltip />
<el-table-column prop="content" label="内容预览" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
{{ row.content?.join(' / ') || '-' }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.remark || '-' }}
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="160" />
<el-table-column prop="endTime" label="结束时间" width="160">
<template #default="{ row }">
{{ row.endTime || '-' }}
</template>
</el-table-column>
<el-table-column prop="creationTime" label="创建时间" width="160" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openDialog('edit', row)">
编辑
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
:hide-on-single-page="false"
background
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
<el-form :model="form" label-width="100px">
<el-form-item label="标题" required>
<el-input v-model="form.title" placeholder="请输入公告标题" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item label="内容" required>
<div style="width: 100%">
<div
v-for="(item, index) in form.content"
:key="index"
style="display: flex; gap: 8px; margin-bottom: 8px"
>
<el-input
v-model="form.content![index]"
placeholder="请输入内容"
maxlength="500"
show-word-limit
/>
<el-button
v-if="form.content && form.content.length > 1"
type="danger"
:icon="Delete"
circle
size="small"
@click="removeContentItem(index)"
/>
</div>
<el-button
v-if="form.content && form.content.length < 10"
type="primary"
:icon="Plus"
size="small"
@click="addContentItem"
>
添加内容项
</el-button>
</div>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入备注(可选)"
maxlength="500"
show-word-limit
:rows="2"
/>
</el-form-item>
<el-form-item label="图片URL">
<el-input v-model="form.imageUrl" placeholder="请输入图片URL可选" maxlength="500" />
</el-form-item>
<el-form-item label="跳转链接">
<el-input v-model="form.url" placeholder="请输入跳转链接(可选)" maxlength="500" />
</el-form-item>
<el-form-item label="开始时间" required>
<el-date-picker
v-model="form.startTime"
type="datetime"
placeholder="选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DDTHH:mm:ss"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="结束时间">
<el-date-picker
v-model="form.endTime"
type="datetime"
placeholder="选择结束时间(可选)"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DDTHH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">
取消
</el-button>
<el-button type="primary" @click="save">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.announcement-management {
height: 100vh;
padding: 16px;
box-sizing: border-box;
background: #f5f7fa;
.management-container {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
overflow: hidden;
}
.announcement-tabs {
padding: 0 16px;
flex-shrink: 0;
:deep(.el-tabs__header) {
margin-bottom: 0;
}
}
.action-bar {
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.table-wrapper {
flex: 1;
overflow: hidden;
padding: 16px 16px 0 16px;
min-height: 0;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding: 16px;
flex-shrink: 0;
border-top: 1px solid #eee;
}
}
</style>