Merge pull request 'Dev jacy # 36' (#6) from dev_jacy into dev
Reviewed-on: https://git.yqidian.com/jacy/dukang/pulls/6
This commit was merged in pull request #6.
This commit is contained in:
@@ -63,11 +63,17 @@ export default defineConfig(async () => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
h5: {
|
h5: {
|
||||||
|
// 生产发版由 remote-release 注入 TARO_H5_PUBLIC_PATH=/user/、TARO_H5_ROUTER_BASENAME=/user
|
||||||
|
// 本地预览勿强制 basename,否则静态托管下易白屏
|
||||||
publicPath: process.env.TARO_H5_PUBLIC_PATH || '/',
|
publicPath: process.env.TARO_H5_PUBLIC_PATH || '/',
|
||||||
|
...(process.env.TARO_H5_ROUTER_BASENAME
|
||||||
|
? {
|
||||||
router: {
|
router: {
|
||||||
mode: 'browser',
|
mode: 'browser' as const,
|
||||||
basename: process.env.TARO_H5_ROUTER_BASENAME || '/',
|
basename: process.env.TARO_H5_ROUTER_BASENAME,
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
staticDirectory: 'static',
|
staticDirectory: 'static',
|
||||||
devServer: {
|
devServer: {
|
||||||
port: 5177,
|
port: 5177,
|
||||||
|
|||||||
@@ -19,3 +19,34 @@ body {
|
|||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 全局隐藏滚动条(H5 + 小程序均可滚动,仅隐藏轨道) */
|
||||||
|
html,
|
||||||
|
page,
|
||||||
|
body,
|
||||||
|
#app,
|
||||||
|
.taro_page,
|
||||||
|
.taro_router,
|
||||||
|
.taro-tabbar__panel {
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* legacy Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar,
|
||||||
|
page::-webkit-scrollbar,
|
||||||
|
body::-webkit-scrollbar,
|
||||||
|
#app::-webkit-scrollbar,
|
||||||
|
.taro_page::-webkit-scrollbar,
|
||||||
|
.taro_router::-webkit-scrollbar,
|
||||||
|
.taro-tabbar__panel::-webkit-scrollbar,
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
display: none !important;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import './lib/text-encoding-polyfill';
|
import './lib/text-encoding-polyfill';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
import WechatShareBootstrap from './components/WechatShareBootstrap';
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
function App({ children }: PropsWithChildren) {
|
function App({ children }: PropsWithChildren) {
|
||||||
return children;
|
return (
|
||||||
|
<>
|
||||||
|
<WechatShareBootstrap />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export default function PageNavBar({
|
|||||||
) : (
|
) : (
|
||||||
<View className="page-nav-bar__btn page-nav-bar__btn--back page-nav-bar__btn--placeholder" />
|
<View className="page-nav-bar__btn page-nav-bar__btn--back page-nav-bar__btn--placeholder" />
|
||||||
)}
|
)}
|
||||||
{right ?? (
|
{right ? (
|
||||||
|
<View className="page-nav-bar__right-slot">{right}</View>
|
||||||
|
) : (
|
||||||
<View className="page-nav-bar__btn page-nav-bar__btn--right page-nav-bar__btn--placeholder" />
|
<View className="page-nav-bar__btn page-nav-bar__btn--right page-nav-bar__btn--placeholder" />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export default function RegionPicker({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView className="region-picker-list" scrollY>
|
<ScrollView className="region-picker-list" scrollY showScrollbar={false}>
|
||||||
{listItems.map((item) => (
|
{listItems.map((item) => (
|
||||||
<View
|
<View
|
||||||
key={item}
|
key={item}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Button, Text, View } from '@tarojs/components';
|
||||||
|
import { handleShareButtonClick, type PageSharePayload } from '../lib/wechat-share';
|
||||||
|
|
||||||
|
type ShareNavButtonProps = {
|
||||||
|
payload?: PageSharePayload;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶栏分享:
|
||||||
|
* - 小程序:open-type=share 弹出微信分享面板
|
||||||
|
* - H5 微信:JSSDK 配置后提示点右上角 ···
|
||||||
|
*/
|
||||||
|
export default function ShareNavButton({ payload }: ShareNavButtonProps) {
|
||||||
|
if (process.env.TARO_ENV === 'weapp') {
|
||||||
|
return (
|
||||||
|
<Button className="page-nav-bar__btn page-nav-bar__share-btn" openType="share" hoverClass="none">
|
||||||
|
<Text className="page-nav-bar__icon page-nav-bar__icon--share">⤴</Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
className="page-nav-bar__btn"
|
||||||
|
onClick={() => {
|
||||||
|
void handleShareButtonClick(payload);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className="page-nav-bar__icon page-nav-bar__icon--share">⤴</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import Taro, { useDidShow } from '@tarojs/taro';
|
||||||
|
import { captureIosJssdkEntryUrl } from '@dukang/weixin-sdk';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { finishLoginNavigate, goLogin } from '../lib/auth-nav';
|
||||||
|
import { toast } from '../lib/api';
|
||||||
|
import { saveWechatLoginResult } from '../lib/pay-wechat';
|
||||||
|
import { applyWechatShare } from '../lib/wechat-share';
|
||||||
|
import { handleWechatAuthCallback } from '../lib/wechat-auth';
|
||||||
|
import { isWechatEnv } from '../lib/weixin';
|
||||||
|
|
||||||
|
function currentPagePathWithQuery(): string {
|
||||||
|
const pages = Taro.getCurrentPages();
|
||||||
|
const cur = pages[pages.length - 1] as
|
||||||
|
| { route?: string; options?: Record<string, string | undefined> }
|
||||||
|
| undefined;
|
||||||
|
if (!cur?.route) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return `${window.location.pathname}${window.location.search}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const path = cur.route.startsWith('/') ? cur.route : `/${cur.route}`;
|
||||||
|
const opts = cur.options ?? {};
|
||||||
|
const qs = Object.entries(opts)
|
||||||
|
.filter(([k, v]) => v != null && v !== '' && k !== 'code' && k !== 'state')
|
||||||
|
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
||||||
|
.join('&');
|
||||||
|
return qs ? `${path}?${qs}` : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H5 全局:
|
||||||
|
* 1. 默认分享卡片
|
||||||
|
* 2. 公众号 OAuth ?code= 统一回调(避免各页重复消费 code)
|
||||||
|
*/
|
||||||
|
export default function WechatShareBootstrap() {
|
||||||
|
const handlingCode = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
|
captureIosJssdkEntryUrl();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
if (process.env.TARO_ENV !== 'h5') return;
|
||||||
|
void applyWechatShare().catch(() => {});
|
||||||
|
|
||||||
|
if (!isWechatEnv()) return;
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (!params.get('code')) return;
|
||||||
|
if (handlingCode.current) return;
|
||||||
|
handlingCode.current = true;
|
||||||
|
|
||||||
|
const returnFromLogin = (() => {
|
||||||
|
const path = currentPagePathWithQuery();
|
||||||
|
if (path.includes('/pages/login/')) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(params.get('return') || '') || undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})();
|
||||||
|
|
||||||
|
handleWechatAuthCallback()
|
||||||
|
.then((result) => {
|
||||||
|
if (!result) return;
|
||||||
|
if (result.needBindPhone && result.wxSessionKey) {
|
||||||
|
goLogin(returnFromLogin, {
|
||||||
|
bindMode: '1',
|
||||||
|
wxSessionKey: result.wxSessionKey,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveWechatLoginResult(result)) {
|
||||||
|
toast('微信授权成功', 'success');
|
||||||
|
if (returnFromLogin !== undefined || currentPagePathWithQuery().includes('/pages/login/')) {
|
||||||
|
finishLoginNavigate(returnFromLogin || params.get('return') || undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast(e instanceof Error ? e.message : '微信授权失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
handlingCode.current = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import Taro, { useDidShow } from '@tarojs/taro';
|
||||||
|
import { captureIosJssdkEntryUrl } from '@dukang/weixin-sdk';
|
||||||
|
import { applyWechatShare, type PageSharePayload } from '../lib/wechat-share';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H5:进入页面时刷新微信分享卡片;
|
||||||
|
* 小程序:开启右上角分享菜单。
|
||||||
|
*/
|
||||||
|
export default function WechatShareReady({ payload }: { payload?: PageSharePayload }) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
|
captureIosJssdkEntryUrl();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
if (process.env.TARO_ENV === 'weapp') {
|
||||||
|
void Taro.showShareMenu({
|
||||||
|
withShareTicket: true,
|
||||||
|
showShareItems: ['shareAppMessage', 'shareTimeline'],
|
||||||
|
}).catch(() => {
|
||||||
|
void Taro.showShareMenu({ withShareTicket: true }).catch(() => {});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void applyWechatShare({
|
||||||
|
title: payload?.title,
|
||||||
|
desc: payload?.desc,
|
||||||
|
imgUrl: payload?.imgUrl,
|
||||||
|
link: payload?.link,
|
||||||
|
}).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (process.env.TARO_ENV !== 'h5') return;
|
||||||
|
void applyWechatShare({
|
||||||
|
title: payload?.title,
|
||||||
|
desc: payload?.desc,
|
||||||
|
imgUrl: payload?.imgUrl,
|
||||||
|
link: payload?.link,
|
||||||
|
}).catch(() => {});
|
||||||
|
}, [payload?.title, payload?.desc, payload?.imgUrl, payload?.link]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { weixinSdk } from './weixin';
|
||||||
|
|
||||||
|
export type ClientGpsLocation = {
|
||||||
|
province?: string;
|
||||||
|
city?: string;
|
||||||
|
district?: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
address?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 尝试获取客户端 GPS(微信 JSSDK / 浏览器 Geolocation),失败返回 null 不阻塞下单 */
|
||||||
|
export async function tryGetClientGpsLocation(): Promise<ClientGpsLocation | null> {
|
||||||
|
if (process.env.TARO_ENV === 'weapp') {
|
||||||
|
try {
|
||||||
|
const Taro = (await import('@tarojs/taro')).default;
|
||||||
|
const loc = await new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
|
||||||
|
Taro.getLocation({
|
||||||
|
type: 'gcj02',
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { latitude: loc.latitude, longitude: loc.longitude };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loc = await weixinSdk.getLocation();
|
||||||
|
if (!loc) return null;
|
||||||
|
return {
|
||||||
|
latitude: loc.latitude,
|
||||||
|
longitude: loc.longitude,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@ const H5_FALLBACK: NavBarMetrics = {
|
|||||||
statusBarHeight: 0,
|
statusBarHeight: 0,
|
||||||
navBarHeight: 56,
|
navBarHeight: 56,
|
||||||
navContentHeight: 56,
|
navContentHeight: 56,
|
||||||
navBarPaddingRight: 16,
|
/** 避开微信内置浏览器右上角 ··· / 设置入口(约一颗胶囊宽) */
|
||||||
|
navBarPaddingRight: 96,
|
||||||
navBarPaddingLeft: 16,
|
navBarPaddingLeft: 16,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,7 +34,13 @@ const WEAPP_FALLBACK: NavBarMetrics = {
|
|||||||
/** 计算小程序自定义导航栏尺寸(对齐微信胶囊按钮) */
|
/** 计算小程序自定义导航栏尺寸(对齐微信胶囊按钮) */
|
||||||
export function getNavBarMetrics(): NavBarMetrics {
|
export function getNavBarMetrics(): NavBarMetrics {
|
||||||
if (process.env.TARO_ENV === 'h5') {
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
return H5_FALLBACK;
|
// H5:在微信浏览器内额外避让右上角菜单;非微信保持较小右侧留白
|
||||||
|
const inWechat =
|
||||||
|
typeof navigator !== 'undefined' && /MicroMessenger/i.test(navigator.userAgent || '');
|
||||||
|
return {
|
||||||
|
...H5_FALLBACK,
|
||||||
|
navBarPaddingRight: inWechat ? 96 : 16,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { goLogin } from './auth-nav';
|
import { goLogin } from './auth-nav';
|
||||||
import { isLoggedIn } from './api';
|
import { isLoggedIn } from './api';
|
||||||
|
import { ensureWechatAuthForPay } from './wechat-auth';
|
||||||
import { fetchClientConfig, fetchUserProfile, needsWechatAuthForPay } from './pay-wechat';
|
import { fetchClientConfig, fetchUserProfile, needsWechatAuthForPay } from './pay-wechat';
|
||||||
|
|
||||||
/** 支付前门禁:未登录/未验手机/未绑微信时跳转登录页 */
|
/**
|
||||||
|
* 支付前门禁:
|
||||||
|
* - 未登录 / 未验手机 → 跳转登录页
|
||||||
|
* - H5 微信内缺 openId → 尝试 OAuth(可能跳转微信授权页)
|
||||||
|
* - 小程序缺绑定 → 跳转登录页 needWechat
|
||||||
|
*/
|
||||||
export async function ensurePayReady(returnPath: string): Promise<boolean> {
|
export async function ensurePayReady(returnPath: string): Promise<boolean> {
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
goLogin(returnPath);
|
goLogin(returnPath);
|
||||||
@@ -15,11 +21,23 @@ export async function ensurePayReady(returnPath: string): Promise<boolean> {
|
|||||||
goLogin(returnPath, { needPhone: '1' });
|
goLogin(returnPath, { needPhone: '1' });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (needsWechatAuthForPay(config, profile)) {
|
if (!needsWechatAuthForPay(config, profile)) {
|
||||||
goLogin(returnPath, { needWechat: '1' });
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
|
const auth = await ensureWechatAuthForPay();
|
||||||
|
if (auth.ok) return true;
|
||||||
|
if ('needBindPhone' in auth && auth.needBindPhone) {
|
||||||
|
goLogin(returnPath, { bindMode: '1', wxSessionKey: auth.wxSessionKey });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
// redirecting:正在跳转微信 OAuth
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
goLogin(returnPath, { needWechat: '1' });
|
||||||
|
return false;
|
||||||
} catch {
|
} catch {
|
||||||
goLogin(returnPath);
|
goLogin(returnPath);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { ClientRuntimeConfig, WechatJsapiPrepayParams, WechatLoginResult, WechatPayOrderResult } from '@dukang/shared-types';
|
import type {
|
||||||
|
ClientRuntimeConfig,
|
||||||
|
WechatJsapiPrepayParams,
|
||||||
|
WechatLoginResult,
|
||||||
|
WechatPayOrderResult,
|
||||||
|
} from '@dukang/shared-types';
|
||||||
import { WECHAT_AUTH_REQUIRED, isWxAuthorizeEnabled } from '@dukang/shared-types';
|
import { WECHAT_AUTH_REQUIRED, isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
import { invokeWechatPay } from '@dukang/weixin-sdk';
|
import { invokeWechatPay } from '@dukang/weixin-sdk';
|
||||||
import Taro from '@tarojs/taro';
|
import { request, saveAuth, type UserProfile } from './api';
|
||||||
import { request, type UserProfile } from './api';
|
import { isWechatEnv, weixinSdk } from './weixin';
|
||||||
import { syncMiniWechatProfile } from './mini-wechat-profile';
|
|
||||||
|
|
||||||
export function isMiniWechatEnv(): boolean {
|
export function isMiniWechatEnv(): boolean {
|
||||||
return process.env.TARO_ENV === 'weapp';
|
return process.env.TARO_ENV === 'weapp';
|
||||||
@@ -21,13 +25,35 @@ export async function fetchUserProfile(): Promise<UserProfile> {
|
|||||||
return request<UserProfile>('/auth/me');
|
return request<UserProfile>('/auth/me');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 真实微信支付且未绑定微信时需要授权 */
|
/** 真实微信支付且未绑定微信时需要授权(小程序 / H5 微信内) */
|
||||||
export function needsWechatAuthForPay(
|
export function needsWechatAuthForPay(
|
||||||
config: ClientRuntimeConfig,
|
config: ClientRuntimeConfig,
|
||||||
profile: UserProfile | null,
|
profile: UserProfile | null,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!isWxAuthorizeEnabled(config)) return false;
|
if (!isWxAuthorizeEnabled(config)) return false;
|
||||||
return !config.mockPay && config.wechatPayEnabled && isMiniWechatEnv() && !profile?.hasWechat;
|
return !config.mockPay && config.wechatPayEnabled && isWechatEnv() && !profile?.hasWechat;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveWechatLoginResult(result: WechatLoginResult): boolean {
|
||||||
|
if (!result.accessToken) return false;
|
||||||
|
saveAuth({
|
||||||
|
accessToken: result.accessToken,
|
||||||
|
refreshToken: result.refreshToken,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** H5:发起公众号 OAuth(可能直接跳转);小程序请用 bindWechatForUser */
|
||||||
|
export async function authorizeWechatForPay(): Promise<WechatLoginResult | void> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return;
|
||||||
|
if (!isWechatEnv()) {
|
||||||
|
throw new Error('请在微信内打开以完成授权');
|
||||||
|
}
|
||||||
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
|
return weixinSdk.login();
|
||||||
|
}
|
||||||
|
throw new Error('请使用小程序微信授权');
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
@@ -49,9 +75,12 @@ export async function payOrder(orderId: string): Promise<'paid' | 'pending'> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.mode === 'jsapi' && result.prepay) {
|
if (result.mode === 'jsapi' && result.prepay) {
|
||||||
await invokeWechatPay(result.prepay as WechatJsapiPrepayParams, {
|
const prepay = result.prepay as WechatJsapiPrepayParams;
|
||||||
platform: isMiniWechatEnv() ? 'mini' : undefined,
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
});
|
await weixinSdk.pay(prepay);
|
||||||
|
} else {
|
||||||
|
await invokeWechatPay(prepay, { platform: 'mini' });
|
||||||
|
}
|
||||||
const paid = await waitOrderPaid(orderId);
|
const paid = await waitOrderPaid(orderId);
|
||||||
return paid ? 'paid' : 'pending';
|
return paid ? 'paid' : 'pending';
|
||||||
}
|
}
|
||||||
@@ -61,23 +90,5 @@ export async function payOrder(orderId: string): Promise<'paid' | 'pending'> {
|
|||||||
|
|
||||||
export type WechatBindResult =
|
export type WechatBindResult =
|
||||||
| { ok: true; profile?: UserProfile }
|
| { ok: true; profile?: UserProfile }
|
||||||
| { ok: false; needBindPhone: true; wxSessionKey: string };
|
| { ok: false; needBindPhone: true; wxSessionKey: string }
|
||||||
|
| { ok: false; redirecting: true };
|
||||||
export async function bindWechatForUser(
|
|
||||||
prefetchedWxProfile?: { nickname?: string; avatarUrl?: string } | null,
|
|
||||||
): Promise<WechatBindResult> {
|
|
||||||
const res = await Taro.login();
|
|
||||||
if (!res.code) {
|
|
||||||
throw new Error(res.errMsg || '微信授权失败');
|
|
||||||
}
|
|
||||||
const data = await request<WechatLoginResult>('/auth/wechat/bind', {
|
|
||||||
method: 'POST',
|
|
||||||
data: { code: res.code, platform: 'mini' },
|
|
||||||
});
|
|
||||||
if (data.needBindPhone && data.wxSessionKey) {
|
|
||||||
return { ok: false, needBindPhone: true, wxSessionKey: data.wxSessionKey };
|
|
||||||
}
|
|
||||||
await syncMiniWechatProfile(prefetchedWxProfile);
|
|
||||||
const profile = await fetchUserProfile();
|
|
||||||
return { ok: true, profile };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { request } from './api';
|
import { getWechatLocationDetailed } from '@dukang/weixin-sdk';
|
||||||
|
import { API_BASE, getToken, request } from './api';
|
||||||
import { DEFAULT_REGION, regionFromGeo, type RegionSelection } from './region-data';
|
import { DEFAULT_REGION, regionFromGeo, type RegionSelection } from './region-data';
|
||||||
import { FALLBACK_CITY_CODE } from './product-images';
|
import { FALLBACK_CITY_CODE } from './product-images';
|
||||||
|
import { ClientApp } from '@dukang/shared-types';
|
||||||
|
|
||||||
export const GPS_CITY_STORAGE_KEY = 'dukang_gps_city';
|
export const GPS_CITY_STORAGE_KEY = 'dukang_gps_city';
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ async function promptLocationAuth() {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
function getMiniLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Taro.getLocation({
|
Taro.getLocation({
|
||||||
type: 'gcj02',
|
type: 'gcj02',
|
||||||
@@ -118,6 +120,37 @@ function getLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveViaH5Jssdk(): Promise<ResolvedUserCity | null> {
|
||||||
|
const outcome = await getWechatLocationDetailed({
|
||||||
|
apiBase: API_BASE,
|
||||||
|
clientApp: ClientApp.USER_H5,
|
||||||
|
getAccessToken: () => getToken() || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!outcome.location) {
|
||||||
|
await reportLocationToServer({
|
||||||
|
sdk: outcome.sdk,
|
||||||
|
status: 'fail',
|
||||||
|
errMsg: outcome.errMsg,
|
||||||
|
}).catch(() => {});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await reportLocationToServer({
|
||||||
|
latitude: outcome.location.latitude,
|
||||||
|
longitude: outcome.location.longitude,
|
||||||
|
sdk: outcome.sdk,
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
const resolved = toResolved(data);
|
||||||
|
if (resolved) writeCache(resolved);
|
||||||
|
return resolved;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取并解析用户当前城市;失败返回郑州市兜底 */
|
/** 获取并解析用户当前城市;失败返回郑州市兜底 */
|
||||||
export async function resolveUserCity(force = false): Promise<ResolvedUserCity> {
|
export async function resolveUserCity(force = false): Promise<ResolvedUserCity> {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
@@ -125,12 +158,17 @@ export async function resolveUserCity(force = false): Promise<ResolvedUserCity>
|
|||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
|
const fromJssdk = await resolveViaH5Jssdk();
|
||||||
|
return fromJssdk ?? FALLBACK_CITY;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.TARO_ENV !== 'weapp') {
|
if (process.env.TARO_ENV !== 'weapp') {
|
||||||
return FALLBACK_CITY;
|
return FALLBACK_CITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loc = await getLocation();
|
const loc = await getMiniLocation();
|
||||||
const data = await reportLocationToServer({
|
const data = await reportLocationToServer({
|
||||||
latitude: loc.latitude,
|
latitude: loc.latitude,
|
||||||
longitude: loc.longitude,
|
longitude: loc.longitude,
|
||||||
|
|||||||
@@ -1,17 +1,124 @@
|
|||||||
import type { WechatLoginResult } from '@dukang/shared-types';
|
import type { WechatLoginResult } from '@dukang/shared-types';
|
||||||
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
|
import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { request } from './api';
|
import { saveAuth } from './api';
|
||||||
|
import { syncMiniWechatProfile } from './mini-wechat-profile';
|
||||||
|
import {
|
||||||
|
authorizeWechatForPay,
|
||||||
|
fetchClientConfig,
|
||||||
|
fetchUserProfile,
|
||||||
|
needsWechatAuthForPay,
|
||||||
|
saveWechatLoginResult,
|
||||||
|
type WechatBindResult,
|
||||||
|
} from './pay-wechat';
|
||||||
|
import { isWechatEnv, weixinSdk } from './weixin';
|
||||||
|
|
||||||
/** 小程序微信授权登录:Taro.login → /auth/login/wechat(资料上报由调用方 saveAuth 后执行) */
|
export type WechatAuthEnsureResult =
|
||||||
export async function loginWithWechat(): Promise<WechatLoginResult> {
|
| { ok: true }
|
||||||
|
| { ok: false; redirecting: true }
|
||||||
|
| { ok: false; needBindPhone: true; wxSessionKey: string };
|
||||||
|
|
||||||
|
/** 小程序:Taro.login → /auth/login/wechat;H5:公众号 OAuth */
|
||||||
|
export async function loginWithWechat(): Promise<WechatLoginResult | void> {
|
||||||
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
|
return loginWithWechatSdk();
|
||||||
|
}
|
||||||
const res = await Taro.login();
|
const res = await Taro.login();
|
||||||
if (!res.code) {
|
if (!res.code) {
|
||||||
throw new Error(res.errMsg || '微信登录失败,未获取到 code');
|
throw new Error(res.errMsg || '微信登录失败,未获取到 code');
|
||||||
}
|
}
|
||||||
|
const { request } = await import('./api');
|
||||||
return request<WechatLoginResult>('/auth/login/wechat', {
|
return request<WechatLoginResult>('/auth/login/wechat', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { code: res.code, platform: 'mini' },
|
data: { code: res.code, platform: 'mini' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { bindWechatForUser } from './pay-wechat';
|
export async function checkNeedsWechatAuth(): Promise<boolean> {
|
||||||
|
const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
|
||||||
|
return needsWechatAuthForPay(config, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 真实微信支付前确保已绑定微信;OAuth 跳转时返回 redirecting */
|
||||||
|
export async function ensureWechatAuthForPay(): Promise<WechatAuthEnsureResult> {
|
||||||
|
if (!isWechatEnv()) return { ok: true };
|
||||||
|
if (!(await checkNeedsWechatAuth())) return { ok: true };
|
||||||
|
|
||||||
|
const result = await authorizeWechatForPay();
|
||||||
|
if (!result) return { ok: false, redirecting: true };
|
||||||
|
|
||||||
|
if (result.needBindPhone && result.wxSessionKey) {
|
||||||
|
return { ok: false, needBindPhone: true, wxSessionKey: result.wxSessionKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveWechatLoginResult(result)) {
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
return { ok: false, redirecting: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理 URL 中 OAuth ?code= 回调(H5 公众号) */
|
||||||
|
export async function handleWechatAuthCallback(): Promise<WechatLoginResult | null> {
|
||||||
|
if (process.env.TARO_ENV !== 'h5') return null;
|
||||||
|
if (!isWechatEnv()) return null;
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return null;
|
||||||
|
const result = await weixinSdk.handleOAuthCallback();
|
||||||
|
if (result) stripOAuthParamsFromLocation();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginWithWechatSdk(): Promise<WechatLoginResult | void> {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
if (!isWxAuthorizeEnabled(config)) return;
|
||||||
|
if (!isWechatEnv()) {
|
||||||
|
throw new Error('请在微信内打开以使用微信一键授权');
|
||||||
|
}
|
||||||
|
return weixinSdk.login();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyWechatLoginResult(result: WechatLoginResult): boolean {
|
||||||
|
return saveWechatLoginResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已登录用户绑定微信:小程序 code;H5 走公众号 OAuth(带 JWT 时服务端会 attach) */
|
||||||
|
export async function bindWechatForUser(
|
||||||
|
prefetchedWxProfile?: { nickname?: string; avatarUrl?: string } | null,
|
||||||
|
): Promise<WechatBindResult> {
|
||||||
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
|
const result = await authorizeWechatForPay();
|
||||||
|
if (!result) {
|
||||||
|
return { ok: false, redirecting: true };
|
||||||
|
}
|
||||||
|
if (result.needBindPhone && result.wxSessionKey) {
|
||||||
|
return { ok: false, needBindPhone: true, wxSessionKey: result.wxSessionKey };
|
||||||
|
}
|
||||||
|
if (result.accessToken) {
|
||||||
|
saveAuth({
|
||||||
|
accessToken: result.accessToken,
|
||||||
|
refreshToken: result.refreshToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const profile = await fetchUserProfile();
|
||||||
|
return { ok: true, profile };
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await Taro.login();
|
||||||
|
if (!res.code) {
|
||||||
|
throw new Error(res.errMsg || '微信授权失败');
|
||||||
|
}
|
||||||
|
const { request } = await import('./api');
|
||||||
|
const data = await request<WechatLoginResult>('/auth/wechat/bind', {
|
||||||
|
method: 'POST',
|
||||||
|
data: { code: res.code, platform: 'mini' },
|
||||||
|
});
|
||||||
|
if (data.needBindPhone && data.wxSessionKey) {
|
||||||
|
return { ok: false, needBindPhone: true, wxSessionKey: data.wxSessionKey };
|
||||||
|
}
|
||||||
|
await syncMiniWechatProfile(prefetchedWxProfile);
|
||||||
|
const profile = await fetchUserProfile();
|
||||||
|
return { ok: true, profile };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { WechatBindResult };
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { BRAND_LOGO_URL } from '@dukang/shared-types';
|
||||||
|
import type { WechatShareData } from '@dukang/weixin-sdk';
|
||||||
|
import { getWechatShareLink, isWechatBrowser, isMiniProgram } from '@dukang/weixin-sdk';
|
||||||
|
import { toast } from './api';
|
||||||
|
import { isWechatEnv, weixinSdk } from './weixin';
|
||||||
|
|
||||||
|
export const DEFAULT_SHARE_TITLE = '你吃饭,杜康买单';
|
||||||
|
export const DEFAULT_SHARE_DESC = '杜康好客 · 买酒享权益,全城门店可用';
|
||||||
|
export const WECHAT_SHARE_HINT = '请点击右上角 ··· 分享给好友';
|
||||||
|
|
||||||
|
export function getDefaultShareImageUrl(): string {
|
||||||
|
return BRAND_LOGO_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDefaultShareData(
|
||||||
|
overrides?: Partial<WechatShareData>,
|
||||||
|
): WechatShareData {
|
||||||
|
let link = overrides?.link;
|
||||||
|
if (!link) {
|
||||||
|
if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') {
|
||||||
|
link = getWechatShareLink();
|
||||||
|
} else {
|
||||||
|
link = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: overrides?.title ?? DEFAULT_SHARE_TITLE,
|
||||||
|
desc: overrides?.desc ?? DEFAULT_SHARE_DESC,
|
||||||
|
link,
|
||||||
|
imgUrl: overrides?.imgUrl ?? getDefaultShareImageUrl(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 配置 H5 微信内右上角分享卡片 */
|
||||||
|
export async function applyWechatShare(
|
||||||
|
overrides?: Partial<WechatShareData>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (process.env.TARO_ENV !== 'h5') return;
|
||||||
|
if (!isWechatBrowser()) return;
|
||||||
|
await weixinSdk.setShare(buildDefaultShareData(overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PageSharePayload = {
|
||||||
|
title?: string;
|
||||||
|
desc?: string;
|
||||||
|
/** 小程序分享 path,如 /pages/product-detail/index?id=1 */
|
||||||
|
path?: string;
|
||||||
|
imgUrl?: string;
|
||||||
|
/** H5 自定义分享 link,默认当前页 */
|
||||||
|
link?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 点击「分享」按钮 */
|
||||||
|
export async function handleShareButtonClick(payload?: PageSharePayload): Promise<void> {
|
||||||
|
// 小程序:由 Button open-type="share" 触发,无需在此拉起
|
||||||
|
if (process.env.TARO_ENV === 'weapp' || isMiniProgram()) {
|
||||||
|
try {
|
||||||
|
await Taro.showShareMenu({ withShareTicket: true, showShareItems: ['shareAppMessage', 'shareTimeline'] });
|
||||||
|
} catch {
|
||||||
|
/* 旧基础库可能不支持 showShareItems */
|
||||||
|
try {
|
||||||
|
await Taro.showShareMenu({ withShareTicket: true });
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toast(WECHAT_SHARE_HINT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isWechatEnv()) {
|
||||||
|
toast('请在微信内打开后分享');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await applyWechatShare({
|
||||||
|
title: payload?.title,
|
||||||
|
desc: payload?.desc,
|
||||||
|
imgUrl: payload?.imgUrl,
|
||||||
|
link: payload?.link,
|
||||||
|
});
|
||||||
|
toast(WECHAT_SHARE_HINT);
|
||||||
|
} catch {
|
||||||
|
toast('分享配置失败,请刷新后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 供 useShareAppMessage 使用的标题/路径/图 */
|
||||||
|
export function toWeappShareMessage(payload?: PageSharePayload) {
|
||||||
|
return {
|
||||||
|
title: payload?.title || DEFAULT_SHARE_TITLE,
|
||||||
|
path: payload?.path || '/pages/home/index',
|
||||||
|
imageUrl: payload?.imgUrl || getDefaultShareImageUrl(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { ClientApp } from '@dukang/shared-types';
|
||||||
|
import { createWeixinSdk, isWechatEnv } from '@dukang/weixin-sdk';
|
||||||
|
import { API_BASE, getToken } from './api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H5 公众号内 JSSDK(分享 / 定位等)走 USER_H5 对应的公众号 appId。
|
||||||
|
* 小程序原生能力不经此门面。
|
||||||
|
*/
|
||||||
|
export const weixinSdk = createWeixinSdk({
|
||||||
|
apiBase: API_BASE,
|
||||||
|
clientApp: process.env.TARO_ENV === 'h5' ? ClientApp.USER_H5 : ClientApp.USER_MINI,
|
||||||
|
getAccessToken: () => getToken() || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { isWechatEnv };
|
||||||
@@ -11,7 +11,10 @@ import PageShell from '../../components/PageShell';
|
|||||||
import WechatLoginButton from '../../components/WechatLoginButton';
|
import WechatLoginButton from '../../components/WechatLoginButton';
|
||||||
import { BRAND_LOGO_WIDE_URL } from '@dukang/shared-types';
|
import { BRAND_LOGO_WIDE_URL } from '@dukang/shared-types';
|
||||||
import { finishLoginNavigate } from '../../lib/auth-nav';
|
import { finishLoginNavigate } from '../../lib/auth-nav';
|
||||||
import { bindWechatForUser } from '../../lib/wechat-auth';
|
import {
|
||||||
|
bindWechatForUser,
|
||||||
|
loginWithWechat,
|
||||||
|
} from '../../lib/wechat-auth';
|
||||||
import { fetchUserProfile } from '../../lib/pay-wechat';
|
import { fetchUserProfile } from '../../lib/pay-wechat';
|
||||||
import { saveUserPhone, resolveDefaultUserPhone } from '../../lib/user-phone';
|
import { saveUserPhone, resolveDefaultUserPhone } from '../../lib/user-phone';
|
||||||
import {
|
import {
|
||||||
@@ -21,7 +24,6 @@ import {
|
|||||||
type MiniWechatProfile,
|
type MiniWechatProfile,
|
||||||
} from '../../lib/mini-wechat-profile';
|
} from '../../lib/mini-wechat-profile';
|
||||||
import { isLoggedIn, request, saveAuth, toast, type SessionPayload } from '../../lib/api';
|
import { isLoggedIn, request, saveAuth, toast, type SessionPayload } from '../../lib/api';
|
||||||
import { loginWithWechat } from '../../lib/wechat-auth';
|
|
||||||
|
|
||||||
function normalizePhone(value: string) {
|
function normalizePhone(value: string) {
|
||||||
return value.replace(/\D/g, '').slice(0, 11);
|
return value.replace(/\D/g, '').slice(0, 11);
|
||||||
@@ -224,7 +226,10 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
if (completeMode === 'wechat' && isLoggedIn()) {
|
if (completeMode === 'wechat' && isLoggedIn()) {
|
||||||
const result = await bindWechatForUser(wxInfo);
|
const result = await bindWechatForUser(wxInfo);
|
||||||
if (!result.ok && result.needBindPhone) {
|
if (!result.ok && 'redirecting' in result && result.redirecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!result.ok && 'needBindPhone' in result && result.needBindPhone) {
|
||||||
setBindMode(true);
|
setBindMode(true);
|
||||||
setWxSessionKey(result.wxSessionKey);
|
setWxSessionKey(result.wxSessionKey);
|
||||||
setCompleteMode('phone');
|
setCompleteMode('phone');
|
||||||
@@ -240,11 +245,13 @@ export default function LoginPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await loginWithWechat();
|
const result = await loginWithWechat();
|
||||||
handleWechatLoginResult(result, wxInfo);
|
if (result) handleWechatLoginResult(result, wxInfo);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const raw = e instanceof Error ? e.message : '微信登录失败';
|
const raw = e instanceof Error ? e.message : '微信登录失败';
|
||||||
const hint = /invalid code/i.test(raw)
|
const hint = /invalid code/i.test(raw)
|
||||||
|
? process.env.TARO_ENV === 'weapp'
|
||||||
? '微信 code 无效:请确认后端 WX_MINI_APP_ID 与小程序 appid 一致,或本地联调使用 MOCK_WECHAT'
|
? '微信 code 无效:请确认后端 WX_MINI_APP_ID 与小程序 appid 一致,或本地联调使用 MOCK_WECHAT'
|
||||||
|
: '微信授权失败:请确认公众号 WX_APP_ID / 网页授权域名配置正确'
|
||||||
: raw;
|
: raw;
|
||||||
setMsg(hint);
|
setMsg(hint);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import { bindWechatForUser } from '../../lib/wechat-auth';
|
|||||||
import { fetchUserProfile } from '../../lib/pay-wechat';
|
import { fetchUserProfile } from '../../lib/pay-wechat';
|
||||||
import {
|
import {
|
||||||
fetchMiniWechatUserInfo,
|
fetchMiniWechatUserInfo,
|
||||||
getCachedWxProfile,
|
|
||||||
mergeWxDisplayProfile,
|
mergeWxDisplayProfile,
|
||||||
} from '../../lib/mini-wechat-profile';
|
} from '../../lib/mini-wechat-profile';
|
||||||
import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api';
|
import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api';
|
||||||
|
import { isWechatEnv } from '../../lib/weixin';
|
||||||
|
|
||||||
const ORDER_SHORTCUTS = [
|
const ORDER_SHORTCUTS = [
|
||||||
{ tab: 'pending_pay', icon: '付', label: '待付款' },
|
{ tab: 'pending_pay', icon: '付', label: '待付款' },
|
||||||
@@ -111,13 +111,39 @@ export default function MinePage() {
|
|||||||
toast('当前环境未开启微信授权');
|
toast('当前环境未开启微信授权');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (process.env.TARO_ENV !== 'weapp') {
|
|
||||||
toast('请在微信小程序中完成微信授权');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setBindingWx(true);
|
setBindingWx(true);
|
||||||
try {
|
try {
|
||||||
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
|
if (!isWechatEnv()) {
|
||||||
|
toast('请在微信内打开后授权');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await bindWechatForUser();
|
||||||
|
if (!result.ok && 'redirecting' in result && result.redirecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!result.ok && 'needBindPhone' in result && result.needBindPhone) {
|
||||||
|
goLogin('/pages/mine/index', {
|
||||||
|
bindMode: '1',
|
||||||
|
wxSessionKey: result.wxSessionKey,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.ok) {
|
||||||
|
setProfile(
|
||||||
|
mergeWxDisplayProfile({
|
||||||
|
...(result.profile ?? {}),
|
||||||
|
id: result.profile?.id ?? profile?.id ?? '',
|
||||||
|
hasWechat: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
loadProfile();
|
||||||
|
toast('微信授权成功', 'success');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let wxInfo = null;
|
let wxInfo = null;
|
||||||
try {
|
try {
|
||||||
wxInfo = await fetchMiniWechatUserInfo();
|
wxInfo = await fetchMiniWechatUserInfo();
|
||||||
@@ -127,7 +153,7 @@ export default function MinePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await bindWechatForUser(wxInfo);
|
const result = await bindWechatForUser(wxInfo);
|
||||||
if (!result.ok && result.needBindPhone) {
|
if (!result.ok && 'needBindPhone' in result && result.needBindPhone) {
|
||||||
goLogin('/pages/mine/index', { bindMode: '1', wxSessionKey: result.wxSessionKey });
|
goLogin('/pages/mine/index', { bindMode: '1', wxSessionKey: result.wxSessionKey });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import Taro, { useRouter } from '@tarojs/taro';
|
|||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import SubPageHeader from '../../components/SubPageHeader';
|
import SubPageHeader from '../../components/SubPageHeader';
|
||||||
import { buildAddressListUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav';
|
import { buildAddressListUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav';
|
||||||
|
import { tryGetClientGpsLocation } from '../../lib/client-location';
|
||||||
import { maskPhone } from '../../lib/phone';
|
import { maskPhone } from '../../lib/phone';
|
||||||
import { ensurePayReady } from '../../lib/pay-ready';
|
import { ensurePayReady } from '../../lib/pay-ready';
|
||||||
import { request, toast } from '../../lib/api';
|
import { request } from '../../lib/api';
|
||||||
import { getProductMainImage } from '../../lib/product-images';
|
import { getProductMainImage } from '../../lib/product-images';
|
||||||
|
|
||||||
type Address = {
|
type Address = {
|
||||||
@@ -127,12 +128,19 @@ export default function OrderConfirmPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doSubmit() {
|
async function doSubmit() {
|
||||||
|
let clientLocation = null;
|
||||||
|
try {
|
||||||
|
clientLocation = await tryGetClientGpsLocation();
|
||||||
|
} catch {
|
||||||
|
/* GPS 获取失败不阻塞下单 */
|
||||||
|
}
|
||||||
const order = await request<{ id: string }>('/trade/orders', {
|
const order = await request<{ id: string }>('/trade/orders', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
productId,
|
productId,
|
||||||
quantity,
|
quantity,
|
||||||
addressId,
|
addressId,
|
||||||
|
...(clientLocation ? { clientLocation } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Taro.redirectTo({
|
Taro.redirectTo({
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
navigationBarTitleText: '订单详情',
|
navigationBarTitleText: '订单详情',
|
||||||
|
enableShareAppMessage: true,
|
||||||
|
enableShareTimeline: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import { useRouter } from '@tarojs/taro';
|
import { useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import SubPageHeader from '../../components/SubPageHeader';
|
import SubPageHeader from '../../components/SubPageHeader';
|
||||||
|
import ShareNavButton from '../../components/ShareNavButton';
|
||||||
|
import WechatShareReady from '../../components/WechatShareReady';
|
||||||
import { request, toast } from '../../lib/api';
|
import { request, toast } from '../../lib/api';
|
||||||
|
import {
|
||||||
|
DEFAULT_SHARE_DESC,
|
||||||
|
DEFAULT_SHARE_TITLE,
|
||||||
|
toWeappShareMessage,
|
||||||
|
} from '../../lib/wechat-share';
|
||||||
|
|
||||||
type OrderDetail = {
|
type OrderDetail = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,9 +35,30 @@ export default function OrderDetailPage() {
|
|||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
||||||
}, [orderId]);
|
}, [orderId]);
|
||||||
|
|
||||||
|
const sharePayload = useMemo(
|
||||||
|
() => ({
|
||||||
|
title: order?.productName
|
||||||
|
? `我买了${order.productName} · 杜康好客`
|
||||||
|
: DEFAULT_SHARE_TITLE,
|
||||||
|
desc: DEFAULT_SHARE_DESC,
|
||||||
|
path: orderId ? `/pages/order-detail/index?id=${orderId}` : '/pages/home/index',
|
||||||
|
}),
|
||||||
|
[order, orderId],
|
||||||
|
);
|
||||||
|
|
||||||
|
useShareAppMessage(() => toWeappShareMessage(sharePayload));
|
||||||
|
useShareTimeline(() => ({
|
||||||
|
title: sharePayload.title || DEFAULT_SHARE_TITLE,
|
||||||
|
query: orderId ? `id=${orderId}` : '',
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="sub" className="order-detail-page">
|
<PageShell variant="sub" className="order-detail-page">
|
||||||
<SubPageHeader title="订单详情" />
|
<WechatShareReady payload={sharePayload} />
|
||||||
|
<SubPageHeader
|
||||||
|
title="订单详情"
|
||||||
|
right={<ShareNavButton payload={sharePayload} />}
|
||||||
|
/>
|
||||||
<View className="sub-page-body">
|
<View className="sub-page-body">
|
||||||
{!order ? (
|
{!order ? (
|
||||||
<View className="u-empty">加载中…</View>
|
<View className="u-empty">加载中…</View>
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro, { useRouter } from '@tarojs/taro';
|
import Taro, { useDidShow, useRouter } from '@tarojs/taro';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import SubPageHeader from '../../components/SubPageHeader';
|
import SubPageHeader from '../../components/SubPageHeader';
|
||||||
|
import WechatLoginButton from '../../components/WechatLoginButton';
|
||||||
import { ensurePayReady } from '../../lib/pay-ready';
|
import { ensurePayReady } from '../../lib/pay-ready';
|
||||||
import {
|
import {
|
||||||
|
authorizeWechatForPay,
|
||||||
fetchClientConfig,
|
fetchClientConfig,
|
||||||
fetchUserProfile,
|
fetchUserProfile,
|
||||||
isWechatAuthRequiredError,
|
isWechatAuthRequiredError,
|
||||||
needsWechatAuthForPay,
|
needsWechatAuthForPay,
|
||||||
payOrder,
|
payOrder,
|
||||||
|
saveWechatLoginResult,
|
||||||
} from '../../lib/pay-wechat';
|
} from '../../lib/pay-wechat';
|
||||||
|
import { applyWechatLoginResult } from '../../lib/wechat-auth';
|
||||||
|
import { isWechatEnv } from '../../lib/weixin';
|
||||||
|
import { goLogin } from '../../lib/auth-nav';
|
||||||
import { request, toast } from '../../lib/api';
|
import { request, toast } from '../../lib/api';
|
||||||
|
|
||||||
export default function PayPage() {
|
export default function PayPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const orderId = router.params.orderId ?? '';
|
const orderId = router.params.orderId ?? '';
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [authLoading, setAuthLoading] = useState(false);
|
||||||
const [mockMode, setMockMode] = useState(true);
|
const [mockMode, setMockMode] = useState(true);
|
||||||
const [needsWechatAuth, setNeedsWechatAuth] = useState(false);
|
const [needsWechatAuth, setNeedsWechatAuth] = useState(false);
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
@@ -27,24 +34,28 @@ export default function PayPage() {
|
|||||||
? `/pages/pay/index?orderId=${orderId}`
|
? `/pages/pay/index?orderId=${orderId}`
|
||||||
: '/pages/pay/index';
|
: '/pages/pay/index';
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshPayReadiness = useCallback(async () => {
|
||||||
if (!orderId) return;
|
|
||||||
void ensurePayReady(returnPath);
|
|
||||||
}, [orderId, returnPath]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function load() {
|
|
||||||
try {
|
try {
|
||||||
const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
|
const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]);
|
||||||
setMockMode(config.mockPay);
|
setMockMode(config.mockPay);
|
||||||
setNeedsWechatAuth(needsWechatAuthForPay(config, profile));
|
setNeedsWechatAuth(needsWechatAuthForPay(config, profile));
|
||||||
|
return profile;
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
return null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
void load();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
void refreshPayReadiness();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!orderId) return;
|
||||||
|
if (process.env.TARO_ENV === 'weapp') {
|
||||||
|
void ensurePayReady(returnPath);
|
||||||
|
}
|
||||||
|
}, [orderId, returnPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!orderId) {
|
if (!orderId) {
|
||||||
setOrderNo('');
|
setOrderNo('');
|
||||||
@@ -67,6 +78,38 @@ export default function PayPage() {
|
|||||||
});
|
});
|
||||||
}, [orderId]);
|
}, [orderId]);
|
||||||
|
|
||||||
|
async function wechatAuthorize() {
|
||||||
|
setAuthLoading(true);
|
||||||
|
setMsg('');
|
||||||
|
try {
|
||||||
|
if (!isWechatEnv()) {
|
||||||
|
setMsg('请在微信内打开以授权微信支付');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (process.env.TARO_ENV === 'weapp') {
|
||||||
|
const ready = await ensurePayReady(returnPath);
|
||||||
|
if (ready) await refreshPayReadiness();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await authorizeWechatForPay();
|
||||||
|
if (result) {
|
||||||
|
if (result.needBindPhone && result.wxSessionKey) {
|
||||||
|
goLogin(returnPath, { bindMode: '1', wxSessionKey: result.wxSessionKey });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveWechatLoginResult(result) || applyWechatLoginResult(result)) {
|
||||||
|
setMsg('');
|
||||||
|
await refreshPayReadiness();
|
||||||
|
toast('微信授权成功', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setMsg(e instanceof Error ? e.message : '微信授权失败');
|
||||||
|
} finally {
|
||||||
|
setAuthLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function pay() {
|
async function pay() {
|
||||||
if (!orderId) {
|
if (!orderId) {
|
||||||
toast('订单不存在');
|
toast('订单不存在');
|
||||||
@@ -74,8 +117,11 @@ export default function PayPage() {
|
|||||||
}
|
}
|
||||||
if (needsWechatAuth) {
|
if (needsWechatAuth) {
|
||||||
setMsg('请先完成微信授权后再支付');
|
setMsg('请先完成微信授权后再支付');
|
||||||
const ready = await ensurePayReady(returnPath);
|
if (process.env.TARO_ENV === 'h5') {
|
||||||
if (!ready) return;
|
await wechatAuthorize();
|
||||||
|
} else {
|
||||||
|
await ensurePayReady(returnPath);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +142,6 @@ export default function PayPage() {
|
|||||||
if (isWechatAuthRequiredError(e)) {
|
if (isWechatAuthRequiredError(e)) {
|
||||||
setNeedsWechatAuth(true);
|
setNeedsWechatAuth(true);
|
||||||
setMsg('微信支付需要先完成微信授权');
|
setMsg('微信支付需要先完成微信授权');
|
||||||
await ensurePayReady(returnPath);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const message = e instanceof Error ? e.message : '支付失败';
|
const message = e instanceof Error ? e.message : '支付失败';
|
||||||
@@ -124,6 +169,20 @@ export default function PayPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text className="pay-status-amount">¥{payAmount}</Text>
|
<Text className="pay-status-amount">¥{payAmount}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{needsWechatAuth ? (
|
||||||
|
<View className="pay-wechat-auth-card">
|
||||||
|
<Text className="pay-wechat-auth-title">尚未授权微信</Text>
|
||||||
|
<Text className="pay-wechat-auth-desc">
|
||||||
|
授权后可安全调起微信支付,不会重复扣款
|
||||||
|
</Text>
|
||||||
|
<WechatLoginButton
|
||||||
|
loading={authLoading}
|
||||||
|
onClick={() => void wechatAuthorize()}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<View className="order-card">
|
<View className="order-card">
|
||||||
<View className="order-row">
|
<View className="order-row">
|
||||||
<Text className="order-row-label">订单号</Text>
|
<Text className="order-row-label">订单号</Text>
|
||||||
@@ -140,15 +199,34 @@ export default function PayPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{msg ? <Text className="u-muted" style={{ display: 'block', marginTop: 12 }}>{msg}</Text> : null}
|
{msg ? (
|
||||||
|
<Text className="pay-wechat-auth-msg" style={{ display: 'block', marginTop: 12 }}>
|
||||||
|
{msg}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<View className="pay-bar">
|
<View className="pay-bar">
|
||||||
<View
|
<View
|
||||||
className="order-confirm-submit"
|
className="order-confirm-submit"
|
||||||
style={{ flex: 1, opacity: loading ? 0.7 : 1 }}
|
style={{ flex: 1, opacity: loading || needsWechatAuth ? 0.7 : 1 }}
|
||||||
onClick={() => !loading && void pay()}
|
onClick={() => {
|
||||||
|
if (loading) return;
|
||||||
|
if (needsWechatAuth) {
|
||||||
|
void wechatAuthorize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void pay();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text>{loading ? '支付中…' : needsWechatAuth ? '去授权' : '立即支付'}</Text>
|
<Text>
|
||||||
|
{loading
|
||||||
|
? '支付中…'
|
||||||
|
: needsWechatAuth
|
||||||
|
? authLoading
|
||||||
|
? '授权中…'
|
||||||
|
: '微信一键授权'
|
||||||
|
: '立即支付'}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</PageShell>
|
</PageShell>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
navigationBarTitleText: '商品详情',
|
navigationBarTitleText: '商品详情',
|
||||||
|
enableShareAppMessage: true,
|
||||||
|
enableShareTimeline: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { View, Text, Image } from '@tarojs/components';
|
import { View, Text, Image } from '@tarojs/components';
|
||||||
import Taro, { usePageScroll, useRouter } from '@tarojs/taro';
|
import Taro, { usePageScroll, useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
||||||
import type { ProductDetailContentDto } from '@dukang/shared-types';
|
import type { ProductDetailContentDto } from '@dukang/shared-types';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import PageNavBar from '../../components/PageNavBar';
|
import PageNavBar from '../../components/PageNavBar';
|
||||||
import ProductCarousel from '../../components/ProductCarousel';
|
import ProductCarousel from '../../components/ProductCarousel';
|
||||||
|
import ShareNavButton from '../../components/ShareNavButton';
|
||||||
|
import WechatShareReady from '../../components/WechatShareReady';
|
||||||
import { goLogin } from '../../lib/auth-nav';
|
import { goLogin } from '../../lib/auth-nav';
|
||||||
import { ensurePayReady } from '../../lib/pay-ready';
|
import { ensurePayReady } from '../../lib/pay-ready';
|
||||||
import { isLoggedIn, request, toast } from '../../lib/api';
|
import { isLoggedIn, request, toast } from '../../lib/api';
|
||||||
import {
|
import {
|
||||||
getProductCarouselImages,
|
getProductCarouselImages,
|
||||||
getProductDetailImages,
|
getProductDetailImages,
|
||||||
|
getProductMainImage,
|
||||||
type ProductImageSource,
|
type ProductImageSource,
|
||||||
} from '../../lib/product-images';
|
} from '../../lib/product-images';
|
||||||
|
import {
|
||||||
|
DEFAULT_SHARE_DESC,
|
||||||
|
DEFAULT_SHARE_TITLE,
|
||||||
|
toWeappShareMessage,
|
||||||
|
} from '../../lib/wechat-share';
|
||||||
import iconHome from '../../assets/tabbar/home.png';
|
import iconHome from '../../assets/tabbar/home.png';
|
||||||
|
|
||||||
type Product = ProductImageSource & {
|
type Product = ProductImageSource & {
|
||||||
@@ -42,6 +50,23 @@ export default function ProductDetailPage() {
|
|||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
||||||
}, [productId]);
|
}, [productId]);
|
||||||
|
|
||||||
|
const sharePayload = useMemo(
|
||||||
|
() => ({
|
||||||
|
title: product?.name || DEFAULT_SHARE_TITLE,
|
||||||
|
desc: product?.subtitle || DEFAULT_SHARE_DESC,
|
||||||
|
path: `/pages/product-detail/index?id=${productId}`,
|
||||||
|
imgUrl: (product ? getProductMainImage(product) : '') || undefined,
|
||||||
|
}),
|
||||||
|
[product, productId],
|
||||||
|
);
|
||||||
|
|
||||||
|
useShareAppMessage(() => toWeappShareMessage(sharePayload));
|
||||||
|
useShareTimeline(() => ({
|
||||||
|
title: sharePayload.title || DEFAULT_SHARE_TITLE,
|
||||||
|
query: productId ? `id=${productId}` : '',
|
||||||
|
imageUrl: sharePayload.imgUrl,
|
||||||
|
}));
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
const pages = Taro.getCurrentPages();
|
const pages = Taro.getCurrentPages();
|
||||||
if (pages.length > 1) {
|
if (pages.length > 1) {
|
||||||
@@ -84,16 +109,13 @@ export default function ProductDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="scroll" className="product-detail-page" hasFixedFooter>
|
<PageShell variant="scroll" className="product-detail-page" hasFixedFooter>
|
||||||
|
<WechatShareReady payload={sharePayload} />
|
||||||
<PageNavBar
|
<PageNavBar
|
||||||
title={product.name}
|
title={product.name}
|
||||||
solid={headerSolid}
|
solid={headerSolid}
|
||||||
titleVisible={headerSolid}
|
titleVisible={headerSolid}
|
||||||
onBack={goBack}
|
onBack={goBack}
|
||||||
right={(
|
right={<ShareNavButton payload={sharePayload} />}
|
||||||
<View className="page-nav-bar__btn" onClick={() => toast('分享功能开发中')}>
|
|
||||||
<Text className="page-nav-bar__icon page-nav-bar__icon--share">⤴</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View className="product-detail-main">
|
<View className="product-detail-main">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
navigationBarTitleText: '门店详情',
|
navigationBarTitleText: '门店详情',
|
||||||
|
enableShareAppMessage: true,
|
||||||
|
enableShareTimeline: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro, { usePageScroll, useRouter } from '@tarojs/taro';
|
import Taro, { usePageScroll, useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import PageNavBar from '../../components/PageNavBar';
|
import PageNavBar from '../../components/PageNavBar';
|
||||||
import ProductCarousel from '../../components/ProductCarousel';
|
import ProductCarousel from '../../components/ProductCarousel';
|
||||||
|
import ShareNavButton from '../../components/ShareNavButton';
|
||||||
|
import WechatShareReady from '../../components/WechatShareReady';
|
||||||
import { request, toast } from '../../lib/api';
|
import { request, toast } from '../../lib/api';
|
||||||
|
import {
|
||||||
|
DEFAULT_SHARE_DESC,
|
||||||
|
DEFAULT_SHARE_TITLE,
|
||||||
|
toWeappShareMessage,
|
||||||
|
} from '../../lib/wechat-share';
|
||||||
|
|
||||||
type Store = {
|
type Store = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -44,6 +51,23 @@ export default function StoreDetailPage() {
|
|||||||
});
|
});
|
||||||
}, [storeId]);
|
}, [storeId]);
|
||||||
|
|
||||||
|
const sharePayload = useMemo(
|
||||||
|
() => ({
|
||||||
|
title: store?.name || DEFAULT_SHARE_TITLE,
|
||||||
|
desc: store?.address || DEFAULT_SHARE_DESC,
|
||||||
|
path: `/pages/store-detail/index?id=${storeId}`,
|
||||||
|
imgUrl: store?.coverUrl || store?.carouselUrls?.[0] || undefined,
|
||||||
|
}),
|
||||||
|
[store, storeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
useShareAppMessage(() => toWeappShareMessage(sharePayload));
|
||||||
|
useShareTimeline(() => ({
|
||||||
|
title: sharePayload.title || DEFAULT_SHARE_TITLE,
|
||||||
|
query: storeId ? `id=${storeId}` : '',
|
||||||
|
imageUrl: sharePayload.imgUrl,
|
||||||
|
}));
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
const pages = Taro.getCurrentPages();
|
const pages = Taro.getCurrentPages();
|
||||||
if (pages.length > 1) Taro.navigateBack();
|
if (pages.length > 1) Taro.navigateBack();
|
||||||
@@ -68,11 +92,13 @@ export default function StoreDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="scroll" className="store-detail-page" hasFixedFooter>
|
<PageShell variant="scroll" className="store-detail-page" hasFixedFooter>
|
||||||
|
<WechatShareReady payload={sharePayload} />
|
||||||
<PageNavBar
|
<PageNavBar
|
||||||
title={store.name}
|
title={store.name}
|
||||||
solid={headerSolid}
|
solid={headerSolid}
|
||||||
titleVisible={headerSolid}
|
titleVisible={headerSolid}
|
||||||
onBack={goBack}
|
onBack={goBack}
|
||||||
|
right={<ShareNavButton payload={sharePayload} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View className="store-detail-hero full-bleed">
|
<View className="store-detail-hero full-bleed">
|
||||||
@@ -98,17 +124,9 @@ export default function StoreDetailPage() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="store-detail-section">
|
|
||||||
<Text className="store-detail-section-title">门店服务</Text>
|
|
||||||
<Text className="store-detail-meta">支持好客权益到店核销,部分门店提供包间预约。</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="store-detail-bar">
|
<View className="store-detail-bar">
|
||||||
<View
|
<View className="u-btn u-btn--block" onClick={() => toast('核销请前往「好客权益」')}>
|
||||||
className="store-detail-bar-btn store-detail-bar-btn--primary store-detail-bar-btn--full"
|
<Text>到店核销</Text>
|
||||||
onClick={() => Taro.navigateTo({ url: '/pages/redeem/index' })}
|
|
||||||
>
|
|
||||||
<Text>去核销</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</PageShell>
|
</PageShell>
|
||||||
|
|||||||
@@ -127,6 +127,26 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 右侧操作槽:固定贴着内容区右缘(内容区已用 navBarPaddingRight 避让微信菜单) */
|
||||||
|
.page-nav-bar__right-slot {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-nav-bar__right-slot .page-nav-bar__btn {
|
||||||
|
position: relative;
|
||||||
|
top: auto;
|
||||||
|
left: auto;
|
||||||
|
right: auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.page-nav-bar--solid .page-nav-bar__btn {
|
.page-nav-bar--solid .page-nav-bar__btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
@@ -149,6 +169,24 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 小程序分享 Button 去默认样式,对齐圆形胶囊按钮 */
|
||||||
|
.page-nav-bar__share-btn {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
background: rgba(255, 255, 255, 0.8) !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-nav-bar__share-btn::after {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-nav-bar--solid .page-nav-bar__share-btn {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
.page-nav-bar__title {
|
.page-nav-bar__title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -157,7 +195,7 @@
|
|||||||
height: var(--nav-content-height);
|
height: var(--nav-content-height);
|
||||||
line-height: var(--nav-content-height);
|
line-height: var(--nav-content-height);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0 48px;
|
padding: 0 88px 0 56px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
font-family: var(--font-headline);
|
font-family: var(--font-headline);
|
||||||
|
|||||||
@@ -278,3 +278,34 @@
|
|||||||
color: var(--color-heritage-red);
|
color: var(--color-heritage-red);
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pay-wechat-auth-card {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-wechat-auth-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-wechat-auth-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-muted, #999);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-wechat-auth-msg {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-heritage-red, #a02d30);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user