feat: v3.4.12 ticket iteration
Refund rollback, winery T+3, multi withdraw, mini-user store detail, dev plan batch edit and WeCom dispatch, support ticket edit/attachments/batch status, package imageUrl. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,8 +2,10 @@ import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Drawer,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
Modal,
|
||||
Radio,
|
||||
@@ -40,6 +42,10 @@ import {
|
||||
import { request, type HqProfile } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import OssUpload from '../components/OssUpload';
|
||||
|
||||
const DISPATCH_WECOM_STORAGE_KEY = 'support_ticket_dispatch_wecom';
|
||||
const DEFAULT_DISPATCH_SUPPLEMENT = '请按以下任务进入开发流程';
|
||||
|
||||
const STATUS_COLOR: Record<SupportTicketStatusDto, string> = {
|
||||
PENDING_REVIEW: 'orange',
|
||||
@@ -88,12 +94,26 @@ export default function SupportTicketsPage() {
|
||||
const [batchPreviewOpen, setBatchPreviewOpen] = useState(false);
|
||||
const [batchPreview, setBatchPreview] = useState<BatchReviewPreviewItem[]>([]);
|
||||
const [batchConfirming, setBatchConfirming] = useState(false);
|
||||
const [batchStatusOpen, setBatchStatusOpen] = useState(false);
|
||||
const [batchStatusSaving, setBatchStatusSaving] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [editSaving, setEditSaving] = useState(false);
|
||||
const [reviewDecision, setReviewDecision] = useState<'APPROVE' | 'REJECT'>('APPROVE');
|
||||
|
||||
const [createForm] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
const [reviewForm] = Form.useForm<{ decision: 'APPROVE' | 'REJECT'; rejectReason?: string; note?: string }>();
|
||||
const [tasksForm] = Form.useForm<{ tasks: Array<{ content: string; type: DevPlanTaskTypeDto }> }>();
|
||||
const [tasksForm] = Form.useForm<{
|
||||
tasks: Array<{ content: string; type: DevPlanTaskTypeDto }>;
|
||||
dispatchToWecom?: boolean;
|
||||
dispatchSupplement?: string;
|
||||
}>();
|
||||
const [batchForm] = Form.useForm<{ items: BatchReviewPreviewItem[] }>();
|
||||
const [batchStatusForm] = Form.useForm<{
|
||||
status: SupportTicketStatusDto;
|
||||
rejectReason?: string;
|
||||
note?: string;
|
||||
}>();
|
||||
|
||||
const isSuperAdmin = profile?.adminRole === 'SUPER_ADMIN';
|
||||
|
||||
@@ -117,6 +137,7 @@ export default function SupportTicketsPage() {
|
||||
title: values.title.trim(),
|
||||
content: values.content?.trim() || undefined,
|
||||
remark: values.remark?.trim() || undefined,
|
||||
attachmentUrls: (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean),
|
||||
}),
|
||||
});
|
||||
message.success('技术支持工单已创建,等待最高管理员评审');
|
||||
@@ -166,13 +187,80 @@ export default function SupportTicketsPage() {
|
||||
const summary = [detail!.title, detail!.content].filter(Boolean).join('\n').slice(0, 500);
|
||||
tasksForm.setFieldsValue({
|
||||
tasks: [{ content: summary || detail!.title, type: mapSupportTicketTypeToDevPlanTask(detail!.ticketType) }],
|
||||
dispatchToWecom: localStorage.getItem(DISPATCH_WECOM_STORAGE_KEY) !== 'false',
|
||||
dispatchSupplement: DEFAULT_DISPATCH_SUPPLEMENT,
|
||||
});
|
||||
setCreateTasksOpen(true);
|
||||
}
|
||||
|
||||
function openEdit() {
|
||||
if (!detail) return;
|
||||
editForm.setFieldsValue({
|
||||
ticketType: detail.ticketType,
|
||||
title: detail.title,
|
||||
content: detail.content || '',
|
||||
remark: detail.remark || '',
|
||||
attachmentUrls: detail.attachmentUrls?.length ? detail.attachmentUrls : [''],
|
||||
});
|
||||
setEditOpen(true);
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
const values = await editForm.validateFields();
|
||||
setEditSaving(true);
|
||||
try {
|
||||
await request(`/admin/support-tickets/${detail!.id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
ticketType: values.ticketType,
|
||||
title: values.title.trim(),
|
||||
content: values.content?.trim() || undefined,
|
||||
remark: values.remark?.trim() || undefined,
|
||||
attachmentUrls: (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean),
|
||||
}),
|
||||
});
|
||||
message.success('已保存');
|
||||
setEditOpen(false);
|
||||
await openDetail(String(detail!.id));
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败');
|
||||
} finally {
|
||||
setEditSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitBatchStatus() {
|
||||
const values = await batchStatusForm.validateFields();
|
||||
setBatchStatusSaving(true);
|
||||
try {
|
||||
const res = await request<{ successCount: number; failCount: number }>(
|
||||
'/admin/support-tickets/batch-update-status',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
ticketIds: selectedRowKeys,
|
||||
status: values.status,
|
||||
rejectReason: values.rejectReason?.trim() || undefined,
|
||||
note: values.note?.trim() || undefined,
|
||||
}),
|
||||
},
|
||||
);
|
||||
message.success(`成功 ${res.successCount} 条,失败 ${res.failCount} 条`);
|
||||
setBatchStatusOpen(false);
|
||||
setSelectedRowKeys([]);
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '批量更新失败');
|
||||
} finally {
|
||||
setBatchStatusSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCreateTasks() {
|
||||
const values = await tasksForm.validateFields();
|
||||
const note = reviewForm.getFieldValue('note') as string | undefined;
|
||||
localStorage.setItem(DISPATCH_WECOM_STORAGE_KEY, values.dispatchToWecom ? 'true' : 'false');
|
||||
setActing(true);
|
||||
try {
|
||||
await request(`/admin/support-tickets/${detail!.id}/review`, {
|
||||
@@ -181,9 +269,11 @@ export default function SupportTicketsPage() {
|
||||
decision: 'APPROVE',
|
||||
note: note?.trim() || undefined,
|
||||
tasks: values.tasks.map((t) => ({ content: t.content.trim(), type: t.type })),
|
||||
dispatchToWecom: !!values.dispatchToWecom,
|
||||
dispatchSupplement: values.dispatchSupplement?.trim() || undefined,
|
||||
}),
|
||||
});
|
||||
message.success('审批通过,已创建开发任务');
|
||||
message.success(values.dispatchToWecom ? '审批通过,已创建任务并派发企微' : '审批通过,已创建开发任务');
|
||||
setCreateTasksOpen(false);
|
||||
setDrawerOpen(false);
|
||||
reload();
|
||||
@@ -309,9 +399,12 @@ export default function SupportTicketsPage() {
|
||||
if (!detail) return null;
|
||||
if (detail.status === 'PENDING_REVIEW' && isSuperAdmin) {
|
||||
return (
|
||||
<Button type="primary" loading={acting} onClick={openReview}>
|
||||
审批
|
||||
</Button>
|
||||
<Space>
|
||||
<Button onClick={openEdit}>编辑</Button>
|
||||
<Button type="primary" loading={acting} onClick={openReview}>
|
||||
审批
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
if (detail.status === 'DEVELOPING') {
|
||||
@@ -346,9 +439,20 @@ export default function SupportTicketsPage() {
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
{isSuperAdmin ? (
|
||||
<Button disabled={!selectedRowKeys.length} loading={acting} onClick={() => void startBatchPreview()}>
|
||||
批量审核
|
||||
</Button>
|
||||
<>
|
||||
<Button disabled={!selectedRowKeys.length} loading={acting} onClick={() => void startBatchPreview()}>
|
||||
批量审核
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!selectedRowKeys.length}
|
||||
onClick={() => {
|
||||
batchStatusForm.setFieldsValue({ status: 'TESTING', rejectReason: '', note: '' });
|
||||
setBatchStatusOpen(true);
|
||||
}}
|
||||
>
|
||||
批量改状态
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Button type="primary" onClick={() => setCreateOpen(true)}>
|
||||
创建工单
|
||||
@@ -612,6 +716,18 @@ export default function SupportTicketsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{detail.attachmentUrls?.length ? (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||||
附件
|
||||
</Typography.Text>
|
||||
<Space wrap style={{ marginTop: 8 }}>
|
||||
{detail.attachmentUrls.map((url) => (
|
||||
<Image key={url} src={url} width={80} height={80} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
|
||||
{/* 关联开发任务 */}
|
||||
@@ -661,7 +777,7 @@ export default function SupportTicketsPage() {
|
||||
</Drawer>
|
||||
|
||||
<Modal title="创建技术支持工单" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={() => void submitCreate()} confirmLoading={creating} destroyOnClose okText="提交">
|
||||
<Form form={createForm} layout="vertical" initialValues={{ ticketType: 'BUG' }}>
|
||||
<Form form={createForm} layout="vertical" initialValues={{ ticketType: 'BUG', attachmentUrls: [''] }}>
|
||||
<Form.Item name="ticketType" label="类型" rules={[{ required: true }]}>
|
||||
<Select options={TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
@@ -674,6 +790,27 @@ export default function SupportTicketsPage() {
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={512} showCount />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件图片">
|
||||
<Form.List name="attachmentUrls">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Space key={field.key} align="start" style={{ display: 'flex', marginBottom: 8 }}>
|
||||
<Form.Item {...field} style={{ flex: 1, marginBottom: 0 }}>
|
||||
<OssUpload bizType="SUPPORT_TICKET_ATTACHMENT" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
{fields.length > 1 ? (
|
||||
<MinusCircleOutlined onClick={() => remove(field.name)} style={{ marginTop: 8 }} />
|
||||
) : null}
|
||||
</Space>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add('')} block icon={<PlusOutlined />}>
|
||||
添加附件
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -725,6 +862,86 @@ export default function SupportTicketsPage() {
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item name="dispatchToWecom" valuePropName="checked" style={{ marginTop: 16 }}>
|
||||
<Checkbox>审批通过后发送到企微开发助手</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="dispatchSupplement" label="派发补充说明">
|
||||
<Input.TextArea rows={2} placeholder={DEFAULT_DISPATCH_SUPPLEMENT} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="编辑工单"
|
||||
open={editOpen}
|
||||
onCancel={() => setEditOpen(false)}
|
||||
onOk={() => void submitEdit()}
|
||||
confirmLoading={editSaving}
|
||||
destroyOnClose
|
||||
width={640}
|
||||
>
|
||||
<Form form={editForm} layout="vertical">
|
||||
<Form.Item name="ticketType" label="类型" rules={[{ required: true }]}>
|
||||
<Select options={TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
|
||||
<Input maxLength={128} showCount />
|
||||
</Form.Item>
|
||||
<Form.Item name="content" label="详细说明">
|
||||
<Input.TextArea rows={5} maxLength={4000} showCount />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={512} showCount />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件图片">
|
||||
<Form.List name="attachmentUrls">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Space key={field.key} align="start" style={{ display: 'flex', marginBottom: 8 }}>
|
||||
<Form.Item {...field} style={{ flex: 1, marginBottom: 0 }}>
|
||||
<OssUpload bizType="SUPPORT_TICKET_ATTACHMENT" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
{fields.length > 1 ? (
|
||||
<MinusCircleOutlined onClick={() => remove(field.name)} style={{ marginTop: 8 }} />
|
||||
) : null}
|
||||
</Space>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add('')} block icon={<PlusOutlined />}>
|
||||
添加附件
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="批量改状态"
|
||||
open={batchStatusOpen}
|
||||
onCancel={() => setBatchStatusOpen(false)}
|
||||
onOk={() => void submitBatchStatus()}
|
||||
confirmLoading={batchStatusSaving}
|
||||
destroyOnClose
|
||||
>
|
||||
<Typography.Paragraph type="secondary">已选 {selectedRowKeys.length} 条工单</Typography.Paragraph>
|
||||
<Form form={batchStatusForm} layout="vertical">
|
||||
<Form.Item name="status" label="目标状态" rules={[{ required: true }]}>
|
||||
<Select options={STATUS_OPTIONS.filter((o) => o.value !== 'PENDING_REVIEW')} />
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.status !== cur.status}>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('status') === 'REJECTED' ? (
|
||||
<Form.Item name="rejectReason" label="驳回理由" rules={[{ required: true }]}>
|
||||
<Input.TextArea rows={2} maxLength={512} showCount />
|
||||
</Form.Item>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item name="note" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={512} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user