feat(mini-user): enable share on home/benefit and OSS qualification
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Wire WeChat share on home and benefit tabs; serve mine qualification from OSS; mask phones on admin users and wechat bindings pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -128,3 +128,18 @@ export const LEDGER_TYPE_LABELS: Record<string, string> = {
|
||||
export function fmtTime(v?: string | null) {
|
||||
return v ? new Date(v).toLocaleString('zh-CN') : '—';
|
||||
}
|
||||
|
||||
/** 手机号脱敏展示:138****5678;空值返回 — */
|
||||
export function maskPhone(phone?: string | null): string {
|
||||
const raw = String(phone ?? '').trim();
|
||||
if (!raw) return '—';
|
||||
const digits = raw.replace(/\D/g, '');
|
||||
if (digits.length >= 11) {
|
||||
return `${digits.slice(0, 3)}****${digits.slice(-4)}`;
|
||||
}
|
||||
if (digits.length >= 7) {
|
||||
return `${digits.slice(0, 3)}****${digits.slice(-2)}`;
|
||||
}
|
||||
if (digits.length > 0) return `${digits.slice(0, 1)}****`;
|
||||
return '****';
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { USER_SOURCE_TYPE_LABELS, type UserSourceType } from '@dukang/shared-types';
|
||||
import { request, type AdminUserRow, type Paginated } from '../lib/api';
|
||||
import { ORDER_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
import { ORDER_STATUS_LABELS, fmtTime, maskPhone } from '../lib/constants';
|
||||
|
||||
type UserOrderRow = {
|
||||
id: string;
|
||||
@@ -222,7 +222,7 @@ export default function UsersPage() {
|
||||
title: '手机',
|
||||
dataIndex: 'phone',
|
||||
width: 120,
|
||||
render: (v) => v || '—',
|
||||
render: (v) => maskPhone(v),
|
||||
},
|
||||
{
|
||||
title: '验手机',
|
||||
@@ -385,7 +385,7 @@ export default function UsersPage() {
|
||||
<Descriptions.Item label="ID">{detail.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户编号">{detail.userNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="昵称">{detail.nickname || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">{detail.phone || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">{maskPhone(detail.phone)}</Descriptions.Item>
|
||||
<Descriptions.Item label="验手机时间">
|
||||
{detail.phoneVerifiedAt ? new Date(detail.phoneVerifiedAt).toLocaleString('zh-CN') : '未验证'}
|
||||
</Descriptions.Item>
|
||||
@@ -419,7 +419,7 @@ export default function UsersPage() {
|
||||
<Descriptions.Item label="deviceKey">{detail.deviceKey || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="合并至">
|
||||
{detail.mergedInto
|
||||
? `${detail.mergedInto.userNo} (${detail.mergedInto.phone || '无手机'})`
|
||||
? `${detail.mergedInto.userNo} (${detail.mergedInto.phone ? maskPhone(detail.mergedInto.phone) : '无手机'})`
|
||||
: '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="被合并访客数">{detail.mergedFromCount ?? 0}</Descriptions.Item>
|
||||
@@ -593,7 +593,7 @@ export default function UsersPage() {
|
||||
columns={[
|
||||
{ title: '用户编号', dataIndex: 'userNo', width: 120 },
|
||||
{ title: '昵称', dataIndex: 'nickname', width: 100, render: (v) => v || '—' },
|
||||
{ title: '手机', dataIndex: 'phone', width: 120, render: (v) => v || '—' },
|
||||
{ title: '手机', dataIndex: 'phone', width: 120, render: (v) => maskPhone(v) },
|
||||
{
|
||||
title: '风险',
|
||||
width: 200,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { request } from '../lib/api';
|
||||
import { AdminCellLine } from '../components/AdminCellLine';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
import { fmtTime, maskPhone } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
|
||||
type ActorType = 'USER' | 'STORE' | 'PARTNER' | 'HQ';
|
||||
@@ -119,7 +119,7 @@ export default function WechatBindingsPage() {
|
||||
title: '手机号',
|
||||
dataIndex: 'primaryPhone',
|
||||
width: 140,
|
||||
render: (v) => v || '—',
|
||||
render: (v) => maskPhone(v),
|
||||
},
|
||||
{
|
||||
title: '身份摘要',
|
||||
@@ -128,7 +128,7 @@ export default function WechatBindingsPage() {
|
||||
<AdminCellLine
|
||||
primary={r.identities.map((i) => ACTOR_TYPE_LABELS[i.actorType]).join(' / ')}
|
||||
secondary={r.identities
|
||||
.map((i) => i.refLabel || i.name || i.phone)
|
||||
.map((i) => i.refLabel || i.name || (i.phone ? maskPhone(i.phone) : ''))
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
/>
|
||||
@@ -164,7 +164,7 @@ export default function WechatBindingsPage() {
|
||||
render: (_, r) => (
|
||||
<AdminCellLine
|
||||
primary={r.name || '—'}
|
||||
secondary={[r.phone, `#${r.actorId}`].filter(Boolean).join(' ')}
|
||||
secondary={[r.phone ? maskPhone(r.phone) : null, `#${r.actorId}`].filter(Boolean).join(' ')}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -3,4 +3,6 @@ export default definePageConfig({
|
||||
navigationBarTitleText: '好客权益',
|
||||
enablePullDownRefresh: true,
|
||||
backgroundTextStyle: 'dark',
|
||||
enableShareAppMessage: true,
|
||||
enableShareTimeline: true,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import ShareNavButton from '../../components/ShareNavButton';
|
||||
import WechatShareReady from '../../components/WechatShareReady';
|
||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { isLoggedIn, request, toast } from '../../lib/api';
|
||||
import { navBarStyle, tabNavContentStyle, useNavBarMetrics } from '../../lib/nav-bar';
|
||||
import {
|
||||
DEFAULT_SHARE_DESC,
|
||||
DEFAULT_SHARE_TITLE,
|
||||
toWeappShareMessage,
|
||||
} from '../../lib/wechat-share';
|
||||
|
||||
type BenefitSummary = {
|
||||
totalBalance: number;
|
||||
@@ -84,8 +91,25 @@ export default function BenefitPage() {
|
||||
const history = coupons.filter((c) => c.status === 'USED_UP' || c.status === 'VOID');
|
||||
const visible = tab === 'available' ? available : history;
|
||||
|
||||
const sharePayload = useMemo(
|
||||
() => ({
|
||||
title: '好客权益 · 杜康好客',
|
||||
desc: DEFAULT_SHARE_DESC,
|
||||
path: '/pages/benefit/index',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useShareAppMessage(() => toWeappShareMessage(sharePayload));
|
||||
useShareTimeline(() => ({
|
||||
title: sharePayload.title || DEFAULT_SHARE_TITLE,
|
||||
query: '',
|
||||
imageUrl: sharePayload.imgUrl,
|
||||
}));
|
||||
|
||||
return (
|
||||
<PageShell variant="tab" className="benefit-page">
|
||||
<WechatShareReady payload={sharePayload} />
|
||||
<View className="benefit-header" style={navBarStyle(metrics)} aria-label="好客权益">
|
||||
{process.env.TARO_ENV !== 'h5' ? (
|
||||
<Text className="benefit-header-title">好客权益</Text>
|
||||
@@ -98,6 +122,9 @@ export default function BenefitPage() {
|
||||
<View className="benefit-header-city-pin" />
|
||||
<Text>郑州市</Text>
|
||||
</View>
|
||||
<View className="benefit-header__share page-nav-bar__right-slot">
|
||||
<ShareNavButton payload={sharePayload} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -2,4 +2,6 @@ export default definePageConfig({
|
||||
navigationBarTitleText: '杜康好客',
|
||||
enablePullDownRefresh: true,
|
||||
backgroundTextStyle: 'dark',
|
||||
enableShareAppMessage: true,
|
||||
enableShareTimeline: true,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import TabMainHeader from '../../components/TabMainHeader';
|
||||
import CouponBadge from '../../components/CouponBadge';
|
||||
import WechatShareReady from '../../components/WechatShareReady';
|
||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { isLoggedIn, request, toast } from '../../lib/api';
|
||||
@@ -11,6 +12,11 @@ import { ensurePayReady } from '../../lib/pay-ready';
|
||||
import { capturePromoSceneAndTouchScan } from '../../lib/promo';
|
||||
import { getProductMainImage } from '../../lib/product-images';
|
||||
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
|
||||
import {
|
||||
DEFAULT_SHARE_DESC,
|
||||
DEFAULT_SHARE_TITLE,
|
||||
toWeappShareMessage,
|
||||
} from '../../lib/wechat-share';
|
||||
|
||||
type Product = {
|
||||
id: string;
|
||||
@@ -139,8 +145,26 @@ export default function HomePage() {
|
||||
const banners = miniHome.banners;
|
||||
const footerUrl = miniHome.footerUrl;
|
||||
|
||||
const sharePayload = useMemo(
|
||||
() => ({
|
||||
title: DEFAULT_SHARE_TITLE,
|
||||
desc: DEFAULT_SHARE_DESC,
|
||||
path: '/pages/home/index',
|
||||
imgUrl: banners[0] || undefined,
|
||||
}),
|
||||
[banners],
|
||||
);
|
||||
|
||||
useShareAppMessage(() => toWeappShareMessage(sharePayload));
|
||||
useShareTimeline(() => ({
|
||||
title: sharePayload.title || DEFAULT_SHARE_TITLE,
|
||||
query: '',
|
||||
imageUrl: sharePayload.imgUrl,
|
||||
}));
|
||||
|
||||
return (
|
||||
<PageShell variant="tab" className="home-page no-tab-header">
|
||||
<WechatShareReady payload={sharePayload} />
|
||||
<TabMainHeader title="杜康好客" />
|
||||
|
||||
{banners.length > 0 ? (
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { View, Text, Image, Button, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import { isWxAuthorizeEnabled, type ClientRuntimeConfig } from '@dukang/shared-types';
|
||||
import {
|
||||
BRAND_LOGO_MARK_URL,
|
||||
QUALIFICATION_DISCLOSURE_URL,
|
||||
isWxAuthorizeEnabled,
|
||||
type ClientRuntimeConfig,
|
||||
} from '@dukang/shared-types';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import TabMainHeader from '../../components/TabMainHeader';
|
||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||
import { BRAND_LOGO_MARK_URL } from '@dukang/shared-types';
|
||||
import { goLogin } from '../../lib/auth-nav';
|
||||
import { bindWechatForUser } from '../../lib/wechat-auth';
|
||||
import {
|
||||
@@ -18,7 +22,6 @@ import {
|
||||
} from '../../lib/mini-wechat-profile';
|
||||
import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api';
|
||||
import { isWechatEnv } from '../../lib/weixin';
|
||||
import qualificationDisclosureImg from '../../assets/qualification-disclosure.png';
|
||||
|
||||
const ORDER_SHORTCUTS = [
|
||||
{ tab: 'pending_pay', icon: '付', label: '待付款' },
|
||||
@@ -533,7 +536,7 @@ export default function MinePage() {
|
||||
<ScrollView scrollY className="mine-qualification-scroll" enhanced showScrollbar>
|
||||
<Image
|
||||
className="mine-qualification-img"
|
||||
src={qualificationDisclosureImg}
|
||||
src={QUALIFICATION_DISCLOSURE_URL}
|
||||
mode="widthFix"
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
@@ -50,6 +50,14 @@
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.benefit-header__share .page-nav-bar__btn {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.benefit-header__share .page-nav-bar__icon {
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.benefit-header-city-pin {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
||||
@@ -42,6 +42,13 @@ export const BRAND_LOGO_WIDE_URL = `${BRAND_LOGO_OSS_BASE}logo1.png`;
|
||||
/** 仅图标 Logo(默认头像:未微信授权时) */
|
||||
export const BRAND_LOGO_MARK_URL = `${BRAND_LOGO_OSS_BASE}logo2.png`;
|
||||
|
||||
/** 小程序静态资源(资质公示等) */
|
||||
export const MINI_USER_STATIC_OSS_BASE =
|
||||
'https://dukang-dev.oss-cn-beijing.aliyuncs.com/static/mini-user/';
|
||||
|
||||
/** 「我的」页资质公示长图 */
|
||||
export const QUALIFICATION_DISCLOSURE_URL = `${MINI_USER_STATIC_OSS_BASE}qualification-disclosure.png`;
|
||||
|
||||
/** 总部客服电话(C 端联系客服) */
|
||||
export const CUSTOMER_SERVICE_PHONE = '400-888-1234';
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 上传小程序「资质公示」静态图到 OSS:static/mini-user/qualification-disclosure.png
|
||||
* 用法(在 server/dukang-api): node scripts/upload-qualification-disclosure.mjs
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const OSS = require('ali-oss');
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const apiRoot = resolve(__dirname, '..');
|
||||
const repoRoot = resolve(apiRoot, '../..');
|
||||
|
||||
function loadEnvFile(path) {
|
||||
if (!existsSync(path)) return {};
|
||||
const out = {};
|
||||
for (const line of readFileSync(path, 'utf8').split(/\r?\n/)) {
|
||||
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
||||
if (!m) continue;
|
||||
let v = m[2];
|
||||
if (
|
||||
(v.startsWith('"') && v.endsWith('"')) ||
|
||||
(v.startsWith("'") && v.endsWith("'"))
|
||||
) {
|
||||
v = v.slice(1, -1);
|
||||
}
|
||||
out[m[1]] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const env = {
|
||||
...loadEnvFile(resolve(apiRoot, '.env.production')),
|
||||
...loadEnvFile(resolve(apiRoot, '.env.development')),
|
||||
...loadEnvFile(resolve(apiRoot, '.env')),
|
||||
};
|
||||
|
||||
const accessKeyId = env.OSS_ACCESS_KEY_ID || '';
|
||||
const accessKeySecret = env.OSS_ACCESS_KEY_SECRET || '';
|
||||
const bucket = env.OSS_BUCKET || '';
|
||||
const region = env.OSS_REGION || 'oss-cn-beijing';
|
||||
const cdnBase = (env.OSS_CDN_BASE || '').replace(/\/$/, '');
|
||||
|
||||
if (!accessKeyId || !accessKeySecret || !bucket) {
|
||||
console.error('缺少 OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET / OSS_BUCKET');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const localFile = resolve(
|
||||
repoRoot,
|
||||
'apps/mini-user/src/assets/qualification-disclosure.png',
|
||||
);
|
||||
if (!existsSync(localFile)) {
|
||||
console.error('本地文件不存在:', localFile);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ossKey = 'static/mini-user/qualification-disclosure.png';
|
||||
const client = new OSS({
|
||||
region,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
bucket,
|
||||
});
|
||||
|
||||
const buffer = readFileSync(localFile);
|
||||
await client.put(ossKey, buffer, {
|
||||
mime: 'image/png',
|
||||
headers: {
|
||||
'Content-Disposition': 'inline',
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
},
|
||||
});
|
||||
|
||||
const url = cdnBase
|
||||
? `${cdnBase}/${ossKey}`
|
||||
: `https://${bucket}.${region}.aliyuncs.com/${ossKey}`;
|
||||
|
||||
console.log('uploaded:', ossKey);
|
||||
console.log('url:', url);
|
||||
Reference in New Issue
Block a user