fix(admin): save packages when clicking store 保存修改
HQ users were uploading package images then clicking drawer 保存修改, which only persisted store media and never PUT packages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { Alert, Button, Form, Input, InputNumber, Modal, Space, Typography, message } from 'antd';
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import type { StorePackageItemDto } from '@dukang/shared-types';
|
||||
import { STORE_PACKAGE_IMAGE_MAX_COUNT, STORE_PACKAGE_MAX_COUNT, normalizeStorePackageImageUrls } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import PackageImagesUpload from './PackageImagesUpload';
|
||||
|
||||
type PackageRow = StorePackageItemDto;
|
||||
|
||||
export type AdminStorePackagesHandle = {
|
||||
/** 套餐已加载时写入;加载中则跳过,避免空数据覆盖线上套餐 */
|
||||
saveIfLoaded: (opts?: { quiet?: boolean }) => Promise<{ skipped: boolean }>;
|
||||
};
|
||||
|
||||
function emptyRow(index = 0): PackageRow {
|
||||
return {
|
||||
name: '',
|
||||
@@ -20,11 +26,22 @@ function emptyRow(index = 0): PackageRow {
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminStorePackagesSection({ storeId }: { storeId: string }) {
|
||||
const AdminStorePackagesSection = forwardRef<AdminStorePackagesHandle, { storeId: string }>(
|
||||
function AdminStorePackagesSection({ storeId }, ref) {
|
||||
const [items, setItems] = useState<PackageRow[]>([emptyRow()]);
|
||||
const [collapsed, setCollapsed] = useState<Record<number, boolean>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const itemsRef = useRef(items);
|
||||
const loadingRef = useRef(loading);
|
||||
|
||||
useEffect(() => {
|
||||
itemsRef.current = items;
|
||||
}, [items]);
|
||||
|
||||
useEffect(() => {
|
||||
loadingRef.current = loading;
|
||||
}, [loading]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
@@ -86,12 +103,14 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
}
|
||||
run();
|
||||
}
|
||||
|
||||
function toggleCollapse(index: number) {
|
||||
setCollapsed((prev) => ({ ...prev, [index]: !prev[index] }));
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const filled = items
|
||||
async function save(opts?: { quiet?: boolean }) {
|
||||
const currentItems = itemsRef.current;
|
||||
const filled = currentItems
|
||||
.map((item, index) => {
|
||||
const imageUrls = normalizeStorePackageImageUrls(item);
|
||||
return {
|
||||
@@ -111,20 +130,20 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
const item = filled[i];
|
||||
if (!item.name) {
|
||||
message.warning(`第 ${i + 1} 条套餐名称不能为空`);
|
||||
return;
|
||||
throw new Error('套餐校验失败');
|
||||
}
|
||||
if (!item.dishes) {
|
||||
message.warning(`第 ${i + 1} 条套餐菜品不能为空`);
|
||||
return;
|
||||
throw new Error('套餐校验失败');
|
||||
}
|
||||
const price = Number(item.price);
|
||||
if (!Number.isFinite(price) || price < 0) {
|
||||
message.warning(`第 ${i + 1} 条套餐价格须为非负数字`);
|
||||
return;
|
||||
throw new Error('套餐校验失败');
|
||||
}
|
||||
if ((item.imageUrls?.length ?? 0) > STORE_PACKAGE_IMAGE_MAX_COUNT) {
|
||||
message.warning(`第 ${i + 1} 条套餐图片最多 ${STORE_PACKAGE_IMAGE_MAX_COUNT} 张`);
|
||||
return;
|
||||
throw new Error('套餐校验失败');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +155,7 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
packages: filled.map((p) => ({ ...p, price: Number(p.price).toFixed(2) })),
|
||||
}),
|
||||
});
|
||||
message.success('套餐已保存并生效');
|
||||
if (!opts?.quiet) message.success('套餐已保存并生效');
|
||||
setItems(
|
||||
data.live?.length
|
||||
? data.live.map((p, i) => {
|
||||
@@ -152,12 +171,21 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
: [emptyRow()],
|
||||
);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败');
|
||||
if (!opts?.quiet) message.error(e instanceof Error ? e.message : '保存失败');
|
||||
throw e;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
saveIfLoaded: async (opts) => {
|
||||
if (loadingRef.current) return { skipped: true };
|
||||
await save(opts);
|
||||
return { skipped: false };
|
||||
},
|
||||
}));
|
||||
|
||||
if (loading) {
|
||||
return <Typography.Text type="secondary">加载套餐中…</Typography.Text>;
|
||||
}
|
||||
@@ -169,14 +197,15 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
<Button onClick={addRow} style={{ marginBottom: 16 }}>
|
||||
添加套餐
|
||||
</Button>
|
||||
<Button type="primary" loading={saving} onClick={() => void save()}>
|
||||
<Button type="primary" loading={saving} onClick={() => void save().catch(() => undefined)}>
|
||||
保存套餐
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
return ( <Form layout="vertical" requiredMark={false}>
|
||||
return (
|
||||
<Form layout="vertical" requiredMark={false}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
@@ -213,7 +242,8 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
<Button type="link" danger onClick={() => removeAt(index)}>
|
||||
删除
|
||||
</Button>
|
||||
) : null} </Space>
|
||||
) : null}
|
||||
</Space>
|
||||
|
||||
{!isCollapsed ? (
|
||||
<>
|
||||
@@ -266,7 +296,8 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="其他说明" style={{ marginBottom: 0 }}> <Input
|
||||
<Form.Item label="其他说明" style={{ marginBottom: 0 }}>
|
||||
<Input
|
||||
placeholder="不可叠加"
|
||||
value={item.otherNotes || ''}
|
||||
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
||||
@@ -285,12 +316,15 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
) : null}
|
||||
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||
上传图片后须点击下方「保存套餐」才会写入数据库;仅上传未保存,刷新后会丢失。
|
||||
上传套餐图后请点右上角「保存修改」(会连同套餐一起保存),或点下方「保存套餐」。仅上传不保存,刷新会丢失。
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Button type="primary" loading={saving} onClick={() => void save()}>
|
||||
<Button type="primary" loading={saving} onClick={() => void save().catch(() => undefined)}>
|
||||
保存套餐
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default AdminStorePackagesSection;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
@@ -42,7 +42,9 @@ import ChinaRegionCascader from '../components/ChinaRegionCascader';
|
||||
import OssUpload from '../components/OssUpload';
|
||||
import MultiImageUpload from '../components/MultiImageUpload';
|
||||
import TencentLocPickerModal from '../components/TencentLocPickerModal';
|
||||
import AdminStorePackagesSection from '../components/AdminStorePackagesSection';
|
||||
import AdminStorePackagesSection, {
|
||||
type AdminStorePackagesHandle,
|
||||
} from '../components/AdminStorePackagesSection';
|
||||
|
||||
const CREATE_STEPS = [
|
||||
{ title: '基本信息' },
|
||||
@@ -114,7 +116,7 @@ function StoreAuditMediaEditor() {
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="可替换或删除门头照 / 环境照 / 签约合同,点击右上角「保存修改」后生效。环境照最多 20 张。"
|
||||
message="可替换或删除门头照 / 环境照 / 签约合同,点击右上角「保存修改」后生效。环境照最多 20 张。套餐请在「套餐」页签编辑,同样由「保存修改」一并提交。"
|
||||
/>
|
||||
<Form.Item name="coverUrl" label="门头照">
|
||||
<OssUpload bizType="STORE_TITLE" mediaType="IMAGE" />
|
||||
@@ -251,6 +253,7 @@ export default function StoresPage() {
|
||||
const [filterPartners, setFilterPartners] = useState<PartnerOption[]>([]);
|
||||
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const packagesRef = useRef<AdminStorePackagesHandle>(null);
|
||||
const [rejectOpen, setRejectOpen] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [auditing, setAuditing] = useState(false);
|
||||
@@ -465,7 +468,10 @@ export default function StoresPage() {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
message.success('门店信息已保存');
|
||||
const packagesResult = await packagesRef.current?.saveIfLoaded({ quiet: true });
|
||||
message.success(
|
||||
packagesResult?.skipped === false ? '门店信息与套餐已保存' : '门店信息已保存',
|
||||
);
|
||||
setDetail(updated);
|
||||
setPhoneMismatch(null);
|
||||
void reload();
|
||||
@@ -1066,7 +1072,8 @@ export default function StoresPage() {
|
||||
{
|
||||
key: 'packages',
|
||||
label: '套餐',
|
||||
children: <AdminStorePackagesSection storeId={String(detail.id)} />,
|
||||
forceRender: true,
|
||||
children: <AdminStorePackagesSection ref={packagesRef} storeId={String(detail.id)} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -39,9 +39,19 @@ export default function MultiOssUploadField({
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const urls = normalizeUrls(value);
|
||||
const urlsRef = useRef(urls);
|
||||
const onChangeRef = useRef(onChange);
|
||||
const remaining = Math.max(0, maxCount - urls.length);
|
||||
const inWechat = isWechatEnv();
|
||||
|
||||
useEffect(() => {
|
||||
urlsRef.current = urls;
|
||||
}, [urls]);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeRef.current = onChange;
|
||||
}, [onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inWechat) return;
|
||||
void weixinSdk.init().catch(() => {});
|
||||
@@ -53,7 +63,7 @@ export default function MultiOssUploadField({
|
||||
}
|
||||
|
||||
async function uploadFiles(files: File[]) {
|
||||
const current = normalizeUrls(value);
|
||||
const current = urlsRef.current;
|
||||
const room = Math.max(0, maxCount - current.length);
|
||||
const picked = files.slice(0, room);
|
||||
if (!picked.length) {
|
||||
@@ -70,7 +80,9 @@ export default function MultiOssUploadField({
|
||||
appended.push(result.url);
|
||||
}
|
||||
if (appended.length) {
|
||||
onChange?.([...current, ...appended]);
|
||||
const next = [...urlsRef.current, ...appended];
|
||||
urlsRef.current = next;
|
||||
onChangeRef.current?.(next);
|
||||
toastSuccess(`已上传 ${appended.length} 张`);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user