导出功能调整

main
kangwenjing 2026-04-15 16:21:52 +08:00
parent b3f85504f6
commit ab711362c7
12 changed files with 137 additions and 35 deletions

View File

@ -24,30 +24,30 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"RequestMappingsPanelOrder0": "0", &quot;RequestMappingsPanelOrder0&quot;: &quot;0&quot;,
"RequestMappingsPanelOrder1": "1", &quot;RequestMappingsPanelOrder1&quot;: &quot;1&quot;,
"RequestMappingsPanelWidth0": "75", &quot;RequestMappingsPanelWidth0&quot;: &quot;75&quot;,
"RequestMappingsPanelWidth1": "75", &quot;RequestMappingsPanelWidth1&quot;: &quot;75&quot;,
"RunOnceActivity.OpenProjectViewOnStart": "true", &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"WebServerToolWindowFactoryState": "false", &quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
"git-widget-placeholder": "main", &quot;git-widget-placeholder&quot;: &quot;main&quot;,
"last_opened_file_path": "/Users/kangwenjing/Downloads/crm/unis_crm", &quot;last_opened_file_path&quot;: &quot;/Users/kangwenjing/Downloads/crm/unis_crm&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"project.structure.last.edited": "项目", &quot;project.structure.last.edited&quot;: &quot;项目&quot;,
"project.structure.proportion": "0.14450866", &quot;project.structure.proportion&quot;: &quot;0.14450866&quot;,
"project.structure.side.proportion": "0.18800648", &quot;project.structure.side.proportion&quot;: &quot;0.18800648&quot;,
"settings.editor.selected.configurable": "configurable.group.appearance", &quot;settings.editor.selected.configurable&quot;: &quot;configurable.group.appearance&quot;,
"ts.external.directory.path": "/Users/kangwenjing/Downloads/crm/unis_crm/frontend/node_modules/typescript/lib", &quot;ts.external.directory.path&quot;: &quot;/Users/kangwenjing/Downloads/crm/unis_crm/frontend/node_modules/typescript/lib&quot;,
"vue.rearranger.settings.migration": "true" &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
} }
}]]></component> }</component>
<component name="RunManager" selected="Spring Boot.UnisCrmBackendApplication"> <component name="RunManager" selected="Spring Boot.UnisCrmBackendApplication">
<configuration name="unis-crm-backend中的所有" type="JUnit" factoryName="JUnit" temporary="true" nameIsGenerated="true"> <configuration name="unis-crm-backend中的所有" type="JUnit" factoryName="JUnit" temporary="true" nameIsGenerated="true">
<module name="unis-crm-backend" /> <module name="unis-crm-backend" />
@ -94,6 +94,8 @@
<workItem from="1775541962012" duration="805000" /> <workItem from="1775541962012" duration="805000" />
<workItem from="1775611219527" duration="1273000" /> <workItem from="1775611219527" duration="1273000" />
<workItem from="1775721940000" duration="8576000" /> <workItem from="1775721940000" duration="8576000" />
<workItem from="1776219416113" duration="650000" />
<workItem from="1776238420843" duration="21000" />
</task> </task>
<task id="LOCAL-00001" summary="修改定位信息 0323"> <task id="LOCAL-00001" summary="修改定位信息 0323">
<option name="closed" value="true" /> <option name="closed" value="true" />

View File

@ -37,6 +37,7 @@ public class ChannelExpansionItemDTO {
private String stage; private String stage;
private Boolean landed; private Boolean landed;
private String expectedSignDate; private String expectedSignDate;
private String updatedAt;
private String notes; private String notes;
private List<ChannelExpansionContactDTO> contacts = new ArrayList<>(); private List<ChannelExpansionContactDTO> contacts = new ArrayList<>();
private List<ChannelRelatedProjectSummaryDTO> relatedProjects = new ArrayList<>(); private List<ChannelRelatedProjectSummaryDTO> relatedProjects = new ArrayList<>();
@ -298,6 +299,14 @@ public class ChannelExpansionItemDTO {
this.expectedSignDate = expectedSignDate; this.expectedSignDate = expectedSignDate;
} }
public String getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
public String getNotes() { public String getNotes() {
return notes; return notes;
} }

