Переглянути джерело

feat(announcement): 新增公告管理模块及工作台公告栏

- 新增公告管理API接口,支持分页查询、发布、下架、恢复、批量删除等操作
- 新增公告管理页面,支持按标题、类型、状态筛选,含置顶标识与显示开关
- 新增公告编辑表单,支持文字公告(富文本)和文件资料(附件上传)两种类型
- 新增工作台公告栏面板,前台展示最新已发布且未过期的公告与资料下载
- 注册公告页面路由并集成到工作台待办页面布局中
- 报修申请单页面新增部门多选筛选,优化日期格式与重置逻辑
dazhaxie 1 тиждень тому
батько
коміт
d8604e9a94

+ 146 - 0
src/api/upms/announcement.js

@@ -0,0 +1,146 @@
+import { axios } from '@/utils/request'
+import { stringify } from 'qs'
+
+/**
+ * 公告管理分页
+ * @param parameter
+ * @returns {*}
+ */
+export function getAnnouncementPage (parameter) {
+  return axios({
+    url: '/upms/announcements/page?' + stringify(parameter),
+    method: 'get',
+    headers: {
+      'Content-Type': 'application/json;charset=UTF-8'
+    }
+  })
+}
+
+/**
+ * 前台公告栏:最新已发布且未过期公告
+ * @param parameter { limit }
+ * @returns {*}
+ */
+export function getLatestAnnouncements (parameter) {
+  return axios({
+    url: '/upms/announcements/latest?' + stringify(parameter),
+    method: 'get',
+    headers: {
+      'Content-Type': 'application/json;charset=UTF-8'
+    }
+  })
+}
+
+/**
+ * 查询单条
+ * @param parameter { id }
+ * @returns {*}
+ */
+export function fetchAnnouncement (parameter) {
+  return axios({
+    url: '/upms/announcements/' + parameter.id,
+    method: 'get',
+    headers: {
+      'Content-Type': 'application/json;charset=UTF-8'
+    }
+  })
+}
+
+/**
+ * 新增
+ * @param parameter
+ * @returns {*}
+ */
+export function addAnnouncement (parameter) {
+  return axios({
+    url: '/upms/announcements',
+    method: 'POST',
+    headers: {
+      'Accept': 'application/json',
+      'Content-Type': 'application/json;charset=UTF-8'
+    },
+    data: parameter
+  })
+}
+
+/**
+ * 修改
+ * @param parameter
+ * @returns {*}
+ */
+export function updateAnnouncement (parameter) {
+  return axios({
+    url: '/upms/announcements/' + parameter.id,
+    method: 'PUT',
+    headers: {
+      'Accept': 'application/json',
+      'Content-Type': 'application/json;charset=UTF-8'
+    },
+    data: parameter
+  })
+}
+
+/**
+ * 发布
+ * @param parameter { id }
+ * @returns {*}
+ */
+export function publishAnnouncement (parameter) {
+  return axios({
+    url: '/upms/announcements/' + parameter.id + '/publish',
+    method: 'PUT'
+  })
+}
+
+/**
+ * 下架
+ * @param parameter { id }
+ * @returns {*}
+ */
+export function hideAnnouncement (parameter) {
+  return axios({
+    url: '/upms/announcements/' + parameter.id + '/hide',
+    method: 'PUT'
+  })
+}
+
+/**
+ * 恢复显示
+ * @param parameter { id }
+ * @returns {*}
+ */
+export function showAnnouncement (parameter) {
+  return axios({
+    url: '/upms/announcements/' + parameter.id + '/show',
+    method: 'PUT'
+  })
+}
+
+/**
+ * 删除单条
+ * @param parameter { id }
+ * @returns {*}
+ */
+export function deleteAnnouncement (parameter) {
+  return axios({
+    url: '/upms/announcements/' + parameter.id,
+    method: 'DELETE'
+  })
+}
+
+/**
+ * 批量删除
+ * @param parameter ids 数组
+ * @returns {*}
+ */
+export function deleteAnnouncements (parameter) {
+  return axios({
+    url: '/upms/announcements',
+    method: 'DELETE',
+    headers: {
+      'Accept': 'application/json',
+      'Content-Type': 'application/json;charset=UTF-8'
+    },
+    data: parameter
+  })
+}

