Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
|
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
|
||||||
import { enqueueUpload } from '../lib/upload-lock';
|
import { enqueueUpload } from '../lib/upload-lock';
|
||||||
import { formatChooseImageFailMessage } from '@dukang/weixin-sdk';
|
|
||||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
|
||||||
import { toastError } from '../lib/toast';
|
import { toastError } from '../lib/toast';
|
||||||
|
|
||||||
type OssUploadFieldProps = {
|
type OssUploadFieldProps = {
|
||||||
@@ -14,27 +12,10 @@ type OssUploadFieldProps = {
|
|||||||
wide?: boolean;
|
wide?: boolean;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
/** 兼容现有调用:选图不再依赖 openId 绑定 */
|
|
||||||
wechatReady?: boolean;
|
|
||||||
onWechatReadyChange?: (ready: boolean) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_MAX_MB = 10;
|
const DEFAULT_MAX_MB = 10;
|
||||||
|
|
||||||
function formatWechatUploadError(e: unknown): string {
|
|
||||||
const msg = e instanceof Error ? e.message : '无法打开相册';
|
|
||||||
const formatted = formatChooseImageFailMessage(msg);
|
|
||||||
if (formatted) return formatted;
|
|
||||||
if (/invalid signature/i.test(msg)) {
|
|
||||||
return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名,并刷新页面后重试';
|
|
||||||
}
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
function acceptsImages(accept: string) {
|
|
||||||
return accept.includes('image');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function OssUploadField({
|
export default function OssUploadField({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -48,7 +29,6 @@ export default function OssUploadField({
|
|||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [showAlbumFallback, setShowAlbumFallback] = useState(false);
|
|
||||||
|
|
||||||
function showUploadError(text: string) {
|
function showUploadError(text: string) {
|
||||||
setError(text);
|
setError(text);
|
||||||
@@ -57,17 +37,6 @@ export default function OssUploadField({
|
|||||||
|
|
||||||
const resolvedAccept =
|
const resolvedAccept =
|
||||||
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? 'image/*,.pdf' : 'image/*');
|
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? 'image/*,.pdf' : 'image/*');
|
||||||
const inWechat = isWechatEnv();
|
|
||||||
const useWechatPicker =
|
|
||||||
inWechat && (mediaType === 'IMAGE' || (mediaType === 'FILE' && acceptsImages(resolvedAccept)));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!useWechatPicker) return;
|
|
||||||
weixinSdk.reset();
|
|
||||||
void weixinSdk.init().catch(() => {
|
|
||||||
/* 点击上传时会再次初始化 */
|
|
||||||
});
|
|
||||||
}, [useWechatPicker]);
|
|
||||||
|
|
||||||
async function persistUpload(file: File) {
|
async function persistUpload(file: File) {
|
||||||
if (!file.size) {
|
if (!file.size) {
|
||||||
@@ -95,60 +64,21 @@ export default function OssUploadField({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pickWechatImage() {
|
function pickFile() {
|
||||||
setUploading(true);
|
|
||||||
setError('');
|
|
||||||
try {
|
|
||||||
weixinSdk.reset();
|
|
||||||
await weixinSdk.init();
|
|
||||||
// 选图 + 上传须在同一个队列任务内完成,避免嵌套 enqueueUpload 死锁
|
|
||||||
await enqueueUpload(async () => {
|
|
||||||
const files = await weixinSdk.chooseImages({
|
|
||||||
count: 1,
|
|
||||||
sourceType: ['album', 'camera'],
|
|
||||||
});
|
|
||||||
if (!files?.[0]) return;
|
|
||||||
await persistUpload(files[0]);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
const msg = e instanceof Error ? e.message : '无法打开相册';
|
|
||||||
if (/cancel/i.test(msg)) return;
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
setUploading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pickFile() {
|
|
||||||
if (uploading) return;
|
if (uploading) return;
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
if (useWechatPicker) {
|
|
||||||
try {
|
|
||||||
await pickWechatImage();
|
|
||||||
} catch (e) {
|
|
||||||
const msg = e instanceof Error ? e.message : '无法打开相册';
|
|
||||||
if (/cancel/i.test(msg)) return;
|
|
||||||
const formatted = formatWechatUploadError(e);
|
|
||||||
setError(`${formatted},可改从系统相册选择`);
|
|
||||||
setShowAlbumFallback(true);
|
|
||||||
inputRef.current?.click();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inputRef.current?.click();
|
inputRef.current?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isImage = mediaType === 'IMAGE' && value;
|
const isImage = mediaType === 'IMAGE' && value;
|
||||||
const isFile = mediaType === 'FILE' && value;
|
const isFile = mediaType === 'FILE' && value;
|
||||||
const busy = uploading;
|
const busy = uploading;
|
||||||
const pickerLabel = label ?? (useWechatPicker ? '拍照 / 从相册选择' : '点击上传');
|
const pickerLabel = label ?? (mediaType === 'IMAGE' ? '从系统相册选择' : '点击上传');
|
||||||
|
|
||||||
const triggerProps = {
|
const triggerProps = {
|
||||||
type: 'button' as const,
|
type: 'button' as const,
|
||||||
disabled: busy,
|
disabled: busy,
|
||||||
onClick: () => void pickFile(),
|
onClick: pickFile,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -162,7 +92,6 @@ export default function OssUploadField({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
setShowAlbumFallback(false);
|
|
||||||
void uploadSelectedFile(file);
|
void uploadSelectedFile(file);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -191,24 +120,13 @@ export default function OssUploadField({
|
|||||||
className={`partner-upload-dashed${wide ? ' partner-upload-dashed--wide' : ''}${compact ? ' partner-upload-dashed--compact' : ''}`}
|
className={`partner-upload-dashed${wide ? ' partner-upload-dashed--wide' : ''}${compact ? ' partner-upload-dashed--compact' : ''}`}
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-primary" style={{ fontSize: compact ? 28 : 36 }}>
|
<span className="material-symbols-outlined text-primary" style={{ fontSize: compact ? 28 : 36 }}>
|
||||||
{busy ? 'hourglass_top' : 'add_a_photo'}
|
{busy ? 'hourglass_top' : 'photo_library'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-primary" style={{ fontWeight: 500 }}>
|
<span className="text-primary" style={{ fontWeight: 500 }}>
|
||||||
{uploading ? '上传中…' : pickerLabel}
|
{uploading ? '上传中…' : pickerLabel}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{showAlbumFallback && useWechatPicker && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="partner-btn-outline"
|
|
||||||
style={{ marginTop: 8, width: '100%' }}
|
|
||||||
disabled={busy}
|
|
||||||
onClick={() => inputRef.current?.click()}
|
|
||||||
>
|
|
||||||
从系统相册选择
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{error && <p className="partner-form-error" role="alert">{error}</p>}
|
{error && <p className="partner-form-error" role="alert">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -17,8 +17,6 @@ import { checkStorePhoneAvailable, sendStorePhoneSms } from '../lib/storePhone';
|
|||||||
|
|
||||||
import { fetchPartnerCities, type OpenCityOption } from '../lib/upload';
|
import { fetchPartnerCities, type OpenCityOption } from '../lib/upload';
|
||||||
|
|
||||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
|
||||||
|
|
||||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -79,16 +77,10 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { account, refresh } = usePartnerSession();
|
const { account } = usePartnerSession();
|
||||||
|
|
||||||
const accountId = account?.id;
|
const accountId = account?.id;
|
||||||
|
|
||||||
const wechatReady = !!account?.hasWechat;
|
|
||||||
|
|
||||||
const handleWechatReadyChange = useCallback(() => {
|
|
||||||
void refresh();
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
const [params, setParams] = useSearchParams();
|
const [params, setParams] = useSearchParams();
|
||||||
|
|
||||||
const saved = loadStoreDraft(accountId);
|
const saved = loadStoreDraft(accountId);
|
||||||
@@ -142,17 +134,6 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (step !== 2 || !isWechatEnv()) return;
|
|
||||||
void refresh();
|
|
||||||
weixinSdk.reset();
|
|
||||||
void weixinSdk.init().catch(() => {
|
|
||||||
/* OssUploadField 点击时会再次初始化 */
|
|
||||||
});
|
|
||||||
}, [step, refresh]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
void fetchPartnerCities()
|
void fetchPartnerCities()
|
||||||
@@ -895,13 +876,9 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
value={form.coverUrl}
|
value={form.coverUrl}
|
||||||
|
|
||||||
wechatReady={wechatReady}
|
|
||||||
|
|
||||||
onWechatReadyChange={handleWechatReadyChange}
|
|
||||||
|
|
||||||
onChange={(coverUrl) => patchForm({ coverUrl })}
|
onChange={(coverUrl) => patchForm({ coverUrl })}
|
||||||
|
|
||||||
label="点击或拖拽上传"
|
label="从系统相册选择"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -931,10 +908,6 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
value={url}
|
value={url}
|
||||||
|
|
||||||
wechatReady={wechatReady}
|
|
||||||
|
|
||||||
onWechatReadyChange={handleWechatReadyChange}
|
|
||||||
|
|
||||||
onChange={(nextUrl) => patchEnvPhotoUrl(index, nextUrl)}
|
onChange={(nextUrl) => patchEnvPhotoUrl(index, nextUrl)}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
@@ -951,7 +924,7 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
<h3 className="headline-md">签约合同 <span className="text-primary">*</span></h3>
|
<h3 className="headline-md">签约合同 <span className="text-primary">*</span></h3>
|
||||||
|
|
||||||
<p className="label-md text-muted" style={{ margin: '4px 0 12px' }}>拍照上传签约协议首页与盖章页</p>
|
<p className="label-md text-muted" style={{ margin: '4px 0 12px' }}>上传签约协议首页与盖章页</p>
|
||||||
|
|
||||||
<OssUploadField
|
<OssUploadField
|
||||||
|
|
||||||
@@ -963,10 +936,6 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
value={form.contractUrl}
|
value={form.contractUrl}
|
||||||
|
|
||||||
wechatReady={wechatReady}
|
|
||||||
|
|
||||||
onWechatReadyChange={handleWechatReadyChange}
|
|
||||||
|
|
||||||
onChange={(contractUrl) => patchForm({ contractUrl })}
|
onChange={(contractUrl) => patchForm({ contractUrl })}
|
||||||
|
|
||||||
label="上传合同副本"
|
label="上传合同副本"
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export default function StoreDetailPage() {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [mediaSaving, setMediaSaving] = useState(false);
|
const [mediaSaving, setMediaSaving] = useState(false);
|
||||||
const [actionError, setActionError] = useState('');
|
const [actionError, setActionError] = useState('');
|
||||||
const [wechatReady, setWechatReady] = useState(false);
|
|
||||||
|
|
||||||
function applyStore(data: Record<string, unknown>) {
|
function applyStore(data: Record<string, unknown>) {
|
||||||
setStore(data);
|
setStore(data);
|
||||||
@@ -284,10 +283,8 @@ export default function StoreDetailPage() {
|
|||||||
bizType="STORE_TITLE"
|
bizType="STORE_TITLE"
|
||||||
mediaType="IMAGE"
|
mediaType="IMAGE"
|
||||||
value={coverUrl}
|
value={coverUrl}
|
||||||
wechatReady={wechatReady}
|
|
||||||
onWechatReadyChange={setWechatReady}
|
|
||||||
onChange={setCoverUrl}
|
onChange={setCoverUrl}
|
||||||
label="点击更换门头照"
|
label="从系统相册选择"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -336,8 +333,6 @@ export default function StoreDetailPage() {
|
|||||||
bizType="STORE_ENV"
|
bizType="STORE_ENV"
|
||||||
mediaType="IMAGE"
|
mediaType="IMAGE"
|
||||||
value={url}
|
value={url}
|
||||||
wechatReady={wechatReady}
|
|
||||||
onWechatReadyChange={setWechatReady}
|
|
||||||
onChange={(nextUrl) => setEnvPhotoUrls((prev) => patchEnvPhotoAt(prev, index, nextUrl))}
|
onChange={(nextUrl) => setEnvPhotoUrls((prev) => patchEnvPhotoAt(prev, index, nextUrl))}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import LoginPage from './pages/LoginPage';
|
|||||||
import LegalPage from './pages/LegalPage';
|
import LegalPage from './pages/LegalPage';
|
||||||
import SelectStorePage from './pages/SelectStorePage';
|
import SelectStorePage from './pages/SelectStorePage';
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
import RedeemConfirmPage from './pages/RedeemConfirmPage';
|
|
||||||
import PhoneRedeemPage from './pages/PhoneRedeemPage';
|
import PhoneRedeemPage from './pages/PhoneRedeemPage';
|
||||||
import RedeemSuccessPage from './pages/RedeemSuccessPage';
|
import RedeemSuccessPage from './pages/RedeemSuccessPage';
|
||||||
import RecordsPage from './pages/RecordsPage';
|
import RecordsPage from './pages/RecordsPage';
|
||||||
@@ -22,7 +21,7 @@ export default function App() {
|
|||||||
<Route path="/legal/privacy-policy" element={<LegalPage docId="privacy-policy" />} />
|
<Route path="/legal/privacy-policy" element={<LegalPage docId="privacy-policy" />} />
|
||||||
<Route path="/select-store" element={<SelectStorePage />} />
|
<Route path="/select-store" element={<SelectStorePage />} />
|
||||||
<Route path="/staff" element={<StaffPage />} />
|
<Route path="/staff" element={<StaffPage />} />
|
||||||
<Route path="/redeem" element={<RedeemConfirmPage />} />
|
<Route path="/redeem" element={<Navigate to="/redeem/phone" replace />} />
|
||||||
<Route path="/redeem/phone" element={<PhoneRedeemPage />} />
|
<Route path="/redeem/phone" element={<PhoneRedeemPage />} />
|
||||||
<Route path="/redeem/success" element={<RedeemSuccessPage />} />
|
<Route path="/redeem/success" element={<RedeemSuccessPage />} />
|
||||||
<Route element={<TabLayout />}>
|
<Route element={<TabLayout />}>
|
||||||
|
|||||||
@@ -1,47 +1,15 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useStoreSession } from '../contexts/StoreSessionContext';
|
|
||||||
import { request } from '../lib/api';
|
import { request } from '../lib/api';
|
||||||
import { parseRedeemTokenFromScan } from '../lib/redeem-scan';
|
|
||||||
import {
|
|
||||||
authorizeShopWechat,
|
|
||||||
checkNeedsWechatAuth,
|
|
||||||
fetchShopAccount,
|
|
||||||
handleShopWechatCallback,
|
|
||||||
handleShopWechatLoginResult,
|
|
||||||
} from '../lib/wechat-auth';
|
|
||||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
|
||||||
import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
|
|
||||||
import WechatScanAuthModal from '../components/WechatScanAuthModal';
|
|
||||||
|
|
||||||
const PENDING_SCAN_KEY = 'shop_pending_scan';
|
|
||||||
|
|
||||||
function formatMoney(n: number) {
|
function formatMoney(n: number) {
|
||||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatScanError(e: unknown): string {
|
|
||||||
const msg = e instanceof Error ? e.message : '扫码失败,请重试';
|
|
||||||
if (/invalid signature/i.test(msg)) {
|
|
||||||
return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名为 user.runxian.top,并刷新页面后重试';
|
|
||||||
}
|
|
||||||
if (/offline verifying|权限验证中|接口未就绪/i.test(msg)) {
|
|
||||||
return `${msg}(可在 URL 后加 ?wxdebug=1 开启 JSSDK 调试查看详情)`;
|
|
||||||
}
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { applySession } = useStoreSession();
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const [dash, setDash] = useState<Record<string, unknown> | null>(null);
|
const [dash, setDash] = useState<Record<string, unknown> | null>(null);
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
const [scanMsg, setScanMsg] = useState('');
|
|
||||||
const [scanning, setScanning] = useState(false);
|
|
||||||
const [authModalOpen, setAuthModalOpen] = useState(false);
|
|
||||||
const [authLoading, setAuthLoading] = useState(false);
|
|
||||||
const [authError, setAuthError] = useState('');
|
|
||||||
|
|
||||||
const loadDashboard = useCallback(() => {
|
const loadDashboard = useCallback(() => {
|
||||||
return request<Record<string, unknown>>('SHOP_H5', '/shop/dashboard')
|
return request<Record<string, unknown>>('SHOP_H5', '/shop/dashboard')
|
||||||
@@ -58,7 +26,6 @@ export default function HomePage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onResume() {
|
function onResume() {
|
||||||
setScanning(false);
|
|
||||||
void loadDashboard();
|
void loadDashboard();
|
||||||
}
|
}
|
||||||
function onVisibility() {
|
function onVisibility() {
|
||||||
@@ -74,88 +41,6 @@ export default function HomePage() {
|
|||||||
};
|
};
|
||||||
}, [loadDashboard]);
|
}, [loadDashboard]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isWechatEnv() || !searchParams.get('code')) return;
|
|
||||||
void handleShopWechatCallback()
|
|
||||||
.then((result) => {
|
|
||||||
if (!result) return;
|
|
||||||
const session = handleShopWechatLoginResult(result);
|
|
||||||
if (session) {
|
|
||||||
applySession(session);
|
|
||||||
}
|
|
||||||
setAuthModalOpen(false);
|
|
||||||
setAuthError('');
|
|
||||||
stripOAuthParamsFromLocation();
|
|
||||||
setSearchParams({}, { replace: true });
|
|
||||||
const shouldScan = sessionStorage.getItem(PENDING_SCAN_KEY) === '1';
|
|
||||||
sessionStorage.removeItem(PENDING_SCAN_KEY);
|
|
||||||
if (shouldScan) {
|
|
||||||
window.setTimeout(() => void runScan(), 0);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
setAuthError(e instanceof Error ? e.message : '微信授权失败');
|
|
||||||
});
|
|
||||||
}, [searchParams, applySession, setSearchParams]);
|
|
||||||
|
|
||||||
async function runScan() {
|
|
||||||
if (!isWechatEnv()) {
|
|
||||||
setScanMsg('请在微信内打开门店端进行扫码核销');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setScanning(true);
|
|
||||||
setScanMsg('');
|
|
||||||
try {
|
|
||||||
await weixinSdk.init();
|
|
||||||
const raw = await weixinSdk.scanQrCode();
|
|
||||||
if (!raw) {
|
|
||||||
void loadDashboard();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const token = parseRedeemTokenFromScan(raw);
|
|
||||||
if (!token) {
|
|
||||||
setScanMsg('无法识别核销码,请扫描用户出示的核销二维码');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigate(`/redeem?token=${encodeURIComponent(token)}`);
|
|
||||||
} catch (e) {
|
|
||||||
setScanMsg(formatScanError(e));
|
|
||||||
} finally {
|
|
||||||
setScanning(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleScan() {
|
|
||||||
setScanMsg('');
|
|
||||||
if (!isWechatEnv()) {
|
|
||||||
setScanMsg('请在微信内打开门店端进行扫码核销');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const profile = await fetchShopAccount();
|
|
||||||
if (await checkNeedsWechatAuth(profile)) {
|
|
||||||
setAuthModalOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await runScan();
|
|
||||||
} catch (e) {
|
|
||||||
setScanMsg(e instanceof Error ? e.message : '无法发起扫码');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startWechatAuth() {
|
|
||||||
setAuthLoading(true);
|
|
||||||
setAuthError('');
|
|
||||||
try {
|
|
||||||
sessionStorage.setItem(PENDING_SCAN_KEY, '1');
|
|
||||||
await authorizeShopWechat();
|
|
||||||
} catch (e) {
|
|
||||||
sessionStorage.removeItem(PENDING_SCAN_KEY);
|
|
||||||
setAuthError(e instanceof Error ? e.message : '微信授权失败');
|
|
||||||
setAuthLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = dash?.store as Record<string, unknown> | undefined;
|
const store = dash?.store as Record<string, unknown> | undefined;
|
||||||
const recent = (dash?.recentRecords as Array<Record<string, unknown>>) || [];
|
const recent = (dash?.recentRecords as Array<Record<string, unknown>>) || [];
|
||||||
const openTime = String(store?.openTime || '10:00');
|
const openTime = String(store?.openTime || '10:00');
|
||||||
@@ -189,20 +74,13 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="shop-home-scan">
|
<section className="shop-home-scan">
|
||||||
<button
|
<Link
|
||||||
type="button"
|
to="/redeem/phone"
|
||||||
className="shop-home-scan-btn"
|
className="shop-home-scan-btn"
|
||||||
disabled={scanning}
|
|
||||||
onClick={() => void handleScan()}
|
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined">qr_code_scanner</span>
|
|
||||||
</button>
|
|
||||||
<p className="shop-home-scan-label">{scanning ? '正在打开相机…' : '扫码核销'}</p>
|
|
||||||
{scanMsg && <p className="shop-home-scan-msg" role="alert">{scanMsg}</p>}
|
|
||||||
<Link to="/redeem/phone" className="shop-home-phone-link">
|
|
||||||
<span className="material-symbols-outlined">smartphone</span>
|
<span className="material-symbols-outlined">smartphone</span>
|
||||||
手机号核销
|
|
||||||
</Link>
|
</Link>
|
||||||
|
<p className="shop-home-scan-label">手机号核销</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="shop-home-status">
|
<section className="shop-home-status">
|
||||||
@@ -249,17 +127,6 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WechatScanAuthModal
|
|
||||||
open={authModalOpen}
|
|
||||||
loading={authLoading}
|
|
||||||
error={authError}
|
|
||||||
onAuthorize={() => void startWechatAuth()}
|
|
||||||
onCancel={() => {
|
|
||||||
setAuthModalOpen(false);
|
|
||||||
setAuthError('');
|
|
||||||
sessionStorage.removeItem(PENDING_SCAN_KEY);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user