0402
parent
dee5da7655
commit
efd3370519
|
|
@ -7,6 +7,7 @@ public class OpportunityMetaDTO {
|
||||||
private List<OpportunityDictOptionDTO> stageOptions;
|
private List<OpportunityDictOptionDTO> stageOptions;
|
||||||
private List<OpportunityDictOptionDTO> operatorOptions;
|
private List<OpportunityDictOptionDTO> operatorOptions;
|
||||||
private List<OpportunityDictOptionDTO> projectLocationOptions;
|
private List<OpportunityDictOptionDTO> projectLocationOptions;
|
||||||
|
private List<OpportunityDictOptionDTO> opportunityTypeOptions;
|
||||||
|
|
||||||
public OpportunityMetaDTO() {
|
public OpportunityMetaDTO() {
|
||||||
}
|
}
|
||||||
|
|
@ -14,10 +15,12 @@ public class OpportunityMetaDTO {
|
||||||
public OpportunityMetaDTO(
|
public OpportunityMetaDTO(
|
||||||
List<OpportunityDictOptionDTO> stageOptions,
|
List<OpportunityDictOptionDTO> stageOptions,
|
||||||
List<OpportunityDictOptionDTO> operatorOptions,
|
List<OpportunityDictOptionDTO> operatorOptions,
|
||||||
List<OpportunityDictOptionDTO> projectLocationOptions) {
|
List<OpportunityDictOptionDTO> projectLocationOptions,
|
||||||
|
List<OpportunityDictOptionDTO> opportunityTypeOptions) {
|
||||||
this.stageOptions = stageOptions;
|
this.stageOptions = stageOptions;
|
||||||
this.operatorOptions = operatorOptions;
|
this.operatorOptions = operatorOptions;
|
||||||
this.projectLocationOptions = projectLocationOptions;
|
this.projectLocationOptions = projectLocationOptions;
|
||||||
|
this.opportunityTypeOptions = opportunityTypeOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<OpportunityDictOptionDTO> getStageOptions() {
|
public List<OpportunityDictOptionDTO> getStageOptions() {
|
||||||
|
|
@ -43,4 +46,12 @@ public class OpportunityMetaDTO {
|
||||||
public void setProjectLocationOptions(List<OpportunityDictOptionDTO> projectLocationOptions) {
|
public void setProjectLocationOptions(List<OpportunityDictOptionDTO> projectLocationOptions) {
|
||||||
this.projectLocationOptions = projectLocationOptions;
|
this.projectLocationOptions = projectLocationOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<OpportunityDictOptionDTO> getOpportunityTypeOptions() {
|
||||||
|
return opportunityTypeOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOpportunityTypeOptions(List<OpportunityDictOptionDTO> opportunityTypeOptions) {
|
||||||
|
this.opportunityTypeOptions = opportunityTypeOptions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.unis.crm.dto.opportunity.CurrentUserAccountDTO;
|
||||||
import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
|
import com.unis.crm.dto.opportunity.CreateOpportunityFollowUpRequest;
|
||||||
import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
|
import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
|
||||||
import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO;
|
import com.unis.crm.dto.opportunity.OmsPreSalesOptionDTO;
|
||||||
|
import com.unis.crm.dto.opportunity.OpportunityDictOptionDTO;
|
||||||
import com.unis.crm.dto.opportunity.OpportunityMetaDTO;
|
import com.unis.crm.dto.opportunity.OpportunityMetaDTO;
|
||||||
import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO;
|
import com.unis.crm.dto.opportunity.OpportunityFollowUpDTO;
|
||||||
import com.unis.crm.dto.opportunity.OpportunityIntegrationTargetDTO;
|
import com.unis.crm.dto.opportunity.OpportunityIntegrationTargetDTO;
|
||||||
|
|
@ -33,6 +34,7 @@ public class OpportunityServiceImpl implements OpportunityService {
|
||||||
|
|
||||||
private static final String STAGE_TYPE_CODE = "sj_xmjd";
|
private static final String STAGE_TYPE_CODE = "sj_xmjd";
|
||||||
private static final String OPERATOR_TYPE_CODE = "sj_yzf";
|
private static final String OPERATOR_TYPE_CODE = "sj_yzf";
|
||||||
|
private static final String OPPORTUNITY_TYPE_CODE = "sj_jslx";
|
||||||
private static final Logger log = LoggerFactory.getLogger(OpportunityServiceImpl.class);
|
private static final Logger log = LoggerFactory.getLogger(OpportunityServiceImpl.class);
|
||||||
|
|
||||||
private final OpportunityMapper opportunityMapper;
|
private final OpportunityMapper opportunityMapper;
|
||||||
|
|
@ -45,10 +47,26 @@ public class OpportunityServiceImpl implements OpportunityService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OpportunityMetaDTO getMeta() {
|
public OpportunityMetaDTO getMeta() {
|
||||||
|
List<OpportunityDictOptionDTO> opportunityTypeOptions = opportunityMapper.selectDictItems(OPPORTUNITY_TYPE_CODE);
|
||||||
return new OpportunityMetaDTO(
|
return new OpportunityMetaDTO(
|
||||||
opportunityMapper.selectDictItems(STAGE_TYPE_CODE),
|
opportunityMapper.selectDictItems(STAGE_TYPE_CODE),
|
||||||
opportunityMapper.selectDictItems(OPERATOR_TYPE_CODE),
|
opportunityMapper.selectDictItems(OPERATOR_TYPE_CODE),
|
||||||
opportunityMapper.selectProvinceAreaOptions());
|
opportunityMapper.selectProvinceAreaOptions(),
|
||||||
|
opportunityTypeOptions.isEmpty() ? buildDefaultOpportunityTypeOptions() : opportunityTypeOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<OpportunityDictOptionDTO> buildDefaultOpportunityTypeOptions() {
|
||||||
|
return List.of(
|
||||||
|
buildDictOption("新建", "新建"),
|
||||||
|
buildDictOption("扩容", "扩容"),
|
||||||
|
buildDictOption("替换", "替换"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OpportunityDictOptionDTO buildDictOption(String label, String value) {
|
||||||
|
OpportunityDictOptionDTO option = new OpportunityDictOptionDTO();
|
||||||
|
option.setLabel(label);
|
||||||
|
option.setValue(value);
|
||||||
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ unisbase:
|
||||||
access-token-safety-seconds: 120
|
access-token-safety-seconds: 120
|
||||||
oms:
|
oms:
|
||||||
enabled: ${OMS_ENABLED:true}
|
enabled: ${OMS_ENABLED:true}
|
||||||
base-url: ${OMS_BASE_URL:http://10.100.52.135:28080}
|
base-url: ${OMS_BASE_URL:http://121.41.238.146:28080}
|
||||||
api-key: ${OMS_API_KEY:c7f858d0-30b8-4b7f-9ea1-0ccf5ceb1c54}
|
api-key: ${OMS_API_KEY:c7f858d0-30b8-4b7f-9ea1-0ccf5ceb1c54}
|
||||||
api-key-header: ${OMS_API_KEY_HEADER:apiKey}
|
api-key-header: ${OMS_API_KEY_HEADER:apiKey}
|
||||||
user-info-path: ${OMS_USER_INFO_PATH:/api/v1/user/info}
|
user-info-path: ${OMS_USER_INFO_PATH:/api/v1/user/info}
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,7 @@ export interface OpportunityMeta {
|
||||||
stageOptions?: OpportunityDictOption[];
|
stageOptions?: OpportunityDictOption[];
|
||||||
operatorOptions?: OpportunityDictOption[];
|
operatorOptions?: OpportunityDictOption[];
|
||||||
projectLocationOptions?: OpportunityDictOption[];
|
projectLocationOptions?: OpportunityDictOption[];
|
||||||
|
opportunityTypeOptions?: OpportunityDictOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OmsPreSalesOption {
|
export interface OmsPreSalesOption {
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ export default function Dashboard() {
|
||||||
<div className="crm-page-heading">
|
<div className="crm-page-heading">
|
||||||
<h1 className="crm-page-title">工作台</h1>
|
<h1 className="crm-page-title">工作台</h1>
|
||||||
<p className="crm-page-subtitle">
|
<p className="crm-page-subtitle">
|
||||||
欢迎回来,{home.realName || "无"}。今天是你入职的第 {home.onboardingDays ?? 0} 天。
|
欢迎回来,{home.realName || "无"}。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,20 @@ function normalizeOptionalText(value?: string) {
|
||||||
return trimmed ? trimmed : undefined;
|
return trimmed ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dedupeExpansionItemsById<T extends { id?: number | string | null }>(items: T[]) {
|
||||||
|
const seenIds = new Set<number | string>();
|
||||||
|
return items.filter((item) => {
|
||||||
|
if (item.id === null || item.id === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (seenIds.has(item.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seenIds.add(item.id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getFieldInputClass(hasError: boolean) {
|
function getFieldInputClass(hasError: boolean) {
|
||||||
return cn(
|
return cn(
|
||||||
"crm-input-box crm-input-text w-full border bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:bg-slate-900/50",
|
"crm-input-box crm-input-text w-full border bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:bg-slate-900/50",
|
||||||
|
|
@ -477,8 +491,8 @@ export default function Expansion() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSalesData(data.salesItems ?? []);
|
setSalesData(dedupeExpansionItemsById(data.salesItems ?? []));
|
||||||
setChannelData(data.channelItems ?? []);
|
setChannelData(dedupeExpansionItemsById(data.channelItems ?? []));
|
||||||
setSelectedItem(null);
|
setSelectedItem(null);
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
|
|
@ -848,6 +862,8 @@ export default function Expansion() {
|
||||||
value={form.officeName || ""}
|
value={form.officeName || ""}
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
sheetTitle="代表处 / 办事处"
|
sheetTitle="代表处 / 办事处"
|
||||||
|
searchable
|
||||||
|
searchPlaceholder="搜索代表处 / 办事处"
|
||||||
options={[
|
options={[
|
||||||
{ value: "", label: "请选择" },
|
{ value: "", label: "请选择" },
|
||||||
...officeOptions.map((option) => ({
|
...officeOptions.map((option) => ({
|
||||||
|
|
@ -877,7 +893,7 @@ export default function Expansion() {
|
||||||
<input
|
<input
|
||||||
value={form.targetDept || ""}
|
value={form.targetDept || ""}
|
||||||
onChange={(e) => onChange("targetDept", e.target.value)}
|
onChange={(e) => onChange("targetDept", e.target.value)}
|
||||||
placeholder="办事处/行业系统部/地市"
|
placeholder="办事处/地市"
|
||||||
className={getFieldInputClass(Boolean(fieldErrors?.targetDept))}
|
className={getFieldInputClass(Boolean(fieldErrors?.targetDept))}
|
||||||
/>
|
/>
|
||||||
{fieldErrors?.targetDept ? <p className="text-xs text-rose-500">{fieldErrors.targetDept}</p> : null}
|
{fieldErrors?.targetDept ? <p className="text-xs text-rose-500">{fieldErrors.targetDept}</p> : null}
|
||||||
|
|
|
||||||
|
|
@ -249,116 +249,112 @@ export default function LoginPage() {
|
||||||
<div className="login-page-shell" style={backgroundStyle}>
|
<div className="login-page-shell" style={backgroundStyle}>
|
||||||
<div className="login-page-backdrop" />
|
<div className="login-page-backdrop" />
|
||||||
<div className="login-page-grid">
|
<div className="login-page-grid">
|
||||||
<section className="login-page-brand">
|
|
||||||
<div className="login-brand-lockup">
|
|
||||||
<div className="login-brand-mark">
|
|
||||||
{platformConfig?.logoUrl ? <img src={platformConfig.logoUrl} alt={appName} /> : <span>紫</span>}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="login-brand-kicker">智慧销售协同平台</p>
|
|
||||||
<h1>{appName}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="login-hero-copy">
|
|
||||||
<p className="login-hero-tag">Sales Workspace</p>
|
|
||||||
<h2>
|
|
||||||
统一客户管理
|
|
||||||
<br />
|
|
||||||
高效推进业务增长
|
|
||||||
</h2>
|
|
||||||
<p>{systemDescription}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="login-brand-meta">
|
|
||||||
<span>首页</span>
|
|
||||||
<span>拓展</span>
|
|
||||||
<span>商机</span>
|
|
||||||
<span>工作</span>
|
|
||||||
<span>我的</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="login-panel">
|
<section className="login-panel">
|
||||||
<div className="login-panel-card">
|
<div className="login-panel-card">
|
||||||
<div className="login-panel-header">
|
<div className="login-panel-layout">
|
||||||
<p className="login-panel-eyebrow">欢迎回来</p>
|
<div className="login-panel-intro">
|
||||||
<h3>紫光汇智CRM系统</h3>
|
<div className="login-brand-lockup">
|
||||||
</div>
|
<div className="login-brand-mark">
|
||||||
|
{platformConfig?.logoUrl ? <img src={platformConfig.logoUrl} alt={appName} /> : <span>紫</span>}
|
||||||
<form className="login-form" onSubmit={handleSubmit}>
|
</div>
|
||||||
<label className="login-field">
|
<div>
|
||||||
<span>用户名</span>
|
<p className="login-brand-kicker">智慧销售协同平台</p>
|
||||||
<input
|
<h1>{appName}</h1>
|
||||||
autoComplete="username"
|
|
||||||
value={form.username}
|
|
||||||
onChange={(event) => handleChange("username", event.target.value)}
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="login-field">
|
|
||||||
<span>密码</span>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
value={form.password}
|
|
||||||
onChange={(event) => handleChange("password", event.target.value)}
|
|
||||||
placeholder="请输入密码"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="login-field">
|
|
||||||
<span>租户编码</span>
|
|
||||||
<input
|
|
||||||
value={form.tenantCode}
|
|
||||||
onChange={(event) => handleChange("tenantCode", event.target.value)}
|
|
||||||
placeholder="可选,不填则按后端默认租户登录"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{captchaEnabled ? (
|
|
||||||
<div className="login-field">
|
|
||||||
<span>验证码</span>
|
|
||||||
<div className="login-captcha-row">
|
|
||||||
<input
|
|
||||||
value={form.captchaCode}
|
|
||||||
onChange={(event) => handleChange("captchaCode", event.target.value)}
|
|
||||||
placeholder="请输入验证码"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="login-captcha-button"
|
|
||||||
type="button"
|
|
||||||
onClick={() => void loadCaptcha()}
|
|
||||||
disabled={initializing}
|
|
||||||
>
|
|
||||||
{captcha?.imageBase64 ? (
|
|
||||||
<img src={captcha.imageBase64} alt="验证码" />
|
|
||||||
) : (
|
|
||||||
<span>{initializing ? "加载中..." : "刷新验证码"}</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<div className="login-intro-copy">
|
||||||
|
<p className="login-panel-eyebrow">欢迎回来</p>
|
||||||
<div className="login-form-meta">
|
<h2>账号登录</h2>
|
||||||
<label className="login-checkbox">
|
<p>{systemDescription}</p>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
<div className="login-intro-points">
|
||||||
checked={form.remember}
|
<span>客户拓展</span>
|
||||||
onChange={(event) => handleChange("remember", event.target.checked)}
|
<span>商机推进</span>
|
||||||
/>
|
<span>销售协同</span>
|
||||||
<span>记住密码(5天)</span>
|
</div>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? <div className="login-error">{error}</div> : null}
|
<div className="login-panel-formWrap">
|
||||||
|
<div className="login-panel-header">
|
||||||
|
<p className="login-panel-eyebrow">欢迎回来</p>
|
||||||
|
<h3>{appName}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button className="login-submit" type="submit" disabled={loading || initializing}>
|
<form className="login-form" onSubmit={handleSubmit}>
|
||||||
{loading ? "登录中..." : "立即登录"}
|
<label className="login-field">
|
||||||
</button>
|
<span>用户名</span>
|
||||||
</form>
|
<input
|
||||||
|
autoComplete="username"
|
||||||
|
value={form.username}
|
||||||
|
onChange={(event) => handleChange("username", event.target.value)}
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="login-field">
|
||||||
|
<span>密码</span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
value={form.password}
|
||||||
|
onChange={(event) => handleChange("password", event.target.value)}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="login-field">
|
||||||
|
<span>租户编码</span>
|
||||||
|
<input
|
||||||
|
value={form.tenantCode}
|
||||||
|
onChange={(event) => handleChange("tenantCode", event.target.value)}
|
||||||
|
placeholder="可选,不填则按后端默认租户登录"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{captchaEnabled ? (
|
||||||
|
<div className="login-field">
|
||||||
|
<span>验证码</span>
|
||||||
|
<div className="login-captcha-row">
|
||||||
|
<input
|
||||||
|
value={form.captchaCode}
|
||||||
|
onChange={(event) => handleChange("captchaCode", event.target.value)}
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="login-captcha-button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void loadCaptcha()}
|
||||||
|
disabled={initializing}
|
||||||
|
>
|
||||||
|
{captcha?.imageBase64 ? (
|
||||||
|
<img src={captcha.imageBase64} alt="验证码" />
|
||||||
|
) : (
|
||||||
|
<span>{initializing ? "加载中..." : "刷新验证码"}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="login-form-meta">
|
||||||
|
<label className="login-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.remember}
|
||||||
|
onChange={(event) => handleChange("remember", event.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>记住密码</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? <div className="login-error">{error}</div> : null}
|
||||||
|
|
||||||
|
<button className="login-submit" type="submit" disabled={loading || initializing}>
|
||||||
|
{loading ? "登录中..." : "立即登录"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ const CONFIDENCE_OPTIONS = [
|
||||||
{ value: "C", label: "C" },
|
{ value: "C", label: "C" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const FALLBACK_OPPORTUNITY_TYPE_OPTIONS = [
|
||||||
|
{ value: "新建", label: "新建" },
|
||||||
|
{ value: "扩容", label: "扩容" },
|
||||||
|
{ value: "替换", label: "替换" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
type ConfidenceGrade = (typeof CONFIDENCE_OPTIONS)[number]["value"];
|
type ConfidenceGrade = (typeof CONFIDENCE_OPTIONS)[number]["value"];
|
||||||
|
|
||||||
const COMPETITOR_OPTIONS = [
|
const COMPETITOR_OPTIONS = [
|
||||||
|
|
@ -52,7 +58,7 @@ const defaultForm: CreateOpportunityPayload = {
|
||||||
expectedCloseDate: "",
|
expectedCloseDate: "",
|
||||||
confidencePct: "C",
|
confidencePct: "C",
|
||||||
stage: "",
|
stage: "",
|
||||||
opportunityType: "新建",
|
opportunityType: "",
|
||||||
productType: "VDI云桌面",
|
productType: "VDI云桌面",
|
||||||
source: "主动开发",
|
source: "主动开发",
|
||||||
salesExpansionId: undefined,
|
salesExpansionId: undefined,
|
||||||
|
|
@ -77,7 +83,7 @@ function toFormFromItem(item: OpportunityItem): CreateOpportunityPayload {
|
||||||
expectedCloseDate: item.date || "",
|
expectedCloseDate: item.date || "",
|
||||||
confidencePct: normalizeConfidenceGrade(item.confidence),
|
confidencePct: normalizeConfidenceGrade(item.confidence),
|
||||||
stage: item.stageCode || item.stage || "",
|
stage: item.stageCode || item.stage || "",
|
||||||
opportunityType: item.type || "新建",
|
opportunityType: item.type || "",
|
||||||
productType: item.product || "VDI云桌面",
|
productType: item.product || "VDI云桌面",
|
||||||
source: item.source || "主动开发",
|
source: item.source || "主动开发",
|
||||||
salesExpansionId: item.salesExpansionId,
|
salesExpansionId: item.salesExpansionId,
|
||||||
|
|
@ -859,6 +865,7 @@ export default function Opportunities() {
|
||||||
const [stageOptions, setStageOptions] = useState<OpportunityDictOption[]>([]);
|
const [stageOptions, setStageOptions] = useState<OpportunityDictOption[]>([]);
|
||||||
const [operatorOptions, setOperatorOptions] = useState<OpportunityDictOption[]>([]);
|
const [operatorOptions, setOperatorOptions] = useState<OpportunityDictOption[]>([]);
|
||||||
const [projectLocationOptions, setProjectLocationOptions] = useState<OpportunityDictOption[]>([]);
|
const [projectLocationOptions, setProjectLocationOptions] = useState<OpportunityDictOption[]>([]);
|
||||||
|
const [opportunityTypeOptions, setOpportunityTypeOptions] = useState<OpportunityDictOption[]>([]);
|
||||||
const [form, setForm] = useState<CreateOpportunityPayload>(defaultForm);
|
const [form, setForm] = useState<CreateOpportunityPayload>(defaultForm);
|
||||||
const [pushPreSalesId, setPushPreSalesId] = useState<number | undefined>(undefined);
|
const [pushPreSalesId, setPushPreSalesId] = useState<number | undefined>(undefined);
|
||||||
const [pushPreSalesName, setPushPreSalesName] = useState("");
|
const [pushPreSalesName, setPushPreSalesName] = useState("");
|
||||||
|
|
@ -927,12 +934,14 @@ export default function Opportunities() {
|
||||||
setStageOptions((data.stageOptions ?? []).filter((item) => item.value));
|
setStageOptions((data.stageOptions ?? []).filter((item) => item.value));
|
||||||
setOperatorOptions((data.operatorOptions ?? []).filter((item) => item.value));
|
setOperatorOptions((data.operatorOptions ?? []).filter((item) => item.value));
|
||||||
setProjectLocationOptions((data.projectLocationOptions ?? []).filter((item) => item.value));
|
setProjectLocationOptions((data.projectLocationOptions ?? []).filter((item) => item.value));
|
||||||
|
setOpportunityTypeOptions((data.opportunityTypeOptions ?? []).filter((item) => item.value));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setStageOptions([]);
|
setStageOptions([]);
|
||||||
setOperatorOptions([]);
|
setOperatorOptions([]);
|
||||||
setProjectLocationOptions([]);
|
setProjectLocationOptions([]);
|
||||||
|
setOpportunityTypeOptions([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -952,6 +961,15 @@ export default function Opportunities() {
|
||||||
setForm((current) => (current.stage ? current : { ...current, stage: defaultStage }));
|
setForm((current) => (current.stage ? current : { ...current, stage: defaultStage }));
|
||||||
}, [stageOptions]);
|
}, [stageOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!opportunityTypeOptions.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOpportunityType = opportunityTypeOptions[0]?.value || "";
|
||||||
|
setForm((current) => (current.opportunityType ? current : { ...current, opportunityType: defaultOpportunityType }));
|
||||||
|
}, [opportunityTypeOptions]);
|
||||||
|
|
||||||
const followUpRecords: OpportunityFollowUp[] = selectedItem?.followUps ?? [];
|
const followUpRecords: OpportunityFollowUp[] = selectedItem?.followUps ?? [];
|
||||||
const visibleItems = items.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)));
|
const visibleItems = items.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)));
|
||||||
const stageFilterOptions = [
|
const stageFilterOptions = [
|
||||||
|
|
@ -972,6 +990,23 @@ export default function Opportunities() {
|
||||||
: []
|
: []
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
const normalizedOpportunityType = form.opportunityType?.trim() || "";
|
||||||
|
const effectiveOpportunityTypeOptions = opportunityTypeOptions.length > 0
|
||||||
|
? opportunityTypeOptions
|
||||||
|
: FALLBACK_OPPORTUNITY_TYPE_OPTIONS;
|
||||||
|
const opportunityTypeSelectOptions = [
|
||||||
|
{ value: "", label: "请选择" },
|
||||||
|
...effectiveOpportunityTypeOptions.map((item) => ({
|
||||||
|
label: item.label || item.value || "",
|
||||||
|
value: item.value || "",
|
||||||
|
})),
|
||||||
|
...(
|
||||||
|
normalizedOpportunityType
|
||||||
|
&& !effectiveOpportunityTypeOptions.some((item) => (item.value || "").trim() === normalizedOpportunityType)
|
||||||
|
? [{ value: normalizedOpportunityType, label: normalizedOpportunityType }]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
];
|
||||||
const activeStageFilterLabel = stageFilterOptions.find((item) => item.value === filter)?.label || "全部";
|
const activeStageFilterLabel = stageFilterOptions.find((item) => item.value === filter)?.label || "全部";
|
||||||
const selectedSalesExpansion = selectedItem?.salesExpansionId
|
const selectedSalesExpansion = selectedItem?.salesExpansionId
|
||||||
? salesExpansionOptions.find((item) => item.id === selectedItem.salesExpansionId) ?? null
|
? salesExpansionOptions.find((item) => item.id === selectedItem.salesExpansionId) ?? null
|
||||||
|
|
@ -1578,12 +1613,9 @@ export default function Opportunities() {
|
||||||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">建设类型<RequiredMark /></span>
|
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">建设类型<RequiredMark /></span>
|
||||||
<AdaptiveSelect
|
<AdaptiveSelect
|
||||||
value={form.opportunityType}
|
value={form.opportunityType}
|
||||||
|
placeholder="请选择"
|
||||||
sheetTitle="建设类型"
|
sheetTitle="建设类型"
|
||||||
options={[
|
options={opportunityTypeSelectOptions}
|
||||||
{ value: "新建", label: "新建" },
|
|
||||||
{ value: "扩容", label: "扩容" },
|
|
||||||
{ value: "替换", label: "替换" },
|
|
||||||
]}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
fieldErrors.opportunityType ? "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" : "",
|
fieldErrors.opportunityType ? "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" : "",
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -28,26 +28,13 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
grid-template-columns: minmax(0, 1.08fr) minmax(360px, 460px);
|
grid-template-columns: minmax(0, 980px);
|
||||||
gap: 48px;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding:
|
padding:
|
||||||
max(24px, env(safe-area-inset-top))
|
max(28px, env(safe-area-inset-top))
|
||||||
clamp(24px, 4vw, 64px)
|
clamp(24px, 4vw, 56px)
|
||||||
max(24px, env(safe-area-inset-bottom));
|
max(28px, env(safe-area-inset-bottom));
|
||||||
}
|
|
||||||
|
|
||||||
.login-page-brand,
|
|
||||||
.login-panel {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-page-brand {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-brand-lockup {
|
.login-brand-lockup {
|
||||||
|
|
@ -93,80 +80,94 @@
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero-copy {
|
.login-panel {
|
||||||
max-width: 680px;
|
justify-content: center;
|
||||||
}
|
display: flex;
|
||||||
|
|
||||||
.login-hero-tag {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 0 20px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(139, 92, 246, 0.08);
|
|
||||||
border: 1px solid rgba(139, 92, 246, 0.12);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #7c3aed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero-copy h2 {
|
.login-panel-card {
|
||||||
margin: 0;
|
width: 100%;
|
||||||
font-size: clamp(2.5rem, 5vw, 4.7rem);
|
max-width: 980px;
|
||||||
line-height: 0.98;
|
padding: 18px;
|
||||||
letter-spacing: -0.04em;
|
border-radius: 32px;
|
||||||
|
background: rgba(255, 255, 255, 0.78);
|
||||||
|
color: #0f172a;
|
||||||
|
border: 1px solid rgba(226, 232, 240, 0.92);
|
||||||
|
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.08);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(360px, 420px);
|
||||||
|
gap: 18px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel-intro {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 32px;
|
||||||
|
padding: 36px;
|
||||||
|
border-radius: 28px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(139, 92, 246, 0.14), transparent 34%),
|
||||||
|
linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(248, 250, 252, 0.86));
|
||||||
|
border: 1px solid rgba(226, 232, 240, 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-intro-copy h2 {
|
||||||
|
margin: 10px 0 12px;
|
||||||
|
font-size: clamp(2rem, 3vw, 3rem);
|
||||||
|
line-height: 1.04;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero-copy p:last-child {
|
.login-intro-copy p:last-child {
|
||||||
margin: 24px 0 0;
|
margin: 0;
|
||||||
max-width: 560px;
|
max-width: 30rem;
|
||||||
font-size: 1.05rem;
|
font-size: 1rem;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-brand-meta {
|
.login-intro-points {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-brand-meta span {
|
.login-intro-points span {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.72);
|
background: rgba(255, 255, 255, 0.74);
|
||||||
border: 1px solid rgba(139, 92, 246, 0.12);
|
border: 1px solid rgba(139, 92, 246, 0.12);
|
||||||
color: #475569;
|
color: #475569;
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-panel {
|
.login-panel-formWrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
padding: 34px 32px;
|
||||||
|
border-radius: 28px;
|
||||||
.login-panel-card {
|
background: rgba(255, 255, 255, 0.94);
|
||||||
width: 100%;
|
border: 1px solid rgba(226, 232, 240, 0.82);
|
||||||
max-width: 460px;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 32px;
|
|
||||||
background: rgba(255, 255, 255, 0.88);
|
|
||||||
color: #0f172a;
|
|
||||||
border: 1px solid rgba(226, 232, 240, 0.92);
|
|
||||||
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-panel-header {
|
.login-panel-header {
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-panel-header h3 {
|
.login-panel-header h3 {
|
||||||
margin: 6px 0 10px;
|
margin: 6px 0 10px;
|
||||||
font-size: 2rem;
|
font-size: 1.9rem;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
color: #020617;
|
color: #020617;
|
||||||
}
|
}
|
||||||
|
|
@ -319,17 +320,27 @@
|
||||||
max(24px, calc(20px + env(safe-area-inset-bottom)));
|
max(24px, calc(20px + env(safe-area-inset-bottom)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-panel {
|
.login-panel-card {
|
||||||
width: 100%;
|
margin: 0 auto;
|
||||||
justify-content: center;
|
max-width: 460px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
backdrop-filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page-brand {
|
.login-panel-layout {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel-intro {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-panel-card {
|
.login-panel-formWrap {
|
||||||
margin: 0 auto;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,12 +354,17 @@
|
||||||
|
|
||||||
.login-panel-card {
|
.login-panel-card {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel-formWrap {
|
||||||
padding: 20px 16px;
|
padding: 20px 16px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-panel-header {
|
.login-panel-header {
|
||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-panel-header h3 {
|
.login-panel-header h3 {
|
||||||
|
|
@ -415,12 +431,8 @@
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-panel-card {
|
.login-panel-formWrap {
|
||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
padding-right: 14px;
|
padding-right: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-brand-meta span:nth-child(3) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- ensure opportunity construction type 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_jslx', '新建', '新建', 1),
|
||||||
|
('sj_jslx', '扩容', '扩容', 2),
|
||||||
|
('sj_jslx', '替换', '替换', 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;
|
||||||
|
|
@ -933,4 +933,20 @@ WITH column_comments(table_name, column_name, comment_text) AS (
|
||||||
SELECT comment_on_column_if_exists(table_name, column_name, comment_text)
|
SELECT comment_on_column_if_exists(table_name, column_name, comment_text)
|
||||||
FROM column_comments;
|
FROM column_comments;
|
||||||
|
|
||||||
|
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_jslx', '新建', '新建', 1),
|
||||||
|
('sj_jslx', '扩容', '扩容', 2),
|
||||||
|
('sj_jslx', '替换', '替换', 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;
|
commit;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue