This commit is contained in:
Austin 2026-07-15 15:54:56 +08:00
parent 9d8b85987b
commit d61d8e67a1
5 changed files with 117 additions and 22 deletions

View File

@ -1,6 +1,6 @@
import { APP_CONFIG } from '@/config'; import { APP_CONFIG } from '@/config';
import { HttpStatusError, httpRequest, toFormUrlEncoded } from '@/utils/http'; import { HttpStatusError, httpRequest, toFormUrlEncoded } from '@/utils/http';
import { postForm } from '@/utils/request'; import { get, postForm } from '@/utils/request';
export interface LoginResponse { export interface LoginResponse {
code: number | string; code: number | string;
@ -58,6 +58,8 @@ export interface BindMobileResponse {
code: number | string; code: number | string;
msg?: string; msg?: string;
data?: { data?: {
token?: string;
expires_in?: number;
code?: string; code?: string;
msg?: string; msg?: string;
mobile?: string; mobile?: string;
@ -76,3 +78,22 @@ export const bindMobile = (encryptedData: string) => {
platform: resolveLoginPlatform() platform: resolveLoginPlatform()
}); });
}; };
export interface OpenInfoResult {
code: number | string;
msg?: string;
data?: {
user?: {
user_id?: number | string;
user_name?: string;
avatar?: string;
};
open?: {
user_id?: number | string;
open_id?: number | string;
};
};
}
/** 当前登录用户信息;有 user_id 视为已绑定过手机号/账号资料 */
export const fetchOpenInfo = () => get<OpenInfoResult>('user/v1/open.info');

View File

