feat: 添加查看共同的群

This commit is contained in:
kuaifan 2025-08-18 08:33:00 +08:00
parent 6964158cf6
commit 1ec4796f72
4 changed files with 409 additions and 36 deletions

View File

@ -2777,6 +2777,83 @@ class DialogController extends AbstractController
]);
}
/**
* @api {get} api/dialog/common/list 56. 共同群组群聊
*
* @apiDescription 需要token身份按置顶时间、用户在群组中的最后活跃时间倒序排列
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName common__list
*
* @apiParam {Number} [target_userid] 目标用户ID和谁的共同群组不传则获取自己所有群组
* @apiParam {Number} [page] 当前页数默认为1
* @apiParam {Number} [pagesize] 每页显示条数默认为20最大100
* @apiParam {String} [only_count] 是否只返回数量,传入 'yes' 则只返回数量不返回列表
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*
* - only_count=yes 时:
* @apiSuccess {Number} data.total 群组数量
*
* - 当获取列表时,返回 Laravel 标准分页格式:
* @apiSuccess {Array} data.data 群组列表数据
* @apiSuccess {Number} data.current_page 当前页数
* @apiSuccess {Number} data.per_page 每页显示条数
* @apiSuccess {Number} data.total 总数量
* @apiSuccess {String} data.first_page_url 第一页链接
* @apiSuccess {String} data.last_page_url 最后页链接
* @apiSuccess {String} data.next_page_url 下一页链接
* @apiSuccess {String} data.prev_page_url 上一页链接
*/
public function common__list()
{
$user = User::auth();
//
$target_userid = intval(Request::input('target_userid'));
$only_count = trim(Request::input('only_count')) === 'yes';
// 参考getDialogList的查询模式
$builder = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->where('u.userid', $user->userid)
->where('d.type', 'group')
->where('d.group_type', 'user')
->whereNull('d.deleted_at');
if ($target_userid) {
// 获取与目标用户的共同群组
$builder->whereExists(function($query) use ($target_userid) {
$query->select(DB::raw(1))
->from('web_socket_dialog_users as du2')
->whereColumn('du2.dialog_id', 'd.id')
->where('du2.userid', $target_userid);
});
}
if ($only_count) {
// 只返回数量
return Base::retSuccess('success', [
'total' => $builder->count()
]);
}
// 返回分页列表参考getDialogList的排序逻辑
$list = $builder
->orderByDesc('u.top_at')
->orderByDesc('u.last_at')
->paginate(Base::getPaginate(100, 20));
// 处理分页数据与getDialogList保持一致的处理方式
$list->transform(function ($item) use ($user) {
return WebSocketDialog::synthesizeData($item, $user->userid);
});
return Base::retSuccess('success', $list);
}
/**
* @api {post} api/dialog/okr/add 56. 创建OKR评论会话
*

View File

@ -6,46 +6,93 @@
:mask-closable="false"
:footer-hide="true"
width="600">
<div class="user-detail-body">
<UserAvatar
:userid="userData.userid"
:size="120"
:show-state-dot="false"
@on-click="onOpenAvatar"/>
<ul class="user-select-auto">
<li class="user-name">
<h1>{{userData.nickname}}</h1>
<em v-if="userData.delete_at" class="deleted no-dark-content">{{$L('已删除')}}</em>
<em v-else-if="userData.disable_at" class="disabled no-dark-content">{{$L('已离职')}}</em>
<div class="user-detail-body">
<UserAvatar
:userid="userData.userid"
:size="120"
:show-state-dot="false"
@on-click="onOpenAvatar"/>
<ul class="user-select-auto">
<li class="user-name">
<h1>{{userData.nickname}}</h1>
<em v-if="userData.delete_at" class="deleted no-dark-content">{{$L('已删除')}}</em>
<em v-else-if="userData.disable_at" class="disabled no-dark-content">{{$L('已离职')}}</em>
</li>
<li v-if="userData.userid != userId && commonDialog.total !== null">
<span>{{$L('共同群聊')}}: </span>
<a href="javascript:void(0)" @click="commonDialogShow=true">{{ $L('(*)', commonDialog.total) }}</a>
</li>
<template v-if="!userData.bot">
<li>
<span>{{$L('部门')}}: </span>
{{userData.department_name || '-'}}
</li>
<template v-if="!userData.bot">
<li>
<span>{{$L('部门')}}: </span>
{{userData.department_name || '-'}}
</li>
<li>
<span>{{$L('职位/职称')}}: </span>
{{userData.profession || '-'}}
</li>
<li>
<span>{{$L('最后在线')}}: </span>
{{$A.newDateString(userData.line_at, 'YYYY-MM-DD HH:mm') || '-'}}
</li>
<li v-if="userData.delete_at">
<strong><span>{{$L('删除时间')}}: </span>{{$A.newDateString(userData.delete_at, 'YYYY-MM-DD HH:mm')}}</strong>
</li>
<li v-else-if="userData.disable_at">
<strong><span>{{$L('离职时间')}}: </span>{{$A.newDateString(userData.disable_at, 'YYYY-MM-DD HH:mm')}}</strong>
</li>
</template>
</ul>
<Button icon="md-chatbubbles" :disabled="!!userData.delete_at" @click="onOpenDialog">{{ $L('开始聊天') }}</Button>
<li>
<span>{{$L('职位/职称')}}: </span>
{{userData.profession || '-'}}
</li>
<li>
<span>{{$L('最后在线')}}: </span>
{{$A.newDateString(userData.line_at, 'YYYY-MM-DD HH:mm') || '-'}}
</li>
<li v-if="userData.delete_at">
<strong><span>{{$L('删除时间')}}: </span>{{$A.newDateString(userData.delete_at, 'YYYY-MM-DD HH:mm')}}</strong>
</li>
<li v-else-if="userData.disable_at">
<strong><span>{{$L('离职时间')}}: </span>{{$A.newDateString(userData.disable_at, 'YYYY-MM-DD HH:mm')}}</strong>
</li>
</template>
</ul>
<Button icon="md-chatbubbles" :disabled="!!userData.delete_at" @click="onOpenDialog">{{ $L('开始聊天') }}</Button>
</div>
<!-- 共同群组 -->
<Modal v-model="commonDialogShow" :title="$L('共同群组') + ' (' + $L('(*)个', commonDialog.total) + ')'" :footer-hide="true" width="500">
<div class="common-dialog-content">
<div v-if="commonDialogLoading > 0 && commonDialog.list.length === 0" class="loading-wrapper">
<Loading/>
</div>
<div v-else-if="commonDialogList.length === 0" class="empty-wrapper">
<div class="empty-content">
<Icon type="ios-people-outline" size="48"/>
<p>{{$L('暂无共同群组')}}</p>
</div>
</div>
<div v-else class="dialog-list">
<div
v-for="dialog in commonDialogList"
:key="dialog.id"
class="dialog-item"
@click="onOpenCommonDialogChat(dialog)">
<div class="dialog-avatar">
<EAvatar v-if="dialog.avatar" :src="dialog.avatar" :size="42"></EAvatar>
<i v-else-if="dialog.group_type=='department'" class="taskfont icon-avatar department">&#xe75c;</i>
<i v-else-if="dialog.group_type=='project'" class="taskfont icon-avatar project">&#xe6f9;</i>
<i v-else-if="dialog.group_type=='task'" class="taskfont icon-avatar task">&#xe6f4;</i>
<i v-else-if="dialog.group_type=='okr'" class="taskfont icon-avatar task">&#xe6f4;</i>
<Icon v-else class="icon-avatar" type="ios-people" />
</div>
<div class="dialog-info">
<div class="dialog-name" v-html="transformEmojiToHtml(dialog.name)"></div>
<div class="dialog-meta">
<span class="member-count">{{$L('(*)人', dialog.people || 0)}}</span>
<span v-if="dialog.last_at" class="last-time">{{$A.timeFormat(dialog.last_at)}}</span>
</div>
</div>
<Icon class="enter-icon" type="ios-arrow-forward" />
</div>
<div v-if="commonDialog.has_more" class="load-more-wrapper">
<Button type="primary" @click="loadCommonDialogList(true)" :loading="commonDialogLoading > 0">{{$L('加载更多')}}</Button>
</div>
</div>
</div>
</Modal>
</ModalAlive>
</template>
<script>
import emitter from "../../../store/events";
import transformEmojiToHtml from "../../../utils/emoji";
import {mapState} from "vuex";
export default {
@ -58,6 +105,15 @@ export default {
},
showModal: false,
commonDialog: {
total: null,
list: [],
page: 1,
has_more: false,
},
commonDialogShow: false,
commonDialogLoading: 0,
}
},
@ -70,16 +126,29 @@ export default {
},
watch: {
...mapState(['cacheUserBasic'])
...mapState(['cacheUserBasic']),
commonDialogShow() {
if (!this.commonDialogShow || this.commonDialog.list.length > 0) {
return;
}
this.loadCommonDialogList(false);
}
},
computed: {
isFullscreen({windowWidth}) {
return windowWidth < 576
},
commonDialogList() {
return this.commonDialog.list || [];
},
},
methods: {
transformEmojiToHtml,
onShow(userid) {
if (!/^\d+$/.test(userid)) {
return
@ -87,13 +156,15 @@ export default {
this.$store.dispatch("showSpinner", 600)
this.$store.dispatch('getUserData', userid).then(user => {
this.userData = user;
this.showModal = true
this.showModal = true;
this.loadCommonDialogCount()
}).finally(_ => {
this.$store.dispatch("hiddenSpinner")
});
},
onHide() {
this.commonDialogShow = false;
this.showModal = false
},
@ -107,7 +178,65 @@ export default {
}).catch(({msg}) => {
$A.modalError(msg)
});
}
},
loadCommonDialogCount() {
const target_userid = this.userData.userid;
if (this.commonDialog.userid !== target_userid) {
this.commonDialog.total = null;
}
this.$store.dispatch('call', {
url: 'dialog/common/list',
data: {
target_userid,
only_count: 'yes'
}
}).then(({data}) => {
if (target_userid !== this.userData.userid) {
return
}
this.commonDialog = Object.assign(data, {
userid: target_userid,
list: [],
has_more: false,
});
});
},
loadCommonDialogList(loadMore = false) {
this.commonDialogLoading++;
const target_userid = this.userData.userid;
this.$store.dispatch('call', {
url: 'dialog/common/list',
data: {
target_userid,
page: loadMore ? this.commonDialog.page + 1 : 1
}
}).then(({data}) => {
if (target_userid !== this.userData.userid) {
return;
}
this.commonDialog = {
...this.commonDialog,
list: loadMore ? [...this.commonDialog.list, ...data.data] : data.data,
total: data.total,
page: data.current_page,
has_more: !!data.next_page_url
}
}).catch(({msg}) => {
$A.modalError(msg || this.$L('加载失败'));
}).finally(() => {
this.commonDialogLoading--;
});
},
onOpenCommonDialogChat(dialog) {
this.$store.dispatch("openDialog", dialog.id).then(() => {
this.onHide();
}).catch(({msg}) => {
$A.modalError(msg);
});
},
}
};
</script>

View File

@ -321,6 +321,18 @@ body.dark-mode-reverse {
}
}
.common-dialog-content {
.dialog-list {
.dialog-item {
.dialog-avatar {
.icon-avatar {
color: #1c1917;
}
}
}
}
}
.file-icon {
&:before {
background-image: url("../images/file/dark/other.svg");

View File

@ -82,3 +82,158 @@
}
}
}
// 共同群组弹窗样式
.common-dialog-content {
margin: -16px -32px 0;
.loading-wrapper {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding-top: 60px;
padding-bottom: 100px;
}
.empty-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding-top: 40px;
padding-bottom: 80px;
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
width: 100%;
color: #999;
> i {
opacity: 0.3;
}
}
}
.dialog-list {
padding: 0 12px;
overflow-y: auto;
max-height: calc(100vh - 310px);
@media (height <= 900px) {
max-height: calc(100vh - 180px);
}
.dialog-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
border-radius: 6px;
margin: 4px 0;
transition: background-color 0.2s;
&:hover {
background-color: #f5f7fa;
}
.dialog-avatar {
flex-shrink: 0;
margin-right: 12px;
.img-avatar,
.user-avatar,
.icon-avatar {
width: 42px;
height: 42px;
margin-right: 2px;
flex-grow: 0;
flex-shrink: 0;
}
.img-avatar {
display: flex;
align-items: center;
justify-content: center;
> img {
width: 100%;
height: 100%;
}
}
.icon-avatar {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 26px;
background-color: #61B2F9;
color: #ffffff;
&.department {
background-color: #5BC7B0;
}
&.project {
background-color: #6E99EB;
}
&.task {
background-color: #9B96DF;
font-size: 24px;
}
}
}
.dialog-info {
flex: 1;
min-width: 0;
.dialog-name {
font-size: 14px;
font-weight: 500;
color: #17233d;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 4px;
}
.dialog-meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: #808695;
.member-count {
flex-shrink: 0;
}
.last-time {
flex-shrink: 0;
}
}
}
.enter-icon {
flex-shrink: 0;
color: #c5c8ce;
font-size: 16px;
margin-left: 8px;
}
}
&:last-child {
padding-bottom: 16px;
}
}
.load-more-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 12px 0;
}
}