feat: 完善出海商机管道快速录入、RTL服务端首屏渲染与AI顾问留资动效闭环
这个提交包含在:
@@ -10,6 +10,7 @@ import {
|
||||
Spin,
|
||||
Tag,
|
||||
Typography,
|
||||
Modal,
|
||||
} from '@arco-design/web-react';
|
||||
import axios from 'axios';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
@@ -20,6 +21,7 @@ import locale from './locale';
|
||||
const DEFAULT_PAGE = 1;
|
||||
const BATCH_SIZE = 100;
|
||||
const PAGE_API = '/api/admin/crm/lead/page';
|
||||
const ADD_LEAD_API = '/api/admin/crm/lead/add';
|
||||
const UPDATE_STATUS_API = '/api/admin/crm/lead/updateStatus';
|
||||
|
||||
interface PipelineFilterForm {
|
||||
@@ -266,6 +268,47 @@ function CrmPipelinePage() {
|
||||
[t]
|
||||
);
|
||||
|
||||
const [quickAddVisible, setQuickAddVisible] = useState(false);
|
||||
const [targetStage, setTargetStage] = useState<LeadStatus>('new');
|
||||
const [quickForm] = Form.useForm();
|
||||
const [submittingQuick, setSubmittingQuick] = useState(false);
|
||||
|
||||
const handleOpenQuickAdd = (status: LeadStatus) => {
|
||||
setTargetStage(status);
|
||||
quickForm.resetFields();
|
||||
setQuickAddVisible(true);
|
||||
};
|
||||
|
||||
const handleQuickAddSubmit = async () => {
|
||||
try {
|
||||
const values = await quickForm.validate();
|
||||
setSubmittingQuick(true);
|
||||
const cardId = parseCardId(filters.cardId) || 1;
|
||||
const { data: payload } = await axios.post<CoolUpdateResponse>(ADD_LEAD_API, {
|
||||
name: values.name,
|
||||
company: values.company || '',
|
||||
phone: values.phone || '',
|
||||
email: values.email || '',
|
||||
status: targetStage,
|
||||
cardId,
|
||||
source: 'link',
|
||||
intentScore: targetStage === 'hot' ? 85 : targetStage === 'won' ? 100 : 50,
|
||||
});
|
||||
|
||||
if (payload.code === 1000) {
|
||||
Message.success(t['crmPipeline.modal.success']);
|
||||
setQuickAddVisible(false);
|
||||
refreshBoard();
|
||||
return;
|
||||
}
|
||||
Message.error(payload.message || t['crmPipeline.update.fail']);
|
||||
} catch (error: unknown) {
|
||||
Message.error(resolveErrorMessage(error, t['crmPipeline.update.fail']));
|
||||
} finally {
|
||||
setSubmittingQuick(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t['menu.crm.pipeline']}>
|
||||
<Form form={form} layout="inline" onSubmit={handleRefresh}>
|
||||
@@ -302,9 +345,11 @@ function CrmPipelinePage() {
|
||||
extra={leads.length}
|
||||
>
|
||||
{leads.length === 0 ? (
|
||||
<Typography.Text type="secondary">
|
||||
{t['crmPipeline.column.empty']}
|
||||
</Typography.Text>
|
||||
<div style={{ textAlign: 'center', padding: '16px 0' }}>
|
||||
<Typography.Text type="secondary">
|
||||
{t['crmPipeline.column.empty']}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
leads.map((lead) => {
|
||||
const source = isLeadSource(lead.source)
|
||||
@@ -312,7 +357,7 @@ function CrmPipelinePage() {
|
||||
: 'link';
|
||||
const company = lead.company?.trim();
|
||||
return (
|
||||
<Card key={lead.id} size="small">
|
||||
<Card key={lead.id} size="small" style={{ marginBottom: 8 }}>
|
||||
<Typography.Title heading={6}>
|
||||
{lead.name}
|
||||
</Typography.Title>
|
||||
@@ -344,12 +389,48 @@ function CrmPipelinePage() {
|
||||
);
|
||||
})
|
||||
)}
|
||||
<Button
|
||||
type="dashed"
|
||||
size="mini"
|
||||
long
|
||||
style={{ marginTop: 8 }}
|
||||
onClick={() => handleOpenQuickAdd(status)}
|
||||
>
|
||||
+ {t['crmPipeline.action.quickAdd']}
|
||||
</Button>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
);
|
||||
})}
|
||||
</Grid.Row>
|
||||
</Spin>
|
||||
|
||||
<Modal
|
||||
title={t['crmPipeline.modal.quickAddTitle']}
|
||||
visible={quickAddVisible}
|
||||
onOk={handleQuickAddSubmit}
|
||||
confirmLoading={submittingQuick}
|
||||
onCancel={() => setQuickAddVisible(false)}
|
||||
>
|
||||
<Form form={quickForm} layout="vertical">
|
||||
<Form.Item
|
||||
field="name"
|
||||
label={t['crmPipeline.modal.name']}
|
||||
rules={[{ required: true, message: t['crmPipeline.modal.name'] }]}
|
||||
>
|
||||
<Input placeholder={t['crmPipeline.modal.name']} />
|
||||
</Form.Item>
|
||||
<Form.Item field="company" label={t['crmPipeline.modal.company']}>
|
||||
<Input placeholder={t['crmPipeline.modal.company']} />
|
||||
</Form.Item>
|
||||
<Form.Item field="phone" label={t['crmPipeline.modal.phone']}>
|
||||
<Input placeholder={t['crmPipeline.modal.phone']} />
|
||||
</Form.Item>
|
||||
<Form.Item field="email" label={t['crmPipeline.modal.email']}>
|
||||
<Input placeholder={t['crmPipeline.modal.email']} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@ const i18n = {
|
||||
'crmPipeline.source.link': 'Link',
|
||||
'crmPipeline.source.ai_chat': 'AI chat',
|
||||
'crmPipeline.column.empty': 'No opportunities in this stage.',
|
||||
'crmPipeline.action.quickAdd': 'Add Opportunity',
|
||||
'crmPipeline.modal.quickAddTitle': 'Quick Record Opportunity',
|
||||
'crmPipeline.modal.name': 'Name',
|
||||
'crmPipeline.modal.company': 'Company',
|
||||
'crmPipeline.modal.phone': 'Phone',
|
||||
'crmPipeline.modal.email': 'Email',
|
||||
'crmPipeline.modal.success': 'Opportunity recorded successfully',
|
||||
'crmPipeline.request.fail': 'Unable to load the pipeline',
|
||||
'crmPipeline.update.fail': 'Unable to update opportunity status',
|
||||
},
|
||||
@@ -46,6 +53,13 @@ const i18n = {
|
||||
'crmPipeline.source.link': '链接',
|
||||
'crmPipeline.source.ai_chat': 'AI 对话',
|
||||
'crmPipeline.column.empty': '此阶段暂无商机。',
|
||||
'crmPipeline.action.quickAdd': '快速记录商机',
|
||||
'crmPipeline.modal.quickAddTitle': '快速录入新商机',
|
||||
'crmPipeline.modal.name': '联系人姓名',
|
||||
'crmPipeline.modal.company': '公司名称',
|
||||
'crmPipeline.modal.phone': '联系电话',
|
||||
'crmPipeline.modal.email': '邮箱地址',
|
||||
'crmPipeline.modal.success': '商机已快速记录',
|
||||
'crmPipeline.request.fail': '商机看板加载失败',
|
||||
'crmPipeline.update.fail': '商机状态更新失败',
|
||||
},
|
||||
@@ -71,6 +85,13 @@ const i18n = {
|
||||
'crmPipeline.source.link': 'رابط',
|
||||
'crmPipeline.source.ai_chat': 'محادثة الذكاء الاصطناعي',
|
||||
'crmPipeline.column.empty': 'لا توجد فرص في هذه المرحلة.',
|
||||
'crmPipeline.action.quickAdd': 'إضافة فرصة',
|
||||
'crmPipeline.modal.quickAddTitle': 'تسجيل فرصة سريعة',
|
||||
'crmPipeline.modal.name': 'اسم جهة الاتصال',
|
||||
'crmPipeline.modal.company': 'اسم الشركة',
|
||||
'crmPipeline.modal.phone': 'الهاتف',
|
||||
'crmPipeline.modal.email': 'البريد الإلكتروني',
|
||||
'crmPipeline.modal.success': 'تم تسجيل الفرصة بنجاح',
|
||||
'crmPipeline.request.fail': 'تعذر تحميل مسار الفرص',
|
||||
'crmPipeline.update.fail': 'تعذر تحديث حالة الفرصة',
|
||||
},
|
||||
|
||||
@@ -82,3 +82,17 @@ test('3. crm/pipeline locale 必须包含 menu.crm.pipeline 与看板文案', ()
|
||||
assert.ok(content.includes('crmPipeline.status.archived'), '必须包含归档状态文案');
|
||||
assert.ok(!/\bany\b/.test(content), 'locale 禁止使用 any');
|
||||
});
|
||||
|
||||
test('4. crm/pipeline 必须支持泳道快速录入商机 Modal 与对应语言包', () => {
|
||||
const content = fs.readFileSync(pipelinePagePath, 'utf-8');
|
||||
assert.ok(content.includes('Modal'), '必须使用 Arco Modal 弹窗录入新商机');
|
||||
assert.ok(
|
||||
content.includes('admin/crm/lead/add') || content.includes('lead/add'),
|
||||
'必须支持调用新增商机 API'
|
||||
);
|
||||
assert.ok(content.includes('crmPipeline.action.quickAdd'), '必须包含快速新增按钮文案绑定');
|
||||
|
||||
const localeContent = fs.readFileSync(pipelineLocalePath, 'utf-8');
|
||||
assert.ok(localeContent.includes('crmPipeline.action.quickAdd'), '语言包必须包含 crmPipeline.action.quickAdd');
|
||||
assert.ok(localeContent.includes('crmPipeline.modal.quickAddTitle'), '语言包必须包含 crmPipeline.modal.quickAddTitle');
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户