Selaa lähdekoodia

feat: 平台公告功能 - 动态页显示平台消息+管理员公告管理

iwt 1 kuukausi sitten
vanhempi
sitoutus
f952980e28

+ 19 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -8427,5 +8427,24 @@ private void runMigration100() {
 
 		// 迁移202: withdrawal_requests表添加 type 字段(提现来源)
 		ensureColumn("withdrawal_requests", "type", "VARCHAR(20) DEFAULT 'platform_points' COMMENT '提现来源: platform_points(CF值) / commission(历史佣金)'");
+
+		// 迁移203: 创建平台公告表
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS notices (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"title VARCHAR(200) NOT NULL COMMENT '公告标题', " +
+				"content TEXT NOT NULL COMMENT '公告内容', " +
+				"type VARCHAR(20) DEFAULT 'platform' COMMENT '类型: platform=平台公告 activity=活动 promotion=优惠', " +
+				"status VARCHAR(20) DEFAULT 'active' COMMENT '状态: draft=草稿 active=已发布 deleted=已删除', " +
+				"admin_id BIGINT DEFAULT NULL COMMENT '发布管理员ID', " +
+				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台公告'"
+			);
+			log.info("已创建notices表");
+		} catch (Exception e) {
+			log.warn("创建notices表失败: " + e.getMessage());
+		}
 	}
 }

+ 111 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminNoticeController.java

