2026-09-09 22:05:00 +08:00
|
|
|
|
/**
|
2026-09-14 11:09:23 +08:00
|
|
|
|
* 找对象页 — 筛选对齐后端 DiscoverController 5 个筛选参数:
|
|
|
|
|
|
* minAge, maxAge, regionCode, maritalStatus, educationCode
|
2026-09-09 22:05:00 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 不变量(AGENTS.md):
|
2026-09-14 11:09:23 +08:00
|
|
|
|
* - Required 不可自动放宽——Discover 仅透传筛选条件到后端
|
|
|
|
|
|
* - Block 即时禁止互动
|
|
|
|
|
|
* - 单向意向不泄露
|
2026-09-09 22:05:00 +08:00
|
|
|
|
*/
|
|
|
|
|
|
// 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';
|
2026-09-23 09:55:40 +08:00
|
|
|
|
// 顶部安全区:实测右上角胶囊按钮矩形,避免筛选条与搜索图标被胶囊压住(见 utils/nav-metrics.js)
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
|
|
|
|
const { navSafeData } = require('../../utils/nav-metrics');
|
2026-09-09 22:05:00 +08:00
|
|
|
|
|
2026-09-14 11:09:23 +08:00
|
|
|
|
interface AppliedFilter {
|
|
|
|
|
|
keyword: string;
|
|
|
|
|
|
regionCode: string; // 后端参数名
|
|
|
|
|
|
minAge: number;
|
|
|
|
|
|
maxAge: number;
|
|
|
|
|
|
educationCode: string; // 后端参数名
|
|
|
|
|
|
maritalStatus: string; // 后端参数名
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-09 22:05:00 +08:00
|
|
|
|
interface DiscoverData {
|
|
|
|
|
|
users: RecommendationCandidate[];
|
|
|
|
|
|
loading: boolean;
|
|
|
|
|
|
loadingMore: boolean;
|
|
|
|
|
|
cursor: string | null;
|
|
|
|
|
|
hasMore: boolean;
|
|
|
|
|
|
keywordInput: string;
|
|
|
|
|
|
showFilter: boolean;
|
2026-09-14 11:09:23 +08:00
|
|
|
|
// 面板临时态
|
|
|
|
|
|
tempRegionCode: string;
|
|
|
|
|
|
tempMinAge: number;
|
|
|
|
|
|
tempMaxAge: number;
|
|
|
|
|
|
tempEducationCode: string;
|
|
|
|
|
|
tempMaritalStatus: string;
|
|
|
|
|
|
applied: AppliedFilter;
|
2026-09-09 22:05:00 +08:00
|
|
|
|
hasActiveFilter: boolean;
|
2026-09-14 11:09:23 +08:00
|
|
|
|
regions: Array<{ code: string; label: string }>;
|
|
|
|
|
|
educations: Array<{ code: string; label: string }>;
|
|
|
|
|
|
maritalStatuses: Array<{ code: string; label: string }>;
|
2026-09-09 22:05:00 +08:00
|
|
|
|
actionLoadingUserId: string;
|
2026-09-14 11:27:52 +08:00
|
|
|
|
// 抽屉滚动/高亮定位
|
|
|
|
|
|
panelScrollTo: string;
|
|
|
|
|
|
panelHighlight: string;
|
2026-09-09 22:05:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 11:09:23 +08:00
|
|
|
|
const DEFAULT_MIN_AGE = 18;
|
|
|
|
|
|
const DEFAULT_MAX_AGE = 60;
|
2026-09-09 22:05:00 +08:00
|
|
|
|
const PAGE_SIZE = 20;
|
|
|
|
|
|
|
|
|
|
|
|
Page({
|
|
|
|
|
|
data: {
|
2026-09-23 09:55:40 +08:00
|
|
|
|
navVars: '',
|
2026-09-09 22:05:00 +08:00
|
|
|
|
users: [],
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
loadingMore: false,
|
|
|
|
|
|
cursor: null,
|
|
|
|
|
|
hasMore: true,
|
|
|
|
|
|
keywordInput: '',
|
|
|
|
|
|
showFilter: false,
|
2026-09-14 11:09:23 +08:00
|
|
|
|
tempRegionCode: '',
|
|
|
|
|
|
tempMinAge: DEFAULT_MIN_AGE,
|
|
|
|
|
|
tempMaxAge: DEFAULT_MAX_AGE,
|
|
|
|
|
|
tempEducationCode: '',
|
|
|
|
|
|
tempMaritalStatus: '',
|
|
|
|
|
|
applied: {
|
|
|
|
|
|
keyword: '',
|
|
|
|
|
|
regionCode: '',
|
|
|
|
|
|
minAge: DEFAULT_MIN_AGE,
|
|
|
|
|
|
maxAge: DEFAULT_MAX_AGE,
|
|
|
|
|
|
educationCode: '',
|
|
|
|
|
|
maritalStatus: '',
|
|
|
|
|
|
},
|
2026-09-09 22:05:00 +08:00
|
|
|
|
hasActiveFilter: false,
|
2026-09-14 11:09:23 +08:00
|
|
|
|
regions: [
|
|
|
|
|
|
{ code: '321181', label: '丹阳' },
|
|
|
|
|
|
{ code: '321100', label: '镇江' },
|
|
|
|
|
|
{ code: '320100', label: '南京' },
|
2026-09-09 22:05:00 +08:00
|
|
|
|
],
|
2026-09-14 11:09:23 +08:00
|
|
|
|
educations: [
|
|
|
|
|
|
{ code: 'high_school', label: '高中及以下' },
|
|
|
|
|
|
{ code: 'college', label: '大专' },
|
|
|
|
|
|
{ code: 'bachelor', label: '本科' },
|
|
|
|
|
|
{ code: 'master', label: '硕士及以上' },
|
|
|
|
|
|
],
|
|
|
|
|
|
maritalStatuses: [
|
|
|
|
|
|
{ code: 'never_married', label: '未婚' },
|
|
|
|
|
|
{ code: 'divorced', label: '离异' },
|
|
|
|
|
|
],
|
|
|
|
|
|
actionLoadingUserId: '',
|
2026-09-14 11:27:52 +08:00
|
|
|
|
panelScrollTo: '',
|
|
|
|
|
|
panelHighlight: '',
|
2026-09-14 11:09:23 +08:00
|
|
|
|
} as DiscoverData,
|
2026-09-09 22:05:00 +08:00
|
|
|
|
|
|
|
|
|
|
onLoad() {
|
2026-09-23 09:55:40 +08:00
|
|
|
|
// 顶部安全区先算:onLoad 早于首次渲染,避免出现「先压住胶囊再跳开」的闪动
|
|
|
|
|
|
this.setData(navSafeData());
|
2026-09-09 22:05:00 +08:00
|
|
|
|
if (!authStore.state.isLoggedIn) return;
|
|
|
|
|
|
this.fetchDiscover(true);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async fetchDiscover(first: boolean) {
|
|
|
|
|
|
if (this.data.loading) return;
|
|
|
|
|
|
this.setData({ loading: true });
|
|
|
|
|
|
try {
|
2026-09-14 11:09:23 +08:00
|
|
|
|
// @ts-ignore - recommendationService 接口待 TS 化
|
2026-09-09 22:05:00 +08:00
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-14 11:09:23 +08:00
|
|
|
|
/** 对齐后端 DiscoverController 5 个筛选参数 */
|
2026-09-09 22:05:00 +08:00
|
|
|
|
_buildParams() {
|
2026-09-14 11:09:23 +08:00
|
|
|
|
const a = this.data.applied;
|
2026-09-09 22:05:00 +08:00
|
|
|
|
const params: Record<string, unknown> = { pageSize: PAGE_SIZE };
|
2026-09-14 11:09:23 +08:00
|
|
|
|
if (a.keyword) params.keyword = a.keyword;
|
|
|
|
|
|
if (a.regionCode) params.regionCode = a.regionCode;
|
|
|
|
|
|
if (a.minAge !== DEFAULT_MIN_AGE) params.minAge = a.minAge;
|
|
|
|
|
|
if (a.maxAge !== DEFAULT_MAX_AGE) params.maxAge = a.maxAge;
|
|
|
|
|
|
if (a.educationCode) params.educationCode = a.educationCode;
|
|
|
|
|
|
if (a.maritalStatus) params.maritalStatus = a.maritalStatus;
|
2026-09-09 22:05:00 +08:00
|
|
|
|
return params;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-14 11:09:23 +08:00
|
|
|
|
_hasActiveFilter(applied: AppliedFilter) {
|
|
|
|
|
|
return !!(
|
|
|
|
|
|
applied.keyword || applied.regionCode ||
|
|
|
|
|
|
applied.minAge !== DEFAULT_MIN_AGE || applied.maxAge !== DEFAULT_MAX_AGE ||
|
|
|
|
|
|
applied.educationCode || applied.maritalStatus
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 搜索框 ==========
|
|
|
|
|
|
|
2026-09-09 22:05:00 +08:00
|
|
|
|
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);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-14 11:09:23 +08:00
|
|
|
|
// ========== 顶部 chip 点击(即时应用) ==========
|
2026-09-09 22:05:00 +08:00
|
|
|
|
|
2026-09-14 11:09:23 +08:00
|
|
|
|
onTopRegionTap() {
|
|
|
|
|
|
// 丹阳本地 chip:regionCode=321181
|
|
|
|
|
|
const target = '321181';
|
|
|
|
|
|
const nextCode = this.data.applied.regionCode === target ? '' : target;
|
|
|
|
|
|
const applied = { ...this.data.applied, regionCode: nextCode };
|
2026-09-14 10:59:43 +08:00
|
|
|
|
this.setData({
|
2026-09-14 11:09:23 +08:00
|
|
|
|
tempRegionCode: nextCode,
|
2026-09-14 10:59:43 +08:00
|
|
|
|
applied,
|
|
|
|
|
|
hasActiveFilter: this._hasActiveFilter(applied),
|
|
|
|
|
|
});
|
|
|
|
|
|
this.fetchDiscover(true);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-14 11:27:52 +08:00
|
|
|
|
/** 打开筛选面板,并同步当前 applied 状态到 temp,可选定位到 target section */
|
|
|
|
|
|
openFilterPanel(e?: WechatMiniprogram.TouchEvent) {
|
|
|
|
|
|
const target = e ? (e.currentTarget.dataset as any).target as string | undefined : undefined;
|
|
|
|
|
|
const sectionMap: Record<string, string> = { age: 'sec-age', education: 'sec-education', marital: 'sec-marital', region: 'sec-region' };
|
2026-09-14 11:09:23 +08:00
|
|
|
|
const a = this.data.applied;
|
|
|
|
|
|
this.setData({
|
|
|
|
|
|
showFilter: true,
|
|
|
|
|
|
tempRegionCode: a.regionCode,
|
|
|
|
|
|
tempMinAge: a.minAge,
|
|
|
|
|
|
tempMaxAge: a.maxAge,
|
|
|
|
|
|
tempEducationCode: a.educationCode,
|
|
|
|
|
|
tempMaritalStatus: a.maritalStatus,
|
2026-09-14 11:27:52 +08:00
|
|
|
|
panelScrollTo: target ? (sectionMap[target] || '') : '',
|
|
|
|
|
|
panelHighlight: target || '',
|
2026-09-14 11:09:23 +08:00
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
closeFilterPanel() {
|
|
|
|
|
|
this.setData({ showFilter: false });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 筛选面板内交互 ==========
|
|
|
|
|
|
|
|
|
|
|
|
onPanelRegionTap(e: WechatMiniprogram.TouchEvent) {
|
|
|
|
|
|
const code = (e.currentTarget.dataset as any).code as string;
|
|
|
|
|
|
this.setData({ tempRegionCode: this.data.tempRegionCode === code ? '' : code });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onPanelEducationTap(e: WechatMiniprogram.TouchEvent) {
|
|
|
|
|
|
const code = (e.currentTarget.dataset as any).code as string;
|
|
|
|
|
|
this.setData({ tempEducationCode: this.data.tempEducationCode === code ? '' : code });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onPanelMaritalTap(e: WechatMiniprogram.TouchEvent) {
|
|
|
|
|
|
const code = (e.currentTarget.dataset as any).code as string;
|
|
|
|
|
|
this.setData({ tempMaritalStatus: this.data.tempMaritalStatus === code ? '' : code });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-09 22:05:00 +08:00
|
|
|
|
onMinAgeChange(e: any) {
|
2026-09-14 11:09:23 +08:00
|
|
|
|
let min = Number(e.detail.value);
|
|
|
|
|
|
if (min > this.data.tempMaxAge) min = this.data.tempMaxAge;
|
|
|
|
|
|
this.setData({ tempMinAge: min });
|
2026-09-09 22:05:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onMaxAgeChange(e: any) {
|
2026-09-14 11:09:23 +08:00
|
|
|
|
let max = Number(e.detail.value);
|
|
|
|
|
|
if (max < this.data.tempMinAge) max = this.data.tempMinAge;
|
|
|
|
|
|
this.setData({ tempMaxAge: max });
|
2026-09-09 22:05:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onApplyFilter() {
|
2026-09-14 11:09:23 +08:00
|
|
|
|
const a = this.data.applied;
|
|
|
|
|
|
const applied: AppliedFilter = {
|
|
|
|
|
|
keyword: a.keyword,
|
|
|
|
|
|
regionCode: this.data.tempRegionCode,
|
|
|
|
|
|
minAge: this.data.tempMinAge,
|
|
|
|
|
|
maxAge: this.data.tempMaxAge,
|
|
|
|
|
|
educationCode: this.data.tempEducationCode,
|
|
|
|
|
|
maritalStatus: this.data.tempMaritalStatus,
|
|
|
|
|
|
};
|
2026-09-09 22:05:00 +08:00
|
|
|
|
this.setData({
|
2026-09-14 11:09:23 +08:00
|
|
|
|
applied,
|
|
|
|
|
|
hasActiveFilter: this._hasActiveFilter(applied),
|
|
|
|
|
|
showFilter: false,
|
2026-09-09 22:05:00 +08:00
|
|
|
|
});
|
|
|
|
|
|
this.fetchDiscover(true);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-14 11:09:23 +08:00
|
|
|
|
onResetFilter() {
|
|
|
|
|
|
const applied: AppliedFilter = {
|
|
|
|
|
|
keyword: '',
|
|
|
|
|
|
regionCode: '',
|
|
|
|
|
|
minAge: DEFAULT_MIN_AGE,
|
|
|
|
|
|
maxAge: DEFAULT_MAX_AGE,
|
|
|
|
|
|
educationCode: '',
|
|
|
|
|
|
maritalStatus: '',
|
|
|
|
|
|
};
|
|
|
|
|
|
this.setData({
|
|
|
|
|
|
keywordInput: '',
|
|
|
|
|
|
tempRegionCode: '',
|
|
|
|
|
|
tempMinAge: DEFAULT_MIN_AGE,
|
|
|
|
|
|
tempMaxAge: DEFAULT_MAX_AGE,
|
|
|
|
|
|
tempEducationCode: '',
|
|
|
|
|
|
tempMaritalStatus: '',
|
|
|
|
|
|
applied,
|
|
|
|
|
|
hasActiveFilter: false,
|
|
|
|
|
|
showFilter: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
this.fetchDiscover(true);
|
2026-09-09 22:05:00 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-14 11:09:23 +08:00
|
|
|
|
// ========== 卡片交互 ==========
|
2026-09-09 22:05:00 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|