fix(admin,mini-user): store lat/lng edit with Baidu map, center pickup qty hint
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
HQ store detail can set coordinates and open Baidu Map search by name/address; admin update API persists lat/lng. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -44,6 +44,42 @@ const CREATE_STEPS = [
|
|||||||
{ title: '结算资质' },
|
{ title: '结算资质' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** 用店名 + 地址打开百度地图搜索,便于人工核对经纬度 */
|
||||||
|
function openBaiduMapSearch(parts: Array<string | null | undefined>) {
|
||||||
|
const query = parts.map((p) => String(p || '').trim()).filter(Boolean).join(' ');
|
||||||
|
if (!query) {
|
||||||
|
message.warning('请先填写门店名称和地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `https://map.baidu.com/search/${encodeURIComponent(query)}/@0,0,12z?querytype=s&da_src=shareurl&wd=${encodeURIComponent(query)}`;
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillGeolocation(
|
||||||
|
setCoords: (lat: number, lng: number) => void,
|
||||||
|
setLoading: (v: boolean) => void,
|
||||||
|
) {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
message.error('当前浏览器不支持定位');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos) => {
|
||||||
|
setCoords(pos.coords.latitude, pos.coords.longitude);
|
||||||
|
message.success(
|
||||||
|
`已获取坐标 ${pos.coords.latitude.toFixed(6)}, ${pos.coords.longitude.toFixed(6)}`,
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
message.error(err.message || '定位失败');
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
{ enableHighAccuracy: true, timeout: 10000 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type StoreMediaItem = {
|
type StoreMediaItem = {
|
||||||
id?: string;
|
id?: string;
|
||||||
bizType?: string;
|
bizType?: string;
|
||||||
@@ -560,6 +596,8 @@ export default function StoresPage() {
|
|||||||
coverUrl: d.coverUrl,
|
coverUrl: d.coverUrl,
|
||||||
address: d.address,
|
address: d.address,
|
||||||
district: d.district,
|
district: d.district,
|
||||||
|
latitude: d.latitude != null ? Number(d.latitude) : undefined,
|
||||||
|
longitude: d.longitude != null ? Number(d.longitude) : undefined,
|
||||||
settlementRate: d.settlementRate != null ? Number(d.settlementRate) * 100 : 60,
|
settlementRate: d.settlementRate != null ? Number(d.settlementRate) * 100 : 60,
|
||||||
openTime: d.openTime || '10:00',
|
openTime: d.openTime || '10:00',
|
||||||
closeTime: d.closeTime || '22:00',
|
closeTime: d.closeTime || '22:00',
|
||||||
@@ -648,13 +686,37 @@ export default function StoresPage() {
|
|||||||
}} />
|
}} />
|
||||||
<Button type="primary" onClick={async () => {
|
<Button type="primary" onClick={async () => {
|
||||||
const v = await editForm.validateFields();
|
const v = await editForm.validateFields();
|
||||||
|
const hasCoords =
|
||||||
|
v.latitude != null &&
|
||||||
|
v.longitude != null &&
|
||||||
|
Number.isFinite(Number(v.latitude)) &&
|
||||||
|
Number.isFinite(Number(v.longitude));
|
||||||
const payload = {
|
const payload = {
|
||||||
...v,
|
name: v.name,
|
||||||
|
phone: v.phone,
|
||||||
|
coverUrl: v.coverUrl,
|
||||||
|
intro: v.intro,
|
||||||
|
district: v.district,
|
||||||
|
address: v.address,
|
||||||
|
avgPrice: v.avgPrice,
|
||||||
|
openTime: v.openTime,
|
||||||
|
closeTime: v.closeTime,
|
||||||
|
openTime2: v.openTime2 || null,
|
||||||
|
closeTime2: v.closeTime2 || null,
|
||||||
settlementRate: v.settlementRate != null ? Number(v.settlementRate) / 100 : undefined,
|
settlementRate: v.settlementRate != null ? Number(v.settlementRate) / 100 : undefined,
|
||||||
|
...(hasCoords
|
||||||
|
? { latitude: Number(v.latitude), longitude: Number(v.longitude) }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
await request(`/admin/stores/${detail.id}`, { method: 'PUT', body: JSON.stringify(payload) });
|
await request(`/admin/stores/${detail.id}`, { method: 'PUT', body: JSON.stringify(payload) });
|
||||||
message.success('已保存');
|
message.success('已保存');
|
||||||
setDetail({ ...detail, ...v });
|
setDetail({
|
||||||
|
...detail,
|
||||||
|
...payload,
|
||||||
|
settlementRate: payload.settlementRate,
|
||||||
|
latitude: hasCoords ? Number(v.latitude) : detail.latitude,
|
||||||
|
longitude: hasCoords ? Number(v.longitude) : detail.longitude,
|
||||||
|
});
|
||||||
void reload();
|
void reload();
|
||||||
}}>保存</Button>
|
}}>保存</Button>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -681,6 +743,11 @@ export default function StoresPage() {
|
|||||||
<Descriptions.Item label="驳回原因">{String(detail.rejectReason || '—')}</Descriptions.Item>
|
<Descriptions.Item label="驳回原因">{String(detail.rejectReason || '—')}</Descriptions.Item>
|
||||||
) : null}
|
) : null}
|
||||||
<Descriptions.Item label="地址">{String(detail.province)}{String(detail.cityName)}{String(detail.district)}{String(detail.address)}</Descriptions.Item>
|
<Descriptions.Item label="地址">{String(detail.province)}{String(detail.cityName)}{String(detail.district)}{String(detail.address)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="经纬度">
|
||||||
|
{detail.latitude != null && detail.longitude != null
|
||||||
|
? `${Number(detail.latitude).toFixed(6)}, ${Number(detail.longitude).toFixed(6)}`
|
||||||
|
: '未设置'}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="营业时间">
|
<Descriptions.Item label="营业时间">
|
||||||
{[
|
{[
|
||||||
detail.openTime && detail.closeTime
|
detail.openTime && detail.closeTime
|
||||||
@@ -741,6 +808,57 @@ export default function StoresPage() {
|
|||||||
<Form.Item name="intro" label="介绍"><Input.TextArea rows={4} /></Form.Item>
|
<Form.Item name="intro" label="介绍"><Input.TextArea rows={4} /></Form.Item>
|
||||||
<Form.Item name="district" label="区县"><Input /></Form.Item>
|
<Form.Item name="district" label="区县"><Input /></Form.Item>
|
||||||
<Form.Item name="address" label="详细地址"><Input /></Form.Item>
|
<Form.Item name="address" label="详细地址"><Input /></Form.Item>
|
||||||
|
<Space wrap style={{ width: '100%' }}>
|
||||||
|
<Form.Item name="latitude" label="纬度" style={{ marginBottom: 8 }}>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: 180 }}
|
||||||
|
precision={7}
|
||||||
|
step={0.000001}
|
||||||
|
placeholder="如 34.7466000"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="longitude" label="经度" style={{ marginBottom: 8 }}>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: 180 }}
|
||||||
|
precision={7}
|
||||||
|
step={0.000001}
|
||||||
|
placeholder="如 113.6253000"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
<Space wrap style={{ marginBottom: 16 }}>
|
||||||
|
<Button
|
||||||
|
loading={locating}
|
||||||
|
onClick={() => {
|
||||||
|
fillGeolocation(
|
||||||
|
(lat, lng) => editForm.setFieldsValue({ latitude: lat, longitude: lng }),
|
||||||
|
setLocating,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
获取当前位置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
const name = String(editForm.getFieldValue('name') || detail.name || '');
|
||||||
|
const district = String(editForm.getFieldValue('district') || detail.district || '');
|
||||||
|
const address = String(editForm.getFieldValue('address') || detail.address || '');
|
||||||
|
openBaiduMapSearch([
|
||||||
|
String(detail.province || ''),
|
||||||
|
String(detail.cityName || ''),
|
||||||
|
district,
|
||||||
|
address,
|
||||||
|
name,
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
百度地图查询
|
||||||
|
</Button>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
打开百度地图核对位置后,将坐标填回上方经纬度
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
<Form.Item name="avgPrice" label="人均费用(选填)">
|
<Form.Item name="avgPrice" label="人均费用(选填)">
|
||||||
<InputNumber min={0} precision={0} style={{ width: '100%' }} addonAfter="元" placeholder="用户端展示" />
|
<InputNumber min={0} precision={0} style={{ width: '100%' }} addonAfter="元" placeholder="用户端展示" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -886,34 +1004,31 @@ export default function StoresPage() {
|
|||||||
<Button
|
<Button
|
||||||
loading={locating}
|
loading={locating}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!navigator.geolocation) {
|
fillGeolocation(
|
||||||
message.error('当前浏览器不支持定位');
|
(lat, lng) => createForm.setFieldsValue({ latitude: lat, longitude: lng }),
|
||||||
return;
|
setLocating,
|
||||||
}
|
|
||||||
setLocating(true);
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
(pos) => {
|
|
||||||
createForm.setFieldsValue({
|
|
||||||
latitude: pos.coords.latitude,
|
|
||||||
longitude: pos.coords.longitude,
|
|
||||||
});
|
|
||||||
message.success(
|
|
||||||
`已获取坐标 ${pos.coords.latitude.toFixed(6)}, ${pos.coords.longitude.toFixed(6)}`,
|
|
||||||
);
|
|
||||||
setLocating(false);
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
message.error(err.message || '定位失败');
|
|
||||||
setLocating(false);
|
|
||||||
},
|
|
||||||
{ enableHighAccuracy: true, timeout: 10000 },
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
获取当前位置
|
获取当前位置
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
const v = createForm.getFieldsValue();
|
||||||
|
openBaiduMapSearch([
|
||||||
|
v.province,
|
||||||
|
v.city,
|
||||||
|
v.district,
|
||||||
|
v.address,
|
||||||
|
v.name,
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
百度地图查询
|
||||||
|
</Button>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
可手动填写,或点击定位填入;便于用户端导航与距离
|
可手动填写 / 定位 / 百度地图核对后填入坐标
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
<Space wrap style={{ width: '100%' }}>
|
<Space wrap style={{ width: '100%' }}>
|
||||||
|
|||||||
@@ -196,6 +196,11 @@ export default function OrderConfirmPickupPage() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
{!quantityOk ? (
|
||||||
|
<Text className="order-qty-hint">
|
||||||
|
{`现场提货至少购买 ${minQty} 瓶,请调整数量`}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="order-card">
|
<View className="order-card">
|
||||||
@@ -219,7 +224,7 @@ export default function OrderConfirmPickupPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{msg ? (
|
{msg ? (
|
||||||
<Text className="u-muted" style={{ display: 'block', marginTop: 8 }}>
|
<Text className="u-muted" style={{ display: 'block', marginTop: 8, textAlign: 'center' }}>
|
||||||
{msg}
|
{msg}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -194,6 +194,20 @@ export class AdminStoresService {
|
|||||||
if (!hoursCheck.ok) throw new BadRequestException(hoursCheck.message);
|
if (!hoursCheck.ok) throw new BadRequestException(hoursCheck.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const latitude = dto.latitude !== undefined ? (dto.latitude == null ? null : Number(dto.latitude)) : undefined;
|
||||||
|
const longitude = dto.longitude !== undefined ? (dto.longitude == null ? null : Number(dto.longitude)) : undefined;
|
||||||
|
if (latitude !== undefined || longitude !== undefined) {
|
||||||
|
if (latitude == null || longitude == null) {
|
||||||
|
throw new BadRequestException('经纬度须同时提供');
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
|
||||||
|
throw new BadRequestException('纬度无效');
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
|
||||||
|
throw new BadRequestException('经度无效');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const store = await this.prisma.store.update({
|
const store = await this.prisma.store.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
@@ -208,6 +222,7 @@ export class AdminStoresService {
|
|||||||
...(dto.openTime2 !== undefined ? { openTime2: dto.openTime2 || null } : {}),
|
...(dto.openTime2 !== undefined ? { openTime2: dto.openTime2 || null } : {}),
|
||||||
...(dto.closeTime2 !== undefined ? { closeTime2: dto.closeTime2 || null } : {}),
|
...(dto.closeTime2 !== undefined ? { closeTime2: dto.closeTime2 || null } : {}),
|
||||||
...(dto.avgPrice !== undefined ? { avgPrice: dto.avgPrice } : {}),
|
...(dto.avgPrice !== undefined ? { avgPrice: dto.avgPrice } : {}),
|
||||||
|
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,14 @@ export class UpdateStoreDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
address?: string;
|
address?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
latitude?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
longitude?: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
district?: string;
|
district?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user