增加通知公告功能
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';
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendOaNoticeController OA通知公告管理。
|
||||
// 数据按租户 tid 隔离;发布后对租户内所有后台用户可见。
|
||||
type BackendOaNoticeController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// Prepare 所有接口执行前确保通知公告表存在。
|
||||
func (c *BackendOaNoticeController) Prepare() {
|
||||
_ = models.EnsureOaNoticeTable()
|
||||
}
|
||||
|
||||
func (c *BackendOaNoticeController) oaNoticeClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
return nil, orm.ErrNoRows
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *BackendOaNoticeController) ontJsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendOaNoticeController) ontOk(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type oaNoticePayload struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
NoticeType int8 `json:"notice_type"`
|
||||
Level int8 `json:"level"`
|
||||
IsTop int8 `json:"is_top"`
|
||||
// Publish 为 true 时保存即发布
|
||||
Publish bool `json:"publish"`
|
||||
}
|
||||
|
||||
// parseOaNoticePayload 读取并校验公告请求体;失败时直接输出错误响应。
|
||||
func (c *BackendOaNoticeController) parseOaNoticePayload() (oaNoticePayload, bool) {
|
||||
var payload oaNoticePayload
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil || json.Unmarshal(raw, &payload) != nil {
|
||||
c.ontJsonErr(400, 400, "参数错误")
|
||||
return payload, false
|
||||
}
|
||||
|
||||
payload.Title = strings.TrimSpace(payload.Title)
|
||||
if payload.Title == "" {
|
||||
c.ontJsonErr(400, 400, "请输入公告标题")
|
||||
return payload, false
|
||||
}
|
||||
if utf8.RuneCountInString(payload.Title) > 100 {
|
||||
c.ontJsonErr(400, 400, "公告标题不能超过100字")
|
||||
return payload, false
|
||||
}
|
||||
if payload.NoticeType < 0 || payload.NoticeType > 2 {
|
||||
payload.NoticeType = 0
|
||||
}
|
||||
if payload.Level < 0 || payload.Level > 2 {
|
||||
payload.Level = 0
|
||||
}
|
||||
if payload.IsTop != 1 {
|
||||
payload.IsTop = 0
|
||||
}
|
||||
payload.Content = strings.TrimSpace(payload.Content)
|
||||
return payload, true
|
||||
}
|
||||
|
||||
// noticeBase 租户内未删除公告的基础查询集
|
||||
func noticeBase(tid int) orm.QuerySeter {
|
||||
return models.Orm.QueryTable(new(models.OaNotice)).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("tid", tid)
|
||||
}
|
||||
|
||||
// List GET /backend/oa/notice/list
|
||||
// 管理端分页列表,支持 keyword/status/notice_type 筛选;置顶优先,再按发布时间倒序。
|
||||
func (c *BackendOaNoticeController) List() {
|
||||
claims, err := c.oaNoticeClaims()
|
||||
if err != nil {
|
||||
c.ontJsonErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
status := strings.TrimSpace(c.GetString("status"))
|
||||
noticeType := strings.TrimSpace(c.GetString("notice_type"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 500 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
qs := noticeBase(claims.TenantId)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("title__icontains", keyword)
|
||||
}
|
||||
if status == "0" || status == "1" || status == "2" {
|
||||
v, _ := strconv.Atoi(status)
|
||||
qs = qs.Filter("status", int8(v))
|
||||
}
|
||||
if noticeType == "0" || noticeType == "1" || noticeType == "2" {
|
||||
v, _ := strconv.Atoi(noticeType)
|
||||
qs = qs.Filter("notice_type", int8(v))
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
|
||||
var list []models.OaNotice
|
||||
_, err = qs.OrderBy("-is_top", "-publish_time", "-id").
|
||||
Limit(pageSize).Offset((page - 1) * pageSize).
|
||||
All(&list)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.ontJsonErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []models.OaNotice{}
|
||||
}
|
||||
|
||||
c.ontOk(map[string]interface{}{"list": list, "total": total})
|
||||
}
|
||||
|
||||
// Portal GET /backend/oa/notice/portal
|
||||
// 工作台/仪表盘用:只返回已发布的公告,置顶优先,取最近 limit 条(默认 5,最多 20)。
|
||||
func (c *BackendOaNoticeController) Portal() {
|
||||
claims, err := c.oaNoticeClaims()
|
||||
if err != nil {
|
||||
c.ontJsonErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := c.GetInt("limit", 5)
|
||||
if limit < 1 || limit > 20 {
|
||||
limit = 5
|
||||
}
|
||||
|
||||
var list []models.OaNotice
|
||||
_, err = noticeBase(claims.TenantId).
|
||||
Filter("status", 1).
|
||||
OrderBy("-is_top", "-publish_time", "-id").
|
||||
Limit(limit).
|
||||
All(&list)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.ontJsonErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []models.OaNotice{}
|
||||
}
|
||||
|
||||
c.ontOk(map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Detail GET /backend/oa/notice/detail/:id
|
||||
// 公告详情,同时累加阅读次数。
|
||||
func (c *BackendOaNoticeController) Detail() {
|
||||
claims, err := c.oaNoticeClaims()
|
||||
if err != nil {
|
||||
c.ontJsonErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.ontJsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var item models.OaNotice
|
||||
err = noticeBase(claims.TenantId).Filter("id", id).One(&item)
|
||||
if err != nil {
|
||||
c.ontJsonErr(404, 404, "公告不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 阅读数自增(并发下允许略有偏差,仅为展示用)
|
||||
_, _ = models.Orm.Raw(
|
||||
"UPDATE yz_backend_oa_notice SET read_count = read_count + 1 WHERE id = ?", id).Exec()
|
||||
item.ReadCount++
|
||||
|
||||
c.ontOk(item)
|
||||
}
|
||||
|
||||
// Create POST /backend/oa/notice/create
|
||||
func (c *BackendOaNoticeController) Create() {
|
||||
claims, err := c.oaNoticeClaims()
|
||||
if err != nil {
|
||||
c.ontJsonErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
payload, ok := c.parseOaNoticePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
item := &models.OaNotice{
|
||||
Tid: claims.TenantId,
|
||||
Title: payload.Title,
|
||||
Content: payload.Content,
|
||||
NoticeType: payload.NoticeType,
|
||||
Level: payload.Level,
|
||||
IsTop: payload.IsTop,
|
||||
Status: 0,
|
||||
PublisherID: uint64(claims.UserID),
|
||||
PublisherName: claims.Username,
|
||||
IsDeleted: 0,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if payload.Publish {
|
||||
item.Status = 1
|
||||
item.PublishTime = &now
|
||||
}
|
||||
|
||||
if _, err := models.Orm.Insert(item); err != nil {
|
||||
c.ontJsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.ontOk(item)
|
||||
}
|
||||
|
||||
// Update POST /backend/oa/notice/update/:id
|
||||
// 编辑公告内容;已发布的公告编辑后保持发布状态(不改变发布时间)。
|
||||
func (c *BackendOaNoticeController) Update() {
|
||||
claims, err := c.oaNoticeClaims()
|
||||
if err != nil {
|
||||
c.ontJsonErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.ontJsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
payload, ok := c.parseOaNoticePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var item models.OaNotice
|
||||
err = noticeBase(claims.TenantId).Filter("id", id).One(&item)
|
||||
if err != nil {
|
||||
c.ontJsonErr(404, 404, "公告不存在")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
item.Title = payload.Title
|
||||
item.Content = payload.Content
|
||||
item.NoticeType = payload.NoticeType
|
||||
item.Level = payload.Level
|
||||
item.IsTop = payload.IsTop
|
||||
item.UpdateTime = &now
|
||||
|
||||
fields := []string{"Title", "Content", "NoticeType", "Level", "IsTop", "UpdateTime"}
|
||||
// 草稿保存时选择"发布"则直接发布
|
||||
if payload.Publish && item.Status != 1 {
|
||||
item.Status = 1
|
||||
item.PublishTime = &now
|
||||
fields = append(fields, "Status", "PublishTime")
|
||||
}
|
||||
|
||||
if _, err := models.Orm.Update(&item, fields...); err != nil {
|
||||
c.ontJsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.ontOk(item)
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/oa/notice/delete/:id 软删除。
|
||||
func (c *BackendOaNoticeController) Delete() {
|
||||
claims, err := c.oaNoticeClaims()
|
||||
if err != nil {
|
||||
c.ontJsonErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.ontJsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := noticeBase(claims.TenantId).Filter("id", id).
|
||||
Update(map[string]interface{}{
|
||||
"IsDeleted": int8(1),
|
||||
"DeleteTime": now,
|
||||
"UpdateTime": now,
|
||||
})
|
||||
if err != nil || n == 0 {
|
||||
c.ontJsonErr(404, 404, "公告不存在或已删除")
|
||||
return
|
||||
}
|
||||
|
||||
c.ontOk(nil)
|
||||
}
|
||||
|
||||
// Publish POST /backend/oa/notice/publish/:id
|
||||
// 发布 / 下架切换:草稿、已下架 -> 已发布(写入发布时间);已发布 -> 已下架。
|
||||
func (c *BackendOaNoticeController) Publish() {
|
||||
claims, err := c.oaNoticeClaims()
|
||||
if err != nil {
|
||||
c.ontJsonErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.ontJsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var item models.OaNotice
|
||||
err = noticeBase(claims.TenantId).Filter("id", id).One(&item)
|
||||
if err != nil {
|
||||
c.ontJsonErr(404, 404, "公告不存在")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if item.Status == 1 {
|
||||
item.Status = 2
|
||||
item.PublishTime = nil
|
||||
} else {
|
||||
item.Status = 1
|
||||
item.PublishTime = &now
|
||||
}
|
||||
item.UpdateTime = &now
|
||||
|
||||
if _, err := models.Orm.Update(&item, "Status", "PublishTime", "UpdateTime"); err != nil {
|
||||
c.ontJsonErr(500, 500, "操作失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.ontOk(map[string]interface{}{"status": item.Status})
|
||||
}
|
||||
|
||||
// Top POST /backend/oa/notice/top/:id 置顶 / 取消置顶切换。
|
||||
func (c *BackendOaNoticeController) Top() {
|
||||
claims, err := c.oaNoticeClaims()
|
||||
if err != nil {
|
||||
c.ontJsonErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.ontJsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var item models.OaNotice
|
||||
err = noticeBase(claims.TenantId).Filter("id", id).One(&item)
|
||||
if err != nil {
|
||||
c.ontJsonErr(404, 404, "公告不存在")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if item.IsTop == 1 {
|
||||
item.IsTop = 0
|
||||
} else {
|
||||
item.IsTop = 1
|
||||
}
|
||||
item.UpdateTime = &now
|
||||
|
||||
if _, err := models.Orm.Update(&item, "IsTop", "UpdateTime"); err != nil {
|
||||
c.ontJsonErr(500, 500, "操作失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.ontOk(map[string]interface{}{"is_top": item.IsTop})
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -113,6 +113,7 @@ func Init(_ string) {
|
||||
new(BackendScheduleReminderSendLog),
|
||||
|
||||
new(OaSchedule),
|
||||
new(OaNotice),
|
||||
new(BackendOaCompensationScheme),
|
||||
new(BackendOaPayroll),
|
||||
new(BackendOaPayrollItem),
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OaNotice OA通知公告: yz_backend_oa_notice
|
||||
// 按租户 tid 隔离:租户内发布,租户内所有后台用户可见。
|
||||
// status: 0-草稿 1-已发布 2-已下架;notice_type: 0-通知 1-公告 2-活动;
|
||||
// level: 0-普通 1-重要 2-紧急;is_top: 0-否 1-是(置顶优先展示)。
|
||||
type OaNotice struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid int `orm:"column(tid)" json:"tid"`
|
||||
Title string `orm:"column(title);size(255)" json:"title"`
|
||||
Content string `orm:"column(content);type(text);null" json:"content"`
|
||||
NoticeType int8 `orm:"column(notice_type);default(0)" json:"notice_type"`
|
||||
Level int8 `orm:"column(level);default(0)" json:"level"`
|
||||
Status int8 `orm:"column(status);default(0)" json:"status"`
|
||||
IsTop int8 `orm:"column(is_top);default(0)" json:"is_top"`
|
||||
PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"`
|
||||
PublisherID uint64 `orm:"column(publisher_id);default(0)" json:"publisher_id"`
|
||||
PublisherName string `orm:"column(publisher_name);size(100);default()" json:"publisher_name"`
|
||||
ReadCount int `orm:"column(read_count);default(0)" json:"read_count"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *OaNotice) TableName() string {
|
||||
return "yz_backend_oa_notice"
|
||||
}
|
||||
|
||||
var oaNoticeTableOnce sync.Once
|
||||
|
||||
// EnsureOaNoticeTable 首次访问通知公告接口时自动建表(若不存在)。
|
||||
// 与日程表保持一致的"运行时自愈"策略,避免新功能上线还要手工执行建表脚本。
|
||||
func EnsureOaNoticeTable() error {
|
||||
if Orm == nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
oaNoticeTableOnce.Do(func() {
|
||||
_, err = Orm.Raw(`
|
||||
CREATE TABLE IF NOT EXISTS yz_backend_oa_notice (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
title varchar(255) NOT NULL DEFAULT '' COMMENT '公告标题',
|
||||
content text COMMENT '公告内容',
|
||||
notice_type tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-通知 1-公告 2-活动',
|
||||
level tinyint NOT NULL DEFAULT 0 COMMENT '紧急程度 0-普通 1-重要 2-紧急',
|
||||
status tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已下架',
|
||||
is_top tinyint NOT NULL DEFAULT 0 COMMENT '是否置顶 0-否 1-是',
|
||||
publish_time datetime DEFAULT NULL COMMENT '发布时间',
|
||||
publisher_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '发布人ID',
|
||||
publisher_name varchar(100) NOT NULL DEFAULT '' COMMENT '发布人姓名',
|
||||
read_count int NOT NULL DEFAULT 0 COMMENT '阅读次数',
|
||||
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
|
||||
create_time datetime DEFAULT NULL COMMENT '创建时间',
|
||||
update_time datetime DEFAULT NULL COMMENT '更新时间',
|
||||
delete_time datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_tid_status (tid, status),
|
||||
KEY idx_tid_top (tid, is_top)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA通知公告表'`).Exec()
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -268,6 +268,16 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/oa/schedule/finish/:id", &controllers.BackendOaScheduleController{}, "post:FinishSchedule")
|
||||
beego.Router("/backend/oa/schedule/carry/:id", &controllers.BackendOaScheduleController{}, "post:CarrySchedule")
|
||||
beego.Router("/backend/oa/schedule/carry-pending", &controllers.BackendOaScheduleController{}, "post:CarryPending")
|
||||
|
||||
// OA通知公告
|
||||
beego.Router("/backend/oa/notice/list", &controllers.BackendOaNoticeController{}, "get:List")
|
||||
beego.Router("/backend/oa/notice/portal", &controllers.BackendOaNoticeController{}, "get:Portal")
|
||||
beego.Router("/backend/oa/notice/detail/:id", &controllers.BackendOaNoticeController{}, "get:Detail")
|
||||
beego.Router("/backend/oa/notice/create", &controllers.BackendOaNoticeController{}, "post:Create")
|
||||
beego.Router("/backend/oa/notice/update/:id", &controllers.BackendOaNoticeController{}, "post:Update")
|
||||
beego.Router("/backend/oa/notice/delete/:id", &controllers.BackendOaNoticeController{}, "delete:Delete")
|
||||
beego.Router("/backend/oa/notice/publish/:id", &controllers.BackendOaNoticeController{}, "post:Publish")
|
||||
beego.Router("/backend/oa/notice/top/:id", &controllers.BackendOaNoticeController{}, "post:Top")
|
||||
}
|
||||
|
||||
// registerOrganizationRoutes 为指定模块前缀注册组织架构路由。
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
-- 通知公告菜单(租户端):挂在“办公自动化”模块目录下。
|
||||
--
|
||||
-- 说明:
|
||||
-- 1. 本脚本只创建/修正 yz_system_menu 菜单记录,不涉及通知公告业务数据表。
|
||||
-- 2. 路由由前端根据登录接口返回的菜单数据动态注册;
|
||||
-- 不需要在 backend/src/router/index.js 中增加静态业务路由。
|
||||
-- 3. 执行完成后,请在角色菜单权限关联表中为目标角色授予“通知公告”菜单权限,
|
||||
-- 然后退出重登或清理菜单缓存。
|
||||
--
|
||||
-- views: [2] = 租户端
|
||||
-- type: 1 = 目录,2 = 页面
|
||||
-- 脚本可重复执行:通过 path 判重,已有记录会被修正为当前配置。
|
||||
|
||||
-- 确保“办公自动化”父级目录存在。
|
||||
INSERT INTO `yz_system_menu`
|
||||
(`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||
SELECT
|
||||
0, '办公自动化', '/apps/oa', '', 'Document', 31, 1, 1, '[2]', 1, '办公自动化模块'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM (SELECT * FROM `yz_system_menu`) AS t
|
||||
WHERE t.`path` = '/apps/oa'
|
||||
);
|
||||
|
||||
SET @oa_pid := (
|
||||
SELECT `id`
|
||||
FROM `yz_system_menu`
|
||||
WHERE `path` = '/apps/oa'
|
||||
ORDER BY `id`
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- 新增“通知公告”页面菜单。
|
||||
INSERT INTO `yz_system_menu`
|
||||
(`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`)
|
||||
SELECT
|
||||
@oa_pid,
|
||||
'通知公告',
|
||||
'/apps/oa/notice',
|
||||
'/apps/oa/notice/index.vue',
|
||||
'Bell',
|
||||
7,
|
||||
1,
|
||||
1,
|
||||
'[2]',
|
||||
2,
|
||||
'通知公告发布与管理'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM (SELECT * FROM `yz_system_menu`) AS t
|
||||
WHERE t.`path` = '/apps/oa/notice'
|
||||
);
|
||||
|
||||
-- 修正历史或手工新增菜单的动态组件路径及基础属性。
|
||||
UPDATE `yz_system_menu`
|
||||
SET
|
||||
`pid` = @oa_pid,
|
||||
`title` = '通知公告',
|
||||
`component_path` = '/apps/oa/notice/index.vue',
|
||||
`icon` = 'Bell',
|
||||
`sort` = 7,
|
||||
`status` = 1,
|
||||
`is_visible` = 1,
|
||||
`views` = '[2]',
|
||||
`type` = 2,
|
||||
`remark` = '通知公告发布与管理'
|
||||
WHERE `path` = '/apps/oa/notice';
|
||||
@@ -0,0 +1,25 @@
|
||||
-- OA 通知公告表
|
||||
-- 在租户业务库执行以下语句创建表结构。
|
||||
-- 数据按租户 tid 隔离;发布后对租户内所有后台用户可见。
|
||||
|
||||
CREATE TABLE `yz_backend_oa_notice` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`tid` int NOT NULL DEFAULT '0' COMMENT '租户ID',
|
||||
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '公告标题',
|
||||
`content` text NULL COMMENT '公告内容',
|
||||
`notice_type` tinyint NOT NULL DEFAULT '0' COMMENT '类型 0-通知 1-公告 2-活动',
|
||||
`level` tinyint NOT NULL DEFAULT '0' COMMENT '紧急程度 0-普通 1-重要 2-紧急',
|
||||
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0-草稿 1-已发布 2-已下架',
|
||||
`is_top` tinyint NOT NULL DEFAULT '0' COMMENT '是否置顶 0-否 1-是',
|
||||
`publish_time` datetime DEFAULT NULL COMMENT '发布时间',
|
||||
`publisher_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '发布人ID',
|
||||
`publisher_name` varchar(100) NOT NULL DEFAULT '' COMMENT '发布人姓名',
|
||||
`read_count` int NOT NULL DEFAULT '0' COMMENT '阅读次数',
|
||||
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tid_status` (`tid`,`status`),
|
||||
KEY `idx_tid_top` (`tid`,`is_top`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA通知公告表';
|
||||
Reference in New Issue
Block a user