View File

@ -29,6 +29,7 @@ public class SalesExpansionItemDTO {
private Boolean active; private Boolean active;
private String employmentStatus; private String employmentStatus;
private String expectedJoinDate; private String expectedJoinDate;
private String updatedAt;
private String notes; private String notes;
private java.util.List<RelatedProjectSummaryDTO> relatedProjects = new java.util.ArrayList<>(); private java.util.List<RelatedProjectSummaryDTO> relatedProjects = new java.util.ArrayList<>();
private List<ExpansionFollowUpDTO> followUps = new ArrayList<>(); private List<ExpansionFollowUpDTO> followUps = new ArrayList<>();
@ -225,6 +226,14 @@ public class SalesExpansionItemDTO {
this.expectedJoinDate = expectedJoinDate; this.expectedJoinDate = expectedJoinDate;
} }
public String getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
public String getNotes() { public String getNotes() {
return notes; return notes;
} }

View File

@ -12,6 +12,7 @@ public class OpportunityItemDTO {
private String name; private String name;
private String client; private String client;
private String owner; private String owner;
private String updatedAt;
private String projectLocation; private String projectLocation;
private String operatorCode; private String operatorCode;
private String operatorName; private String operatorName;
@ -85,6 +86,14 @@ public class OpportunityItemDTO {
this.owner = owner; this.owner = owner;
} }
public String getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
public String getProjectLocation() { public String getProjectLocation() {
return projectLocation; return projectLocation;
} }

View File

