#
Junjie
2026-01-24 241ab413828a3627a123ae001fc05f5c35031861
#
7个文件已添加
3个文件已修改
869 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/OpenController.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/enums/RedisKeyType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/controller/AnnouncementController.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/Announcement.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/mapper/AnnouncementMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/service/AnnouncementService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/service/impl/AnnouncementServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/AnnouncementMapper.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/announcement/announcement.html 405 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/tvDevice/tvDevice.html 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/OpenController.java
@@ -1,7 +1,9 @@
package com.zy.asrs.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.annotations.ManagerAuth;
import com.core.common.Cools;
import com.core.common.R;
import com.zy.asrs.entity.BasStationTv;
@@ -16,6 +18,8 @@
import com.zy.asrs.utils.StationUtils;
import com.zy.asrs.utils.Utils;
import com.zy.common.utils.RedisUtil;
import com.zy.system.entity.Announcement;
import com.zy.system.service.AnnouncementService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
@@ -40,6 +44,8 @@
    private StationUtils stationUtils;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private AnnouncementService announcementService;
    @Value("${app.version:1.0.0}")
    private String appVersion;
@@ -77,6 +83,12 @@
        }
        String manualError = "";
        Object manualErrorObj = redisUtil.get(RedisKeyType.TV_MANUAL_ERROR_MSG.key);
        if (manualErrorObj != null) {
            manualError = String.valueOf(manualErrorObj);
        }
        List<TvDataDto> list = new ArrayList<>();
        for (BasStationTv relation : relations) {
            WcsStationDto wcsStationDto = stationUtils.stationMap.get(relation.getStationId());
@@ -97,6 +109,12 @@
            if (!Cools.isEmpty(wcsStationDto.getSystemWarning())) {
                errorMsg +=  wcsStationDto.getSystemWarning();
            }
            if (!Cools.isEmpty(manualError)) {
                if (!Cools.isEmpty(errorMsg)) {
                    errorMsg += ";";
                }
                errorMsg += manualError;
            }
            TvDataDto tvDataDto = new TvDataDto();
            tvDataDto.setStationId(wcsStationDto.getStationId());
@@ -118,6 +136,84 @@
        return R.ok().add(list);
    }
    @GetMapping("/getError")
    public R getError(HttpServletRequest request) {
        Set<String> errors = new LinkedHashSet<>();
        Object manualErrorObj = redisUtil.get(RedisKeyType.TV_MANUAL_ERROR_MSG.key);
        String manualError = manualErrorObj == null ? "" : String.valueOf(manualErrorObj);
        if (!Cools.isEmpty(manualError)) {
            errors.add(manualError);
        }
        String remoteAddr = request.getRemoteAddr();
        TvDevice tvDevice = tvDeviceService.selectOne(
                new EntityWrapper<TvDevice>().eq("ip", remoteAddr));
        if (tvDevice != null) {
            List<BasStationTv> relations = basStationTvService
                    .selectList(new EntityWrapper<BasStationTv>().eq("tv_id", tvDevice.getId()));
            if (relations != null && !relations.isEmpty()) {
                for (BasStationTv relation : relations) {
                    WcsStationDto wcsStationDto = stationUtils.stationMap.get(relation.getStationId());
                    if (wcsStationDto == null) {
                        continue;
                    }
                    if (wcsStationDto.getLoading() != 1) {
                        continue;
                    }
                    String deviceError = "";
                    if (!Cools.isEmpty(wcsStationDto.getErrorMsg())) {
                        deviceError += wcsStationDto.getErrorMsg();
                    }
                    if (!Cools.isEmpty(wcsStationDto.getSystemWarning())) {
                        if (!Cools.isEmpty(deviceError)) {
                            deviceError += ";";
                        }
                        deviceError += wcsStationDto.getSystemWarning();
                    }
                    if (!Cools.isEmpty(deviceError)) {
                        errors.add(deviceError);
                    }
                }
            }
        }
        String errorMsg = String.join(";", errors);
        Map<String, Object> result = new HashMap<>();
        result.put("errorMsg", errorMsg);
        return R.ok(result);
    }
    @GetMapping("/manualError/auth")
    @ManagerAuth
    public R getManualErrorAuth() {
        Object manualErrorObj = redisUtil.get(RedisKeyType.TV_MANUAL_ERROR_MSG.key);
        String manualError = manualErrorObj == null ? "" : String.valueOf(manualErrorObj);
        Map<String, Object> result = new HashMap<>();
        result.put("manualError", manualError);
        return R.ok(result);
    }
    @PostMapping("/manualError/auth")
    @ManagerAuth
    public R setManualError(@RequestBody JSONObject param) {
        String manualError = param.getString("manualError");
        if (Cools.isEmpty(manualError)) {
            redisUtil.del(RedisKeyType.TV_MANUAL_ERROR_MSG.key);
            return R.ok();
        }
        redisUtil.set(RedisKeyType.TV_MANUAL_ERROR_MSG.key, manualError);
        return R.ok();
    }
    @GetMapping("/announcement")
    public R top5(){
        EntityWrapper<Announcement> wrapper = new EntityWrapper<>();
        wrapper.eq("status", 1);
        wrapper.orderBy("create_time", false);
        wrapper.last("limit 5");
        return R.ok(announcementService.selectList(wrapper));
    }
    /**
     * 获取当前时间
     */