@@ -0,0 +1,111 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Notice;
+import com.etotem.cfc.mapper.NoticeMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Tag(name = "公告管理", description = "平台公告的增删改查")
+@RestController
+@RequestMapping("/api/admin/notices")
+public class AdminNoticeController {
+
+    @Resource
+    private NoticeMapper noticeMapper;
+
+    @Operation(summary = "公告列表")
+    @PostMapping("/list")
+    public Result<List<Notice>> list(@RequestBody java.util.Map<String, Object> body) {
+        Integer page = body.get("page") != null ? Integer.valueOf(body.get("page").toString()) : 1;
+        Integer size = body.get("size") != null ? Integer.valueOf(body.get("size").toString()) : 20;
+        String status = body.get("status") != null ? body.get("status").toString() : null;
+        String type = body.get("type") != null ? body.get("type").toString() : null;
+
+        LambdaQueryWrapper<Notice> q = new LambdaQueryWrapper<>();
+        q.eq("status", "deleted").ne(1, 0);
+        if (status != null && !status.isEmpty()) q.eq(Notice::getStatus, status);
+        if (type != null && !type.isEmpty()) q.eq(Notice::getType, type);
+        q.orderByDesc(Notice::getCreatedAt);
+        q.last("LIMIT " + ((page - 1) * size) + "," + size);
+
+        List<Notice> list = noticeMapper.selectList(q);
+        return Result.success(list);
+    }
+
+    @Operation(summary = "全量公告列表(不分页)")
+    @PostMapping("/all")
+    public Result<List<Notice>> all(@RequestBody java.util.Map<String, Object> body) {
+        String status = body.get("status") != null ? body.get("status").toString() : "active";
+        LambdaQueryWrapper<Notice> q = new LambdaQueryWrapper<>();
+        q.eq(Notice::getStatus, status);
+        q.orderByDesc(Notice::getCreatedAt);
+        q.last("LIMIT 20");
+        return Result.success(noticeMapper.selectList(q));
+    }
+
+    @Operation(summary = "创建公告")
+    @PostMapping("/create")
+    public Result<Notice> create(@RequestBody Notice notice,
+                                  @RequestAttribute("userId") Long userId) {
+        if (notice.getTitle() == null || notice.getTitle().trim().isEmpty()) {
+            return Result.error("标题不能为空");
+        }
+        if (notice.getContent() == null || notice.getContent().trim().isEmpty()) {
+            return Result.error("内容不能为空");
+        }
+        if (notice.getType() == null || notice.getType().isEmpty()) {
+            notice.setType("platform");
+        }
+        notice.setStatus("active");
+        notice.setAdminId(userId);
+        notice.setCreatedAt(new Date());
+        notice.setUpdatedAt(new Date());
+        noticeMapper.insert(notice);
+        return Result.success(notice);
+    }
+
+    @Operation(summary = "更新公告")
+    @PostMapping("/update")
+    public Result<Notice> update(@RequestBody Notice notice) {
+        if (notice.getId() == null) return Result.error("id不能为空");
+        Notice existing = noticeMapper.selectById(notice.getId());
+        if (existing == null) return Result.error("公告不存在");
+        existing.setTitle(notice.getTitle());
+        existing.setContent(notice.getContent());
+        existing.setType(notice.getType());
+        existing.setUpdatedAt(new Date());
+        noticeMapper.updateById(existing);
+        return Result.success(existing);
+    }
+
+    @Operation(summary = "删除公告(软删)")
+    @PostMapping("/delete")
+    public Result<String> delete(@RequestBody java.util.Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        Notice notice = noticeMapper.selectById(id);
+        if (notice == null) return Result.error("公告不存在");
+        notice.setStatus("deleted");
+        notice.setUpdatedAt(new Date());
+        noticeMapper.updateById(notice);
+        return Result.success("已删除");
+    }
+
+    @Operation(summary = "置顶(将指定公告移到最前)")
+    @PostMapping("/pin")
+    public Result<String> pin(@RequestBody java.util.Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        Notice notice = noticeMapper.selectById(id);
+        if (notice == null) return Result.error("公告不存在");
+        notice.setCreatedAt(new Date());
+        notice.setUpdatedAt(new Date());
+        noticeMapper.updateById(notice);
+        return Result.success("已置顶");
+    }
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/notice/NoticeController.java

@@ -0,0 +1,31 @@
+package com.etotem.cfc.controller.notice;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Notice;
+import com.etotem.cfc.mapper.NoticeMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Tag(name = "公告", description = "前端公告列表")
+@RestController
+@RequestMapping("/api/notices")
+public class NoticeController {
+
+    @Resource
+    private NoticeMapper noticeMapper;
+
+    @Operation(summary = "获取已发布公告列表")
+    @GetMapping("/list")
+    public Result<List<Notice>> list() {
+        LambdaQueryWrapper<Notice> q = new LambdaQueryWrapper<>();
+        q.eq(Notice::getStatus, "active");
+        q.orderByDesc(Notice::getCreatedAt);
+        q.last("LIMIT 10");
+        return Result.success(noticeMapper.selectList(q));
+    }
+}

+ 25 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Notice.java

@@ -0,0 +1,25 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("notices")
+public class Notice implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String title;
+    private String content;
+    /** type: platform=平台公告 activity=活动 promotion=优惠 */
+    private String type;
+    /** status: draft=草稿 active=已发布 deleted=已删除 */
+    private String status;
+    private Long adminId;
+    private Date createdAt;
+    private Date updatedAt;
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/NoticeMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.Notice;
+
+public interface NoticeMapper extends BaseMapper<Notice> {
+}

+ 88 - 12
cfc-frontend/components/FamilyFeed.vue

@@ -2,16 +2,29 @@
   <view class="family-feed">
     <view class="feed-header" v-if="showHeader">
       <text class="feed-icon">📢</text>
-      <text class="feed-title">家庭动态</text>
+      <text class="feed-title">动态</text>
     </view>
-    <scroll-view scroll-x class="feed-scroll" scroll-with-animation :show-scrollbar="false">
+    <!-- 平台公告(置顶显示) -->
+    <view class="notice-list" v-if="notices.length > 0">
+      <view class="notice-item" v-for="(n, idx) in notices" :key="'n' + idx" @click="onNoticeClick(n)">
+        <view class="notice-tag" :class="'notice-type-' + n.type">{{ noticeTypeLabel(n.type) }}</view>
+        <text class="notice-title">{{ n.title }}</text>
+        <text class="notice-time">{{ n.time }}</text>
+      </view>
+    </view>
+    <!-- 家庭动态(横向滚动) -->
+    <scroll-view scroll-x class="feed-scroll" scroll-with-animation :show-scrollbar="false" v-if="familyItems.length > 0">
       <view class="feed-list">
-        <view class="feed-item" v-for="(item, idx) in feedItems" :key="idx">
+        <view class="feed-item" v-for="(item, idx) in familyItems" :key="idx">
           <text class="feed-msg">{{ item.text }}</text>
           <text class="feed-time">{{ item.time }}</text>
         </view>
       </view>
     </scroll-view>
+    <!-- 空态 -->
+    <view v-if="notices.length === 0 && familyItems.length === 0" class="feed-empty">
+      <text class="feed-empty-text">暂无动态</text>
+    </view>
   </view>
 </template>
 
@@ -23,21 +36,29 @@ export default {
       type: Array,
       default: function() { return [] }
     },
+    notices: {
+      type: Array,
+      default: function() { return [] }
+    },
     showHeader: {
       type: Boolean,
       default: true
     }
   },
