跟进记录日期展示优化;重复提交Bug修复;提交按钮添加loading限制

main
kangwenjing 2026-08-20 09:35:12 +08:00
parent 2d502751f5
commit 0da982b584
12 changed files with 817 additions and 29 deletions

View File

@ -4,7 +4,13 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="修改定位信息 0323" />
<list default="true" id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="修改定位信息 0323">
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/pages/Expansion.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/pages/Expansion.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/pages/Work.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/pages/Work.tsx" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -96,6 +102,7 @@
<workItem from="1775721940000" duration="8576000" />
<workItem from="1776219416113" duration="650000" />
<workItem from="1776238420843" duration="21000" />
<workItem from="1787113214298" duration="3043000" />
</task>
<task id="LOCAL-00001" summary="修改定位信息 0323">
<option name="closed" value="true" />

View File

@ -117,7 +117,7 @@ public class ExpansionServiceImpl implements ExpansionService {
@Override
public ExpansionOverviewDTO getOverview(Long userId, String keyword, boolean includeDetails, Integer limit) {
String normalizedKeyword = normalizeKeyword(keyword);
Integer normalizedLimit = limit != null && limit > 0 ? Math.min(limit, 1000) : null;
Integer normalizedLimit = limit != null && limit > 0 ? limit : null;
List<Long> extraVisibleOwnerUserIds = resolveExtraVisibleOwnerUserIds(userId);
List<SalesExpansionItemDTO> salesItems = mergeSalesExpansionItems(
expansionMapper.selectSalesExpansions(userId, normalizedKeyword, normalizedLimit),

View File

@ -177,12 +177,24 @@
order by cc.sort_order asc nulls last, cc.id asc
limit 1
) primary_contact on true
left join cnarea province_area
on province_area.level = 1
and (province_area.name = c.province or province_area.area_code = c.province)
left join cnarea city_area
on city_area.level = 2
and (city_area.name = c.city or city_area.area_code = c.city)
left join lateral (
select
name
from cnarea
where level = 1
and (name = c.province or area_code = c.province)
order by name asc, area_code asc
limit 1
) province_area on true
left join lateral (
select
name
from cnarea
where level = 2
and (name = c.city or area_code = c.city)
order by name asc, area_code asc
limit 1
) city_area on true
left join sys_dict_item channel_attribute_dict
on channel_attribute_dict.type_code = 'tz_qdsx'
and channel_attribute_dict.item_value = c.channel_attribute
@ -460,12 +472,24 @@
order by cc.sort_order asc nulls last, cc.id asc
limit 1
) primary_contact on true
left join cnarea province_area
on province_area.level = 1
and (province_area.name = c.province or province_area.area_code = c.province)
left join cnarea city_area
on city_area.level = 2
and (city_area.name = c.city or city_area.area_code = c.city)
left join lateral (
select
name
from cnarea
where level = 1
and (name = c.province or area_code = c.province)
order by name asc, area_code asc
limit 1
) province_area on true
left join lateral (
select
name
from cnarea
where level = 2
and (name = c.city or area_code = c.city)
order by name asc, area_code asc
limit 1
) city_area on true
left join sys_dict_item channel_attribute_dict
on channel_attribute_dict.type_code = 'tz_qdsx'
and channel_attribute_dict.item_value = c.channel_attribute
@ -703,12 +727,24 @@
order by cc.sort_order asc nulls last, cc.id asc
limit 1
) primary_contact on true
left join cnarea province_area
on province_area.level = 1
and (province_area.name = c.province or province_area.area_code = c.province)
left join cnarea city_area
on city_area.level = 2
and (city_area.name = c.city or city_area.area_code = c.city)
left join lateral (
select
name
from cnarea
where level = 1
and (name = c.province or area_code = c.province)
order by name asc, area_code asc
limit 1
) province_area on true
left join lateral (
select
name
from cnarea
where level = 2
and (name = c.city or area_code = c.city)
order by name asc, area_code asc
limit 1
) city_area on true
left join sys_dict_item channel_attribute_dict
on channel_attribute_dict.type_code = 'tz_qdsx'
and channel_attribute_dict.item_value = c.channel_attribute
@ -767,7 +803,14 @@
f.biz_id as bizId,
f.biz_type as bizType,
f.followup_time as followUpTime,
to_char(f.followup_time, 'YYYY-MM-DD HH24:MI') as date,
case
when (f.followup_type = '工作日报' or f.source_type = 'work_report')
and (f.followup_time at time zone 'Asia/Shanghai')::time &lt; time '10:00'
then ((f.followup_time at time zone 'Asia/Shanghai')::date - 1)::text
when (f.followup_type = '工作日报' or f.source_type = 'work_report')
then (f.followup_time at time zone 'Asia/Shanghai')::date::text
else to_char(f.followup_time, 'YYYY-MM-DD HH24:MI')
end as date,
f.followup_type as type,
coalesce(f.content, '无') as content,
coalesce(u.display_name, '无') as user,
@ -831,7 +874,14 @@
f.biz_id as bizId,
f.biz_type as bizType,
f.followup_time as followUpTime,
to_char(f.followup_time, 'YYYY-MM-DD HH24:MI') as date,
case
when (f.followup_type = '工作日报' or f.source_type = 'work_report')
and (f.followup_time at time zone 'Asia/Shanghai')::time &lt; time '10:00'
then ((f.followup_time at time zone 'Asia/Shanghai')::date - 1)::text
when (f.followup_type = '工作日报' or f.source_type = 'work_report')
then (f.followup_time at time zone 'Asia/Shanghai')::date::text
else to_char(f.followup_time, 'YYYY-MM-DD HH24:MI')
end as date,
f.followup_type as type,
coalesce(f.content, '无') as content,
coalesce(u.display_name, '无') as user,