src/main/java/com/zy/asrs/enums/RedisKeyType.java
@@ -5,6 +5,7 @@
    TV_LOC_DATA_DTO("tvLocDataDto"),
    TV_LINE_CHARTS("tvLineCharts"),
    TV_LOC_DETL_STATISTICS("tvLocDetlStatistics"),
    TV_MANUAL_ERROR_MSG("tvManualErrorMsg"),
    ;
    public String key;
src/main/java/com/zy/system/controller/AnnouncementController.java
New file
@@ -0,0 +1,128 @@
package com.zy.system.controller;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.plugins.Page;
import com.core.annotations.ManagerAuth;
import com.core.common.Cools;
import com.core.common.DateUtils;
import com.core.common.R;
import com.core.controller.AbstractBaseController;
import com.zy.system.entity.Announcement;
import com.zy.system.service.AnnouncementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
public class AnnouncementController extends AbstractBaseController {
    @Autowired
    private AnnouncementService announcementService;
    @RequestMapping(value = "/announcement/{id}/auth")
    @ManagerAuth
    public R get(@PathVariable("id") Long id) {
        return R.ok(announcementService.selectById(String.valueOf(id)));
    }
    @RequestMapping(value = "/announcement/list/auth")
    @ManagerAuth
    public R list(@RequestParam(defaultValue = "1")Integer curr,
                  @RequestParam(defaultValue = "10")Integer limit,
                  @RequestParam(required = false)String orderByField,
                  @RequestParam(required = false)String orderByType,
                  @RequestParam Map<String, Object> param){
        excludeTrash(param);
        EntityWrapper<Announcement> wrapper = new EntityWrapper<>();
        convert(param, wrapper);
        if (!Cools.isEmpty(orderByField)){
            wrapper.orderBy(humpToLine(orderByField), "asc".equals(orderByType));
        }
        return R.ok(announcementService.selectPage(new Page<>(curr, limit), wrapper));
    }
    private void convert(Map<String, Object> map, EntityWrapper wrapper){
        for (Map.Entry<String, Object> entry : map.entrySet()){
            if ("status".equals(entry.getKey())) {
                wrapper.eq(entry.getKey(), entry.getValue());
                continue;
            }
            if (entry.getKey().endsWith(">")) {
                wrapper.ge(Cools.deleteChar(entry.getKey()), DateUtils.convert(String.valueOf(entry.getValue())));
            } else if (entry.getKey().endsWith("<")) {
                wrapper.le(Cools.deleteChar(entry.getKey()), DateUtils.convert(String.valueOf(entry.getValue())));
            } else {
                wrapper.like(entry.getKey(), String.valueOf(entry.getValue()));
            }
        }
    }
    @RequestMapping(value = "/announcement/edit/auth")
    @ManagerAuth
    public R edit(Announcement announcement) {
        if (Cools.isEmpty(announcement)){
            return R.error();
        }
        if (null == announcement.getId()){
            announcement.setCreateTime(new Date());
            announcement.setUpdateTime(new Date());
            announcementService.insert(announcement);
        } else {
            announcement.setUpdateTime(new Date());
            announcementService.updateById(announcement);
        }
        return R.ok();
    }
    @RequestMapping(value = "/announcement/add/auth")
    @ManagerAuth
    public R add(Announcement announcement) {
        announcement.setCreateTime(new Date());
        announcement.setUpdateTime(new Date());
        announcementService.insert(announcement);
        return R.ok();
    }
    @RequestMapping(value = "/announcement/update/auth")
    @ManagerAuth
    public R update(Announcement announcement){
        if (Cools.isEmpty(announcement) || null==announcement.getId()){
            return R.error();
        }
        announcement.setUpdateTime(new Date());
        announcementService.updateById(announcement);
        return R.ok();
    }
    @RequestMapping(value = "/announcement/delete/auth")
    @ManagerAuth
    public R delete(Integer[] ids){
        if (Cools.isEmpty(ids)){
            return R.error();
        }
        announcementService.deleteBatchIds(Arrays.asList(ids));
        return R.ok();
    }
    @RequestMapping(value = "/announcement/export/auth")
    @ManagerAuth
    public R export(@RequestBody JSONObject param){
        List<String> fields = JSONObject.parseArray(param.getJSONArray("fields").toJSONString(), String.class);
        EntityWrapper<Announcement> wrapper = new EntityWrapper<>();
        Map<String, Object> map = excludeTrash(param.getJSONObject("announcement"));
        convert(map, wrapper);
        List<Announcement> list = announcementService.selectList(wrapper);
        return R.ok(exportSupport(list, fields));
    }
    @GetMapping("/announcement/top5")
    public R top5(){
        EntityWrapper<Announcement> wrapper = new EntityWrapper<>();
        wrapper.eq("status", 1);
        wrapper.orderBy("create_time", false);
        wrapper.last("limit 5");
        return R.ok(announcementService.selectList(wrapper));
    }
}
src/main/java/com/zy/system/entity/Announcement.java
New file
@@ -0,0 +1,108 @@
package com.zy.system.entity;
import com.baomidou.mybatisplus.annotations.TableField;
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.annotations.TableName;
import com.baomidou.mybatisplus.enums.IdType;
import com.core.common.Cools;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@TableName("sys_announcement")
public class Announcement implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String title;
    private String content;
    private Integer status;
    @TableField("create_time")
    private Date createTime;
    @TableField("update_time")
    private Date updateTime;
    public Announcement() {}
    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 Integer getStatus() {
        return status;
    }
    public String getStatus$() {
        if (null == this.status) { return null; }
        switch (this.status) {
            case 1:
                return "正常";
            case 0:
                return "禁用";
            default:
                return String.valueOf(this.status);
        }
    }
    public void setStatus(Integer status) {
        this.status = status;
    }
    public Date getCreateTime() {
        return createTime;
    }
    public String getCreateTime$() {
        if (Cools.isEmpty(this.createTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
    }
    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
    public Date getUpdateTime() {
        return updateTime;
    }
    public String getUpdateTime$() {
        if (Cools.isEmpty(this.updateTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.updateTime);
    }
    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
}
src/main/java/com/zy/system/mapper/AnnouncementMapper.java
New file
@@ -0,0 +1,11 @@
package com.zy.system.mapper;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.zy.system.entity.Announcement;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface AnnouncementMapper extends BaseMapper<Announcement> {
}
src/main/java/com/zy/system/service/AnnouncementService.java
New file
@@ -0,0 +1,7 @@
package com.zy.system.service;
import com.baomidou.mybatisplus.service.IService;
import com.zy.system.entity.Announcement;
public interface AnnouncementService extends IService<Announcement> {
}
src/main/java/com/zy/system/service/impl/AnnouncementServiceImpl.java
New file
@@ -0,0 +1,11 @@
package com.zy.system.service.impl;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.zy.system.entity.Announcement;
import com.zy.system.mapper.AnnouncementMapper;
import com.zy.system.service.AnnouncementService;
import org.springframework.stereotype.Service;
@Service("announcementService")
public class AnnouncementServiceImpl extends ServiceImpl<AnnouncementMapper, Announcement> implements AnnouncementService {
}
src/main/resources/mapper/AnnouncementMapper.xml
New file
@@ -0,0 +1,14 @@
<?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.zy.system.mapper.AnnouncementMapper">
    <resultMap id="BaseResultMap" type="com.zy.system.entity.Announcement">
        <id column="id" property="id" />
        <result column="title" property="title" />
        <result column="content" property="content" />
        <result column="status" property="status" />
        <result column="create_time" property="createTime" />
        <result column="update_time" property="updateTime" />
    </resultMap>
</mapper>
src/main/webapp/views/announcement/announcement.html
New file
@@ -0,0 +1,405 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>系统公告管理</title>
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link rel="stylesheet" href="../../static/vue/element/element.css">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
            background: #f5f7fa;
            padding: 15px;
        }
        .app-container {
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
            padding: 20px;
        }
        .header-section {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            flex-wrap: wrap;
            gap: 10px;
        }
        .search-section {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            align-items: center;
        }
        .action-buttons {
            display: flex;
            gap: 10px;
        }
        .table-section {
            margin-top: 15px;
        }
        .pagination-section {
            margin-top: 20px;
            display: flex;
            justify-content: flex-end;
        }
    </style>
</head>
<body>
    <div id="app">
        <div class="app-container" v-loading="tableLoading">
            <div class="header-section">
                <div class="search-section">
                    <el-input v-model="searchForm.title" placeholder="标题" size="small" clearable style="width: 180px;">
                    </el-input>
                    <el-select v-model="searchForm.status" placeholder="状态" clearable size="small" style="width: 120px;">
                        <el-option label="正常" value="1"></el-option>
                        <el-option label="禁用" value="0"></el-option>
                    </el-select>
                    <el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch">搜索</el-button>
                    <el-button size="small" icon="el-icon-refresh-left" @click="handleReset">重置</el-button>
                </div>
                <div class="action-buttons">
                    <el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd">新增</el-button>
                    <el-button type="danger" size="small" icon="el-icon-delete" @click="handleBatchDelete"
                        :disabled="selectedRows.length === 0">删除</el-button>
                    <!-- <el-button size="small" icon="el-icon-download" @click="handleExport">导出</el-button> -->
                </div>
            </div>
            <div class="table-section">
                <el-table :data="tableData" border stripe @selection-change="handleSelectionChange"
                    style="width: 100%;" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
                    <el-table-column type="selection" width="50" align="center"></el-table-column>
                    <el-table-column prop="id" label="ID" width="70" align="center"></el-table-column>
                    <el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip></el-table-column>
                    <el-table-column prop="content" label="内容" min-width="260" show-overflow-tooltip></el-table-column>
                    <el-table-column prop="status$" label="状态" width="90" align="center"></el-table-column>
                    <el-table-column prop="createTime$" label="创建时间" width="170" align="center"></el-table-column>
                    <el-table-column prop="updateTime$" label="修改时间" width="170" align="center"></el-table-column>
                    <el-table-column label="操作" width="160" align="center" fixed="right">
                        <template slot-scope="scope">
                            <el-button type="text" size="small" @click="openDetail(scope.row)">详情</el-button>
                            <el-button type="text" size="small" @click="openEdit(scope.row)">编辑</el-button>
                            <el-button type="text" size="small" style="color:#f56c6c"
                                @click="handleDelete(scope.row)">删除</el-button>
                        </template>
                    </el-table-column>
                </el-table>
            </div>
            <div class="pagination-section">
                <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"
                    :page-size="pageSize" :current-page="currentPage" :page-sizes="[10, 20, 50, 100]"
                    @size-change="handleSizeChange" @current-change="handlePageChange">
                </el-pagination>
            </div>
        </div>
        <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="520px" :close-on-click-modal="false">
            <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
                <el-form-item label="标题" prop="title">
                    <el-input v-model="formData.title" :disabled="isDetail"></el-input>
                </el-form-item>
                <el-form-item label="内容" prop="content">
                    <el-input type="textarea" v-model="formData.content" :rows="5" :disabled="isDetail"></el-input>
                </el-form-item>
                <el-form-item label="状态" prop="status">
                    <el-select v-model="formData.status" :disabled="isDetail" style="width: 140px;">
                        <el-option label="正常" value="1"></el-option>
                        <el-option label="禁用" value="0"></el-option>
                    </el-select>
                </el-form-item>
            </el-form>
            <div slot="footer" class="dialog-footer">
                <el-button @click="dialogVisible = false">取消</el-button>
                <el-button type="primary" @click="submitForm" v-if="!isDetail" :loading="submitting">保存</el-button>
            </div>
        </el-dialog>
    </div>
    <script src="../../static/vue/js/vue.min.js"></script>
    <script src="../../static/vue/element/element.js"></script>
    <script src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
    <script src="../../static/js/common.js"></script>
    <script>
        new Vue({
            el: '#app',
            data: {
                tableData: [],
                tableLoading: false,
                total: 0,
                currentPage: 1,
                pageSize: 10,
                selectedRows: [],
                searchForm: {
                    title: '',
                    status: ''
                },
                dialogVisible: false,
                dialogMode: 'add',
                submitting: false,
                formData: {
                    id: '',
                    title: '',
                    content: '',
                    status: '1'
                },
                formRules: {
                    title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
                    content: [{ required: true, message: '请输入内容', trigger: 'blur' }],
                    status: [{ required: true, message: '请选择状态', trigger: 'change' }]
                }
            },
            computed: {
                isDetail() {
                    return this.dialogMode === 'detail';
                },
                dialogTitle() {
                    if (this.dialogMode === 'add') {
                        return '新增公告';
                    }
                    if (this.dialogMode === 'edit') {
                        return '编辑公告';
                    }
                    return '公告详情';
                }
            },
            created() {
                this.loadData();
            },
            methods: {
                getHeaders() {
                    return { 'token': localStorage.getItem('token') };
                },
                loadData() {
                    this.tableLoading = true;
                    const params = {
                        curr: this.currentPage,
                        limit: this.pageSize,
                        ...this.searchForm
                    };
                    $.ajax({
                        url: baseUrl + '/announcement/list/auth',
                        headers: this.getHeaders(),
                        data: params,
                        success: (res) => {
                            this.tableLoading = false;
                            if (res.code === 200) {
                                this.tableData = res.data.records || [];
                                this.total = res.data.total || 0;
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + '/';
                            } else {
                                this.$message.error(res.msg || '加载失败');
                            }
                        },
                        error: () => {
                            this.tableLoading = false;
                            this.$message.error('请求失败');
                        }
                    });
                },
                handleSearch() {
                    this.currentPage = 1;
                    this.loadData();
                },
                handleReset() {
                    this.searchForm = { title: '', status: '' };
                    this.currentPage = 1;
                    this.loadData();
                },
                handleSizeChange(size) {
                    this.pageSize = size;
                    this.currentPage = 1;
                    this.loadData();
                },
                handlePageChange(page) {
                    this.currentPage = page;
                    this.loadData();
                },
                handleSelectionChange(rows) {
                    this.selectedRows = rows;
                },
                openAdd() {
                    this.dialogMode = 'add';
                    this.formData = { id: '', title: '', content: '', status: '1' };
                    this.dialogVisible = true;
                    this.$nextTick(() => {
                        if (this.$refs.formRef) {
                            this.$refs.formRef.clearValidate();
                        }
                    });
                },
                openEdit(row) {
                    this.dialogMode = 'edit';
                    this.formData = {
                        id: row.id,
                        title: row.title,
                        content: row.content,
                        status: String(row.status)
                    };
                    this.dialogVisible = true;
                    this.$nextTick(() => {
                        if (this.$refs.formRef) {
                            this.$refs.formRef.clearValidate();
                        }
                    });
                },
                openDetail(row) {
                    this.dialogMode = 'detail';
                    this.formData = {
                        id: row.id,
                        title: row.title,
                        content: row.content,
                        status: String(row.status)
                    };
                    this.dialogVisible = true;
                },
                submitForm() {
                    this.$refs.formRef.validate((valid) => {
                        if (!valid) {
                            return;
                        }
                        this.submitting = true;
                        const payload = {
                            id: this.formData.id,
                            title: this.formData.title,
                            content: this.formData.content,
                            status: this.formData.status
                        };
                        $.ajax({
                            url: baseUrl + '/announcement/edit/auth',
                            headers: this.getHeaders(),
                            data: payload,
                            method: 'POST',
                            success: (res) => {
                                this.submitting = false;
                                if (res.code === 200) {
                                    this.$message.success('保存成功');
                                    this.dialogVisible = false;
                                    this.loadData();
                                } else if (res.code === 403) {
                                    top.location.href = baseUrl + '/';
                                } else {
                                    this.$message.error(res.msg || '保存失败');
                                }
                            },
                            error: () => {
                                this.submitting = false;
                                this.$message.error('请求失败');
                            }
                        });
                    });
                },
                handleDelete(row) {
                    this.$confirm('确定删除该公告吗?', '提示', { type: 'warning' }).then(() => {
                        $.ajax({
                            url: baseUrl + '/announcement/delete/auth',
                            headers: this.getHeaders(),
                            data: { ids: [row.id] },
                            method: 'POST',
                            traditional: true,
                            success: (res) => {
                                if (res.code === 200) {
                                    this.$message.success('删除成功');
                                    this.loadData();
                                } else if (res.code === 403) {
                                    top.location.href = baseUrl + '/';
                                } else {
                                    this.$message.error(res.msg || '删除失败');
                                }
                            }
                        });
                    }).catch(() => {});
                },
                handleBatchDelete() {
                    const ids = this.selectedRows.map(item => item.id);
                    if (!ids.length) {
                        this.$message.warning('请选择数据');
                        return;
                    }
                    this.$confirm('确定删除选中的公告吗?', '提示', { type: 'warning' }).then(() => {
                        $.ajax({
                            url: baseUrl + '/announcement/delete/auth',
                            headers: this.getHeaders(),
                            data: { ids: ids },
                            method: 'POST',
                            traditional: true,
                            success: (res) => {
                                if (res.code === 200) {
                                    this.$message.success('删除成功');
                                    this.selectedRows = [];
                                    this.loadData();
                                } else if (res.code === 403) {
                                    top.location.href = baseUrl + '/';
                                } else {
                                    this.$message.error(res.msg || '删除失败');
                                }
                            }
                        });
                    }).catch(() => {});
                },
                handleExport() {
                    const fields = ['id', 'title', 'content', 'status', 'createTime', 'updateTime'];
                    const titles = ['ID', '标题', '内容', '状态', '创建时间', '修改时间'];
                    const params = {
                        announcement: { ...this.searchForm },
                        fields: fields
                    };
                    $.ajax({
                        url: baseUrl + '/announcement/export/auth',
                        headers: this.getHeaders(),
                        data: JSON.stringify(params),
                        dataType: 'json',
                        contentType: 'application/json;charset=UTF-8',
                        method: 'POST',
                        success: (res) => {
                            if (res.code === 200) {
                                const rows = res.data || [];
                                const csvRows = [titles.join(',')];
                                rows.forEach(row => {
                                    const values = fields.map(field => {
                                        const value = row[field];
                                        const text = value == null ? '' : String(value).replace(/"/g, '""');
                                        return `"${text}"`;
                                    });
                                    csvRows.push(values.join(','));
                                });
                                const blob = new Blob([csvRows.join('\n')], { type: 'text/csv;charset=utf-8;' });
                                const link = document.createElement('a');
                                link.href = URL.createObjectURL(blob);
                                link.download = 'announcement.csv';
                                document.body.appendChild(link);
                                link.click();
                                document.body.removeChild(link);
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + '/';
                            } else {
                                this.$message.error(res.msg || '导出失败');
                            }
                        }
                    });
                }
            }
        });
    </script>
</body>
</html>
src/main/webapp/views/tvDevice/tvDevice.html
@@ -157,6 +157,24 @@
                </div>
            </div>
            <div style="margin-bottom: 15px;">
                <el-card shadow="hover">
                    <div slot="header">
                        <span>手动异常信息</span>
                    </div>
                    <el-form label-position="top" size="small">
                        <el-form-item label="异常信息">
                            <el-input type="textarea" v-model="manualError" :rows="3"
                                placeholder="输入需要下发到设备的异常信息"></el-input>
                        </el-form-item>
                        <el-button type="primary" size="small" @click="saveManualError"
                            :loading="manualErrorSaving">保存</el-button>
                        <el-button size="small" @click="clearManualError"
                            :disabled="manualErrorSaving">清空</el-button>
                    </el-form>
                </el-card>
            </div>
            <!-- 表格区域 -->
            <div class="table-section">
                <el-table :data="tableData" border stripe @selection-change="handleSelectionChange" style="width: 100%;"
@@ -222,12 +240,9 @@
                                    <el-select v-model="installForm.taskId" placeholder="请选择已完成的打包任务"
                                        style="width: 100%;" filterable>
                                        <el-option v-for="task in completedTasks" :key="task.id"
                                            :label="task.projectName || task.taskId" :value="task.id"
                                            :label="buildTaskLabel(task)" :value="task.id"
                                            :disabled="!task.apkPath">
                                            <span>{{ task.projectName || task.repoAlias }}</span>
                                            <span style="float: right; color: #909399; font-size: 12px;">
                                                {{ task.apkPath ? '已下载' : '未下载' }}
                                            </span>
                                            <span>{{ buildTaskLabel(task) }}</span>
                                        </el-option>
                                    </el-select>
                                </el-form-item>
@@ -430,7 +445,10 @@
                screenshotImage: '',
                screenshotLoading: false,
                autoRefreshScreenshot: false,
                screenshotTimer: null
                screenshotTimer: null,
                manualError: '',
                manualErrorSaving: false
            },
            computed: {
@@ -449,12 +467,60 @@
                this.loadData();
                this.loadAllDevices();
                this.loadCompletedTasks();
                this.loadManualError();
            },
            methods: {
                // 获取请求头
                getHeaders() {
                    return { 'token': localStorage.getItem('token') };
                },
                loadManualError() {
                    $.ajax({
                        url: baseUrl + '/openapi/manualError/auth',
                        headers: this.getHeaders(),
                        method: 'GET',
                        success: (res) => {
                            if (res.code === 200) {
                                this.manualError = (res.data && res.data.manualError) ? res.data.manualError : '';
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + '/';
                            } else {
                                this.$message.error(res.msg || '加载失败');
                            }
                        },
                        error: () => {
                            this.$message.error('请求失败');
                        }
                    });
                },
                saveManualError() {
                    this.manualErrorSaving = true;
                    $.ajax({
                        url: baseUrl + '/openapi/manualError/auth',
                        headers: this.getHeaders(),
                        method: 'POST',
                        contentType: 'application/json;charset=UTF-8',
                        data: JSON.stringify({ manualError: this.manualError }),
                        success: (res) => {
                            this.manualErrorSaving = false;
                            if (res.code === 200) {
                                this.$message.success('保存成功');
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + '/';
                            } else {
                                this.$message.error(res.msg || '保存失败');
                            }
                        },
                        error: () => {
                            this.manualErrorSaving = false;
                            this.$message.error('请求失败');
                        }
                    });
                },
                clearManualError() {
                    this.manualError = '';
                    this.saveManualError();
                },
                // 加载数据
@@ -951,6 +1017,14 @@
                    this.$message.error('上传失败');
                },
                buildTaskLabel(task) {
                    const name = task.projectName || task.repoAlias || task.taskId || '';
                    const time = this.formatDate(task.createdAt) || '-';
                    const id = task.id != null ? task.id : '';
                    const status = task.apkPath ? '已下载' : '未下载';
                    return `${name} | ID: ${id} | ${time} | ${status}`;
                },
                // 格式化日期
                formatDate(timestamp) {
                    if (!timestamp) return '';
@@ -968,4 +1042,4 @@
    </script>
</body>
</html>
</html>