fix: polish product pricing training layout

bugfix/ai-assessment-evidence-rescore
陈沅 2 months ago
parent 7b8644a8ec
commit 2c8615c47e

@ -14,41 +14,28 @@
/> />
<section class="price-workbench"> <section class="price-workbench">
<!-- legacy malformed navigation retained only for source compatibility <nav class="step-indicator" aria-label="新产品定价步骤">
<nav class="step-switcher" aria-label="新产品定价步骤"> <template v-for="step in pricingSteps" :key="step.no">
<button type="button" :class="{ active: currentStep === 1 }" @click="switchStep(1)"> <button
<span>01</span>{{ labels.pricingTitle }} type="button"
</button> class="step-tab"
<i>→</i> :class="{ active: currentStep === step.no, complete: currentStep > step.no }"
<button type="button" :class="{ active: currentStep === 2 }" @click="switchStep(2)"> :aria-current="currentStep === step.no ? 'step' : undefined"
<span>02</span>{{ labels.competitorPricingTitle }} @click="switchStep(step.no)"
</button> >
<i>→</i> <span class="step-marker">{{ String(step.no).padStart(2, "0") }}</span>
<button type="button" :class="{ active: currentStep === 4 }" @click="switchStep(4)"> <span class="step-copy">
<span>04</span>{{ labels.psmTitle }} <strong>{{ step.title }}</strong>
</button> <span>{{ currentStep === step.no ? "当前步骤" : "点击切换" }}</span>
<i>→</i> </span>
<button type="button" :class="{ active: currentStep === 5 }" @click="switchStep(5)">
<span>05</span>{{ labels.finalPricingTitle }}
</button>
</nav>
-->
<nav class="step-switcher" aria-label="新产品定价步骤">
<button type="button" :class="{ active: currentStep === 1 }" @click="switchStep(1)">
<span>01</span>{{ labels.pricingTitle }}
</button>
<i>→</i>
<button type="button" :class="{ active: currentStep === 2 }" @click="switchStep(2)">
<span>02</span>{{ labels.competitorPricingTitle }}
</button>
<i>→</i>
<button type="button" :class="{ active: currentStep === 3 }" @click="switchStep(3)">
<span>03</span>{{ labels.valueBasedPricingTitle }}
</button>
<i>→</i>
<button type="button" :class="{ active: currentStep === 4 }" @click="switchStep(4)">
<span>04</span>{{ labels.psmTitle }}
</button> </button>
<span
v-if="step.no < pricingSteps.length"
class="step-connector"
:class="{ complete: currentStep > step.no }"
aria-hidden="true"
/>
</template>
</nav> </nav>
<header class="workbench-head"> <header class="workbench-head">
@ -658,6 +645,13 @@ const finalPricingAnalysis = ref("");
const dynamicStepAnswers = ref({}); const dynamicStepAnswers = ref({});
const canRemovePsmRow = computed(() => psmRows.value.some((row) => !row.isDefault)); const canRemovePsmRow = computed(() => psmRows.value.some((row) => !row.isDefault));
const canRemovePerceivedValueItem = computed(() => perceivedValueItems.value.some((row) => !row.isDefault)); const canRemovePerceivedValueItem = computed(() => perceivedValueItems.value.some((row) => !row.isDefault));
const pricingSteps = computed(() => [
{ no: 1, title: labels.pricingTitle },
{ no: 2, title: labels.competitorPricingTitle },
{ no: 3, title: labels.valueBasedPricingTitle },
{ no: 4, title: labels.psmTitle },
{ no: 5, title: labels.finalPricingTitle },
]);
const currentStepTitle = computed(() => { const currentStepTitle = computed(() => {
if (currentStep.value === 1) return labels.pricingTitle; if (currentStep.value === 1) return labels.pricingTitle;
if (currentStep.value === 2) return labels.competitorPricingTitle; if (currentStep.value === 2) return labels.competitorPricingTitle;
@ -1574,48 +1568,185 @@ onMounted(async () => {
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.2); box-shadow: 0 20px 45px rgba(0, 0, 0, 0.2);
} }
.step-switcher { .step-indicator {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 10px; width: calc(100% - 48px);
padding: 18px 26px 0; margin: 18px 24px 0;
padding: 12px 14px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 14px;
background: linear-gradient(110deg, rgba(4, 39, 58, 0.9), rgba(2, 20, 33, 0.94));
box-shadow: inset 0 1px 0 rgba(169, 229, 255, 0.08);
}
button { .step-tab {
display: inline-flex; display: flex;
flex: 1 0 136px;
align-items: center; align-items: center;
gap: 8px; min-width: 0;
padding: 8px 12px; min-height: 52px;
color: var(--muted); padding: 7px 9px;
color: #9ab3d0;
cursor: pointer; cursor: pointer;
border: 1px solid var(--line); border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 999px; border-radius: 8px;
background: transparent; background: rgba(3, 27, 43, 0.52);
transition: transform 0.2s, color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s;
&:hover,
&:focus-visible {
color: #fff;
border-color: rgba(89, 223, 255, 0.82);
outline: none;
background: rgba(8, 92, 131, 0.52);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
} }
button.active { &.active {
color: #e9fbff; color: #fff;
border-color: var(--line-strong); border-color: rgba(89, 226, 255, 0.85);
background: rgba(20, 149, 190, 0.25); background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
} }
span { &.complete {
display: grid; color: #dffbff;
width: 20px; }
height: 20px; }
place-items: center;
color: #7de2ff; .step-marker {
display: inline-flex;
flex: 0 0 30px;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
color: #a6c4d9;
border: 1px solid rgba(101, 176, 208, 0.6);
border-radius: 50%;
background: rgba(3, 25, 40, 0.8);
box-shadow: inset 0 0 12px rgba(38, 174, 233, 0.08);
font-size: 11px; font-size: 11px;
font-weight: 900;
letter-spacing: 0.04em;
}
.step-copy {
display: grid;
min-width: 0;
gap: 3px;
margin-left: 8px;
strong {
display: -webkit-box;
overflow: hidden;
color: inherit;
font-size: 13px;
line-height: 1.25;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
span {
color: #66b6d4;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
}
}
.step-tab.active .step-marker {
color: #fff;
border-color: #72e8ff;
background: linear-gradient(135deg, #08b7e8, #1375d0);
box-shadow: 0 0 0 5px rgba(25, 179, 237, 0.14), 0 0 18px rgba(46, 208, 255, 0.58);
}
.step-tab.complete .step-marker {
color: #062b37;
border-color: rgba(84, 230, 221, 0.85);
background: #6ef0e1;
box-shadow: 0 0 12px rgba(79, 231, 218, 0.35);
}
.step-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
color: transparent;
background: rgba(98, 169, 201, 0.34);
&::after {
position: absolute;
top: 50%;
right: -1px;
width: 6px;
height: 6px;
border-top: 1px solid #73b1ce;
border-right: 1px solid #73b1ce;
content: "";
transform: translateY(-50%) rotate(45deg);
}
&.complete {
background: linear-gradient(90deg, #6ef0e1, #1cb8e9);
box-shadow: 0 0 9px rgba(53, 218, 235, 0.46);
&::after {
border-color: #63e6f1;
}
}
}
.primary-button,
.secondary-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 0 14px;
color: #e9fbff;
cursor: pointer;
border: 1px solid rgba(99, 217, 255, 0.55);
border-radius: 8px;
font: inherit;
font-size: 14px;
font-weight: 700; font-weight: 700;
background: rgba(117, 226, 255, 0.18); line-height: 1;
border-radius: 50%; transition: transform 0.18s, border-color 0.18s, background 0.18s, opacity 0.18s;
&:hover:not(:disabled),
&:focus-visible:not(:disabled) {
border-color: #74ebff;
outline: none;
transform: translateY(-1px);
} }
i { &:disabled {
color: #55d3f8; cursor: not-allowed;
font-size: 18px; opacity: 0.45;
font-style: normal; }
}
.primary-button {
background: linear-gradient(135deg, #08a5d6, #147bb9);
} }
.secondary-button {
background: rgba(17, 126, 178, 0.36);
}
.toolbar-actions {
display: flex;
flex: none;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
} }
.workbench-head { .workbench-head {
@ -1995,7 +2126,7 @@ onMounted(async () => {
} }
} }
@media (max-width: 1200px) { @media (max-width: 1440px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@ -2024,138 +2155,183 @@ onMounted(async () => {
} }
} }
} }
.psm-section { /* PSM dark theme */
.price-page {
.psm-section {
display: grid; display: grid;
gap: 20px; gap: 20px;
} padding: 0 26px 26px;
}
.psm-toolbar, .psm-toolbar,
.psm-chart-head { .psm-chart-head {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
} }
.psm-toolbar p, .psm-toolbar p,
.psm-chart-head p { .psm-chart-head p,
.psm-guide-row p {
margin: 0; margin: 0;
color: #5e6b7b; color: var(--muted);
line-height: 1.7; line-height: 1.7;
} }
.psm-subtitle { .psm-subtitle {
margin-top: 8px !important; margin-top: 8px !important;
color: #25384c !important; color: #dffaff !important;
font-weight: 700; font-weight: 700;
} }
.toolbar-actions { .table-scroll {
display: flex; overflow-x: auto;
flex-wrap: wrap; border: 1px solid var(--line);
justify-content: flex-end; border-radius: 14px;
gap: 10px; background: rgba(2, 19, 28, 0.42);
} }
.psm-table {
width: 100%;
min-width: 980px;
border-collapse: collapse;
.psm-table input, th,
.psm-point-grid input { td {
padding: 12px;
border-bottom: 1px solid var(--line);
text-align: left;
}
th {
color: #ddf8ff;
font-size: 14px;
background: rgba(25, 122, 157, 0.46);
}
td {
min-width: 150px;
color: #dffaff;
}
td:first-child {
min-width: 120px;
font-weight: 700;
}
tbody tr:last-child td {
border-bottom: 0;
}
}
.psm-table input,
.psm-point-grid input {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
min-height: 38px;
padding: 8px 10px; padding: 8px 10px;
color: #243447; color: #edfaff;
background: #fff; background: rgba(2, 19, 28, 0.7);
border: 1px solid #d8e1eb; border: 1px solid rgba(94, 213, 245, 0.3);
border-radius: 6px; border-radius: 7px;
} outline: none;
.psm-table td { &:focus {
min-width: 170px; border-color: #63e5ff;
} box-shadow: 0 0 0 3px rgba(50, 204, 241, 0.12);
}
}
.psm-point-section, .psm-point-section,
.psm-chart-section { .psm-chart-section {
padding: 20px; padding: 20px;
background: #fbfdff; background: rgba(5, 35, 48, 0.72);
border: 1px solid #dbe5ef; border: 1px solid var(--line);
border-radius: 12px; border-radius: 12px;
} }
.psm-point-section h3, .psm-point-section h3,
.psm-chart-section h3 { .psm-chart-section h3 {
margin: 0 0 14px; margin: 0 0 14px;
color: #20344b; color: #e5f9ff;
font-size: 17px; font-size: 17px;
} }
.psm-point-grid { .psm-point-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px; gap: 16px;
} }
.psm-point-grid label { .psm-point-grid label {
display: grid; display: grid;
gap: 8px; gap: 8px;
color: #45566a; color: #dffaff;
font-weight: 600; font-weight: 600;
} }
.file-picker { .file-picker {
display: none; display: none;
} }
.psm-chart-preview { .psm-chart-preview {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 14px; gap: 14px;
padding: 12px; padding: 12px;
margin-top: 16px; margin-top: 16px;
background: #fff; background: rgba(2, 19, 28, 0.7);
border: 1px solid #dbe5ef; border: 1px solid var(--line);
border-radius: 10px; border-radius: 10px;
} }
.psm-chart-image { .psm-chart-image {
width: 148px; width: 148px;
height: 92px; height: 92px;
overflow: hidden; overflow: hidden;
border-radius: 8px; border-radius: 8px;
cursor: zoom-in; cursor: zoom-in;
} }
.psm-chart-preview p { .psm-chart-preview p {
margin: 0 0 7px; margin: 0 0 7px;
color: #45566a; color: #dffaff;
word-break: break-all; word-break: break-all;
} }
.psm-guide-row { .psm-guide-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
padding-top: 16px; padding-top: 16px;
margin-top: 16px; margin-top: 16px;
border-top: 1px solid #dbe5ef; border-top: 1px solid var(--line);
} }
.psm-guide-row p { .step-footer {
margin: 0; display: flex;
color: #5e6b7b; justify-content: flex-end;
line-height: 1.7; }
}
.text-button { .text-button {
padding: 0; padding: 0;
color: #1687c6; color: #63e5ff;
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
border: 0; border: 0;
} }
@media (max-width: 768px) {
.psm-section {
padding: 0 14px 18px;
}
@media (max-width: 768px) {
.psm-toolbar, .psm-toolbar,
.psm-chart-head { .psm-chart-head,
.psm-guide-row {
align-items: stretch;
flex-direction: column; flex-direction: column;
} }
@ -2166,10 +2342,6 @@ onMounted(async () => {
.psm-point-grid { .psm-point-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.psm-guide-row {
align-items: flex-start;
flex-direction: column;
} }
} }
</style> </style>

@ -0,0 +1,32 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const page = fs.readFileSync(
path.resolve(__dirname, "../src/views/product/price.vue"),
"utf8",
);
const stepper = page.match(
/<nav class="step-indicator" aria-label="新产品定价步骤">([\s\S]*?)<\/nav>/,
)?.[1] || "";
assert.ok(stepper, "product pricing must use the shared step-indicator navigation pattern");
assert.match(stepper, /v-for="step in pricingSteps"/, "the step navigation must be rendered from actual step data");
assert.match(stepper, /@click="switchStep\(step\.no\)"/, "each pricing step must be navigable");
assert.match(page, /const pricingSteps = computed\(\(\) => \[/, "the page must define its pricing steps");
for (const label of ["pricingTitle", "competitorPricingTitle", "valueBasedPricingTitle", "psmTitle", "finalPricingTitle"]) {
assert.match(page, new RegExp(`title: labels\\.${label}`), `${label} must appear in the five-step navigation`);
}
assert.match(page, /\.step-indicator\s*\{/, "the page must style the shared step-indicator layout");
assert.match(page, /\.step-connector\s*\{[\s\S]*?flex:\s*0 1 40px;/, "connectors must use the compact shared spacing");
assert.match(page, /\.primary-button,\s*\.secondary-button\s*\{[\s\S]*?color:\s*#e9fbff;/, "pricing toolbar buttons must use the student dark theme");
assert.match(page, /\.primary-button\s*\{[\s\S]*?background:\s*linear-gradient/, "the primary action must use the blue gradient button style");
assert.match(page, /\.toolbar-actions\s*\{[\s\S]*?flex:\s*none;/, "toolbar actions must retain their width instead of being compressed");
const psmTheme = page.match(/\/\* PSM dark theme \*\/([\s\S]*?)<\/style>/)?.[1] || "";
assert.ok(psmTheme, "the PSM step must define its dark theme locally");
assert.match(psmTheme, /\.psm-point-section,\s*\.psm-chart-section\s*\{[\s\S]*?background:\s*rgba\(5, 35, 48, 0\.72\)/, "PSM panels must not fall back to white cards");
assert.match(psmTheme, /\.psm-table\s*\{[\s\S]*?width:\s*100%/, "the PSM table must fill its available workspace");
assert.match(psmTheme, /\.psm-table input,\s*\.psm-point-grid input\s*\{[\s\S]*?background:\s*rgba\(2, 19, 28, 0\.7\)/, "PSM inputs must use the dark form style");
console.log("Product pricing step indicator contract passed.");
Loading…
Cancel
Save