Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 281cfdb0e4 |
@@ -49,6 +49,17 @@ const statusTag = (s: string) => {
|
||||
return <Tag color={m.color}>{m.label}</Tag>;
|
||||
};
|
||||
|
||||
// 反馈类型:comparison=比价反馈 / profile(及旧数据)=普通反馈
|
||||
const SOURCE_META: Record<string, { label: string; color: string }> = {
|
||||
comparison: { label: '比价反馈', color: 'geekblue' },
|
||||
profile: { label: '普通反馈', color: 'default' },
|
||||
};
|
||||
|
||||
const sourceTag = (s: string) => {
|
||||
const m = SOURCE_META[s] ?? { label: '普通反馈', color: 'default' };
|
||||
return <Tag color={m.color}>{m.label}</Tag>;
|
||||
};
|
||||
|
||||
function FeedbackImages({ images }: { images: string[] | null }) {
|
||||
if (!images || !images.length) return <Text type="secondary">无截图</Text>;
|
||||
return (
|
||||
@@ -113,12 +124,14 @@ export default function FeedbackHandleDrawer({ feedback, open, onClose, onDone }
|
||||
await api.post(`/admin/api/feedbacks/${feedback.id}/approve`, {
|
||||
reward_coins: v.reward_coins,
|
||||
note: v.note?.trim() || null,
|
||||
reply: v.reply?.trim() || null,
|
||||
});
|
||||
message.success(`已采纳,发放 ${v.reward_coins} 金币`);
|
||||
} else {
|
||||
await api.post(`/admin/api/feedbacks/${feedback.id}/reject`, {
|
||||
reason: v.reason.trim(),
|
||||
note: v.note?.trim() || null,
|
||||
reply: v.reply?.trim() || null,
|
||||
});
|
||||
message.success('已拒绝');
|
||||
}
|
||||
@@ -155,6 +168,14 @@ export default function FeedbackHandleDrawer({ feedback, open, onClose, onDone }
|
||||
<Text type="secondary">
|
||||
用户 {feedback.user_id} · {formatWallTime(feedback.created_at)}
|
||||
</Text>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
{sourceTag(feedback.source)}
|
||||
{feedback.scene ? (
|
||||
<Text type="secondary" style={{ marginLeft: 6 }}>
|
||||
场景:{feedback.scene}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
<Paragraph style={{ whiteSpace: 'pre-wrap', marginTop: 8, marginBottom: 8 }}>
|
||||
{feedback.content || '-'}
|
||||
</Paragraph>
|
||||
@@ -200,7 +221,11 @@ export default function FeedbackHandleDrawer({ feedback, open, onClose, onDone }
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: 6 }}>
|
||||
审核备注:{feedback.review_note || <Text type="secondary">(无)</Text>}
|
||||
审核备注(内部):{feedback.review_note || <Text type="secondary">(无)</Text>}
|
||||
</div>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
给用户的回复(用户可见):
|
||||
{feedback.admin_reply || <Text type="secondary">(无)</Text>}
|
||||
</div>
|
||||
{isPending(feedback.status) && (
|
||||
<div style={{ marginTop: 6 }}>
|
||||
@@ -243,11 +268,23 @@ export default function FeedbackHandleDrawer({ feedback, open, onClose, onDone }
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="note"
|
||||
label="采纳要点 / 审核备注(选填)"
|
||||
label="采纳要点 / 审核备注(选填,内部)"
|
||||
rules={[{ max: 256, message: '最多 256 字' }]}
|
||||
>
|
||||
<Input.TextArea rows={3} maxLength={256} showCount placeholder="如:建议已采纳,将在下个版本上线" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="reply"
|
||||
label="给用户的回复(选填,用户端可见)"
|
||||
rules={[{ max: 256, message: '最多 256 字' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
maxLength={256}
|
||||
showCount
|
||||
placeholder="将展示在用户的「我的反馈」里,如:感谢反馈,已收到~"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -268,6 +305,18 @@ export default function FeedbackHandleDrawer({ feedback, open, onClose, onDone }
|
||||
>
|
||||
<Input.TextArea rows={2} maxLength={256} showCount placeholder="运营内部备注" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="reply"
|
||||
label="给用户的回复(选填,用户端可见)"
|
||||
rules={[{ max: 256, message: '最多 256 字' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
maxLength={256}
|
||||
showCount
|
||||
placeholder="除未采纳原因外,想额外对用户说的话(选填)"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
@@ -294,7 +343,10 @@ export default function FeedbackHandleDrawer({ feedback, open, onClose, onDone }
|
||||
<Text type="secondary">
|
||||
#{h.id} · {formatWallTime(h.created_at)}
|
||||
</Text>
|
||||
{statusTag(h.status)}
|
||||
<Space size={4}>
|
||||
{sourceTag(h.source)}
|
||||
{statusTag(h.status)}
|
||||
</Space>
|
||||
</div>
|
||||
<Paragraph
|
||||
style={{ whiteSpace: 'pre-wrap', margin: '6px 0 0' }}
|
||||
@@ -313,6 +365,9 @@ export default function FeedbackHandleDrawer({ feedback, open, onClose, onDone }
|
||||
未采纳:{h.reject_reason || '-'}
|
||||
</Text>
|
||||
)}
|
||||
{h.admin_reply ? (
|
||||
<div style={{ fontSize: 12, marginTop: 2 }}>回复:{h.admin_reply}</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
@@ -49,7 +49,42 @@ const statusTag = (s: string) => {
|
||||
return <Tag color={m.color}>{m.label}</Tag>;
|
||||
};
|
||||
|
||||
// 审核结果展示(采纳=金币+批注 / 未采纳=原因 / 待审核=-),主表与「该用户全部反馈」抽屉共用
|
||||
// 反馈类型:comparison=比价反馈(比价结果页入口) / profile=普通反馈(「我的」页入口)。
|
||||
// 旧数据与未识别来源按普通反馈展示。
|
||||
const SOURCE_META: Record<string, { label: string; color: string }> = {
|
||||
comparison: { label: '比价反馈', color: 'geekblue' },
|
||||
profile: { label: '普通反馈', color: 'default' },
|
||||
};
|
||||
|
||||
const SOURCE_OPTIONS = [
|
||||
{ value: 'comparison', label: '比价反馈' },
|
||||
{ value: 'profile', label: '普通反馈' },
|
||||
];
|
||||
|
||||
// 反馈类型标签 +(比价反馈时)问题场景副标题
|
||||
const renderSource = (f: Feedback) => {
|
||||
const m = SOURCE_META[f.source] ?? { label: '普通反馈', color: 'default' };
|
||||
return (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Tag color={m.color}>{m.label}</Tag>
|
||||
{f.scene ? (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{f.scene}
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
// 用户可见的运营回复留言(采纳/未采纳都可能有)
|
||||
const replyLine = (f: Feedback) =>
|
||||
f.admin_reply ? (
|
||||
<Text style={{ fontSize: 12 }} ellipsis>
|
||||
回复:{f.admin_reply}
|
||||
</Text>
|
||||
) : null;
|
||||
|
||||
// 审核结果展示(采纳=金币+批注 / 未采纳=原因 / 回复留言 / 待审核=-),主表与「该用户全部反馈」抽屉共用
|
||||
const renderReview = (f: Feedback) => {
|
||||
if (f.status === 'adopted') {
|
||||
return (
|
||||
@@ -60,14 +95,18 @@ const renderReview = (f: Feedback) => {
|
||||
{f.review_note}
|
||||
</Text>
|
||||
) : null}
|
||||
{replyLine(f)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
if (f.status === 'rejected') {
|
||||
return (
|
||||
<Text type="secondary" style={{ fontSize: 12 }} ellipsis>
|
||||
未采纳:{f.reject_reason || '-'}
|
||||
</Text>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }} ellipsis>
|
||||
未采纳:{f.reject_reason || '-'}
|
||||
</Text>
|
||||
{replyLine(f)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
return <Text type="secondary">-</Text>;
|
||||
@@ -95,6 +134,7 @@ const renderDeviceOs = (f: Feedback) => {
|
||||
const RECORD_COLUMNS: ColumnsType<Feedback> = [
|
||||
{ title: '提交时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatWallTime(v) },
|
||||
{ title: '状态', dataIndex: 'status', width: 80, render: statusTag },
|
||||
{ title: '反馈类型', key: 'source', width: 100, render: (_: unknown, f: Feedback) => renderSource(f) },
|
||||
{
|
||||
title: '内容',
|
||||
dataIndex: 'content',
|
||||
@@ -116,6 +156,7 @@ const RECORD_COLUMNS: ColumnsType<Feedback> = [
|
||||
export default function FeedbacksPage() {
|
||||
// 筛选草稿:点「查询」才应用(避免输入即刷新)
|
||||
const [status, setStatus] = useState<string | undefined>();
|
||||
const [source, setSource] = useState<string | undefined>();
|
||||
const [userId, setUserId] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [createdRange, setCreatedRange] = useState<[Dayjs, Dayjs] | null>(null);
|
||||
@@ -158,6 +199,7 @@ export default function FeedbacksPage() {
|
||||
const search = () =>
|
||||
setApplied({
|
||||
status,
|
||||
source,
|
||||
user_id: userId.trim() ? Number(userId.trim()) : undefined,
|
||||
content: content.trim() || undefined,
|
||||
created_from: createdRange?.[0] ? createdRange[0].startOf('day').toISOString() : undefined,
|
||||
@@ -166,6 +208,7 @@ export default function FeedbacksPage() {
|
||||
|
||||
const resetFilters = () => {
|
||||
setStatus(undefined);
|
||||
setSource(undefined);
|
||||
setUserId('');
|
||||
setContent('');
|
||||
setCreatedRange(null);
|
||||
@@ -216,6 +259,12 @@ export default function FeedbacksPage() {
|
||||
width: 120,
|
||||
render: (v: string | null) => v || <Text type="secondary">无昵称</Text>,
|
||||
},
|
||||
{
|
||||
title: '反馈类型',
|
||||
key: 'source',
|
||||
width: 110,
|
||||
render: (_: unknown, f: Feedback) => renderSource(f),
|
||||
},
|
||||
{
|
||||
title: '提交版本号',
|
||||
dataIndex: 'app_version',
|
||||
@@ -325,6 +374,14 @@ export default function FeedbacksPage() {
|
||||
{ value: 'rejected', label: '未采纳' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
placeholder="反馈类型"
|
||||
value={source}
|
||||
onChange={setSource}
|
||||
allowClear
|
||||
style={{ width: 130 }}
|
||||
options={SOURCE_OPTIONS}
|
||||
/>
|
||||
<Input
|
||||
placeholder="用户ID"
|
||||
value={userId}
|
||||
@@ -367,7 +424,7 @@ export default function FeedbacksPage() {
|
||||
onChange: onPageChange,
|
||||
}}
|
||||
onChange={onTableChange}
|
||||
scroll={{ x: 1610 }}
|
||||
scroll={{ x: 1720 }}
|
||||
/>
|
||||
|
||||
<FeedbackHandleDrawer
|
||||
|
||||
@@ -104,6 +104,19 @@ const SORT_OPTIONS = [
|
||||
{ value: 'created_at', label: '申请时间' },
|
||||
{ value: 'amount_cents', label: '提现金额' },
|
||||
];
|
||||
// 提现类型:coin_cash=福利页提现(金币兑换现金) / invite_cash=邀请提现(邀请奖励金)
|
||||
const SOURCE_LABEL: Record<string, string> = {
|
||||
coin_cash: '福利页提现',
|
||||
invite_cash: '邀请提现',
|
||||
};
|
||||
const SOURCE_COLOR: Record<string, string> = {
|
||||
coin_cash: 'blue',
|
||||
invite_cash: 'purple',
|
||||
};
|
||||
const SOURCE_OPTIONS = [
|
||||
{ value: 'coin_cash', label: '福利页提现' },
|
||||
{ value: 'invite_cash', label: '邀请提现' },
|
||||
];
|
||||
const DATE_FIELD_OPTIONS = [
|
||||
{ value: 'created_at', label: '申请时间' },
|
||||
{ value: 'updated_at', label: '更新时间' },
|
||||
@@ -184,9 +197,11 @@ export default function WithdrawsPage() {
|
||||
const [sortBy, setSortBy] = useState<'created_at' | 'amount_cents'>('created_at');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
const [quickFilter, setQuickFilter] = useState<string | undefined>(undefined);
|
||||
const [source, setSource] = useState<string | undefined>(undefined);
|
||||
|
||||
const filters: Record<string, unknown> = {};
|
||||
if (activeStatus !== 'all') filters.status = activeStatus;
|
||||
if (source) filters.source = source;
|
||||
if (keyword.trim()) filters.keyword = keyword.trim();
|
||||
if (dateRange?.[0]) filters.date_from = dateRange[0].startOf('day').toISOString();
|
||||
if (dateRange?.[1]) filters.date_to = dateRange[1].endOf('day').toISOString();
|
||||
@@ -211,6 +226,7 @@ export default function WithdrawsPage() {
|
||||
setSortBy('created_at');
|
||||
setSortOrder('desc');
|
||||
setQuickFilter(undefined);
|
||||
setSource(undefined);
|
||||
};
|
||||
|
||||
const applyQuickFilter = (value?: string) => {
|
||||
@@ -481,6 +497,12 @@ export default function WithdrawsPage() {
|
||||
width: 120,
|
||||
render: (v: number) => <Text strong>{yuan(v)}</Text>,
|
||||
},
|
||||
{
|
||||
title: '提现类型',
|
||||
dataIndex: 'source',
|
||||
width: 110,
|
||||
render: (v: string) => <Tag color={SOURCE_COLOR[v] || 'default'}>{SOURCE_LABEL[v] || v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '累计提现',
|
||||
dataIndex: 'cumulative_success_cents',
|
||||
@@ -724,6 +746,14 @@ export default function WithdrawsPage() {
|
||||
}}
|
||||
onSearch={(value) => setKeyword(value.trim())}
|
||||
/>
|
||||
<Select
|
||||
value={source}
|
||||
allowClear
|
||||
placeholder="提现类型"
|
||||
style={{ width: 140 }}
|
||||
options={SOURCE_OPTIONS}
|
||||
onChange={(value?: string) => setSource(value)}
|
||||
/>
|
||||
<Select
|
||||
value={quickFilter}
|
||||
allowClear
|
||||
@@ -849,7 +879,7 @@ export default function WithdrawsPage() {
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: onPageChange,
|
||||
}}
|
||||
scroll={{ x: 1350 }}
|
||||
scroll={{ x: 1460 }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => openDetail(record),
|
||||
style: { cursor: 'pointer' },
|
||||
@@ -881,6 +911,11 @@ export default function WithdrawsPage() {
|
||||
<Descriptions.Item label="金额">
|
||||
<Text strong>{yuan(detail.order.amount_cents)}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提现类型">
|
||||
<Tag color={SOURCE_COLOR[detail.order.source] || 'default'}>
|
||||
{SOURCE_LABEL[detail.order.source] || detail.order.source}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提现实名">
|
||||
{detail.order.user_name || '-'}
|
||||
</Descriptions.Item>
|
||||
|
||||
+8
-1
@@ -117,6 +117,8 @@ export interface WithdrawOrder {
|
||||
user_id: number;
|
||||
out_bill_no: string;
|
||||
amount_cents: number;
|
||||
// 提现类型:coin_cash(福利页提现,金币兑换现金) / invite_cash(邀请提现,邀请奖励金)
|
||||
source: string;
|
||||
user_name: string | null;
|
||||
status: string; // reviewing / pending / success / failed / rejected
|
||||
wechat_state: string | null;
|
||||
@@ -239,12 +241,17 @@ export interface Feedback {
|
||||
user_id: number;
|
||||
content: string;
|
||||
contact: string;
|
||||
// 反馈来源入口:profile(「我的」页=普通反馈) / comparison(比价结果页=比价反馈)
|
||||
source: string;
|
||||
// 比价反馈的问题场景(找错商品/优惠不对…);普通反馈为 null
|
||||
scene: string | null;
|
||||
images: string[] | null;
|
||||
// pending(审核中) / adopted(已采纳) / rejected(未采纳);历史数据可能为 new
|
||||
status: string;
|
||||
reject_reason: string | null; // 未采纳原因(用户端可见)
|
||||
reward_coins: number | null; // 采纳后发放金币
|
||||
review_note: string | null; // 审核批注(采纳要点 / 内部备注)
|
||||
review_note: string | null; // 审核批注(采纳要点 / 内部备注,用户不可见)
|
||||
admin_reply: string | null; // 运营给用户的回复留言(用户端可见)
|
||||
reviewed_by_admin_id: number | null;
|
||||
reviewed_at: string | null;
|
||||
created_at: string;
|
||||
|
||||
Reference in New Issue
Block a user