+ 1 - 0
src/router/generator-platform-routers.js

@@ -297,6 +297,7 @@ const constantRouterComponents = {
   'FillProjectQuantity': () => import('@/views/fill/project-quantity/FillProjectQuantity'),
   'SbCheckDetail': () => import('@/views/sb/sbcheckdetail/SbCheckDetail'),
   'Article': () => import('@/views/operate/article/Article'),
+  'Announcement': () => import('@/views/upms/announcement/Announcement'),
 
   // 备件盘点
   'StoreCheckPlan': () => import('@/views/store/storecheckplan/StoreCheckPlan'),

+ 63 - 14
src/views/repair/application-form/RepairApplicationForm.vue

@@ -25,6 +25,16 @@
               </a-form-item>
             </a-col>
             <a-col :md="6" :sm="24">
+              <a-form-item label="部门">
+                <a-select v-model="queryParam.deptIdList" mode="multiple" placeholder="请选择部门" allowClear showSearch
+                  optionFilterProp="label">
+                  <a-select-option v-for="item in deptList" :key="item.deptId" :label="item.name"
+                    :value="item.deptId">{{ item.name }}
+                  </a-select-option>
+                </a-select>
+              </a-form-item>
+            </a-col>
+            <a-col :md="6" :sm="24">
               <a-form-item label="问题描述">
                 <a-input v-model="queryParam.content" placeholder="问题描述模糊查询" />
               </a-form-item>
@@ -50,13 +60,13 @@
             <a-col :md="6" :sm="24">
               <a-form-item label="报修起始日期">
                 <a-date-picker v-model="queryParam.searchStartTime" style="width: 100%"
-                  :format="BaseTool.Date.PICKER_NORM_DATETIME_PATTERN" />
+                  :format="BaseTool.Date.PICKER_NORM_DATE_PATTERN" />
               </a-form-item>
             </a-col>
             <a-col :md="6" :sm="24">
               <a-form-item label="报修结束日期">
                 <a-date-picker v-model="queryParam.searchEndTime" style="width: 100%"
-                  :format="BaseTool.Date.PICKER_NORM_DATETIME_PATTERN" />
+                  :format="BaseTool.Date.PICKER_NORM_DATE_PATTERN" />
               </a-form-item>
             </a-col>
             <a-col :md="8 || 24" :sm="24">
@@ -124,6 +134,7 @@ import Detail from './modules/Detail'
 import DetailOut from './modules/DetailRepairOut'
 import { getRepairApplicationFormPage, deleteRepairApplicationForms, fetchRepairApplicationForm, exportRepairApplicationForm } from '@/api/repair/application-form'
 import DispatchBaseForm from '@/views/repair/application-form/modules/DispatchBaseForm'
+import { queryDept } from '@/api/upms/dept'
 
 export default {
   name: 'RepairForm',
@@ -140,24 +151,31 @@ export default {
   props: {
     filter: {
       type: Number,
-      default: 0
+      default: -1
     },
     searchType: {
       type: Number,
-      default: 1
+      default: null
     }
   },
   data() {
+    // 与「所有工单」逻辑一致:默认展示本部门;通知跳转(带no参数)不限制部门
+    const hasNoParam = this.$route.query.no != null
+    const skipDeptDefault = hasNoParam || this.searchType === 2
     return {
       // 查询参数
       queryParam: {
-        filter: this.filter,
-        searchType: this.searchType,
+        keyword: null,
+        zbh: null,
+        actualUser: null,
+        repairUserName: null,
+        deptIdList: skipDeptDefault ? [] : [this.$store.getters.userInfo.deptId],
+        content: null,
+        categoryList: undefined,
+        statusList: undefined,
         searchStartTime: null,
         searchEndTime: null
       },
-      // 刚进页面时的查询参数快照,重置时恢复
-      initQueryParam: {},
       visible: true,
       // 表头
       columns: [
@@ -286,11 +304,15 @@ export default {
       needStopMap: {},
       categoryMap: {},
       faultTypeMap: {},
+      // 部门下拉数据
+      deptList: [],
       // 加载数据方法 必须为 Promise 对象
       loadData: parameter => {
         parameter = {
           ...parameter,
           ...this.queryParam,
+          filter: this.filter,
+          searchType: this.searchType,
           type: 1,
           dataScope: {
             sortBy: 'asc, desc',
@@ -327,9 +349,13 @@ export default {
     const no = this.$route.query.no
     if (no != null) {
       this.queryParam.no = no
+      // 从通知跳转过来时,清除部门筛选,确保能看到该工单
+      this.queryParam.deptIdList = []
     }
-    // 记录进页面时的参数,重置时恢复为相同逻辑
-    this.initQueryParam = { ...this.queryParam }
+    // 部门下拉数据
+    queryDept({}).then(res => {
+      this.deptList = res.data || []
+    })
     this.tableOption()
   },
   methods: {
@@ -396,8 +422,8 @@ export default {
     },
     handleOk() {
       this.visible = true
-      this.queryParam.searchStartTime = this.queryParam.searchStartTime ? this.BaseTool.Date.formatter(this.queryParam.searchStartTime, this.BaseTool.Date.PICKER_NORM_DATETIME_PATTERN) : null
-      this.queryParam.searchEndTime = this.queryParam.searchEndTime ? this.BaseTool.Date.formatter(this.queryParam.searchEndTime, this.BaseTool.Date.PICKER_NORM_DATETIME_PATTERN) : null
+      this.queryParam.searchStartTime = this.queryParam.searchStartTime ? this.BaseTool.Date.formatter(this.queryParam.searchStartTime, this.BaseTool.Date.PICKER_NORM_DATE_PATTERN) + ' 00:00:01' : null
+      this.queryParam.searchEndTime = this.queryParam.searchEndTime ? this.BaseTool.Date.formatter(this.queryParam.searchEndTime, this.BaseTool.Date.PICKER_NORM_DATE_PATTERN) + ' 23:59:59' : null
       this.$refs.table.refresh()
     },
     onSelectChange(selectedRowKeys, selectedRows) {
@@ -405,8 +431,31 @@ export default {
       this.selectedRows = selectedRows
     },
     resetSearchForm() {
-      // 恢复为刚进页面时的查询状态(保留 filter/searchType/no 等固定参数)
-      this.queryParam = { ...this.initQueryParam }
+      // 重置为刚进界面时的查询状态,与「所有工单」逻辑一致
+      const no = this.$route.query.no
+      const skipDeptDefault = no != null || this.searchType === 2
+      // 清空所有查询条件,与初始化时保持一致
+      this.queryParam.keyword = null
+      this.queryParam.zbh = null
+      this.queryParam.actualUser = null
+      this.queryParam.repairUserName = null
+      this.queryParam.content = null
+      this.queryParam.categoryList = undefined
+      this.queryParam.statusList = undefined
+      this.queryParam.searchStartTime = null
+      this.queryParam.searchEndTime = null
+      // 部门:先清空让 a-select 同步状态
+      this.queryParam.deptIdList = []
+      // 保留 no 参数(通知跳转场景)
+      if (no != null) {
+        this.queryParam.no = no
+      }
+      // 默认部门为本部门,等 a-select 同步清空后再设值,避免标签删不掉
+      if (!skipDeptDefault) {
+        this.$nextTick(() => {
+          this.queryParam.deptIdList = [this.$store.getters.userInfo.deptId]
+        })
+      }
       this.$refs.table.refresh(true)
     },
     doExport() {

+ 262 - 0
src/views/upms/announcement/Announcement.vue

@@ -0,0 +1,262 @@
+<template>
+  <a-card :bordered="false">
+    <div class="table-page-search-wrapper">
+      <a-form layout="inline">
+        <a-row :gutter="48">
+          <a-col :md="6" :sm="24">
+            <a-form-item label="标题">
+              <a-input v-model="queryParam.title" placeholder="请输入标题"/>
+            </a-form-item>
+          </a-col>
+          <a-col :md="5" :sm="24">
+            <a-form-item label="类型">
+              <a-select v-model="queryParam.type" placeholder="请选择" allowClear>
+                <a-select-option :value="1">文字公告</a-select-option>
+                <a-select-option :value="2">文件资料</a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+          <a-col :md="5" :sm="24">
+            <a-form-item label="状态">
+              <a-select v-model="queryParam.status" placeholder="请选择" allowClear>
+                <a-select-option :value="0">草稿</a-select-option>
+                <a-select-option :value="1">已发布</a-select-option>
+                <a-select-option :value="2">已下架</a-select-option>
+              </a-select>
+            </a-form-item>
+          </a-col>
+          <a-col :md="8" :sm="24">
+            <span class="table-page-search-submitButtons">
+              <a-button type="primary" @click="$refs.table.refresh(true)">查询</a-button>
+              <a-button style="margin-left: 8px" @click="resetSearchForm">重置</a-button>
+            </span>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+
+    <div class="table-operator">
+      <a-button v-if="$auth('announcement-add')" type="primary" icon="plus" @click="$refs.baseModal.base()">新增</a-button>
+      <a-dropdown v-if="selectedRowKeys.length > 0 && $auth('announcement-del')">
+        <a-menu slot="overlay">
+          <a-menu-item key="1" @click="batchDelete()">
+            <a-icon type="delete"/>批量删除
+          </a-menu-item>
+        </a-menu>
+        <a-button style="margin-left: 8px">批量操作 <a-icon type="down"/></a-button>
+      </a-dropdown>
+    </div>
+
+    <s-table
+      ref="table"
+      size="default"
+      rowKey="id"
+      :columns="columns"
+      :data="loadData"
+      :alert="options.alert"
+      :rowSelection="options.rowSelection"
+      :scroll="{ x: 1250 }"
+      showPagination="auto"
+    >
+      <span slot="topFlag" slot-scope="text">
+        <a-tag v-if="text === 1" color="red">置顶</a-tag>
+        <span v-else>—</span>
+      </span>
+
+      <span slot="showSwitch" slot-scope="record">
+        <template v-if="record.status === 0">
+          <span style="color:#999;">未发布</span>
+        </template>
+        <a-switch
+          v-else
+          :checked="record.status === 1"
+          :loading="record._switchLoading"
+          :disabled="!$auth('announcement-publish')"
+          checked-children="显示"
+          un-checked-children="下架"
+          @change="checked => handleToggle(checked, record)"
+        />
+      </span>
+
+      <span slot="action" slot-scope="record">
+        <template>
+          <a v-if="$auth('announcement-edit')" @click="handleEdit(record)">编辑</a>
+          <a-divider v-if="record.status === 0 && $auth('announcement-publish')" type="vertical"/>
+          <a v-if="record.status === 0 && $auth('announcement-publish')" @click="handlePublish(record)">发布</a>
+          <a-divider v-if="$auth('announcement-del')" type="vertical"/>
+          <a-popconfirm v-if="$auth('announcement-del')" title="确定删除该公告?" @confirm="handleDelete(record)">
+            <a>删除</a>
+          </a-popconfirm>
+        </template>
+      </span>
+    </s-table>
+
+    <base-form ref="baseModal" @ok="handleOk"/>
+  </a-card>
+</template>
+
+<script>
+import { STable } from '@/components'
+import BaseForm from './modules/BaseForm'
+import {
+  getAnnouncementPage,
+  deleteAnnouncement,
+  deleteAnnouncements,
+  publishAnnouncement,
+  hideAnnouncement,
+  showAnnouncement
+} from '@/api/upms/announcement'
+
+const typeMap = { 1: '文字公告', 2: '文件资料' }
+
+export default {
+  name: 'Announcement',
+  components: { STable, BaseForm },
+  data () {
+    return {
+      queryParam: {},
+      columns: [
+        {
+          title: '序号',
+          dataIndex: 'index',
+          width: 60,
+          align: 'center',
+          customRender: (text, record, index) => {
+            return `${(this.$refs.table.localPagination.current - 1) * this.$refs.table.localPagination.pageSize + index + 1}`
+          }
+        },
+        {
+          title: '标题',
+          dataIndex: 'title',
+          width: 260,
+          customRender: (text) => this.BaseTool.Table.customRenderWidthDefault(this, text)
+        },
+        {
+          title: '类型',
+          dataIndex: 'type',
+          width: 100,
+          align: 'center',
+          customRender: (text) => typeMap[text] || '—'
+        },
+        {
+          title: '显示/下架',
+          key: 'showSwitch',
+          width: 110,
+          align: 'center',
+          scopedSlots: { customRender: 'showSwitch' }
+        },
+        {
+          title: '置顶',
+          dataIndex: 'topFlag',
+          width: 80,
+          align: 'center',
+          scopedSlots: { customRender: 'topFlag' }
+        },
+        {
+          title: '发布时间',
+          dataIndex: 'publishTime',
+          width: 160,
+          align: 'center',
+          customRender: (text) => text || '—'
+        },
+        {
+          title: '有效期至',
+          dataIndex: 'expireTime',
+          width: 160,
+          align: 'center',
+          customRender: (text) => text || '长期有效'
+        },
+        {
+          title: '发布人',
+          dataIndex: 'createdUserName',
+          width: 100,
+          align: 'center'
+        },
+        {
+          title: '操作',
+          key: 'action',
+          width: 160,
+          align: 'center',
+          scopedSlots: { customRender: 'action' }
+        }
+      ],
+      loadData: parameter => {
+        parameter = { ...parameter, ...this.queryParam }
+        return getAnnouncementPage(parameter).then(res => res.data)
+      },
+      selectedRowKeys: [],
+      selectedRows: [],
+      options: {
+        alert: { show: true, clear: () => { this.selectedRowKeys = [] } },
+        rowSelection: {
+          selectedRowKeys: this.selectedRowKeys,
+          onChange: this.onSelectChange
+        }
+      },
+      optionAlertShow: true
+    }
+  },
+  methods: {
+    handleEdit (record) {
+      this.$refs.baseModal.base(record)
+    },
+    handlePublish (record) {
+      publishAnnouncement({ id: record.id }).then(() => {
+        this.$message.success('发布成功')
+        this.handleOk()
+      })
+    },
+    handleToggle (checked, record) {
+      if (!checked) {
+        // 下架
+        record._switchLoading = true
+        hideAnnouncement({ id: record.id }).then(() => {
+          this.$message.success('已下架')
+          this.handleOk()
+        }).finally(() => {
+          record._switchLoading = false
+        })
+      } else {
+        // 显示:从下架恢复用 show,其他情况用 publish
+        record._switchLoading = true
+        const req = record.status === 2 ? showAnnouncement({ id: record.id }) : publishAnnouncement({ id: record.id })
+        req.then(() => {
+          this.$message.success('已恢复显示')
+          this.handleOk()
+        }).finally(() => {
+          record._switchLoading = false
+        })
+      }
+    },
+    handleDelete (record) {
+      deleteAnnouncement({ id: record.id }).then(() => {
+        this.$message.success('删除成功')
+        this.handleOk()
+      })
+    },
+    batchDelete () {
+      if (this.selectedRowKeys.length <= 0) {
+        this.$message.info('请选择要删除的记录')
+        return
+      }
+      const ids = this.selectedRows.map(item => item.id)
+      deleteAnnouncements(ids).then(() => {
+        this.$message.success('批量删除成功')
+        this.$refs.table.clearSelected()
+        this.handleOk()
+      })
+    },
+    handleOk () {
+      this.$refs.table.refresh()
+    },
+    onSelectChange (selectedRowKeys, selectedRows) {
+      this.selectedRowKeys = selectedRowKeys
+      this.selectedRows = selectedRows
+    },
+    resetSearchForm () {
+      this.queryParam = {}
+      this.$refs.table.refresh(true)
+    }
+  }
+}
+</script>

+ 238 - 0
src/views/upms/announcement/modules/BaseForm.vue

@@ -0,0 +1,238 @@
+<template>
+  <a-modal
+    :title="modalTitle"
+    :width="900"
+    :visible="visible"
+    :confirmLoading="confirmLoading"
+    @cancel="handleCancel"
+  >
+    <a-form :form="form">
+      <a-form-item v-show="false">
+        <a-input v-decorator="['id']" type="hidden"/>
+      </a-form-item>
+
+      <a-form-item label="标题" :labelCol="labelCol" :wrapperCol="wrapperCol">
+        <a-input
+          placeholder="请输入公告标题"
+          v-decorator="['title', {rules: [{required: true, message: '标题不能为空'}]}]"/>
+      </a-form-item>
+
+      <a-form-item label="类型" :labelCol="labelCol" :wrapperCol="wrapperCol">
+        <a-radio-group v-decorator="['type', {initialValue: 1, rules: [{required: true, message: '类型不能为空'}]}]"
+                        @change="handleTypeChange">
+          <a-radio :value="1">文字公告</a-radio>
+          <a-radio :value="2">文件资料</a-radio>
+        </a-radio-group>
+      </a-form-item>
+
+      <a-form-item v-show="type === 1" label="内容" :labelCol="labelCol" :wrapperCol="wrapperCol">
+        <div class="editor-wrap">
+          <wang-editor :content="content" @catchData="catchData"></wang-editor>
+        </div>
+      </a-form-item>
+
+      <a-form-item v-show="type === 2" label="文件" :labelCol="labelCol" :wrapperCol="wrapperCol">
+        <a-upload
+          :fileList="fileList"
+          :customRequest="customUpload"
+          :remove="handleRemoveFile"
+        >
+          <a-button> <a-icon type="upload"/> 选择文件 </a-button>
+        </a-upload>
+        <div style="color:#999;font-size:12px;">支持上传操作手册等附件,供全员下载查看</div>
+      </a-form-item>
+
+      <a-form-item label="是否置顶" :labelCol="labelCol" :wrapperCol="wrapperCol">
+        <a-radio-group v-model="topFlag">
+          <a-radio :value="1">是</a-radio>
+          <a-radio :value="0">否</a-radio>
+        </a-radio-group>
+      </a-form-item>
+
+      <a-form-item label="有效期至" :labelCol="labelCol" :wrapperCol="wrapperCol">
+        <a-date-picker
+          v-model="expireTime"
+          show-time
+          format="YYYY-MM-DD HH:mm:ss"
+          placeholder="留空表示长期有效"/>
+      </a-form-item>
+    </a-form>
+
+    <template slot="footer">
+      <a-button @click="handleCancel">取消</a-button>
+      <a-button :loading="confirmLoading" type="primary" @click="save()">保存</a-button>
+    </template>
+  </a-modal>
+</template>
+
+<script>
+import pick from 'lodash.pick'
+import moment from 'moment'
+import { addAnnouncement, updateAnnouncement } from '@/api/upms/announcement'
+import { uploadFile } from '@/api/upms/file'
+import WangEditor from '@/components/Editor/WangEditor'
+
+export default {
+  name: 'AnnouncementBaseForm',
+  components: {
+    WangEditor
+  },
+  data () {
+    return {
+      labelCol: { xs: { span: 24 }, sm: { span: 4 } },
+      wrapperCol: { xs: { span: 24 }, sm: { span: 18 } },
+      visible: false,
+      confirmLoading: false,
+      modalTitle: '添加公告',
+      form: this.$form.createForm(this),
+      type: 1,
+      content: '',
+      resultContent: '',
+      topFlag: 0,
+      expireTime: null,
+      fileList: [],
+      fileUrl: '',
+      fileName: '',
+      fileSize: null
+    }
+  },
+  methods: {
+    base (record) {
+      this.visible = true
+      this.resetState()
+      if (this.BaseTool.Object.isBlank(record)) {
+        this.modalTitle = '添加公告'
+        return
+      }
+      this.modalTitle = '编辑公告'
+      const { form: { setFieldsValue } } = this
+      this.$nextTick(() => {
+        setFieldsValue(pick(record, ['id', 'title', 'type']))
+        this.type = record.type || 1
+        this.content = record.content || ''
+        this.topFlag = record.topFlag || 0
+        this.expireTime = record.expireTime ? moment(record.expireTime) : null
+        if (record.fileUrl) {
+          this.fileUrl = record.fileUrl
+          this.fileName = record.fileName
+          this.fileSize = record.fileSize
+          this.fileList = [{
+            uid: '-1',
+            name: record.fileName,
+            status: 'done',
+            url: this.BaseTool.Constant.FILE_URL + record.fileUrl
+          }]
+        }
+      })
+    },
+    resetState () {
+      this.type = 1
+      this.content = ''
+      this.resultContent = ''
+      this.topFlag = 0
+      this.expireTime = null
+      this.fileList = []
+      this.fileUrl = ''
+      this.fileName = ''
+      this.fileSize = null
+    },
+    handleTypeChange (e) {
+      this.type = e.target.value
+    },
+    catchData (content) {
+      this.resultContent = content
+    },
+    async customUpload (data) {
+      const formData = new FormData()
+      formData.append('file', data.file)
+      data.onProgress()
+      try {
+        const res = await uploadFile(formData)
+        const file = res.data
+        this.fileUrl = file.url
+        this.fileName = file.name || file.fileName
+        this.fileSize = data.file.size
+        this.fileList = [{
+          uid: '-1',
+          name: file.name || file.fileName,
+          status: 'done',
+          url: this.BaseTool.Constant.FILE_URL + file.url
+        }]
+        data.onSuccess()
+      } catch (err) {
+        data.onError(err)
+        this.$message.error('文件上传失败')
+      }
+    },
+    handleRemoveFile () {
+      this.fileList = []
+      this.fileUrl = ''
+      this.fileName = ''
+      this.fileSize = null
+      return true
+    },
+    save () {
+      const { form: { validateFieldsAndScroll } } = this
+      this.confirmLoading = true
+      validateFieldsAndScroll((errors, values) => {
+        if (errors) {
+          this.confirmLoading = false
+          return
+        }
+        // 业务校验
+        const finalContent = this.resultContent ? this.resultContent : this.content
+        if (values.type === 1 && this.BaseTool.String.isBlank(finalContent)) {
+          this.$message.error('请填写公告内容')
+          this.confirmLoading = false
+          return
+        }
+        if (values.type === 2 && this.BaseTool.String.isBlank(this.fileUrl)) {
+          this.$message.error('请上传公告文件')
+          this.confirmLoading = false
+          return
+        }
+        const params = {
+          ...values,
+          content: values.type === 1 ? finalContent : '',
+          fileUrl: values.type === 2 ? this.fileUrl : '',
+          fileName: values.type === 2 ? this.fileName : '',
+          fileSize: values.type === 2 ? this.fileSize : null,
+          topFlag: this.topFlag,
+          expireTime: this.expireTime ? this.expireTime.format('YYYY-MM-DD HH:mm:ss') : null
+        }
+        if (this.BaseTool.String.isBlank(params.id)) {
+          addAnnouncement(params).then(() => {
+            this.handleCancel(params)
+          }).catch(() => {
+            this.confirmLoading = false
+          })
+        } else {
+          updateAnnouncement(params).then(() => {
+            this.handleCancel(params)
+          }).catch(() => {
+            this.confirmLoading = false
+          })
+        }
+      })
+    },
+    handleCancel (values) {
+      this.visible = false
+      this.confirmLoading = false
+      this.form.resetFields()
+      if (values && this.BaseTool.Object.isNotBlank(values)) {
+        this.$emit('ok', values)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.editor-wrap {
+  width: 100%;
+  /* WangEditor 组件根 a-card 写死了 width:794px,这里在当前表单内覆盖为自适应,避免溢出弹窗 */
+  /deep/ .ant-card {
+    width: 100% !important;
+  }
+}
+</style>

+ 9 - 0
src/views/workplace/backlog/WorkplaceBacklog.vue

@@ -9,6 +9,8 @@
 
     <a-card :bordered="false" v-show="dataFlag">
       <div v-show="visible">
+        <a-row :gutter="16">
+        <a-col :md="16" :sm="24">
         <div class="table-page-search-wrapper">
           <a-form layout="inline">
             <a-row :gutter="48">
@@ -62,6 +64,11 @@
             <span slot="content" slot-scope="text" v-html="text">
           </span>
         </s-table>
+        </a-col>
+        <a-col :md="8" :sm="24">
+          <announcement-panel />
+        </a-col>
+        </a-row>
       </div>
       <detail ref="detail" />
       <detail-repair-application-form ref="detailRepairApplicationFormModal" @ok="handleOk"/>
@@ -89,6 +96,7 @@ import { PageView } from '@/layouts'
 import HeadInfo from '@/components/tools/HeadInfo'
 import { Radar, STable } from '@/components'
 import Detail from './modules/Detail'
+import AnnouncementPanel from './modules/AnnouncementPanel'
 import SbOilTask from '@/views/sb/oil/SbOilTask'
 import SbOilAudit from '@/views/sb/oil/SbOilAudit'
 import SbOilConfirmBaseForm from '@/views/sb/oil/modules/ConfirmBaseForm'
@@ -120,6 +128,7 @@ export default {
     HeadInfo,
     Radar,
     Detail,
+    AnnouncementPanel,
     SbOilTask,
     SbOilAudit,
     DetailRepairApplicationForm,

+ 142 - 0
src/views/workplace/backlog/modules/AnnouncementPanel.vue

@@ -0,0 +1,142 @@
+<template>
+  <a-card :bordered="false" title="公告栏" class="announcement-panel">
+    <a-tabs default-active-key="1">
+      <!-- 文字公告 -->
+      <a-tab-pane key="1" :tab="`公告 (${textList.length})`">
+        <a-spin :spinning="loading">
+          <a-list v-if="textList.length" size="small" :data-source="textList">
+            <a-list-item slot="renderItem" slot-scope="item">
+              <div class="anno-item" @click="viewText(item)">
+                <a-tag v-if="item.topFlag === 1" color="red">置顶</a-tag>
+                <span class="anno-title">{{ item.title }}</span>
+                <span class="anno-time">{{ formatDate(item.publishTime) }}</span>
+              </div>
+            </a-list-item>
+          </a-list>
+          <a-empty v-else description="暂无公告"/>
+        </a-spin>
+      </a-tab-pane>
+
+      <!-- 文件资料 -->
+      <a-tab-pane key="2" :tab="`资料下载 (${fileList.length})`">
+        <a-spin :spinning="loading">
+          <a-list v-if="fileList.length" size="small" :data-source="fileList">
+            <a-list-item slot="renderItem" slot-scope="item">
+              <div class="anno-item" @click="downloadFile(item)">
+                <a-icon type="paper-clip" class="anno-icon"/>
+                <span class="anno-title">{{ item.fileName || item.title }}</span>
+                <span class="anno-time">{{ formatDate(item.publishTime) }}</span>
+              </div>
+            </a-list-item>
+          </a-list>
+          <a-empty v-else description="暂无资料"/>
+        </a-spin>
+      </a-tab-pane>
+    </a-tabs>
+
+    <a-modal
+      :title="current.title"
+      :visible="textVisible"
+      :footer="null"
+      :width="720"
+      @cancel="textVisible = false"
+    >
+      <div class="anno-content" v-html="current.content"></div>
+    </a-modal>
+  </a-card>
+</template>
+
+<script>
+import { getLatestAnnouncements } from '@/api/upms/announcement'
+
+export default {
+  name: 'AnnouncementPanel',
+  data () {
+    return {
+      loading: false,
+      textList: [],
+      fileList: [],
+      textVisible: false,
+      current: {}
+    }
+  },
+  created () {
+    this.load()
+  },
+  methods: {
+    load () {
+      this.loading = true
+      getLatestAnnouncements({ limit: 10 }).then(res => {
+        const rows = res.data || []
+        this.textList = rows.filter(item => item.type === 1)
+        this.fileList = rows.filter(item => item.type === 2)
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    viewText (item) {
+      this.current = item
+      this.textVisible = true
+    },
+    downloadFile (item) {
+      if (this.BaseTool.String.isBlank(item.fileUrl)) {
+        this.$message.warning('文件不存在')
+        return
+      }
+      window.open(this.BaseTool.Constant.FILE_URL + item.fileUrl, '_blank')
+    },
+    formatDate (val) {
+      if (this.BaseTool.String.isBlank(val)) {
+        return ''
+      }
+      return this.BaseTool.Date.formatter(val, this.BaseTool.Date.PICKER_NORM_DATE_PATTERN)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.announcement-panel {
+  /deep/ .ant-card-head {
+    min-height: 44px;
+    padding: 0 16px;
+    font-weight: 600;
+  }
+  /deep/ .ant-card-body {
+    padding: 8px 16px;
+  }
+}
+.anno-item {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  cursor: pointer;
+  overflow: hidden;
+  .anno-icon {
+    margin-right: 6px;
+    color: #1890ff;
+  }
+  .anno-title {
+    flex: 1;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    &:hover {
+      color: #1890ff;
+    }
+  }
+  .anno-time {
+    margin-left: 8px;
+    color: #999;
+    font-size: 12px;
+    flex-shrink: 0;
+  }
+}
+.anno-content {
+  max-height: 60vh;
+  overflow-y: auto;
+  img {
+    max-width: 100%;
+  }
+}
+</style>