feat: audit diff, mock SMS 999888, env photos, proxy pay lock, mini-user UX
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -82,12 +82,18 @@ function timeToMinutes(hhmm: string): number {
|
||||
return h * 60 + m;
|
||||
}
|
||||
|
||||
export const MIN_ENV_PHOTO_COUNT = 3;
|
||||
|
||||
export function normalizeStringArray(urls: unknown, minLen: number): string[] {
|
||||
const arr = Array.isArray(urls) ? urls.map((u) => String(u ?? '')) : [];
|
||||
while (arr.length < minLen) arr.push('');
|
||||
return arr;
|
||||
}
|
||||
|
||||
export function addEnvPhotoSlot(urls: string[]): string[] {
|
||||
return [...urls, ''];
|
||||
}
|
||||
|
||||
export function normalizeStoreDraftForm(raw: Partial<StoreDraftForm> | null | undefined): StoreDraftForm {
|
||||
const base = defaultStoreForm();
|
||||
if (!raw) return base;
|
||||
@@ -104,7 +110,7 @@ export function normalizeStoreDraftForm(raw: Partial<StoreDraftForm> | null | un
|
||||
openTime2: String(raw.openTime2 ?? base.openTime2),
|
||||
closeTime2: String(raw.closeTime2 ?? base.closeTime2),
|
||||
avgPrice: String(raw.avgPrice ?? base.avgPrice),
|
||||
envPhotoUrls: normalizeStringArray(raw.envPhotoUrls, 3),
|
||||
envPhotoUrls: normalizeStringArray(raw.envPhotoUrls, MIN_ENV_PHOTO_COUNT),
|
||||
packages: Array.isArray(raw.packages)
|
||||
? raw.packages.map((p, i) => ({
|
||||
name: String((p as { name?: string }).name ?? ''),
|
||||
@@ -215,13 +221,14 @@ export function validateStoreStep2(
|
||||
): string | null {
|
||||
if (!form.coverUrl.trim()) return '请上传门头照';
|
||||
const envCount = form.envPhotoUrls.filter((u) => u.trim()).length;
|
||||
if (envCount < 3) return '请上传至少 3 张环境照片';
|
||||
if (envCount < MIN_ENV_PHOTO_COUNT) return `请上传至少 ${MIN_ENV_PHOTO_COUNT} 张环境照片`;
|
||||
if (!form.contractUrl.trim()) return '请上传签约合同';
|
||||
return null;
|
||||
}
|
||||
|
||||
export function patchEnvPhotoAt(urls: string[], index: number, url: string): string[] {
|
||||
const envPhotoUrls = normalizeStringArray(urls, 3);
|
||||
const envPhotoUrls = [...urls];
|
||||
while (envPhotoUrls.length <= index) envPhotoUrls.push('');
|
||||
envPhotoUrls[index] = url;
|
||||
return envPhotoUrls;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ export default function ProxyOrderDetailPage() {
|
||||
const [payMethod, setPayMethod] = useState<ProxyPayMethod>('NATIVE');
|
||||
const [codeUrl, setCodeUrl] = useState<string | null>(null);
|
||||
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const pollRef = useRef<number | null>(null);
|
||||
|
||||
function stopPoll() {
|
||||
@@ -77,7 +78,12 @@ export default function ProxyOrderDetailPage() {
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
void request<PartnerProxyOrderListItem>('PARTNER_H5', `/partner/proxy-orders/${id}`)
|
||||
.then(setOrder)
|
||||
.then((data) => {
|
||||
setOrder(data);
|
||||
if (data.proxyPayMethod) {
|
||||
setPayMethod(data.proxyPayMethod);
|
||||
}
|
||||
})
|
||||
.catch((e) => setError(e instanceof Error ? e.message : '加载失败'));
|
||||
}, [id]);
|
||||
|
||||
@@ -129,6 +135,15 @@ export default function ProxyOrderDetailPage() {
|
||||
|
||||
async function startPay(method: ProxyPayMethod) {
|
||||
if (!id) return;
|
||||
const locked = order?.proxyPayMethod;
|
||||
if (locked && locked !== method) {
|
||||
setPayMsg(
|
||||
locked === 'JSAPI'
|
||||
? '该订单已发起微信代付,请先取消支付后重新下单'
|
||||
: '该订单已生成收款码,请先取消支付后重新下单',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setPaying(true);
|
||||
setPayMsg('');
|
||||
setPayMethod(method);
|
||||
@@ -166,12 +181,14 @@ export default function ProxyOrderDetailPage() {
|
||||
|
||||
if (pay.mode === 'native' && pay.codeUrl) {
|
||||
setCodeUrl(pay.codeUrl);
|
||||
setOrder((prev) => (prev ? { ...prev, proxyPayMethod: method } : prev));
|
||||
startPoll(id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pay.mode === 'jsapi' && pay.prepay) {
|
||||
setCodeUrl(null);
|
||||
setOrder((prev) => (prev ? { ...prev, proxyPayMethod: method } : prev));
|
||||
await invokeWechatPay(pay.prepay, {
|
||||
apiBase: '/api/v1',
|
||||
clientApp: 'PARTNER_H5',
|
||||
@@ -191,14 +208,27 @@ export default function ProxyOrderDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const autoPayStartedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!id || !unpaid || autoPayStartedRef.current) return;
|
||||
autoPayStartedRef.current = true;
|
||||
void startPay('NATIVE');
|
||||
// 仅首次进入未支付详情时自动拉收款码
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, unpaid]);
|
||||
async function cancelPay() {
|
||||
if (!id) return;
|
||||
setCancelling(true);
|
||||
setPayMsg('');
|
||||
stopPoll();
|
||||
try {
|
||||
await request('PARTNER_H5', `/partner/proxy-orders/${id}/cancel-pay`, {
|
||||
method: 'POST',
|
||||
body: '{}',
|
||||
});
|
||||
toastSuccess('已取消支付,可重新下单并选择其他支付方式');
|
||||
navigate('/center/proxy-orders');
|
||||
} catch (e) {
|
||||
setPayMsg(e instanceof Error ? e.message : '取消失败');
|
||||
toastError(e instanceof Error ? e.message : '取消失败');
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
}
|
||||
|
||||
const lockedPayMethod = order?.proxyPayMethod ?? null;
|
||||
|
||||
const img = order?.imageResource?.url || '';
|
||||
const isOnSite = String(order?.deliveryType || '').toUpperCase() === 'ON_SITE_PICKUP';
|
||||
@@ -285,6 +315,7 @@ export default function ProxyOrderDetailPage() {
|
||||
<button
|
||||
type="button"
|
||||
className={`partner-proxy-mode-tab${payMethod === 'NATIVE' ? ' is-active' : ''}`}
|
||||
disabled={!!lockedPayMethod && lockedPayMethod !== 'NATIVE'}
|
||||
onClick={() => void startPay('NATIVE')}
|
||||
>
|
||||
收款码
|
||||
@@ -292,11 +323,21 @@ export default function ProxyOrderDetailPage() {
|
||||
<button
|
||||
type="button"
|
||||
className={`partner-proxy-mode-tab${payMethod === 'JSAPI' ? ' is-active' : ''}`}
|
||||
disabled={!!lockedPayMethod && lockedPayMethod !== 'JSAPI'}
|
||||
onClick={() => void startPay('JSAPI')}
|
||||
>
|
||||
微信代付
|
||||
</button>
|
||||
</div>
|
||||
{lockedPayMethod ? (
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
已选择{lockedPayMethod === 'NATIVE' ? '收款码' : '微信代付'},切换方式请先取消支付
|
||||
</p>
|
||||
) : (
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
请选择支付方式并点击下方按钮发起支付
|
||||
</p>
|
||||
)}
|
||||
|
||||
{payMethod === 'NATIVE' ? (
|
||||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||||
@@ -338,6 +379,16 @@ export default function ProxyOrderDetailPage() {
|
||||
{payMsg}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-block"
|
||||
style={{ marginTop: 16 }}
|
||||
disabled={cancelling || paying}
|
||||
onClick={() => void cancelPay()}
|
||||
>
|
||||
{cancelling ? '取消中…' : '取消支付'}
|
||||
</button>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ export default function ProxyOrderPage() {
|
||||
const [codeUrl, setCodeUrl] = useState<string | null>(null);
|
||||
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||||
const [paying, setPaying] = useState(false);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const [lockedPayMethod, setLockedPayMethod] = useState<ProxyPayMethod | null>(null);
|
||||
const pollRef = useRef<number | null>(null);
|
||||
|
||||
const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]);
|
||||
@@ -234,8 +236,17 @@ export default function ProxyOrderPage() {
|
||||
}
|
||||
|
||||
async function startPay(orderId: string, method: ProxyPayMethod) {
|
||||
if (lockedPayMethod && lockedPayMethod !== method) {
|
||||
setMsg(
|
||||
lockedPayMethod === 'JSAPI'
|
||||
? '该订单已发起微信代付,请先取消支付后重新下单'
|
||||
: '该订单已生成收款码,请先取消支付后重新下单',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setPaying(true);
|
||||
setMsg('');
|
||||
setPayMethod(method);
|
||||
try {
|
||||
if (method === 'JSAPI') {
|
||||
if (!isWechatEnv()) {
|
||||
@@ -266,11 +277,14 @@ export default function ProxyOrderPage() {
|
||||
|
||||
if (pay.mode === 'native' && pay.codeUrl) {
|
||||
setCodeUrl(pay.codeUrl);
|
||||
setLockedPayMethod(method);
|
||||
startPoll(orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pay.mode === 'jsapi' && pay.prepay) {
|
||||
setCodeUrl(null);
|
||||
setLockedPayMethod(method);
|
||||
await invokeWechatPay(pay.prepay, {
|
||||
apiBase: '/api/v1',
|
||||
clientApp: 'PARTNER_H5',
|
||||
@@ -289,6 +303,40 @@ export default function ProxyOrderPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelPay(orderId: string) {
|
||||
setCancelling(true);
|
||||
setMsg('');
|
||||
stopPoll();
|
||||
try {
|
||||
await request('PARTNER_H5', `/partner/proxy-orders/${orderId}/cancel-pay`, {
|
||||
method: 'POST',
|
||||
body: '{}',
|
||||
});
|
||||
toastSuccess('已取消支付,可重新下单并选择其他支付方式');
|
||||
setStep('form');
|
||||
setCreated(null);
|
||||
setCodeUrl(null);
|
||||
setLockedPayMethod(null);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '取消失败');
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
}
|
||||
|
||||
function switchPayMethod(method: ProxyPayMethod) {
|
||||
if (lockedPayMethod && lockedPayMethod !== method) {
|
||||
setMsg(
|
||||
lockedPayMethod === 'JSAPI'
|
||||
? '该订单已发起微信代付,请先取消支付后重新下单'
|
||||
: '该订单已生成收款码,请先取消支付后重新下单',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setPayMethod(method);
|
||||
setMsg('');
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
setMsg('');
|
||||
const err = validateForm();
|
||||
@@ -322,6 +370,7 @@ export default function ProxyOrderPage() {
|
||||
setCreated(order);
|
||||
setStep('pay');
|
||||
setCodeUrl(null);
|
||||
setLockedPayMethod(null);
|
||||
await startPay(order.id, payMethod);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '下单失败');
|
||||
@@ -359,24 +408,25 @@ export default function ProxyOrderPage() {
|
||||
<button
|
||||
type="button"
|
||||
className={`partner-proxy-mode-tab${payMethod === 'NATIVE' ? ' is-active' : ''}`}
|
||||
onClick={() => {
|
||||
setPayMethod('NATIVE');
|
||||
void startPay(created.id, 'NATIVE');
|
||||
}}
|
||||
disabled={!!lockedPayMethod && lockedPayMethod !== 'NATIVE'}
|
||||
onClick={() => switchPayMethod('NATIVE')}
|
||||
>
|
||||
收款码
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`partner-proxy-mode-tab${payMethod === 'JSAPI' ? ' is-active' : ''}`}
|
||||
onClick={() => {
|
||||
setPayMethod('JSAPI');
|
||||
void startPay(created.id, 'JSAPI');
|
||||
}}
|
||||
disabled={!!lockedPayMethod && lockedPayMethod !== 'JSAPI'}
|
||||
onClick={() => switchPayMethod('JSAPI')}
|
||||
>
|
||||
微信代付
|
||||
</button>
|
||||
</div>
|
||||
{lockedPayMethod ? (
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
已选择{lockedPayMethod === 'NATIVE' ? '收款码' : '微信代付'},切换方式请先取消支付
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{payMethod === 'NATIVE' ? (
|
||||
@@ -419,6 +469,16 @@ export default function ProxyOrderPage() {
|
||||
{msg}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-block"
|
||||
style={{ marginTop: 16 }}
|
||||
disabled={cancelling || paying}
|
||||
onClick={() => void cancelPay(created.id)}
|
||||
>
|
||||
{cancelling ? '取消中…' : '取消支付'}
|
||||
</button>
|
||||
</>
|
||||
) : loadingOptions ? (
|
||||
<p className="label-md text-muted">加载商品…</p>
|
||||
|
||||
@@ -40,7 +40,8 @@ import {
|
||||
validateStoreStep3,
|
||||
|
||||
patchEnvPhotoAt,
|
||||
|
||||
addEnvPhotoSlot,
|
||||
MIN_ENV_PHOTO_COUNT,
|
||||
} from '../lib/storeDraft';
|
||||
import StorePackagesForm from '../components/StorePackagesForm';
|
||||
import { normalizePackageFormItems, validatePackageFormItems } from '../lib/storePackages';
|
||||
@@ -956,7 +957,7 @@ export default function StoreCreatePage() {
|
||||
|
||||
<h3 className="headline-md">环境照片 <span className="text-primary">*</span></h3>
|
||||
|
||||
<p className="label-md text-muted" style={{ margin: '4px 0 12px' }}>至少 3 张,展示店内整洁环境</p>
|
||||
<p className="label-md text-muted" style={{ margin: '4px 0 12px' }}>至少 {MIN_ENV_PHOTO_COUNT} 张,展示店内整洁环境</p>
|
||||
|
||||
<div className="partner-upload-grid">
|
||||
|
||||
@@ -982,6 +983,17 @@ export default function StoreCreatePage() {
|
||||
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-outline"
|
||||
style={{ width: '100%', marginTop: 12 }}
|
||||
onClick={() =>
|
||||
setForm((prev) => ({ ...prev, envPhotoUrls: addEnvPhotoSlot(prev.envPhotoUrls) }))
|
||||
}
|
||||
>
|
||||
添加环境照片
|
||||
</button>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { request } from '../lib/api';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
import { canManagePartnerStore } from '../lib/partnerAccess';
|
||||
import { normalizeStringArray, patchEnvPhotoAt } from '../lib/storeDraft';
|
||||
import { MIN_ENV_PHOTO_COUNT, normalizeStringArray, patchEnvPhotoAt, addEnvPhotoSlot } from '../lib/storeDraft';
|
||||
import OssUploadField from '../components/OssUploadField';
|
||||
import TencentLocPickerOverlay from '../components/TencentLocPickerOverlay';
|
||||
import {
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
} from '../lib/storeStatus';
|
||||
|
||||
const STATUS_OPTIONS: StoreStatusValue[] = ['OPEN', 'PAUSED', 'CLOSED'];
|
||||
const ENV_SLOT_COUNT = 3;
|
||||
|
||||
function uniqueEnvUrls(urls: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
@@ -81,7 +80,7 @@ export default function StoreDetailPage() {
|
||||
.map((m) => String(m.url || '')),
|
||||
)
|
||||
: [];
|
||||
setEnvPhotoUrls(normalizeStringArray(envFromMedia, ENV_SLOT_COUNT));
|
||||
setEnvPhotoUrls(normalizeStringArray(envFromMedia, MIN_ENV_PHOTO_COUNT));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -196,8 +195,8 @@ export default function StoreDetailPage() {
|
||||
setActionError('请上传门头照');
|
||||
return;
|
||||
}
|
||||
if (nextEnv.length < ENV_SLOT_COUNT) {
|
||||
setActionError(`请上传至少 ${ENV_SLOT_COUNT} 张环境照片`);
|
||||
if (nextEnv.length < MIN_ENV_PHOTO_COUNT) {
|
||||
setActionError(`请上传至少 ${MIN_ENV_PHOTO_COUNT} 张环境照片`);
|
||||
return;
|
||||
}
|
||||
setMediaSaving(true);
|
||||
@@ -400,7 +399,7 @@ export default function StoreDetailPage() {
|
||||
<h3 className="headline-md" style={{ borderLeft: '4px solid var(--color-heritage-red)', paddingLeft: 12 }}>店内环境</h3>
|
||||
<span className="label-md text-muted">
|
||||
{canMutate && !readOnly
|
||||
? `需 ${ENV_SLOT_COUNT} 张 · 已选 ${uniqueEnvUrls(envPhotoUrls).length} 张`
|
||||
? `至少 ${MIN_ENV_PHOTO_COUNT} 张 · 已选 ${uniqueEnvUrls(envPhotoUrls).length} 张`
|
||||
: envPhotos.length
|
||||
? `已上传 ${envPhotos.length} 张`
|
||||
: '暂无照片'}
|
||||
@@ -420,6 +419,14 @@ export default function StoreDetailPage() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-outline"
|
||||
style={{ width: '100%', marginTop: 12 }}
|
||||
onClick={() => setEnvPhotoUrls((prev) => addEnvPhotoSlot(prev))}
|
||||
>
|
||||
添加环境照片
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-outline"
|
||||
|
||||
Reference in New Issue
Block a user