0.4.1更新消息提醒

This commit is contained in:
磷叶 2025-02-22 17:44:28 +08:00
parent 916efb8df5
commit 3da7554476
22 changed files with 955 additions and 53 deletions

View File

@ -13,3 +13,5 @@ VUE_CLI_BABEL_TRANSPILE_MODULES = true
# 七牛云域名
VUE_APP_QINIU_DOMAIN = 'https://api.ccttiot.com'
# WebSocket地址
VUE_APP_WS_HOST = 'ws://localhost:4001'

View File

@ -9,3 +9,6 @@ VUE_APP_BASE_API = '/prod-api'
# 七牛云域名
VUE_APP_QINIU_DOMAIN = 'https://api.ccttiot.com'
# WebSocket地址
VUE_APP_WS_HOST = 'wss://pm.chuangtewl.com'

39
src/api/app/message.js Normal file
View File

@ -0,0 +1,39 @@
import request from '@/utils/request'
/**
* 获取本人接收的消息列表
* @param {Object} query 查询参数
* @returns {Promise}
*/
export function appGetReceiveList(query) {
return request({
url: '/app/message/receiveList',
method: 'get',
params: query
})
}
/**
* 标记消息已读
* @param {number} id 消息ID
* @returns {Promise}
*/
export function appReadMessage(id) {
return request({
url: '/app/message/read',
method: 'post',
params: { id }
})
}
/**
* 查询本人未读消息数量
* @returns {Promise}
*/
export function appGetUnreadCount() {
return request({
url: '/app/message/unreadCount',
method: 'get'
})
}

44
src/api/bst/message.js Normal file
View File

@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询消息列表
export function listMessage(query) {
return request({
url: '/bst/message/list',
method: 'get',
params: query
})
}
// 查询消息详细
export function getMessage(id) {
return request({
url: '/bst/message/' + id,
method: 'get'
})
}
// 新增消息
export function addMessage(data) {
return request({
url: '/bst/message',
method: 'post',
data: data
})
}
// 修改消息
export function updateMessage(data) {
return request({
url: '/bst/message',
method: 'put',
data: data
})
}
// 删除消息
export function delMessage(id) {
return request({
url: '/bst/message/' + id,
method: 'delete'
})
}

View File

@ -1,10 +1,5 @@
<template>
<div
class="avatar-wrapper"
:style="{
backgroundColor: backgroundColor
}"
>
<div class="avatar-wrapper" :style="{backgroundColor: backgroundColor}">
<el-avatar :size="size" :src="src" v-if="src"/>
<el-avatar :size="size" v-else :style="{fontSize: fontSize}">
{{ displayChar }}
@ -65,7 +60,7 @@ export default {
const index = Math.abs(sum) % colors.length
return colors[index]
}
}
},
}
</script>

View File

