2025-03-03 07:01:49 +08:00

428 lines
14 KiB
Vue
Executable File

<template>
<Modal
v-model="showModal"
class-name="common-search-box-modal"
:closable="!isFullscreen"
:fullscreen="isFullscreen"
:mask-closable="false"
:footer-hide="true"
width="640">
<div class="search-header">
<div class="search-input">
<div class="search-pre">
<Loading v-if="loadIng > 0"/>
<Icon v-else type="ios-search" />
</div>
<Input ref="searchKey" v-model="searchKey" :placeholder="$L('请输入关键字')"/>
</div>
<i class="taskfont search-close" @click="onHide">&#xe6e5;</i>
</div>
<div class="search-body">
<template v-if="listLength === 0">
<div v-if="loadIng > 0 || !searchKey.trim()" class="search-empty">
<i class="taskfont">&#xe60b;</i>
<span>{{ $L(loadIng > 0 ? '正在拼命搜索...' : '请输入关键字搜索') }}</span>
</div>
<div v-else class="search-empty">
<i class="taskfont">&#xe60b;</i>
<span class="empty-label">{{ $L('暂无相关结果') }}</span>
<span>{{ $L('未搜到跟「(*)」相关的结果', searchKey) }}</span>
</div>
</template>
<div v-else class="search-list">
<ul v-for="(items, type) in list" :key="type">
<li class="item-label">{{typeLabel(type)}}</li>
<li v-for="item in items" :key="item.id" @click="onClick(item)">
<div class="item-icon">
<div v-if="item.icons[0]==='file'" :class="`no-dark-content file-icon ${item.icons[1]}`"></div>
<i v-else-if="item.icons[0]==='department'" class="taskfont icon-avatar department">&#xe75c;</i>
<i v-else-if="item.icons[0]==='project'" class="taskfont icon-avatar project">&#xe6f9;</i>
<i v-else-if="item.icons[0]==='task'" class="taskfont icon-avatar task">&#xe6f4;</i>
<UserAvatar v-else-if="item.icons[0]==='user'" class="user-avatar" :userid="item.icons[1]" :size="38"/>
<EAvatar v-else-if="item.icons[0]==='avatar'" class="img-avatar" :src="item.icons[1]" :size="38"/>
<Icon v-else-if="item.icons[0]==='people'" class="icon-avatar" type="ios-people" />
<Icon v-else class="icon-avatar" type="md-person" />
</div>
<div class="item-content">
<div class="item-title">
<div class="title-text">{{item.title}}</div>
<div
v-if="item.activity"
class="title-activity"
:title="item.activity">{{activityFormat(item.activity)}}</div>
</div>
<div class="item-desc">
<span
class="desc-tag"
v-if="item.tags"
v-for="tag in item.tags"
:style="tag.style">{{tag.name}}</span>
<span class="desc-text" v-html="item.desc"></span>
</div>
</div>
</li>
</ul>
</div>
</div>
</Modal>
</template>
<script>
import {mapState} from "vuex";
import emitter from "../store/events";
export default {
name: 'SearchBox',
props: {
//
},
data() {
return {
loadIng: 0,
searchKey: '',
searchResults: [],
searchTimer: null,
showModal: false,
}
},
mounted() {
emitter.on('openSearch', this.onShow);
},
beforeDestroy() {
emitter.off('openSearch', this.onShow);
},
watch: {
searchKey: {
handler: function () {
if (!this.searchKey.trim()) {
return;
}
if (this.searchTimer) {
clearTimeout(this.searchTimer)
this.searchTimer = null;
this.loadIng--;
}
this.loadIng++;
this.searchTimer = setTimeout(() => {
if (this.searchKey.trim()) {
this.onSearch()
}
this.searchTimer = null;
this.loadIng--;
}, 500)
},
immediate: true
}
},
computed: {
...mapState([
'themeName',
]),
list({searchKey, searchResults}) {
const items = searchResults.filter(item => item.key === searchKey)
const groups = {}
items.forEach(item => {
if (!groups[item.type]) {
groups[item.type] = []
}
if (groups[item.type].length < 10) {
groups[item.type].push(item)
}
})
return groups
},
listLength({searchKey, searchResults}) {
const items = searchResults.filter(item => item.key === searchKey)
return items.length
},
isFullscreen({windowWidth}) {
return windowWidth < 576
},
},
methods: {
typeLabel(type) {
switch (type) {
case 'task':
return this.$L('任务')
case 'message':
return this.$L('消息')
case 'contact':
return this.$L('联系人')
case 'file':
return this.$L('文件')
case 'project':
return this.$L('项目')
}
return type;
},
activityFormat(date) {
const local = $A.daytz(),
time = $A.dayjs(date);
if (local.format("YYYY/MM/DD") === time.format("YYYY/MM/DD")) {
return time.format("HH:mm")
}
if (local.year() === time.year()) {
return time.format("MM/DD")
}
return time.format("YYYY/MM/DD") || '';
},
onClick(item) {
console.log(item);
},
onShow() {
this.showModal = true
this.$nextTick(() => {
this.$refs.searchKey?.focus()
})
},
onHide() {
this.showModal = false
},
onSearch() {
this.searchTask(this.searchKey)
this.searchProject(this.searchKey)
this.searchMessage(this.searchKey)
this.searchContact(this.searchKey)
this.searchFile(this.searchKey)
},
pushResults(items) {
items.forEach(item => {
const index = this.searchResults.findIndex(({id, type}) => id === item.id && type === item.type)
if (index > -1) {
this.searchResults.splice(index, 1, item)
} else {
this.searchResults.push(item)
}
})
},
searchTask(key) {
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/task/lists',
data: {
keys: {name: key},
archived: 'all',
pagesize: 10,
},
}).then(({data}) => {
const nowTime = $A.dayjs().unix()
const items = data.data.map(item => {
const tags = [];
if (item.complete_at) {
tags.push({
name: this.$L('已完成'),
style: 'background-color:#ccc',
})
} else if (item.overdue) {
tags.push({
name: this.$L('超期'),
style: 'background-color:#f00',
})
} else if ($A.dayjs(item.end_at).unix() - nowTime < 86400) {
tags.push({
name: this.$L('即将到期'),
style: 'background-color:#f80',
})
}
return {
key,
type: 'task',
icons: ['task', null],
tags,
id: item.id,
title: item.name,
desc: item.desc,
activity: item.end_at,
rawData: item,
};
});
this.pushResults(items)
}).finally(_ => {
this.loadIng--;
})
},
searchProject(key) {
this.loadIng++;
this.$store.dispatch("call", {
url: 'project/lists',
data: {
keys: {
name: key
},
archived: 'all',
pagesize: 10,
},
}).then(({data}) => {
const items = data.data.map(item => {
const tags = [];
if (item.owner) {
tags.push({
name: this.$L('负责人'),
style: 'background-color:#0bc037',
})
}
if (item.archived_at) {
tags.push({
name: this.$L('已归档'),
style: 'background-color:#ccc',
})
}
return {
key,
type: 'project',
icons: ['project', null],
tags,
id: item.id,
title: item.name,
desc: item.desc || '',
activity: item.updated_at,
rawData: item,
};
})
this.pushResults(items)
}).finally(_ => {
this.loadIng--;
})
},
searchMessage(key) {
this.loadIng++;
this.$store.dispatch("call", {
url: 'dialog/msg/esearch',
data: {
key,
pagesize: 10,
},
}).then(({data}) => {
const items = data.data.map(item => {
let icon = 'person';
let desc = null;
if (item.type == 'group') {
if (item.avatar) {
icon = 'avatar';
desc = item.avatar;
} else if (item.group_type == 'department') {
icon = 'department';
} else if (item.group_type == 'project') {
icon = 'project';
} else if (['task', 'okr'].includes(item.group_type)) {
icon = 'task';
} else {
icon = 'people';
}
} else if (item.dialog_user) {
icon = 'user';
desc = item.dialog_user.userid
}
return {
key,
type: 'message',
icons: [icon, desc],
tags: [],
id: item.id,
title: item.name,
desc: $A.getMsgSimpleDesc(item.last_msg),
activity: item.last_at,
rawData: item,
};
})
this.pushResults(items)
}).finally(_ => {
this.loadIng--;
})
},
searchContact(key) {
this.loadIng++;
this.$store.dispatch("call", {
url: 'users/search',
data: {
keys: {key},
pagesize: 10,
},
}).then(({data}) => {
const items = data.map(item => {
return {
key,
type: 'contact',
icons: ['user', item.userid],
tags: [],
id: item.userid,
title: item.nickname,
desc: item.profession || '',
activity: item.line_at,
rawData: item,
};
})
this.pushResults(items)
}).finally(_ => {
this.loadIng--;
})
},
searchFile(key) {
this.loadIng++;
this.$store.dispatch("call", {
url: 'file/search',
data: {key},
}).then(({data}) => {
const items = data.map(item => {
const tags = [];
if (item.share) {
tags.push({
name: this.$L(item.userid == this.userId ? '已共享' : '共享'),
style: 'background-color:#0bc037',
})
}
return {
key,
type: 'file',
icons: ['file', item.type],
tags,
id: item.id,
title: item.name,
desc: item.type === 'folder' ? '' : $A.bytesToSize(item.size),
activity: item.updated_at,
rawData: item,
};
})
this.pushResults(items)
}).finally(_ => {
this.loadIng--;
})
}
}
};
</script>