Compare commits
3 Commits
b322c930fa
...
021045e285
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
021045e285 | ||
|
|
67edbe2077 | ||
|
|
ac52cbdbfb |
@@ -8,4 +8,15 @@ describe("LeadsPage initial loading", () => {
|
||||
|
||||
expect(source).toMatch(/onMounted\(load\)/);
|
||||
});
|
||||
|
||||
it("keeps the grouped lead detail layout hooks", async () => {
|
||||
const source = await readFile(fileURLToPath(new URL("./LeadsPage.vue", import.meta.url)), "utf8");
|
||||
|
||||
expect(source).toContain('class="lead-detail-card"');
|
||||
expect(source).toContain('class="lead-detail-item"');
|
||||
expect(source).toContain('class="lead-detail-section-title"');
|
||||
expect(source).toContain('class="lead-detail-section lead-detail-note"');
|
||||
expect(source.match(/class="lead-detail-item"/g)).toHaveLength(6);
|
||||
expect(source).toContain("v-permission=\"'admin:leads:update'\"");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,10 +117,31 @@ onMounted(load);
|
||||
|
||||
<el-dialog v-model="detailOpen" title="线索详情" width="min(720px, 94vw)" destroy-on-close>
|
||||
<template v-if="selectedLead">
|
||||
<div class="lead-detail-toolbar"><el-tag :type="statusTagType(selectedLead.status)" effect="plain">{{ leadStatusLabel(selectedLead.status) }}</el-tag><el-select v-permission="'admin:leads:update'" :model-value="selectedLead.status" :loading="updatingId === selectedLead.id" size="small" @update:model-value="changeStatus(selectedLead, $event)"><el-option v-for="option in leadStatusOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select></div>
|
||||
<dl class="lead-detail-grid"><div><dt>联系人</dt><dd>{{ selectedLead.contactName || "未填写" }}</dd></div><div><dt>联系方式</dt><dd>{{ selectedLead.phone || "未填写" }}</dd></div><div><dt>需求类型</dt><dd>{{ selectedLead.leadType === "vehicle" ? "用车需求" : "一般需求" }}</dd></div><div><dt>提交时间</dt><dd>{{ formatDate(selectedLead.createdAt) }}</dd></div><div><dt>目的地</dt><dd>{{ selectedLead.destination || "未填写" }}</dd></div><div><dt>出行日期</dt><dd>{{ formatDate(selectedLead.vehicleDemand?.travelDate || selectedLead.travelDate) }}</dd></div></dl>
|
||||
<div v-if="selectedLead.vehicleDemand" class="lead-detail-section"><strong>用车行程</strong><p>{{ selectedLead.vehicleDemand.pickupLocation }} → {{ selectedLead.vehicleDemand.dropoffLocation }}</p><p>{{ selectedLead.vehicleDemand.peopleCount }} 人 · 行李 {{ selectedLead.vehicleDemand.luggageCount ?? "未填写" }} 件 · {{ selectedLead.vehicleDemand.vehicleOptionTitle || "未指定车型" }}</p></div>
|
||||
<div class="lead-detail-section"><strong>备注</strong><p>{{ selectedLead.vehicleDemand?.specialRequirements || selectedLead.note || "暂无备注" }}</p></div>
|
||||
<div class="lead-detail-toolbar">
|
||||
<el-tag :type="statusTagType(selectedLead.status)" effect="plain">{{ leadStatusLabel(selectedLead.status) }}</el-tag>
|
||||
<el-select v-permission="'admin:leads:update'" :model-value="selectedLead.status" :loading="updatingId === selectedLead.id" size="small" @update:model-value="changeStatus(selectedLead, $event)">
|
||||
<el-option v-for="option in leadStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="lead-detail-card">
|
||||
<dl class="lead-detail-grid">
|
||||
<div class="lead-detail-item"><dt>联系人</dt><dd>{{ selectedLead.contactName || "未填写" }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>联系方式</dt><dd>{{ selectedLead.phone || "未填写" }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>需求类型</dt><dd>{{ selectedLead.leadType === "vehicle" ? "用车需求" : "一般需求" }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>提交时间</dt><dd>{{ formatDate(selectedLead.createdAt) }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>目的地</dt><dd>{{ selectedLead.destination || "未填写" }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>出行日期</dt><dd>{{ formatDate(selectedLead.vehicleDemand?.travelDate || selectedLead.travelDate) }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<section v-if="selectedLead.vehicleDemand" class="lead-detail-section lead-detail-trip">
|
||||
<div class="lead-detail-section-title"><strong>用车行程</strong></div>
|
||||
<p>{{ selectedLead.vehicleDemand.pickupLocation }} → {{ selectedLead.vehicleDemand.dropoffLocation }}</p>
|
||||
<p>{{ selectedLead.vehicleDemand.peopleCount }} 人 · 行李 {{ selectedLead.vehicleDemand.luggageCount ?? "未填写" }} 件 · {{ selectedLead.vehicleDemand.vehicleOptionTitle || "未指定车型" }}</p>
|
||||
</section>
|
||||
<section class="lead-detail-section lead-detail-note">
|
||||
<div class="lead-detail-section-title"><strong>备注</strong></div>
|
||||
<p>{{ selectedLead.vehicleDemand?.specialRequirements || selectedLead.note || "暂无备注" }}</p>
|
||||
</section>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
@@ -61,15 +61,21 @@ button, input { font: inherit; }
|
||||
.lead-filter-card { margin-bottom: 16px; }
|
||||
.lead-filters { display: grid; grid-template-columns: 170px 170px minmax(180px, 1fr) auto; gap: 10px; }
|
||||
.lead-pagination { display: flex; justify-content: flex-end; align-items: center; gap: 14px; padding-top: 16px; color: var(--muted); font-size: 12px; }
|
||||
.lead-detail-toolbar { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-bottom: 18px; }
|
||||
.lead-detail-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; background: #f8fafc; }
|
||||
.lead-detail-toolbar .el-tag { flex: 0 0 auto; }
|
||||
.lead-detail-toolbar .el-select { min-width: 0; flex: 1 1 auto; max-width: 310px; }
|
||||
.lead-summary { display: grid; gap: 4px; min-width: 0; }
|
||||
.lead-summary span { overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.lead-detail-card { margin-top: 16px; }
|
||||
.lead-detail-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin: 0; }
|
||||
.lead-detail-grid dt { color: var(--muted); font-size: 12px; }
|
||||
.lead-detail-grid dd { margin: 5px 0 0; color: var(--ink); font-weight: 650; }
|
||||
.lead-detail-section { margin-top: 20px; padding: 14px; border-radius: 9px; background: #f8fafc; }
|
||||
.lead-detail-section p { margin: 7px 0 0; color: #475569; line-height: 1.6; }
|
||||
.lead-detail-card { overflow: hidden; margin-top: 0; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); }
|
||||
.lead-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin: 0; background: var(--line); }
|
||||
.lead-detail-item { min-width: 0; padding: 15px 16px; background: var(--surface); }
|
||||
.lead-detail-grid dt { color: var(--muted); font-size: 12px; line-height: 1.4; }
|
||||
.lead-detail-grid dd { min-width: 0; margin: 5px 0 0; overflow-wrap: anywhere; color: var(--ink); font-weight: 650; line-height: 1.45; }
|
||||
.lead-detail-section { margin-top: 16px; padding: 14px 16px; border: 1px solid transparent; border-radius: 9px; background: #f8fafc; }
|
||||
.lead-detail-section-title { display: flex; align-items: center; gap: 8px; color: #475569; font-size: 13px; line-height: 1.4; }
|
||||
.lead-detail-section-title::before { width: 3px; height: 14px; border-radius: 3px; background: var(--blue); content: ""; }
|
||||
.lead-detail-section p { margin: 7px 0 0; overflow-wrap: anywhere; color: #475569; line-height: 1.6; }
|
||||
.lead-detail-note { border-color: #e4eaf2; background: #f5f8fc; }
|
||||
.media-toolbar { justify-content: flex-end; }
|
||||
.media-toolbar .el-input { width: 150px; }
|
||||
.media-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
|
||||
@@ -190,9 +196,9 @@ button, input { font: inherit; }
|
||||
.system-editor-form .el-col-12 { width: 100%; max-width: 100%; flex: 0 0 100%; }
|
||||
}
|
||||
@media (max-width: 880px) { .wanfa-workspace { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 980px) { .media-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .lead-detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 980px) { .media-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@media (max-width: 760px) { .concierge-row { align-items: flex-start; flex-direction: column; } .concierge-actions { justify-content: flex-start; } .lead-filters { grid-template-columns: 1fr 1fr; } .lead-filters .el-input { grid-column: 1 / -1; } .operations-grid { grid-template-columns: 1fr; } .media-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 640px) { .site-config-intro { align-items: flex-start; } .site-config-intro .el-button, .page-intro-actions { width: 100%; } .page-intro-actions { justify-content: stretch; } .page-intro-actions .el-button { flex: 1; } .editor-options { grid-template-columns: 1fr; gap: 0; } .site-config-panel .panel-heading { align-items: flex-start; flex-direction: column; } .site-config-panel .panel-heading .el-button { width: 100%; } .concierge-detail-row { grid-template-columns: 1fr; } .lead-filters { grid-template-columns: 1fr; } .lead-filters .el-input { grid-column: auto; } .lead-detail-grid { grid-template-columns: 1fr; } .media-toolbar { align-items: stretch; flex-direction: column; width: 100%; } .media-toolbar .el-input { width: 100%; } .media-grid { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 640px) { .site-config-intro { align-items: flex-start; } .site-config-intro .el-button, .page-intro-actions { width: 100%; } .page-intro-actions { justify-content: stretch; } .page-intro-actions .el-button { flex: 1; } .editor-options { grid-template-columns: 1fr; gap: 0; } .site-config-panel .panel-heading { align-items: flex-start; flex-direction: column; } .site-config-panel .panel-heading .el-button { width: 100%; } .concierge-detail-row { grid-template-columns: 1fr; } .lead-filters { grid-template-columns: 1fr; } .lead-filters .el-input { grid-column: auto; } .lead-detail-grid { grid-template-columns: 1fr; } .lead-detail-toolbar { align-items: stretch; flex-direction: column; } .lead-detail-toolbar .el-select { width: 100%; max-width: none; } .media-toolbar { align-items: stretch; flex-direction: column; width: 100%; } .media-toolbar .el-input { width: 100%; } .media-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
/* RuoYi-compatible shell: fixed sidebar, compact navbar, tags view and responsive content. */
|
||||
.admin-layout {
|
||||
|
||||
181
docs/superpowers/plans/2026-08-27-leads-detail-dialog-plan.md
Normal file
181
docs/superpowers/plans/2026-08-27-leads-detail-dialog-plan.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# 线索详情弹窗视觉优化 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 将线索详情弹窗调整为 A 方案「信息分组卡片」,提升字段层级、长文本可读性和窄屏适配,同时保持现有业务行为不变。
|
||||
|
||||
**Architecture:** 继续使用 `LeadsPage.vue` 的现有 `selectedLead` 数据和状态更新逻辑,只为详情模板增加基础信息容器、字段项和区块标题语义。所有视觉变化集中在 `styles.css`,不增加组件、接口或数据层。
|
||||
|
||||
**Tech Stack:** Vue 3、TypeScript、Element Plus、Vitest、Vite、CSS Grid/Flexbox。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add a regression test for the A-layout structure
|
||||
|
||||
**Files:**
|
||||
- Modify: `WonderQ-Admin-UI-Vue/src/pages/LeadsPage.test.ts`
|
||||
|
||||
- [ ] **Step 1: Add assertions for the required semantic hooks before changing production code**
|
||||
|
||||
Append this test to the existing `describe` block:
|
||||
|
||||
```ts
|
||||
it("keeps the grouped lead detail layout hooks", async () => {
|
||||
const source = await readFile(fileURLToPath(new URL("./LeadsPage.vue", import.meta.url)), "utf8");
|
||||
|
||||
expect(source).toContain('class="lead-detail-card"');
|
||||
expect(source).toContain('class="lead-detail-item"');
|
||||
expect(source).toContain('class="lead-detail-section-title"');
|
||||
expect(source).toContain('class="lead-detail-section lead-detail-note"');
|
||||
expect(source.match(/class="lead-detail-item"/g)).toHaveLength(6);
|
||||
expect(source).toContain("v-permission=\"'admin:leads:update'\"");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused test and confirm it fails for the missing hooks**
|
||||
|
||||
Run from `WonderQ-Admin-UI-Vue`:
|
||||
|
||||
```powershell
|
||||
yarn vitest run src/pages/LeadsPage.test.ts
|
||||
```
|
||||
|
||||
Expected: the existing initial-loading test passes and the new grouped-layout test fails because the current template does not contain the new wrapper/item/title classes.
|
||||
|
||||
### Task 2: Implement the grouped detail markup and styles
|
||||
|
||||
**Files:**
|
||||
- Modify: `WonderQ-Admin-UI-Vue/src/pages/LeadsPage.vue:118-123`
|
||||
- Modify: `WonderQ-Admin-UI-Vue/src/styles.css:64-72,193-195`
|
||||
|
||||
- [ ] **Step 1: Wrap the detail fields without changing their data expressions**
|
||||
|
||||
Replace the current detail content after `<template v-if="selectedLead">` with this structure:
|
||||
|
||||
```vue
|
||||
<div class="lead-detail-toolbar">
|
||||
<el-tag :type="statusTagType(selectedLead.status)" effect="plain">{{ leadStatusLabel(selectedLead.status) }}</el-tag>
|
||||
<el-select v-permission="'admin:leads:update'" :model-value="selectedLead.status" :loading="updatingId === selectedLead.id" size="small" @update:model-value="changeStatus(selectedLead, $event)">
|
||||
<el-option v-for="option in leadStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="lead-detail-card">
|
||||
<dl class="lead-detail-grid">
|
||||
<div class="lead-detail-item"><dt>联系人</dt><dd>{{ selectedLead.contactName || "未填写" }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>联系方式</dt><dd>{{ selectedLead.phone || "未填写" }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>需求类型</dt><dd>{{ selectedLead.leadType === "vehicle" ? "用车需求" : "一般需求" }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>提交时间</dt><dd>{{ formatDate(selectedLead.createdAt) }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>目的地</dt><dd>{{ selectedLead.destination || "未填写" }}</dd></div>
|
||||
<div class="lead-detail-item"><dt>出行日期</dt><dd>{{ formatDate(selectedLead.vehicleDemand?.travelDate || selectedLead.travelDate) }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<section v-if="selectedLead.vehicleDemand" class="lead-detail-section lead-detail-trip">
|
||||
<div class="lead-detail-section-title"><strong>用车行程</strong></div>
|
||||
<p>{{ selectedLead.vehicleDemand.pickupLocation }} → {{ selectedLead.vehicleDemand.dropoffLocation }}</p>
|
||||
<p>{{ selectedLead.vehicleDemand.peopleCount }} 人 · 行李 {{ selectedLead.vehicleDemand.luggageCount ?? "未填写" }} 件 · {{ selectedLead.vehicleDemand.vehicleOptionTitle || "未指定车型" }}</p>
|
||||
</section>
|
||||
<section class="lead-detail-section lead-detail-note">
|
||||
<div class="lead-detail-section-title"><strong>备注</strong></div>
|
||||
<p>{{ selectedLead.vehicleDemand?.specialRequirements || selectedLead.note || "暂无备注" }}</p>
|
||||
</section>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the detail-specific CSS with the two-column card treatment**
|
||||
|
||||
Replace the rules for `.lead-detail-toolbar` through `.lead-detail-section p` with:
|
||||
|
||||
```css
|
||||
.lead-detail-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; background: #f8fafc; }
|
||||
.lead-detail-toolbar .el-tag { flex: 0 0 auto; }
|
||||
.lead-detail-toolbar .el-select { min-width: 0; flex: 1 1 auto; max-width: 310px; }
|
||||
.lead-summary { display: grid; gap: 4px; min-width: 0; }
|
||||
.lead-summary span { overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.lead-detail-card { overflow: hidden; margin-top: 0; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); }
|
||||
.lead-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin: 0; background: var(--line); }
|
||||
.lead-detail-item { min-width: 0; padding: 15px 16px; background: var(--surface); }
|
||||
.lead-detail-grid dt { color: var(--muted); font-size: 12px; line-height: 1.4; }
|
||||
.lead-detail-grid dd { min-width: 0; margin: 5px 0 0; overflow-wrap: anywhere; color: var(--ink); font-weight: 650; line-height: 1.45; }
|
||||
.lead-detail-section { margin-top: 16px; padding: 14px 16px; border: 1px solid transparent; border-radius: 9px; background: #f8fafc; }
|
||||
.lead-detail-section-title { display: flex; align-items: center; gap: 8px; color: #475569; font-size: 13px; line-height: 1.4; }
|
||||
.lead-detail-section-title::before { width: 3px; height: 14px; border-radius: 3px; background: var(--blue); content: ""; }
|
||||
.lead-detail-section p { margin: 7px 0 0; overflow-wrap: anywhere; color: #475569; line-height: 1.6; }
|
||||
.lead-detail-note { border-color: #e4eaf2; background: #f5f8fc; }
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Keep responsive rules aligned with the new grid**
|
||||
|
||||
Change the existing lead-detail media rules to:
|
||||
|
||||
```css
|
||||
@media (max-width: 980px) { .media-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@media (max-width: 760px) { .concierge-row { align-items: flex-start; flex-direction: column; } .concierge-actions { justify-content: flex-start; } .lead-filters { grid-template-columns: 1fr 1fr; } .lead-filters .el-input { grid-column: 1 / -1; } .operations-grid { grid-template-columns: 1fr; } .media-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 640px) { .site-config-intro { align-items: flex-start; } .site-config-intro .el-button, .page-intro-actions { width: 100%; } .page-intro-actions { justify-content: stretch; } .page-intro-actions .el-button { flex: 1; } .editor-options { grid-template-columns: 1fr; gap: 0; } .site-config-panel .panel-heading { align-items: flex-start; flex-direction: column; } .site-config-panel .panel-heading .el-button { width: 100%; } .concierge-detail-row { grid-template-columns: 1fr; } .lead-filters { grid-template-columns: 1fr; } .lead-filters .el-input { grid-column: auto; } .lead-detail-grid { grid-template-columns: 1fr; } .lead-detail-toolbar { align-items: stretch; flex-direction: column; } .lead-detail-toolbar .el-select { width: 100%; max-width: none; } .media-toolbar { align-items: stretch; flex-direction: column; width: 100%; } .media-toolbar .el-input { width: 100%; } .media-grid { grid-template-columns: 1fr; } }
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the focused test and confirm it passes**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
yarn vitest run src/pages/LeadsPage.test.ts
|
||||
```
|
||||
|
||||
Expected: both tests pass with no new warnings.
|
||||
|
||||
### Task 3: Run project verification and inspect the rendered dialog
|
||||
|
||||
**Files:**
|
||||
- Verify: `WonderQ-Admin-UI-Vue/src/pages/LeadsPage.vue`
|
||||
- Verify: `WonderQ-Admin-UI-Vue/src/styles.css`
|
||||
- Verify: `WonderQ-Admin-UI-Vue/src/pages/LeadsPage.test.ts`
|
||||
|
||||
- [ ] **Step 1: Run the complete frontend test suite**
|
||||
|
||||
Run from `WonderQ-Admin-UI-Vue`:
|
||||
|
||||
```powershell
|
||||
yarn test
|
||||
```
|
||||
|
||||
Expected: Vitest exits with code 0.
|
||||
|
||||
- [ ] **Step 2: Run the production build**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
yarn build
|
||||
```
|
||||
|
||||
Expected: Vite completes successfully and produces `dist/` without TypeScript or template errors.
|
||||
|
||||
- [ ] **Step 3: Check the final diff and repository status**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git diff --check
|
||||
git diff -- WonderQ-Admin-UI-Vue/src/pages/LeadsPage.vue WonderQ-Admin-UI-Vue/src/styles.css WonderQ-Admin-UI-Vue/src/pages/LeadsPage.test.ts
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: only the planned frontend files are modified, with no whitespace errors, secrets, build artifacts, or environment-file changes.
|
||||
|
||||
- [ ] **Step 4: Manually inspect the dialog in the local browser**
|
||||
|
||||
Open `http://localhost:5602`, go to 需求线索, open a detail dialog, and verify:
|
||||
|
||||
1. Status tag and status select remain usable and permission-controlled.
|
||||
2. Six fields appear in a bordered two-column card.
|
||||
3. A long contact value wraps inside its card without horizontal overflow.
|
||||
4. Vehicle demand shows its own highlighted section, while ordinary leads show only the note section.
|
||||
5. At a narrow viewport the six fields become one column and the status controls stack vertically.
|
||||
|
||||
- [ ] **Step 5: Commit the implementation as one atomic change**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
git add -- WonderQ-Admin-UI-Vue/src/pages/LeadsPage.vue WonderQ-Admin-UI-Vue/src/styles.css WonderQ-Admin-UI-Vue/src/pages/LeadsPage.test.ts
|
||||
git commit -m "fix: improve lead detail dialog hierarchy"
|
||||
```
|
||||
@@ -0,0 +1,55 @@
|
||||
# 线索详情弹窗视觉优化设计说明
|
||||
|
||||
## 背景
|
||||
|
||||
当前 `WonderQ-Admin-UI-Vue` 的线索详情弹窗使用四列无分组信息网格。联系人、联系方式、需求类型、提交时间、目的地和出行日期在同一视觉层级中展示,长联系方式容易挤压其他字段;备注区域与基础信息的层次也不够明确。
|
||||
|
||||
本次采用已确认的 A 方案「信息分组卡片」,目标是在不改变接口、数据结构和状态操作的前提下,提高线索详情的扫描效率和信息层次。
|
||||
|
||||
## 设计目标
|
||||
|
||||
- 保留现有线索详情字段、状态下拉操作、用车行程和备注内容。
|
||||
- 用两列信息卡片承载基础字段,降低长文本对布局的影响。
|
||||
- 让状态操作、基础信息、用车行程和备注形成清晰的阅读顺序。
|
||||
- 沿用项目现有蓝色主色、浅色背景、细边框和 Element Plus 视觉语言。
|
||||
- 弹窗窄屏时从两列退化为一列,长联系方式和日期自然换行。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不修改 Admin API、请求参数、响应字段、鉴权或状态流转逻辑。
|
||||
- 不新增字段、筛选能力、按钮或业务动作。
|
||||
- 不重构通用弹窗组件或全局主题,只调整线索详情所需的局部模板和样式。
|
||||
|
||||
## 页面结构
|
||||
|
||||
弹窗内容顺序固定为:
|
||||
|
||||
1. Element Plus 弹窗标题和关闭操作。
|
||||
2. 状态摘要行:当前状态标签 + 权限控制的状态选择器。
|
||||
3. 基础信息卡片:联系人、联系方式、需求类型、提交时间、目的地、出行日期,两列排列。
|
||||
4. 条件区块:仅当存在 `vehicleDemand` 时展示用车行程。
|
||||
5. 备注区块:展示特殊要求、备注或空态文案。
|
||||
|
||||
基础信息卡片使用统一边框容器和细分隔线;每个字段采用“弱化标签 + 强化值”的两级排版。联系方式使用 `overflow-wrap: anywhere`,确保测试串和真实长联系方式不会撑破弹窗。
|
||||
|
||||
## 视觉与交互规则
|
||||
|
||||
- 弹窗主体保持白色,信息卡片使用项目现有的 `--line`、`--muted`、`--ink` 等变量。
|
||||
- 状态行与信息卡片之间使用稳定的垂直间距,不额外增加装饰性渐变、光晕或重阴影。
|
||||
- 用车行程和备注继续使用浅色区块,区块标题增加左侧短强调线,帮助快速定位。
|
||||
- 状态选择器继续由 `v-permission="'admin:leads:update'"` 控制,加载状态继续使用 `updatingId`。
|
||||
- 所有内容仍通过当前 `selectedLead`、`formatDate` 和 `leadStatusLabel` 渲染,不引入新的数据源。
|
||||
|
||||
## 响应式规则
|
||||
|
||||
- 默认弹窗宽度仍由现有 `width="min(720px, 94vw)"` 控制。
|
||||
- 宽度小于 640px 时,基础信息由两列切换为一列。
|
||||
- 字段值允许换行,避免水平滚动和文本截断。
|
||||
- 用车行程和备注区块保持完整宽度,按钮与状态操作不改变现有权限行为。
|
||||
|
||||
## 实施范围与验证
|
||||
|
||||
- 修改 `WonderQ-Admin-UI-Vue/src/pages/LeadsPage.vue`:为基础信息增加语义化分组标记,保留现有业务表达式。
|
||||
- 修改 `WonderQ-Admin-UI-Vue/src/styles.css`:调整详情弹窗的网格、信息卡片、区块标题、长文本和响应式规则。
|
||||
- 更新 `WonderQ-Admin-UI-Vue/src/pages/LeadsPage.test.ts`:增加模板结构回归检查,确保六个基础字段、状态选择器和备注区块未被遗漏。
|
||||
- 运行 `yarn test` 和 `yarn build`;再通过本地浏览器检查普通线索、长联系方式、用车线索和窄屏布局。
|
||||
Reference in New Issue
Block a user