feat: 接龙功能 - 100%

This commit is contained in:
weifashi 2023-12-07 20:26:32 +08:00
parent d72ab58f98
commit 85ef2d9687
22 changed files with 424 additions and 124 deletions

View File

@ -1991,4 +1991,71 @@ class DialogController extends AbstractController
}
return Base::retSuccess('success', $dialog);
}
/**
* @api {post} api/dialog/msg/wordchain 15. 发送接龙消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__wordchain
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} uuid 接龙ID
* @apiParam {String} text 接龙内容
* @apiParam {Array} list 接龙列表
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__wordchain()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$uuid = trim(Request::input('uuid'));
$text = trim(Request::input('text'));
$list = Request::input('list');
//
$result = [];
//
WebSocketDialog::checkDialog($dialog_id);
$strlen = mb_strlen($text);
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
if ($strlen < 1) {
return Base::retError('内容不能为空');
}
if ($noimglen > 200000) {
return Base::retError('内容最大不能超过200000字');
}
//
$userid = $user->userid;
if($uuid){
$dialogMsg = WebSocketDialogMsg::whereDialogId($dialog_id)
->whereType('word-chain')
->orderByDesc('created_at')
->where('msg','like',"%$uuid%")
->value('msg');
$list = array_reverse(array_merge($dialogMsg['list'] ?? [], $list));
$list = array_reduce($list, function ($result, $item) {
$fieldValue = $item['id']; // 指定字段名
if(!isset($result[$fieldValue])) {
$result[$fieldValue] = $item;
}
return $result;
}, []);
$list = array_reverse(array_values($list));
}
//
$msgData = [
'text' => $text,
'list' => $list,
'userid' => $userid,
'uuid' => $uuid ?: Base::generatePassword(36),
];
$result = WebSocketDialogMsg::sendMsg(null, $dialog_id, 'word-chain', $msgData, $user->userid);
//
return $result;
}
}

View File

@ -1431,3 +1431,14 @@ APP推送
状态
协助人
未变更移动项
发起接龙
发起接龙,参与接龙目前共(*)人
可填写接龙格式
重复内容将不再计入接龙结果
返回编辑
继续发送
接龙结果
选择群组发起接龙
来自

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1701947484494" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14371" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M671.1 405.7C524.4 405.7 405 525.1 405 671.8s119.4 266.1 266.1 266.1 266.1-119.4 266.1-266.1-119.4-266.1-266.1-266.1z m74.5 298.1H703v42.6c0 17.6-14.3 31.9-31.9 31.9-17.6 0-31.9-14.3-31.9-31.9v-42.6h-42.6c-17.6 0-31.9-14.3-31.9-31.9 0-17.6 14.3-31.9 31.9-31.9h42.6v-42.6c0-17.6 14.3-31.9 31.9-31.9 17.6 0 31.9 14.3 31.9 31.9V640h42.6c17.6 0 31.9 14.3 31.9 31.9 0 17.6-14.3 31.9-31.9 31.9zM202.6 509.7c0-170.2 138.5-308.7 308.7-308.7 22.3 0 44 2.5 65 7-47.5-73.3-129.8-122-223.4-122C206.2 86 86.8 205.4 86.8 352.1c0 94.1 49.3 176.8 123.2 224.1-4.7-21.4-7.4-43.6-7.4-66.5z" p-id="14372" fill="#1296db"></path><path d="M362.4 671.8c0-170.2 138.5-308.7 308.7-308.7 22.9 0 45.2 2.7 66.6 7.4-46.9-76-130.7-127-226.4-127-146.7 0-266.1 119.4-266.1 266.1 0 94.2 49.4 177 123.5 224.3-4.2-20-6.3-40.8-6.3-62.1z" p-id="14373" fill="#1296db"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,90 +0,0 @@
<template>
<Modal class-name="chain-reaction-wrapper"
v-model="show"
:mask-closable="false"
:title="$L('发起接龙')"
:closable="!isFullscreen"
:fullscreen="isFullscreen"
:footer-hide="isFullscreen">
<!-- 顶部 -->
<template #header>
<div v-if="isFullscreen" class="user-modal-header">
<div class="user-modal-close" @click="show = false">
{{ $L('关闭') }}
</div>
<div class="user-modal-title">1</div>
<div class="user-modal-submit" @click="showMultiple = true">
{{$L('多选')}}
</div>
</div>
</template>
<template #close>
<i class="ivu-icon ivu-icon-ios-close"></i>
</template>
<!-- -->
2212
<div slot="footer">
<Button type="default" @click="show=false">{{$L('取消')}}</Button>
<Button type="primary" @click="onSend">{{$L('发送')}}</Button>
</div>
<!-- <TaskDetail ref="taskDetail" :task-id="taskId" :open-task="taskData" modalMode/> -->
</Modal>
<!-- <Modal
v-model="fullInput"
:mask-closable="false"
:beforeClose="onFullBeforeClose"
class-name="chat-input-full-input"
footer-hide
fullscreen>
<div class="chat-input-box">
<div class="chat-input-wrapper">
<div ref="editorFull" class="no-dark-content"></div>
</div>
</div>
<i slot="close" class="taskfont">&#xe6ab;</i>
</Modal> -->
</template>
<script>
import {mapState} from "vuex";
export default {
name: 'ChainReaction',
props: {
disabled: {
type: Boolean,
default: false
},
},
data() {
return {
show: false, // overflow
}
},
computed: {
...mapState(['chainReaction']),
isFullscreen({ windowWidth }) {
return windowWidth < 576;
},
},
watch: {
chainReaction(data) {
if(data.type == 'create' && data.dialog_id){
this.show = true;
}
}
},
methods: {
onSend(e) {
this.$emit("on-click", e)
}
}
}
</script>

View File

@ -30,7 +30,7 @@
({{selects.length}}<span v-if="multipleMax">/{{multipleMax}}</span>)
</template>
</div>
<div v-else class="user-modal-submit" @click="showMultiple = true">
<div v-else-if="!forcedRadio" class="user-modal-submit" @click="showMultiple = true">
{{$L('多选')}}
</div>
</div>
@ -145,19 +145,18 @@
<!-- 底部 -->
<template #footer>
<Button v-if="!multipleChoice && showMultiple" @click="showMultiple = false">
<Button v-if="!forcedRadio && !multipleChoice && showMultiple" @click="showMultiple = false">
{{$L('取消')}}
</Button>
<Button v-if="showMultiple" type="primary" :loading="submittIng > 0" @click="onSubmit(1)">
<Button v-if="!forcedRadio && showMultiple" type="primary" :loading="submittIng > 0" @click="onSubmit(1)">
{{$L('确定')}}
<template v-if="selects.length > 0">
({{selects.length}}<span v-if="multipleMax">/{{multipleMax}}</span>)
</template>
</Button>
<Button v-else type="primary" @click="showMultiple = true">
<Button v-else-if="!forcedRadio" type="primary" @click="showMultiple = true">
{{$L('多选')}}
</Button>
</template>
</Modal>
@ -336,6 +335,16 @@ export default {
type: Boolean,
default: true
},
//
forcedRadio: {
type: Boolean,
default: false
},
//
group: {
type: Boolean,
default: false
},
//
beforeSubmit: Function
@ -379,7 +388,7 @@ export default {
},
isWhole: {
handler(value) {
if (value) {
if (value || this.group) {
this.switchActive = 'recent';
}
else {
@ -392,6 +401,9 @@ export default {
if (value) {
this.searchBefore();
this.showMultiple = this.multipleChoice
if(this.forcedRadio){
this.showMultiple = false;
}
}
else {
this.searchKey = "";
@ -413,7 +425,7 @@ export default {
return windowWidth < 576;
},
isWhole({ projectId, noProjectId, dialogId }) {
return projectId === 0 && noProjectId === 0 && dialogId === 0;
return projectId === 0 && noProjectId === 0 && dialogId === 0 && !this.group;
},
lists({ switchActive, searchKey, recents, contacts, projects }) {
switch (switchActive) {
@ -528,6 +540,9 @@ export default {
},
searchRecent() {
this.recents = this.cacheDialogs.filter(dialog => {
if(this.group && dialog.type != 'group'){
return false;
}
if (dialog.name === undefined || dialog.dialog_delete === 1) {
return false;
}

View File

@ -0,0 +1,226 @@
<template>
<Modal class-name="word-chain-wrapper"
v-model="show"
:mask-closable="false"
:title="wordChain.type == 'create' ? $L('发起接龙') : $L('接龙结果')"
:closable="!isFullscreen"
:fullscreen="isFullscreen"
:footer-hide="isFullscreen">
<!-- 顶部 -->
<template #header>
<div v-if="isFullscreen" class="chain-modal-header">
<div class="chain-modal-close" @click="show = false">
{{ $L('取消') }}
</div>
<div class="chain-modal-title">
{{ wordChain.type == 'create' ? $L('发起接龙') : $L('接龙结果') }}
</div>
<div class="chain-modal-submit" :class="{'disabled': !isEdit}" @click="onSend" >
<div v-if="loadIng > 0" class="submit-loading"><Loading /></div>
{{$L('发送')}}
</div>
</div>
</template>
<template #close>
<i class="ivu-icon ivu-icon-ios-close"></i>
</template>
<div ref="wordChainBodyRef" class="word-chain-body">
<div class="source" v-if="wordChain.type == 'create'">
{{$L('来自')}}
<span>{{ dialog.name }}</span>
</div>
<div class="initiate">
<span>{{ $L('由') }}</span>
<UserAvatar :userid="createId" :size="22" :showName="true" tooltipDisabled/>
<span> {{ $L('发起,参与接龙目前共'+num+'人') }}</span>
</div>
<div class="textarea">
<Input ref="wordChainTextareaRef" v-model="value" type="textarea" :autosize="{minRows: 3,maxRows: 5}" :disabled="wordChain.type != 'create'" />
</div>
<ul ref="wordChainListRef">
<li v-for="(item,index) in list" :key="index" v-if="item.type == 'case' && (wordChain.type == 'create' || item.text)">
<span>{{ $L('例') }}</span>
<Input v-model="item.text" :placeholder="$L('可填写接龙格式')" :disabled="wordChain.type != 'create'" />
</li>
<li v-for="(item,index) in list" :key="index" v-if="item.type != 'case'">
<span>{{index}}</span>
<Input v-model="item.text" :disabled="item.userid != userId"/>
</li>
<li class="add">
<i class="taskfont" @click="add">&#xe78c;</i>
</li>
</ul>
</div>
<div slot="footer">
<Button type="default" @click="show=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="loadIng > 0" @click="onSend" :disabled="!isEdit">{{$L('发送')}}</Button>
</div>
</Modal>
</template>
<script>
import {mapState} from "vuex";
export default {
name: 'WordChain',
data() {
return {
show: false,
createId: 0,
value: "#接龙 \n",
list: [],
oldData: '',
loadIng: 0,
}
},
computed: {
...mapState(['wordChain', 'userInfo', 'dialogMsgs', 'cacheDialogs']),
isFullscreen({ windowWidth }) {
return windowWidth < 576;
},
num(){
return this.list.filter(h=>h.type != 'case')?.length || 0;
},
allList(){
let list = JSON.parse(JSON.stringify(this.wordChain.msgData?.msg?.list)) || [];
this.dialogMsgs.filter(h=>{
return h.type == "word-chain" && h.msg?.uuid == this.wordChain.msgData?.msg?.uuid
}).forEach((h)=>{
(h.msg.list || []).forEach(k=>{
if( k.type != 'case' && list.map(j=>j.id).indexOf(k.id) == -1 ){
list.push(k)
}
})
});
return list;
},
isEdit(){
return this.oldData != JSON.stringify(this.list)
},
dialog(){
return this.cacheDialogs.find(h=>h.id == this.wordChain.dialog_id) || {}
},
},
watch: {
show(val){
if(!val){
this.value = "#接龙 \n";
this.list = [];
}else{
if(this.wordChain.type == 'create'){
this.$nextTick(()=>{
this.$refs.wordChainTextareaRef.focus()
})
}
}
},
wordChain(data) {
if(data.type == 'create' && data.dialog_id){
this.show = true;
this.createId = this.userId
this.list.push({
id: Date.now(),
type: 'case',
userid: this.userId,
text: '',
})
this.list.push({
id: Date.now() + 1,
type: 'text',
userid: this.userId,
text: this.userInfo.nickname,
})
}
if(data.type == 'participate' && data.dialog_id && data.msgData){
this.show = true;
this.createId = data.msgData.msg.userid
this.value = data.msgData.msg.text
this.list = this.allList
this.oldData = JSON.stringify(this.list)
}
}
},
methods: {
add(){
this.list.push({
id: Date.now(),
type: 'text',
userid: this.userId,
text: this.userInfo.nickname,
})
this.$nextTick(()=>{
this.$refs.wordChainListRef.scrollTo(0, 99999);
})
},
onSend() {
if( !this.isEdit ){
return;
}
const texts = this.list.map(h=> h.text);
if( texts.length != [...new Set(texts)].length ){
$A.modalConfirm({
content: '重复内容将不再计入接龙结果',
cancelText: '返回编辑',
okText: '继续发送',
onOk: () => {
this.send()
}
})
return;
}
this.send()
},
/**
* 发送消息
*/
send() {
const list = [];
this.list.forEach(h=>{
if( list.map(h=> h.text).indexOf(h.text) == -1){
list.push(h);
}
});
//
this.loadIng++;
this.$store.dispatch("call", {
url: 'dialog/msg/wordchain',
method: 'post',
data: {
dialog_id: this.wordChain.dialog_id,
text: this.value,
list: list,
uuid: this.wordChain.msgData?.msg?.uuid || ''
}
}).then(({data}) => {
this.show = false;
this.$store.dispatch("saveDialogMsg", data);
}).catch(({msg}) => {
if( msg.indexOf("System error") !== -1){
$A.modalInfo({
language: false,
title: '版本过低',
content: '服务器版本过低,请升级服务器。',
})
return;
}
$A.modalError(msg);
}).finally(_ => {
this.loadIng--;
});
}
}
}
</script>

