feat(release): v3.4.13 experience optimizations across admin, mini-user, and H5 login
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,11 +13,16 @@ type Row = {
|
|||||||
settleAmount: number;
|
settleAmount: number;
|
||||||
channel?: RedeemChannel;
|
channel?: RedeemChannel;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
user?: { userNo: string; phone: string | null };
|
user?: { userNo: string; phone: string | null; nickname?: string | null };
|
||||||
store?: { name: string; cityName: string };
|
store?: { name: string; cityName: string };
|
||||||
coupon?: { couponNo: string };
|
coupon?: { couponNo: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function maskPhone(phone: string | null | undefined) {
|
||||||
|
if (!phone || phone.length < 7) return phone ?? '—';
|
||||||
|
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default function RedeemRecordsPage() {
|
export default function RedeemRecordsPage() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||||
@@ -50,7 +55,19 @@ export default function RedeemRecordsPage() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ title: '用户', dataIndex: ['user', 'userNo'], width: 110 },
|
{ title: '用户编号', dataIndex: ['user', 'userNo'], width: 110 },
|
||||||
|
{
|
||||||
|
title: '用户昵称',
|
||||||
|
dataIndex: ['user', 'nickname'],
|
||||||
|
width: 100,
|
||||||
|
render: (v: string | null | undefined) => v || '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户手机',
|
||||||
|
dataIndex: ['user', 'phone'],
|
||||||
|
width: 120,
|
||||||
|
render: (v: string | null | undefined) => maskPhone(v),
|
||||||
|
},
|
||||||
{ title: '门店', dataIndex: ['store', 'name'] },
|
{ title: '门店', dataIndex: ['store', 'name'] },
|
||||||
{ title: '核销额', dataIndex: 'amount', width: 90, render: (v) => `¥${v}` },
|
{ title: '核销额', dataIndex: 'amount', width: 90, render: (v) => `¥${v}` },
|
||||||
{ title: '结算额', dataIndex: 'settleAmount', width: 90, render: (v) => `¥${v}` },
|
{ title: '结算额', dataIndex: 'settleAmount', width: 90, render: (v) => `¥${v}` },
|
||||||
@@ -113,7 +130,7 @@ export default function RedeemRecordsPage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.items ?? []}
|
dataSource={data?.items ?? []}
|
||||||
scroll={{ x: 1100 }}
|
scroll={{ x: 1300 }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -138,6 +155,19 @@ export default function RedeemRecordsPage() {
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="核销额">¥{String(detail.amount)}</Descriptions.Item>
|
<Descriptions.Item label="核销额">¥{String(detail.amount)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="结算额">¥{String(detail.settleAmount)}</Descriptions.Item>
|
<Descriptions.Item label="结算额">¥{String(detail.settleAmount)}</Descriptions.Item>
|
||||||
|
{detail.user && typeof detail.user === 'object' ? (
|
||||||
|
<>
|
||||||
|
<Descriptions.Item label="用户编号">
|
||||||
|
{String((detail.user as { userNo?: string }).userNo ?? '—')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="用户昵称">
|
||||||
|
{String((detail.user as { nickname?: string | null }).nickname ?? '—')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="用户手机">
|
||||||
|
{maskPhone((detail.user as { phone?: string | null }).phone)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt))}</Descriptions.Item>
|
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt))}</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
DEV_PLAN_TASK_TYPE_LABELS,
|
DEV_PLAN_TASK_TYPE_LABELS,
|
||||||
SUPPORT_TICKET_STATUS_LABELS,
|
SUPPORT_TICKET_STATUS_LABELS,
|
||||||
SUPPORT_TICKET_TYPE_LABELS,
|
SUPPORT_TICKET_TYPE_LABELS,
|
||||||
|
SUPPORT_TICKET_PRIORITY_LABELS,
|
||||||
mapSupportTicketTypeToDevPlanTask,
|
mapSupportTicketTypeToDevPlanTask,
|
||||||
type BatchReviewPreviewItem,
|
type BatchReviewPreviewItem,
|
||||||
type BatchReviewPreviewResponse,
|
type BatchReviewPreviewResponse,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
type DevPlanVersionDto,
|
type DevPlanVersionDto,
|
||||||
type SupportTicketDto,
|
type SupportTicketDto,
|
||||||
type SupportTicketLinkedTaskDto,
|
type SupportTicketLinkedTaskDto,
|
||||||
|
type SupportTicketPriorityDto,
|
||||||
type SupportTicketStatusDto,
|
type SupportTicketStatusDto,
|
||||||
type SupportTicketTypeDto,
|
type SupportTicketTypeDto,
|
||||||
} from '@dukang/shared-types';
|
} from '@dukang/shared-types';
|
||||||
@@ -64,6 +66,17 @@ const STATUS_OPTIONS = (Object.keys(SUPPORT_TICKET_STATUS_LABELS) as SupportTick
|
|||||||
(value) => ({ value, label: SUPPORT_TICKET_STATUS_LABELS[value] }),
|
(value) => ({ value, label: SUPPORT_TICKET_STATUS_LABELS[value] }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PRIORITY_COLOR: Record<SupportTicketPriorityDto, string> = {
|
||||||
|
LOW: 'default',
|
||||||
|
NORMAL: 'blue',
|
||||||
|
HIGH: 'orange',
|
||||||
|
URGENT: 'red',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PRIORITY_OPTIONS = (Object.keys(SUPPORT_TICKET_PRIORITY_LABELS) as SupportTicketPriorityDto[]).map(
|
||||||
|
(value) => ({ value, label: SUPPORT_TICKET_PRIORITY_LABELS[value] }),
|
||||||
|
);
|
||||||
|
|
||||||
const TASK_TYPE_OPTIONS = (Object.keys(DEV_PLAN_TASK_TYPE_LABELS) as DevPlanTaskTypeDto[]).map(
|
const TASK_TYPE_OPTIONS = (Object.keys(DEV_PLAN_TASK_TYPE_LABELS) as DevPlanTaskTypeDto[]).map(
|
||||||
(v) => ({ value: v, label: DEV_PLAN_TASK_TYPE_LABELS[v] }),
|
(v) => ({ value: v, label: DEV_PLAN_TASK_TYPE_LABELS[v] }),
|
||||||
);
|
);
|
||||||
@@ -80,6 +93,7 @@ export default function SupportTicketsPage() {
|
|||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (filters.ticketType) qs.set('ticketType', filters.ticketType);
|
if (filters.ticketType) qs.set('ticketType', filters.ticketType);
|
||||||
if (filters.status) qs.set('status', filters.status);
|
if (filters.status) qs.set('status', filters.status);
|
||||||
|
if (filters.priority) qs.set('priority', filters.priority);
|
||||||
return qs;
|
return qs;
|
||||||
},
|
},
|
||||||
[filters],
|
[filters],
|
||||||
@@ -154,6 +168,7 @@ export default function SupportTicketsPage() {
|
|||||||
title: values.title.trim(),
|
title: values.title.trim(),
|
||||||
content: values.content?.trim() || undefined,
|
content: values.content?.trim() || undefined,
|
||||||
remark: values.remark?.trim() || undefined,
|
remark: values.remark?.trim() || undefined,
|
||||||
|
priority: values.priority,
|
||||||
attachmentUrls: (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean),
|
attachmentUrls: (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -217,6 +232,7 @@ export default function SupportTicketsPage() {
|
|||||||
title: detail.title,
|
title: detail.title,
|
||||||
content: detail.content || '',
|
content: detail.content || '',
|
||||||
remark: detail.remark || '',
|
remark: detail.remark || '',
|
||||||
|
priority: detail.priority ?? 'NORMAL',
|
||||||
attachmentUrls: detail.attachmentUrls?.length ? detail.attachmentUrls : [''],
|
attachmentUrls: detail.attachmentUrls?.length ? detail.attachmentUrls : [''],
|
||||||
});
|
});
|
||||||
setEditOpen(true);
|
setEditOpen(true);
|
||||||
@@ -233,6 +249,7 @@ export default function SupportTicketsPage() {
|
|||||||
title: values.title.trim(),
|
title: values.title.trim(),
|
||||||
content: values.content?.trim() || undefined,
|
content: values.content?.trim() || undefined,
|
||||||
remark: values.remark?.trim() || undefined,
|
remark: values.remark?.trim() || undefined,
|
||||||
|
priority: values.priority,
|
||||||
attachmentUrls: (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean),
|
attachmentUrls: (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -451,6 +468,16 @@ export default function SupportTicketsPage() {
|
|||||||
width: 90,
|
width: 90,
|
||||||
render: (t: SupportTicketTypeDto) => SUPPORT_TICKET_TYPE_LABELS[t] ?? t,
|
render: (t: SupportTicketTypeDto) => SUPPORT_TICKET_TYPE_LABELS[t] ?? t,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '优先级',
|
||||||
|
dataIndex: 'priority',
|
||||||
|
width: 90,
|
||||||
|
render: (p: SupportTicketPriorityDto) => (
|
||||||
|
<Tag color={PRIORITY_COLOR[p] ?? 'default'}>
|
||||||
|
{SUPPORT_TICKET_PRIORITY_LABELS[p] ?? p ?? '普通'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
@@ -562,6 +589,9 @@ export default function SupportTicketsPage() {
|
|||||||
<Form.Item name="status" label="状态">
|
<Form.Item name="status" label="状态">
|
||||||
<Select allowClear style={{ width: 120 }} options={STATUS_OPTIONS} />
|
<Select allowClear style={{ width: 120 }} options={STATUS_OPTIONS} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="priority" label="优先级">
|
||||||
|
<Select allowClear style={{ width: 120 }} options={PRIORITY_OPTIONS} />
|
||||||
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type="primary" htmlType="submit">
|
||||||
筛选
|
筛选
|
||||||
</Button>
|
</Button>
|
||||||
@@ -603,6 +633,9 @@ export default function SupportTicketsPage() {
|
|||||||
{detail.ticketNo}
|
{detail.ticketNo}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Tag>{SUPPORT_TICKET_TYPE_LABELS[detail.ticketType] ?? detail.ticketType}</Tag>
|
<Tag>{SUPPORT_TICKET_TYPE_LABELS[detail.ticketType] ?? detail.ticketType}</Tag>
|
||||||
|
<Tag color={PRIORITY_COLOR[detail.priority ?? 'NORMAL']}>
|
||||||
|
{SUPPORT_TICKET_PRIORITY_LABELS[detail.priority ?? 'NORMAL']}
|
||||||
|
</Tag>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -861,10 +894,13 @@ export default function SupportTicketsPage() {
|
|||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<Modal title="创建技术支持工单" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={() => void submitCreate()} confirmLoading={creating} destroyOnClose okText="提交">
|
<Modal title="创建技术支持工单" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={() => void submitCreate()} confirmLoading={creating} destroyOnClose okText="提交">
|
||||||
<Form form={createForm} layout="vertical" initialValues={{ ticketType: 'BUG', attachmentUrls: [''] }}>
|
<Form form={createForm} layout="vertical" initialValues={{ ticketType: 'BUG', priority: 'NORMAL', attachmentUrls: [''] }}>
|
||||||
<Form.Item name="ticketType" label="类型" rules={[{ required: true }]}>
|
<Form.Item name="ticketType" label="类型" rules={[{ required: true }]}>
|
||||||
<Select options={TYPE_OPTIONS} />
|
<Select options={TYPE_OPTIONS} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="priority" label="优先级" rules={[{ required: true }]}>
|
||||||
|
<Select options={PRIORITY_OPTIONS} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
|
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
|
||||||
<Input maxLength={128} showCount />
|
<Input maxLength={128} showCount />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -968,6 +1004,9 @@ export default function SupportTicketsPage() {
|
|||||||
<Form.Item name="ticketType" label="类型" rules={[{ required: true }]}>
|
<Form.Item name="ticketType" label="类型" rules={[{ required: true }]}>
|
||||||
<Select options={TYPE_OPTIONS} />
|
<Select options={TYPE_OPTIONS} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="priority" label="优先级" rules={[{ required: true }]}>
|
||||||
|
<Select options={PRIORITY_OPTIONS} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
|
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
|
||||||
<Input maxLength={128} showCount />
|
<Input maxLength={128} showCount />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -212,6 +212,14 @@ export default function PromoCodeDetailPage() {
|
|||||||
<Statistic title="扫码进入次数" value={stats?.scanCount ?? detail.scanCount} />
|
<Statistic title="扫码进入次数" value={stats?.scanCount ?? detail.scanCount} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col xs={12} sm={6}>
|
||||||
|
<Card size="small">
|
||||||
|
<Statistic
|
||||||
|
title="归因用户数"
|
||||||
|
value={stats?.attributionCount ?? 0}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic
|
<Statistic
|
||||||
|
|||||||
@@ -168,12 +168,22 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
if (!ensureAgreed()) return;
|
if (!ensureAgreed()) return;
|
||||||
|
const trimmedPhone = phone.trim();
|
||||||
|
const trimmedCode = code.trim();
|
||||||
|
if (!trimmedPhone) {
|
||||||
|
setMsg('phone should not be empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!trimmedCode) {
|
||||||
|
setMsg('code should not be empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMsg('');
|
setMsg('');
|
||||||
try {
|
try {
|
||||||
const data = await request<PartnerSessionPayload>('PARTNER_H5', '/partner/auth/login/sms', {
|
const data = await request<PartnerSessionPayload>('PARTNER_H5', '/partner/auth/login/sms', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ phone, code }),
|
body: JSON.stringify({ phone: trimmedPhone, code: trimmedCode }),
|
||||||
silent: true,
|
silent: true,
|
||||||
});
|
});
|
||||||
saveRememberedSession(data);
|
saveRememberedSession(data);
|
||||||
|
|||||||
@@ -140,12 +140,22 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
if (!ensureAgreed()) return;
|
if (!ensureAgreed()) return;
|
||||||
|
const trimmedPhone = phone.trim();
|
||||||
|
const trimmedCode = code.trim();
|
||||||
|
if (!trimmedPhone) {
|
||||||
|
setMsg('phone should not be empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!trimmedCode) {
|
||||||
|
setMsg('code should not be empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMsg('');
|
setMsg('');
|
||||||
try {
|
try {
|
||||||
const data = await request<ShopSessionPayload>('SHOP_H5', '/shop/auth/login/sms', {
|
const data = await request<ShopSessionPayload>('SHOP_H5', '/shop/auth/login/sms', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ phone, code }),
|
body: JSON.stringify({ phone: trimmedPhone, code: trimmedCode }),
|
||||||
});
|
});
|
||||||
saveRememberedSession(data);
|
saveRememberedSession(data);
|
||||||
applySession(data);
|
applySession(data);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dukang/mini-user",
|
"name": "@dukang/mini-user",
|
||||||
"version": "0.1.0",
|
"version": "3.4.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "杜康好客 · C 端用户微信小程序(Taro)",
|
"description": "杜康好客 · C 端用户微信小程序(Taro)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default defineAppConfig({
|
|||||||
'pages/pay/index',
|
'pages/pay/index',
|
||||||
'pages/orders/index',
|
'pages/orders/index',
|
||||||
'pages/order-detail/index',
|
'pages/order-detail/index',
|
||||||
|
'pages/order-logistics/index',
|
||||||
'pages/pickup-receive/index',
|
'pages/pickup-receive/index',
|
||||||
'pages/addresses/index',
|
'pages/addresses/index',
|
||||||
'pages/address-edit/index',
|
'pages/address-edit/index',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default function ProductCarousel({
|
|||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
const prefix =
|
const prefix =
|
||||||
variant === 'store' ? 'store-detail-carousel' : variant === 'home' ? 'home-carousel' : 'detail-carousel';
|
variant === 'store' ? 'store-detail-carousel' : variant === 'home' ? 'home-carousel' : 'detail-carousel';
|
||||||
|
const imageMode = variant === 'store' ? 'aspectFit' : 'aspectFill';
|
||||||
|
|
||||||
function previewAt(index: number) {
|
function previewAt(index: number) {
|
||||||
const urls = slides.filter(Boolean);
|
const urls = slides.filter(Boolean);
|
||||||
@@ -41,7 +42,7 @@ export default function ProductCarousel({
|
|||||||
<Image
|
<Image
|
||||||
className={`${prefix}-image`}
|
className={`${prefix}-image`}
|
||||||
src={src}
|
src={src}
|
||||||
mode="aspectFill"
|
mode={imageMode}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
onClick={previewable ? () => previewAt(index) : undefined}
|
onClick={previewable ? () => previewAt(index) : undefined}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useRef } from 'react';
|
|||||||
import { finishLoginNavigate, forceReloadAfterAccountMerge, goLogin } from '../lib/auth-nav';
|
import { finishLoginNavigate, forceReloadAfterAccountMerge, goLogin } from '../lib/auth-nav';
|
||||||
import { toast } from '../lib/api';
|
import { toast } from '../lib/api';
|
||||||
import { capturePromoSceneAndTouchScan } from '../lib/promo';
|
import { capturePromoSceneAndTouchScan } from '../lib/promo';
|
||||||
|
import { initClientVersionChecks } from '../lib/client-version';
|
||||||
import { saveWechatLoginResult } from '../lib/pay-wechat';
|
import { saveWechatLoginResult } from '../lib/pay-wechat';
|
||||||
import { applyWechatShare } from '../lib/wechat-share';
|
import { applyWechatShare } from '../lib/wechat-share';
|
||||||
import { handleWechatAuthCallback } from '../lib/wechat-auth';
|
import { handleWechatAuthCallback } from '../lib/wechat-auth';
|
||||||
@@ -38,6 +39,7 @@ export default function WechatShareBootstrap() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void capturePromoSceneAndTouchScan();
|
void capturePromoSceneAndTouchScan();
|
||||||
|
initClientVersionChecks();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { fetchClientConfig } from './pay-wechat';
|
||||||
|
|
||||||
|
/** 与 package.json version 同步,供服务端 minClientVersion 比对 */
|
||||||
|
export const APP_VERSION = '3.4.13';
|
||||||
|
|
||||||
|
function parseSemver(v: string): number[] {
|
||||||
|
return v.split('.').map((n) => parseInt(n, 10) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareSemver(a: string, b: string): number {
|
||||||
|
const pa = parseSemver(a);
|
||||||
|
const pb = parseSemver(b);
|
||||||
|
const len = Math.max(pa.length, pb.length);
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
const da = pa[i] ?? 0;
|
||||||
|
const db = pb[i] ?? 0;
|
||||||
|
if (da !== db) return da - db;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkClientVersionGate() {
|
||||||
|
try {
|
||||||
|
const config = await fetchClientConfig();
|
||||||
|
const min = config.minClientVersion?.trim();
|
||||||
|
if (min && compareSemver(APP_VERSION, min) < 0) {
|
||||||
|
await Taro.showModal({
|
||||||
|
title: '版本过低',
|
||||||
|
content: '当前小程序版本过低,请更新至最新版本后继续使用。',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '我知道了',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 配置拉取失败不阻塞启动 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupWeappUpdateManager() {
|
||||||
|
if (process.env.TARO_ENV !== 'weapp') return;
|
||||||
|
if (typeof Taro.getUpdateManager !== 'function') return;
|
||||||
|
|
||||||
|
const manager = Taro.getUpdateManager();
|
||||||
|
manager.onCheckForUpdate((res) => {
|
||||||
|
if (!res.hasUpdate) return;
|
||||||
|
manager.onUpdateReady(() => {
|
||||||
|
void Taro.showModal({
|
||||||
|
title: '更新提示',
|
||||||
|
content: '新版本已准备好,是否重启应用?',
|
||||||
|
confirmText: '立即更新',
|
||||||
|
success: (r) => {
|
||||||
|
if (r.confirm) manager.applyUpdate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
manager.onUpdateFailed(() => {
|
||||||
|
void Taro.showModal({
|
||||||
|
title: '更新失败',
|
||||||
|
content: '新版本下载失败,请删除小程序后重新搜索打开。',
|
||||||
|
showCancel: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initClientVersionChecks() {
|
||||||
|
setupWeappUpdateManager();
|
||||||
|
void checkClientVersionGate();
|
||||||
|
}
|
||||||
@@ -141,6 +141,14 @@ export default function OrderConfirmPickupPage() {
|
|||||||
const ready = await ensurePayReady(returnPath);
|
const ready = await ensurePayReady(returnPath);
|
||||||
if (!ready) return;
|
if (!ready) return;
|
||||||
|
|
||||||
|
const { confirm } = await Taro.showModal({
|
||||||
|
title: '确认提交订单',
|
||||||
|
content: `确认提交现场提货订单?共 ${quantity} 瓶,应付 ¥${Number(preview?.payAmount ?? 0).toFixed(2)}。`,
|
||||||
|
confirmText: '确认提交',
|
||||||
|
cancelText: '再想想',
|
||||||
|
});
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMsg('');
|
setMsg('');
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -44,8 +44,15 @@ type OrderDetail = {
|
|||||||
orderType?: string;
|
orderType?: string;
|
||||||
isProxyOrder?: boolean;
|
isProxyOrder?: boolean;
|
||||||
proxyPartnerName?: string | null;
|
proxyPartnerName?: string | null;
|
||||||
|
deliveryType?: string;
|
||||||
items?: OrderItem[];
|
items?: OrderItem[];
|
||||||
wechatConfirm?: WechatConfirmPayload | null;
|
wechatConfirm?: WechatConfirmPayload | null;
|
||||||
|
delivery?: {
|
||||||
|
provider?: string;
|
||||||
|
trackingNo?: string;
|
||||||
|
logisticsCompany?: string;
|
||||||
|
manualQueryUrl?: string;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, string> = {
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
@@ -99,6 +106,13 @@ export default function OrderDetailPage() {
|
|||||||
const canPay = !!order && order.status === 'PENDING_PAY' && !isReship;
|
const canPay = !!order && order.status === 'PENDING_PAY' && !isReship;
|
||||||
const canConfirmReceive =
|
const canConfirmReceive =
|
||||||
!!order && !isReship && !isProxy && ['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '');
|
!!order && !isReship && !isProxy && ['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '');
|
||||||
|
const canViewLogistics =
|
||||||
|
!!order &&
|
||||||
|
order.deliveryType !== 'ON_SITE_PICKUP' &&
|
||||||
|
!isReship &&
|
||||||
|
['PENDING_SHIP', 'OUT_WAREHOUSE', 'SHIPPING', 'SHIPPED', 'PENDING_RECEIVE', 'DELIVERED', 'COMPLETED'].includes(
|
||||||
|
order.status || '',
|
||||||
|
);
|
||||||
|
|
||||||
const item = order?.items?.[0];
|
const item = order?.items?.[0];
|
||||||
const productName = item?.productName || order?.productName || '杜康商品';
|
const productName = item?.productName || order?.productName || '杜康商品';
|
||||||
@@ -134,6 +148,11 @@ export default function OrderDetailPage() {
|
|||||||
Taro.navigateTo({ url: '/pages/customer-service/index' });
|
Taro.navigateTo({ url: '/pages/customer-service/index' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goLogistics() {
|
||||||
|
if (!order) return;
|
||||||
|
Taro.navigateTo({ url: `/pages/order-logistics/index?id=${order.id}` });
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmReceive() {
|
async function confirmReceive() {
|
||||||
if (!order || !canConfirmReceive || confirming) return;
|
if (!order || !canConfirmReceive || confirming) return;
|
||||||
|
|
||||||
@@ -211,6 +230,11 @@ export default function OrderDetailPage() {
|
|||||||
{isProxy && order.proxyPartnerName ? (
|
{isProxy && order.proxyPartnerName ? (
|
||||||
<Text className="order-proxy-hint">由合伙人 {order.proxyPartnerName} 代下</Text>
|
<Text className="order-proxy-hint">由合伙人 {order.proxyPartnerName} 代下</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
{canViewLogistics ? (
|
||||||
|
<Text className="order-logistics-link" onClick={goLogistics}>
|
||||||
|
查看物流追踪
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<View className="order-card">
|
<View className="order-card">
|
||||||
<Text className="order-card-title">商品信息</Text>
|
<Text className="order-card-title">商品信息</Text>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '物流追踪',
|
||||||
|
});
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { View, Text } from '@tarojs/components';
|
||||||
|
import Taro, { useRouter } from '@tarojs/taro';
|
||||||
|
import PageShell from '../../components/PageShell';
|
||||||
|
import SubPageHeader from '../../components/SubPageHeader';
|
||||||
|
import { request, toast } from '../../lib/api';
|
||||||
|
import { usePageView } from '../../lib/usePageView';
|
||||||
|
|
||||||
|
type OrderDelivery = {
|
||||||
|
provider?: string;
|
||||||
|
trackingNo?: string;
|
||||||
|
logisticsCompany?: string;
|
||||||
|
manualQueryUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TrackNode = {
|
||||||
|
trackInfo?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OrderDetail = {
|
||||||
|
id: string;
|
||||||
|
orderNo?: string;
|
||||||
|
status?: string;
|
||||||
|
delivery?: OrderDelivery | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
|
PENDING_SHIP: '待发货',
|
||||||
|
OUT_WAREHOUSE: '出库中',
|
||||||
|
SHIPPING: '配送中',
|
||||||
|
SHIPPED: '配送中',
|
||||||
|
PENDING_RECEIVE: '待签收',
|
||||||
|
DELIVERED: '待签收',
|
||||||
|
COMPLETED: '已完成',
|
||||||
|
};
|
||||||
|
|
||||||
|
function deliveryProviderLabel(provider?: string, company?: string) {
|
||||||
|
if (company) return company;
|
||||||
|
if (!provider || provider === 'MOCK' || provider === 'XFX' || provider === 'XIAOFEIXIA') {
|
||||||
|
return '小飞侠配送';
|
||||||
|
}
|
||||||
|
if (provider === 'LOGISTICS') return '快递配送';
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value: string) {
|
||||||
|
return String(value).slice(0, 19).replace('T', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OrderLogisticsPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const orderId = router.params.id ?? '';
|
||||||
|
usePageView('order_logistics_view', orderId ? { orderId } : undefined);
|
||||||
|
const [order, setOrder] = useState<OrderDetail | null>(null);
|
||||||
|
const [trackNodes, setTrackNodes] = useState<TrackNode[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!orderId) return;
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const data = await request<OrderDetail>(`/trade/orders/${orderId}`);
|
||||||
|
if (cancelled) return;
|
||||||
|
setOrder(data);
|
||||||
|
if (data.delivery?.trackingNo || data.delivery?.provider === 'XFX') {
|
||||||
|
try {
|
||||||
|
const track = await request<{ nodes?: TrackNode[] }>(`/trade/orders/${orderId}/track`);
|
||||||
|
if (!cancelled) setTrackNodes(track.nodes ?? []);
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setTrackNodes([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!cancelled) toast(e instanceof Error ? e.message : '加载失败');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [orderId]);
|
||||||
|
|
||||||
|
function openManualQuery() {
|
||||||
|
const url = order?.delivery?.manualQueryUrl?.trim();
|
||||||
|
if (!url) {
|
||||||
|
toast('暂无物流查询链接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Taro.setClipboardData({ data: url })
|
||||||
|
.then(() => toast('查询链接已复制,请在浏览器中打开'))
|
||||||
|
.catch(() => toast('无法打开物流查询'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const delivery = order?.delivery;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageShell variant="sub" className="order-logistics-page">
|
||||||
|
<SubPageHeader title="物流追踪" />
|
||||||
|
<View className="sub-page-body">
|
||||||
|
{!order ? (
|
||||||
|
<View className="u-empty">加载中…</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View className="order-card">
|
||||||
|
<Text className="order-card-title">配送状态</Text>
|
||||||
|
<Text className="order-list-status">
|
||||||
|
{STATUS_LABELS[order.status || ''] || order.status || '处理中'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="order-card">
|
||||||
|
<Text className="order-card-title">物流信息</Text>
|
||||||
|
<View className="order-row">
|
||||||
|
<Text className="order-row-label">配送方式</Text>
|
||||||
|
<Text className="order-row-value">
|
||||||
|
{deliveryProviderLabel(delivery?.provider, delivery?.logisticsCompany)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{delivery?.trackingNo ? (
|
||||||
|
<View className="order-row">
|
||||||
|
<Text className="order-row-label">运单号</Text>
|
||||||
|
<Text className="order-row-value">{delivery.trackingNo}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
{delivery?.manualQueryUrl ? (
|
||||||
|
<View className="order-row">
|
||||||
|
<Text className="order-row-label">物流查询</Text>
|
||||||
|
<Text className="order-row-value order-row-value--link" onClick={openManualQuery}>
|
||||||
|
查看物流
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
{!delivery?.trackingNo && !delivery?.manualQueryUrl ? (
|
||||||
|
<Text className="u-muted">物流信息待更新,请稍后查看</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{trackNodes.length > 0 ? (
|
||||||
|
<View className="order-card">
|
||||||
|
<Text className="order-card-title">物流动态</Text>
|
||||||
|
{trackNodes.map((node, index) => (
|
||||||
|
<View key={`${node.trackInfo}-${index}`} className="order-logistics-node">
|
||||||
|
<Text className="order-logistics-node-time">
|
||||||
|
{node.createdAt ? formatDateTime(node.createdAt) : '—'}
|
||||||
|
</Text>
|
||||||
|
<Text className="order-logistics-node-info">{node.trackInfo || '—'}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</PageShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ 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 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';
|
||||||
@@ -167,12 +166,11 @@ export default function ProductDetailPage() {
|
|||||||
solid={headerSolid}
|
solid={headerSolid}
|
||||||
titleVisible={headerSolid}
|
titleVisible={headerSolid}
|
||||||
onBack={goBack}
|
onBack={goBack}
|
||||||
right={<ShareNavButton payload={sharePayload} />}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View className="product-detail-main">
|
<View className="product-detail-main">
|
||||||
<View className="product-detail-hero full-bleed">
|
<View className="product-detail-hero full-bleed">
|
||||||
<ProductCarousel images={carouselImages} alt={product.name} variant="detail" />
|
<ProductCarousel images={carouselImages} alt={product.name} variant="detail" previewable />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="product-detail-info">
|
<View className="product-detail-info">
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import ShareNavButton from '../../components/ShareNavButton';
|
|||||||
import StoreRedeemMarquee from '../../components/StoreRedeemMarquee';
|
import StoreRedeemMarquee from '../../components/StoreRedeemMarquee';
|
||||||
import WechatShareReady from '../../components/WechatShareReady';
|
import WechatShareReady from '../../components/WechatShareReady';
|
||||||
import { request, toast } from '../../lib/api';
|
import { request, toast } from '../../lib/api';
|
||||||
|
import { maskPhone } from '../../lib/phone';
|
||||||
|
import { track } from '../../lib/analytics';
|
||||||
import { formatShanghaiDateTime } from '../../lib/datetime';
|
import { formatShanghaiDateTime } from '../../lib/datetime';
|
||||||
import {
|
import {
|
||||||
DEFAULT_SHARE_DESC,
|
DEFAULT_SHARE_DESC,
|
||||||
@@ -154,6 +156,7 @@ export default function StoreDetailPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loadError, setLoadError] = useState('');
|
const [loadError, setLoadError] = useState('');
|
||||||
const [headerSolid, setHeaderSolid] = useState(false);
|
const [headerSolid, setHeaderSolid] = useState(false);
|
||||||
|
const [expandedPackages, setExpandedPackages] = useState<Record<number, boolean>>({});
|
||||||
const storeRef = useRef<Store | null>(null);
|
const storeRef = useRef<Store | null>(null);
|
||||||
storeRef.current = store;
|
storeRef.current = store;
|
||||||
|
|
||||||
@@ -271,6 +274,7 @@ export default function StoreDetailPage() {
|
|||||||
toast('暂无门店电话');
|
toast('暂无门店电话');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
track('store_phone_call', { storeId: store.id });
|
||||||
Taro.makePhoneCall({ phoneNumber: store.phone }).catch(() => toast('无法拨打电话'));
|
Taro.makePhoneCall({ phoneNumber: store.phone }).catch(() => toast('无法拨打电话'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +378,9 @@ export default function StoreDetailPage() {
|
|||||||
|
|
||||||
{store.phone ? (
|
{store.phone ? (
|
||||||
<View className="store-detail-row">
|
<View className="store-detail-row">
|
||||||
<Text className="store-detail-meta store-detail-meta--flex">电话: {store.phone}</Text>
|
<Text className="store-detail-meta store-detail-meta--flex">
|
||||||
|
电话: {maskPhone(store.phone)}
|
||||||
|
</Text>
|
||||||
<Text className="store-detail-action" onClick={callStore}>
|
<Text className="store-detail-action" onClick={callStore}>
|
||||||
拨打
|
拨打
|
||||||
</Text>
|
</Text>
|
||||||
@@ -399,15 +405,32 @@ export default function StoreDetailPage() {
|
|||||||
{store.packages && store.packages.length > 0 ? (
|
{store.packages && store.packages.length > 0 ? (
|
||||||
<View className="store-detail-section">
|
<View className="store-detail-section">
|
||||||
<Text className="store-detail-section-title">门店套餐</Text>
|
<Text className="store-detail-section-title">门店套餐</Text>
|
||||||
{store.packages.map((pkg, index) => (
|
{store.packages.map((pkg, index) => {
|
||||||
<View key={`${pkg.name}-${index}`} className="store-detail-package-card">
|
const expanded = !!expandedPackages[index];
|
||||||
|
const bodyText = `${formatRedeemAmountYuan(pkg.price)} 元 · ${pkg.dishes}`;
|
||||||
|
const longBody = bodyText.length > 48 || (pkg.otherNotes?.length ?? 0) > 40;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={`${pkg.name}-${index}`}
|
||||||
|
className={`store-detail-package-card${expanded || !longBody ? '' : ' store-detail-package-card--collapsed'}`}
|
||||||
|
>
|
||||||
{pkg.imageUrl ? (
|
{pkg.imageUrl ? (
|
||||||
<Image className="store-detail-package-img" src={pkg.imageUrl} mode="aspectFill" />
|
<Image className="store-detail-package-img" src={pkg.imageUrl} mode="aspectFill" />
|
||||||
) : null}
|
) : null}
|
||||||
<Text className="store-detail-package-name">{pkg.name}</Text>
|
<View className="store-detail-package-head">
|
||||||
<Text className="store-detail-package-body">
|
<Text className="store-detail-package-name">{pkg.name}</Text>
|
||||||
{formatRedeemAmountYuan(pkg.price)} 元 · {pkg.dishes}
|
{longBody ? (
|
||||||
</Text>
|
<Text
|
||||||
|
className="store-detail-package-toggle"
|
||||||
|
onClick={() =>
|
||||||
|
setExpandedPackages((prev) => ({ ...prev, [index]: !prev[index] }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{expanded ? '收起' : '展开'}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
<Text className="store-detail-package-body">{bodyText}</Text>
|
||||||
{pkg.usableTime ? (
|
{pkg.usableTime ? (
|
||||||
<Text className="store-detail-package-meta">使用时间:{pkg.usableTime}</Text>
|
<Text className="store-detail-package-meta">使用时间:{pkg.usableTime}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -415,7 +438,8 @@ export default function StoreDetailPage() {
|
|||||||
<Text className="store-detail-package-meta">说明:{pkg.otherNotes}</Text>
|
<Text className="store-detail-package-meta">说明:{pkg.otherNotes}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -488,3 +488,42 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-logistics-page {
|
||||||
|
background: var(--color-background);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-logistics-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-heritage-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-row-value--link {
|
||||||
|
color: var(--color-heritage-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-logistics-node {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-logistics-node:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-logistics-node-time {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-logistics-node-info {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,13 +10,15 @@
|
|||||||
.store-detail-carousel-wrap {
|
.store-detail-carousel-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 280px;
|
min-height: 200px;
|
||||||
|
max-height: 360px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: var(--color-surface-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-detail-carousel {
|
.store-detail-carousel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-detail-carousel-item,
|
.store-detail-carousel-item,
|
||||||
@@ -241,6 +243,27 @@
|
|||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-detail-package-card--collapsed .store-detail-package-body,
|
||||||
|
.store-detail-package-card--collapsed .store-detail-package-meta {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-detail-package-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-detail-package-toggle {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-heritage-red);
|
||||||
|
}
|
||||||
|
|
||||||
.store-detail-package-img {
|
.store-detail-package-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
|
|||||||
@@ -33,11 +33,24 @@ export const SUPPORT_TICKET_STATUS_LABELS: Record<SupportTicketStatusDto, string
|
|||||||
PASSED: '通过',
|
PASSED: '通过',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 技术支持工单优先级 */
|
||||||
|
export type SupportTicketPriorityDto = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';
|
||||||
|
|
||||||
|
export const SUPPORT_TICKET_PRIORITIES = ['LOW', 'NORMAL', 'HIGH', 'URGENT'] as const;
|
||||||
|
|
||||||
|
export const SUPPORT_TICKET_PRIORITY_LABELS: Record<SupportTicketPriorityDto, string> = {
|
||||||
|
LOW: '低',
|
||||||
|
NORMAL: '普通',
|
||||||
|
HIGH: '高',
|
||||||
|
URGENT: '紧急',
|
||||||
|
};
|
||||||
|
|
||||||
export interface SupportTicketDto {
|
export interface SupportTicketDto {
|
||||||
id: string;
|
id: string;
|
||||||
ticketNo: string;
|
ticketNo: string;
|
||||||
ticketType: SupportTicketTypeDto;
|
ticketType: SupportTicketTypeDto;
|
||||||
status: SupportTicketStatusDto;
|
status: SupportTicketStatusDto;
|
||||||
|
priority: SupportTicketPriorityDto;
|
||||||
title: string;
|
title: string;
|
||||||
content?: string | null;
|
content?: string | null;
|
||||||
rejectReason?: string | null;
|
rejectReason?: string | null;
|
||||||
@@ -59,6 +72,7 @@ export interface CreateSupportTicketRequest {
|
|||||||
content?: string;
|
content?: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
attachmentUrls?: string[];
|
attachmentUrls?: string[];
|
||||||
|
priority?: SupportTicketPriorityDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateSupportTicketRequest {
|
export interface UpdateSupportTicketRequest {
|
||||||
@@ -67,6 +81,7 @@ export interface UpdateSupportTicketRequest {
|
|||||||
content?: string;
|
content?: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
attachmentUrls?: string[];
|
attachmentUrls?: string[];
|
||||||
|
priority?: SupportTicketPriorityDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RejectSupportTicketRequest {
|
export interface RejectSupportTicketRequest {
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export type ClientRuntimeConfig = {
|
|||||||
banners: string[];
|
banners: string[];
|
||||||
footerUrl: string | null;
|
footerUrl: string | null;
|
||||||
};
|
};
|
||||||
|
/** 小程序最低兼容版本(semver,如 3.4.13);客户端低于此值时提示更新 */
|
||||||
|
minClientVersion?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 是否展示微信授权入口 */
|
/** 是否展示微信授权入口 */
|
||||||
|
|||||||
@@ -109,6 +109,13 @@ enum SupportTicketStatus {
|
|||||||
PASSED
|
PASSED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SupportTicketPriority {
|
||||||
|
LOW
|
||||||
|
NORMAL
|
||||||
|
HIGH
|
||||||
|
URGENT
|
||||||
|
}
|
||||||
|
|
||||||
enum DevPlanTaskType {
|
enum DevPlanTaskType {
|
||||||
BUG
|
BUG
|
||||||
REQUIREMENT
|
REQUIREMENT
|
||||||
@@ -646,6 +653,7 @@ model CommonSupportTicket {
|
|||||||
ticketNo String @unique @map("ticket_no") @db.VarChar(32)
|
ticketNo String @unique @map("ticket_no") @db.VarChar(32)
|
||||||
ticketType SupportTicketType @map("ticket_type")
|
ticketType SupportTicketType @map("ticket_type")
|
||||||
status SupportTicketStatus @default(PENDING_REVIEW)
|
status SupportTicketStatus @default(PENDING_REVIEW)
|
||||||
|
priority SupportTicketPriority @default(NORMAL) @map("priority")
|
||||||
title String @db.VarChar(128)
|
title String @db.VarChar(128)
|
||||||
content String? @db.Text
|
content String? @db.Text
|
||||||
rejectReason String? @map("reject_reason") @db.VarChar(512)
|
rejectReason String? @map("reject_reason") @db.VarChar(512)
|
||||||
|
|||||||
@@ -116,6 +116,15 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
|
|||||||
requiresRestart: false,
|
requiresRestart: false,
|
||||||
description: '小程序商品首页底部 footer,建议比例 15:4;上传后需点击右上角「保存」',
|
description: '小程序商品首页底部 footer,建议比例 15:4;上传后需点击右上角「保存」',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'MINI_USER_MIN_VERSION',
|
||||||
|
label: '小程序最低版本',
|
||||||
|
group: G.wechat_mini,
|
||||||
|
type: 'string',
|
||||||
|
requiresRestart: false,
|
||||||
|
placeholder: '3.4.13',
|
||||||
|
description: 'semver 格式;客户端低于此版本时提示更新',
|
||||||
|
},
|
||||||
|
|
||||||
{ key: 'OSS_ACCESS_KEY_ID', label: 'OSS AccessKey ID', group: G.oss, type: 'password', secret: true, requiresRestart: true },
|
{ key: 'OSS_ACCESS_KEY_ID', label: 'OSS AccessKey ID', group: G.oss, type: 'password', secret: true, requiresRestart: true },
|
||||||
{ key: 'OSS_ACCESS_KEY_SECRET', label: 'OSS AccessKey Secret', group: G.oss, type: 'password', secret: true, requiresRestart: true },
|
{ key: 'OSS_ACCESS_KEY_SECRET', label: 'OSS AccessKey Secret', group: G.oss, type: 'password', secret: true, requiresRestart: true },
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export class ClientConfigController {
|
|||||||
const cfg = this.systemConfig.getAppConfig();
|
const cfg = this.systemConfig.getAppConfig();
|
||||||
const env = this.systemConfig.getMergedEnv();
|
const env = this.systemConfig.getMergedEnv();
|
||||||
const footer = (env.MINI_HOME_FOOTER_URL ?? '').trim();
|
const footer = (env.MINI_HOME_FOOTER_URL ?? '').trim();
|
||||||
|
const minClientVersion = (env.MINI_USER_MIN_VERSION ?? '').trim() || null;
|
||||||
return {
|
return {
|
||||||
mockPay: cfg.mockPay,
|
mockPay: cfg.mockPay,
|
||||||
wechatPayEnabled: cfg.wechatPayEnabled,
|
wechatPayEnabled: cfg.wechatPayEnabled,
|
||||||
@@ -19,6 +20,7 @@ export class ClientConfigController {
|
|||||||
wxAuthorize: cfg.wxAuthorize,
|
wxAuthorize: cfg.wxAuthorize,
|
||||||
/** 可选暴露;选点已改为服务端 /common/lbs,前端可不依赖此字段 */
|
/** 可选暴露;选点已改为服务端 /common/lbs,前端可不依赖此字段 */
|
||||||
tencentLbsKey: cfg.tencentLbsKey || undefined,
|
tencentLbsKey: cfg.tencentLbsKey || undefined,
|
||||||
|
minClientVersion,
|
||||||
miniHome: {
|
miniHome: {
|
||||||
banners: parseMiniHomeBanners(env.MINI_HOME_BANNERS),
|
banners: parseMiniHomeBanners(env.MINI_HOME_BANNERS),
|
||||||
footerUrl: footer || null,
|
footerUrl: footer || null,
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ export class SupportTicketListQueryDto {
|
|||||||
@IsIn(['PENDING_REVIEW', 'REJECTED', 'DEVELOPING', 'TESTING', 'PASSED'])
|
@IsIn(['PENDING_REVIEW', 'REJECTED', 'DEVELOPING', 'TESTING', 'PASSED'])
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['LOW', 'NORMAL', 'HIGH', 'URGENT'])
|
||||||
|
priority?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@@ -46,6 +50,10 @@ export class CreateSupportTicketDto {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
attachmentUrls?: string[];
|
attachmentUrls?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['LOW', 'NORMAL', 'HIGH', 'URGENT'])
|
||||||
|
priority?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateSupportTicketDto {
|
export class UpdateSupportTicketDto {
|
||||||
@@ -72,6 +80,10 @@ export class UpdateSupportTicketDto {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
attachmentUrls?: string[];
|
attachmentUrls?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['LOW', 'NORMAL', 'HIGH', 'URGENT'])
|
||||||
|
priority?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BatchUpdateSupportTicketStatusDto {
|
export class BatchUpdateSupportTicketStatusDto {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import type { SupportTicketStatus, SupportTicketType } from '@prisma/client';
|
import type { SupportTicketStatus, SupportTicketType, SupportTicketPriority } from '@prisma/client';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import type { CreateDevPlanTaskFromTicketInput } from '@dukang/shared-types';
|
import type { CreateDevPlanTaskFromTicketInput } from '@dukang/shared-types';
|
||||||
import { mapSupportTicketTypeToDevPlanTask } from '@dukang/shared-types';
|
import { mapSupportTicketTypeToDevPlanTask } from '@dukang/shared-types';
|
||||||
@@ -68,6 +68,7 @@ export class SupportTicketService {
|
|||||||
title: dto.title.trim(),
|
title: dto.title.trim(),
|
||||||
content: dto.content?.trim() || null,
|
content: dto.content?.trim() || null,
|
||||||
remark: dto.remark?.trim() || null,
|
remark: dto.remark?.trim() || null,
|
||||||
|
priority: (dto.priority as SupportTicketPriority | undefined) ?? 'NORMAL',
|
||||||
attachmentUrls: dto.attachmentUrls?.length
|
attachmentUrls: dto.attachmentUrls?.length
|
||||||
? (dto.attachmentUrls.map((u) => u.trim()).filter(Boolean) as unknown as Prisma.InputJsonValue)
|
? (dto.attachmentUrls.map((u) => u.trim()).filter(Boolean) as unknown as Prisma.InputJsonValue)
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -178,6 +179,7 @@ export class SupportTicketService {
|
|||||||
const urls = dto.attachmentUrls.map((u) => u.trim()).filter(Boolean);
|
const urls = dto.attachmentUrls.map((u) => u.trim()).filter(Boolean);
|
||||||
data.attachmentUrls = urls.length ? (urls as unknown as Prisma.InputJsonValue) : Prisma.JsonNull;
|
data.attachmentUrls = urls.length ? (urls as unknown as Prisma.InputJsonValue) : Prisma.JsonNull;
|
||||||
}
|
}
|
||||||
|
if (dto.priority != null) data.priority = dto.priority as SupportTicketPriority;
|
||||||
const updated = await this.prisma.commonSupportTicket.update({ where: { id }, data });
|
const updated = await this.prisma.commonSupportTicket.update({ where: { id }, data });
|
||||||
return serializeBigInt(mapSupportTicketRow(updated));
|
return serializeBigInt(mapSupportTicketRow(updated));
|
||||||
}
|
}
|
||||||
@@ -247,6 +249,9 @@ export class SupportTicketService {
|
|||||||
if (query.status) {
|
if (query.status) {
|
||||||
where.status = query.status as SupportTicketStatus;
|
where.status = query.status as SupportTicketStatus;
|
||||||
}
|
}
|
||||||
|
if (query.priority) {
|
||||||
|
where.priority = query.priority as SupportTicketPriority;
|
||||||
|
}
|
||||||
|
|
||||||
const [items, total] = await Promise.all([
|
const [items, total] = await Promise.all([
|
||||||
this.prisma.commonSupportTicket.findMany({
|
this.prisma.commonSupportTicket.findMany({
|
||||||
|
|||||||
@@ -1692,6 +1692,10 @@ export class AuthService {
|
|||||||
throw new BadRequestException('首次登录请使用手机验证码,登录后将自动关联微信');
|
throw new BadRequestException('首次登录请使用手机验证码,登录后将自动关联微信');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.status !== 'ACTIVE') {
|
||||||
|
throw new BadRequestException('合伙人账号已停用');
|
||||||
|
}
|
||||||
|
|
||||||
account = await this.prisma.partnerAccount.update({
|
account = await this.prisma.partnerAccount.update({
|
||||||
where: { id: account.id },
|
where: { id: account.id },
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -44,6 +44,12 @@
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **3.4.12** | 2026-08-04 | 售后退款回滚、mini-user 门店详情、酒厂 T+3、门店多笔提现、开发计划批量编辑/审批企微派发、技术支持编辑/附件/批量改状态、套餐 imageUrl;开发设计见 [`杜康好客-v3.4.12-工单迭代开发文档.md`](./杜康好客-v3.4.12-工单迭代开发文档.md) |
|
| **3.4.12** | 2026-08-04 | 售后退款回滚、mini-user 门店详情、酒厂 T+3、门店多笔提现、开发计划批量编辑/审批企微派发、技术支持编辑/附件/批量改状态、套餐 imageUrl;开发设计见 [`杜康好客-v3.4.12-工单迭代开发文档.md`](./杜康好客-v3.4.12-工单迭代开发文档.md) |
|
||||||
|
|
||||||
|
### 0.5 变更:3.4.13 体验优化
|
||||||
|
|
||||||
|
| 版本 | 日期 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **3.4.13** | 2026-08-05 | 推广码归因统计、核销用户信息、技术支持工单优先级、mini-user 门店/商品/提货/版本/物流体验、H5 登录校验、合伙人微信暂停禁登;开发设计见 [`杜康好客-v3.4.13-体验优化开发文档.md`](./杜康好客-v3.4.13-体验优化开发文档.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 背景与目标
|
## 1. 背景与目标
|
||||||
|
|||||||
+18
-1
@@ -330,10 +330,27 @@ C2~C7、C14 见 §1.3。
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 变更记录
|
## 10. v3.4.13 体验优化(2026-08-05)
|
||||||
|
|
||||||
|
| 项 | 状态 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| 推广码 attributionCount | ✅ | HQ 详情统计卡 |
|
||||||
|
| 核销用户信息 | ✅ | RedeemRecordsPage 列表+详情 |
|
||||||
|
| 技术支持优先级 | ✅ | Prisma + shared-types + HQ UI |
|
||||||
|
| 合伙人微信暂停禁登 | ✅ | loginPartnerWechat |
|
||||||
|
| H5 登录前端校验 | ✅ | partner/shop LoginPage |
|
||||||
|
| mini-user 门店体验 | ✅ | 电话脱敏/埋点、门头 aspectFit、套餐折叠 |
|
||||||
|
| mini-user 商品/提货 | ✅ | 去分享、首图 preview、提货确认弹框 |
|
||||||
|
| mini-user 版本/物流 | ✅ | minClientVersion + UpdateManager;order-logistics |
|
||||||
|
| 文档 | ✅ | PRD §0.5 + v3.4.13 开发文档 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 变更记录
|
||||||
|
|
||||||
| 日期 | 说明 |
|
| 日期 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| 2026-08-05 | v3.4.13 体验优化(20 条 ST) |
|
||||||
| 2026-08-04 | v3.4.12 工单迭代(退款/财务/C端/开发计划/技术支持/套餐) |
|
| 2026-08-04 | v3.4.12 工单迭代(退款/财务/C端/开发计划/技术支持/套餐) |
|
||||||
| 2026-08-04 | v3.4.11 开发计划 + 企微智能机器人/消息推送 + 角色权限重构 |
|
| 2026-08-04 | v3.4.11 开发计划 + 企微智能机器人/消息推送 + 角色权限重构 |
|
||||||
| 2026-07-12 | **P0 已执行**:C2~C7、C14 代码与文档对齐;§1 改为计划+状态表 |
|
| 2026-07-12 | **P0 已执行**:C2~C7、C14 代码与文档对齐;§1 改为计划+状态表 |
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 杜康好客 · v3.4.13 体验优化开发文档
|
||||||
|
|
||||||
|
> 版本:**v3.4.13** · 日期:2026-08-05
|
||||||
|
> 需求源:20 条 ST 工单(体验优化 / Bug / 客户端验证)
|
||||||
|
|
||||||
|
## 1. 范围
|
||||||
|
|
||||||
|
| 模块 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| admin-web | 推广码 attributionCount;核销记录用户信息;技术支持工单优先级 |
|
||||||
|
| mini-user | 门店电话脱敏+拨打埋点;门头/套餐展示;商品去分享+首图 preview;提货确认弹框;版本更新提示;物流追踪页 |
|
||||||
|
| h5-partner / h5-shop | 登录 phone/code 前端校验 |
|
||||||
|
| 后端 | 工单 priority 字段;client-config minClientVersion;合伙人微信登录暂停拦截 |
|
||||||
|
|
||||||
|
## 2. ST 映射
|
||||||
|
|
||||||
|
| ST | 标题 | 状态 |
|
||||||
|
|----|------|------|
|
||||||
|
| ST1785925037781309 | 总部端-推广码数据跟踪优化 | ✅ attributionCount 统计卡 |
|
||||||
|
| ST1785924286682833 | 用户端-门店电话加密+拨打埋点 | ✅ maskPhone + store_phone_call |
|
||||||
|
| ST1785921693982470 | 技术支持-工单优先级 | ✅ priority 枚举 + HQ UI |
|
||||||
|
| ST1785921585900725 | 门店端扫一扫授权异常 | ✅ 已有(v3.4.12 前) |
|
||||||
|
| ST1785907536648201 | 版本不对提示更新 | ✅ UpdateManager + minClientVersion |
|
||||||
|
| ST1785906800359657 | 合伙人暂停后禁登 | ✅ loginPartnerWechat status |
|
||||||
|
| ST1785906592340255 | PARTNER_H5 login 校验 | ✅ 前端空字段拦截 |
|
||||||
|
| ST1785906390321653 | 现场提货提交确认弹框 | ✅ showModal |
|
||||||
|
| ST1785905773501871 | 核销记录用户信息 | ✅ 列表+详情 |
|
||||||
|
| ST1785904849234806 | 工单中心 | ✅ 已有 |
|
||||||
|
| ST1785904076632841 | 门店列表开城合伙人 | ✅ 已有 |
|
||||||
|
| ST1785902173093977 | 去掉商品详情分享按钮 | ✅ 移除 ShareNavButton |
|
||||||
|
| ST1785902141731113 | 商品首图大图 | ✅ previewable |
|
||||||
|
| ST1785901870913349 | 门店列表营业时间 | ✅ 已有 |
|
||||||
|
| ST1785901838948572 | SHOP 未绑定门店 | ✅ 已有 |
|
||||||
|
| ST1785901775231811 | 门店套餐遮挡 | ✅ 折叠/行数限制 |
|
||||||
|
| ST1785901711627824 | SHOP login 校验 | ✅ 前端空字段拦截 |
|
||||||
|
| ST1785901314893145 | 门头照裁剪 | ✅ aspectFit + preview |
|
||||||
|
| ST1785939375449985 | 订单物流追踪页 | ✅ order-logistics |
|
||||||
|
|
||||||
|
## 3. 后端 API / 配置
|
||||||
|
|
||||||
|
| 方法 | 路径 / 配置 | 说明 |
|
||||||
|
|------|-------------|------|
|
||||||
|
| GET | `/common/client-config` | 新增 `minClientVersion`(env `MINI_USER_MIN_VERSION`) |
|
||||||
|
| — | `CommonSupportTicket.priority` | `LOW \| NORMAL \| HIGH \| URGENT`,默认 NORMAL |
|
||||||
|
| — | `loginPartnerWechat` | 非 ACTIVE 账号抛出「合伙人账号已停用」 |
|
||||||
|
| GET | `/trade/orders/:id/track` | 物流追踪页复用 |
|
||||||
|
|
||||||
|
## 4. 数据表
|
||||||
|
|
||||||
|
- `common_support_ticket.priority` ENUM,默认 `NORMAL`
|
||||||
|
|
||||||
|
## 5. HQ 开发计划
|
||||||
|
|
||||||
|
在 admin-web **开发计划 → 版本列表** 创建 `v3.4.13`(状态 `IN_PROGRESS`),审批 ST 后关联 `dev_plan_task`。
|
||||||
|
|
||||||
|
## 6. 验收 ACC
|
||||||
|
|
||||||
|
- [ ] 推广码详情展示 attributionCount
|
||||||
|
- [ ] 核销记录含 userNo/nickname/phone
|
||||||
|
- [ ] 技术支持可创建/筛选/编辑优先级
|
||||||
|
- [ ] 合伙人/门店登录空字段前端提示;暂停合伙人微信登录被拒
|
||||||
|
- [ ] mini-user:电话脱敏、拨打埋点、提货确认、无分享按钮、首图 preview、门头/套餐正常
|
||||||
|
- [ ] mini-user:低版本弹窗、UpdateManager、物流追踪页
|
||||||
|
- [ ] `pnpm lint` 无新增错误
|
||||||
Reference in New Issue
Block a user