package com.example.springboot.controller;

import com.example.springboot.entity.Notice;
import com.example.springboot.service.NoticeService;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;

import java.util.Date;
import java.util.List;

@RestController
@RequestMapping("/notices")
public class NoticeController {

    private final NoticeService noticeService;

    public NoticeController(NoticeService noticeService) {
        this.noticeService = noticeService;
    }

    /**
     * 新增公告
     */
    @PostMapping
    public Notice create(@RequestBody Notice notice) {
        return noticeService.createNotice(notice);
    }

    /**
     * 修改公告
     */
    @PutMapping("/{id}")
    public Notice update(@PathVariable Long id, @RequestBody Notice notice) {
        notice.setId(id);
        return noticeService.updateNotice(notice);
    }

    /**
     * 删除公告
     */
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        noticeService.deleteNotice(id);
    }

    /**
     * 根据ID获取公告详情
     */
    @GetMapping("/{id}")
    public Notice get(@PathVariable Long id) {
        return noticeService.getNoticeById(id);
    }

    /**
     * 查询公告列表，支持模糊查询title和content，精确匹配发布时间（createTime）
     *
     */
    @GetMapping
    public List<Notice> search(
            @RequestParam(required = false) String title,
            @RequestParam(required = false) String content,
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date createTime
    ) {
        return noticeService.searchNotices(title, content, createTime);
    }
}
package com.example.springboot.entity;

import java.util.Date;

public class Notice {

    private Long id;
    private String title;
    private String content;
    private Date createTime;
    private Date updateTime;

    // Getter & Setter

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
}
package com.example.springboot.mapper;

import com.example.springboot.entity.Notice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.Date;
import java.util.List;

@Mapper
public interface NoticeMapper {

    int insert(Notice notice);

    int update(Notice notice);

    int deleteById(Long id);

    Notice selectById(Long id);

    /**
     * 带分页的模糊查询
     *
     * @param title 模糊匹配标题，可以null或空字符串
     * @param content 模糊匹配内容，可以null或空字符串
     * @param createTime 精确匹配发布时间，可以null
     * @return 符合条件的notice列表
     */
    List<Notice> selectByCondition(
            @Param("title") String title,
            @Param("content") String content,
            @Param("createTime") Date createTime
    );
}
package com.example.springboot.service;

import com.example.springboot.entity.Notice;
import com.example.springboot.mapper.NoticeMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;

@Service
public class NoticeService {

    private final NoticeMapper noticeMapper;

    public NoticeService(NoticeMapper noticeMapper) {
        this.noticeMapper = noticeMapper;
    }

    @Transactional
    public Notice createNotice(Notice notice) {
        Date now = new Date();
        notice.setCreateTime(now);
        notice.setUpdateTime(now);
        noticeMapper.insert(notice);
        return notice;
    }

    @Transactional
    public Notice updateNotice(Notice notice) {
        notice.setUpdateTime(new Date());
        noticeMapper.update(notice);
        return noticeMapper.selectById(notice.getId());
    }

    @Transactional
    public void deleteNotice(Long id) {
        noticeMapper.deleteById(id);
    }

    public Notice getNoticeById(Long id) {
        return noticeMapper.selectById(id);
    }

