增加通知公告功能
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// OA 通知公告
|
||||
// 响应拦截器只返回 {code, data, msg} 包装,调用方需自行取 res.data
|
||||
|
||||
export function getNoticeList(params) {
|
||||
return request({
|
||||
url: '/backend/oa/notice/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 工作台/仪表盘用:已发布公告,置顶优先
|
||||
export function getNoticePortal(params) {
|
||||
return request({
|
||||
url: '/backend/oa/notice/portal',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getNoticeDetail(id) {
|
||||
return request({
|
||||
url: `/backend/oa/notice/detail/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createNotice(data) {
|
||||
return request({
|
||||
url: '/backend/oa/notice/create',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateNotice(id, data) {
|
||||
return request({
|
||||
url: `/backend/oa/notice/update/${id}`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteNotice(id) {
|
||||
return request({
|
||||
url: `/backend/oa/notice/delete/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 发布 / 下架切换
|
||||
export function publishNotice(id) {
|
||||
return request({
|
||||
url: `/backend/oa/notice/publish/${id}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 置顶 / 取消置顶切换
|
||||
export function topNotice(id) {
|
||||
return request({
|
||||
url: `/backend/oa/notice/top/${id}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
@@ -93,8 +93,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧 40%:工作日程提醒 -->
|
||||
<!-- 右侧 30%:通知公告 + 工作日程提醒 -->
|
||||
<div class="col-right">
|
||||
<div class="chart-card notice-card">
|
||||
<div class="chart-title notice-title">
|
||||
<span>通知公告</span>
|
||||
<el-button link type="primary" @click="goNotice"
|
||||
>查看全部</el-button
|
||||
>
|
||||
</div>
|
||||
<div v-loading="noticeLoading" class="notice-list">
|
||||
<div
|
||||
v-for="item in notices"
|
||||
:key="item.id"
|
||||
class="notice-item"
|
||||
@click="openNoticeDetail(item)"
|
||||
>
|
||||
<el-tag
|
||||
:type="noticeTypeTag(item.notice_type)"
|
||||
size="small"
|
||||
effect="light"
|
||||
class="notice-type"
|
||||
>
|
||||
{{ noticeTypeLabel(item.notice_type) }}
|
||||
</el-tag>
|
||||
<span v-if="item.is_top === 1" class="notice-top">置顶</span>
|
||||
<span class="notice-text">{{ item.title }}</span>
|
||||
<span class="notice-time">{{ noticeTime(item) }}</span>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!notices.length && !noticeLoading"
|
||||
description="暂无通知公告"
|
||||
:image-size="60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-card schedule-card">
|
||||
<div class="chart-title schedule-title">
|
||||
<span>工作日程提醒</span>
|
||||
@@ -179,6 +213,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通知公告详情 -->
|
||||
<el-dialog
|
||||
v-model="noticeDetailVisible"
|
||||
:title="noticeDetailItem?.title"
|
||||
width="620px"
|
||||
>
|
||||
<div v-if="noticeDetailItem" class="notice-detail-body">
|
||||
<div class="notice-detail-meta">
|
||||
<el-tag
|
||||
:type="noticeTypeTag(noticeDetailItem.notice_type)"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
{{ noticeTypeLabel(noticeDetailItem.notice_type) }}
|
||||
</el-tag>
|
||||
<span class="notice-detail-text"
|
||||
>{{ noticeDetailItem.publisher_name }} ·
|
||||
{{ formatDateTime(noticeDetailItem.publish_time) }}</span
|
||||
>
|
||||
<span class="notice-detail-text"
|
||||
>阅读 {{ noticeDetailItem.read_count || 0 }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="notice-detail-content">
|
||||
{{ noticeDetailItem.content || "暂无内容" }}
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 日程详情抽屉:点击任务后由用户明确选择延期 / 已完成 / 反审核 -->
|
||||
<ScheduleDetail
|
||||
v-model="scheduleDetailVisible"
|
||||
@@ -205,6 +268,10 @@ import { ElMessage } from "element-plus";
|
||||
import * as echarts from "echarts";
|
||||
import { Refresh } from "@element-plus/icons-vue";
|
||||
import { getReimbursementDashboard } from "@/api/reimburse";
|
||||
import {
|
||||
getNoticeDetail,
|
||||
getNoticePortal
|
||||
} from "@/api/oaNotice";
|
||||
import { finishSchedule, getScheduleList } from "@/api/oaSchedule";
|
||||
import ScheduleDetail from "../schedule/components/detail.vue";
|
||||
import SchedulePostpone from "../schedule/components/postponeDialog.vue";
|
||||
@@ -459,6 +526,62 @@ const editSchedule = (item) => {
|
||||
router.push("/apps/oa/schedule");
|
||||
};
|
||||
|
||||
// ---------- 通知公告 ----------
|
||||
const notices = ref([]);
|
||||
const noticeLoading = ref(false);
|
||||
const noticeDetailVisible = ref(false);
|
||||
const noticeDetailItem = ref(null);
|
||||
|
||||
const noticeTypeLabel = (type) => ({ 1: "公告", 2: "活动" }[type] || "通知");
|
||||
const noticeTypeTag = (type) => ({ 1: "primary", 2: "success" }[type] || "info");
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) return "-";
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return String(value);
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(
|
||||
d.getHours()
|
||||
)}:${pad2(d.getMinutes())}`;
|
||||
};
|
||||
|
||||
// 列表右侧时间:今天显示 HH:mm,本年显示 M/D,跨年显示 YYYY-M-D
|
||||
const noticeTime = (item) => {
|
||||
const d = item.publish_time ? new Date(item.publish_time) : null;
|
||||
if (!d || Number.isNaN(d.getTime())) return "";
|
||||
const now = new Date();
|
||||
if (fmtDate(d) === fmtDate(now)) {
|
||||
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
||||
}
|
||||
if (d.getFullYear() === now.getFullYear()) {
|
||||
return `${d.getMonth() + 1}/${d.getDate()}`;
|
||||
}
|
||||
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
||||
};
|
||||
|
||||
const loadNotices = async () => {
|
||||
noticeLoading.value = true;
|
||||
try {
|
||||
const res = await getNoticePortal({ limit: 6 });
|
||||
notices.value = res?.data?.list || [];
|
||||
} catch (error) {
|
||||
console.warn("加载通知公告失败:", error?.message);
|
||||
} finally {
|
||||
noticeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openNoticeDetail = async (item) => {
|
||||
const res = await getNoticeDetail(item.id);
|
||||
if (res?.code === 200) {
|
||||
noticeDetailItem.value = res.data;
|
||||
noticeDetailVisible.value = true;
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "加载公告失败");
|
||||
}
|
||||
};
|
||||
|
||||
const goNotice = () => router.push("/apps/oa/notice");
|
||||
|
||||
const loadDashboard = async () => {
|
||||
try {
|
||||
const data = responseData(await getReimbursementDashboard());
|
||||
@@ -473,6 +596,7 @@ const loadDashboard = async () => {
|
||||
}
|
||||
renderTrendChart();
|
||||
loadScheduleReminders();
|
||||
loadNotices();
|
||||
};
|
||||
|
||||
const handleResize = () => trendChart?.resize();
|
||||
@@ -629,6 +753,90 @@ onBeforeUnmount(() => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.notice-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-height: 80px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
background: #fafbfc;
|
||||
|
||||
&:hover {
|
||||
background: #eef2f7;
|
||||
}
|
||||
|
||||
.notice-type {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notice-top {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.notice-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notice-time {
|
||||
flex-shrink: 0;
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-detail-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.notice-detail-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.notice-detail-content {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.schedule-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑公告' : '发布公告'"
|
||||
size="560px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||
<el-form-item label="公告标题" prop="title">
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
placeholder="请输入公告标题"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-radio-group v-model="form.notice_type">
|
||||
<el-radio-button :value="0">通知</el-radio-button>
|
||||
<el-radio-button :value="1">公告</el-radio-button>
|
||||
<el-radio-button :value="2">活动</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="紧急程度">
|
||||
<el-radio-group v-model="form.level">
|
||||
<el-radio-button :value="0">普通</el-radio-button>
|
||||
<el-radio-button :value="1">重要</el-radio-button>
|
||||
<el-radio-button :value="2">紧急</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="置顶">
|
||||
<el-switch
|
||||
v-model="form.is_top"
|
||||
active-text="置顶显示"
|
||||
inactive-text="不置顶"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="公告内容" prop="content">
|
||||
<el-input
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
maxlength="5000"
|
||||
show-word-limit
|
||||
placeholder="请输入公告正文(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!isPublished">
|
||||
<el-checkbox v-model="form.publish">保存后立即发布</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave"
|
||||
>保存</el-button
|
||||
>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { createNotice, updateNotice } from "@/api/oaNotice";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 编辑时传入的公告对象;null 表示新建
|
||||
notice: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "saved"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit("update:modelValue", value)
|
||||
});
|
||||
|
||||
const formRef = ref(null);
|
||||
const saving = ref(false);
|
||||
const editingId = ref(0);
|
||||
|
||||
const form = reactive({
|
||||
title: "",
|
||||
content: "",
|
||||
notice_type: 0,
|
||||
level: 0,
|
||||
is_top: false,
|
||||
publish: true
|
||||
});
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: "请输入公告标题", trigger: "blur" }]
|
||||
};
|
||||
|
||||
const isEdit = computed(() => !!props.notice);
|
||||
// 已发布的公告保存时不再显示"立即发布"勾选框
|
||||
const isPublished = computed(() => props.notice?.status === 1);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
open => {
|
||||
if (open) {
|
||||
initForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function initForm() {
|
||||
if (props.notice) {
|
||||
editingId.value = props.notice.id;
|
||||
form.title = props.notice.title || "";
|
||||
form.content = props.notice.content || "";
|
||||
form.notice_type = props.notice.notice_type || 0;
|
||||
form.level = props.notice.level || 0;
|
||||
form.is_top = props.notice.is_top === 1;
|
||||
form.publish = props.notice.status === 1;
|
||||
} else {
|
||||
editingId.value = 0;
|
||||
form.title = "";
|
||||
form.content = "";
|
||||
form.notice_type = 0;
|
||||
form.level = 0;
|
||||
form.is_top = false;
|
||||
form.publish = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
title: form.title.trim(),
|
||||
content: form.content,
|
||||
notice_type: form.notice_type,
|
||||
level: form.level,
|
||||
is_top: form.is_top ? 1 : 0,
|
||||
publish: form.publish
|
||||
};
|
||||
const res = isEdit.value
|
||||
? await updateNotice(editingId.value, payload)
|
||||
: await createNotice(payload);
|
||||
if (res?.code === 200) {
|
||||
emit("saved");
|
||||
visible.value = false;
|
||||
}
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="notice-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>通知公告</h2>
|
||||
<p>发布与管理租户内的通知、公告与活动</p>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate"
|
||||
>发布公告</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
clearable
|
||||
placeholder="搜索公告标题"
|
||||
style="width: 220px"
|
||||
@keyup.enter="loadList"
|
||||
/>
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
clearable
|
||||
placeholder="全部状态"
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="草稿" value="0" />
|
||||
<el-option label="已发布" value="1" />
|
||||
<el-option label="已下架" value="2" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filters.notice_type"
|
||||
clearable
|
||||
placeholder="全部类型"
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="通知" value="0" />
|
||||
<el-option label="公告" value="1" />
|
||||
<el-option label="活动" value="2" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" stripe>
|
||||
<el-table-column label="标题" min-width="260" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="title-cell">
|
||||
<el-tag v-if="row.is_top === 1" type="danger" size="small" effect="dark"
|
||||
>置顶</el-tag
|
||||
>
|
||||
<el-tag :type="typeTagType(row.notice_type)" size="small" effect="light">
|
||||
{{ typeLabel(row.notice_type) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="row.level > 0"
|
||||
:type="levelTagType(row.level)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
{{ levelLabel(row.level) }}
|
||||
</el-tag>
|
||||
<span class="title-text" @click="openDetail(row)">{{
|
||||
row.title
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">{{
|
||||
statusLabel(row.status)
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="publisher_name" label="发布人" width="120" />
|
||||
<el-table-column label="发布时间" width="170">
|
||||
<template #default="{ row }">{{
|
||||
formatDateTime(row.publish_time)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="read_count" label="阅读" width="90" />
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="togglePublish(row)">{{
|
||||
row.status === 1 ? "下架" : "发布"
|
||||
}}</el-button>
|
||||
<el-button link type="primary" @click="toggleTop(row)">{{
|
||||
row.is_top === 1 ? "取消置顶" : "置顶"
|
||||
}}</el-button>
|
||||
<el-button link type="primary" @click="openEdit(row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button link type="danger" @click="handleDelete(row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无公告" /></template>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@update:current-page="pagination.page = $event"
|
||||
@update:page-size="pagination.pageSize = $event"
|
||||
@current-change="loadList"
|
||||
@size-change="loadList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoticeEdit v-model="editVisible" :notice="editingItem" @saved="loadList" />
|
||||
|
||||
<!-- 公告详情 -->
|
||||
<el-dialog v-model="detailVisible" :title="detailItem?.title" width="620px">
|
||||
<div v-if="detailItem" class="notice-detail">
|
||||
<div class="detail-meta">
|
||||
<el-tag :type="typeTagType(detailItem.notice_type)" size="small">{{
|
||||
typeLabel(detailItem.notice_type)
|
||||
}}</el-tag>
|
||||
<el-tag
|
||||
v-if="detailItem.level > 0"
|
||||
:type="levelTagType(detailItem.level)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
{{ levelLabel(detailItem.level) }}
|
||||
</el-tag>
|
||||
<span class="meta-text"
|
||||
>{{ detailItem.publisher_name }} ·
|
||||
{{ formatDateTime(detailItem.publish_time) }}</span
|
||||
>
|
||||
<span class="meta-text">阅读 {{ detailItem.read_count || 0 }}</span>
|
||||
</div>
|
||||
<div class="detail-content">{{ detailItem.content || "暂无内容" }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh, Search } from "@element-plus/icons-vue";
|
||||
import NoticeEdit from "./components/edit.vue";
|
||||
import {
|
||||
deleteNotice,
|
||||
getNoticeDetail,
|
||||
getNoticeList,
|
||||
publishNotice,
|
||||
topNotice
|
||||
} from "@/api/oaNotice";
|
||||
|
||||
const filters = reactive({ keyword: "", status: "", notice_type: "" });
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getNoticeList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: filters.keyword,
|
||||
status: filters.status,
|
||||
notice_type: filters.notice_type
|
||||
});
|
||||
const data = res?.data || {};
|
||||
list.value = data.list || [];
|
||||
pagination.total = data.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1;
|
||||
loadList();
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = "";
|
||||
filters.status = "";
|
||||
filters.notice_type = "";
|
||||
pagination.page = 1;
|
||||
loadList();
|
||||
}
|
||||
|
||||
// ---------- 发布/编辑 ----------
|
||||
const editVisible = ref(false);
|
||||
const editingItem = ref(null);
|
||||
|
||||
function openCreate() {
|
||||
editingItem.value = null;
|
||||
editVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(item) {
|
||||
editingItem.value = item;
|
||||
editVisible.value = true;
|
||||
}
|
||||
|
||||
// ---------- 详情 ----------
|
||||
const detailVisible = ref(false);
|
||||
const detailItem = ref(null);
|
||||
|
||||
async function openDetail(row) {
|
||||
const res = await getNoticeDetail(row.id);
|
||||
if (res?.code === 200) {
|
||||
detailItem.value = res.data;
|
||||
detailVisible.value = true;
|
||||
loadList();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 操作 ----------
|
||||
async function togglePublish(row) {
|
||||
const res = await publishNotice(row.id);
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success(res.data?.status === 1 ? "已发布" : "已下架");
|
||||
loadList();
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTop(row) {
|
||||
const res = await topNotice(row.id);
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success(res.data?.is_top === 1 ? "已置顶" : "已取消置顶");
|
||||
loadList();
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除公告「${row.title}」吗?`,
|
||||
"删除确认",
|
||||
{ type: "warning", confirmButtonText: "删除", cancelButtonText: "取消" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await deleteNotice(row.id);
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 展示辅助 ----------
|
||||
function typeLabel(type) {
|
||||
return { 1: "公告", 2: "活动" }[type] || "通知";
|
||||
}
|
||||
function typeTagType(type) {
|
||||
return { 1: "primary", 2: "success" }[type] || "info";
|
||||
}
|
||||
function levelLabel(level) {
|
||||
return { 1: "重要", 2: "紧急" }[level] || "普通";
|
||||
}
|
||||
function levelTagType(level) {
|
||||
return { 1: "warning", 2: "danger" }[level] || "info";
|
||||
}
|
||||
function statusLabel(status) {
|
||||
return { 1: "已发布", 2: "已下架" }[status] || "草稿";
|
||||
}
|
||||
function statusTagType(status) {
|
||||
return { 1: "success", 2: "info" }[status] || "warning";
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
return String(value);
|
||||
}
|
||||
const pad = n => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(
|
||||
d.getHours()
|
||||
)}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
onMounted(loadList);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notice-page {
|
||||
padding: 16px 20px 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.title-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title-text:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.notice-detail {
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -39,10 +39,6 @@
|
||||
v-if="activeTab === 'legalNotice'"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="其他设置" name="other">
|
||||
<otherSettings ref="otherSettingsRef" v-if="activeTab === 'other'" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +49,6 @@ import { ref, onMounted } from "vue";
|
||||
import normalSettings from "./components/normalSettings.vue";
|
||||
import seoSettings from "./components/seoSettings.vue";
|
||||
import contactSettings from "./components/contactSettings.vue";
|
||||
import otherSettings from "./components/otherSettings.vue";
|
||||
import legalNoticeSettings from "./components/legalNotice.vue";
|
||||
import loginVerificationSettings from "./components/loginVerification.vue";
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
Reference in New Issue
Block a user