View File

@ -436,7 +436,14 @@
select
f.id,
f.opportunity_id as opportunityId,
to_char(f.followup_time, 'YYYY-MM-DD HH24:MI') as date,
case
when (f.followup_type = '工作日报' or f.source_type = 'work_report')
and (f.followup_time at time zone 'Asia/Shanghai')::time &lt; time '10:00'
then ((f.followup_time at time zone 'Asia/Shanghai')::date - 1)::text
when (f.followup_type = '工作日报' or f.source_type = 'work_report')
then (f.followup_time at time zone 'Asia/Shanghai')::date::text
else to_char(f.followup_time, 'YYYY-MM-DD HH24:MI')
end as date,
coalesce(f.followup_type, '无') as type,
coalesce(f.content, '无') as content,
coalesce(f.next_action, '') as nextAction,

Binary file not shown.

File diff suppressed because one or more lines are too long

1
dist/assets/index-CoNZrZYC.css vendored 100644

File diff suppressed because one or more lines are too long

581
dist/assets/index-D1YmE68l.js vendored 100644

File diff suppressed because one or more lines are too long

46
dist/crm-favicon.svg vendored 100644
View File

@ -0,0 +1,46 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
<title id="title">Ziguang Huizhi CRM Icon Logo</title>
<desc id="desc">Brand-oriented CRM logo icon with an abstract converging symbol, data nodes, and geometric structure in purple tones.</desc>
<defs>
<linearGradient id="bg" x1="72" y1="60" x2="448" y2="460" gradientUnits="userSpaceOnUse">
<stop stop-color="#744CF7"/>
<stop offset="1" stop-color="#2E1685"/>
</linearGradient>
<linearGradient id="mark" x1="170" y1="152" x2="342" y2="360" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFFFFF"/>
<stop offset="1" stop-color="#D4C4FF"/>
</linearGradient>
<linearGradient id="accent" x1="198" y1="184" x2="322" y2="322" gradientUnits="userSpaceOnUse">
<stop stop-color="#A98CFF"/>
<stop offset="1" stop-color="#6C44F2"/>
</linearGradient>
</defs>
<rect x="40" y="40" width="432" height="432" rx="112" fill="url(#bg)"/>
<path d="M130 256C130 185.307 185.307 130 256 130C326.693 130 382 185.307 382 256C382 326.693 326.693 382 256 382C185.307 382 130 326.693 130 256Z" stroke="#FFFFFF" stroke-opacity="0.1" stroke-width="12"/>
<path d="M256 150L344 202V310L256 362L168 310V202L256 150Z" fill="#FFFFFF" fill-opacity="0.08"/>
<path d="M256 174L323 213V299L256 338L189 299V213L256 174Z" stroke="#FFFFFF" stroke-opacity="0.16" stroke-width="2"/>
<path d="M206 193L256 242L306 193" stroke="url(#mark)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M206 319L256 270L306 319" stroke="url(#mark)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M206 193V319" stroke="url(#mark)" stroke-width="18" stroke-linecap="round"/>
<path d="M306 193V319" stroke="url(#mark)" stroke-width="18" stroke-linecap="round"/>
<path d="M228 221L256 249L284 221" stroke="url(#accent)" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M228 291L256 263L284 291" stroke="url(#accent)" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="206" cy="193" r="14" fill="#FFFFFF"/>
<circle cx="306" cy="193" r="14" fill="#FFFFFF"/>
<circle cx="206" cy="319" r="14" fill="#FFFFFF"/>
<circle cx="306" cy="319" r="14" fill="#FFFFFF"/>
<circle cx="256" cy="242" r="14" fill="#FFFFFF"/>
<circle cx="256" cy="270" r="14" fill="#FFFFFF"/>
<circle cx="206" cy="193" r="5" fill="#7A57F8"/>
<circle cx="306" cy="193" r="5" fill="#7A57F8"/>
<circle cx="206" cy="319" r="5" fill="#7A57F8"/>
<circle cx="306" cy="319" r="5" fill="#7A57F8"/>
<circle cx="256" cy="242" r="5" fill="#7A57F8"/>
<circle cx="256" cy="270" r="5" fill="#7A57F8"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

14
dist/index.html vendored 100644
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/crm-favicon.svg" />
<title>紫光汇智CRM</title>
<script type="module" crossorigin src="/assets/index-D1YmE68l.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CoNZrZYC.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -1342,6 +1342,8 @@ export default function Expansion() {
const [visibleItemCount, setVisibleItemCount] = useState(LIST_PAGE_SIZE);
const loadMoreRef = useRef<HTMLDivElement | null>(null);
const loadingMoreRef = useRef(false);
const [loadingMore, setLoadingMore] = useState(false);
const [loadMoreError, setLoadMoreError] = useState("");
const [officeOptions, setOfficeOptions] = useState<ExpansionDictOption[]>([]);
const [industryOptions, setIndustryOptions] = useState<ExpansionDictOption[]>([]);
const [provinceOptions, setProvinceOptions] = useState<ExpansionDictOption[]>([]);
@ -1590,14 +1592,19 @@ export default function Expansion() {
return;
}
loadingMoreRef.current = true;
setLoadingMore(true);
setLoadMoreError("");
const nextVisibleCount = visibleItemCount + LIST_PAGE_SIZE;
try {
const data = await getExpansionOverview(keyword, false, nextVisibleCount + 1);
setSalesData(dedupeExpansionItemsById(data.salesItems ?? []));
setChannelData(dedupeExpansionItemsById(data.channelItems ?? []));
setVisibleItemCount(nextVisibleCount);
} catch {
setLoadMoreError("加载更多失败,请重试");
} finally {
loadingMoreRef.current = false;
setLoadingMore(false);
}
};
@ -2775,6 +2782,11 @@ export default function Expansion() {
);
})
) : renderEmpty()}
{loadMoreError ? (
<div className="rounded-lg border border-rose-200 bg-rose-50 py-3 text-center text-sm font-medium text-rose-600 dark:border-rose-500/20 dark:bg-rose-500/10 dark:text-rose-300">
{loadMoreError}
</div>
) : null}
{hasMoreItems ? (
<div
ref={loadMoreRef}
@ -2789,7 +2801,7 @@ export default function Expansion() {
}}
className="cursor-pointer rounded-lg border border-slate-200 bg-white py-3 text-center text-sm font-medium text-violet-600 shadow-sm dark:border-slate-700 dark:bg-slate-900 dark:text-violet-300"
>
{loadingMore ? "加载中..." : "展示更多"}
</div>
) : null}
</div>

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, ty
import { format } from "date-fns";
import { zhCN } from "date-fns/locale";
import { motion } from "motion/react";
import { ArrowUp, Camera, CheckCircle2, ChevronDown, Download, FileText, Hash, ListTodo, MapPin, Mic, NotebookPen, Paperclip, Plus, RefreshCw, RotateCcw, Search, Send, Square, Trash2, UserRoundPlus, X } from "lucide-react";
import { ArrowUp, Camera, CheckCircle2, ChevronDown, Download, FileText, Hash, ListTodo, Loader2, MapPin, Mic, NotebookPen, Paperclip, Plus, RefreshCw, RotateCcw, Search, Send, Square, Trash2, UserRoundPlus, X } from "lucide-react";
import { flushSync } from "react-dom";
import { Link, Navigate, useLocation, useNavigate } from "react-router-dom";
import {
@ -835,10 +835,12 @@ export default function Work() {
const historyLoadingRequestRef = useRef<string | null>(null);
const historyLoadedPagesRef = useRef(new Map<WorkSection, Set<number>>());
const openedRouteReportIdRef = useRef<number | null>(null);
const reportSubmitInFlightRef = useRef(false);
const [loading, setLoading] = useState(true);
const [refreshingLocation, setRefreshingLocation] = useState(false);
const [submittingCheckIn, setSubmittingCheckIn] = useState(false);
const [submittingReport, setSubmittingReport] = useState(false);
const [reportSubmitLocked, setReportSubmitLocked] = useState(false);
const [uploadingPhoto, setUploadingPhoto] = useState(false);
const [uploadingReportAttachmentIndex, setUploadingReportAttachmentIndex] = useState<number | null>(null);
const [uploadingReportAttachmentProgress, setUploadingReportAttachmentProgress] = useState<Record<number, number>>({});
@ -2153,7 +2155,7 @@ export default function Work() {
};
const handleReportSubmit = async () => {
if (submittingReport) {
if (submittingReport || reportSubmitLocked || reportSubmitInFlightRef.current) {
return;
}
@ -2182,6 +2184,8 @@ export default function Work() {
}
validateReportLineItems(normalizedLineItems, reportOpportunityStageOptions, dailyReportObjectSelectionRequired);
reportSubmitInFlightRef.current = true;
setReportSubmitLocked(true);
await saveWorkDailyReport({
workContent: buildReportSummary(normalizedLineItems),
lineItems: normalizedLineItems,
@ -2189,6 +2193,8 @@ export default function Work() {
tomorrowPlan: buildPlanSummary(normalizedPlanItems),
sourceType: reportForm.sourceType || "manual",
});
reportSubmitInFlightRef.current = false;
setReportSubmitLocked(false);
await Promise.all([
loadOverview(),
loadHistory(historySection, 1, true),
@ -2319,6 +2325,7 @@ export default function Work() {
reportSuccess={reportSuccess}
pageError={pageError}
submittingReport={submittingReport}
reportSubmitLocked={reportSubmitLocked}
onSubmit={() => void handleReportSubmit()}
disableMobileMotion={disableMobileMotion}
/>
@ -4202,6 +4209,7 @@ function ReportPanel({
reportSuccess,
pageError,
submittingReport,
reportSubmitLocked,
onSubmit,
disableMobileMotion,
}: {
@ -4235,6 +4243,7 @@ function ReportPanel({
reportSuccess: string;
pageError: string;
submittingReport: boolean;
reportSubmitLocked: boolean;
onSubmit: () => void;
disableMobileMotion: boolean;
}) {
@ -5034,14 +5043,18 @@ function ReportPanel({
<button
type="button"
onClick={onSubmit}
disabled={submittingReport || loading || !dailyReportFeatureLoaded}
disabled={submittingReport || reportSubmitLocked || loading || !dailyReportFeatureLoaded}
className={cn(
"crm-btn crm-btn-primary mt-6 flex w-full items-center justify-center gap-2 disabled:cursor-not-allowed disabled:opacity-60",
disableMobileMotion ? "active:scale-100" : "active:scale-[0.98]",
)}
>
<Send className="crm-icon-md" />
{submittingReport ? "提交中..." : "提交日报"}
{submittingReport || reportSubmitLocked ? (
<Loader2 className="crm-icon-md animate-spin" />
) : (
<Send className="crm-icon-md" />
)}
{submittingReport ? "提交中..." : reportSubmitLocked ? "提交未确认,请刷新后重试" : "提交日报"}
</button>
</motion.div>
);