@ -40,7 +40,7 @@ public class OpportunitySearchToolProvider extends PermissionedMcpToolProvider {
properties.put("keyword", stringProperty("关键词,匹配商机名称、编号、客户名称、项目地点、产品类型。")); properties.put("keyword", stringProperty("关键词,匹配商机名称、编号、客户名称、项目地点、产品类型。"));
properties.put("stage", stringProperty("商机阶段编码,例如 initial_contact/solution_discussion/bidding/business_negotiation/won/lost。")); properties.put("stage", stringProperty("商机阶段编码,例如 initial_contact/solution_discussion/bidding/business_negotiation/won/lost。"));
properties.put("ownerUserId", integerProperty("商机负责人用户 ID实际可见范围仍按系统数据权限裁剪。")); properties.put("ownerUserId", integerProperty("商机负责人用户 ID实际可见范围仍按系统数据权限裁剪。"));
properties.put("includeArchived", booleanProperty("是否包含已归档商机,默认 false。")); properties.put("includeArchived", booleanProperty("是否包含已签单商机,默认 false。"));
properties.put("page", integerProperty("页码,默认 1。")); properties.put("page", integerProperty("页码,默认 1。"));
properties.put("pageSize", integerProperty("每页条数,默认 10最大 50。")); properties.put("pageSize", integerProperty("每页条数,默认 10最大 50。"));
properties.put("limit", integerProperty("返回条数,默认 10最大 50。")); properties.put("limit", integerProperty("返回条数,默认 10最大 50。"));

View File

@ -89,8 +89,26 @@
(s.employment_status = 'active') as active, (s.employment_status = 'active') as active,
s.employment_status as employmentStatus, s.employment_status as employmentStatus,
coalesce(to_char(s.expected_join_date, 'YYYY-MM-DD'), '无') as expectedJoinDate, coalesce(to_char(s.expected_join_date, 'YYYY-MM-DD'), '无') as expectedJoinDate,
coalesce(
to_char(
case
when sales_followup.latest_followup_time is null then s.updated_at
else greatest(s.updated_at, sales_followup.latest_followup_time)
end,
'YYYY-MM-DD HH24:MI'
),
'无'
) as updatedAt,
coalesce(s.remark, '无') as notes coalesce(s.remark, '无') as notes
from crm_sales_expansion s from crm_sales_expansion s
left join (
select
f.biz_id,
max(f.followup_time) as latest_followup_time
from crm_expansion_followup f
where f.biz_type = 'sales'
group by f.biz_id
) sales_followup on sales_followup.biz_id = s.id
left join sys_user u left join sys_user u
on u.user_id = s.owner_user_id on u.user_id = s.owner_user_id
and u.is_deleted = 0 and u.is_deleted = 0
@ -168,8 +186,26 @@
end as stage, end as stage,
c.landed_flag as landed, c.landed_flag as landed,
coalesce(to_char(c.expected_sign_date, 'YYYY-MM-DD'), '无') as expectedSignDate, coalesce(to_char(c.expected_sign_date, 'YYYY-MM-DD'), '无') as expectedSignDate,
coalesce(
to_char(
case
when channel_followup.latest_followup_time is null then c.updated_at
else greatest(c.updated_at, channel_followup.latest_followup_time)
end,
'YYYY-MM-DD HH24:MI'
),
'无'
) as updatedAt,
coalesce(c.remark, '无') as notes coalesce(c.remark, '无') as notes
from crm_channel_expansion c from crm_channel_expansion c
left join (
select
f.biz_id,
max(f.followup_time) as latest_followup_time
from crm_expansion_followup f
where f.biz_type = 'channel'
group by f.biz_id
) channel_followup on channel_followup.biz_id = c.id
left join sys_user u left join sys_user u
on u.user_id = c.owner_user_id on u.user_id = c.owner_user_id
and u.is_deleted = 0 and u.is_deleted = 0

View File

@ -54,6 +54,16 @@
o.opportunity_name as name, o.opportunity_name as name,
coalesce(c.customer_name, '未填写最终客户') as client, coalesce(c.customer_name, '未填写最终客户') as client,
coalesce(u.display_name, '当前用户') as owner, coalesce(u.display_name, '当前用户') as owner,
coalesce(
to_char(
case
when opportunity_followup.latest_followup_time is null then o.updated_at
else greatest(o.updated_at, opportunity_followup.latest_followup_time)
end,
'YYYY-MM-DD HH24:MI'
),
'无'
) as updatedAt,
coalesce(o.project_location, '') as projectLocation, coalesce(o.project_location, '') as projectLocation,
coalesce(operator_dict.item_value, o.operator_name, '') as operatorCode, coalesce(operator_dict.item_value, o.operator_name, '') as operatorCode,
coalesce(operator_dict.item_label, nullif(o.operator_name, ''), '') as operatorName, coalesce(operator_dict.item_label, nullif(o.operator_name, ''), '') as operatorName,
@ -117,6 +127,13 @@
), '') as nextPlan, ), '') as nextPlan,
coalesce(o.description, '') as notes coalesce(o.description, '') as notes
from crm_opportunity o from crm_opportunity o
left join (
select
f.opportunity_id,
max(f.followup_time) as latest_followup_time
from crm_opportunity_followup f
group by f.opportunity_id
) opportunity_followup on opportunity_followup.opportunity_id = o.id
left join crm_customer c on c.id = o.customer_id left join crm_customer c on c.id = o.customer_id
left join sys_user u on u.user_id = o.owner_user_id left join sys_user u on u.user_id = o.owner_user_id
left join crm_sales_expansion se on se.id = o.sales_expansion_id left join crm_sales_expansion se on se.id = o.sales_expansion_id

View File

@ -386,7 +386,7 @@ flowchart TD
建议对以下数据建立归档策略: 建议对以下数据建立归档策略:
- 已归档商机 - 已签单商机
- 早期历史动态日志 - 早期历史动态日志
- 历史打卡照片 - 历史打卡照片
- 大体量日报附件或冗余快照 - 大体量日报附件或冗余快照
@ -638,4 +638,3 @@ flowchart TD
- `docs/opportunity-integration-api.md` - `docs/opportunity-integration-api.md`
- `backend/README.md` - `backend/README.md`
- `frontend/README.md` - `frontend/README.md`

View File