View File

@ -265,7 +265,7 @@
<MobileNotification ref="mobileNotification"/>
<!--接龙-->
<ChainReaction/>
<WordChain/>
<!-- okr明细 -->
<MicroApps v-show="false" v-if="$route.name != 'manage-apps'" name="okr-details" :url="okrUrl" :datas="okrWindow"/>
@ -293,7 +293,7 @@ import ApproveExport from "./manage/components/ApproveExport";
import notificationKoro from "notification-koro1";
import {Store} from "le5le-store";
import MicroApps from "../components/MicroApps.vue";
import ChainReaction from "../components/ChainReaction.vue";
import WordChain from "../components/WordChain.vue";
export default {
components: {
@ -313,7 +313,7 @@ export default {
TeamManagement,
ProjectArchived,
MicroApps,
ChainReaction
WordChain
},
directives: {longpress},
data() {

View File

@ -189,6 +189,19 @@
</div>
</Modal>
<!-- 发起接龙 -->
<UserSelect
ref="wordChain"
v-model="sendData"
:multiple-max="50"
:title="$L('选择群组发起接龙')"
:before-submit="onWordChain"
:show-select-all="false"
:forced-radio="true"
:group="true"
show-dialog
module/>
</div>
</template>
@ -279,6 +292,8 @@ export default {
scanLoginShow: false,
scanLoginLoad: false,
scanLoginCode: '',
//
sendData: []
}
},
activated() {
@ -311,7 +326,7 @@ export default {
{ value: "signin", label: "签到" },
{ value: "meeting", label: "会议" },
{ value: "calendar", label: "日历" },
{ value: "jielong", label: "接龙" },
{ value: "word-chain", label: "接龙" },
];
// wap
let appApplyList = this.windowOrientation != 'portrait' ? (
@ -401,8 +416,9 @@ export default {
case 'scan':
$A.eeuiAppScan(this.scanResult);
return;
case 'jielong':
// DOTO
case 'word-chain':
this.sendData = [];
this.$refs.wordChain.onSelection()
return;
}
this.$emit("on-click", item.value)
@ -511,7 +527,7 @@ export default {
this.scanLoginLoad = false
});
},
//
//
openDetail(desc){
$A.modalInfo({
content: desc,
@ -532,6 +548,27 @@ export default {
})
},
});
},
//
onWordChain(){
const dialog_id = Number(this.sendData[0].replace('d:', ''))
if(this.windowPortrait){
this.$store.dispatch("openDialog", dialog_id ).then(() => {
this.$store.state.wordChain = {
type: 'create',
dialog_id: dialog_id
}
})
}else{
this.goForward({ name: 'manage-messenger', params: { dialog_id: dialog_id}});
setTimeout(()=>{
this.$store.state.wordChain = {
type: 'create',
dialog_id: dialog_id
}
},100)
}
}
}
}

