feat: 完善出海商机管道快速录入、RTL服务端首屏渲染与AI顾问留资动效闭环

这个提交包含在:
wikiboss
2026-09-21 01:45:14 +08:00
父节点 1faaef3198
当前提交 6b79b8d26b
共有 15 个文件被更改,包括 1070 次插入 和 6 次删除

查看文件

@@ -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');
});