index.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <template>
  2. <div v-if="isOpen" class="search-dialog-overlay" @click.self="closeDialog">
  3. <div class="search-dialog-container">
  4. <!-- 搜索框 -->
  5. <div class="search-header">
  6. <el-input
  7. v-model="searchQuery"
  8. placeholder="输入全部搜索........."
  9. clearable
  10. @keydown.escape="closeDialog"
  11. autofocus
  12. >
  13. <template #prefix>
  14. <el-icon><Search /></el-icon>
  15. </template>
  16. </el-input>
  17. </div>
  18. <!-- 项目 -->
  19. <div class="search-section" v-if="!searchQuery">
  20. <div class="section-title">项目</div>
  21. <div class="search-items">
  22. <div
  23. class="search-item"
  24. v-for="item in projectItems"
  25. :key="item.id"
  26. @click="selectItem(item)"
  27. >
  28. <span class="item-icon">{{ item.icon }}</span>
  29. <span class="item-text">{{ item.text }}</span>
  30. </div>
  31. </div>
  32. </div>
  33. <!-- 工作流程 -->
  34. <div class="search-section" v-if="!searchQuery">
  35. <div class="section-title">工作流程</div>
  36. <div class="search-items">
  37. <div class="search-item action-item" @click="handleCreateWorkflow">
  38. <span class="item-icon">+</span>
  39. <span class="item-text">创建工作流程</span>
  40. </div>
  41. </div>
  42. </div>
  43. <!-- 搜索结果 -->
  44. <div class="search-section" v-if="searchQuery">
  45. <div class="section-title">搜索结果</div>
  46. <div class="search-items">
  47. <div
  48. class="search-item"
  49. v-for="item in filteredResults"
  50. :key="item.id"
  51. @click="selectItem(item)"
  52. >
  53. <span class="item-icon">{{ item.icon || '→' }}</span>
  54. <span class="item-text">{{ item.text }}</span>
  55. </div>
  56. <div v-if="filteredResults.length === 0" class="no-results">没有找到相关结果</div>
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. </template>
  62. <script setup lang="ts">
  63. import { ref, computed, onMounted, onUnmounted } from 'vue'
  64. import { useRouter } from 'vue-router'
  65. import { Search } from '@element-plus/icons-vue'
  66. interface SearchItem {
  67. id: string
  68. text: string
  69. icon?: string
  70. action?: () => void
  71. }
  72. const router = useRouter()
  73. const props = defineProps({
  74. isOpen: {
  75. type: Boolean,
  76. required: true
  77. }
  78. })
  79. const emit = defineEmits(['close', 'select'])
  80. const searchQuery = ref('')
  81. const recentItems: SearchItem[] = [
  82. {
  83. id: '1',
  84. text: '利用 Gemini AI, OCR 和 Google Sheets 编成,打开并解析发票的文档。'
  85. },
  86. {
  87. id: '2',
  88. text: '打开我的工作流程'
  89. },
  90. {
  91. id: '3',
  92. text: '并启与新闻的期间内'
  93. }
  94. ]
  95. const projectItems: SearchItem[] = [
  96. {
  97. id: '4',
  98. text: '创建项目',
  99. icon: '📁'
  100. },
  101. {
  102. id: '5',
  103. text: '开放项目',
  104. icon: '📁'
  105. }
  106. ]
  107. const allItems = [...recentItems, ...projectItems]
  108. const filteredResults = computed(() => {
  109. if (!searchQuery.value.trim()) {
  110. return []
  111. }
  112. const query = searchQuery.value.toLowerCase()
  113. return allItems.filter((item) => item.text.toLowerCase().includes(query))
  114. })
  115. const closeDialog = () => {
  116. searchQuery.value = ''
  117. emit('close')
  118. }
  119. const selectItem = (item: SearchItem) => {
  120. emit('select', item)
  121. closeDialog()
  122. }
  123. const handleCreateWorkflow = () => {
  124. router.push('/workflow/0')
  125. closeDialog()
  126. }
  127. const handleKeyDown = (e: KeyboardEvent) => {
  128. if (e.key === 'Escape' && props.isOpen) {
  129. closeDialog()
  130. }
  131. }
  132. onMounted(() => {
  133. window.addEventListener('keydown', handleKeyDown)
  134. })
  135. onUnmounted(() => {
  136. window.removeEventListener('keydown', handleKeyDown)
  137. })
  138. </script>
  139. <style lang="less" scoped>
  140. .search-dialog-overlay {
  141. position: fixed;
  142. top: 0;
  143. left: 0;
  144. right: 0;
  145. bottom: 0;
  146. background: rgba(0, 0, 0, 0.45);
  147. display: flex;
  148. justify-content: center;
  149. align-items: flex-start;
  150. padding-top: 60px;
  151. z-index: 1000;
  152. animation: fadeIn 0.2s ease-out;
  153. @keyframes fadeIn {
  154. from {
  155. background: rgba(0, 0, 0, 0);
  156. }
  157. to {
  158. background: rgba(0, 0, 0, 0.45);
  159. }
  160. }
  161. }
  162. .search-dialog-container {
  163. width: 600px;
  164. max-height: 70vh;
  165. background: #fff;
  166. border-radius: 12px;
  167. box-shadow:
  168. 0 3px 12px rgba(0, 0, 0, 0.15),
  169. 0 6px 24px rgba(0, 0, 0, 0.1);
  170. overflow: hidden;
  171. display: flex;
  172. flex-direction: column;
  173. animation: slideDown 0.3s ease-out;
  174. @keyframes slideDown {
  175. from {
  176. opacity: 0;
  177. transform: translateY(-20px);
  178. }
  179. to {
  180. opacity: 1;
  181. transform: translateY(0);
  182. }
  183. }
  184. }
  185. .search-header {
  186. padding: 16px 20px;
  187. border-bottom: 1px solid #f0f0f0;
  188. display: flex;
  189. gap: 12px;
  190. align-items: center;
  191. background: #fafafa;
  192. :deep(.el-input) {
  193. input {
  194. border-radius: 8px;
  195. font-size: 14px;
  196. &::placeholder {
  197. color: #d0d0d0;
  198. }
  199. }
  200. }
  201. }
  202. .search-content {
  203. flex: 1;
  204. overflow-y: auto;
  205. padding: 12px 0;
  206. &::-webkit-scrollbar {
  207. width: 6px;
  208. }
  209. &::-webkit-scrollbar-track {
  210. background: transparent;
  211. }
  212. &::-webkit-scrollbar-thumb {
  213. background: #e0e0e0;
  214. border-radius: 3px;
  215. &:hover {
  216. background: #ccc;
  217. }
  218. }
  219. }
  220. .search-section {
  221. padding: 8px 0;
  222. }
  223. .section-title {
  224. font-size: 12px;
  225. color: #999;
  226. padding: 8px 20px;
  227. font-weight: 500;
  228. text-transform: uppercase;
  229. letter-spacing: 0.5px;
  230. }
  231. .search-items {
  232. display: flex;
  233. flex-direction: column;
  234. gap: 4px;
  235. padding: 0 8px;
  236. }
  237. .search-item {
  238. padding: 10px 16px;
  239. margin: 0 8px;
  240. background: transparent;
  241. border-radius: 8px;
  242. cursor: pointer;
  243. display: flex;
  244. align-items: center;
  245. gap: 12px;
  246. transition: all 0.2s ease;
  247. color: #333;
  248. font-size: 13px;
  249. &:hover {
  250. background: #f5f5f5;
  251. color: #ff6b6b;
  252. .item-icon {
  253. color: #ff6b6b;
  254. }
  255. }
  256. &.action-item {
  257. color: #999;
  258. &:hover {
  259. color: #ff6b6b;
  260. background: #fff5f5;
  261. }
  262. }
  263. }
  264. .item-icon {
  265. font-size: 14px;
  266. width: 20px;
  267. text-align: center;
  268. color: #ccc;
  269. transition: color 0.2s ease;
  270. flex-shrink: 0;
  271. }
  272. .item-text {
  273. flex: 1;
  274. white-space: nowrap;
  275. overflow: hidden;
  276. text-overflow: ellipsis;
  277. }
  278. .no-results {
  279. padding: 40px 20px;
  280. text-align: center;
  281. color: #999;
  282. font-size: 14px;
  283. }
  284. </style>