View File

@ -107,7 +107,7 @@
{{$L('全屏输入')}}
</div>
<div v-if="dialogData.type == 'group'" class="chat-input-popover-item" @click="onToolbar('chain-reaction')">
<i class="taskfont">&#xe6a7;</i>
<i class="taskfont">&#xe807;</i>
{{$L('接龙')}}
</div>
</EPopover>
@ -500,7 +500,7 @@ export default {
separateSendButton() {
return $A.jsonParse(window.localStorage.getItem("__keyboard:data__"))?.separate_send_button === 'open';
},
},
watch: {
// Watch content change
@ -1234,7 +1234,7 @@ export default {
break;
case 'chain-reaction':
this.$store.state.chainReaction = {
this.$store.state.wordChain = {
type: 'create',
dialog_id: this.dialogId
}

View File

@ -66,6 +66,17 @@
</li>
</ul>
</div>
<!--接龙-->
<div v-else-if="msgData.type === 'word-chain'" class="content-text content-word-chain no-dark-content">
<pre v-html="$A.formatTextMsg(msgData.msg.text, userId)"></pre>
<ul>
<li v-for="(item,index) in (msgData.msg.list || [])" :key="index">
<span v-if="item.type == 'case' && item.text">{{ $L('') }} {{ item.text }}</span>
<span v-else-if="item.type != 'case'">{{index}}. {{item.text}}</span>
</li>
<li @click="onWordChain" class="participate">{{ $L('参与接龙') }}<span>></span></li>
</ul>
</div>
<!--等待-->
<div v-else-if="msgData.type === 'loading'" class="content-loading">
<Icon v-if="msgData.error === true" type="ios-alert-outline" />
@ -504,6 +515,14 @@ export default {
onShowEmojiUser(item) {
this.$emit("on-show-emoji-user", item)
},
onWordChain(){
this.$store.state.wordChain = {
type: 'participate',
dialog_id: this.msgData.dialog_id,
msgData: this.msgData,
}
}
}
}
</script>

View File

@ -256,7 +256,7 @@
<i class="taskfont" v-html="item.icon"></i>
<span>{{ $L(item.label) }}</span>
</li>
<li @click="onOperate('forward')">
<li v-if="operateItem.type !== 'word-chain' && operateItem.type !== 'vote'" @click="onOperate('forward')">
<i class="taskfont">&#xe638;</i>
<span>{{ $L('转发') }}</span>
</li>

View File

@ -210,7 +210,7 @@ export default {
},
// 接龙
chainReaction: {},
wordChain: {},
// okr窗口
okrWindow: {

View File

@ -29,4 +29,4 @@
@import "project-menu";
@import "calendar";
@import "home-calendar";
@import "chain-reaction";
@import "word-chain";

View File

@ -1,14 +0,0 @@
.chain-reaction-wrapper {
}
body.window-portrait {
.chain-reaction-wrapper {
}
@media (max-width: 640px) {
.chain-reaction-wrapper {
}
}
}

View File

@ -956,6 +956,25 @@
text-decoration: underline;
color: $primary-title-color;
}
.content-word-chain{
ul{
list-style-type:none;
margin-top: 20px;
li{
margin-top: 5px;
}
li.participate{
cursor: pointer;
margin-top: 10px;
color: #0bc037;
>span{
font-size: 12px;
margin-left: 2px;
}
}
}
}
}
.dialog-emoji {
@ -1205,6 +1224,14 @@
.content-unknown {
color: #ffffff;
}
.content-word-chain{
ul{
li.participate{
color: #23241f;
}
}
}
}
.dialog-emoji {

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1701947484494" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14371" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M671.1 405.7C524.4 405.7 405 525.1 405 671.8s119.4 266.1 266.1 266.1 266.1-119.4 266.1-266.1-119.4-266.1-266.1-266.1z m74.5 298.1H703v42.6c0 17.6-14.3 31.9-31.9 31.9-17.6 0-31.9-14.3-31.9-31.9v-42.6h-42.6c-17.6 0-31.9-14.3-31.9-31.9 0-17.6 14.3-31.9 31.9-31.9h42.6v-42.6c0-17.6 14.3-31.9 31.9-31.9 17.6 0 31.9 14.3 31.9 31.9V640h42.6c17.6 0 31.9 14.3 31.9 31.9 0 17.6-14.3 31.9-31.9 31.9zM202.6 509.7c0-170.2 138.5-308.7 308.7-308.7 22.3 0 44 2.5 65 7-47.5-73.3-129.8-122-223.4-122C206.2 86 86.8 205.4 86.8 352.1c0 94.1 49.3 176.8 123.2 224.1-4.7-21.4-7.4-43.6-7.4-66.5z" p-id="14372" fill="#1296db"></path><path d="M362.4 671.8c0-170.2 138.5-308.7 308.7-308.7 22.9 0 45.2 2.7 66.6 7.4-46.9-76-130.7-127-226.4-127-146.7 0-266.1 119.4-266.1 266.1 0 94.2 49.4 177 123.5 224.3-4.2-20-6.3-40.8-6.3-62.1z" p-id="14373" fill="#1296db"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB