feat(bulletin-card): 添加公告通知卡片功能

- 新增公告通知卡片页面和相关组件
- 实现公告列表查询、筛选和分页功能
- 添加公告详情页面
- 优化公告卡片样式和布局
This commit is contained in:
Kven 2025-01-14 16:28:04 +08:00
parent 9f98fe832a
commit 8fa3b8f0c3
5 changed files with 520 additions and 0 deletions

View File

@ -12,6 +12,14 @@ export interface BulletinsRecord {
current: number; current: number;
} }
export interface BulletinsList extends BulletinsRecord{
id?: number;
remark: string;
createBy: string;
content: string;
publishTime: string;
}
// 查看详情 // 查看详情
export function queryBulletinsListAll(id: number) { export function queryBulletinsListAll(id: number) {
return axios.get(`/api/rest/bulletin/self/${id}`); return axios.get(`/api/rest/bulletin/self/${id}`);

View File

@ -44,6 +44,17 @@ const USER: AppRouteRecordRaw = {
permissions: ['*'], permissions: ['*'],
}, },
}, },
{
path: 'bulletinCard',
name: 'BulletinCard',
component: () => import('@/views/user/bulletin-card/index.vue'),
meta: {
locale: '公告通知(卡片)',
title: '公告通知(卡片)',
requiresAuth: true,
permissions: ['*'],
},
},
{ {
path: 'messages', path: 'messages',
name: 'Messages', name: 'Messages',

View File

@ -0,0 +1,148 @@
<template>
<div class="container">
<Breadcrumb :items="['个人中心', '公告通知','公告详情']" />
<a-card class="general-card" title=" ">
<div class="announcement-detail">
<div class="header">
<h1>{{ renderData.title }}</h1>
<div class="meta">
<span>作者: {{ renderData.createBy }}</span>
<span
>时间: {{ dayjs(renderData.publishTime).format('YYYY-MM-DD') }}</span
>
</div>
</div>
<a-divider style="margin-top: 0" />
<div v-html="renderData.content" class="content"></div>
<div>
<ul class="attachments">
<li v-for="(item, index) in renderData.attachments" :key="index">
<a
:href="item.url"
target="_blank"
rel="noopener noreferrer"
>
<icon-download />
{{ item.fileName }}
</a>
</li>
</ul>
</div>
</div>
</a-card>
</div>
</template>
<script lang="ts" setup>
import dayjs from 'dayjs';
import { useBulletinStore } from '@/store';
import { useRoute } from 'vue-router';
import { onMounted, ref } from 'vue';
const bulletinStore = useBulletinStore();
const route = useRoute();
const id = Number(route.params.id);
const renderData = ref<any>([]);
// const attachmentList = ref<any>([]);
const fetchData = async (Id: number) => {
const res = await bulletinStore.queryBulletinListAll(Id);
// attachmentList.value = await bulletinStore.queryAttachmentInfo(
// '28452d83420650425d45110c6417bf693b966b29'
// );
// eslint-disable-next-line prefer-destructuring
renderData.value = res.data;
};
onMounted(() => {
fetchData(id);
});
</script>
<style scoped>
.container {
padding: 0 20px 20px 20px;
}
.announcement-detail {
min-width: 800px;
max-width: 800px;
margin: auto;
padding: 20px;
border: 1px solid #ddd;
background-color: #fff; /* 白色背景,与淡蓝色背景区分开来 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
h1 {
font-size: 24px;
margin-bottom: 10px;
}
.meta {
margin-bottom: 20px;
color: #777;
}
.meta span {
margin-right: 20px;
}
.content {
line-height: 1.6;
}
.attachments li {
margin-bottom: 10px;
}
.attachments a {
display: flex;
align-items: center;
font-size: 14px;
color: #1890ff;
text-decoration: none;
}
.attachments a:hover {
text-decoration: underline;
}
.attachments .arco-icon {
margin-right: 8px;
color: #1890ff;
}
.layout-demo :deep(.arco-layout-header),
.layout-demo :deep(.arco-layout-footer),
.layout-demo :deep(.arco-layout-sider-children),
.layout-demo :deep(.arco-layout-content) {
display: flex;
flex-direction: column;
justify-content: center;
color: var(--color-white);
font-size: 16px;
font-stretch: condensed;
text-align: center;
}
.layout-demo :deep(.arco-layout-header),
.layout-demo :deep(.arco-layout-footer) {
height: 64px;
background-color: var(--color-primary-light-4);
}
.layout-demo :deep(.arco-layout-sider) {
width: 206px;
background-color: var(--color-primary-light-3);
}
.layout-demo :deep(.arco-layout-content) {
background-color: rgb(var(--arcoblue-6));
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<div class="card-wrap">
<a-card v-if="loading" :bordered="false" hoverable>
<slot name="skeleton"></slot>
</a-card>
<a-card v-else :bordered="false" hoverable>
<a-space align="start">
<a-card-meta>
<template #title>
<a-typography-text style="margin-right: 10px">
{{ title }}
</a-typography-text>
</template>
<template #description>
<a-descriptions :column="1">
<a-descriptions-item label="创建人">
{{ createBy }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ dayjs(publishTime).format('YYYY-MM-DD HH:mm:ss') }}
</a-descriptions-item>
<a-descriptions-item label="备注">
{{ remark }}
</a-descriptions-item>
</a-descriptions>
</template>
</a-card-meta>
</a-space>
<template #actions>
<div>
<a-space>
<a-button type="outline" @click="handleDetail(id)">
详情
</a-button>
</a-space>
</div>
</template>
</a-card>
</div>
</template>
<script lang="ts" setup>
import router from '@/router';
import dayjs from 'dayjs';
const props = defineProps({
loading: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
remark: {
type: String,
default: '',
},
id: {
type: Number,
default: -1,
},
createBy: {
type: String,
default: '',
},
closeTxt: {
type: String,
default: '',
},
publishTime: {
type: String,
default: '',
},
});
//
const handleDetail = async (id: number) => {
await router.push({ name: 'Details', params: { id } });
};
</script>
<style scoped lang="less">
.card-wrap {
height: 100%;
transition: all 0.3s;
border: 1px solid var(--color-neutral-3);
border-radius: 4px;
&:hover {
transform: translateY(-4px);
// box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.1);
}
:deep(.arco-card) {
height: 100%;
border-radius: 4px;
.arco-card-body {
height: 100%;
.arco-space {
width: 100%;
height: 100%;
.arco-space-item {
height: 100%;
&:last-child {
flex: 1;
}
.arco-card-meta {
height: 100%;
display: flex;
flex-flow: column;
.arco-card-meta-content {
flex: 1;
.arco-card-meta-description {
margin-top: 8px;
color: rgb(var(--gray-6));
line-height: 20px;
font-size: 12px;
}
}
.arco-card-meta-footer {
margin-top: 0;
}
}
}
}
}
}
:deep(.arco-card-meta-title) {
display: flex;
align-items: center;
// To prevent the shaking
line-height: 28px;
}
:deep(.arco-skeleton-line) {
&:last-child {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
}
}
</style>

View File

@ -0,0 +1,210 @@
<template>
<div class="container">
<Breadcrumb :items="['个人中心', '公告通知']" />
<a-card class="general-card" title=" ">
<a-row>
<a-col :flex="1">
<a-form
:model="formModel"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }"
label-align="right"
>
<a-row :gutter="18">
<a-col :span="9">
<a-form-item field="title" label="标题">
<a-input
v-model="formModel.title"
style="width: 360px"
placeholder="请输入标题"
/>
</a-form-item>
</a-col>
<a-col :span="10">
<a-form-item field="Time" label="时间">
<a-range-picker
show-time
format="YYYY-MM-DD HH:mm"
@ok="timeRangs"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="9">-->
<!-- <a-form-item field="isRead" label="状态">-->
<!-- <a-select-->
<!-- v-model="formModel.isRead"-->
<!-- style="width: 360px"-->
<!-- placeholder="请选择状态"-->
<!-- :options="statusOptions"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
</a-form>
</a-col>
<!-- <a-divider style="height: 84px" direction="vertical" />-->
<a-col :flex="'46px'" style="text-align: right">
<a-space :size="18">
<a-button type="primary" @click="search">
<template #icon>
<icon-search />
</template>
查询
</a-button>
<a-button @click="reset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</a-space>
</a-col>
</a-row>
<!-- <a-divider style="margin-top: 0" />-->
<div class="list-wrap">
<a-row class="list-row" :gutter="20">
<a-col
v-for="item in renderData"
:key="item.id"
class="list-col"
:xs="12"
:sm="12"
:md="12"
:lg="6"
:xl="6"
:xxl="6"
>
<CardWrap
:loading="loading"
:id="item.id"
:title="item.title"
:remark="item.remark"
:createBy="item.createBy"
:publishTime="item.publishTime"
open-txt="详情"
>
<template #skeleton>
<a-skeleton :animation="true">
<a-skeleton-line
:widths="['50%', '50%', '100%', '40%']"
:rows="4"
/>
<a-skeleton-line :widths="['40%']" :rows="1" />
</a-skeleton>
</template>
</CardWrap>
</a-col>
</a-row>
</div>
<a-pagination
style="float: right; position: relative; right: 1px; bottom: 25px"
:total="pagination.total"
show-total
show-jumper
show-page-size
@page-size-change="onSizeChange"
@change="onPageChange"
/>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import useLoading from '@/hooks/loading';
import { useBulletinsStore } from '@/store';
import usePagination from '@/hooks/pagination';
import { BulletinsList, BulletinsRecord } from '@/api/bulletins';
import CardWrap from './components/card-wrap.vue';
const generateFormModel = () => {
return {
title: '',
state: '',
publishTimeBegin: '',
publishTimeEnd: '',
};
};
const { loading, setLoading } = useLoading(true);
const renderData = ref<BulletinsList[]>([]);
const formModel = ref(generateFormModel());
const { pagination, setPagination} = usePagination();
const bulletinsStore = useBulletinsStore();
//
const fetchData = async (
params: BulletinsRecord = { size: 8, current: 1 }
) => {
setLoading(true);
try {
const res = await bulletinsStore.getBulletinsList(params);
renderData.value = res.data.records;
setPagination(res.data);
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false);
}
};
//
const timeRangs = (dateString: string[]) => {
// eslint-disable-next-line prefer-destructuring
formModel.value.publishTimeBegin = dateString[0];
// eslint-disable-next-line prefer-destructuring
formModel.value.publishTimeEnd = dateString[1];
};
//
const search = () => {
fetchData({
...pagination,
...formModel.value,
} as unknown as BulletinsRecord);
};
//
const onPageChange = (current: number) => {
setPagination({
current,
})
search();
};
//
const onSizeChange = (total: number) => {
setPagination({
total,
})
search();
};
onMounted(() => {
search();
})
//
const reset = () => {
formModel.value = generateFormModel();
};
</script>
<style scoped>
.container {
padding: 0 20px 20px 20px;
}
:deep(.list-wrap) {
.list-row {
align-items: stretch;
.list-col {
margin-bottom: 16px;
}
}
}
</style>