@ -291,8 +291,8 @@ const opportunityOverview = {
amount: 530000, amount: 530000,
date: "2026-03-18", date: "2026-03-18",
confidence: "C", confidence: "C",
stageCode: "已归档", stageCode: "已签单",
stage: "已归档", stage: "已签单",
type: "替换", type: "替换",
archived: true, archived: true,
pushedToOms: false, pushedToOms: false,

View File

@ -299,6 +299,7 @@ export interface OpportunityItem {
name?: string; name?: string;
client?: string; client?: string;
owner?: string; owner?: string;
updatedAt?: string;
projectLocation?: string; projectLocation?: string;
operatorCode?: string; operatorCode?: string;
operatorName?: string; operatorName?: string;
@ -339,6 +340,7 @@ export interface OpportunityMeta {
operatorOptions?: OpportunityDictOption[]; operatorOptions?: OpportunityDictOption[];
projectLocationOptions?: OpportunityDictOption[]; projectLocationOptions?: OpportunityDictOption[];
opportunityTypeOptions?: OpportunityDictOption[]; opportunityTypeOptions?: OpportunityDictOption[];
confidenceOptions?: OpportunityDictOption[];
} }
export interface OmsPreSalesOption { export interface OmsPreSalesOption {
@ -416,6 +418,7 @@ export interface SalesExpansionItem {
active?: boolean; active?: boolean;
employmentStatus?: string; employmentStatus?: string;
expectedJoinDate?: string; expectedJoinDate?: string;
updatedAt?: string;
notes?: string; notes?: string;
relatedProjects?: RelatedProjectSummary[]; relatedProjects?: RelatedProjectSummary[];
followUps?: ExpansionFollowUp[]; followUps?: ExpansionFollowUp[];
@ -461,6 +464,7 @@ export interface ChannelExpansionItem {
stage?: string; stage?: string;
landed?: boolean; landed?: boolean;
expectedSignDate?: string; expectedSignDate?: string;
updatedAt?: string;
notes?: string; notes?: string;
contacts?: ChannelExpansionContact[]; contacts?: ChannelExpansionContact[];
relatedProjects?: ChannelRelatedProjectSummary[]; relatedProjects?: ChannelRelatedProjectSummary[];

View File

@ -330,6 +330,7 @@ function buildSalesExportHeaders(items: SalesExpansionItem[]) {
"工号", "工号",
"姓名", "姓名",
"创建人", "创建人",
"更新修改时间",
"联系方式", "联系方式",
"代表处 / 办事处", "代表处 / 办事处",
"所属部门", "所属部门",
@ -357,6 +358,7 @@ function buildSalesExportData(items: SalesExpansionItem[]) {
normalizeExportText(item.employeeNo), normalizeExportText(item.employeeNo),
normalizeExportText(item.name), normalizeExportText(item.name),
normalizeExportText(item.owner), normalizeExportText(item.owner),
normalizeExportText(item.updatedAt),
normalizeExportText(item.phone), normalizeExportText(item.phone),
normalizeExportText(item.officeName), normalizeExportText(item.officeName),
normalizeExportText(item.dept), normalizeExportText(item.dept),
@ -389,6 +391,7 @@ function buildChannelExportHeaders(items: ChannelExpansionItem[]) {
"编码", "编码",
"渠道名称", "渠道名称",
"创建人", "创建人",
"更新修改时间",
"省份", "省份",
"市", "市",
"办公地址", "办公地址",
@ -426,6 +429,7 @@ function buildChannelExportData(items: ChannelExpansionItem[]) {
normalizeExportText(item.channelCode), normalizeExportText(item.channelCode),
normalizeExportText(item.name), normalizeExportText(item.name),
normalizeExportText(item.owner), normalizeExportText(item.owner),
normalizeExportText(item.updatedAt),
normalizeExportText(item.province), normalizeExportText(item.province),
normalizeExportText(item.city), normalizeExportText(item.city),
normalizeExportText(item.officeAddress), normalizeExportText(item.officeAddress),

View File

@ -83,6 +83,13 @@ function formatAmount(value?: number) {
return new Intl.NumberFormat("zh-CN").format(Number(value)); return new Intl.NumberFormat("zh-CN").format(Number(value));
} }
function formatOpportunityBoolean(value?: boolean, trueLabel = "是", falseLabel = "否") {
if (value === null || value === undefined) {
return "";
}
return value ? trueLabel : falseLabel;
}
function normalizeOpportunityExportText(value?: string | number | boolean | null) { function normalizeOpportunityExportText(value?: string | number | boolean | null) {
if (value === null || value === undefined) { if (value === null || value === undefined) {
return ""; return "";
@ -648,8 +655,8 @@ function OpportunityExportFilterModal({
return ( return (
<ModalShell <ModalShell
title={`导出${archiveTab === "active" ? "未归档" : "已归档"}商机`} title={`导出${archiveTab === "active" ? "未签单" : "已签单"}商机`}
subtitle="选择条件后导出 Excel不填条件则导出当前归档页签下的全部可见商机。" subtitle="选择条件后导出 Excel不填条件则导出当前签单页签下的全部可见商机。"
onClose={onClose} onClose={onClose}
footer={( footer={(
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between"> <div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
@ -1603,7 +1610,7 @@ export default function Opportunities() {
.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived))) .filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)))
.filter((item) => matchesOpportunityExportFilters(item, filters, effectiveConfidenceOptions)); .filter((item) => matchesOpportunityExportFilters(item, filters, effectiveConfidenceOptions));
if (exportItems.length <= 0) { if (exportItems.length <= 0) {
throw new Error(`当前筛选条件下暂无可导出的${archiveTab === "active" ? "未归档" : "已归档"}商机`); throw new Error(`当前筛选条件下暂无可导出的${archiveTab === "active" ? "未签单" : "已签单"}商机`);
} }
const ExcelJS = await import("exceljs"); const ExcelJS = await import("exceljs");
@ -1613,6 +1620,9 @@ export default function Opportunities() {
"项目编号", "项目编号",
"项目名称", "项目名称",
"创建人", "创建人",
"更新修改时间",
"是否签单",
"是否推送OMS",
"项目地", "项目地",
"最终用户", "最终用户",
"建设类型", "建设类型",
@ -1661,6 +1671,9 @@ export default function Opportunities() {
normalizeOpportunityExportText(item.code), normalizeOpportunityExportText(item.code),
normalizeOpportunityExportText(item.name), normalizeOpportunityExportText(item.name),
normalizeOpportunityExportText(item.owner), normalizeOpportunityExportText(item.owner),
normalizeOpportunityExportText(item.updatedAt),
formatOpportunityBoolean(item.archived, "已签单", "未签单"),
formatOpportunityBoolean(item.pushedToOms, "已推送", "未推送"),
normalizeOpportunityExportText(item.projectLocation), normalizeOpportunityExportText(item.projectLocation),
normalizeOpportunityExportText(item.client), normalizeOpportunityExportText(item.client),
normalizeOpportunityExportText(item.type || "新建"), normalizeOpportunityExportText(item.type || "新建"),
@ -1726,7 +1739,7 @@ export default function Opportunities() {
}); });
const buffer = await workbook.xlsx.writeBuffer(); const buffer = await workbook.xlsx.writeBuffer();
const filename = `商机储备_${archiveTab === "active" ? "未归档" : "已归档"}_${formatOpportunityExportFilenameTime()}.xlsx`; const filename = `商机储备_${archiveTab === "active" ? "未签单" : "已签单"}_${formatOpportunityExportFilenameTime()}.xlsx`;
downloadOpportunityExcelFile(filename, buffer); downloadOpportunityExcelFile(filename, buffer);
setExportFilterOpen(false); setExportFilterOpen(false);
} catch (exportErr) { } catch (exportErr) {
@ -1943,7 +1956,7 @@ export default function Opportunities() {
const renderEmpty = () => ( const renderEmpty = () => (
<div className="crm-empty-panel"> <div className="crm-empty-panel">
{archiveTab === "active" ? "暂无未归档商机,先新增一条试试。" : "暂无已归档商机。"} {archiveTab === "active" ? "暂无未签单商机,先新增一条试试。" : "暂无已签单商机。"}
</div> </div>
); );
@ -1987,7 +2000,7 @@ export default function Opportunities() {
: "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white" : "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
}`} }`}
> >
</button> </button>
<button <button
onClick={() => { onClick={() => {
@ -2000,7 +2013,7 @@ export default function Opportunities() {
: "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white" : "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
}`} }`}
> >
</button> </button>
</div> </div>