修改商机字段展示与展示标签

main
kangwenjing 2026-04-09 15:35:10 +08:00
parent 9733de9f22
commit 2709171839
9 changed files with 211 additions and 54 deletions

View File

@ -91,6 +91,7 @@
<workItem from="1775198320186" duration="1451000" />
<workItem from="1775200047785" duration="3902000" />
<workItem from="1775541962012" duration="805000" />
<workItem from="1775611219527" duration="1273000" />
</task>
<task id="LOCAL-00001" summary="修改定位信息 0323">
<option name="closed" value="true" />

View File

@ -8,6 +8,7 @@ public class OpportunityMetaDTO {
private List<OpportunityDictOptionDTO> operatorOptions;
private List<OpportunityDictOptionDTO> projectLocationOptions;
private List<OpportunityDictOptionDTO> opportunityTypeOptions;
private List<OpportunityDictOptionDTO> confidenceOptions;
public OpportunityMetaDTO() {
}
@ -16,11 +17,13 @@ public class OpportunityMetaDTO {
List<OpportunityDictOptionDTO> stageOptions,
List<OpportunityDictOptionDTO> operatorOptions,
List<OpportunityDictOptionDTO> projectLocationOptions,
List<OpportunityDictOptionDTO> opportunityTypeOptions) {
List<OpportunityDictOptionDTO> opportunityTypeOptions,
List<OpportunityDictOptionDTO> confidenceOptions) {
this.stageOptions = stageOptions;
this.operatorOptions = operatorOptions;
this.projectLocationOptions = projectLocationOptions;
this.opportunityTypeOptions = opportunityTypeOptions;
this.confidenceOptions = confidenceOptions;
}
public List<OpportunityDictOptionDTO> getStageOptions() {
@ -54,4 +57,12 @@ public class OpportunityMetaDTO {
public void setOpportunityTypeOptions(List<OpportunityDictOptionDTO> opportunityTypeOptions) {
this.opportunityTypeOptions = opportunityTypeOptions;
}
public List<OpportunityDictOptionDTO> getConfidenceOptions() {
return confidenceOptions;
}
public void setConfidenceOptions(List<OpportunityDictOptionDTO> confidenceOptions) {
this.confidenceOptions = confidenceOptions;
}
}

View File

@ -25,6 +25,10 @@ public interface OpportunityMapper {
@Param("typeCode") String typeCode,
@Param("itemValue") String itemValue);
String selectDictValueByLabel(
@Param("typeCode") String typeCode,
@Param("itemLabel") String itemLabel);
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
List<OpportunityItemDTO> selectOpportunities(
@Param("userId") Long userId,

View File

@ -35,6 +35,7 @@ public class OpportunityServiceImpl implements OpportunityService {
private static final String STAGE_TYPE_CODE = "sj_xmjd";
private static final String OPERATOR_TYPE_CODE = "sj_yzf";
private static final String OPPORTUNITY_TYPE_CODE = "sj_jslx";
private static final String CONFIDENCE_TYPE_CODE = "sj_xmbwd";
private static final Logger log = LoggerFactory.getLogger(OpportunityServiceImpl.class);
private final OpportunityMapper opportunityMapper;
@ -48,11 +49,13 @@ public class OpportunityServiceImpl implements OpportunityService {
@Override
public OpportunityMetaDTO getMeta() {
List<OpportunityDictOptionDTO> opportunityTypeOptions = opportunityMapper.selectDictItems(OPPORTUNITY_TYPE_CODE);
List<OpportunityDictOptionDTO> confidenceOptions = opportunityMapper.selectDictItems(CONFIDENCE_TYPE_CODE);
return new OpportunityMetaDTO(
opportunityMapper.selectDictItems(STAGE_TYPE_CODE),
opportunityMapper.selectDictItems(OPERATOR_TYPE_CODE),
opportunityMapper.selectProvinceAreaOptions(),
opportunityTypeOptions.isEmpty() ? buildDefaultOpportunityTypeOptions() : opportunityTypeOptions);
opportunityTypeOptions.isEmpty() ? buildDefaultOpportunityTypeOptions() : opportunityTypeOptions,
confidenceOptions.isEmpty() ? buildDefaultConfidenceOptions() : confidenceOptions);
}
private List<OpportunityDictOptionDTO> buildDefaultOpportunityTypeOptions() {
@ -62,6 +65,13 @@ public class OpportunityServiceImpl implements OpportunityService {
buildDictOption("替换", "替换"));
}
private List<OpportunityDictOptionDTO> buildDefaultConfidenceOptions() {
return List.of(
buildDictOption("A", "A"),
buildDictOption("B", "B"),
buildDictOption("C", "C"));
}
private OpportunityDictOptionDTO buildDictOption(String label, String value) {
OpportunityDictOptionDTO option = new OpportunityDictOptionDTO();
option.setLabel(label);
@ -488,13 +498,44 @@ public class OpportunityServiceImpl implements OpportunityService {
throw new BusinessException(blankMessage);
}
String upper = normalized.toUpperCase();
String dictValue = opportunityMapper.selectDictValueByLabel(CONFIDENCE_TYPE_CODE, normalized);
if (!isBlank(dictValue)) {
return dictValue;
}
String dictLabel = opportunityMapper.selectDictLabel(CONFIDENCE_TYPE_CODE, normalized);
if (!isBlank(dictLabel)) {
return normalized;
}
String legacyGrade = normalizeLegacyConfidenceGrade(normalized);
if (legacyGrade != null) {
String legacyDictValue = opportunityMapper.selectDictValueByLabel(CONFIDENCE_TYPE_CODE, legacyGrade);
if (!isBlank(legacyDictValue)) {
return legacyDictValue;
}
String legacyDictLabel = opportunityMapper.selectDictLabel(CONFIDENCE_TYPE_CODE, legacyGrade);
if (!isBlank(legacyDictLabel)) {
return legacyGrade;
}
return legacyGrade;
}
throw new BusinessException("项目把握度无效: " + normalized);
}
private String normalizeLegacyConfidenceGrade(String value) {
if (isBlank(value)) {
return null;
}
String upper = value.trim().toUpperCase();
if ("A".equals(upper) || "B".equals(upper) || "C".equals(upper)) {
return upper;
}
if (normalized.matches("\\d+(\\.\\d+)?")) {
double numericValue = Double.parseDouble(normalized);
if (upper.matches("\\d+(\\.\\d+)?")) {
double numericValue = Double.parseDouble(upper);
if (numericValue >= 80) {
return "A";
}
@ -503,8 +544,7 @@ public class OpportunityServiceImpl implements OpportunityService {
}
return "C";
}
throw new BusinessException("项目把握度仅支持A、B、C");
return null;
}
private BigDecimal requirePositiveAmount(BigDecimal value, String message) {

View File

@ -72,7 +72,7 @@ unisbase:
access-token-safety-seconds: 120
oms:
enabled: ${OMS_ENABLED:true}
base-url: ${OMS_BASE_URL:http://121.41.238.146:28080}
base-url: ${OMS_BASE_URL:https://oms.unissense.top}
api-key: ${OMS_API_KEY:c7f858d0-30b8-4b7f-9ea1-0ccf5ceb1c54}
api-key-header: ${OMS_API_KEY_HEADER:apiKey}
user-info-path: ${OMS_USER_INFO_PATH:/api/v1/user/info}

View File

@ -35,6 +35,17 @@
limit 1
</select>
<select id="selectDictValueByLabel" resultType="java.lang.String">
select item_value
from sys_dict_item
where type_code = #{typeCode}
and item_label = #{itemLabel}
and status = 1
and coalesce(is_deleted, 0) = 0
order by sort_order asc nulls last, dict_item_id asc
limit 1
</select>
<select id="selectOpportunities" resultType="com.unis.crm.dto.opportunity.OpportunityItemDTO">
select
o.id,

View File

@ -7,7 +7,7 @@ import { AdaptiveSelect } from "@/components/AdaptiveSelect";
import { useIsWecomBrowser } from "@/hooks/useIsWecomBrowser";
import { cn } from "@/lib/utils";
const CONFIDENCE_OPTIONS = [
const FALLBACK_CONFIDENCE_OPTIONS = [
{ value: "A", label: "A" },
{ value: "B", label: "B" },
{ value: "C", label: "C" },
@ -19,8 +19,6 @@ const FALLBACK_OPPORTUNITY_TYPE_OPTIONS = [
{ value: "替换", label: "替换" },
] as const;
type ConfidenceGrade = (typeof CONFIDENCE_OPTIONS)[number]["value"];
const COMPETITOR_OPTIONS = [
"深信服",
"锐捷",
@ -159,7 +157,11 @@ function matchesOpportunityRelationFilter(hasRelation: boolean, filterValue?: st
return true;
}
function matchesOpportunityExportFilters(item: OpportunityItem, filters: OpportunityExportFilters) {
function matchesOpportunityExportFilters(
item: OpportunityItem,
filters: OpportunityExportFilters,
confidenceOptions: OpportunityDictOption[],
) {
const keywordText = [
item.code,
item.name,
@ -188,7 +190,7 @@ function matchesOpportunityExportFilters(item: OpportunityItem, filters: Opportu
if (!matchesOpportunityTextFilter([item.stageCode, item.stage], filters.stage)) {
return false;
}
if (filters.confidence && normalizeConfidenceGrade(item.confidence) !== filters.confidence) {
if (filters.confidence && normalizeConfidenceValue(item.confidence, confidenceOptions) !== filters.confidence) {
return false;
}
if (!matchesOpportunityTextFilter([item.projectLocation], filters.projectLocation)) {
@ -206,14 +208,14 @@ function matchesOpportunityExportFilters(item: OpportunityItem, filters: Opportu
return matchesOpportunityRelationFilter(Boolean(item.channelExpansionId || item.channelExpansionName), filters.hasChannelExpansion);
}
function toFormFromItem(item: OpportunityItem): CreateOpportunityPayload {
function toFormFromItem(item: OpportunityItem, confidenceOptions: OpportunityDictOption[]): CreateOpportunityPayload {
return {
opportunityName: item.name || "",
customerName: item.client || "",
projectLocation: item.projectLocation || "",
amount: item.amount || 0,
expectedCloseDate: item.date || "",
confidencePct: normalizeConfidenceGrade(item.confidence),
confidencePct: normalizeConfidenceValue(item.confidence, confidenceOptions),
stage: item.stageCode || item.stage || "",
opportunityType: item.type || "",
productType: item.product || "VDI云桌面",
@ -226,9 +228,9 @@ function toFormFromItem(item: OpportunityItem): CreateOpportunityPayload {
};
}
function normalizeConfidenceGrade(value?: string | number | null): ConfidenceGrade {
function normalizeLegacyConfidenceGrade(value?: string | number | null) {
if (value === null || value === undefined) {
return "C";
return "";
}
if (typeof value === "number") {
@ -247,22 +249,62 @@ function normalizeConfidenceGrade(value?: string | number | null): ConfidenceGra
if (normalized === "60") {
return "B";
}
return "C";
return "";
}
function getConfidenceOptionValue(score?: string | number | null) {
return normalizeConfidenceGrade(score);
function getEffectiveConfidenceOptions(confidenceOptions: OpportunityDictOption[]) {
return confidenceOptions.length > 0 ? confidenceOptions : [...FALLBACK_CONFIDENCE_OPTIONS];
}
function getConfidenceLabel(score?: string | number | null) {
const matchedOption = CONFIDENCE_OPTIONS.find((item) => item.value === normalizeConfidenceGrade(score));
return matchedOption?.label || "C";
function normalizeConfidenceValue(score: string | number | null | undefined, confidenceOptions: OpportunityDictOption[]) {
const rawValue = typeof score === "string" ? score.trim() : typeof score === "number" ? String(score) : "";
const options = getEffectiveConfidenceOptions(confidenceOptions);
if (!rawValue) {
return "";
}
function getConfidenceBadgeClass(score?: string | number | null) {
const normalizedGrade = normalizeConfidenceGrade(score);
if (normalizedGrade === "A") return "crm-pill crm-pill-emerald";
if (normalizedGrade === "B") return "crm-pill crm-pill-amber";
const matchedByValue = options.find((item) => (item.value || "").trim() === rawValue);
if (matchedByValue?.value) {
return matchedByValue.value;
}
const matchedByLabel = options.find((item) => (item.label || "").trim() === rawValue);
if (matchedByLabel?.value) {
return matchedByLabel.value;
}
const legacyGrade = normalizeLegacyConfidenceGrade(score);
if (!legacyGrade) {
return rawValue;
}
const legacyValueMatch = options.find((item) => (item.value || "").trim().toUpperCase() === legacyGrade);
if (legacyValueMatch?.value) {
return legacyValueMatch.value;
}
const legacyLabelMatch = options.find((item) => (item.label || "").trim().toUpperCase() === legacyGrade);
if (legacyLabelMatch?.value) {
return legacyLabelMatch.value;
}
return legacyGrade;
}
function getConfidenceLabel(score: string | number | null | undefined, confidenceOptions: OpportunityDictOption[]) {
const normalizedValue = normalizeConfidenceValue(score, confidenceOptions);
const matchedOption = getEffectiveConfidenceOptions(confidenceOptions).find((item) => (item.value || "") === normalizedValue);
return matchedOption?.label || matchedOption?.value || normalizedValue || "无";
}
function getConfidenceBadgeClass(score: string | number | null | undefined, confidenceOptions: OpportunityDictOption[]) {
const options = getEffectiveConfidenceOptions(confidenceOptions);
const normalizedValue = normalizeConfidenceValue(score, confidenceOptions);
const matchedIndex = options.findIndex((item) => (item.value || "") === normalizedValue);
if (matchedIndex < 0) return "crm-pill crm-pill-rose";
if (matchedIndex === 0) return "crm-pill crm-pill-emerald";
if (matchedIndex === 1) return "crm-pill crm-pill-amber";
return "crm-pill crm-pill-rose";
}
@ -553,6 +595,7 @@ function OpportunityExportFilterModal({
exportError,
archiveTab,
stageOptions,
confidenceOptions,
projectLocationOptions,
opportunityTypeOptions,
operatorOptions,
@ -564,6 +607,7 @@ function OpportunityExportFilterModal({
exportError: string;
archiveTab: OpportunityArchiveTab;
stageOptions: OpportunityDictOption[];
confidenceOptions: OpportunityDictOption[];
projectLocationOptions: OpportunityDictOption[];
opportunityTypeOptions: OpportunityDictOption[];
operatorOptions: OpportunityDictOption[];
@ -673,7 +717,10 @@ function OpportunityExportFilterModal({
value={draftFilters.confidence ?? ""}
options={[
{ value: "", label: "全部把握度" },
...CONFIDENCE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
...getEffectiveConfidenceOptions(confidenceOptions).map((option) => ({
value: option.value || "",
label: option.label || option.value || "",
})),
]}
placeholder="全部把握度"
sheetTitle="选择项目把握度"
@ -1315,6 +1362,7 @@ export default function Opportunities() {
const [operatorOptions, setOperatorOptions] = useState<OpportunityDictOption[]>([]);
const [projectLocationOptions, setProjectLocationOptions] = useState<OpportunityDictOption[]>([]);
const [opportunityTypeOptions, setOpportunityTypeOptions] = useState<OpportunityDictOption[]>([]);
const [confidenceOptions, setConfidenceOptions] = useState<OpportunityDictOption[]>([]);
const [form, setForm] = useState<CreateOpportunityPayload>(defaultForm);
const [pushPreSalesId, setPushPreSalesId] = useState<number | undefined>(undefined);
const [pushPreSalesName, setPushPreSalesName] = useState("");
@ -1384,6 +1432,7 @@ export default function Opportunities() {
setOperatorOptions((data.operatorOptions ?? []).filter((item) => item.value));
setProjectLocationOptions((data.projectLocationOptions ?? []).filter((item) => item.value));
setOpportunityTypeOptions((data.opportunityTypeOptions ?? []).filter((item) => item.value));
setConfidenceOptions((data.confidenceOptions ?? []).filter((item) => item.value));
}
} catch {
if (!cancelled) {
@ -1391,6 +1440,7 @@ export default function Opportunities() {
setOperatorOptions([]);
setProjectLocationOptions([]);
setOpportunityTypeOptions([]);
setConfidenceOptions([]);
}
}
}
@ -1419,6 +1469,21 @@ export default function Opportunities() {
setForm((current) => (current.opportunityType ? current : { ...current, opportunityType: defaultOpportunityType }));
}, [opportunityTypeOptions]);
useEffect(() => {
const effectiveConfidenceOptions = getEffectiveConfidenceOptions(confidenceOptions);
if (!effectiveConfidenceOptions.length) {
return;
}
const defaultConfidence = effectiveConfidenceOptions[0]?.value || "";
setForm((current) => ({
...current,
confidencePct: current.confidencePct
? normalizeConfidenceValue(current.confidencePct, effectiveConfidenceOptions)
: defaultConfidence,
}));
}, [confidenceOptions]);
const followUpRecords: OpportunityFollowUp[] = selectedItem?.followUps ?? [];
const visibleItems = items.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)));
const stageFilterOptions = [
@ -1443,6 +1508,11 @@ export default function Opportunities() {
const effectiveOpportunityTypeOptions = opportunityTypeOptions.length > 0
? opportunityTypeOptions
: FALLBACK_OPPORTUNITY_TYPE_OPTIONS;
const effectiveConfidenceOptions = getEffectiveConfidenceOptions(confidenceOptions);
const buildEmptyForm = (): CreateOpportunityPayload => ({
...defaultForm,
confidencePct: effectiveConfidenceOptions[0]?.value || defaultForm.confidencePct,
});
const opportunityTypeSelectOptions = [
{ value: "", label: "请选择" },
...effectiveOpportunityTypeOptions.map((item) => ({
@ -1510,9 +1580,11 @@ export default function Opportunities() {
}, [archiveTab, selectedItem, visibleItems]);
const getConfidenceColor = (score?: string | number | null) => {
const normalizedGrade = normalizeConfidenceGrade(score);
if (normalizedGrade === "A") return "text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20";
if (normalizedGrade === "B") return "text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border-amber-200 dark:border-amber-500/20";
const normalizedValue = normalizeConfidenceValue(score, effectiveConfidenceOptions);
const matchedIndex = effectiveConfidenceOptions.findIndex((item) => (item.value || "") === normalizedValue);
if (matchedIndex < 0) return "text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/10 border-rose-200 dark:border-rose-500/20";
if (matchedIndex === 0) return "text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20";
if (matchedIndex === 1) return "text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border-amber-200 dark:border-amber-500/20";
return "text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/10 border-rose-200 dark:border-rose-500/20";
};
@ -1529,7 +1601,7 @@ export default function Opportunities() {
const overview = await getOpportunityOverview("", "全部");
const exportItems = (overview.items ?? [])
.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)))
.filter((item) => matchesOpportunityExportFilters(item, filters));
.filter((item) => matchesOpportunityExportFilters(item, filters, effectiveConfidenceOptions));
if (exportItems.length <= 0) {
throw new Error(`当前筛选条件下暂无可导出的${archiveTab === "active" ? "未归档" : "已归档"}商机`);
}
@ -1594,7 +1666,7 @@ export default function Opportunities() {
normalizeOpportunityExportText(item.type || "新建"),
normalizeOpportunityExportText(item.operatorName),
normalizeOpportunityExportText(item.stage),
getConfidenceLabel(item.confidence),
getConfidenceLabel(item.confidence, effectiveConfidenceOptions),
item.amount === null || item.amount === undefined ? "" : `¥${formatAmount(item.amount)}`,
normalizeOpportunityExportText(item.date),
normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name),
@ -1678,7 +1750,7 @@ export default function Opportunities() {
const handleOpenCreate = () => {
setError("");
setFieldErrors({});
setForm(defaultForm);
setForm(buildEmptyForm());
setSelectedCompetitors([]);
setCustomCompetitorName("");
setCreateOpen(true);
@ -1690,7 +1762,7 @@ export default function Opportunities() {
setSubmitting(false);
setError("");
setFieldErrors({});
setForm(defaultForm);
setForm(buildEmptyForm());
setSelectedCompetitors([]);
setCustomCompetitorName("");
};
@ -1739,7 +1811,7 @@ export default function Opportunities() {
}
setError("");
setFieldErrors({});
setForm(toFormFromItem(selectedItem));
setForm(toFormFromItem(selectedItem, effectiveConfidenceOptions));
const competitorState = parseCompetitorState(selectedItem.competitorName);
setSelectedCompetitors(competitorState.selections);
setCustomCompetitorName(competitorState.customName);
@ -1990,18 +2062,8 @@ export default function Opportunities() {
<h3 className="truncate text-base font-semibold text-slate-900 dark:text-white sm:text-lg">{opp.name || "未命名商机"}</h3>
<p className="mt-1 truncate text-xs leading-5 text-slate-500 dark:text-slate-400 sm:text-sm">{opp.code || "待生成"}</p>
<div className="mt-2 flex flex-wrap items-center gap-2">
<span
className={cn(
"rounded-full border px-2.5 py-1 text-[11px] font-semibold leading-none",
isOwnedByCurrentUser
? "border-emerald-200 bg-emerald-50 text-emerald-600 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300"
: "border-slate-200 bg-slate-50 text-slate-500 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-300",
)}
>
{isOwnedByCurrentUser ? "我的" : "只读"}
</span>
<span className={getConfidenceBadgeClass(opp.confidence)}>
{getConfidenceLabel(opp.confidence)}
<span className={getConfidenceBadgeClass(opp.confidence, effectiveConfidenceOptions)}>
{getConfidenceLabel(opp.confidence, effectiveConfidenceOptions)}
</span>
<span className="crm-pill crm-pill-neutral">
{opp.stage || "初步沟通"}
@ -2057,6 +2119,7 @@ export default function Opportunities() {
exportError={exportError}
archiveTab={archiveTab}
stageOptions={stageOptions}
confidenceOptions={effectiveConfidenceOptions}
projectLocationOptions={projectLocationOptions}
opportunityTypeOptions={opportunityTypeOptions}
operatorOptions={operatorOptions}
@ -2231,14 +2294,17 @@ export default function Opportunities() {
<label className="space-y-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"><RequiredMark /></span>
<AdaptiveSelect
value={getConfidenceOptionValue(form.confidencePct)}
value={normalizeConfidenceValue(form.confidencePct, effectiveConfidenceOptions)}
placeholder="请选择"
sheetTitle="项目把握度"
options={CONFIDENCE_OPTIONS.map((item) => ({ value: item.value, label: item.label }))}
options={effectiveConfidenceOptions.map((item) => ({
value: item.value || "",
label: item.label || item.value || "",
}))}
className={cn(
fieldErrors.confidencePct ? "border-rose-400 bg-rose-50/60 focus:border-rose-500 focus:ring-rose-500 dark:border-rose-500/70 dark:bg-rose-500/10" : "",
)}
onChange={(value) => handleChange("confidencePct", normalizeConfidenceGrade(value))}
onChange={(value) => handleChange("confidencePct", normalizeConfidenceValue(value, effectiveConfidenceOptions))}
/>
{fieldErrors.confidencePct ? <p className="text-xs text-rose-500">{fieldErrors.confidencePct}</p> : null}
</label>
@ -2461,7 +2527,7 @@ export default function Opportunities() {
<h3 className="line-clamp-1 text-lg font-bold leading-tight text-slate-900 dark:text-white sm:break-anywhere sm:line-clamp-none sm:text-xl">{selectedItem.name || "未命名商机"}</h3>
<div className="mt-3 flex flex-wrap gap-2">
<span className="crm-pill crm-pill-neutral">{selectedItem.stage || "初步沟通"}</span>
<span className={`rounded-full px-2.5 py-1 text-xs font-medium ${getConfidenceColor(selectedItem.confidence)}`}> {getConfidenceLabel(selectedItem.confidence)}</span>
<span className={`rounded-full px-2.5 py-1 text-xs font-medium ${getConfidenceColor(selectedItem.confidence)}`}> {getConfidenceLabel(selectedItem.confidence, effectiveConfidenceOptions)}</span>
</div>
</div>
@ -2478,7 +2544,7 @@ export default function Opportunities() {
<DetailItem label="售前" value={selectedPreSalesName} icon={<User className="h-3 w-3" />} />
<DetailItem label="预计金额(元)" value={<span className="text-rose-600 dark:text-rose-400">¥{formatAmount(selectedItem.amount)}</span>} icon={<DollarSign className="h-3 w-3" />} />
<DetailItem label="预计下单时间" value={selectedItem.date || "待定"} icon={<Calendar className="h-3 w-3" />} />
<DetailItem label="项目把握度" value={getConfidenceLabel(selectedItem.confidence)} />
<DetailItem label="项目把握度" value={getConfidenceLabel(selectedItem.confidence, effectiveConfidenceOptions)} />
<DetailItem label="项目阶段" value={selectedItem.stage || "无"} icon={<Activity className="h-3 w-3" />} />
<DetailItem
label="是否已推送OMS"

View File

@ -0,0 +1,21 @@
-- ensure opportunity confidence dictionary exists
begin;
insert into sys_dict_item (type_code, item_label, item_value, sort_order, status, is_deleted, remark)
select v.type_code, v.item_label, v.item_value, v.sort_order, 1, 0, '商机项目把握度'
from (
values
('sj_xmbwd', 'A', 'A', 1),
('sj_xmbwd', 'B', 'B', 2),
('sj_xmbwd', 'C', 'C', 3)
) as v(type_code, item_label, item_value, sort_order)
where not exists (
select 1
from sys_dict_item s
where s.type_code = v.type_code
and s.item_value = v.item_value
and coalesce(s.is_deleted, 0) = 0
);
commit;

View File

@ -943,7 +943,10 @@ FROM (
VALUES
('sj_jslx', '新建', '新建', 1),
('sj_jslx', '扩容', '扩容', 2),
('sj_jslx', '替换', '替换', 3)
('sj_jslx', '替换', '替换', 3),
('sj_xmbwd', 'A', 'A', 1),
('sj_xmbwd', 'B', 'B', 2),
('sj_xmbwd', 'C', 'C', 3)
) AS v(type_code, item_label, item_value, sort_order)
WHERE NOT EXISTS (
SELECT 1