| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 |
- <template>
- <view class="sr-page">
- <view class="sr-hd" :style="{ paddingTop: (statusBar + 12) + 'px' }">
- <view class="sr-hd-row">
- <view class="sr-back" @tap="goBack">
- <text class="sr-back-icon">←</text>
- </view>
- <view class="sr-search-bar" @tap="openSearch">
- <text class="sr-search-icon">🔍</text>
- <text class="sr-search-text">{{ keyword || copy.placeholder }}</text>
- </view>
- </view>
- <scroll-view scroll-x class="sr-tabs-scroll" :show-scrollbar="false">
- <view class="sr-tabs">
- <view
- v-for="(tab, index) in tabs"
- :key="tab.type"
- class="sr-tab"
- :class="{ 'sr-tab--on': currentTab === index }"
- @tap="onTabChange(index)"
- >
- <text class="sr-tab-text">{{ tabLabel(index) }}</text>
- </view>
- </view>
- </scroll-view>
- </view>
- <!-- 作品:双列网格 -->
- <scroll-view
- v-if="currentTab === 0"
- scroll-y
- class="sr-scroll"
- :style="{ height: scrollHeight + 'px' }"
- :lower-threshold="120"
- @scrolltolower="loadMore"
- >
- <view class="sr-body" :style="{ paddingBottom: safeBottom + 'px' }">
- <view v-if="activeTab.loading && !activeTab.rows.length" class="sr-state">
- <text class="sr-state-txt">{{ copy.loading }}</text>
- </view>
- <empty
- v-else-if="activeTab.loaded && !activeTab.rows.length"
- src="find_default3x"
- :text="copy.emptyWorks"
- />
- <view v-else class="sr-works">
- <view
- v-for="(item, index) in activeTab.rows"
- :key="item.id || index"
- class="sr-work-card"
- >
- <view class="sr-work-cover-wrap" @tap="openFind(item)">
- <image
- v-if="coverOf(item)"
- class="sr-work-cover"
- :src="coverOf(item)"
- mode="aspectFill"
- />
- <view v-else class="sr-work-cover sr-work-cover--ph" />
- <view v-if="isVideoItem(item)" class="sr-work-video-badge">
- <text class="sr-work-video-icon">▶</text>
- </view>
- </view>
- <view class="sr-work-body">
- <view class="sr-work-title" @tap="openFind(item)">
- <text v-if="item.type_text" class="sr-work-tag">{{ item.type_text }}</text>
- <text class="sr-work-txt">{{ formatContent(item.content) }}</text>
- </view>
- <view class="sr-work-ft">
- <text class="sr-work-date">{{ formatDate(item.createtime) }}</text>
- <view class="sr-work-like" @tap.stop="onLike(index)">
- <text
- class="sr-work-like-icon"
- :class="{ 'sr-work-like-icon--on': item.isLike === 1 }"
- >♥</text>
- <text class="sr-work-like-num">{{ item.likes || 0 }}</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- <uni-load-more
- v-if="activeTab.rows.length"
- :status="activeTab.loadStatus"
- :content-text="loadMoreText"
- />
- </view>
- </scroll-view>
- <!-- 用户 -->
- <scroll-view
- v-else
- scroll-y
- class="sr-scroll"
- :style="{ height: scrollHeight + 'px' }"
- :lower-threshold="120"
- @scrolltolower="loadMore"
- >
- <view class="sr-body" :style="{ paddingBottom: safeBottom + 'px' }">
- <view v-if="activeTab.loading && !activeTab.rows.length" class="sr-state">
- <text class="sr-state-txt">{{ copy.loading }}</text>
- </view>
- <empty
- v-else-if="activeTab.loaded && !activeTab.rows.length"
- src="find_default3x"
- :text="copy.emptyUsers"
- />
- <view v-else class="sr-users">
- <view
- v-for="(item, index) in activeTab.rows"
- :key="item.user_no || index"
- class="sr-user-card"
- @tap="openUser(item)"
- >
- <image class="sr-user-avatar" :src="userAvatar(item)" mode="aspectFill" />
- <view class="sr-user-main">
- <view class="sr-user-info">
- <view class="sr-user-name-row">
- <text v-if="item.shop" class="sr-user-shop-tag">{{ copy.shopTag }}</text>
- <text class="sr-user-name">{{ userName(item) }}</text>
- </view>
- <view class="sr-user-stats">
- <text class="sr-user-stat">{{ statFans(item) }}</text>
- <text class="sr-user-stat">{{ statPraised(item) }}</text>
- </view>
- </view>
- <button
- class="sr-follow-btn"
- :class="{ 'sr-follow-btn--on': item.isFollow === 1 }"
- @tap.stop="onFollow(index)"
- >
- {{ item.isFollow === 1 ? copy.followed : copy.follow }}
- </button>
- </view>
- </view>
- </view>
- <uni-load-more
- v-if="activeTab.rows.length"
- :status="activeTab.loadStatus"
- :content-text="loadMoreText"
- />
- </view>
- </scroll-view>
- </view>
- </template>
- <script>
- import { fetchFindSearchPage } from '@/features/search/search-gateway'
- import { stripHtml } from '@/features/feed/clip-normalize'
- import { toggleClipLike } from '@/features/feed/feed-gateway'
- import { toggleProfileFollow } from '@/features/profile/profile-gateway'
- import { detailPageUrl, videoDetailUrl } from '@/features/dynamics/post-media'
- function createTabState(type) {
- return {
- type,
- rows: [],
- page: 1,
- loadStatus: 'more',
- loading: false,
- loaded: false
- }
- }
- export default {
- data() {
- return {
- statusBar: 0,
- safeBottom: 0,
- scrollHeight: 600,
- keyword: '',
- currentTab: 0,
- tabs: [
- createTabState('works'),
- createTabState('user')
- ]
- }
- },
- computed: {
- copy() {
- const t = this.$t.bind(this)
- return {
- placeholder: t('search.placeholder'),
- tabWorks: t('search.tabWorks'),
- tabUser: t('search.tabUser'),
- loading: t('search.loading'),
- emptyWorks: t('search.emptyWorks'),
- emptyUsers: t('search.emptyUsers'),
- shopTag: t('search.shopTag'),
- follow: t('search.follow'),
- followed: t('search.followed'),
- loadFailed: t('search.loadFailed'),
- actionFailed: t('search.actionFailed'),
- contentUnavailable: t('search.contentUnavailable')
- }
- },
- loadMoreText() {
- const t = this.$t.bind(this)
- return {
- contentdown: t('search.loadMore'),
- contentrefresh: t('search.loading'),
- contentnomore: t('search.noMore')
- }
- },
- activeTab() {
- return this.tabs[this.currentTab] || this.tabs[0]
- },
- headerHeight() {
- return this.statusBar + uni.upx2px(180)
- }
- },
- onLoad(query) {
- this.keyword = query && query.search ? decodeURIComponent(query.search) : ''
- this.initLayout()
- this.loadTab(0, true)
- },
- onPullDownRefresh() {
- this.loadTab(this.currentTab, true).finally(() => {
- try { uni.stopPullDownRefresh() } catch (_) {}
- })
- },
- methods: {
- tabLabel(index) {
- if (index === 0) return this.copy.tabWorks
- if (index === 1) return this.copy.tabUser
- return ''
- },
- statFans(item) {
- return this.$t('search.statFans', { n: item.fans || 0 })
- },
- statPraised(item) {
- return this.$t('search.statPraised', { n: item.praised || 0 })
- },
- initLayout() {
- try {
- const info = uni.getSystemInfoSync()
- this.statusBar = info.statusBarHeight || 0
- this.safeBottom = (info.safeAreaInsets && info.safeAreaInsets.bottom) || 0
- this.scrollHeight = (info.windowHeight || 667) - this.headerHeight
- } catch (_) {}
- },
- goBack() {
- uni.navigateBack({
- fail: () => uni.switchTab({ url: '/pages/index/index' })
- })
- },
- openSearch() {
- const q = this.keyword
- ? `?keywords=${encodeURIComponent(this.keyword)}&type=find`
- : '?type=find'
- uni.navigateTo({ url: `/pages/search/index${q}` })
- },
- onTabChange(index) {
- if (this.currentTab === index) return
- this.currentTab = index
- const tab = this.tabs[index]
- if (!tab.loaded) {
- this.loadTab(index, true)
- }
- },
- loadMore() {
- this.loadTab(this.currentTab, false)
- },
- async loadTab(tabIndex, reset) {
- const tab = this.tabs[tabIndex]
- if (!tab) return
- if (tab.loadStatus === 'noMore' && !reset) return
- if (tab.loading) return
- if (reset) {
- tab.page = 1
- tab.rows = []
- tab.loadStatus = 'more'
- tab.loaded = false
- }
- tab.loading = true
- tab.loadStatus = 'loading'
- try {
- const pack = await fetchFindSearchPage(tab.page, {
- search: this.keyword,
- listType: tab.type
- })
- const list = (pack.rows || []).map((row) =>
- Object.assign({}, row, { listPage: pack.currentPage || tab.page })
- )
- tab.rows = reset ? list : tab.rows.concat(list)
- tab.page = pack.currentPage || tab.page
- if ((pack.currentPage || 1) >= (pack.lastPage || 1)) {
- tab.loadStatus = 'noMore'
- } else {
- tab.loadStatus = 'more'
- tab.page = (pack.currentPage || tab.page) + 1
- }
- tab.loaded = true
- } catch (err) {
- tab.loadStatus = 'more'
- if (reset) tab.rows = []
- tab.loaded = true
- this.$appKit && this.$appKit.msg(err.message || this.copy.loadFailed)
- } finally {
- tab.loading = false
- }
- },
- oss(url, w = 400, h = 0) {
- if (this.$appKit && typeof this.$appKit.oss === 'function') {
- return this.$appKit.oss(url, w, h)
- }
- return url || ''
- },
- coverOf(item) {
- const images = Array.isArray(item.images) ? item.images : []
- const cover =
- images[0] ||
- (item.video && item.video.cover_url) ||
- item.cover ||
- ''
- return cover ? this.oss(cover, 400, 460) : ''
- },
- formatContent(content) {
- const text = stripHtml(content)
- if (!text) return ' '
- return text.length > 48 ? `${text.slice(0, 48)}…` : text
- },
- formatDate(ts) {
- if (this.$appKit && typeof this.$appKit.timeFormat === 'function') {
- return this.$appKit.timeFormat(ts, 'mm-dd')
- }
- return ''
- },
- userAvatar(item) {
- if (item.shop && item.shop.avatar) return this.oss(item.shop.avatar, 88, 88)
- if (item.user && item.user.avatar) return this.oss(item.user.avatar, 88, 88)
- return ''
- },
- userName(item) {
- if (item.shop && item.shop.shopname) return item.shop.shopname
- if (item.user && item.user.nickname) return item.user.nickname
- return '—'
- },
- isVideoItem(item) {
- if (!item) return false
- if (item.type === 'video') return true
- return !!(item.video && (item.video.url || item.video.cover_url))
- },
- openFind(item) {
- if (!item) return
- if (item.state && item.state !== 'normal') {
- this.$appKit && this.$appKit.msg(this.copy.contentUnavailable)
- return
- }
- if (item.type === 'live' && item.live_id) {
- uni.navigateTo({ url: `/pages/live/watch?id=${item.live_id}` })
- return
- }
- if (this.isVideoItem(item)) {
- uni.navigateTo({
- url: videoDetailUrl(item.id, {
- type: 'work',
- page: item.listPage || item.page || 1,
- search: this.keyword || undefined
- })
- })
- return
- }
- if (item.id) {
- uni.navigateTo({ url: detailPageUrl(item.id) })
- }
- },
- openUser(item) {
- if (!item || !item.user_no) return
- uni.navigateTo({
- url: `/pages/profile/show?id=${encodeURIComponent(String(item.user_no))}`
- })
- },
- async onLike(index) {
- if (!this.$appKit || !this.$appKit.requireLogin()) return
- const tab = this.tabs[0]
- const row = tab.rows[index]
- if (!row || !row.id) return
- try {
- const flag = await toggleClipLike(row.id)
- if (flag === 0 || flag === '0') {
- row.isLike = 0
- row.likes = Math.max(0, Number(row.likes || 0) - 1)
- } else if (flag === 1 || flag === '1') {
- row.isLike = 1
- row.likes = Number(row.likes || 0) + 1
- }
- } catch (err) {
- this.$appKit.msg(err.message || this.copy.actionFailed)
- }
- },
- async onFollow(index) {
- if (!this.$appKit || !this.$appKit.requireLogin()) return
- const tab = this.tabs[1]
- const row = tab.rows[index]
- if (!row || !row.user_no) return
- try {
- const flag = await toggleProfileFollow(row.user_no)
- if (flag === 0 || flag === '0') row.isFollow = 0
- else if (flag === 1 || flag === '1') row.isFollow = 1
- } catch (err) {
- this.$appKit.msg(err.message || this.copy.actionFailed)
- }
- }
- }
- }
- </script>
- <style scoped>
- .sr-page {
- min-height: 100vh;
- background-color: #eef3fb;
- }
- .sr-hd {
- padding-left: 32rpx;
- padding-right: 32rpx;
- padding-bottom: 8rpx;
- }
- .sr-hd-row {
- display: flex;
- flex-direction: row;
- align-items: center;
- margin-bottom: 20rpx;
- }
- .sr-back {
- width: 72rpx;
- height: 72rpx;
- border-radius: 20rpx;
- background-color: #ffffff;
- box-shadow: 0 6rpx 20rpx rgba(74, 140, 255, 0.12);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- margin-right: 20rpx;
- }
- .sr-back-icon {
- font-size: 36rpx;
- color: #4a8cff;
- line-height: 1;
- }
- .sr-search-bar {
- flex: 1;
- min-width: 0;
- height: 72rpx;
- border-radius: 20rpx;
- background-color: #ffffff;
- box-shadow: 0 6rpx 20rpx rgba(74, 140, 255, 0.1);
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 0 24rpx;
- box-sizing: border-box;
- }
- .sr-search-icon {
- font-size: 28rpx;
- margin-right: 12rpx;
- opacity: 0.7;
- }
- .sr-search-text {
- flex: 1;
- font-size: 28rpx;
- color: #1a1d29;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .sr-tabs-scroll {
- white-space: nowrap;
- }
- .sr-tabs {
- display: inline-flex;
- flex-direction: row;
- padding-bottom: 12rpx;
- }
- .sr-tab {
- padding: 12rpx 28rpx;
- margin-right: 16rpx;
- border-radius: 999rpx;
- background-color: rgba(255, 255, 255, 0.7);
- }
- .sr-tab--on {
- background: linear-gradient(135deg, #4a8cff 0%, #6ba0ff 100%);
- box-shadow: 0 6rpx 16rpx rgba(74, 140, 255, 0.25);
- }
- .sr-tab-text {
- font-size: 28rpx;
- color: #8a8d9e;
- font-weight: 500;
- }
- .sr-tab--on .sr-tab-text {
- color: #ffffff;
- font-weight: 600;
- }
- .sr-body {
- padding: 16rpx 24rpx 0;
- box-sizing: border-box;
- }
- .sr-state {
- padding: 120rpx 0;
- text-align: center;
- }
- .sr-state-txt {
- font-size: 28rpx;
- color: #8a8d9e;
- }
- .sr-works {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: space-between;
- }
- .sr-work-card {
- width: calc((100% - 16rpx) / 2);
- margin-bottom: 24rpx;
- background-color: #ffffff;
- border-radius: 24rpx;
- overflow: hidden;
- box-shadow: 0 6rpx 24rpx rgba(74, 140, 255, 0.08);
- }
- .sr-work-cover-wrap {
- position: relative;
- width: 100%;
- height: 460rpx;
- background-color: #eef2f8;
- }
- .sr-work-video-badge {
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- width: 72rpx;
- height: 72rpx;
- border-radius: 36rpx;
- background-color: rgba(0, 0, 0, 0.42);
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .sr-work-video-icon {
- color: #ffffff;
- font-size: 32rpx;
- margin-left: 4rpx;
- }
- .sr-work-cover {
- width: 100%;
- height: 100%;
- display: block;
- }
- .sr-work-cover--ph {
- background: linear-gradient(135deg, #c1c8d6 0%, #8e96a6 100%);
- }
- .sr-work-body {
- padding: 16rpx 18rpx 18rpx;
- }
- .sr-work-title {
- margin-bottom: 12rpx;
- }
- .sr-work-tag {
- display: inline-block;
- font-size: 20rpx;
- color: #4a8cff;
- background-color: rgba(74, 140, 255, 0.12);
- padding: 2rpx 10rpx;
- border-radius: 8rpx;
- margin-right: 8rpx;
- vertical-align: middle;
- }
- .sr-work-txt {
- font-size: 26rpx;
- font-weight: 600;
- color: #1a1d29;
- line-height: 36rpx;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }
- .sr-work-ft {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- }
- .sr-work-date {
- font-size: 22rpx;
- color: #8a8d9e;
- }
- .sr-work-like {
- display: flex;
- flex-direction: row;
- align-items: center;
- }
- .sr-work-like-icon {
- font-size: 28rpx;
- color: #b0b4c0;
- margin-right: 6rpx;
- }
- .sr-work-like-icon--on {
- color: #ff4d4f;
- }
- .sr-work-like-num {
- font-size: 24rpx;
- color: #5c6070;
- }
- .sr-users {
- display: flex;
- flex-direction: column;
- }
- .sr-user-card {
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 24rpx;
- margin-bottom: 20rpx;
- background-color: #ffffff;
- border-radius: 24rpx;
- box-shadow: 0 6rpx 24rpx rgba(74, 140, 255, 0.08);
- }
- .sr-user-avatar {
- width: 100rpx;
- height: 100rpx;
- border-radius: 50rpx;
- flex-shrink: 0;
- background-color: #eef2f8;
- }
- .sr-user-main {
- flex: 1;
- min-width: 0;
- margin-left: 20rpx;
- display: flex;
- flex-direction: row;
- align-items: center;
- }
- .sr-user-info {
- flex: 1;
- min-width: 0;
- margin-right: 16rpx;
- }
- .sr-user-name-row {
- display: flex;
- flex-direction: row;
- align-items: center;
- margin-bottom: 8rpx;
- }
- .sr-user-shop-tag {
- flex-shrink: 0;
- margin-right: 8rpx;
- padding: 2rpx 10rpx;
- border-radius: 8rpx;
- background: linear-gradient(135deg, #4a8cff 0%, #6ec8ff 100%);
- font-size: 20rpx;
- color: #ffffff;
- }
- .sr-user-name {
- font-size: 30rpx;
- font-weight: 600;
- color: #1a1d26;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .sr-user-stats {
- display: flex;
- flex-direction: row;
- }
- .sr-user-stat {
- font-size: 24rpx;
- color: #8a8d9e;
- margin-right: 20rpx;
- }
- .sr-follow-btn {
- flex-shrink: 0;
- margin: 0;
- padding: 0 28rpx;
- height: 56rpx;
- line-height: 56rpx;
- font-size: 24rpx;
- color: #ffffff;
- background: linear-gradient(135deg, #4a8cff 0%, #6ec8ff 100%);
- border-radius: 999rpx;
- border: none;
- }
- .sr-follow-btn--on {
- color: #8a8d9e;
- background: #f4f6fb;
- border: 1rpx solid #e8edf5;
- }
- .sr-follow-btn::after {
- border: none;
- }
- </style>
|