@ -11,12 +11,12 @@
marginLeft: index !== 0 ? `-${size * 0.3}px` : '0',
zIndex: list.length - index
}"
@click="handleClick(item)"
>
<avatar
:size="size"
:src="item[avatarProp]"
:name="item[showProp]"
:id="item[idProp]"
:char-index="charIndex"
/>
</div>
@ -31,25 +31,17 @@
:width="360"
>
<div class="total-list">
<div v-for="item in list" :key="item[idProp]" class="total-item">
<div v-for="item in list" :key="item[idProp]" @click="handleClick(item)" class="total-item">
<avatar
:size="size"
:src="item[avatarProp]"
:name="item[showProp]"
:id="item[idProp]"
:char-index="charIndex"
/>
<span class="total-item-name">{{ item[showProp] }}</span>
</div>
</div>
<div
slot="reference"
class="avatar-item total-text"
:style="{
marginLeft: `0px`,
zIndex: 0
}"
>
<div slot="reference" class="avatar-item total-text" style="margin-left: 0px; z-index: 0;">
<span class="total-count">{{ list.length }}{{ unit }}</span>
</div>
</el-popover>
@ -96,6 +88,11 @@ export default {
unit: {
type: String,
default: '项'
},
// user
bstType: {
type: String,
default: null
}
},
computed: {
@ -110,6 +107,13 @@ export default {
}
},
methods: {
handleClick(item) {
if (this.bstType === 'user') {
this.$router.push({
path: `/view/user/${item[this.idProp]}`
})
}
},
//
getFirstChar(name) {
if (!name) {
@ -192,6 +196,7 @@ export default {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
.total-item-name {
font-size: 16px;

View File

@ -6,7 +6,7 @@
filterable
:disabled="disabled"
:multiple="multiple"
v-on="$listeners"
@change="handleChange"
style="width: 100%;"
>
<el-option
@ -65,6 +65,15 @@ export default {
this.list = response.data;
});
},
handleChange(val) {
if (this.multiple) {
let projectList = this.list.filter(item => val.includes(item.id));
this.$emit("change", projectList);
} else {
let project = this.list.find(item => val === item.id);
this.$emit("change", project);
}
}
}
};
</script>

View File

@ -6,8 +6,9 @@
filterable
:disabled="disabled"
:multiple="multiple"
v-on="$listeners"
@change="handleChange"
style="width: 100%;"
@focus="handleFocus"
>
<div v-if="multiple" class="select-footer">
<el-button size="mini" type="text" @click.stop="handleSelectAll">
@ -15,7 +16,7 @@
</el-button>
</div>
<el-option
v-for="item in list"
v-for="item in filterList"
:key="item.userId"
:label="item.nickName"
:value="item.userId"
@ -44,11 +45,19 @@ export default {
multiple: {
type: Boolean,
default: false
},
//
filterMethod: {
type: Function,
default: (data) => {
return data;
}
}
},
data() {
return {
list: [],
filterList: []
};
},
computed: {
@ -62,16 +71,20 @@ export default {
},
//
isAllSelected() {
return this.multiple && this.list.length > 0 && Array.isArray(this.value) && this.value.length === this.list.length;
return this.multiple && this.filterList.length > 0 && Array.isArray(this.value) && this.value.length === this.filterList.length;
}
},
created() {
this.getList();
},
methods: {
handleFocus() {
this.filterList = this.filterMethod(this.list);
},
getList() {
listAllUser().then(response => {
this.list = response.data;
this.filterList = response.data;
});
},
//
@ -79,9 +92,18 @@ export default {
if (this.isAllSelected) {
this.$emit("input", []);
} else {
const allUserIds = this.list.map(item => item.userId);
const allUserIds = this.filterList.map(item => item.userId);
this.$emit("input", allUserIds);
}
},
handleChange(val) {
if (this.multiple) {
let userList = this.list.filter(item => val.includes(item.userId));
this.$emit("change", userList);
} else {
let user = this.list.find(item => val === item.userId);
this.$emit("change", user);
}
}
}
};

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="screenfull-container">
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
</div>
</template>
@ -45,7 +45,13 @@ export default {
}
</script>
<style scoped>
<style scoped lang="scss">
.screenfull-container {
cursor: pointer;
&:hover svg {
color: #409EFF;
}
}
.screenfull-svg {
display: inline-block;
cursor: pointer;

View File

@ -8,7 +8,8 @@
<div class="right-menu">
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" />
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<screenfull id="screenfull" class="right-menu-item" />
<notice-tip id="notice-tip" class="right-menu-item" />
<!-- <el-tooltip content="布局大小" effect="dark" placement="bottom">-->
<!-- <size-select id="size-select" class="right-menu-item hover-effect" />-->
@ -49,6 +50,7 @@ import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import NoticeTip from '@/views/bst/notice/noticeTip/index.vue'
export default {
components: {
@ -59,7 +61,8 @@ export default {
SizeSelect,
Search,
RuoYiGit,
RuoYiDoc
RuoYiDoc,
NoticeTip
},
computed: {
...mapGetters([

View File

@ -96,3 +96,17 @@ export const NoticeLevel = {
MEDIUM: "2", // 中
LOW: "3" // 低
}
// 公告类型
export const NoticeType = {
NOTICE: "1", // 公告
MESSAGE: "2" // 消息
}
// 消息类型
export const MessageBstType = {
TASK: "task", // 任务
TASK_SUBMIT: "task_submit", // 任务提交
CUSTOMER: "customer", // 客户
}

View File

@ -11,6 +11,9 @@
<el-form-item label="分类" prop="classifyId">
<attach-classify-select v-model="form.classifyId" check-strictly/>
</el-form-item>
<el-form-item label="所属部门" prop="deptId">
<dept-select v-model="form.deptId" check-strictly/>
</el-form-item>
<el-form-item label="资源名称" prop="name" v-if="form.id != null">
<el-input v-model="form.name" placeholder="请输入资源名称" />
</el-form-item>
@ -29,11 +32,14 @@
import { addAttach, updateAttach, getAttach } from "@/api/bst/attach"
import AttachClassifySelect from '@/components/Business/AttachClassify/AttachClassifySelect.vue'
import { FileType } from '@/utils/constants'
import { mapGetters } from 'vuex'
import DeptSelect from '@/components/Business/Dept/DeptSelect.vue'
export default {
name: "AttachClassifyEditDialog",
components: {
AttachClassifySelect
AttachClassifySelect,
DeptSelect
},
props: {
show: {
@ -71,6 +77,7 @@ export default {
}
},
computed: {
...mapGetters(['deptId']),
title() {
return this.id ? '修改资源' : '新增资源'
},
@ -99,6 +106,7 @@ export default {
id: null,
name: null,
classifyId: null,
deptId: this.deptId,
urls: null,
...this.initData
}

View File

@ -14,6 +14,9 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="所属部门" prop="deptId">
<dept-select v-model="queryParams.deptId" @change="handleQuery" check-strictly/>
</el-form-item>
<el-form-item label="创建人" prop="createId">
<user-select v-model="queryParams.createId" @change="handleQuery"/>
</el-form-item>
@ -108,6 +111,8 @@ import AttachClassifyTree from '../attachClassify/components/AttachClassifyTree.
import AttachCard from '@/components/AttachCard/index.vue';
import UserSelect from '@/components/Business/User/UserSelect.vue';
import AttachEditDialog from '@/views/bst/attach/components/AttachEditDialog.vue';
import DeptSelect from '@/components/Business/Dept/DeptSelect.vue';
//
const defaultSort = {
@ -118,7 +123,7 @@ const defaultSort = {
export default {
name: "Attach",
mixins: [$showColumns],
components: {FormCol, AttachClassifyTree, AttachCard, UserSelect, AttachEditDialog},
components: {FormCol, AttachClassifyTree, AttachCard, UserSelect, AttachEditDialog, DeptSelect},
data() {
return {
row: {},
@ -129,6 +134,7 @@ export default {
{key: 'classifyName', visible: true, label: '分类', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'name', visible: false, label: '名称', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'url', visible: false, label: 'URL', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'deptName', visible: true, label: '所属部门', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'createName', visible: true, label: '创建人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'createTime', visible: true, label: '创建时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
],
@ -161,6 +167,7 @@ export default {
isAsc: defaultSort.order,
id: null,
classifyId: null,
deptId: null,
name: null,
url: null,
createId: null,

View File

@ -5,7 +5,7 @@
<el-button type="primary" @click="submitForm" :loading="submitLoading" plain icon="el-icon-check"> </el-button>
</edit-header>
<div class="app-container">
<el-form ref="form" :model="form" :rules="rules" label-width="80px" v-loading="loading">
<el-form ref="form" :model="form" :rules="rules" label-width="80px" size="small" v-loading="loading">
<el-row>
<form-col :span="24" label="客户名称" prop="name">
<el-input v-model="form.name" placeholder="请输入客户名称" />
@ -57,18 +57,32 @@
</el-select>
</form-col>
<form-col :span="span" label="跟进人" prop="followId">
<user-select v-model="form.followId" />
<user-select v-model="form.followId" :disabled="disabledFollowUser" />
</form-col>
<form-col :span="24" label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" maxlength="200" show-word-limit />
</form-col>
<form-col :span="span" label="顾虑点" prop="concern">
<el-input v-model="form.concern" type="textarea" placeholder="请输入顾虑点" maxlength="200" show-word-limit />
</form-col>
<form-col :span="span" label="痛点" prop="pain">
<el-input v-model="form.pain" type="textarea" placeholder="请输入痛点" maxlength="200" show-word-limit />
</form-col>
<form-col :span="span" label="关注点" prop="attention">
<el-input v-model="form.attention" type="textarea" placeholder="请输入关注点" maxlength="200" show-word-limit />
</form-col>
<form-col :span="span" label="需求点" prop="demand">
<el-input v-model="form.demand" type="textarea" placeholder="请输入需求点" maxlength="200" show-word-limit />
</form-col>
</el-row>
<el-tabs v-if="form.id == null">
<el-tab-pane label="跟进记录">
<customer-follow-form :form="form.follow" :hide="['customerId']" :prop-prefix="'follow.'" :span="span"/>
</el-tab-pane>
</el-tabs>
<template v-if="form.id == null">
<el-tabs>
<el-tab-pane label="跟进记录">
<customer-follow-form :form="form.follow" :hide="['customerId']" prop-prefix="follow." :span="span"/>
</el-tab-pane>
</el-tabs>
</template>
</el-form>
</div>
@ -84,6 +98,7 @@ import UserSelect from '@/components/Business/User/UserSelect.vue';
import CustomerFollowForm from '@/views/bst/customerFollow/components/CustomerFollowForm.vue';
import { parseTime } from '@/utils/ruoyi';
import EditHeader from '@/components/EditHeader/index.vue';
import { checkRole } from '@/utils/permission';
export default {
name: "CustomerEdit",
components: { FormCol, UserSelect, CustomerFollowForm, EditHeader },
@ -132,6 +147,10 @@ export default {
},
computed: {
...mapGetters(['userId']),
//
disabledFollowUser() {
return !checkRole(['sys_admin', 'admin']);
},
title() {
return this.id ? '修改客户' : '新增客户';
},

View File

@ -18,6 +18,10 @@
<el-descriptions-item label="备注">{{ detail.remark | dv }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.createTime | dv }}</el-descriptions-item>
<el-descriptions-item label="最后跟进时间">{{ detail.lastFollowTime | dv }}</el-descriptions-item>
<el-descriptions-item label="顾虑点">{{ detail.concern | dv }}</el-descriptions-item>
<el-descriptions-item label="痛点">{{ detail.pain | dv }}</el-descriptions-item>
<el-descriptions-item label="关注点">{{ detail.attention | dv }}</el-descriptions-item>
<el-descriptions-item label="需求点">{{ detail.demand | dv }}</el-descriptions-item>
</el-descriptions>
</el-card>

View File

@ -0,0 +1,375 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="接收人" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入接收人"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="业务ID" prop="bstId">
<el-input
v-model="queryParams.bstId"
placeholder="请输入业务ID"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入标题"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="是否已读" prop="isRead">
<el-input
v-model="queryParams.isRead"
placeholder="请输入是否已读"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="已读时间" prop="readTime">
<el-date-picker clearable
v-model="queryParams.readTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择已读时间">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-has-permi="['bst:message:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-has-permi="['bst:message:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-has-permi="['bst:message:export']"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="messageList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="onSortChange">
<el-table-column type="selection" width="55" align="center" />
<template v-for="column of showColumns">
<el-table-column
:key="column.key"
:label="column.label"
:prop="column.key"
:align="column.align"
:min-width="column.minWidth"
:sort-orders="orderSorts"
:sortable="column.sortable"
:show-overflow-tooltip="column.overflow"
:width="column.width"
>
<template slot-scope="d">
<template v-if="column.key === 'id'">
{{d.row[column.key]}}
</template>
<template v-else>
{{d.row[column.key]}}
</template>
</template>
</el-table-column>
</template>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-has-permi="['bst:message:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-has-permi="['bst:message:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改消息对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<form-col :span="span" label="接收人" prop="userId">
<el-input v-model="form.userId" placeholder="请输入接收人" />
</form-col>
<form-col :span="span" label="业务ID" prop="bstId">
<el-input v-model="form.bstId" placeholder="请输入业务ID" />
</form-col>
<form-col :span="span" label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</form-col>
<form-col :span="span" label="内容">
<editor v-model="form.content" :min-height="192"/>
</form-col>
<form-col :span="span" label="是否已读" prop="isRead">
<el-input v-model="form.isRead" placeholder="请输入是否已读" />
</form-col>
<form-col :span="span" label="已读时间" prop="readTime">
<el-date-picker clearable
v-model="form.readTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择已读时间">
</el-date-picker>
</form-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listMessage, getMessage, delMessage, addMessage, updateMessage } from "@/api/bst/message";
import { $showColumns } from '@/utils/mixins';
import FormCol from "@/components/FormCol/index.vue";
//
const defaultSort = {
prop: "createTime",
order: "descending"
}
export default {
name: "Message",
mixins: [$showColumns],
components: {FormCol},
data() {
return {
span: 24,
//
columns: [
{key: 'id', visible: true, label: 'ID', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'userId', visible: true, label: '接收人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'bstType', visible: true, label: '业务类型', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'bstId', visible: true, label: '业务ID', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'title', visible: true, label: '标题', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'content', visible: true, label: '内容', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'isRead', visible: true, label: '是否已读', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'readTime', visible: true, label: '已读时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
],
//
orderSorts: ['ascending', 'descending', null],
//
loading: true,
//
ids: [],
//
single: true,
//
multiple: true,
//
showSearch: true,
//
total: 0,
//
messageList: [],
//
title: "",
//
open: false,
defaultSort,
//
queryParams: {
pageNum: 1,
pageSize: 20,
orderByColumn: defaultSort.prop,
isAsc: defaultSort.order,
id: null,
userId: null,
bstType: null,
bstId: null,
title: null,
content: null,
isRead: null,
readTime: null
},
//
form: {},
//
rules: {
userId: [
{ required: true, message: "接收人不能为空", trigger: "blur" }
],
bstType: [
{ required: true, message: "业务类型不能为空", trigger: "change" }
],
title: [
{ required: true, message: "标题不能为空", trigger: "blur" }
],
isRead: [
{ required: true, message: "是否已读不能为空", trigger: "blur" }
],
createTime: [
{ required: true, message: "创建时间不能为空", trigger: "blur" }
],
}
};
},
created() {
this.getList();
},
methods: {
/** 当排序按钮被点击时触发 **/
onSortChange(column) {
if (column.order == null) {
this.queryParams.orderByColumn = defaultSort.prop;
this.queryParams.isAsc = defaultSort.order;
} else {
this.queryParams.orderByColumn = column.prop;
this.queryParams.isAsc = column.order;
}
this.getList();
},
/** 查询消息列表 */
getList() {
this.loading = true;
listMessage(this.queryParams).then(response => {
this.messageList = response.rows;
this.total = response.total;
this.loading = false;
});
},
//
cancel() {
this.open = false;
this.reset();
},
//
reset() {
this.form = {
id: null,
userId: null,
bstType: null,
bstId: null,
title: null,
content: null,
isRead: null,
createTime: null,
readTime: null
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
//
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加消息";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const id = row.id || this.ids
getMessage(id).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改消息";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != null) {
updateMessage(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
addMessage(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal.confirm('是否确认删除消息编号为"' + ids + '"的数据项?').then(function() {
return delMessage(ids);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 导出按钮操作 */
handleExport() {
this.download('bst/message/export', {
...this.queryParams
}, `message_${new Date().getTime()}.xlsx`)
}
}
};
</script>

View File

@ -10,9 +10,14 @@
>
<el-form ref="form" :model="form" :rules="rules" label-width="80px" v-loading="loading">
<el-row>
<form-col :span="24" label="标题" prop="title">
<form-col :span="span" label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" />
</form-col>
<form-col :span="span" label="类型" prop="type">
<el-radio-group v-model="form.type" placeholder="请选择类型" style="width: 100%;">
<el-radio-button v-for="dict in dict.type.notice_type" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</form-col>
<form-col :span="span" label="重要程度" prop="level">
<el-radio-group v-model="form.level" placeholder="请选择重要程度" style="width: 100%;">
<el-radio-button v-for="dict in dict.type.notice_level" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
@ -51,7 +56,7 @@ import { NoticeLevel } from '@/utils/enums';
export default {
name: "NoticeEditDialog",
components: { FormCol, DeptSelect, UserSelect },
dicts: ['notice_level'],
dicts: ['notice_level', 'notice_type'],
props: {
show: {
type: Boolean,

View File

@ -151,7 +151,7 @@
<script>
import { listNotice, delNotice } from "@/api/bst/notice";
import { $showColumns } from '@/utils/mixins';
import NoticeEditDialog from './components/NoticeEditDialog.vue';
import NoticeEditDialog from '@/views/bst/notice/components/NoticeEditDialog.vue';
import BooleanTag from '@/components/BooleanTag/index.vue';
import AvatarList from '@/components/AvatarList/index.vue';

View File

@ -0,0 +1,307 @@
<template>
<div class="bst-notice-container">
<el-popover
placement="bottom"
width="500"
trigger="click"
popper-class="bst-notice-popover"
@show="handleAfterEnter"
>
<div slot="reference">
<el-badge :hidden="unreadCount <= 0" :value="unreadCount" :max="99" class="notice-badge">
<i class="el-icon-bell"></i>
</el-badge>
</div>
<el-row type="flex" justify="space-between" style="padding:10px;">
<div class="filter-section">
<el-radio-group v-model="queryParams.isRead" size="mini" @change="handleQuery">
<el-radio-button :label="null">全部</el-radio-button>
<el-radio-button :label="false">未读</el-radio-button>
<el-radio-button :label="true">已读</el-radio-button>
</el-radio-group>
</div>
<el-button type="text" size="mini" @click="handleRead(null)">全部已读</el-button>
</el-row>
<div class="notice-list" @scroll="handleScroll">
<el-collapse accordion v-if="messgeList.length" @change="handleChangeCollapse">
<el-collapse-item v-for="message in messgeList" :name="message.id">
<div slot="title" class="notice-title-box">
<div class="notice-title">
<dict-tag :options="dict.type.message_bst_type" :value="message.bstType" size="mini"/>
[{{message.title}}]
{{message.content}}
</div>
<el-badge is-dot :hidden="message.isRead">
<div class="notice-time">{{message.createTime}}</div>
</el-badge>
</div>
<div class="notice-content" @click="handleClickMessage(message)">{{message.content}}</div>
</el-collapse-item>
</el-collapse>
<el-empty v-if="messgeList.length == 0 && finished" description="暂无消息"></el-empty>
<div v-if="!finished && !loading" class="loading-more">下拉加载更多</div>
<div v-if="finished && messgeList.length > 0" class="loading-more">没有更多了</div>
<div v-if="loading" class="loading-more"><i class="el-icon-loading"></i>加载中...</div>
</div>
</el-popover>
<task-view-dialog :show.sync="showTaskDialog" :id="bstId" />
</div>
</template>
<script>
import { appGetReceiveList, appReadMessage, appGetUnreadCount } from '@/api/app/message'
import { getToken } from '@/utils/auth'
import { MessageBstType } from '@/utils/enums'
import TaskViewDialog from '@/views/bst/task/components/TaskViewDialog.vue'
export default {
name: 'NoticeTip',
components: {
TaskViewDialog
},
dicts: ['message_bst_type'],
data() {
return {
messgeList: [],
queryParams: {
pageNum: 1,
pageSize: 10,
isRead: null
},
unreadCount: 0,
selectedNotice: null,
detailVisible: false,
ws: null,
loading: false,
total: 0,
row: null,
finished: false,
showTaskDialog: false,
bstId: null
}
},
created() {
this.getUnReadCount();
this.initWebSocket();
},
mounted() {
this.tryGetPermission();
},
beforeDestroy() {
if (this.ws) {
this.ws.close()
}
},
methods: {
//
handleClickMessage(message) {
if (message == null) {
return;
}
this.bstId = message.bstId;
if (message.bstType == MessageBstType.TASK) {
this.showTaskDialog = true;
}
},
//
handleAfterEnter() {
this.getUnReadCount();
this.getList();
},
//
async getUnReadCount() {
const res = await appGetUnreadCount();
this.unreadCount = res.data;
},
//
async getList() {
this.total = 0;
this.messgeList = [];
this.queryParams.pageNum = 0;
await this.lodList();
},
async lodList() {
this.queryParams.pageNum ++;
this.loading = true;
try {
const res = await appGetReceiveList(this.queryParams);
this.messgeList.push(...res.rows);
this.total = res.total;
this.finished = this.messgeList.length >= this.total;
} catch (error) {
console.error('获取消息列表失败:', error)
} finally {
this.loading = false;
}
},
initWebSocket() {
const token = getToken(); // token
this.ws = new WebSocket(`${process.env.VUE_APP_WS_HOST}/ws/message?token=${token}`);
this.ws.onopen = () => {
console.log('WebSocket连接成功');
};
this.ws.onmessage = (event) => {
//
let message = JSON.parse(event.data);
this.showDesktopNotification(message);
this.unreadCount ++;
};
this.ws.onclose = (event) => {
console.log('WebSocket连接关闭尝试重连...');
setTimeout(this.initWebSocket, 5000); // 5
};
this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
},
//
showDesktopNotification(message) {
this.tryGetPermission();
if (Notification.permission === 'granted') {
new Notification(`${message.title}`, {
body: message.content,
icon: '/favicon.ico'
})
}
},
//
tryGetPermission() {
if (Notification.permission !== 'granted') {
Notification.requestPermission();
}
},
//
handleChangeCollapse(name) {
let row = this.messgeList.find(item => item.id == name);
if (row != null && !row.isRead) {
this.handleRead(row.id);
}
},
//
async handleRead(id) {
//
let res = await appReadMessage(id);
if (res.code === 200 && res.data > 0) {
this.getUnReadCount();
if (id != null) {
this.messgeList.forEach(item => {
if (item.id == id) {
item.isRead = true;
}
});
} else {
this.getList();
}
}
},
//
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
handleScroll(e) {
const { scrollHeight, scrollTop, clientHeight } = e.target;
// 20px
if (scrollHeight - scrollTop - clientHeight < 20 && !this.loading && !this.finished) {
this.lodList();
}
}
}
}
</script>
<style lang="scss" scoped>
.bst-notice-container {
position: relative;
display: inline-block;
.notice-badge {
cursor: pointer;
::v-deep .el-badge__content {
top: 12px;
right: 12px;
}
.el-icon-bell {
font-size: 20px;
color: #606266;
&:hover {
color: #409EFF;
}
}
}
}
.notice-list {
max-height: 300px;
overflow-y: auto;
padding: 10px;
.loading-more {
text-align: center;
color: #909399;
font-size: 12px;
padding: 10px 0;
}
.notice-title-box {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
width: calc(100% - 1em);
.notice-title {
padding-left: 10px;
position: relative;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.notice-time {
position: relative;
font-size: 12px;
color: #999;
align-items: center;
height: 1.5em;
line-height: 1.5em;
min-width: 120px;
margin-left: 6px;
}
}
.notice-content {
cursor: pointer;
padding-left: 10px;
padding-right: 10px;
font-size: 12px;
color: #606266;
transition: color 0.3s;
&:hover {
color: #409EFF;
}
}
}
</style>
<style lang="scss">
.bst-notice-popover {
padding: 0 !important;
}
</style>

View File

@ -11,17 +11,17 @@
<el-form ref="form" :model="form" :rules="rules" label-width="80px" v-loading="loading">
<el-row>
<form-col :span="24" label="项目" prop="projectId">
<project-select v-model="form.projectId" :disabled="initData.projectId != null"/>
<project-select v-model="form.projectId" :disabled="initData.projectId != null" @change="onChangeProject"/>
</form-col>
<form-col :span="24" label="附件" prop="picture">
<image-upload v-model="form.picture" :file-type="FileType"/>
</form-col>
<form-col :span="span" label="类型" prop="type">
<form-col :span="24" label="类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio-button v-for="dict in dict.type.task_type" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</form-col>
<form-col :span="span" label="优先级" prop="level">
<form-col :span="24" label="优先级" prop="level">
<el-radio-group v-model="form.level">
<el-radio-button v-for="dict in dict.type.task_level" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
@ -30,7 +30,7 @@
<el-input v-model="form.description" placeholder="请输入任务内容" type="textarea" maxlength="1000" show-word-limit/>
</form-col>
<form-col :span="24" label="负责人" prop="ownerIds">
<user-select v-model="form.ownerIds" multiple />
<user-select v-model="form.ownerIds" multiple :filter-method="filterOwnerMethod"/>
</form-col>
<form-col :span="span" label="截止时间" prop="expireTime">
<el-date-picker clearable
@ -120,6 +120,29 @@ export default {
}
},
methods: {
filterOwnerMethod(data) {
console.log("filterOwnerMethod", data, this.form);
if (this.form.projectId == null) {
return data;
}
if (this.form.projectOwnerId == null || this.form.projectFollowId == null || this.form.projectMemberIds == null) {
return [];
}
//
let userList = data.filter(item => {
return item.userId === this.form.projectOwnerId || item.userId === this.form.projectFollowId || this.form.projectMemberIds.includes(item.userId);
});
return userList;
},
onChangeProject(project) {
this.form.projectOwnerId = project.ownerId;
this.form.projectFollowId = project.followId;
this.form.projectMemberIds = project.memberIds;
if (this.form.ownerIds.length > 0) {
this.form.ownerIds = [];
this.$message("由于更换了项目,负责人已清空");
}
},
/** 获取详细信息 */
getInfo(id) {
this.loading = true;

View File

@ -53,7 +53,7 @@
</el-card>
<!-- 提交记录审核 -->
<task-verify-panel :task="detail" :submit-list="detail.submitList" @success="getInfo(detail.id)"/>
<task-verify-panel :task="detail" :submit-list="detail.submitList" @success="onSubmit"/>
</el-col>
@ -72,7 +72,7 @@
</el-row>
<!-- 取消任务 -->
<task-cancel-dialog :id="detail.id" :show.sync="showCancelDialog" @success="getInfo(detail.id)"/>
<task-cancel-dialog :id="detail.id" :show.sync="showCancelDialog" @success="onCancel"/>
</el-drawer>
</template>
@ -128,6 +128,17 @@ export default {
}
},
methods: {
onCancel() {
this.refreshList();
this.getInfo(this.detail.id);
},
onSubmit() {
this.refreshList();
this.getInfo(this.detail.id);
},
refreshList() {
this.$emit('refresh');
},
getRealUrl,
//
handleCancel() {

View File

@ -121,11 +121,12 @@
</template>
<template v-else-if="column.key === 'createTime'">
创建:{{d.row.createTime | dv}}<br/>
截止:{{d.row.expireTime | dv}}
截止:{{d.row.expireTime | dv}}<br/>
完成:{{d.row.passTime | dv}}<br/>
</template>
<template v-else-if="column.key === 'owner'">
<div style="margin: 0 auto; width: fit-content">
<avatar-list :list="d.row.ownerList" :max-count="3" unit="人" :char-index="-1"/>
<avatar-list :list="d.row.ownerList" :max-count="3" unit="人" :char-index="-1" bst-type="user"/>
</div>
</template>
<template v-else>
@ -181,6 +182,7 @@
<task-view-dialog
:show.sync="showViewDialog"
:id="row.id"
@refresh="getList"
/>
</div>
</template>
@ -227,14 +229,13 @@ export default {
columns: [
{key: 'id', visible: false, label: 'ID', minWidth: null, sortable: true, overflow: false, align: 'center', width: "80"},
{key: 'description', visible: true, label: '任务内容', minWidth: "350", sortable: false, overflow: false, align: 'left', width: null},
{key: 'type', visible: true, label: '类型', minWidth: null, sortable: true, overflow: false, align: 'center', width: "80"},
{key: 'level', visible: true, label: '优先', minWidth: null, sortable: true, overflow: false, align: 'center', width: "80"},
{key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: "80"},
{key: 'picture', visible: true, label: '图片', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'projectName', visible: true, label: '项目', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'owner', visible: true, label: '负责人', minWidth: null, sortable: false, overflow: false, align: 'center', width: "150"},
{key: 'createName', visible: true, label: '创建人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'createTime', visible: true, label: '时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "190"},
{key: 'submitCount', visible: true, label: '提交', minWidth: null, sortable: false, overflow: false, align: 'center', width: "50"},
{key: 'passCount', visible: true, label: '完成', minWidth: null, sortable: false, overflow: false, align: 'center', width: "50"},
{key: 'createTime', visible: true, label: '时间', minWidth: null, sortable: true, overflow: false, align: 'left', width: "190"},
],
//
orderSorts: ['ascending', 'descending', null],