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:
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Alert, Button, Form, Input, InputNumber, Space, Typography, message } from 'antd';
|
||||
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_MAX_COUNT } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
|
||||
import OssUpload from './OssUpload';
|
||||
type PackageRow = StorePackageItemDto;
|
||||
|
||||
function emptyRow(index = 0): PackageRow {
|
||||
return { name: '', price: '0', dishes: '', usableTime: '', otherNotes: '', sortOrder: index };
|
||||
return { name: '', price: '0', dishes: '', usableTime: '', otherNotes: '', imageUrl: '', sortOrder: index };
|
||||
}
|
||||
|
||||
export default function AdminStorePackagesSection({ storeId }: { storeId: string }) {
|
||||
@@ -41,18 +41,33 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
}
|
||||
|
||||
function removeAt(index: number) {
|
||||
setItems((prev) => prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, sortOrder: i })));
|
||||
setCollapsed((prev) => {
|
||||
const next: Record<number, boolean> = {};
|
||||
Object.entries(prev).forEach(([k, v]) => {
|
||||
const i = Number(k);
|
||||
if (i < index) next[i] = v;
|
||||
else if (i > index) next[i - 1] = v;
|
||||
const run = () => {
|
||||
setItems((prev) => {
|
||||
const next = prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, sortOrder: i }));
|
||||
return next.length ? next : [];
|
||||
});
|
||||
return next;
|
||||
});
|
||||
setCollapsed((prev) => {
|
||||
const next: Record<number, boolean> = {};
|
||||
Object.entries(prev).forEach(([k, v]) => {
|
||||
const i = Number(k);
|
||||
if (i < index) next[i] = v;
|
||||
else if (i > index) next[i - 1] = v;
|
||||
});
|
||||
return next;
|
||||
});
|
||||
};
|
||||
if (items.length === 1) {
|
||||
Modal.confirm({
|
||||
title: '清空门店套餐',
|
||||
content: '删除最后一条套餐后,该门店将无展示套餐,确认继续?',
|
||||
okText: '确认删除',
|
||||
cancelText: '取消',
|
||||
onOk: run,
|
||||
});
|
||||
return;
|
||||
}
|
||||
run();
|
||||
}
|
||||
|
||||
function toggleCollapse(index: number) {
|
||||
setCollapsed((prev) => ({ ...prev, [index]: !prev[index] }));
|
||||
}
|
||||
@@ -65,6 +80,7 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
dishes: item.dishes.trim(),
|
||||
usableTime: item.usableTime?.trim() || null,
|
||||
otherNotes: item.otherNotes?.trim() || null,
|
||||
imageUrl: item.imageUrl?.trim() || null,
|
||||
sortOrder: index,
|
||||
}))
|
||||
.filter((item) => item.name || item.dishes || item.price);
|
||||
@@ -106,8 +122,21 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
return <Typography.Text type="secondary">加载套餐中…</Typography.Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form layout="vertical" requiredMark={false}>
|
||||
if (!items.length) {
|
||||
return (
|
||||
<Form layout="vertical" requiredMark={false}>
|
||||
<Alert type="info" showIcon style={{ marginBottom: 16 }} message="该门店暂无套餐,可添加或保存为空。" />
|
||||
<Button onClick={addRow} style={{ marginBottom: 16 }}>
|
||||
添加套餐
|
||||
</Button>
|
||||
<Button type="primary" loading={saving} onClick={() => void save()}>
|
||||
保存套餐
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
return ( <Form layout="vertical" requiredMark={false}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
@@ -140,12 +169,11 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
{displayName}
|
||||
</Typography.Title>
|
||||
</Button>
|
||||
{items.length > 1 ? (
|
||||
{items.length > 0 ? (
|
||||
<Button type="link" danger onClick={() => removeAt(index)}>
|
||||
删除
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
) : null} </Space>
|
||||
|
||||
{!isCollapsed ? (
|
||||
<>
|
||||
@@ -186,8 +214,16 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="其他说明" style={{ marginBottom: 0 }}>
|
||||
<Input
|
||||
<Form.Item label="套餐图片" style={{ marginBottom: 12 }}>
|
||||
<OssUpload
|
||||
bizType="STORE_PACKAGE"
|
||||
mediaType="IMAGE"
|
||||
value={item.imageUrl || undefined}
|
||||
onChange={(url) => updateAt(index, { imageUrl: url })}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="其他说明" style={{ marginBottom: 0 }}> <Input
|
||||
placeholder="不可叠加"
|
||||
value={item.otherNotes || ''}
|
||||
onChange={(e) => updateAt(index, { otherNotes: e.target.value })}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type DevPlanTaskDto,
|
||||
type DevPlanTaskStatusDto,
|
||||
type DevPlanTaskTypeDto,
|
||||
type DevPlanVersionDto,
|
||||
} from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
@@ -46,11 +47,22 @@ export default function DevPlanTasksPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [dispatchOpen, setDispatchOpen] = useState(false);
|
||||
const [batchEditOpen, setBatchEditOpen] = useState(false);
|
||||
const [batchSaving, setBatchSaving] = useState(false);
|
||||
const [versions, setVersions] = useState<DevPlanVersionDto[]>([]);
|
||||
const [editing, setEditing] = useState<DevPlanTaskDto | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [dispatching, setDispatching] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [dispatchForm] = Form.useForm<{ supplement?: string }>();
|
||||
const [batchForm] = Form.useForm<{ status?: DevPlanTaskStatusDto; versionId?: string }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!batchEditOpen) return;
|
||||
request<{ items: DevPlanVersionDto[] }>('/admin/dev-plan/versions?pageSize=100')
|
||||
.then((res) => setVersions(res.items ?? []))
|
||||
.catch(() => setVersions([]));
|
||||
}, [batchEditOpen]);
|
||||
|
||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<DevPlanTaskDto>(
|
||||
'/admin/dev-plan/tasks',
|
||||
@@ -119,6 +131,38 @@ export default function DevPlanTasksPage() {
|
||||
setDispatchOpen(true);
|
||||
}
|
||||
|
||||
function openBatchEdit() {
|
||||
batchForm.setFieldsValue({ status: undefined, versionId: undefined });
|
||||
setBatchEditOpen(true);
|
||||
}
|
||||
|
||||
async function submitBatchEdit() {
|
||||
const values = await batchForm.validateFields();
|
||||
if (!values.status && !values.versionId) {
|
||||
message.warning('请至少选择状态或关联版本');
|
||||
return;
|
||||
}
|
||||
setBatchSaving(true);
|
||||
try {
|
||||
await request('/admin/dev-plan/tasks/batch-update', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
taskIds: selectedRowKeys,
|
||||
status: values.status || undefined,
|
||||
versionId: values.versionId || undefined,
|
||||
}),
|
||||
});
|
||||
message.success('批量更新成功');
|
||||
setBatchEditOpen(false);
|
||||
setSelectedRowKeys([]);
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '批量更新失败');
|
||||
} finally {
|
||||
setBatchSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDispatch() {
|
||||
const values = await dispatchForm.validateFields();
|
||||
setDispatching(true);
|
||||
@@ -186,6 +230,9 @@ export default function DevPlanTasksPage() {
|
||||
开发计划 · 任务列表
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Button disabled={!selectedRowKeys.length} onClick={openBatchEdit}>
|
||||
批量编辑
|
||||
</Button>
|
||||
<Button disabled={!selectedRowKeys.length} onClick={openDispatch}>
|
||||
评审
|
||||
</Button>
|
||||
@@ -280,6 +327,29 @@ export default function DevPlanTasksPage() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="批量编辑任务"
|
||||
open={batchEditOpen}
|
||||
onCancel={() => setBatchEditOpen(false)}
|
||||
onOk={() => void submitBatchEdit()}
|
||||
confirmLoading={batchSaving}
|
||||
destroyOnClose
|
||||
>
|
||||
<Typography.Paragraph type="secondary">已选 {selectedRowKeys.length} 条任务</Typography.Paragraph>
|
||||
<Form form={batchForm} layout="vertical">
|
||||
<Form.Item name="status" label="状态(不修改请留空)">
|
||||
<Select allowClear placeholder="不修改" options={STATUS_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="versionId" label="关联版本(追加关联,不修改请留空)">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="不修改"
|
||||
options={versions.map((v) => ({ value: v.id, label: v.versionNo }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,10 +168,10 @@ function ProductDetailFields({ form, aromaType }: { form: FormInstance; aromaTyp
|
||||
<>
|
||||
<ProductDetailTemplatePicker form={form} aromaType={aromaType} />
|
||||
<Divider />
|
||||
<Typography.Text type="secondary">详情页轮播(CAROUSEL)</Typography.Text>
|
||||
<Typography.Text type="secondary">详情页轮播(CAROUSEL,单张最大 10MB)</Typography.Text>
|
||||
<ImageUrlList name="carouselUrls" label="轮播图" bizType="CAROUSEL" />
|
||||
<Divider />
|
||||
<Typography.Text type="secondary">详情长图(DETAIL,可逐张修改)</Typography.Text>
|
||||
<Typography.Text type="secondary">详情长图(DETAIL,可逐张修改,单张最大 10MB)</Typography.Text>
|
||||
<DetailImageUrlList label="详情图" bizType="DETAIL" />
|
||||
<Divider />
|
||||
<Form.Item name="storyTitle" label="故事标题">
|
||||
|
||||
@@ -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