mirror of
https://github.com/kuaifan/dootask.git
synced 2026-10-29 22:10:42 +00:00
feat(dialog): 消息操作菜单的复制项精简为「复制 / 选择复制」,新增全屏选择复制页
- 移除「复制原文 / 复制选择 / 复制全部」;「选择复制」打开复制页,可自由选字、底部按钮复制全部,Markdown 消息支持预览 / 原文切换 - 复制页标题与其他会话抽屉一致(居中标题),Markdown 预览沿用消息气泡的深色模式处理 - 移动端打开菜单后不再允许长按选字,移除 selectionchange 监听与 operateEnter - 桌面端保留在消息中选字,直接用键盘复制 - 更新语言包与 ai-kb 移动端手势说明
This commit is contained in:
parent
10378850d2
commit
2556c53fbc
@ -2855,3 +2855,5 @@ DooTask 新版本 (*) 已发布,建议更新以获得更好的使用体验。
|
||||
系统配置
|
||||
使用官方推送服务,无需配置。
|
||||
使用自己的 DooPush 应用,需搭配自行打包的 App。
|
||||
选择复制
|
||||
原文
|
||||
|
||||
@ -41194,5 +41194,29 @@
|
||||
"fr": "Mode de notification invalide",
|
||||
"id": "Mode push tidak valid",
|
||||
"ru": "Неверный режим push-уведомлений"
|
||||
},
|
||||
{
|
||||
"key": "选择复制",
|
||||
"zh": "",
|
||||
"zh-CHT": "選擇複製",
|
||||
"en": "Select & Copy",
|
||||
"ko": "선택하여 복사",
|
||||
"ja": "選択してコピー",
|
||||
"de": "Auswählen und kopieren",
|
||||
"fr": "Sélectionner et copier",
|
||||
"id": "Pilih dan Salin",
|
||||
"ru": "Выбрать и копировать"
|
||||
},
|
||||
{
|
||||
"key": "原文",
|
||||
"zh": "",
|
||||
"zh-CHT": "原文",
|
||||
"en": "Source",
|
||||
"ko": "원문",
|
||||
"ja": "原文",
|
||||
"de": "Quelltext",
|
||||
"fr": "Source",
|
||||
"id": "Sumber",
|
||||
"ru": "Исходный текст"
|
||||
}
|
||||
]
|
||||
2
public/language/web/de.js
vendored
2
public/language/web/de.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/en.js
vendored
2
public/language/web/en.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/fr.js
vendored
2
public/language/web/fr.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/id.js
vendored
2
public/language/web/id.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/ja.js
vendored
2
public/language/web/ja.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/key.js
vendored
2
public/language/web/key.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/ko.js
vendored
2
public/language/web/ko.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/ru.js
vendored
2
public/language/web/ru.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/zh-CHT.js
vendored
2
public/language/web/zh-CHT.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/zh.js
vendored
2
public/language/web/zh.js
vendored
File diff suppressed because one or more lines are too long
@ -31,7 +31,7 @@ last_verified: v1.10.12
|
||||
| 手势 | 触发位置 | 行为 |
|
||||
|---|---|---|
|
||||
| 点击 | 任意可交互元素 | 打开 / 选中 |
|
||||
| 长按 | 消息气泡 | 弹出操作菜单(复制、撤回、转发、回复、翻译等) |
|
||||
| 长按 | 消息气泡 | 弹出操作菜单(复制、选择复制、撤回、转发、回复、翻译等) |
|
||||
| 长按 | 任务卡片 | 弹出快捷操作(完成、归档、更多) |
|
||||
| 双击 | 消息气泡 | 部分版本支持快速点赞 |
|
||||
| 下拉 | 列表顶部 | 刷新 |
|
||||
@ -44,6 +44,7 @@ last_verified: v1.10.12
|
||||
|
||||
## 消息相关
|
||||
- **长按消息**:复制 / 撤回(2 分钟内) / 转发 / 回复 / @TA / 翻译 / 收藏 / 删除
|
||||
- **复制文字**:长按消息 →「复制」复制整条;「选择复制」打开全屏复制页,在页内长按自由选择部分文字,右上角「复制全部」;Markdown 消息可在「预览 / 原文」间切换,「原文」即 Markdown 源码。操作菜单打开时消息本身不再支持长按选字
|
||||
- **左划聊天列表**:置顶 / 免打扰 / 删除会话
|
||||
- **下拉聊天页**:加载历史消息
|
||||
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="dialog-text-select">
|
||||
<div class="text-select-title" :class="{bordered: !source}">{{ $L('选择复制') }}</div>
|
||||
<ul v-if="source" class="text-select-tabs">
|
||||
<li :class="{active: tab === 'preview'}" @click="tab = 'preview'">{{ $L('预览') }}</li>
|
||||
<li :class="{active: tab === 'source'}" @click="tab = 'source'">{{ $L('原文') }}</li>
|
||||
</ul>
|
||||
<div class="text-select-content user-select-auto">
|
||||
<DialogMarkdown v-if="source && tab === 'preview'" class="no-dark-content" :text="source"/>
|
||||
<pre v-else>{{ currentText }}</pre>
|
||||
</div>
|
||||
<div class="text-select-button">
|
||||
<Button type="primary" size="large" icon="md-copy" long @click="onCopyAll">{{ $L('复制全部') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogMarkdown from "./DialogMarkdown.vue";
|
||||
|
||||
export default {
|
||||
name: "DialogTextSelect",
|
||||
components: {DialogMarkdown},
|
||||
props: {
|
||||
// 消息显示的文本
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// Markdown 原文(仅 Markdown 消息)
|
||||
source: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
tab: 'preview',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentText() {
|
||||
return this.source && this.tab === 'source' ? this.source : this.text
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onCopyAll() {
|
||||
this.copyText(this.currentText)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -300,7 +300,6 @@ export default {
|
||||
return {
|
||||
timeShow: false,
|
||||
operateAction: false,
|
||||
operateEnter: false,
|
||||
|
||||
percentageLoad: 0,
|
||||
percentageShow: false,
|
||||
@ -399,7 +398,7 @@ export default {
|
||||
|
||||
replyClass() {
|
||||
const classArray = [];
|
||||
if (this.operateEnter || this.pointerMouse) {
|
||||
if (this.pointerMouse) {
|
||||
classArray.push('user-select-auto')
|
||||
}
|
||||
return classArray;
|
||||
@ -409,7 +408,7 @@ export default {
|
||||
const {type, msg} = this.msgData;
|
||||
const classArray = [];
|
||||
|
||||
if (this.operateEnter || this.pointerMouse) {
|
||||
if (this.pointerMouse) {
|
||||
classArray.push('user-select-auto')
|
||||
}
|
||||
|
||||
@ -439,12 +438,6 @@ export default {
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
operateAction(val) {
|
||||
this.operateEnter = false;
|
||||
if (val) {
|
||||
setTimeout(_ => this.operateEnter = true, 500)
|
||||
}
|
||||
},
|
||||
voteData: {
|
||||
handler(val) {
|
||||
const voteData = JSON.parse(window.localStorage.getItem('__cache:vote__')) || {}
|
||||
|
||||
@ -359,11 +359,10 @@
|
||||
</li>
|
||||
<li
|
||||
v-for="(item, index) in operateCopys"
|
||||
v-if="item.visible !== false"
|
||||
:key="index"
|
||||
@click="onOperate('copy', item)">
|
||||
<i class="taskfont" v-html="item.icon"></i>
|
||||
<span>{{ $L(item.label || item.title) }}</span>
|
||||
<span>{{ $L(item.label) }}</span>
|
||||
</li>
|
||||
<li v-if="actionPermission(operateItem, 'forward')" @click="onOperate('forward')">
|
||||
<i class="taskfont"></i>
|
||||
@ -711,6 +710,14 @@
|
||||
</template>
|
||||
</DrawerOverlay>
|
||||
|
||||
<!--选择复制-->
|
||||
<DrawerOverlay
|
||||
v-model="textSelectShow"
|
||||
placement="right"
|
||||
:size="600">
|
||||
<DialogTextSelect v-if="textSelectShow" :text="textSelectData.text" :source="textSelectData.source"/>
|
||||
</DrawerOverlay>
|
||||
|
||||
<!-- 群接龙 -->
|
||||
<DialogGroupWordChain/>
|
||||
|
||||
@ -727,6 +734,7 @@ import DialogUpload from "./DialogUpload";
|
||||
import DrawerOverlay from "../../../components/DrawerOverlay";
|
||||
import DialogGroupInfo from "./DialogGroupInfo";
|
||||
import DialogRespond from "./DialogRespond";
|
||||
import DialogTextSelect from "./DialogTextSelect.vue";
|
||||
import DialogSessionHistory from "./DialogSessionHistory";
|
||||
import ChatInput from "./ChatInput";
|
||||
|
||||
@ -759,6 +767,7 @@ export default {
|
||||
UserSelect,
|
||||
ImgUpload,
|
||||
DialogRespond,
|
||||
DialogTextSelect,
|
||||
DialogSessionHistory,
|
||||
DialogItem,
|
||||
VirtualList,
|
||||
@ -871,6 +880,10 @@ export default {
|
||||
// 操作菜单状态(通过 provide 下发给消息项,避免作为 props 传递导致打开菜单时所有消息重渲染)
|
||||
dialogOperate: {visible: false, actionId: 0},
|
||||
|
||||
// 选择复制
|
||||
textSelectShow: false,
|
||||
textSelectData: {},
|
||||
|
||||
multiSelectMode: false,
|
||||
selectedMsgIds: [],
|
||||
|
||||
@ -951,13 +964,11 @@ export default {
|
||||
emitter.on('websocketMsg', this.onWebsocketMsg);
|
||||
emitter.on('streamMsgData', this.onMsgChange);
|
||||
this.keepInterval = setInterval(this.keepIntoInput, 1000)
|
||||
this.windowTouch && document.addEventListener('selectionchange', this.onSelectionchange);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (!this.isStaticMode) {
|
||||
this.windowTouch && document.removeEventListener('selectionchange', this.onSelectionchange);
|
||||
clearInterval(this.keepInterval);
|
||||
emitter.off('streamMsgData', this.onMsgChange);
|
||||
emitter.off('websocketMsg', this.onWebsocketMsg);
|
||||
@ -3359,38 +3370,24 @@ export default {
|
||||
value: event.target.href,
|
||||
})
|
||||
}
|
||||
this.operateCopys.push({
|
||||
type: 'selected',
|
||||
icon: '',
|
||||
label: '复制选择',
|
||||
value: '',
|
||||
visible: false,
|
||||
})
|
||||
if (data.type === 'text') {
|
||||
if (data.msg.text.replace(/<[^>]+>/g,"").length > 0) {
|
||||
this.operateCopys.push({
|
||||
type: 'text',
|
||||
icon: '',
|
||||
label: null,
|
||||
title: this.operateCopys.length > 1 ? '复制文本' : '复制',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
if (data.msg.type === 'md') {
|
||||
this.operateCopys.push({
|
||||
type: 'md',
|
||||
icon: '',
|
||||
label: '复制原文',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
if (data.type === 'text' && data.msg.text.replace(/<[^>]+>/g,"").length > 0) {
|
||||
this.operateCopys.push({
|
||||
type: 'text',
|
||||
icon: '',
|
||||
label: this.operateCopys.length > 0 ? '复制文本' : '复制',
|
||||
value: '',
|
||||
}, {
|
||||
type: 'textselect',
|
||||
icon: '',
|
||||
label: '选择复制',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
if (this.operateVisible) {
|
||||
this.checkMessageFavoriteStatus(this.operateItem);
|
||||
}
|
||||
this.operateItem.clientX = event.clientX
|
||||
this.operateItem.clientY = event.clientY
|
||||
this.onSelectionchange()
|
||||
this.onUpdateOperate(element)
|
||||
})
|
||||
break;
|
||||
@ -3421,25 +3418,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
onSelectionchange() {
|
||||
if (!this.operateVisible) {
|
||||
return
|
||||
}
|
||||
const selectedItem = this.operateCopys.find(({type}) => type === 'selected')
|
||||
if (!selectedItem) {
|
||||
return;
|
||||
}
|
||||
const selectText = this.getSelectedTextInElement(this.$refs.scroller.$el.querySelector(`[data-id="${this.operateItem.id}"]`))
|
||||
selectedItem.value = selectText
|
||||
selectedItem.visible = selectText.length > 0
|
||||
//
|
||||
const textItem = this.operateCopys.find(({type}) => type === 'text');
|
||||
if (!textItem) {
|
||||
return;
|
||||
}
|
||||
textItem.label = selectText.length > 0 ? '复制全部' : null
|
||||
},
|
||||
|
||||
onUpdateOperate(el) {
|
||||
if (!el) {
|
||||
return
|
||||
@ -3775,29 +3753,40 @@ export default {
|
||||
this.copyText(value);
|
||||
break;
|
||||
|
||||
case 'selected':
|
||||
this.copyText(value);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
const copyEl = this.$refs.scroller.$el.querySelector(`[data-id="${this.operateItem.id}"]`)?.querySelector(".dialog-content")
|
||||
if (copyEl) {
|
||||
let copyText = copyEl.innerText;
|
||||
if ($A.getObject(this.operateItem.msg, 'type') !== 'md') {
|
||||
copyText = copyText.replace(/\n\n/g, "\n").replace(/(^\s*)|(\s*$)/g, "")
|
||||
}
|
||||
case 'textselect':
|
||||
const copyText = this.getOperateText()
|
||||
if (copyText === null) {
|
||||
$A.messageWarning('不可复制的内容');
|
||||
} else if (type === 'text') {
|
||||
this.copyText(copyText)
|
||||
} else {
|
||||
$A.messageWarning('不可复制的内容');
|
||||
this.textSelectData = {
|
||||
text: copyText,
|
||||
source: $A.getObject(this.operateItem.msg, 'type') === 'md' ? this.operateItem.msg.text : '',
|
||||
}
|
||||
this.textSelectShow = true
|
||||
}
|
||||
break;
|
||||
|
||||
case 'md':
|
||||
this.copyText(this.operateItem.msg.text)
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前操作消息显示的文本
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getOperateText() {
|
||||
const copyEl = this.$refs.scroller.$el.querySelector(`[data-id="${this.operateItem.id}"]`)?.querySelector(".dialog-content")
|
||||
if (!copyEl) {
|
||||
return null
|
||||
}
|
||||
let copyText = copyEl.innerText;
|
||||
if ($A.getObject(this.operateItem.msg, 'type') !== 'md') {
|
||||
copyText = copyText.replace(/\n\n/g, "\n").replace(/(^\s*)|(\s*$)/g, "")
|
||||
}
|
||||
return copyText
|
||||
},
|
||||
|
||||
onWithdraw() {
|
||||
const operateItem = this.operateItem;
|
||||
$A.modalConfirm({
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
@import "dialog-modal";
|
||||
@import "dialog-select";
|
||||
@import "dialog-session-history";
|
||||
@import "dialog-text-select";
|
||||
@import "dialog-wrapper";
|
||||
@import "favorite-management";
|
||||
@import "recent-management";
|
||||
|
||||
73
resources/assets/sass/pages/components/dialog-text-select.scss
vendored
Normal file
73
resources/assets/sass/pages/components/dialog-text-select.scss
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
.dialog-text-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
.text-select-title {
|
||||
flex-shrink: 0;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
padding: 0 56px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $primary-title-color;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.bordered {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.text-select-tabs {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
> li {
|
||||
list-style: none;
|
||||
line-height: 42px;
|
||||
font-size: 14px;
|
||||
color: #888888;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
color: $primary-title-color;
|
||||
font-weight: 500;
|
||||
border-bottom-color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-select-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px 20px 24px;
|
||||
user-select: text;
|
||||
|
||||
> pre {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: $primary-title-color;
|
||||
}
|
||||
}
|
||||
|
||||
.text-select-button {
|
||||
flex-shrink: 0;
|
||||
margin: 12px 20px 18px;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user