    /**
     * 根据条件查询，传null或空字符串即不作为过滤条件
     */
    public List<Notice> searchNotices(String title, String content, Date createTime) {
        return noticeMapper.selectByCondition(title, content, createTime);
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.springboot.mapper.NoticeMapper">

    <resultMap id="BaseResultMap" type="com.example.springboot.entity.Notice">
        <id property="id" column="id" />
        <result property="title" column="title" />
        <result property="content" column="content" />
        <result property="createTime" column="create_time" />
        <result property="updateTime" column="update_time" />
    </resultMap>

    <insert id="insert" parameterType="com.example.springboot.entity.Notice" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO notice (title, content, create_time, update_time)
        VALUES (#{title}, #{content}, #{createTime}, #{updateTime})
    </insert>

    <update id="update" parameterType="com.example.springboot.entity.Notice">
        UPDATE notice
        SET title = #{title},
            content = #{content},
            update_time = #{updateTime}
        WHERE id = #{id}
    </update>

    <delete id="deleteById" parameterType="long">
        DELETE FROM notice WHERE id = #{id}
    </delete>

    <select id="selectById" parameterType="long" resultMap="BaseResultMap">
        SELECT id, title, content, create_time, update_time
        FROM notice
        WHERE id = #{id}
    </select>

    <select id="selectByCondition" resultMap="BaseResultMap" parameterType="map">
        SELECT id, title, content, create_time, update_time
        FROM notice
        <where>
            <if test="title != null and title.trim() != ''">
                AND title LIKE CONCAT('%', #{title}, '%')
            </if>
            <if test="content != null and content.trim() != ''">
                AND content LIKE CONCAT('%', #{content}, '%')
            </if>
            <if test="createTime != null">
                AND create_time <![CDATA[>=]]> #{createTime}
                AND create_time <![CDATA[<]]> DATE_ADD(#{createTime}, INTERVAL 1 DAY)
            </if>
        </where>
        ORDER BY create_time DESC
    </select>

</mapper>
<template>
  <el-card>
    <h2>公告管理</h2>

    <!-- 查询条件 -->
    <el-form :inline="true" :model="searchForm" class="search-form" @submit.prevent>
      <el-form-item label="标题">
        <el-input
            v-model="searchForm.title"
            placeholder="请输入标题关键字"
            clearable
        />
      </el-form-item>

      <el-form-item label="内容">
        <el-input
            v-model="searchForm.content"
            placeholder="请输入内容关键字"
            clearable
        />
      </el-form-item>

      <el-form-item label="发布时间">
        <!-- 年 -->
        <el-select v-model="searchForm.year" placeholder="年" style="width: 100px" @change="onYearMonthChange">
          <el-option
              v-for="year in yearOptions"
              :key="year"
              :label="year + '年'"
              :value="year"
          />
        </el-select>

        <!-- 月 -->
        <el-select
            v-model="searchForm.month"
            placeholder="月"
            style="width: 80px; margin: 0 10px"
            @change="onYearMonthChange"
        >
          <el-option
              v-for="month in monthOptions"
              :key="month"
              :label="month + '月'"
              :value="month"
          />
        </el-select>

        <!-- 日 -->
        <el-select v-model="searchForm.day" placeholder="日" style="width: 80px">
          <el-option
              v-for="day in dayOptions"
              :key="day"
              :label="day + '日'"
              :value="day"
          />
        </el-select>
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="loadNotices">查询</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 新增按钮 -->
    <el-button type="primary" @click="openForm" style="margin-bottom: 15px;">
      新增公告
    </el-button>

    <!-- 公告列表 -->
    <el-table
        :data="notices"
        v-loading="loading"
        style="width: 100%"
        @row-dblclick="openForm"
        highlight-current-row
    >
      <el-table-column prop="title" label="标题" />
      <el-table-column prop="content" label="内容" />
      <el-table-column
          prop="createTime"
          label="发布时间"
          :formatter="formatDate"
          width="180"
      />
      <el-table-column
          prop="updateTime"
          label="更新时间"
          :formatter="formatDate"
          width="180"
      />
      <el-table-column label="操作" width="150" fixed="right">
        <template #default="{ row }">
          <div>
            <div style="margin-bottom: 6px;">
              <el-button size="mini" @click="openForm(row)">编辑</el-button>
            </div>
            <div>
              <el-button size="mini" type="danger" @click="removeNotice(row)">
                删除
              </el-button>
            </div>
          </div>
        </template>
      </el-table-column>
    </el-table>

    <!-- 新增/编辑弹窗 -->
    <el-dialog
        v-model="formVisible"
        :title="form.id ? '编辑公告' : '新增公告'"
        width="600px"
    >
      <el-form
          :model="form"
          :rules="rules"
          ref="formRef"
          label-width="80px"
          status-icon
      >
        <el-form-item prop="title" label="标题">
          <el-input v-model="form.title" maxlength="255" show-word-limit />
        </el-form-item>
        <el-form-item prop="content" label="内容">
          <el-input
              v-model="form.content"
              type="textarea"
              rows="6"
              maxlength="2000"
              show-word-limit
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="formVisible = false">取消</el-button>
        <el-button type="primary" @click="saveNotice">保存</el-button>
      </template>
    </el-dialog>
  </el-card>
</template>

<script setup>
import { ref, reactive, computed, onMounted } from "vue";
import axios from "axios";
import dayjs from "dayjs";
import { ElMessageBox, ElMessage } from "element-plus";

axios.defaults.baseURL = "http://localhost:8080";

const currentYear = new Date().getFullYear();

const searchForm = reactive({
  title: "",
  content: "",
  year: null,
  month: null,
  day: null,
});

const notices = ref([]);
const loading = ref(false);

const formVisible = ref(false);
const form = reactive({
  id: null,
  title: "",
  content: "",
  createTime: "",
  updateTime: "",
});

const formRef = ref(null);

const rules = {
  title: [
    { required: true, message: "请输入标题", trigger: "blur" },
    { max: 255, message: "标题最多255个字符", trigger: "blur" },
  ],
  content: [
    { required: true, message: "请输入内容", trigger: "blur" },
    { max: 2000, message: "内容最多2000个字符", trigger: "blur" },
  ],
};

const yearOptions = Array.from({ length: 10 }, (_, i) => currentYear - 5 + i);
const monthOptions = Array.from({ length: 12 }, (_, i) => i + 1);

const dayOptions = computed(() => {
  if (!searchForm.year || !searchForm.month) return [];
  const daysInMonth = dayjs(`${searchForm.year}-${String(searchForm.month).padStart(2, "0")}-01`).daysInMonth();
  return Array.from({ length: daysInMonth }, (_, i) => i + 1);
});

function onYearMonthChange() {
  if (searchForm.day && searchForm.day > dayOptions.value.length) {
    searchForm.day = null;
  }
}

function formatDate(row, column, cellValue) {
  return cellValue ? dayjs(cellValue).format("YYYY-MM-DD") : "";
}

async function loadNotices() {
  loading.value = true;
  try {
    const params = {};
    if (searchForm.title) params.title = searchForm.title;
    if (searchForm.content) params.content = searchForm.content;

    if (searchForm.year && searchForm.month && searchForm.day) {
      const y = String(searchForm.year);
      const m = String(searchForm.month).padStart(2, "0");
      const d = String(searchForm.day).padStart(2, "0");
      params.createTime = `${y}-${m}-${d}`;
    }

    const res = await axios.get("/notices", { params });
    notices.value = res.data;
  } catch (error) {
    console.error("查询公告失败", error);
    ElMessage.error("查询公告失败");
  } finally {
    loading.value = false;
  }
}

function resetSearch() {
  searchForm.title = "";
  searchForm.content = "";
  searchForm.year = null;
  searchForm.month = null;
  searchForm.day = null;
  loadNotices();
}

function openForm(row = null) {
  if (row) {
    formVisible.value = true;
    loadNotice(row.id);
  } else {
    formVisible.value = true;
    form.id = null;
    form.title = "";
    form.content = "";
    form.createTime = "";
    form.updateTime = "";
  }
}

async function loadNotice(id) {
  try {
    const res = await axios.get(`/notices/${id}`);
    Object.assign(form, res.data);
  } catch (error) {
    console.error("获取公告详情失败", error);
    ElMessage.error("获取公告详情失败");
  }
}

function validateForm() {
  return new Promise((resolve, reject) => {
    formRef.value.validate((valid) => {
      if (valid) {
        resolve();
      } else {
        reject();
      }
    });
  });
}

async function saveNotice() {
  try {
    await validateForm();
    if (form.id) {
      await axios.put(`/notices/${form.id}`, {
        title: form.title,
        content: form.content,
      });
      ElMessage.success("公告更新成功");
    } else {
      await axios.post("/notices", {
        title: form.title,
        content: form.content,
      });
      ElMessage.success("公告新增成功");
    }
    formVisible.value = false;
    loadNotices();
  } catch (error) {
    console.error("保存公告失败", error);
    ElMessage.error("保存公告失败");
  }
}

async function removeNotice(row) {
  try {
    await ElMessageBox.confirm("确定要删除该公告吗？", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    });
    await axios.delete(`/notices/${row.id}`);
    ElMessage.success("删除成功");
    loadNotices();
  } catch (error) {
    if (error !== "cancel") {
      console.error("删除公告失败", error);
      ElMessage.error("删除公告失败");
    }
  }
}

onMounted(() => {
  loadNotices();
});
</script>

<style scoped>
.el-card {
  max-width: 960px;
  margin: 20px auto;
  padding: 20px;
}

.search-form {
  margin-bottom: 10px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
}

.el-form-item {
  margin-right: 15px;
  margin-bottom: 10px;
}
</style>
<template>
  <el-card class="notice-card">
    <div class="header-wrapper">
      <h2 class="title">公告列表</h2>
      <el-button
          type="primary"
          :loading="loading"
          @click="handleRefresh"
          size="big"
      >
        刷新
      </el-button>
    </div>

    <el-table
        v-if="notices.length"
        :data="notices"
        style="width: 100%"
        @row-click="handleRowClick"
        highlight-current-row
        size="medium"
        :row-style="{ 'min-height': '48px' }"
        :header-cell-style="{ 'background-color': '#f5f7fa', 'font-weight': '600' }"
    >
      <el-table-column prop="title" label="标题" min-width="180" />
      <el-table-column prop="content" label="内容" min-width="300" />
      <el-table-column
          prop="createTime"
          label="发布时间"
          :formatter="formatDate"
          width="180"
      />
    </el-table>

    <div v-else class="empty-text">暂无公告</div>

    <el-dialog
        :title="selectedNotice?.title"
        :visible.sync="dialogVisible"
        width="50%"
        v-if="selectedNotice"
        :close-on-click-modal="false"
        :destroy-on-close="true"
    >
      <div class="dialog-content">
        <p><strong>发布时间：</strong>{{ formatDate(null, null, selectedNotice.createTime) }}</p>
        <p><strong>内容：</strong></p>
        <p class="content-text">{{ selectedNotice.content }}</p>
      </div>

      <template #footer>
        <el-button @click="dialogVisible = false" size="small">关闭</el-button>
      </template>
    </el-dialog>
  </el-card>
</template>

<script setup>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import dayjs from "dayjs";

const notices = ref([]);
const selectedNotice = ref(null);
const dialogVisible = ref(false);
const loading = ref(false);

function formatDate(row, column, cellValue) {
  if (!cellValue) return "";
  const d = dayjs(cellValue);
  if (!d.isValid()) return "Invalid Date";
  return d.format("YYYY-MM-DD HH:mm:ss");
}

async function fetchNotices() {
  loading.value = true;
  try {
    const res = await fetch("http://localhost:8080/notices");
    if (!res.ok) throw new Error("请求失败");
    const data = await res.json();
    notices.value = data;
  } catch (err) {
    ElMessage.error("加载公告失败：" + err.message);
  } finally {
    loading.value = false;
  }
}

function handleRowClick(row) {
  selectedNotice.value = row;
  dialogVisible.value = true;
}

function handleRefresh() {
  fetchNotices();
}

onMounted(() => {
  fetchNotices();
});
</script>

<style scoped>
.notice-card {
  max-width: 900px;
  margin: 30px auto;
  padding: 24px 32px;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
  background-color: #fff;
}

.header-wrapper {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 24px;
}

.title {
  font-weight: 700;
  font-size: 24px;
  color: #303133;
  margin: 0;
}

.empty-text {
  text-align: center;
  margin-top: 80px;
  font-size: 18px;
  color: #909399;
  user-select: none;
}

.dialog-content p {
  margin: 8px 0;
  line-height: 1.6;
  color: #606266;
}

.content-text {
  white-space: pre-wrap;
  background: #fafafa;
  padding: 12px 16px;
  border-radius: 5px;
  border: 1px solid #ebeef5;
  color: #333;
  font-size: 14px;
}

.el-dialog__footer {
  text-align: right;
}

.el-button[loading] {
  cursor: not-allowed;
}
</style>

这个功能的运行流程是什么？