0402
parent
dee5da7655
commit
efd3370519
|
|
@ -7,6 +7,7 @@ public class OpportunityMetaDTO {
|
|||
private List<OpportunityDictOptionDTO> stageOptions;
|
||||
private List<OpportunityDictOptionDTO> operatorOptions;
|
||||
private List<OpportunityDictOptionDTO> projectLocationOptions;
|
||||
private List<OpportunityDictOptionDTO> opportunityTypeOptions;
|
||||
|
||||
public OpportunityMetaDTO() {
|
||||
}
|
||||
|
|
@ -14,10 +15,12 @@ public class OpportunityMetaDTO {
|
|||
public OpportunityMetaDTO(
|
||||
List<OpportunityDictOptionDTO> stageOptions,
|
||||
List<OpportunityDictOptionDTO> operatorOptions,
|
||||
List<OpportunityDictOptionDTO> projectLocationOptions) {
|
||||
List<OpportunityDictOptionDTO> projectLocationOptions,
|
||||
List<OpportunityDictOptionDTO> opportunityTypeOptions) {
|
||||
this.stageOptions = stageOptions;
|
||||
this.operatorOptions = operatorOptions;
|
||||
this.projectLocationOptions = projectLocationOptions;
|
||||
this.opportunityTypeOptions = opportunityTypeOptions;
|
||||
}
|
||||
|
||||
public List<OpportunityDictOptionDTO> getStageOptions() {
|
||||
|
|
@ -43,4 +46,12 @@ public class OpportunityMetaDTO {
|
|||
public void setProjectLocationOptions(List<OpportunityDictOptionDTO> 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.CreateOpportunityRequest;
|
||||
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.OpportunityFollowUpDTO;
|
||||
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 OPERATOR_TYPE_CODE = "sj_yzf";
|
||||
private static final String OPPORTUNITY_TYPE_CODE = "sj_jslx";
|
||||
private static final Logger log = LoggerFactory.getLogger(OpportunityServiceImpl.class);
|
||||
|
||||
private final OpportunityMapper opportunityMapper;
|
||||
|
|
@ -45,10 +47,26 @@ public class OpportunityServiceImpl implements OpportunityService {
|
|||
|
||||
@Override
|
||||
public OpportunityMetaDTO getMeta() {
|
||||
List<OpportunityDictOptionDTO> opportunityTypeOptions = opportunityMapper.selectDictItems(OPPORTUNITY_TYPE_CODE);
|
||||
return new OpportunityMetaDTO(
|
||||
opportunityMapper.selectDictItems(STAGE_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
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ unisbase:
|
|||
access-token-safety-seconds: 120
|
||||
oms:
|
||||
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-header: ${OMS_API_KEY_HEADER:apiKey}
|
||||
user-info-path: ${OMS_USER_INFO_PATH:/api/v1/user/info}
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ export interface OpportunityMeta {
|
|||
stageOptions?: OpportunityDictOption[];
|
||||
operatorOptions?: OpportunityDictOption[];
|
||||
projectLocationOptions?: OpportunityDictOption[];
|
||||
opportunityTypeOptions?: OpportunityDictOption[];
|
||||
}
|
||||
|
||||
export interface OmsPreSalesOption {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export default function Dashboard() {
|
|||
<div className="crm-page-heading">
|
||||
<h1 className="crm-page-title">工作台</h1>
|
||||
<p className="crm-page-subtitle">
|
||||
欢迎回来,{home.realName || "无"}。今天是你入职的第 {home.onboardingDays ?? 0} 天。
|
||||
欢迎回来,{home.realName || "无"}。
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -102,6 +102,20 @@ function normalizeOptionalText(value?: string) {
|
|||
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) {
|
||||
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",
|
||||
|
|
@ -477,8 +491,8 @@ export default function Expansion() {
|
|||
return;
|
||||
}
|
||||
|
||||
setSalesData(data.salesItems ?? []);
|
||||
setChannelData(data.channelItems ?? []);
|
||||
setSalesData(dedupeExpansionItemsById(data.salesItems ?? []));
|
||||
setChannelData(dedupeExpansionItemsById(data.channelItems ?? []));
|
||||
setSelectedItem(null);
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
|
|
@ -848,6 +862,8 @@ export default function Expansion() {
|
|||
value={form.officeName || ""}
|
||||
placeholder="请选择"
|
||||
sheetTitle="代表处 / 办事处"
|
||||
searchable
|
||||
searchPlaceholder="搜索代表处 / 办事处"
|
||||
options={[
|
||||
{ value: "", label: "请选择" },
|
||||
...officeOptions.map((option) => ({
|
||||
|
|
@ -877,7 +893,7 @@ export default function Expansion() {
|
|||
<input
|
||||
value={form.targetDept || ""}
|
||||
onChange={(e) => onChange("targetDept", e.target.value)}
|
||||
placeholder="办事处/行业系统部/地市"
|
||||
placeholder="办事处/地市"
|
||||
className={getFieldInputClass(Boolean(fieldErrors?.targetDept))}
|
||||
/>
|
||||
{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-backdrop" />
|
||||
<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">
|
||||
<div className="login-panel-card">
|
||||
<div className="login-panel-header">
|
||||
<p className="login-panel-eyebrow">欢迎回来</p>
|
||||
<h3>紫光汇智CRM系统</h3>
|
||||
</div>
|
||||
|
||||
<form className="login-form" onSubmit={handleSubmit}>
|
||||
<label className="login-field">
|
||||
<span>用户名</span>
|
||||
<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 className="login-panel-layout">
|
||||
<div className="login-panel-intro">
|
||||
<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>
|
||||
) : null}
|
||||
|
||||
<div className="login-form-meta">
|
||||
<label className="login-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.remember}
|
||||
onChange={(event) => handleChange("remember", event.target.checked)}
|
||||
/>
|
||||
<span>记住密码(5天)</span>
|
||||
</label>
|
||||
<div className="login-intro-copy">
|
||||
<p className="login-panel-eyebrow">欢迎回来</p>
|
||||
<h2>账号登录</h2>
|
||||
<p>{systemDescription}</p>
|
||||
</div>
|
||||
<div className="login-intro-points">
|
||||
<span>客户拓展</span>
|
||||
<span>商机推进</span>
|
||||
<span>销售协同</span>
|
||||
</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}>
|
||||
{loading ? "登录中..." : "立即登录"}
|
||||
</button>
|
||||
</form>
|
||||
<form className="login-form" onSubmit={handleSubmit}>
|
||||
<label className="login-field">
|
||||
<span>用户名</span>
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ const CONFIDENCE_OPTIONS = [
|
|||
{ value: "C", label: "C" },
|
||||
] as const;
|
||||
|
||||
const FALLBACK_OPPORTUNITY_TYPE_OPTIONS = [
|
||||
{ value: "新建", label: "新建" },
|
||||
{ value: "扩容", label: "扩容" },
|
||||
{ value: "替换", label: "替换" },
|
||||
] as const;
|
||||
|
||||
type ConfidenceGrade = (typeof CONFIDENCE_OPTIONS)[number]["value"];
|
||||
|
||||
const COMPETITOR_OPTIONS = [
|
||||
|
|
@ -52,7 +58,7 @@ const defaultForm: CreateOpportunityPayload = {
|
|||
expectedCloseDate: "",
|
||||
confidencePct: "C",
|
||||
stage: "",
|
||||
opportunityType: "新建",
|
||||
opportunityType: "",
|
||||
productType: "VDI云桌面",
|
||||
source: "主动开发",
|
||||
salesExpansionId: undefined,
|
||||
|
|
@ -77,7 +83,7 @@ function toFormFromItem(item: OpportunityItem): CreateOpportunityPayload {
|
|||
expectedCloseDate: item.date || "",
|
||||
confidencePct: normalizeConfidenceGrade(item.confidence),
|
||||
stage: item.stageCode || item.stage || "",
|
||||
opportunityType: item.type || "新建",
|
||||
opportunityType: item.type || "",
|
||||
productType: item.product || "VDI云桌面",
|
||||
source: item.source || "主动开发",
|
||||
salesExpansionId: item.salesExpansionId,
|
||||
|
|
@ -859,6 +865,7 @@ export default function Opportunities() {
|
|||
const [stageOptions, setStageOptions] = useState<OpportunityDictOption[]>([]);
|
||||
const [operatorOptions, setOperatorOptions] = useState<OpportunityDictOption[]>([]);
|
||||
const [projectLocationOptions, setProjectLocationOptions] = useState<OpportunityDictOption[]>([]);
|
||||
const [opportunityTypeOptions, setOpportunityTypeOptions] = useState<OpportunityDictOption[]>([]);
|
||||
const [form, setForm] = useState<CreateOpportunityPayload>(defaultForm);
|
||||
const [pushPreSalesId, setPushPreSalesId] = useState<number | undefined>(undefined);
|
||||
const [pushPreSalesName, setPushPreSalesName] = useState("");
|
||||
|
|
@ -927,12 +934,14 @@ export default function Opportunities() {
|
|||
setStageOptions((data.stageOptions ?? []).filter((item) => item.value));
|
||||
setOperatorOptions((data.operatorOptions ?? []).filter((item) => item.value));
|
||||
setProjectLocationOptions((data.projectLocationOptions ?? []).filter((item) => item.value));
|
||||
setOpportunityTypeOptions((data.opportunityTypeOptions ?? []).filter((item) => item.value));
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setStageOptions([]);
|
||||
setOperatorOptions([]);
|
||||
setProjectLocationOptions([]);
|
||||
setOpportunityTypeOptions([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -952,6 +961,15 @@ export default function Opportunities() {
|
|||
setForm((current) => (current.stage ? current : { ...current, stage: defaultStage }));
|
||||
}, [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 visibleItems = items.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)));
|
||||
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 selectedSalesExpansion = selectedItem?.salesExpansionId
|
||||
? 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>
|
||||
<AdaptiveSelect
|
||||
value={form.opportunityType}
|
||||
placeholder="请选择"
|
||||
sheetTitle="建设类型"
|
||||
options={[
|
||||
{ value: "新建", label: "新建" },
|
||||
{ value: "扩容", label: "扩容" },
|
||||
{ value: "替换", label: "替换" },
|
||||
]}
|
||||
options={opportunityTypeSelectOptions}
|
||||
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" : "",
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -28,26 +28,13 @@
|
|||
display: grid;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
grid-template-columns: minmax(0, 1.08fr) minmax(360px, 460px);
|
||||
gap: 48px;
|
||||
grid-template-columns: minmax(0, 980px);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding:
|
||||
max(24px, env(safe-area-inset-top))
|
||||
clamp(24px, 4vw, 64px)
|
||||
max(24px, 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;
|
||||
max(28px, env(safe-area-inset-top))
|
||||
clamp(24px, 4vw, 56px)
|
||||
max(28px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.login-brand-lockup {
|
||||
|
|
@ -93,80 +80,94 @@
|
|||
color: #0f172a;
|
||||
}
|
||||
|
||||
.login-hero-copy {
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.login-hero-tag {
|
||||
display: inline-flex;
|
||||
.login-panel {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
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 {
|
||||
margin: 0;
|
||||
font-size: clamp(2.5rem, 5vw, 4.7rem);
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.04em;
|
||||
.login-panel-card {
|
||||
width: 100%;
|
||||
max-width: 980px;
|
||||
padding: 18px;
|
||||
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;
|
||||
}
|
||||
|
||||
.login-hero-copy p:last-child {
|
||||
margin: 24px 0 0;
|
||||
max-width: 560px;
|
||||
font-size: 1.05rem;
|
||||
.login-intro-copy p:last-child {
|
||||
margin: 0;
|
||||
max-width: 30rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.login-brand-meta {
|
||||
.login-intro-points {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.login-brand-meta span {
|
||||
.login-intro-points span {
|
||||
padding: 10px 14px;
|
||||
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);
|
||||
color: #475569;
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
.login-panel-formWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-panel-card {
|
||||
width: 100%;
|
||||
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);
|
||||
padding: 34px 32px;
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(226, 232, 240, 0.82);
|
||||
}
|
||||
|
||||
.login-panel-header {
|
||||
margin-bottom: 28px;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.login-panel-header h3 {
|
||||
margin: 6px 0 10px;
|
||||
font-size: 2rem;
|
||||
font-size: 1.9rem;
|
||||
line-height: 1.1;
|
||||
color: #020617;
|
||||
}
|
||||
|
|
@ -319,17 +320,27 @@
|
|||
max(24px, calc(20px + env(safe-area-inset-bottom)));
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
.login-panel-card {
|
||||
margin: 0 auto;
|
||||
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;
|
||||
}
|
||||
|
||||
.login-panel-card {
|
||||
margin: 0 auto;
|
||||
.login-panel-formWrap {
|
||||
padding: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -343,12 +354,17 @@
|
|||
|
||||
.login-panel-card {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.login-panel-formWrap {
|
||||
padding: 20px 16px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.login-panel-header {
|
||||
margin-bottom: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-panel-header h3 {
|
||||
|
|
@ -415,12 +431,8 @@
|
|||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.login-panel-card {
|
||||
.login-panel-formWrap {
|
||||
padding-left: 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)
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue