<template>
<div
:class="['talk-item', isLeft? 'left' : 'right']"
:key="data.timestamp"
v-if="isTemp == 1? true : text"
>
<!-- 遍历布局节点 -->
<template v-for="it in layout[isLeft? 'left' : 'right']">
<!-- 头像部分 -->
<div
v-if="it === LayoutNode.AVATAR"
:key="`${data.startFrame}-${it}`"
class="talk-avatar"
>
<el-avatar
class="avatar"
size="large"
:style="{ 'background-image': backgroundImg }"
>
<!-- avatar 用于展示头像 -->
</el-avatar>
</div>
<!-- 文本段落部分 -->
<div
v-if="it === LayoutNode.PARAGRAPH"
:key="`${data.startFrame}-${it}`"
class="highlighted"
:class="{ 'talk-paragraph': true, 'highlight': highlight, 'blink': isBlink }"
>
<!-- 时间和其他操作按钮 -->
<div class="time">
<!-- 左侧时间显示 -->
<span v-if="isLeft" style="font-size: 12px;">
{{ timestamp | formatDate('hh:mm:ss') }}
</span>
<!-- 复制按钮 -->
<i
v-if="canCopy && data.text.length > 1"
class="el-icon-s-claim"
@click="setAnswer"
></i>
<!--编辑按钮-->
<el-tooltip
v-if="canEdit"
effect="dark"
content="编辑"
placement="top-start"
class="item"
>
<i
class="el-icon-edit"
@click="changeEditStatus"
v-if="(data.text.length > 0 && status == RealTimeStatus.History) ||
(data.text.length > 0 && status == RealTimeStatus.RealTime)"
></i>
</el-tooltip>
<!-- 右侧时间显示 -->
<span v-if="!isLeft" style="font-size: 11px;">
{{ timestamp | formatDate('hh:mm:ss') }}
</span>
</div>
<!-- 文本内容容器 -->
<div :class="['text-container']" ref="textContainer" @dblclick="fetchTalkItem">
<!-- 统一前置 -->
<i class="isIcon iconfont icon-yinbo"></i>
<!-- 编辑模式下的文本框 -->
<el-input
v-if="isEdit"
:class="['text-box',{highlighted: isPlaying }]"
type="textarea"
style="font-size: 16px;"
:rows="3"
v-model="data.text"
@input="handleInput"
></el-input>
<!-- 非编辑模式下的文本显示 -->
<div
v-else
v-html="`${text} `"
:class="['text-box']"
style="font-size: 16px;"></div>
<!-- 段落进度条 -->
<div class="progress-highlight" :style="{ width: progressWidth }"></div>
</div>
<!-- 命中标签 -->
<div class="isHit" v-if="matchedHitRuleNames.length > 0">
<i style="color:#007bff;" class="el-icon-circle-check"></i>
<span v-for="(name, index) in matchedHitRuleNames" :key="index">
{{ name }}
</span>
</div>
</div>
</template>
</div>
</template>
<script lang="ts">
import { Message, MessageBox } from "element-ui";
import { Component, Prop, Vue, Watch } from "nuxt-property-decorator";
import { Keyword, TalkItem, TalkState } from "../../../types";
import * as dayjs from "dayjs";
import '../../../assets/iconFont/iconfont.css'
// import img_police from "~/assets/img/anonymity-police.jpg"
// 设置谈话人 被谈话人的位置
import img_police from "../../../assets/img/anonymity-telephonist.jpg";
import img_usr from "../../../assets/img/anonymity-square.png";
import { State } from "vuex-class";
const enum LayoutNode {
AVATAR,
PARAGRAPH,
}
/**
* ZKer 设置谈话人是否在左边
*/
const IsLeft = true;
@Component({
name: "talkItem",
components: {},
filters: {
formatDate(value) {
return dayjs(new Date(value)).format("YYYY-MM-DD HH:mm:ss");
},
},
})
export default class extends Vue {
@Prop({ type: Object, required: true, default: {} })
data!: TalkItem;
@Prop({ type: Boolean, required: true })
highlight: boolean;
@Prop({ type: Boolean, required: true })
isPolice: boolean;
@Prop({ type: Boolean, required: true })
canCopy: boolean;
@Prop({ required: true })
canEdit; // 判断编辑是否可修改(历史谈话可/实时谈话)
@Prop({ type: [], required: true })
RealTimeStatus; // 历史 实时谈话状态
@Prop({ required: true })
status;
@Prop() busGroups!: any;
isEdit: boolean = false;
isLeft: boolean = false;
isBlink: boolean = false;
isTemp: any = null; // 判断用户删除(监听input的输入事件时 证明在删除 否则反之)
timestamp: string | number = " ";
text: string = " ";
backgroundImg: string = `url(${this.isPolice? img_police : img_usr}`;
LayoutNode: any = LayoutNode;
layout: any = {
// 统计左(对话数据)右()
left: [LayoutNode.AVATAR, LayoutNode.PARAGRAPH],
right: [LayoutNode.PARAGRAPH, LayoutNode.AVATAR],
};
@State talk!: TalkState;
matchedHitRuleNames: string[] = [];
// -=-=
@Prop({ type: Boolean, default: false }) isPlaying!: boolean;
progressWidth: string = '0%';
duration: number = null; // 播放完一段
animationFrameId: number | null = null;
@Watch('isPlaying')
onIsPlayingChange(isPlaying: boolean) {
if (isPlaying) {
this.startProgressAnimation();
} else {
this.stopProgressAnimation();
}
}
startProgressAnimation() {
const startTime = performance.now();
const animate = (now: number) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / this.duration, 1);
this.progressWidth = `${progress * 100}%`;
if (progress < 1) {
this.animationFrameId = requestAnimationFrame(animate);
}
};
this.animationFrameId = requestAnimationFrame(animate);
}
stopProgressAnimation() {
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
this.progressWidth = '0%';
}
beforeDestroy() {
this.stopProgressAnimation();
}
// -=-=
@Watch("data", { deep: true, immediate: true })
onDataChange(newData: TalkItem) {
// if (!newData.policy) {
// this.console.debug(newData,newData.last, newData.text, JSON.stringify(newData.keywords), newData.startFrame)
// }
this.text = newData.text;
this.setKeyword(true);
}
@Watch("canEdit")
onCanEditChange(newValue: boolean) {
if (newValue) {
this.$store.dispatch("talk/FetchKeywords");
}
}
// handleClick(){//单击文本字段
// if (this.status == this.RealTimeStatus.History) {
// this.$emit('jump', this.data)
// console.log('this.newData',this.data);
//
// }
// console.log(121212);
//
// }
created() {
// 对讲内容
// console.log(this.data, "opop");
// 命中次数
// console.log(this.busGroups.hitRules, "1212121");
}
/**
* 统一中英文标点符号为英文格式
*/
normalizePunctuation(text: string): string {
const punctuationMap: { [key: string]: string } = {
',': ',', '。': '.', '?': '?', '!': '!', ';': ';',
':': ':', '“': '"', '”': '"', '‘': "'", '’': "'",
'(': '(', ')': ')', '【': '[', '】': ']', '《': '<', '》': '>',
'、': '\\', '——': '-', '…': '...', '—': '-', '·': '.'
};
return text.replace(/[^\u0000-\u00ff]/g, ch => punctuationMap[ch] || ch);
}
scrollToParagraph() {//添加 scrollToParagraph 方法,用于滚动到指定段落
const element = this.$el;
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
mounted() {
this.text = this.data.text;
this.isLeft = Boolean(Number(this.isPolice) ^ Number(IsLeft));
this.timestamp = this.data.timestamp;
this.setKeyword();
if (this.data.startFrame && this.data.endFrame) {
this.duration = this.data.endFrame - this.data.startFrame; // 动态计算段落的时间长度
}
// **新增延迟逻辑:通过 $watch 监听 busGroups 变化**
this.$watch('busGroups', (newGroups) => {
if (newGroups && newGroups.hitRules) {
this.matchHitRules(newGroups.hitRules); // 自定义标签匹配方法
}
}, { deep: true, immediate: true });
}
// 新增标签匹配方法
matchHitRules(hitRules: any[]) {
const matchedRuleNamesSet = new Set<string>();
const normalizedDataText = this.normalizePunctuation(this.text).toLowerCase();
const currentSpeakerPrefix = this.data.policy ? '管教民警:' : '在押人员:';
hitRules.forEach(rule => {
const sentences = this.splitSentences(rule.hitSentence);
sentences.forEach(sentence => {
const normalizedSentence = this.normalizePunctuation(sentence).toLowerCase();
if (normalizedSentence.startsWith(currentSpeakerPrefix.toLowerCase()) &&
normalizedSentence.includes(normalizedDataText)) {
matchedRuleNamesSet.add(rule.hitRuleName);
}
});
});
this.matchedHitRuleNames = Array.from(matchedRuleNamesSet);
}
// 按管教民警或被监管人分割句子
splitSentences(text: string): string[] {
const pattern = /(管教民警|被监管人):/g;
const matches = text.match(pattern);
const sentences: string[] = [];
let startIndex = 0;
if (matches) {
matches.forEach((match, index) => {
const endIndex = index === matches.length - 1
? text.length
: text.indexOf(matches[index + 1]);
const sentence = text.substring(startIndex, endIndex).trim();
if (sentence) {
sentences.push(sentence);
}
startIndex = endIndex;
});
}
return sentences;
}
handleInput(value) {
this.isTemp = 1; // 如果输入框的输入发生改变证明在删除 赋值为1 {上面的判断证明如果输入框变化 为true 否则循环的text(文本)有值显示 没值不显示}
// 监听input输入的文本长度
if (value.length == 1) {
this.$message.error("输入字符长度至少1位");
// 阻止表单提交
} else {
this.data.text = value;
}
}
changeEditStatus() {
const initialText = this.data.text;
this.$prompt("", "文本修正", {
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPlaceholder: "",
inputValue: initialText, // 设置$prompt的input初始值为编辑的文本
})
.then(({ value }) => {
if (value.length < 1) {
Message({ message: "最少保留1个字符内容!", type: "error" });
return;
} else {
// 正常输入
this.data.text = value;
this.submitChange();
}
})
.catch(() => {
this.$message({
type: "info",
message: "取消操作",
});
});
}
/**
* 修改文本提交
*/
async submitChange() {
this.$emit("editContent", Number(this.data.startFrame), this.data.text, this.data.policy);
this.setKeyword(false, this.talk.keywords.map((it: Keyword) => {
return it.text;
}));
}
/**
* 关键字用label标签
*/
setKeyword(forceUpdate: boolean = false, keywords: string[] = this.data.keywords) {
// const keywords = this.canEdit? this.talk.keywords.map((it: Keyword) => {
// return it.text
// }) : this.data.keywords
if (keywords == null || keywords.length == 0) {
return;
}
if (this.data.policy /*|| !this.data.last*/) {
return;
}
keywords.forEach((it) => {
if (it.length == 0) {
return;
}
this.text = this.text.replace(new RegExp(it, "gm"), `<label class='keyword'>${it}</label>`);
});
if (forceUpdate) {
this.$nextTick(() => {
this.$forceUpdate();
});
}
}
/**
* 搜索字用span标签
*/
setSearchWord(word: string) {
if (word == null || word.length == 0) {
return;
}
this.text = this.text.replace(new RegExp(word, "gm"), `<span class='searched'>${word}</span>`);
}
setGaugesWord(word: string) {
if (word == null || word.length == 0) {
return;
}
this.text = this.text.replace(new RegExp(word, "gm"), `<span class='gauges'>${word}</span>`);
}
clearSearchWord() {
this.text = this.text.replace(new RegExp("<span class='searched'>", "gm"), "");
this.text = this.text.replace(new RegExp("</span>", "gm"), "");
}
copy() {
this.$copyText(this.text)
.then(() => {
this.$notify({
title: "成功",
message: "谈话内容已成功复制到粘贴板!",
duration: 3000,
type: "success",
});
})
.catch(() => {
this.$notify({
title: "失败",
message: "谈话内容复制到粘贴板失败!",
duration: 3000,
type: "error",
});
});
}
fetchTalkItem() {//单个段落
console.log("this.data",this.data);
// 确保有有效的startFrame和endFrame
if (this.data.startFrame && this.data.endFrame &&
this.data.endFrame - this.data.startFrame > 10) { // 最小10ms播放
this.$emit("fetchTalkItem", this.data);
} else {
console.warn("段落时间太短,不播放:", this.data);
}
}
setAnswer() {
this.$emit("setAnswer", this.data.text);
}
blink() {
this.isBlink = true;
this.console.debug(this);
setTimeout(() => {
this.isBlink = false;
}, 1500);
}
}
</script>
<style lang="less" scoped>
@import "../../../assets/styles/variables";
// 对讲内容是否命中
.isHit {
font-size: 13px;
color: #8b9199;
margin: 8px 0 0 0;
align-self: flex-start;
}
// 谈话框样式 公共配置
.talk-item {
color: #47494e;
padding: 0 0.5rem;
.talk-avatar {
display: inline-block;
.el-avatar {
position: relative;
}
.avatar {
border-radius: 50%;
background-size: 40px;
}
}
.talk-paragraph {
display: inline-block;
padding: 0 1rem 10px 1rem!important;
max-width: 70%!important;
margin-bottom:8px!important;
box-sizing: border-box;
.time {
padding-left: 1rem;
padding-bottom: 0.3rem;
font-size: 1rem;
color: #ccc;
/*min-height: 14px;*/
i {
cursor: pointer;
color: #7ea1de;
}
i:hover {
color: slateblue;
}
}
.progress-highlight{
position: absolute;
top: 0;
left: 0;
border-radius: 8px;
height: 93%;
background-color: rgba(61, 111, 205, 0.5); /* 半透明蓝色 */
z-index: 0;
transition: width 0.1s linear;
}
.text-container {
display: flow-root;
border-radius: 8px;
min-width:120px;
height: fit-content !important;
overflow-x: hidden;
.text-box {
position: relative;
display: inline-block;
border-radius: 8px;
padding: 10px;
user-select: text;
word-wrap: break-word; // 确保长单词换行
word-break: break-word; // 处理中文换行
overflow-wrap: break-word; // 处理长URL等
min-width:85%;
max-width: 28ch; /* 限制最大宽度为大约28个字符的宽度包括符号(宽度自适应) */
overflow-x: hidden; /* 隐藏水平溢出 */
white-space: pre-wrap; /* 保留空白符序列,但正常地进行换行 */
}
}
}
// -=-=-=-=-=-start播放命中高亮
.text-box.highlighted {
background-color: #3d6fcd !important;
color: white !important;
transition: background-color 0.2s ease;
}
.talk-item.left .text-container:hover .isIcon.iconfont.icon-yinbo,
.talk-item.right .text-container:hover .isIcon.iconfont.icon-yinbo,
.text-container .isIcon.iconfont.icon-yinbo[style*="display: inline-block"] {
display: inline-block !important;
}
// -=-=--=-=-end
.talk-paragraph::before {
display: inline-block;
content: "";
width: 0;
height: 0;
line-height: normal;
// border-width: 10px;
// border-style: solid;
// border-color: transparent;
position: relative;
top: 49px;
}
.highlight {
.text-box {
background-color: @talkItemHighlightBGColor !important;
// background-color: #3d6fcd !important; // 原为 @talkItemHighlightBGColor,改为悬停色
// color: white !important; // 新增文字颜色
}
}
.blink {
.text-box {
background-color: @talkItemHighlightBGColor !important;
animation: blink 0.5s 3;
-webkit-animation-name: blink;
-webkit-animation-duration: 500ms;
-webkit-animation-iteration-count: 3;
-webkit-animation-timing-function: ease-in-out;
}
}
@keyframes blink {
0% {
color: #fab4b4;
}
25% {
color: #fa6161;
}
50% {
color: #ff0000;
}
75% {
color: #fa6161;
}
100% {
color: #fab4b4;
}
}
&:last-child {
margin-bottom: 30px;
}
}
//////////////////////////////////////////////////////////////////////////
// 左侧谈话框样式
.talk-item.left {
display: flex;
box-sizing: border-box;
// &:hover {
// width: 100% !important;
// cursor: pointer;
// border-radius: 10px;
// background-color: #d7ecff !important;
// border-left: 3px solid #409EFF !important;
// transition: background-color 0.3s ease;
// }
.talk-avatar {
.el-avatar {
top: 38px;
}
}
.talk-paragraph {
.time {
i {
margin-left: 1rem;
}
}
.text-container {
position: relative;
.isIcon.iconfont.icon-yinbo{
display: none; /* 默认隐藏 */
position: absolute;
right: -28px;
top: 8px;
z-index: 1;
font-size: 20px !important;
border-radius: 50% !important;
background: white !important;
border: 1px solid rgb(219, 215, 215) !important;
font-size: 20px !important;
color: #3d6fcd !important;
}
&:hover .isIcon.iconfont.icon-yinbo {
display: inline-block !important; /* 悬停时显示图标 */
}
.text-box {
background-color: @talkItemLeftBGColor;
&:hover {
cursor: pointer;
color: white;
background-color:#3d6fcd;
}
&:hover .isIcon.iconfont.icon-yinbo {
display: inline-block; /* 悬停时显示图标 */
}
}
}
}
.talk-paragraph::before {
border-right-width: 10px;
border-right-color: @talkItemLeftBGColor;
left: -19px;
}
.highlight::before,
.blink::before {
border-right-color: @talkItemHighlightBGColor !important;
}
}
//////////////////////////////////////////////////////////////////////////
// 右侧谈话框样式
.talk-item.right {
text-align: right;
box-sizing: border-box;
// &:hover {
// width: 100% !important;
// cursor: pointer;
// border-radius: 10px;
// background-color: #d7ecff !important;
// border-left: 3px solid #409EFF !important;
// transition: background-color 0.3s ease;
// }
.talk-avatar {
float: right;
.el-avatar {
top: 36px;
}
}
.talk-paragraph {
.time {
i {
margin-right: 1rem;
}
}
.text-container {//右侧谈话
position: relative;
.isIcon.iconfont.icon-yinbo {
display: none; /* 默认隐藏 */
position: absolute;
left: -28px;
top: 8px;
z-index: 1;
font-size: 20px !important;
border-radius: 50% !important;
background: white !important;
border: 1px solid rgb(219, 215, 215) !important;
font-size: 20px !important;
color: #3d6fcd !important;
}
&:hover .isIcon.iconfont.icon-yinbo {
display: inline-block !important; /* 悬停时显示图标 */
}
.text-box {
text-align: left;
background-color: @talkItemRightBGColor;
&:hover {
cursor: pointer;
color: white;
background-color:#3d6fcd;
}
}
}
}
.talk-paragraph::before {
border-left-width: 10px;
border-left-color: @talkItemRightBGColor;
right: -19px;
}
.highlight::before, .blink::before {
border-left-color: @talkItemHighlightBGColor !important;
}
}
</style>
<style lang="less">
.text-container {
.text-box {
.searched {
color: #f54646;
background-color: #f3f35d;
}
.keyword {
color: red;
font-weight: bold;
}
.gauges {
color: #2fefd8;
}
}
.text-box.el-input {
padding: 5px !important;
.el-input__inner {
color: #560692;
background: inherit;
padding: 0;
border-width: 0;
}
}
.text-box.el-textarea {
width: 350px;
padding: 5px !important;
.el-textarea__inner {
color: #560692;
background: inherit;
padding: 0;
border-width: 0;
}
}
}
.talk-item.right {
.text-box.el-input, .text-box.el-textarea {
float: right;
right: 0px;
}
}
</style>
import jsonData from '../public/123.json' 基于引用数据来操作<template>
<div class="main">
<div class="sec">
<div v-for="item in list" :key="item.timestamp">
{{ item.text }}
</div>
</div>
</div>
</template>
<script lang="ts">
import { reactive, toRefs, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import jsonData from '../public/123.json'
// 定义 content 中每项的类型
interface ContentItem {
count: number
policy: boolean
speaker: string
timestamp: number
text: string
startFrame: string
endFrame: string
emotions: null
keywords: null
last: boolean
emotionSeg: Record<string, unknown>
}
export default {
name: '',
setup() {
const router = useRouter()
const route = useRoute()
const data = reactive<{
list: ContentItem[]
}>({
list: [] // 初始化为空数组但有明确类型
})
onMounted(() => {
data.list = jsonData.data.content as ContentItem[]
console.log(data.list)
console.log(jsonData.data.content, 'jsonData')
})
const refData = toRefs(data)
return {
...refData
}
}
}
</script>
<style lang="scss" scoped>
.main{
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.sec{
width: 800px;
height: 600px;
border: 1px solid red;
}
}
</style> 修复一下吧亲 基于我提供代码
最新发布