-  data: function() {
-    return {
-      scrollLeft: 0
+  computed: {
+    familyItems: function() {
+      return this.items.filter(function(item) {
+        return item.type !== 'notice'
+      })
     }
   },
-  computed: {
-    feedItems: function() {
-      return this.items.length > 0 ? this.items : [
-        { text: '暂无动态,先做几个任务吧', time: '' }
-      ]
+  methods: {
+    noticeTypeLabel: function(type) {
+      var labels = { platform: '公告', activity: '活动', promotion: '优惠' }
+      return labels[type] || '公告'
+    },
+    onNoticeClick: function(n) {
+      this.$emit('notice-click', n)
     }
   }
 }
@@ -70,6 +91,51 @@ export default {
   color: #333;
 }
 
+/* 平台公告 */
+.notice-list {
+  margin-bottom: 12rpx;
+}
+
+.notice-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 10rpx 12rpx;
+  background: #FFF7ED;
+  border-radius: 10rpx;
+  margin-bottom: 8rpx;
+  border-left: 4rpx solid #F97316;
+}
+
+.notice-tag {
+  font-size: 18rpx;
+  padding: 2rpx 10rpx;
+  border-radius: 8rpx;
+  margin-right: 10rpx;
+  flex-shrink: 0;
+}
+
+.notice-tag-platform { background: #FEF3C7; color: #92400E; }
+.notice-tag-activity { background: #DBEAFE; color: #1E40AF; }
+.notice-tag-promotion { background: #FCE7F3; color: #9D174D; }
+
+.notice-title {
+  flex: 1;
+  font-size: 24rpx;
+  color: #333;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.notice-time {
+  font-size: 20rpx;
+  color: #999;
+  flex-shrink: 0;
+  margin-left: 12rpx;
+}
+
+/* 家庭动态横向滚动 */
 .feed-scroll {
   width: 100%;
   white-space: nowrap;
@@ -107,4 +173,14 @@ export default {
   color: #999;
   flex-shrink: 0;
 }
-</style>
+
+.feed-empty {
+  padding: 20rpx 0;
+  text-align: center;
+}
+
+.feed-empty-text {
+  font-size: 24rpx;
+  color: #ccc;
+}
+</style>

+ 30 - 3
cfc-frontend/pages/index-home/index.vue

@@ -61,9 +61,10 @@
       showDimensionBar
       starHeight="480rpx" />
 
-    <!-- 家庭动态 -->
+    <!-- 动态 -->
     <FamilyFeed
       :items="familyFeed"
+      :notices="notices"
       :showHeader="true" />
 
     <!-- 会员入口:非会员显示开通 / 快到期显示续费 -->
@@ -325,7 +326,7 @@ import FamilyFeed from '../../components/FamilyFeed.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
-import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory } from '../../utils/api.js'
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices } from '../../utils/api.js'
 import config from '@/config.js'
 
 export default {
@@ -390,6 +391,7 @@ export default {
       activeChallenges: [],
       pendingTasks: [],
       familyFeed: [],
+      notices: [],
       loggedInInitDone: false,
 
       // 会员状态(沙盘下方会员入口使用)
@@ -510,6 +512,7 @@ export default {
         self.childrenChecked = true
         self.loadTodayTasks()
         self.loadFamilyFeed()
+        self.loadNotices()
       }).catch(function(e) {
         console.log('获取孩子列表失败', e)
         self.childrenChecked = true
@@ -608,7 +611,8 @@ export default {
             }
             return {
               text: '✅ ' + (child.nickname || '小名') + ' 完成「' + (t.title || '任务') + '」 +' + points + '分',
-              time: timeStr
+              time: timeStr,
+              type: 'task'
             }
           })
         }).catch(function() { return [] })
@@ -625,6 +629,29 @@ export default {
         self.familyFeed = []
       })
     },
+    loadNotices: function() {
+      var self = this
+      getNotices().then(function(res) {
+        if (res.code === 200 && res.data) {
+          var notices = res.data.map(function(n) {
+            var timeStr = ''
+            if (n.createdAt) {
+              var d = new Date(n.createdAt)
+              timeStr = (d.getMonth() + 1) + '月' + d.getDate() + '日'
+            }
+            return {
+              text: '📢 ' + n.title,
+              time: timeStr,
+              type: 'notice',
+              notice: n
+            }
+          })
+          self.notices = notices
+          // 合并 notices 到 familyFeed(优先显示)
+          self.familyFeed = notices.concat(self.familyFeed).slice(0, 10)
+        }
+      }).catch(function() {})
+    },
     quickCompleteTask: function(task) {
       var self = this
       completeTaskApi(task.id, self.currentChildId, '').then(function(res) {

+ 5 - 0
cfc-frontend/utils/api.js

@@ -230,6 +230,11 @@ export const wechatPhoneLogin = (data) => {
   return request('/api/auth/wechat-phone-login', 'POST', data)
 }
 
+// 获取平台公告
+export const getNotices = () => {
+  return request('/api/notices/list', 'GET')
+}
+
 // 微信静默登录
 export const silentLogin = (code) => {
   return request('/api/auth/silent-login', 'POST', { code })

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-571ea65b1dbf73503d22a22880b14bfe46a1c820
+4c75428cb5c4ef8480582deb3b0825e2ff7d29bc

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.896",
+  "version": "1.0.897",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.896",
+      "version": "1.0.897",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 21 - 0
cfc-web/src/api/notice.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+export function adminNoticeList(data) {
+  return request({ url: '/api/admin/notices/list', method: 'post', data })
+}
+
+export function adminNoticeAll(data) {
+  return request({ url: '/api/admin/notices/all', method: 'post', data })
+}
+
+export function adminNoticeCreate(data) {
+  return request({ url: '/api/admin/notices/create', method: 'post', data })
+}
+
+export function adminNoticeUpdate(data) {
+  return request({ url: '/api/admin/notices/update', method: 'post', data })
+}
+
+export function adminNoticeDelete(data) {
+  return request({ url: '/api/admin/notices/delete', method: 'post', data })
+}

+ 6 - 0
cfc-web/src/router/index.js

@@ -343,6 +343,12 @@ const routes = [
         component: () => import('@/views/admin/CommentReview.vue'),
         meta: { title: '评论审核', perm: 'articles:manage' }
       },
+      {
+        path: 'notice-manage',
+        name: 'NoticeManage',
+        component: () => import('@/views/admin/NoticeManage.vue'),
+        meta: { title: '公告管理', perm: 'admin' }
+      },
       {
         path: 'periodic-service-config',
         name: 'PeriodicServiceConfig',

+ 177 - 0
cfc-web/src/views/admin/NoticeManage.vue

@@ -0,0 +1,177 @@
+<template>
+  <div class="admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">公告管理</span>
+        <el-button type="primary" size="small" @click="openCreate">+ 新建公告</el-button>
+      </div>
+
+      <div class="table-scroll-wrap-lg">
+        <el-table :data="list" v-loading="loading" border stripe :max-height="tableHeight">
+          <el-table-column prop="id" label="ID" width="70" />
+          <el-table-column label="类型" width="90">
+            <template slot-scope="{ row }">
+              <el-tag size="mini" :type="typeTagType(row.type)">{{ typeLabel(row.type) }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="标题" min-width="200">
+            <template slot-scope="{ row }">{{ row.title }}</template>
+          </el-table-column>
+          <el-table-column label="内容" min-width="200" show-overflow-tooltip>
+            <template slot-scope="{ row }">{{ row.content }}</template>
+          </el-table-column>
+          <el-table-column label="状态" width="90">
+            <template slot-scope="{ row }">
+              <el-tag size="mini" :type="row.status === 'active' ? 'success' : 'info'">
+                {{ row.status === 'active' ? '已发布' : '草稿' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" width="160">
+            <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
+          </el-table-column>
+          <el-table-column label="操作" width="140" fixed="right">
+            <template slot-scope="{ row }">
+              <el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
+              <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-card>
+
+    <!-- 创建/编辑弹窗 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="类型" prop="type">
+          <el-radio-group v-model="form.type">
+            <el-radio label="platform">平台公告</el-radio>
+            <el-radio label="activity">活动</el-radio>
+            <el-radio label="promotion">优惠</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="标题" prop="title">
+          <el-input v-model="form.title" placeholder="请输入标题" maxlength="200" show-word-limit />
+        </el-form-item>
+        <el-form-item label="内容" prop="content">
+          <el-input v-model="form.content" type="textarea" :rows="5" placeholder="请输入公告内容" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { adminNoticeList, adminNoticeCreate, adminNoticeUpdate, adminNoticeDelete } from '@/api/notice'
+
+export default {
+  name: 'NoticeManage',
+  data() {
+    return {
+      list: [],
+      loading: false,
+      tableHeight: 400,
+      dialogVisible: false,
+      submitting: false,
+      editId: null,
+      form: {
+        type: 'platform',
+        title: '',
+        content: ''
+      },
+      rules: {
+        type: [{ required: true, message: '请选择类型', trigger: 'change' }],
+        title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+        content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
+      }
+    }
+  },
+  computed: {
+    dialogTitle() {
+      return this.editId ? '编辑公告' : '新建公告'
+    }
+  },
+  mounted() {
+    this.loadList()
+    window.addEventListener('resize', this.calcHeight)
+    this.calcHeight()
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.calcHeight)
+  },
+  methods: {
+    calcHeight() {
+      this.tableHeight = window.innerHeight - 280
+    },
+    loadList() {
+      this.loading = true
+      adminNoticeList({ page: 1, size: 50 }).then(res => {
+        this.list = res.data || []
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    openCreate() {
+      this.editId = null
+      this.form = { type: 'platform', title: '', content: '' }
+      this.dialogVisible = true
+      this.$nextTick(() => { this.$refs.form && this.$refs.form.clearValidate() })
+    },
+    openEdit(row) {
+      this.editId = row.id
+      this.form = { type: row.type, title: row.title, content: row.content }
+      this.dialogVisible = true
+      this.$nextTick(() => { this.$refs.form && this.$refs.form.clearValidate() })
+    },
+    handleSubmit() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        this.submitting = true
+        var fn = this.editId ? adminNoticeUpdate : adminNoticeCreate
+        fn(this.form).then(res => {
+          this.submitting = false
+          this.dialogVisible = false
+          this.loadList()
+          this.$message.success(this.editId ? '更新成功' : '创建成功')
+        }).catch(() => {
+          this.submitting = false
+        })
+      })
+    },
+    handleDelete(row) {
+      this.$confirm('确定删除该公告?', '提示', { type: 'warning' }).then(() => {
+        adminNoticeDelete({ id: row.id }).then(() => {
+          this.loadList()
+          this.$message.success('删除成功')
+        })
+      }).catch(() => {})
+    },
+    typeLabel(type) {
+      var map = { platform: '公告', activity: '活动', promotion: '优惠' }
+      return map[type] || type
+    },
+    typeTagType(type) {
+      var map = { platform: 'warning', activity: 'primary', promotion: 'danger' }
+      return map[type] || ''
+    },
+    formatTime(time) {
+      if (!time) return '-'
+      var d = new Date(time)
+      return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.admin-page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>