@ -1,17 +1,20 @@
// #ifdef MP-ALIPAY // #ifdef MP-ALIPAY
const APP_ID = '123019'; // const APP_ID = '123019';
const APP_ID = '12317';
// #endif // #endif
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
const APP_ID = '123018'; // const APP_ID = '123018';
const APP_ID = '12316';
// #endif // #endif
// #ifndef MP-WEIXIN // #ifndef MP-WEIXIN
// #ifndef MP-ALIPAY // #ifndef MP-ALIPAY
const APP_ID = '123018'; // const APP_ID = '123018';
const APP_ID = '12316';
// #endif // #endif
// #endif // #endif
export const APP_CONFIG = { export const APP_CONFIG = {
API_BASE_URL: 'https://dev.api.leapy.cn', API_BASE_URL: 'https://api.leapy.cn',
APP_ID, APP_ID,
RESOURCE_URL: 'https://resource.leapy.cn/staff/', RESOURCE_URL: 'https://resource.leapy.cn/staff/',
PROPERTY_IMG_URL: 'https://resource.leapy.cn/', PROPERTY_IMG_URL: 'https://resource.leapy.cn/',

View File

@ -22,13 +22,14 @@
</view> </view>
<view v-if="records.length && (loading || finished)" class="records-footer"> <view v-if="records.length && (loading || finished)" class="records-footer">
<wd-loadmore :state="loadMoreState" /> <wd-loadmore v-if="loading" state="loading" />
<text v-else class="records-finished">没有更多了</text>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { ref } from 'vue';
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'; import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app';
import { platformScoreRecord } from '@/api/cashier'; import { platformScoreRecord } from '@/api/cashier';
import type { ScoreRecordItem } from '@/api/cashier'; import type { ScoreRecordItem } from '@/api/cashier';
@ -42,7 +43,6 @@ const loading = ref(false);
const finished = ref(false); const finished = ref(false);
const emptyImage = `${APP_CONFIG.RESOURCE_URL}tip/message.png`; const emptyImage = `${APP_CONFIG.RESOURCE_URL}tip/message.png`;
const loadMoreState = computed(() => (loading.value ? 'loading' : 'finished'));
const formatMoney = (value?: string | number) => { const formatMoney = (value?: string | number) => {
const amount = Number(value || 0); const amount = Number(value || 0);
@ -198,4 +198,12 @@ onReachBottom(() => {
.records-footer { .records-footer {
padding: 12rpx 0 24rpx; padding: 12rpx 0 24rpx;
} }
.records-finished {
display: block;
padding: 8rpx 0;
color: #9ca3af;
font-size: 24rpx;
text-align: center;
}
</style> </style>

View File

@ -35,7 +35,7 @@
<view class="merchant-label"> <view class="merchant-label">
<view class="merchant-label-inner"> <view class="merchant-label-inner">
<text class="muted"></text> <text class="muted"></text>
<text class="merchant-name">{{ merchant.name }}</text> <text class="merchant-name" :style="{ fontSize: merchantNameFontSize }">{{ merchant.name }}</text>
<text class="muted">付款</text> <text class="muted">付款</text>
</view> </view>
</view> </view>
@ -88,7 +88,6 @@
<view <view
v-else v-else
class="property-row" class="property-row"
:class="{ 'property-row--disabled': alipayPropertyUnbound }"
hover-class="property-row-hover" hover-class="property-row-hover"
@tap.stop="handlePropertyRow" @tap.stop="handlePropertyRow"
> >
@ -189,7 +188,7 @@ import { computed, ref } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app'; import { onLoad, onShow } from '@dcloudio/uni-app';
import { bindMerchantByUrl, cancelTrade, changeMerchant, merchantListByUrl, supplierByUrl } from '@/api/cashier'; import { bindMerchantByUrl, cancelTrade, changeMerchant, merchantListByUrl, supplierByUrl } from '@/api/cashier';
import type { MerchantListItem } from '@/api/cashier'; import type { MerchantListItem } from '@/api/cashier';
import { bindMobile, resolveLoginPlatform } from '@/api/auth'; import { bindMobile, fetchOpenInfo, resolveLoginPlatform } from '@/api/auth';
import { postForm } from '@/utils/request'; import { postForm } from '@/utils/request';
import { DEFAULT_MERCHANT, APP_CONFIG, PROPERTY_APP } from '@/config'; import { DEFAULT_MERCHANT, APP_CONFIG, PROPERTY_APP } from '@/config';
import { import {
@ -203,7 +202,7 @@ import {
resolveMerchantUrlFromScanResult, resolveMerchantUrlFromScanResult,
syncActiveMerchantContext syncActiveMerchantContext
} from '@/utils/merchant-context'; } from '@/utils/merchant-context';
import { encryptedStorage } from '@/utils/storage'; import { authStorage, encryptedStorage } from '@/utils/storage';
import { formatPaymentError, isPaymentCancelled, requestPlatformPayment } from '@/utils/wxpay'; import { formatPaymentError, isPaymentCancelled, requestPlatformPayment } from '@/utils/wxpay';
interface MerchantInfo { interface MerchantInfo {
@ -247,11 +246,22 @@ const accumulationAmount = computed(() => {
}); });
const showBenefitTip = computed(() => Number(merchant.value.accumulationRate) > 0); const showBenefitTip = computed(() => Number(merchant.value.accumulationRate) > 0);
const benefitTitle = computed(() => `预计获得${scoreName.value || '物业公积金'}`); const benefitTitle = computed(() => `预计获得${scoreName.value || '物业公积金'}`);
/** 商户名越长字号越小,尽量单行展示「向 xxx 付款」 */
const merchantNameFontSize = computed(() => {
const len = String(merchant.value.name || '').trim().length;
if (len <= 6) return '34rpx';
if (len <= 10) return '30rpx';
if (len <= 14) return '26rpx';
if (len <= 18) return '24rpx';
return '22rpx';
});
const propertyRowText = computed(() => { const propertyRowText = computed(() => {
if (propertyName.value) return propertyName.value; if (propertyName.value) return propertyName.value;
if (propertyList.value.length) return '选择物业'; if (propertyList.value.length) return '选择物业';
if (IS_ALIPAY) { if (IS_ALIPAY) {
if (alipayPropertyUnbound.value) return '暂未绑定,请去微信小程序绑定'; if (alipayPropertyUnbound.value) {
return `未绑定,微信搜「${PROPERTY_APP.WECHAT_NAME}」绑定房源`;
}
return '绑定手机号查看房源'; return '绑定手机号查看房源';
} }
return '业主去绑定'; return '业主去绑定';
@ -260,7 +270,7 @@ const isPropertyActionText = computed(() => !propertyName.value);
const showAlipayPhoneAuthBtn = computed( const showAlipayPhoneAuthBtn = computed(
() => IS_ALIPAY && !propertyName.value && !propertyList.value.length && !alipayPropertyUnbound.value () => IS_ALIPAY && !propertyName.value && !propertyList.value.length && !alipayPropertyUnbound.value
); );
const showPropertyArrow = computed(() => !IS_ALIPAY || !alipayPropertyUnbound.value); const showPropertyArrow = computed(() => true);
const propertySheetTitle = computed(() => (propertyName.value ? '切换物业' : '选择物业')); const propertySheetTitle = computed(() => (propertyName.value ? '切换物业' : '选择物业'));
interface PropertyBindingCache { interface PropertyBindingCache {
@ -582,6 +592,32 @@ const resolvePropertyListFallback = async (merchantUrl: string) => {
} }
}; };
/**
* 支付宝无物业名/列表时 open.info user_id 判断是否已绑过手机号
* user_id 显示未绑定微信搜乐享云绑定房源否则 绑定手机号查看房源
*/
const resolveAlipayEmptyPropertyState = async (options: { assumeBoundIfEmpty?: boolean } = {}) => {
if (!IS_ALIPAY) return;
if (propertyName.value || propertyList.value.length) {
alipayPropertyUnbound.value = false;
return;
}
try {
const response = await fetchOpenInfo();
const userId = response?.data?.user?.user_id ?? response?.data?.open?.user_id;
if (userId != null && String(userId).trim() !== '') {
alipayPropertyUnbound.value = true;
return;
}
} catch (error) {
console.log('open.info empty property state', error);
}
alipayPropertyUnbound.value = !!options.assumeBoundIfEmpty;
};
const loadPropertyBinding = async (merchantUrl: string, options: { skipFallback?: boolean } = {}) => { const loadPropertyBinding = async (merchantUrl: string, options: { skipFallback?: boolean } = {}) => {
const cacheKey = syncBindingCacheForUrl(merchantUrl); const cacheKey = syncBindingCacheForUrl(merchantUrl);
const cached = encryptedStorage.get<PropertyBindingCache>(cacheKey); const cached = encryptedStorage.get<PropertyBindingCache>(cacheKey);
@ -595,17 +631,20 @@ const loadPropertyBinding = async (merchantUrl: string, options: { skipFallback?
if (!options.skipFallback) { if (!options.skipFallback) {
await resolvePropertyListFallback(merchantUrl); await resolvePropertyListFallback(merchantUrl);
} }
await resolveAlipayEmptyPropertyState();
return; return;
} }
const latestBinding = buildBindingCache(response.data || {}); const latestBinding = buildBindingCache(response.data || {});
applyBindingInfo(latestBinding); applyBindingInfo(latestBinding);
encryptedStorage.set(cacheKey, latestBinding, PROPERTY_CACHE_TTL_SECONDS); encryptedStorage.set(cacheKey, latestBinding, PROPERTY_CACHE_TTL_SECONDS);
await resolveAlipayEmptyPropertyState();
} catch (error) { } catch (error) {
console.log('bind merchant fallback cache', error); console.log('bind merchant fallback cache', error);
if (!options.skipFallback) { if (!options.skipFallback) {
await resolvePropertyListFallback(merchantUrl); await resolvePropertyListFallback(merchantUrl);
} }
await resolveAlipayEmptyPropertyState();
} }
}; };
@ -654,7 +693,7 @@ const deleteKey = () => {
const showAlipayUnboundTip = () => { const showAlipayUnboundTip = () => {
uni.showModal({ uni.showModal({
title: '暂未绑定房源', title: '暂未绑定房源',
content: '您在微信小程序中暂无绑定房源,请前往微信小程序完成绑定后再使用。', content: `请打开微信,搜索并进入「${PROPERTY_APP.WECHAT_NAME}」小程序,完成房源绑定后消费即可获得物业基金。`,
showCancel: false, showCancel: false,
confirmText: '知道了' confirmText: '知道了'
}); });
@ -668,16 +707,17 @@ const refreshPropertyAfterPhoneBind = async () => {
propertyList.value = []; propertyList.value = [];
encryptedStorage.remove(getPropertyBindingCacheKey(activeMerchantUrl.value)); encryptedStorage.remove(getPropertyBindingCacheKey(activeMerchantUrl.value));
await loadPropertyBinding(activeMerchantUrl.value); await loadPropertyBinding(activeMerchantUrl.value);
// open.info user_id
if (!propertyName.value && !propertyList.value.length && !alipayPropertyUnbound.value) {
await resolveAlipayEmptyPropertyState({ assumeBoundIfEmpty: true });
}
if (propertyName.value || propertyList.value.length) { if (propertyName.value || propertyList.value.length) {
uni.showToast({ uni.showToast({
title: '已匹配到物业信息', title: '已匹配到物业信息',
icon: 'none' icon: 'none'
}); });
return;
} }
alipayPropertyUnbound.value = true;
}; };
const tryParseJson = (value: any) => { const tryParseJson = (value: any) => {
@ -775,6 +815,13 @@ const handleAlipayPhoneAuthorize = async (event?: any) => {
throw new Error(response?.msg || '手机号绑定失败'); throw new Error(response?.msg || '手机号绑定失败');
} }
const nextToken = String(response?.data?.token || '').trim();
if (!nextToken) {
throw new Error('绑定成功但未返回 token');
}
authStorage.setToken(nextToken, Number(response?.data?.expires_in || 0));
logPhoneAuth('已替换登录 token', { token: nextToken });
await refreshPropertyAfterPhoneBind(); await refreshPropertyAfterPhoneBind();
} catch (error: any) { } catch (error: any) {
logPhoneAuth('授权失败', error); logPhoneAuth('授权失败', error);
@ -1232,6 +1279,7 @@ onShow(() => {
.merchant-label-inner { .merchant-label-inner {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
flex-wrap: nowrap;
max-width: 88%; max-width: 88%;
padding: 14rpx 32rpx; padding: 14rpx 32rpx;
border-radius: 999rpx; border-radius: 999rpx;
@ -1239,16 +1287,22 @@ onShow(() => {
} }
.muted { .muted {
flex-shrink: 0;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
font-size: 28rpx;
} }
.merchant-name { .merchant-name {
min-width: 0;
margin: 0 8rpx; margin: 0 8rpx;
color: #FFFFFF; color: #FFFFFF;
font-size: 34rpx; font-size: 34rpx;
font-weight: 800; font-weight: 800;
line-height: 1.3; line-height: 1.3;
text-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.35); text-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.35);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
.amount-zone { .amount-zone {
@ -1388,6 +1442,8 @@ onShow(() => {
.property-name-tip { .property-name-tip {
color: #8B929E; color: #8B929E;
font-weight: 500; font-weight: 500;
text-decoration: underline;
text-underline-offset: 4rpx;
} }
.property-arrow { .property-arrow {

View File

@ -33,13 +33,14 @@
</view> </view>
<view v-if="records.length && (loading || finished)" class="records-footer"> <view v-if="records.length && (loading || finished)" class="records-footer">
<wd-loadmore :state="loadMoreState" /> <wd-loadmore v-if="loading" state="loading" />
<text v-else class="records-finished">没有更多了</text>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { ref } from 'vue';
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'; import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app';
import { tradeList } from '@/api/cashier'; import { tradeList } from '@/api/cashier';
import type { TradeListItem } from '@/api/cashier'; import type { TradeListItem } from '@/api/cashier';
@ -54,8 +55,6 @@ const finished = ref(false);
const emptyImage = `${APP_CONFIG.RESOURCE_URL}tip/message.png`; const emptyImage = `${APP_CONFIG.RESOURCE_URL}tip/message.png`;
const loadMoreState = computed(() => (loading.value ? 'loading' : 'finished'));
const formatMoney = (value?: string | number) => { const formatMoney = (value?: string | number) => {
const amount = Number(value || 0); const amount = Number(value || 0);
if (!Number.isFinite(amount)) return '0.00'; if (!Number.isFinite(amount)) return '0.00';
@ -262,4 +261,12 @@ onReachBottom(() => {
.records-footer { .records-footer {
padding: 12rpx 0 24rpx; padding: 12rpx 0 24rpx;
} }
.records-finished {
display: block;
padding: 8rpx 0;
color: #9ca3af;
font-size: 24rpx;
text-align: center;
}
</style> </style>