chore: establish V1.1 recovery baseline
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 找对象页 U19-U20(重构:用 ui-user-card + matchStore.like + recommendationService.discover)
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 2 条:Required 不可自动放宽——Discover 搜索仅透传筛选条件到后端,
|
||||
* 由后端按 PartnerPreference 做硬约束过滤,前端不做任何"放宽再查"。
|
||||
* - 第 4 条:Block 即时禁止互动——本页 Like 后由 matchStore 从列表移除。
|
||||
* - 第 6 条:单向意向不泄露——Pass/Like 未 Mutual 时对方不收到通知。
|
||||
* - 第 9 条:PostgreSQL 是 Source of Truth——搜索结果不在 Store 持久化,
|
||||
* 列表状态为页面局部状态(BaseStore 原则 2)。
|
||||
*/
|
||||
const { matchStore, appStore, authStore } = require('../../stores');
|
||||
const { recommendationService } = require('../../services');
|
||||
const { to } = require('../../constants');
|
||||
|
||||
const DEFAULT_AGE_MIN = 18;
|
||||
const DEFAULT_AGE_MAX = 60;
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
Page({
|
||||
data: {
|
||||
users: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
cursor: null,
|
||||
hasMore: true,
|
||||
|
||||
// 搜索栏关键词(编辑中)
|
||||
keywordInput: '',
|
||||
showFilter: false,
|
||||
|
||||
// 临时筛选(编辑中)
|
||||
tempRegion: '',
|
||||
tempAgeMin: DEFAULT_AGE_MIN,
|
||||
tempAgeMax: DEFAULT_AGE_MAX,
|
||||
|
||||
// 已应用筛选
|
||||
applied: { keyword: '', region: '', ageMin: DEFAULT_AGE_MIN, ageMax: DEFAULT_AGE_MAX },
|
||||
hasActiveFilter: false,
|
||||
|
||||
regions: ['丹阳', '镇江', '南京'],
|
||||
actionLoadingUserId: '',
|
||||
|
||||
emptyActions: [
|
||||
{ text: '清空筛选', event: 'reset', variant: 'primary' },
|
||||
{ text: '去今日推荐', event: 'home', variant: 'secondary' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
if (!authStore.state.isLoggedIn) return;
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
/** 拉取搜索结果(首页) */
|
||||
async fetchDiscover(first) {
|
||||
if (this.data.loading) return;
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
const result = await recommendationService.discover(this._buildParams());
|
||||
const items = result.items || [];
|
||||
this.setData({
|
||||
users: first ? items : [...this.data.users, ...items],
|
||||
cursor: result.nextCursor || null,
|
||||
hasMore: !!result.nextCursor,
|
||||
loading: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setData({ loading: false });
|
||||
appStore.toastError(err);
|
||||
}
|
||||
},
|
||||
|
||||
/** 加载更多 */
|
||||
async loadMore() {
|
||||
if (this.data.loadingMore || !this.data.hasMore || !this.data.cursor) return;
|
||||
this.setData({ loadingMore: true });
|
||||
try {
|
||||
const result = await recommendationService.discover({
|
||||
...this._buildParams(),
|
||||
cursor: this.data.cursor,
|
||||
});
|
||||
const items = result.items || [];
|
||||
this.setData({
|
||||
users: [...this.data.users, ...items],
|
||||
cursor: result.nextCursor || null,
|
||||
hasMore: !!result.nextCursor,
|
||||
});
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ loadingMore: false });
|
||||
}
|
||||
},
|
||||
|
||||
/** 构造请求参数(仅透传,后端守 Required 硬约束) */
|
||||
_buildParams() {
|
||||
const { applied } = this.data;
|
||||
const params = { pageSize: PAGE_SIZE };
|
||||
if (applied.keyword) params.keyword = applied.keyword;
|
||||
if (applied.region) params.region = applied.region;
|
||||
if (applied.ageMin !== DEFAULT_AGE_MIN) params.ageMin = applied.ageMin;
|
||||
if (applied.ageMax !== DEFAULT_AGE_MAX) params.ageMax = applied.ageMax;
|
||||
return params;
|
||||
},
|
||||
|
||||
// ============ 搜索栏 ============
|
||||
|
||||
onKeywordInput(e) {
|
||||
this.setData({ keywordInput: e.detail.value });
|
||||
},
|
||||
|
||||
/** 关键词搜索(键盘确认或点搜索图标) */
|
||||
onSearch() {
|
||||
const keyword = (this.data.keywordInput || '').trim();
|
||||
const applied = { ...this.data.applied, keyword };
|
||||
this.setData({
|
||||
applied,
|
||||
hasActiveFilter: this._hasActiveFilter(applied),
|
||||
});
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
// ============ 筛选面板 ============
|
||||
|
||||
toggleFilter() {
|
||||
this.setData({ showFilter: !this.data.showFilter });
|
||||
},
|
||||
|
||||
onRegionTap(e) {
|
||||
const val = e.currentTarget.dataset.val;
|
||||
this.setData({ tempRegion: this.data.tempRegion === val ? '' : val });
|
||||
},
|
||||
|
||||
onMinAgeChange(e) {
|
||||
let min = e.detail.value;
|
||||
if (min > this.data.tempAgeMax) min = this.data.tempAgeMax;
|
||||
this.setData({ tempAgeMin: min });
|
||||
},
|
||||
|
||||
onMaxAgeChange(e) {
|
||||
let max = e.detail.value;
|
||||
if (max < this.data.tempAgeMin) max = this.data.tempAgeMin;
|
||||
this.setData({ tempAgeMax: max });
|
||||
},
|
||||
|
||||
onApplyFilter() {
|
||||
const { tempRegion, tempAgeMin, tempAgeMax, keywordInput } = this.data;
|
||||
const applied = {
|
||||
keyword: (keywordInput || '').trim(),
|
||||
region: tempRegion,
|
||||
ageMin: tempAgeMin,
|
||||
ageMax: tempAgeMax,
|
||||
};
|
||||
this.setData({
|
||||
applied,
|
||||
hasActiveFilter: this._hasActiveFilter(applied),
|
||||
showFilter: false,
|
||||
});
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
onResetFilter() {
|
||||
const applied = { keyword: '', region: '', ageMin: DEFAULT_AGE_MIN, ageMax: DEFAULT_AGE_MAX };
|
||||
this.setData({
|
||||
keywordInput: '',
|
||||
tempRegion: '',
|
||||
tempAgeMin: DEFAULT_AGE_MIN,
|
||||
tempAgeMax: DEFAULT_AGE_MAX,
|
||||
applied,
|
||||
hasActiveFilter: false,
|
||||
showFilter: false,
|
||||
});
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
_hasActiveFilter(applied) {
|
||||
return !!(
|
||||
applied.keyword ||
|
||||
applied.region ||
|
||||
applied.ageMin !== DEFAULT_AGE_MIN ||
|
||||
applied.ageMax !== DEFAULT_AGE_MAX
|
||||
);
|
||||
},
|
||||
|
||||
onEmptyAction(e) {
|
||||
const { event } = e.detail || {};
|
||||
if (event === 'reset') {
|
||||
this.onResetFilter();
|
||||
} else if (event === 'home') {
|
||||
wx.switchTab({ url: '/pages/index/index' });
|
||||
}
|
||||
},
|
||||
|
||||
// ============ 卡片操作 ============
|
||||
|
||||
/** 卡片点击:查看资料 */
|
||||
onViewDetail(e) {
|
||||
const { userId } = e.detail || {};
|
||||
if (!userId) return;
|
||||
wx.navigateTo({ url: to.detail(userId) });
|
||||
},
|
||||
|
||||
/** 喜欢(actions slot 按钮,catchtap 阻止冒泡到卡片) */
|
||||
async onLike(e) {
|
||||
const userId = e.currentTarget.dataset.userId;
|
||||
if (!userId || this.data.actionLoadingUserId) return;
|
||||
this.setData({ actionLoadingUserId: userId });
|
||||
try {
|
||||
const result = await matchStore.like(userId);
|
||||
// 从列表移除(无论是否 Mutual,已做出判断)
|
||||
this.setData({ users: this.data.users.filter((u) => u.userId !== userId) });
|
||||
if (result && result.mutualMatch) {
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
||||
if (result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
}
|
||||
} else {
|
||||
appStore.toastSuccess('已表达喜欢');
|
||||
}
|
||||
} catch (err) {
|
||||
appStore.toastError(err);
|
||||
} finally {
|
||||
this.setData({ actionLoadingUserId: '' });
|
||||
}
|
||||
},
|
||||
|
||||
// ============ 列表交互 ============
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchDiscover(true).finally(() => wx.stopPullDownRefresh());
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
this.loadMore();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"navigationBarTitleText": "找对象",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundColor": "#F7F8FA",
|
||||
"backgroundTextStyle": "dark",
|
||||
"usingComponents": {
|
||||
"ui-user-card": "/components/user-card/index",
|
||||
"ui-button": "/components/button/index",
|
||||
"ui-loading": "/components/loading/index",
|
||||
"ui-empty": "/components/empty/index",
|
||||
"ui-section-header": "/components/section-header/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 找对象页 U19-U20(TypeScript)
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 2 条:Required 不可自动放宽——Discover 搜索仅透传筛选条件到后端
|
||||
* - 第 4 条:Block 即时禁止互动
|
||||
* - 第 6 条:单向意向不泄露
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { matchStore, appStore, authStore } from '../../stores';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { recommendationService } from '../../services';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { to } from '../../constants';
|
||||
import type { RecommendationCandidate } from '../../types';
|
||||
|
||||
interface DiscoverData {
|
||||
users: RecommendationCandidate[];
|
||||
loading: boolean;
|
||||
loadingMore: boolean;
|
||||
cursor: string | null;
|
||||
hasMore: boolean;
|
||||
keywordInput: string;
|
||||
showFilter: boolean;
|
||||
tempRegion: string;
|
||||
tempAgeMin: number;
|
||||
tempAgeMax: number;
|
||||
applied: { keyword: string; region: string; ageMin: number; ageMax: number };
|
||||
hasActiveFilter: boolean;
|
||||
regions: string[];
|
||||
actionLoadingUserId: string;
|
||||
emptyActions: Array<{ text: string; event: string; variant: string }>;
|
||||
}
|
||||
|
||||
const DEFAULT_AGE_MIN = 18;
|
||||
const DEFAULT_AGE_MAX = 60;
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
Page({
|
||||
data: {
|
||||
users: [],
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
cursor: null,
|
||||
hasMore: true,
|
||||
keywordInput: '',
|
||||
showFilter: false,
|
||||
tempRegion: '',
|
||||
tempAgeMin: DEFAULT_AGE_MIN,
|
||||
tempAgeMax: DEFAULT_AGE_MAX,
|
||||
applied: { keyword: '', region: '', ageMin: DEFAULT_AGE_MIN, ageMax: DEFAULT_AGE_MAX },
|
||||
hasActiveFilter: false,
|
||||
regions: ['丹阳', '镇江', '南京'],
|
||||
actionLoadingUserId: '',
|
||||
emptyActions: [
|
||||
{ text: '清空筛选', event: 'reset', variant: 'primary' },
|
||||
{ text: '去今日推荐', event: 'home', variant: 'secondary' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
if (!authStore.state.isLoggedIn) return;
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
async fetchDiscover(first: boolean) {
|
||||
if (this.data.loading) return;
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
// @ts-ignore - recommendationService 接口待 TypeScript 化
|
||||
const result = await recommendationService.discover(this._buildParams());
|
||||
const items: RecommendationCandidate[] = (result?.items as RecommendationCandidate[]) || [];
|
||||
this.setData({
|
||||
users: first ? items : [...this.data.users, ...items],
|
||||
cursor: result?.nextCursor || null,
|
||||
hasMore: !!result?.nextCursor,
|
||||
loading: false,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
this.setData({ loading: false });
|
||||
appStore.toastError(err instanceof Error ? err.message : '加载失败');
|
||||
}
|
||||
},
|
||||
|
||||
async loadMore() {
|
||||
if (this.data.loadingMore || !this.data.hasMore || !this.data.cursor) return;
|
||||
this.setData({ loadingMore: true });
|
||||
try {
|
||||
// @ts-ignore
|
||||
const result = await recommendationService.discover({
|
||||
...this._buildParams(),
|
||||
cursor: this.data.cursor,
|
||||
});
|
||||
const items: RecommendationCandidate[] = (result?.items as RecommendationCandidate[]) || [];
|
||||
this.setData({
|
||||
users: [...this.data.users, ...items],
|
||||
cursor: result?.nextCursor || null,
|
||||
hasMore: !!result?.nextCursor,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
appStore.toastError(err instanceof Error ? err.message : '加载失败');
|
||||
} finally {
|
||||
this.setData({ loadingMore: false });
|
||||
}
|
||||
},
|
||||
|
||||
_buildParams() {
|
||||
const { applied } = this.data;
|
||||
const params: Record<string, unknown> = { pageSize: PAGE_SIZE };
|
||||
if (applied.keyword) params.keyword = applied.keyword;
|
||||
if (applied.region) params.region = applied.region;
|
||||
if (applied.ageMin !== DEFAULT_AGE_MIN) params.ageMin = applied.ageMin;
|
||||
if (applied.ageMax !== DEFAULT_AGE_MAX) params.ageMax = applied.ageMax;
|
||||
return params;
|
||||
},
|
||||
|
||||
onKeywordInput(e: any) {
|
||||
this.setData({ keywordInput: e.detail.value });
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
const keyword = (this.data.keywordInput || '').trim();
|
||||
const applied = { ...this.data.applied, keyword };
|
||||
this.setData({ applied, hasActiveFilter: this._hasActiveFilter(applied) });
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
toggleFilter() {
|
||||
this.setData({ showFilter: !this.data.showFilter });
|
||||
},
|
||||
|
||||
onRegionTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const val = (e.currentTarget.dataset as any).val as string;
|
||||
this.setData({ tempRegion: this.data.tempRegion === val ? '' : val });
|
||||
},
|
||||
|
||||
onMinAgeChange(e: any) {
|
||||
let min = Number((e as any).detail.value);
|
||||
if (min > this.data.tempAgeMax) min = this.data.tempAgeMax;
|
||||
this.setData({ tempAgeMin: min });
|
||||
},
|
||||
|
||||
onMaxAgeChange(e: any) {
|
||||
let max = Number((e as any).detail.value);
|
||||
if (max < this.data.tempAgeMin) max = this.data.tempAgeMin;
|
||||
this.setData({ tempAgeMax: max });
|
||||
},
|
||||
|
||||
onApplyFilter() {
|
||||
const { tempRegion, tempAgeMin, tempAgeMax, keywordInput } = this.data;
|
||||
const applied = { keyword: (keywordInput || '').trim(), region: tempRegion, ageMin: tempAgeMin, ageMax: tempAgeMax };
|
||||
this.setData({ applied, hasActiveFilter: this._hasActiveFilter(applied), showFilter: false });
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
onResetFilter() {
|
||||
const applied = { keyword: '', region: '', ageMin: DEFAULT_AGE_MIN, ageMax: DEFAULT_AGE_MAX };
|
||||
this.setData({
|
||||
keywordInput: '', tempRegion: '', tempAgeMin: DEFAULT_AGE_MIN, tempAgeMax: DEFAULT_AGE_MAX,
|
||||
applied, hasActiveFilter: false, showFilter: false,
|
||||
});
|
||||
this.fetchDiscover(true);
|
||||
},
|
||||
|
||||
_hasActiveFilter(applied: { keyword: string; region: string; ageMin: number; ageMax: number }) {
|
||||
return !!(
|
||||
applied.keyword || applied.region ||
|
||||
applied.ageMin !== DEFAULT_AGE_MIN || applied.ageMax !== DEFAULT_AGE_MAX
|
||||
);
|
||||
},
|
||||
|
||||
onEmptyAction(e: WechatMiniprogram.TouchEvent) {
|
||||
const { event } = (e.detail as any) || {};
|
||||
if (event === 'reset') this.onResetFilter();
|
||||
else if (event === 'home') wx.switchTab({ url: '/pages/index/index' });
|
||||
},
|
||||
|
||||
onViewDetail(e: WechatMiniprogram.TouchEvent) {
|
||||
const { userId } = (e.detail as any) || {};
|
||||
if (!userId) return;
|
||||
wx.navigateTo({ url: to.detail(userId) });
|
||||
},
|
||||
|
||||
async onLike(e: WechatMiniprogram.TouchEvent) {
|
||||
const userId = (e.currentTarget.dataset as any).userId as string | undefined;
|
||||
if (!userId || this.data.actionLoadingUserId) return;
|
||||
this.setData({ actionLoadingUserId: userId });
|
||||
try {
|
||||
// @ts-ignore
|
||||
const result = await matchStore.like(userId);
|
||||
this.setData({ users: this.data.users.filter((u) => u.userId !== userId) });
|
||||
if (result?.mutualMatch) {
|
||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
||||
if (result.matchedConversationId) {
|
||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
||||
}
|
||||
} else {
|
||||
appStore.toastSuccess('已表达喜欢');
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
appStore.toastError(err instanceof Error ? err.message : '操作失败');
|
||||
} finally {
|
||||
this.setData({ actionLoadingUserId: '' });
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchDiscover(true).finally(() => wx.stopPullDownRefresh());
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
this.loadMore();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
<!-- 找对象页 U19-U20(重构:ui-user-card + matchStore + recommendationService.discover) -->
|
||||
<view class="page">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
class="search-field"
|
||||
placeholder="搜索符合条件的对象"
|
||||
placeholder-class="search-placeholder"
|
||||
value="{{keywordInput}}"
|
||||
bindinput="onKeywordInput"
|
||||
confirm-type="search"
|
||||
bindconfirm="onSearch"
|
||||
/>
|
||||
</view>
|
||||
<view class="filter-btn {{showFilter ? 'active' : ''}} {{hasActiveFilter ? 'has-filter' : ''}}" bindtap="toggleFilter">
|
||||
<text>筛选</text>
|
||||
<view wx:if="{{hasActiveFilter}}" class="filter-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选面板 -->
|
||||
<view wx:if="{{showFilter}}" class="filter-panel">
|
||||
<view class="filter-section">
|
||||
<text class="filter-label">年龄范围</text>
|
||||
<view class="range-row">
|
||||
<text class="range-value">{{tempAgeMin}} 岁</text>
|
||||
<slider
|
||||
class="range-slider"
|
||||
min="{{18}}"
|
||||
max="{{60}}"
|
||||
value="{{tempAgeMin}}"
|
||||
activeColor="{{'#E96B6B'}}"
|
||||
backgroundColor="{{'#F0F0F0'}}"
|
||||
block-size="{{20}}"
|
||||
bindchange="onMinAgeChange"
|
||||
/>
|
||||
<text class="range-value">{{tempAgeMax}} 岁</text>
|
||||
</view>
|
||||
<slider
|
||||
class="range-slider"
|
||||
min="{{18}}"
|
||||
max="{{60}}"
|
||||
value="{{tempAgeMax}}"
|
||||
activeColor="{{'#E96B6B'}}"
|
||||
backgroundColor="{{'#F0F0F0'}}"
|
||||
block-size="{{20}}"
|
||||
bindchange="onMaxAgeChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="filter-section">
|
||||
<text class="filter-label">地区</text>
|
||||
<view class="tag-group">
|
||||
<view
|
||||
wx:for="{{regions}}"
|
||||
wx:key="*this"
|
||||
class="tag {{tempRegion === item ? 'selected' : ''}}"
|
||||
bindtap="onRegionTap"
|
||||
data-val="{{item}}"
|
||||
>{{item}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-actions">
|
||||
<ui-button variant="ghost" size="md" block bind:tap="onResetFilter">重置</ui-button>
|
||||
<ui-button variant="primary" size="md" block bind:tap="onApplyFilter">应用筛选</ui-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 区块标题 -->
|
||||
<ui-section-header
|
||||
wx:if="{{users.length > 0 || loading}}"
|
||||
title="找对象"
|
||||
subtitle="{{hasActiveFilter ? '已应用筛选' : '共 ' + users.length + ' 位'}}"
|
||||
/>
|
||||
|
||||
<!-- 首次加载(骨架屏) -->
|
||||
<ui-loading
|
||||
wx:if="{{loading && users.length === 0}}"
|
||||
mode="skeleton"
|
||||
rows="5"
|
||||
/>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<block wx:elif="{{users.length > 0}}">
|
||||
<view class="user-list">
|
||||
<ui-user-card
|
||||
wx:for="{{users}}"
|
||||
wx:key="userId"
|
||||
user="{{item}}"
|
||||
bind:tap="onViewDetail"
|
||||
>
|
||||
<view
|
||||
slot="actions"
|
||||
class="card-action"
|
||||
catchtap="onLike"
|
||||
data-user-id="{{item.userId}}"
|
||||
>
|
||||
<ui-button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
loading="{{actionLoadingUserId === item.userId}}"
|
||||
>喜欢</ui-button>
|
||||
</view>
|
||||
</ui-user-card>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<ui-loading
|
||||
wx:if="{{loadingMore}}"
|
||||
mode="spinner"
|
||||
inline
|
||||
text="加载更多…"
|
||||
/>
|
||||
<view wx:if="{{!hasMore && users.length > 0}}" class="list-end-hint">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<ui-empty
|
||||
wx:elif="{{!loading}}"
|
||||
illustration="search"
|
||||
title="暂无符合条件的人选"
|
||||
description="尝试调整筛选条件,或看看今天为你推荐的人。"
|
||||
actions="{{emptyActions}}"
|
||||
bind:action="onEmptyAction"
|
||||
/>
|
||||
</view>
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 找对象页样式(重构后大幅简化,用户卡片主体样式封装在 ui-user-card 内)
|
||||
*/
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
/* ============ 搜索栏 ============ */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-3) var(--spacing-page-x);
|
||||
background: var(--color-bg-card);
|
||||
gap: var(--spacing-2);
|
||||
border-bottom: var(--border-width) solid var(--color-divider);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
background: var(--color-bg-page);
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing-3);
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
.search-field {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: var(--color-text-placeholder);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
min-width: 120rpx;
|
||||
height: 72rpx;
|
||||
background: var(--color-primary-light);
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-primary);
|
||||
position: relative;
|
||||
padding: 0 var(--spacing-3);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: var(--color-primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.filter-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
}
|
||||
|
||||
.filter-btn.active .filter-dot {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
/* ============ 筛选面板 ============ */
|
||||
.filter-panel {
|
||||
background: var(--color-bg-card);
|
||||
padding: var(--spacing-4) var(--spacing-page-x);
|
||||
border-bottom: var(--border-width) solid var(--color-divider);
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
.range-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.range-value {
|
||||
min-width: 96rpx;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.range-slider {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tag-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: var(--spacing-1) var(--spacing-3);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-sm);
|
||||
background: var(--color-bg-page);
|
||||
color: var(--color-text-secondary);
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.tag.selected {
|
||||
background: var(--color-primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
/* ============ 用户列表 ============ */
|
||||
.user-list {
|
||||
background: var(--color-bg-card);
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
.card-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-end-hint {
|
||||
text-align: center;
|
||||
padding: var(--spacing-3);
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
Reference in New Issue
Block a user