SectionMarketplace.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. <template>
  2. <section class="section-marketplace">
  3. <div class="marketplace-inner">
  4. <div class="marketplace-header">
  5. <div class="section-badge">
  6. <span class="badge-dot"></span>
  7. <span>{{ $t('marketplace.badge') }}</span>
  8. </div>
  9. <h2 class="section-title">{{ $t('marketplace.title') }} <span class="gradient-text">200K+</span> {{ $t('marketplace.subtitle') }}</h2>
  10. <p class="section-desc">{{ $t('marketplace.description') }}</p>
  11. </div>
  12. <!-- 搜索栏 -->
  13. <div class="search-bar">
  14. <div class="search-input-wrap">
  15. <span class="search-icon">🔍</span>
  16. <input
  17. v-model="searchQuery"
  18. type="text"
  19. :placeholder="$t('marketplace.search.placeholder')"
  20. class="search-input"
  21. @input="filterWorkflows"
  22. />
  23. <button class="search-btn" @click="filterWorkflows">{{ $t('marketplace.search.button') }}</button>
  24. </div>
  25. </div>
  26. <!-- 分类标签 -->
  27. <div class="category-tabs">
  28. <button
  29. v-for="cat in categories"
  30. :key="cat.category2Name"
  31. class="cat-tab"
  32. :class="{ active: activeCategory === cat.categoryId2 }"
  33. @click="selectCategory(cat.categoryId2)"
  34. >
  35. <span>{{ cat.icon }}</span>
  36. <span>{{ cat.category2Name }}</span>
  37. <span class="cat-count">{{ cat.downCount }}</span>
  38. </button>
  39. </div>
  40. <!-- 工作流卡片网格 -->
  41. <div class="marketplace-grid">
  42. <div
  43. v-for="(wf, i) in filteredWorkflows"
  44. :key="i"
  45. class="market-card"
  46. @mouseenter="hoveredCard = i"
  47. @mouseleave="hoveredCard = null"
  48. >
  49. <!-- 封面图 -->
  50. <div class="market-card-cover">
  51. <img :src="wf.coverImage" :alt="wf.title" class="market-cover-img" />
  52. <div class="market-cover-overlay"></div>
  53. <div class="market-platform-chip" :style="{ background: wf.platformColor }">{{ wf.categoryName1 }}</div>
  54. <div class="market-views">▶ {{ wf.downCount }}</div>
  55. </div>
  56. <!-- 卡片内容 -->
  57. <div class="market-card-body">
  58. <div class="market-card-top">
  59. <h4 class="market-title">{{ wf.workflowTitle }}</h4>
  60. <!-- <div class="market-rating">⭐ {{ wf.rating }}</div> -->
  61. </div>
  62. <p class="market-desc">{{ wf.description }}</p>
  63. <!-- <div class="market-tags">
  64. <span class="market-tag" v-for="tag in wf.tags" :key="tag">{{ tag }}</span>
  65. </div> -->
  66. <div class="market-footer">
  67. <div class="market-author">
  68. <img :src="wf.userAvatar" :alt="wf.author" class="market-avatar" />
  69. <span>{{ wf.nickName }}</span>
  70. </div>
  71. <button class="market-use-btn">{{ $t('marketplace.cta.use') }} →</button>
  72. </div>
  73. </div>
  74. <!-- 悬停遮罩 -->
  75. <div class="market-hover-overlay" :class="{ visible: hoveredCard === i }">
  76. <button class="overlay-btn primary" @click="handleUseClick($event, wf)">{{ $t('marketplace.cta.use2') }}</button>
  77. <button class="overlay-btn secondary" @click="handlePreviewClick($event, wf)">{{ $t('marketplace.cta.details') }}</button>
  78. </div>
  79. </div>
  80. </div>
  81. <!-- 查看更多 -->
  82. <div class="marketplace-footer">
  83. <button class="view-all-btn" @click="goWorkflowMarket">{{ $t('marketplace.cta.viewAll') }} →</button>
  84. </div>
  85. </div><!-- /.marketplace-inner -->
  86. </section>
  87. </template>
  88. <script setup>
  89. import {ref, computed, onMounted, inject} from 'vue'
  90. import { useI18n } from 'vue-i18n'
  91. import { useRouter } from 'vue-router'
  92. import {isLogin, openNewTab} from "@/utils/util.js";
  93. import { getPublishList,workflowCategoryStats } from '@/api/publish.js'
  94. const { t } = useI18n()
  95. const router = useRouter()
  96. const searchQuery = ref('')
  97. const activeCategory = ref('')
  98. const hoveredCard = ref(null)
  99. const icons = ['🌟','✍️','📊','🤖','💼','⚡']
  100. const categories = ref([
  101. // { label: '全部', icon: '🌟', count: '200K+' },
  102. // { label: '内容创作', icon: '✍️', count: '45K+' },
  103. // { label: '数据分析', icon: '📊', count: '38K+' },
  104. // { label: '客服自动化', icon: '🤖', count: '52K+' },
  105. // { label: '销售运营', icon: '💼', count: '29K+' },
  106. // { label: '效率工具', icon: '⚡', count: '36K+' }
  107. ])
  108. const allWorkflows = ref([])
  109. const filteredWorkflows = computed(() => {
  110. return allWorkflows.value.filter(wf => {
  111. const matchCategory = wf.categoryId2 === activeCategory.value
  112. const matchSearch = !searchQuery.value ||
  113. (wf.workflowTitle && wf.workflowTitle.includes(searchQuery.value)) ||
  114. (wf.description && wf.description.includes(searchQuery.value))
  115. return matchCategory && matchSearch
  116. })
  117. })
  118. function selectCategory(cat) {
  119. activeCategory.value = cat;
  120. searchFom.value.categoryId2=activeCategory.value;
  121. getList();
  122. }
  123. function filterWorkflows() {
  124. // 搜索过滤由 computed 处理
  125. }
  126. const searchFom = ref({
  127. pageNum: 0,
  128. pageSize: 8,
  129. orderByColumn: 'downCount',
  130. isAsc: 'desc',
  131. })
  132. const getWorkflowCategoryStats =async () => {
  133. let obj ={
  134. pageNum: 0,
  135. pageSize: 50}
  136. const res = await workflowCategoryStats(obj);
  137. if (res && res.rows) {
  138. // 处理数据,添加图标
  139. const processedCategories = res.rows.map((category, index) => ({
  140. ...category,
  141. icon: icons[index % icons.length] // 循环使用图标数组
  142. }));
  143. categories.value = processedCategories;
  144. activeCategory.value=categories.value[0].categoryId2;
  145. console.log(activeCategory.value)
  146. searchFom.value.categoryId2=activeCategory.value;
  147. getList()
  148. } else {
  149. console.error('Invalid response from workflowCategoryStats');
  150. }
  151. }
  152. const getList = async () => {
  153. try {
  154. const res = await getPublishList(searchFom.value);
  155. if (res && res.rows) {
  156. // 转换真实数据格式
  157. allWorkflows.value = res.rows;
  158. }
  159. } catch (error) {
  160. console.error('Error loading workflows:', error);
  161. // 加载失败时使用假数据
  162. allWorkflows.value = [];
  163. }
  164. };
  165. const openLoginDialog = inject('openLoginDialog')
  166. const handleUseClick = (event, item) => {
  167. event.stopPropagation(); // 阻止冒泡
  168. if(!isLogin({callback: openLoginDialog,t})){
  169. return;
  170. }
  171. openNewTab(item.categoryUrl);
  172. };
  173. // 处理预览按钮点击
  174. const handlePreviewClick = (event, item) => {
  175. event.stopPropagation(); // 阻止冒泡
  176. //判断是否登录
  177. if(!isLogin({callback: openLoginDialog,t})){
  178. return;
  179. }
  180. let path = `/workflow-detail`
  181. //增加参数名称
  182. router.push({
  183. path: path,
  184. query: {
  185. publishId: item.publishId,
  186. metaTitle: 'route.WorkflowDetail'
  187. }
  188. })
  189. };
  190. const goWorkflowMarket = () => { router.push({ path: '/workflow-trade' }); };
  191. onMounted(() => {
  192. getWorkflowCategoryStats();
  193. })
  194. </script>
  195. <style scoped>
  196. .section-marketplace {
  197. padding: 100px 0;
  198. background: #FFFFFF;
  199. }
  200. /* 内容容器:限制最大宽度,居中对齐 */
  201. .marketplace-inner {
  202. max-width: 1400px;
  203. margin: 0 auto;
  204. padding: 0 40px;
  205. }
  206. .marketplace-header {
  207. text-align: center;
  208. margin-bottom: 40px;
  209. }
  210. .section-badge {
  211. display: inline-flex;
  212. align-items: center;
  213. gap: 8px;
  214. background: #F5F3FF;
  215. border: 1px solid rgba(99,102,241,0.2);
  216. border-radius: 100px;
  217. padding: 6px 16px;
  218. font-size: 13px;
  219. color: #6366F1;
  220. font-weight: 500;
  221. margin-bottom: 20px;
  222. }
  223. .badge-dot {
  224. width: 6px; height: 6px;
  225. background: #6366F1; border-radius: 50%;
  226. animation: pulse 2s infinite;
  227. }
  228. @keyframes pulse {
  229. 0%, 100% { opacity: 1; transform: scale(1); }
  230. 50% { opacity: 0.5; transform: scale(1.3); }
  231. }
  232. .section-title {
  233. font-size: 44px;
  234. font-weight: 700;
  235. color: #111827;
  236. margin-bottom: 16px;
  237. }
  238. .gradient-text {
  239. background: linear-gradient(135deg, #6366F1, #EC4899);
  240. -webkit-background-clip: text;
  241. -webkit-text-fill-color: transparent;
  242. }
  243. .section-desc {
  244. font-size: 17px;
  245. color: #6B7280;
  246. max-width: 500px;
  247. margin: 0 auto;
  248. line-height: 1.7;
  249. }
  250. /* 搜索栏 */
  251. .search-bar {
  252. max-width: 600px;
  253. margin: 0 auto 32px;
  254. }
  255. .search-input-wrap {
  256. display: flex;
  257. align-items: center;
  258. background: white;
  259. border: 1.5px solid rgba(0,0,0,0.1);
  260. border-radius: 14px;
  261. padding: 4px 4px 4px 16px;
  262. box-shadow: 0 4px 16px rgba(0,0,0,0.06);
  263. transition: all 0.3s;
  264. }
  265. .search-input-wrap:focus-within {
  266. border-color: #6366F1;
  267. box-shadow: 0 4px 20px rgba(99,102,241,0.15);
  268. }
  269. .search-icon { font-size: 16px; margin-right: 8px; }
  270. .search-input {
  271. flex: 1;
  272. border: none;
  273. outline: none;
  274. font-size: 15px;
  275. color: #111827;
  276. background: transparent;
  277. }
  278. .search-btn {
  279. padding: 10px 24px;
  280. background: linear-gradient(135deg, #6366F1, #8B5CF6);
  281. color: white;
  282. border: none;
  283. border-radius: 10px;
  284. font-size: 14px;
  285. font-weight: 600;
  286. cursor: pointer;
  287. }
  288. /* 分类标签 */
  289. .category-tabs {
  290. display: flex;
  291. gap: 10px;
  292. justify-content: center;
  293. margin-bottom: 40px;
  294. flex-wrap: wrap;
  295. }
  296. .cat-tab {
  297. display: flex;
  298. align-items: center;
  299. gap: 6px;
  300. padding: 8px 18px;
  301. border-radius: 100px;
  302. border: 1.5px solid rgba(0,0,0,0.08);
  303. background: white;
  304. cursor: pointer;
  305. font-size: 14px;
  306. font-weight: 500;
  307. color: #6B7280;
  308. transition: all 0.3s;
  309. }
  310. .cat-tab:hover { border-color: #6366F1; color: #6366F1; }
  311. .cat-tab.active {
  312. background: linear-gradient(135deg, #6366F1, #8B5CF6);
  313. color: white;
  314. border-color: transparent;
  315. box-shadow: 0 4px 16px rgba(99,102,241,0.3);
  316. }
  317. .cat-count {
  318. font-size: 11px;
  319. background: rgba(255,255,255,0.2);
  320. padding: 1px 6px;
  321. border-radius: 100px;
  322. }
  323. .cat-tab:not(.active) .cat-count {
  324. background: #F3F4F6;
  325. color: #9CA3AF;
  326. }
  327. /* 卡片网格 */
  328. .marketplace-grid {
  329. display: grid;
  330. /* 自适应列数:每列最小 260px,最大 1fr,宽屏不超过 4 列 */
  331. grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  332. gap: 20px;
  333. margin-bottom: 40px;
  334. }
  335. /* 大屏(≥1400px)强制 4 列,防止卡片过宽 */
  336. @media (min-width: 1400px) {
  337. .marketplace-grid {
  338. grid-template-columns: repeat(4, 1fr);
  339. }
  340. }
  341. .market-card {
  342. background: white;
  343. border-radius: 16px;
  344. border: 1px solid rgba(0,0,0,0.06);
  345. overflow: hidden;
  346. box-shadow: 0 4px 16px rgba(0,0,0,0.06);
  347. transition: all 0.3s ease;
  348. position: relative;
  349. }
  350. .market-card:hover {
  351. transform: translateY(-6px);
  352. box-shadow: 0 16px 48px rgba(0,0,0,0.12);
  353. }
  354. .market-card.hidden {
  355. opacity: 0.3;
  356. transform: scale(0.95);
  357. }
  358. /* 封面图 */
  359. .market-card-cover {
  360. position: relative;
  361. height: 160px;
  362. overflow: hidden;
  363. }
  364. .market-cover-img {
  365. width: 100%;
  366. height: 100%;
  367. object-fit: cover;
  368. transition: transform 0.4s;
  369. }
  370. .market-card:hover .market-cover-img { transform: scale(1.05); }
  371. .market-cover-overlay {
  372. position: absolute;
  373. inset: 0;
  374. background: linear-gradient(to bottom, transparent 40%, rgba(0,0,0,0.4));
  375. }
  376. .market-platform-chip {
  377. position: absolute;
  378. top: 10px;
  379. left: 10px;
  380. padding: 3px 10px;
  381. border-radius: 100px;
  382. font-size: 11px;
  383. font-weight: 700;
  384. color: white;
  385. }
  386. .market-views {
  387. position: absolute;
  388. bottom: 10px;
  389. right: 10px;
  390. color: white;
  391. font-size: 12px;
  392. font-weight: 600;
  393. }
  394. /* 卡片内容 */
  395. .market-card-body { padding: 16px; }
  396. .market-card-top {
  397. display: flex;
  398. justify-content: space-between;
  399. align-items: flex-start;
  400. margin-bottom: 8px;
  401. }
  402. .market-title {
  403. font-size: 14px;
  404. font-weight: 700;
  405. color: #111827;
  406. flex: 1;
  407. margin-right: 8px;
  408. line-height: 1.3;
  409. }
  410. .market-rating { font-size: 12px; color: #F59E0B; font-weight: 600; white-space: nowrap; }
  411. .market-desc {
  412. font-size: 12px;
  413. color: #6B7280;
  414. line-height: 1.5;
  415. margin-bottom: 10px;
  416. }
  417. .market-tags {
  418. display: flex;
  419. flex-wrap: wrap;
  420. gap: 4px;
  421. margin-bottom: 12px;
  422. }
  423. .market-tag {
  424. padding: 2px 8px;
  425. background: #F3F4F6;
  426. border-radius: 100px;
  427. font-size: 10px;
  428. color: #6B7280;
  429. }
  430. .market-footer {
  431. display: flex;
  432. justify-content: space-between;
  433. align-items: center;
  434. }
  435. .market-author {
  436. display: flex;
  437. align-items: center;
  438. gap: 6px;
  439. font-size: 12px;
  440. color: #6B7280;
  441. }
  442. .market-avatar {
  443. width: 22px; height: 22px;
  444. border-radius: 50%;
  445. object-fit: cover;
  446. }
  447. .market-use-btn {
  448. padding: 5px 12px;
  449. background: #F5F3FF;
  450. color: #6366F1;
  451. border: 1px solid rgba(99,102,241,0.2);
  452. border-radius: 8px;
  453. font-size: 12px;
  454. font-weight: 600;
  455. cursor: pointer;
  456. transition: all 0.2s;
  457. }
  458. .market-use-btn:hover {
  459. background: #6366F1;
  460. color: white;
  461. }
  462. /* 悬停遮罩 */
  463. .market-hover-overlay {
  464. position: absolute;
  465. inset: 0;
  466. background: rgba(99,102,241,0.92);
  467. display: flex;
  468. flex-direction: column;
  469. align-items: center;
  470. justify-content: center;
  471. gap: 12px;
  472. opacity: 0;
  473. transition: opacity 0.3s;
  474. }
  475. .market-hover-overlay.visible { opacity: 1; }
  476. .overlay-btn {
  477. width: 160px;
  478. padding: 10px;
  479. border-radius: 10px;
  480. font-size: 14px;
  481. font-weight: 600;
  482. cursor: pointer;
  483. border: none;
  484. transition: all 0.2s;
  485. }
  486. .overlay-btn.primary {
  487. background: white;
  488. color: #6366F1;
  489. }
  490. .overlay-btn.secondary {
  491. background: rgba(255,255,255,0.15);
  492. color: white;
  493. border: 1px solid rgba(255,255,255,0.3);
  494. }
  495. /* ===== 响应式 ===== */
  496. /* 标题自适应 */
  497. .section-title {
  498. font-size: clamp(1.75rem, 4vw, 2.75rem);
  499. }
  500. .section-desc {
  501. font-size: clamp(0.875rem, 1.8vw, 1.0625rem);
  502. }
  503. /* 平板(≤1200px):3 列 */
  504. @media (max-width: 1200px) {
  505. .marketplace-inner { padding: 0 24px; }
  506. .marketplace-grid {
  507. grid-template-columns: repeat(3, 1fr);
  508. }
  509. }
  510. /* 小平板(≤900px):2 列 */
  511. @media (max-width: 900px) {
  512. .section-marketplace { padding: 4rem 0; }
  513. .marketplace-inner { padding: 0 16px; }
  514. .marketplace-grid {
  515. grid-template-columns: repeat(2, 1fr);
  516. gap: 16px;
  517. }
  518. .market-card-cover { height: 140px; }
  519. }
  520. /* 手机竖屏(≤600px):2 列紧凑 */
  521. @media (max-width: 600px) {
  522. .section-marketplace { padding: 3rem 0; }
  523. .marketplace-inner { padding: 0 12px; }
  524. .marketplace-grid {
  525. grid-template-columns: repeat(2, 1fr);
  526. gap: 12px;
  527. }
  528. .market-card-cover { height: 120px; }
  529. .market-card-body { padding: 12px; }
  530. .market-title { font-size: 12px; }
  531. .market-desc { font-size: 11px; }
  532. .market-tags { display: none; }
  533. .cat-tab { padding: 6px 12px; font-size: 12px; }
  534. .cat-count { display: none; }
  535. }
  536. /* 极小屏(≤400px):1 列 */
  537. @media (max-width: 400px) {
  538. .marketplace-grid {
  539. grid-template-columns: 1fr;
  540. }
  541. .market-card-cover { height: 160px; }
  542. }
  543. /* 查看更多 */
  544. .marketplace-footer { text-align: center; }
  545. .view-all-btn {
  546. padding: 14px 40px;
  547. background: white;
  548. border: 1.5px solid rgba(99,102,241,0.3);
  549. border-radius: 12px;
  550. font-size: 15px;
  551. font-weight: 600;
  552. color: #6366F1;
  553. cursor: pointer;
  554. transition: all 0.3s;
  555. }
  556. .view-all-btn:hover {
  557. background: #F5F3FF;
  558. transform: translateY(-2px);
  559. box-shadow: 0 4px 16px rgba(99,102,241,0.15);
  560. }
  561. </style>