index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. <template>
  2. <div class="collectpage">
  3. <!-- 搜索页面 -->
  4. <div v-if="showSearchPage" class="search-page">
  5. <SearchPageComponent
  6. :initialSearch="searchParams"
  7. :categoryOptions="categoryOptions"
  8. :levelOptions="levelOptions"
  9. :materialOptions="materialOptions"
  10. :eraOptions="eraOptions"
  11. :orderByOptions="orderByOptions"
  12. @search="handleSearch"
  13. @close="closeSearchPage"
  14. />
  15. </div>
  16. <!-- 主要内容 -->
  17. <div v-if="!showSearchPage" class="main-content">
  18. <!-- 搜索输入框 -->
  19. <div class="search-container">
  20. <div class="search-wrapper">
  21. <!-- 搜索输入框和检索按钮 -->
  22. <div class="search-input-wrapper">
  23. <el-input v-model="searchParams.searchText" placeholder="请输入关键词..." class="search-input cut-corner-input" @keyup.enter="quickSearch">
  24. <template #suffix>
  25. <el-button @click="quickSearch" class="quick-search-btn">
  26. <img src="@/assets/img/icon_search.png" alt="检索" class="search-icon" />
  27. </el-button>
  28. </template>
  29. </el-input>
  30. <button @click="openSearchPage" class="search-btn">
  31. <img src="@/assets/img/icon_select.png" alt="检索" class="search-icon" />
  32. <span>检索</span>
  33. </button>
  34. </div>
  35. <!-- 复选框区域 -->
  36. <div class="checkbox-group">
  37. <el-checkbox @change="quickSearch" v-model="filters.hasImage" label="二维文物" />
  38. <el-checkbox @change="quickSearch" v-model="filters.hasModel" label="三维文物" />
  39. </div>
  40. </div>
  41. </div>
  42. <!-- 搜索结果列表 -->
  43. <div v-loading="loading" class="search-results" @scroll="handleScroll">
  44. <div
  45. v-for="item in searchResults"
  46. :key="item.id"
  47. class="result-item"
  48. :class="{ 'loading-waterfall': item.isLoading }"
  49. @click="viewDetails(item)"
  50. >
  51. <div class="waterfall-mask" v-if="item.isLoading"></div>
  52. <img :src="item.image" :alt="item.name" class="result-image" />
  53. <div class="result-info">
  54. <div class="result-name">{{ item.name }}</div>
  55. <div class="more-info"><img src="@/assets/img/icon_collect_more.png" alt="更多" class="more-icon" /></div>
  56. </div>
  57. </div>
  58. <!-- 加载更多 -->
  59. <div class="loading-more" v-if="loading">
  60. <span>加载中...</span>
  61. </div>
  62. </div>
  63. <!-- 暂无数据提示 -->
  64. <div v-if="searched && searchResults.length === 0" class="no-data">
  65. <p>暂无符合条件的藏品</p>
  66. </div>
  67. </div>
  68. </div>
  69. </template>
  70. <script setup>
  71. import { ref, onMounted, computed } from 'vue'
  72. import { useRouter } from 'vue-router'
  73. import SearchPageComponent from './components/SearchPageComponent.vue'
  74. import getBookCountApi from '@/api'
  75. import { useStore } from 'vuex'
  76. const store = useStore()
  77. defineOptions({
  78. name: 'CollectPage'
  79. })
  80. const imgUrl = import.meta.env.VITE_COS_BASE_URL
  81. const router = useRouter()
  82. // 响应式数据
  83. const showSearchPage = ref(false)
  84. const searchParams = ref({searchText: '', dim: 3, orderBy: 'grade', level: '', category: '', material: '', era: ''})
  85. const searchResults = ref([]) // 当前显示的搜索结果(分页显示)
  86. const searched = ref(false)
  87. const loading = ref(false)
  88. // 前端分页相关数据
  89. const allSearchResults = ref([]) // 所有搜索结果数据(API返回的完整数据集)
  90. const currentPage = ref(1)
  91. const pageSize = 20 // 每页显示20条
  92. const filters = ref({
  93. hasImage: false,
  94. hasModel: false
  95. })
  96. // 所有选项列表 - 从接口获取
  97. const levelOptions = ref([])
  98. const categoryOptions = ref([])
  99. const materialOptions = ref([])
  100. const eraOptions = ref([])
  101. const orderByOptions = ref([])
  102. // 计算总页数
  103. const totalPages = computed(() => {
  104. return Math.ceil(allSearchResults.value.length / pageSize);
  105. })
  106. // 构建orderbyList的通用方法
  107. const buildOrderbyList = (orderByValue = searchParams.value.orderBy) => {
  108. let orderbyList = [];
  109. if (orderByValue) {
  110. if (orderByValue === 'grade') {
  111. levelOptions.value.forEach(item => {
  112. orderbyList.push(item.value);
  113. });
  114. } else if (orderByValue === 'category') {
  115. categoryOptions.value.forEach(item => {
  116. orderbyList.push(item.value);
  117. });
  118. } else if (orderByValue === 'texture') {
  119. materialOptions.value.forEach(item => {
  120. orderbyList.push(item.value);
  121. });
  122. } else if (orderByValue === 'era') {
  123. eraOptions.value.forEach(item => {
  124. orderbyList.push(item.value);
  125. });
  126. }
  127. }
  128. console.log('移动端主页buildOrderbyList:', orderByValue, orderbyList);
  129. return orderbyList;
  130. };
  131. // 打开搜索页
  132. const openSearchPage = () => {
  133. showSearchPage.value = true
  134. }
  135. // 关闭搜索页
  136. const closeSearchPage = () => {
  137. showSearchPage.value = false
  138. }
  139. // 快速搜索(回车键触发)
  140. const quickSearch = () => {
  141. searchParams.value.dim = getSelectedDim()
  142. const orderbyList = buildOrderbyList()
  143. console.log('searchParams', searchParams.value)
  144. handleSearch({...searchParams.value, orderbyList})
  145. }
  146. // 计算dim参数值
  147. const getSelectedDim = () => {
  148. const { hasImage, hasModel } = filters.value
  149. // 都选中或都不选中时传3-全部
  150. if ((hasImage && hasModel) || (!hasImage && !hasModel)) {
  151. return 3;
  152. }
  153. // 只选hasImage时传1-二维
  154. if (hasImage && !hasModel) {
  155. return 1;
  156. }
  157. // 只选hasModel时传2-三维
  158. if (!hasImage && hasModel) {
  159. return 2;
  160. }
  161. return 3; // 默认全部
  162. }
  163. // 加载显示的数据(分页)
  164. const loadDisplayedData = () => {
  165. const startIndex = (currentPage.value - 1) * pageSize;
  166. const endIndex = startIndex + pageSize;
  167. const newData = allSearchResults.value.slice(startIndex, endIndex);
  168. if (currentPage.value === 1) {
  169. // 第一页,直接替换
  170. searchResults.value = newData.map(item => ({ ...item, isLoading: true }));
  171. } else {
  172. // 后续页,追加数据
  173. const newItems = newData.map(item => ({ ...item, isLoading: true }));
  174. searchResults.value.push(...newItems);
  175. }
  176. // 延迟触发瀑布效果
  177. setTimeout(() => {
  178. newData.forEach((item, index) => {
  179. setTimeout(() => {
  180. const foundItem = searchResults.value.find(listItem => listItem.id === item.id && listItem.isLoading);
  181. if (foundItem) {
  182. foundItem.isLoading = false;
  183. }
  184. }, index * 150 + 600);
  185. });
  186. }, 100);
  187. };
  188. // 处理搜索
  189. const handleSearch = async (searchFilters = {}) => {
  190. showSearchPage.value = false
  191. loading.value = true
  192. // 合并搜索参数,保留现有值,只更新传入的非空值
  193. Object.keys(searchFilters).forEach(key => {
  194. // if (searchFilters[key] !== undefined && searchFilters[key] !== null && searchFilters[key] !== '') {
  195. searchParams.value[key] = searchFilters[key]
  196. // }
  197. })
  198. // 确保dim参数正确更新
  199. searchParams.value.dim = getSelectedDim()
  200. // 存储排序参数到store
  201. if (searchFilters.orderBy) {
  202. store.dispatch('setOrderBy', searchFilters.orderBy)
  203. }
  204. if (searchFilters.orderbyList) {
  205. store.dispatch('setOrderbyList', searchFilters.orderbyList)
  206. }
  207. try {
  208. const searchObj = {
  209. dim: searchParams.value.dim,
  210. searchText: searchParams.value.searchText,
  211. grade: searchParams.value.level,
  212. category: searchParams.value.category,
  213. texture: searchParams.value.material,
  214. agetype: searchParams.value.era,
  215. orderBy: searchParams.value.orderBy,
  216. orderbyList: searchFilters.orderbyList || []
  217. }
  218. // 重置分页数据
  219. currentPage.value = 1;
  220. searchResults.value = []; // 清空当前显示的数据
  221. // 调用实际的搜索API,使用与CollectionContent相同的API
  222. const response = await getBookCountApi.getArtifactListApi(searchObj)
  223. // 处理返回的数据
  224. const artifacts = response.list || response.records || response || [];
  225. console.log('获取到搜索结果:', artifacts.length, '条');
  226. // 数据格式化处理
  227. const processedArtifacts = artifacts.map(item => ({
  228. id: item.id,
  229. name: item.name || item.title,
  230. description: item.description || item.desc || '暂无描述',
  231. era: item.era || item.agetype || '近现代',
  232. category: item.category || '其他',
  233. level: item.level || item.grade || '未定级',
  234. material: item.texture || '其他',
  235. image: `${imgUrl}${item.thumbnail}`,
  236. hasImage: item.hasImage !== false,
  237. hasModel: item.hasModel !== false,
  238. isLoading: false
  239. }))
  240. // 存储所有结果数据
  241. allSearchResults.value = processedArtifacts;
  242. // 加载第一页数据
  243. loadDisplayedData();
  244. searched.value = true
  245. } catch (error) {
  246. console.error('搜索失败:', error)
  247. allSearchResults.value = []
  248. searchResults.value = []
  249. searched.value = true
  250. } finally {
  251. loading.value = false
  252. }
  253. }
  254. // 查看详情
  255. const viewDetails = (item) => {
  256. router.push({
  257. path: '/collectDetail',
  258. query: {
  259. id: item.id
  260. }
  261. })
  262. }
  263. // 滚动处理 - 加载更多
  264. const handleScroll = async (event) => {
  265. const container = event.target;
  266. if (!container || loading.value) return;
  267. // 检查是否滚动到底部
  268. const isScrolledToBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 50;
  269. if (isScrolledToBottom && currentPage.value < totalPages.value) {
  270. console.log('加载下一页数据...');
  271. currentPage.value++;
  272. loadDisplayedData();
  273. }
  274. };
  275. // 获取所有字典数据
  276. const loadDictionaryData = async () => {
  277. try {
  278. const dictionaryData = await getBookCountApi.getDictionaryListApi();
  279. console.log('获取到字典数据:', dictionaryData);
  280. // 处理等级数据
  281. if (dictionaryData.grade) {
  282. levelOptions.value = dictionaryData.grade.map(item => ({
  283. label: item,
  284. value: item
  285. }));
  286. }
  287. // 处理分类数据
  288. if (dictionaryData.category) {
  289. const processedCategories = dictionaryData.category.map(item => ({
  290. label: item,
  291. value: item
  292. }));
  293. categoryOptions.value = processedCategories;
  294. }
  295. // 处理材质数据
  296. if (dictionaryData.texture) {
  297. materialOptions.value = dictionaryData.texture.map(item => ({
  298. label: item,
  299. value: item
  300. }));
  301. }
  302. // 处理年代数据
  303. if (dictionaryData.agetype) {
  304. eraOptions.value = dictionaryData.agetype.map(item => ({
  305. label: item,
  306. value: item
  307. }));
  308. }
  309. // 处理排序数据
  310. orderByOptions.value = [
  311. { label: '等级', value: 'grade' },
  312. { label: '类别', value: 'category' },
  313. { label: '材质', value: 'texture' },
  314. { label: '年代', value: 'era' }
  315. ];
  316. // 字典数据加载完成后,进行初始化搜索
  317. console.log('移动端字典数据加载完成,开始初始化搜索');
  318. performInitialSearch();
  319. } catch (error) {
  320. console.error('获取字典数据失败:', error);
  321. // 如果接口失败,使用默认值
  322. levelOptions.value = [
  323. { label: '一级', value: '一级' },
  324. { label: '二级', value: '二级' },
  325. { label: '三级', value: '三级' },
  326. { label: '一般', value: '一般' },
  327. { label: '未定级', value: '未定级' }
  328. ];
  329. materialOptions.value = [
  330. { label: '石', value: '石' },
  331. { label: '铜', value: '铜' },
  332. { label: '铜宝玉石', value: '铜宝玉石' },
  333. { label: '瓷', value: '瓷' },
  334. { label: '陶', value: '陶' },
  335. { label: '宝玉石', value: '宝玉石' },
  336. { label: '玻璃', value: '玻璃' },
  337. { label: '其他', value: '其他' }
  338. ];
  339. eraOptions.value = [
  340. { label: '近现代', value: '近现代' }
  341. ];
  342. orderByOptions.value = [
  343. { label: '等级', value: 'grade' },
  344. { label: '类别', value: 'category' },
  345. { label: '材质', value: 'material' },
  346. { label: '年代', value: 'era' }
  347. ];
  348. // 即使使用默认数据,也要进行初始化搜索
  349. console.log('移动端使用默认字典数据,开始初始化搜索');
  350. performInitialSearch();
  351. }
  352. };
  353. // 执行初始化搜索
  354. const performInitialSearch = () => {
  355. // 检查是否有来自store的搜索关键词
  356. const homeSearchText = store.getters.getHomeSearchText;
  357. if (homeSearchText) {
  358. searchParams.value.searchText = homeSearchText;
  359. store.dispatch('setHomeSearchText', ''); // 清除store中的关键词
  360. }
  361. // 构建orderbyList(使用默认的grade排序)
  362. const orderbyList = buildOrderbyList();
  363. // 执行搜索
  364. handleSearch({
  365. ...searchParams.value,
  366. orderbyList
  367. });
  368. };
  369. // 组件挂载
  370. onMounted(() => {
  371. // 加载字典数据,字典数据加载完成后会自动进行初始化搜索
  372. loadDictionaryData();
  373. })
  374. </script>
  375. <style lang="scss" scoped>
  376. .collectpage {
  377. width: 100%;
  378. height: 100vh;
  379. position: relative;
  380. background: #f5f3ec;
  381. }
  382. .search-page {
  383. position: absolute;
  384. top: 0;
  385. left: 0;
  386. width: 100%;
  387. height: 100%;
  388. z-index: 1000;
  389. background: #f5f3ec;
  390. }
  391. .main-content {
  392. width: 100%;
  393. height: 100%;
  394. position: relative;
  395. }
  396. .top-title {
  397. position: absolute;
  398. top: 20px;
  399. left: 0;
  400. width: 100%;
  401. text-align: center;
  402. z-index: 10;
  403. img {
  404. max-width: 300px;
  405. height: auto;
  406. }
  407. }
  408. .background-image {
  409. position: absolute;
  410. top: 0;
  411. left: 0;
  412. width: 100%;
  413. height: 100%;
  414. z-index: 1;
  415. img {
  416. width: 100%;
  417. height: 100%;
  418. object-fit: cover;
  419. }
  420. }
  421. .search-container {
  422. position: absolute;
  423. top: 20px;
  424. left: 50%;
  425. transform: translateX(-50%);
  426. z-index: 10;
  427. width: calc(100% - 40px);
  428. max-width: 600px;
  429. }
  430. .search-wrapper {
  431. display: flex;
  432. flex-direction: column;
  433. border-bottom: 1px dotted #d3bfa2;
  434. border-radius: 6px;
  435. gap: 2px;
  436. }
  437. .checkbox-group {
  438. display: flex;
  439. gap: 6px;
  440. white-space: nowrap;
  441. :deep(.el-checkbox) {
  442. .el-checkbox__label {
  443. font-size: 14px;
  444. color: #666;
  445. font-weight: 400;
  446. }
  447. .el-checkbox__inner {
  448. border-color: #d3bfa2;
  449. &:hover {
  450. border-color: #d3bfa2;
  451. }
  452. }
  453. &.is-checked .el-checkbox__inner {
  454. background-color: #d3bfa2;
  455. border-color: #d3bfa2;
  456. }
  457. &:hover .el-checkbox__label {
  458. color: #333;
  459. }
  460. }
  461. }
  462. .search-input-wrapper {
  463. display: flex;
  464. align-items: center;
  465. gap: 2px;
  466. .quick-search-btn {
  467. position: relative;
  468. width: 30px;
  469. height: 30px;
  470. border: none;
  471. background: url('@/assets/img/btn_active.png') no-repeat;
  472. background-size: 30px 30px;
  473. .search-icon{
  474. position: absolute;
  475. top: 50%;
  476. left: 50%;
  477. transform: translate(-50%, -50%);
  478. width: 20px;
  479. height: 20px;
  480. object-fit: contain;
  481. }
  482. }
  483. }
  484. .search-input {
  485. flex: 1;
  486. width: 288px;
  487. :deep(.el-input__wrapper) {
  488. height: 40px;
  489. border-radius: 4px;
  490. }
  491. :deep(.el-input__inner) {
  492. font-size: 14px;
  493. color: #333;
  494. &::placeholder {
  495. color: #aaa;
  496. }
  497. }
  498. }
  499. .search-btn {
  500. display: flex;
  501. align-items: center;
  502. justify-content: center;
  503. gap: 2px;
  504. border: none;
  505. border-radius: 4px;
  506. color: #B49D7E;
  507. font-size: 16px;
  508. font-weight: 500;
  509. cursor: pointer;
  510. white-space: nowrap;
  511. transition: all 0.3s ease;
  512. height: 40px;
  513. min-width: 80px;
  514. background: transparent;
  515. .search-icon {
  516. width: 30px;
  517. height: 30px;
  518. object-fit: contain;
  519. }
  520. &:hover {
  521. background: #6a5a36;
  522. transform: translateY(-1px);
  523. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  524. }
  525. &:active {
  526. transform: translateY(0);
  527. }
  528. }
  529. .search-results {
  530. position: absolute;
  531. top: 110px;
  532. left: 0;
  533. width: 100%;
  534. height: calc(100% - 130px);
  535. overflow-y: auto;
  536. z-index: 10;
  537. padding: 0 20px;
  538. box-sizing: border-box;
  539. }
  540. .result-item {
  541. display: flex;
  542. flex-direction: column;
  543. background: rgba(255, 255, 255, 0.9);
  544. margin-bottom: 15px;
  545. padding: 10px;
  546. cursor: pointer;
  547. transition: all 0.3s ease;
  548. position: relative;
  549. overflow: hidden;
  550. &:hover {
  551. background: rgba(255, 255, 255, 1);
  552. transform: translateY(-2px);
  553. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
  554. }
  555. // 瀑布加载状态
  556. &.loading-waterfall {
  557. .result-image, .result-info {
  558. opacity: 0.3;
  559. filter: grayscale(100%);
  560. transition: opacity 0.3s ease, filter 0.3s ease;
  561. }
  562. }
  563. .waterfall-mask {
  564. position: absolute;
  565. top: 0;
  566. left: 0;
  567. width: 100%;
  568. height: 100%;
  569. background: linear-gradient(180deg,
  570. rgba(0, 0, 0, 0.9) 0%,
  571. rgba(0, 0, 0, 0.8) 50%,
  572. rgba(0, 0, 0, 0.9) 100%);
  573. z-index: 10;
  574. transform: translateY(-100%);
  575. border-radius: 8px;
  576. animation: waterfallDown 1.0s ease-out forwards, waterfallUp 1.0s ease-in 1.0s forwards;
  577. will-change: transform, opacity;
  578. }
  579. }
  580. .result-image {
  581. width: 100%;
  582. height: 181px;
  583. object-fit: cover;
  584. margin-right: 15px;
  585. transition: opacity 0.3s ease, filter 0.3s ease;
  586. }
  587. .result-info {
  588. flex: 1;
  589. display: flex;
  590. justify-content: space-between;
  591. align-items: center;
  592. transition: opacity 0.3s ease, filter 0.3s ease;
  593. .result-name {
  594. width: 85%;
  595. font-size: 16px;
  596. color: #464646;
  597. font-weight: 400;
  598. padding-top: 6px;
  599. }
  600. .more-info {
  601. width: 15%;
  602. display: flex;
  603. justify-content: flex-end;
  604. }
  605. .result-category,
  606. .result-era {
  607. margin: 4px 0;
  608. font-size: 14px;
  609. color: #666;
  610. }
  611. }
  612. .no-data {
  613. position: absolute;
  614. top: 50%;
  615. left: 50%;
  616. transform: translate(-50%, -50%);
  617. text-align: center;
  618. z-index: 10;
  619. p {
  620. font-size: 16px;
  621. color: #999;
  622. }
  623. }
  624. .loading-more {
  625. grid-column: 1 / -1;
  626. text-align: center;
  627. padding: 20px;
  628. color: #666;
  629. }
  630. // 瀑布动画关键帧
  631. @keyframes waterfallDown {
  632. 0% {
  633. transform: translateY(-100%);
  634. opacity: 0;
  635. }
  636. 15% {
  637. opacity: 1;
  638. }
  639. 100% {
  640. transform: translateY(0%);
  641. opacity: 1;
  642. }
  643. }
  644. @keyframes waterfallUp {
  645. 0% {
  646. transform: translateY(0%);
  647. opacity: 1;
  648. }
  649. 85% {
  650. opacity: 1;
  651. }
  652. 100% {
  653. transform: translateY(100%);
  654. opacity: 0;
  655. }
  656. }
  657. </style>