- 店铺字典(迁移 0057):大按钮点选 + 「新店」自定义自动登记;重命名可级联、 停用/备注;列表行内改、详情、筛选条共用同一个 picker - 新建强校验:订单编号/店铺/CK/礼包名/主页截图必填;贴 CK 即探针预览, 读不到绑定角色禁止提交;礼包用工作台配置的活动 SID 现拉(浏览器缓存 + 强制刷新) - 图片:绑定图停用、「绑定斗鱼号」下线(绑定关系改由探针读 bind_game_*), 主页截图改为新建必填(弹窗内先传 OSS 再随表单落库) - 同买家多单软识别:按「绑定账号 + 绑定角色」整批一次查询,列表「同买家」列 + 详情分组,点单号直接切单 - uid 放开唯一性(迁移 0058):同一份 CK 可服务多单,原 409 冲突改为提示 + 审计 - 列表体验:序号列、搜索扩到 11 个字段、CK 状态列截断、新建弹窗放大并去滚动条 - 验证:pytest 932 passed / 6 skipped;接口冒烟 56 项全绿;tsc+eslint+build 通过
277 lines
8.4 KiB
TypeScript
277 lines
8.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import {
|
|
Button, Checkbox, Empty, Input, InputNumber, Modal, Popconfirm, Space, Switch,
|
|
Table, Tag, Tooltip, Typography,
|
|
} from 'antd';
|
|
import type { TableProps } from 'antd';
|
|
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
import { huyaApi, type HuyaCollabShop } from '../api/modules';
|
|
import { message } from '../utils/antdMessage';
|
|
import { getErrorMessage } from '../utils/error';
|
|
|
|
const { Text } = Typography;
|
|
|
|
/**
|
|
* 店铺管理:维护「新建协作单」下拉里的店铺。
|
|
*
|
|
* - 加 / 排序 / 备注 / 停用(停用=不再出现在新建下拉,历史订单照旧);
|
|
* - **重命名**会级联更新已有订单里的店铺名(弹窗里可取消勾选),
|
|
* 否则筛选里会同时出现新旧两个名字;
|
|
* - 删除只删字典行、不动订单(需要 `huya:collab:manage`)。
|
|
*/
|
|
export default function HuyaCollabShopModal({
|
|
open,
|
|
shops,
|
|
canEdit,
|
|
canManage,
|
|
onClose,
|
|
onChanged,
|
|
}: {
|
|
open: boolean;
|
|
shops: HuyaCollabShop[];
|
|
canEdit: boolean;
|
|
canManage: boolean;
|
|
onClose: () => void;
|
|
onChanged: () => void;
|
|
}) {
|
|
const [newName, setNewName] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
const [renameTarget, setRenameTarget] = useState<HuyaCollabShop | null>(null);
|
|
const [renameValue, setRenameValue] = useState('');
|
|
const [renameOrders, setRenameOrders] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setNewName('');
|
|
setRenameTarget(null);
|
|
}
|
|
}, [open]);
|
|
|
|
const create = async () => {
|
|
const name = newName.trim();
|
|
if (!name) {
|
|
message.warning('请先输入店铺名');
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
try {
|
|
await huyaApi.createCollabShop({ name });
|
|
message.success(`已添加店铺「${name}」`);
|
|
setNewName('');
|
|
onChanged();
|
|
} catch (e: unknown) {
|
|
message.error(getErrorMessage(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const patch = async (shop: HuyaCollabShop, payload: { sort_order?: number; is_active?: boolean; remark?: string }) => {
|
|
setBusy(true);
|
|
try {
|
|
await huyaApi.updateCollabShop(shop.id, payload);
|
|
onChanged();
|
|
} catch (e: unknown) {
|
|
message.error(getErrorMessage(e));
|
|
onChanged();
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const submitRename = async () => {
|
|
if (!renameTarget) return;
|
|
const name = renameValue.trim();
|
|
if (!name || name === renameTarget.name) {
|
|
setRenameTarget(null);
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
try {
|
|
await huyaApi.updateCollabShop(renameTarget.id, { name, rename_orders: renameOrders });
|
|
message.success(renameOrders ? `已重命名为「${name}」,已有订单里的旧名字也同步了` : `已重命名为「${name}」`);
|
|
setRenameTarget(null);
|
|
onChanged();
|
|
} catch (e: unknown) {
|
|
message.error(getErrorMessage(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const remove = async (shop: HuyaCollabShop) => {
|
|
setBusy(true);
|
|
try {
|
|
await huyaApi.deleteCollabShop(shop.id);
|
|
message.success(`已从字典删除「${shop.name}」`);
|
|
onChanged();
|
|
} catch (e: unknown) {
|
|
message.error(getErrorMessage(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const columns: TableProps<HuyaCollabShop>['columns'] = [
|
|
{
|
|
title: '店铺名', dataIndex: 'name', width: 180,
|
|
render: (value: string, row) => (
|
|
<Space size={4}>
|
|
<Text strong>{value}</Text>
|
|
{!row.is_active && <Tag>已停用</Tag>}
|
|
{canEdit && (
|
|
<Tooltip title="重命名">
|
|
<Button
|
|
size="small"
|
|
type="text"
|
|
icon={<EditOutlined />}
|
|
disabled={busy}
|
|
onClick={() => {
|
|
setRenameTarget(row);
|
|
setRenameValue(row.name);
|
|
setRenameOrders(true);
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: '排序', dataIndex: 'sort_order', width: 100,
|
|
render: (value: number, row) => (
|
|
<InputNumber
|
|
size="small"
|
|
value={value}
|
|
disabled={!canEdit || busy}
|
|
onChange={(next) => {
|
|
if (next === null || next === value) return;
|
|
void patch(row, { sort_order: Number(next) });
|
|
}}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '新建时可选', dataIndex: 'is_active', width: 110, align: 'center',
|
|
render: (value: boolean, row) => (
|
|
<Switch
|
|
size="small"
|
|
checked={value}
|
|
disabled={!canEdit || busy}
|
|
checkedChildren="启用"
|
|
unCheckedChildren="停用"
|
|
onChange={(checked) => void patch(row, { is_active: checked })}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '备注', dataIndex: 'remark', width: 200,
|
|
render: (value: string, row) => (
|
|
<Input
|
|
size="small"
|
|
defaultValue={value}
|
|
disabled={!canEdit || busy}
|
|
placeholder="备注(失焦保存)"
|
|
onBlur={(event) => {
|
|
const next = event.target.value.trim();
|
|
if (next !== value) void patch(row, { remark: next });
|
|
}}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '最后改动', width: 150,
|
|
render: (_, row) => (
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
{row.updated_by_name || '-'}
|
|
{row.updated_at ? ` · ${new Date(row.updated_at).toLocaleString()}` : ''}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
title: '操作', width: 90, align: 'center',
|
|
render: (_, row) =>
|
|
canManage ? (
|
|
<Popconfirm
|
|
title={`从字典删除「${row.name}」?`}
|
|
description="只删字典行,已有订单里的店铺名不变"
|
|
onConfirm={() => void remove(row)}
|
|
disabled={busy}
|
|
>
|
|
<Button size="small" danger type="text" icon={<DeleteOutlined />} disabled={busy} />
|
|
</Popconfirm>
|
|
) : (
|
|
<Text type="secondary">-</Text>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<Modal
|
|
title="店铺管理"
|
|
open={open}
|
|
onCancel={onClose}
|
|
footer={<Button onClick={onClose}>关闭</Button>}
|
|
width={860}
|
|
destroyOnHidden
|
|
>
|
|
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
|
<Text type="secondary">
|
|
这里的店铺就是新建协作单时下拉里能点选的那些。停用只是不再出现在新建下拉里,
|
|
历史订单不受影响;重命名可以选择是否同步已有订单。
|
|
</Text>
|
|
{canEdit && (
|
|
<Space.Compact style={{ width: '100%' }}>
|
|
<Input
|
|
placeholder="输入店铺名,回车添加"
|
|
value={newName}
|
|
maxLength={64}
|
|
onChange={(event) => setNewName(event.target.value)}
|
|
onPressEnter={() => void create()}
|
|
/>
|
|
<Button type="primary" icon={<PlusOutlined />} loading={busy} onClick={() => void create()}>
|
|
添加
|
|
</Button>
|
|
</Space.Compact>
|
|
)}
|
|
<Table<HuyaCollabShop>
|
|
rowKey="id"
|
|
size="small"
|
|
columns={columns}
|
|
dataSource={shops}
|
|
pagination={false}
|
|
scroll={{ y: 320 }}
|
|
locale={{
|
|
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="还没有店铺,先加一个" />,
|
|
}}
|
|
/>
|
|
</Space>
|
|
</Modal>
|
|
|
|
<Modal
|
|
title={renameTarget ? `重命名「${renameTarget.name}」` : '重命名店铺'}
|
|
open={!!renameTarget}
|
|
onCancel={() => setRenameTarget(null)}
|
|
onOk={() => void submitRename()}
|
|
confirmLoading={busy}
|
|
okText="保存"
|
|
destroyOnHidden
|
|
>
|
|
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
|
<Input
|
|
value={renameValue}
|
|
maxLength={64}
|
|
autoFocus
|
|
onChange={(event) => setRenameValue(event.target.value)}
|
|
onPressEnter={() => void submitRename()}
|
|
/>
|
|
<Checkbox checked={renameOrders} onChange={(event) => setRenameOrders(event.target.checked)}>
|
|
同时更新已有订单里的店铺名(不勾选的话,筛选里会同时出现新旧两个名字)
|
|
</Checkbox>
|
|
</Space>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|