ForgeFlow-ERP/docs/superpowers/plans/2026-06-09-erp-frontend-visual-upgrade.md
2026-06-12 16:00:56 +08:00

67 KiB
Raw Blame History

ERP Frontend Visual Upgrade 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: Upgrade the ERP frontend from a demo-like interface to a polished, production-grade industrial operations UI without changing business fields, backend APIs, or core workflows.

Architecture: Build a global visual system first, then migrate high-frequency pages onto it. The upgrade is CSS-first and component-first: shared tokens, shell, icon, button, status, table, drawer, select, and feedback primitives will be improved before page-specific polish. Business logic remains in existing views; new components only change presentation and interaction ergonomics.

Tech Stack: Vue 3, Vite 5, Vue Router, CSS variables in frontend/src/styles/main.css, existing shared Vue components under frontend/src/components, existing utility modules under frontend/src/utils.


Design Skill Map

  • frontend-design: Use the visual direction 工业精密操作台. DFII score: 13. Aesthetic impact 4, context fit 5, feasibility 5, performance safety 4, consistency risk 5. The recognizable anchor is a deep industrial navigation rail plus bright, precise data surfaces with strong status chips.
  • tailwind-patterns: Use Tailwind-style token architecture without adding Tailwind as a dependency. Tokens live as CSS variables: semantic colors, spacing, radius, shadow, z-index, density, motion.
  • iconsax-library: Use a single Iconsax-inspired two-tone/linear icon language. Do not mix random emoji, text symbols, and unrelated icon weights.
  • design-spells: Add restrained micro-interactions: drawer entrance, table row hover rail, status pulse for warning/danger, button press feedback, scroll shadows, focus rings.
  • magic-ui-generator: Use the skill as a design-variation discipline. Before implementing each major primitive, compare 2-3 variants in comments/screenshots and implement the selected variant only. For this ERP, the selected variant should be "industrial precision", not purple SaaS, dark-only, or generic admin.

Non-Negotiable Scope Boundary

  • Do not remove existing form fields.
  • Do not rename business fields unless the user separately requests it.
  • Do not alter backend payloads, validation rules, or workflow sequencing.
  • Do not add a UI library dependency.
  • Do not rewrite large business views purely for aesthetics.
  • Do not leave English status values visible to users.
  • Do not rely on permanent gray explanation blocks when a tooltip, popover, or compact hint can carry the same information.

File Structure

Create:

  • frontend/src/components/AppIcon.vue - shared inline SVG icon renderer using a single Iconsax-inspired style.
  • frontend/src/components/StatusBadge.vue - shared status badge for tables, cards, select labels, and summaries.
  • frontend/src/components/SmartDrawer.vue - enhanced drawer shell for complex business forms.
  • frontend/src/components/FormSection.vue - consistent form section card with optional compact help.
  • frontend/src/components/FieldHint.vue - tooltip/popover style helper text component.
  • frontend/src/components/ActionButton.vue - consistent button primitive with variants.
  • frontend/src/components/MetricStrip.vue - compact summary metrics used in drawers and page headers.
  • frontend/src/utils/statusToneMap.js - Chinese-first status label and tone dictionary.
  • frontend/src/utils/iconMap.js - stable icon names mapped to SVG path data.
  • frontend/src/utils/uiAudit.js - development-only helpers for checking demo-like UI smells.

Modify:

  • frontend/src/styles/main.css - tokens, shell, tables, drawers, form, badges, buttons, motion, responsive rules.
  • frontend/src/main.js - register or preserve global status enhancer depending on migration stage.
  • frontend/src/App.vue - app shell, sidebar, top bar, navigation icons, active state, responsive behavior.
  • frontend/src/components/TableControls.vue - search/filter bar and sort icon behavior.
  • frontend/src/components/DrawerDataTable.vue - readable drawer table layout.
  • frontend/src/components/GlobalSearchableSelect.vue - better long-option select panel styling and metadata layout.
  • frontend/src/components/StockLotTagSelect.vue - match global select/tag behavior.
  • frontend/src/components/StocktakeDialog.vue - align stocktake modal with the new drawer/table/status system.
  • frontend/src/components/OrgMindMap.vue - match system-management visual style, controls, and canvas polish.
  • frontend/src/components/OrgPermissionTree.vue - align tree/table with new status and action components.
  • frontend/src/views/DashboardView.vue - dashboard table and header polish.
  • frontend/src/views/InventoryLedgerView.vue - warehouse operation zone, inventory table, and operation drawers.
  • frontend/src/views/PurchaseOrderView.vue - procurement list and create/edit drawer.
  • frontend/src/views/PurchaseReceiptView.vue - receipt list and drawer.
  • frontend/src/views/QualityInspectionView.vue - pending/completed status visibility.
  • frontend/src/views/SalesPlanningView.vue - customer/order tables, drawer forms, contract action area.
  • frontend/src/views/ProductionLedgerView.vue - production ledger table and detail drawer.
  • frontend/src/views/OperationReportView.vue - date filter, table, empty state, status.
  • frontend/src/views/DeliveryManagementView.vue - grouped delivery ledger table and detail drawer.
  • frontend/src/views/FinanceReportView.vue - large financial spreadsheet mode.
  • frontend/src/views/SystemPermissionView.vue - tab layout, org mode controls, role tables.

Test/Verify:

  • npm run build
  • Browser inspection at http://127.0.0.1:5173/
  • Browser inspection at desktop 1440x900
  • Browser inspection at narrow 390x844
  • Browser screenshots for /, /inventory-ledger, /purchase-orders, /order-list, /production-ledger, /delivery-management, /finance-reports, /system-permissions

Task 1: Baseline UI Audit And Design Tokens

Files:

  • Modify: frontend/src/styles/main.css

  • Create: frontend/src/utils/uiAudit.js

  • Verify: browser screenshots and npm run build

  • Step 1: Capture current baseline screenshots

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

vite v5.x building for production...
✓ built

Use the browser plugin to capture these pages at 1440x900 and 390x844:

http://127.0.0.1:5173/
http://127.0.0.1:5173/inventory-ledger
http://127.0.0.1:5173/purchase-orders
http://127.0.0.1:5173/order-list
http://127.0.0.1:5173/finance-reports
http://127.0.0.1:5173/system-permissions

Expected findings to record in the execution notes:

sidebar squeezes narrow viewport
table sort arrows show as raw text
status display is inconsistent
drawers feel crowded
long select options are hard to scan
permanent helper text takes too much space
  • Step 2: Add development UI audit helper

Create frontend/src/utils/uiAudit.js:

export function collectUiAuditSnapshot(root = document) {
  const normalize = (value) => String(value || "").trim().replace(/\s+/g, " ");
  const tables = Array.from(root.querySelectorAll("table")).map((table, index) => {
    const rect = table.getBoundingClientRect();
    const wrap = table.closest(".table-wrap, .drawer-data-table-wrap, .finance-sheet-wrap") || table.parentElement;
    const wrapRect = wrap?.getBoundingClientRect();
    return {
      index,
      width: Math.round(rect.width),
      wrapWidth: Math.round(wrapRect?.width || rect.width),
      hasRawSortText: Array.from(table.querySelectorAll("th")).some((th) => normalize(th.textContent).includes("↕")),
      headerCount: table.querySelectorAll("th").length
    };
  });

  const visibleEnglishStatuses = Array.from(root.querySelectorAll("td, option, span, button"))
    .map((element) => normalize(element.textContent))
    .filter((text) => /\b(ACTIVE|INACTIVE|PENDING|LOCKED|OPEN|CLOSED|DONE|PASS|REJECT|SETTLED)\b/.test(text));

  const longHelperBlocks = Array.from(root.querySelectorAll("small, .workflow-tip, .feedback-banner"))
    .map((element) => normalize(element.textContent))
    .filter((text) => text.length > 48);

  return {
    path: window.location.pathname,
    viewport: { width: window.innerWidth, height: window.innerHeight },
    tables,
    visibleEnglishStatuses,
    longHelperBlocks
  };
}

export function printUiAuditSnapshot(root = document) {
  const snapshot = collectUiAuditSnapshot(root);
  console.table(snapshot.tables);
  console.log("visibleEnglishStatuses", snapshot.visibleEnglishStatuses);
  console.log("longHelperBlocks", snapshot.longHelperBlocks);
  return snapshot;
}
  • Step 3: Add semantic design tokens

Append a new token section near the top of frontend/src/styles/main.css, after the existing :root block. Keep old variables during migration so existing pages do not break.

:root {
  --ui-font-display: "Bahnschrift", "DIN Alternate", "Segoe UI", sans-serif;
  --ui-font-body: "Microsoft YaHei UI", "PingFang SC", "Noto Sans CJK SC", sans-serif;
  --ui-bg-canvas: #eef3f7;
  --ui-bg-panel: rgba(255, 255, 255, 0.92);
  --ui-bg-panel-solid: #ffffff;
  --ui-bg-subtle: #f6f8fb;
  --ui-ink: #15202b;
  --ui-ink-soft: #4f6275;
  --ui-ink-muted: #7b8da0;
  --ui-line: rgba(58, 82, 105, 0.14);
  --ui-line-strong: rgba(58, 82, 105, 0.24);
  --ui-steel: #2563eb;
  --ui-steel-soft: #dbeafe;
  --ui-cyan: #0ea5e9;
  --ui-green: #16a34a;
  --ui-amber: #d97706;
  --ui-orange: #ea580c;
  --ui-red: #dc2626;
  --ui-gray: #64748b;
  --ui-radius-sm: 8px;
  --ui-radius-md: 12px;
  --ui-radius-lg: 18px;
  --ui-radius-xl: 24px;
  --ui-shadow-card: 0 16px 42px rgba(15, 23, 42, 0.08);
  --ui-shadow-float: 0 28px 80px rgba(15, 23, 42, 0.18);
  --ui-focus: 0 0 0 3px rgba(37, 99, 235, 0.18);
  --ui-transition-fast: 140ms ease;
  --ui-transition-normal: 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
  --ui-sidebar-width: 220px;
  --ui-sidebar-collapsed-width: 76px;
}
  • Step 4: Add base application surface styles

Append to frontend/src/styles/main.css:

body {
  font-family: var(--ui-font-body);
  color: var(--ui-ink);
  background:
    radial-gradient(circle at 16% 8%, rgba(37, 99, 235, 0.12), transparent 28%),
    linear-gradient(135deg, #eef3f7 0%, #f8fafc 48%, #e8eef5 100%);
}

button,
input,
select,
textarea {
  font: inherit;
}

button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
a:focus-visible {
  outline: none;
  box-shadow: var(--ui-focus);
}
  • Step 5: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser check:

Open http://127.0.0.1:5173/
Confirm no layout regression from token addition.
  • Step 6: Commit

If the executor is inside a git worktree:

git add frontend/src/styles/main.css frontend/src/utils/uiAudit.js
git commit -m "style: add frontend visual design tokens"

If the current directory has no .git, record this in the execution notes:

Commit skipped because current workspace is not a git repository.

Task 2: Iconsax-Inspired Icon System

Files:

  • Create: frontend/src/utils/iconMap.js

  • Create: frontend/src/components/AppIcon.vue

  • Modify: frontend/src/App.vue

  • Modify: frontend/src/styles/main.css

  • Step 1: Create icon map

Create frontend/src/utils/iconMap.js:

export const ICON_PATHS = {
  dashboard: [
    "M4 13h6v7H4z",
    "M14 4h6v16h-6z",
    "M4 4h6v5H4z"
  ],
  masterData: [
    "M5 5h14v4H5z",
    "M5 15h14v4H5z",
    "M8 9v6",
    "M16 9v6"
  ],
  order: [
    "M7 3h8l4 4v14H7z",
    "M15 3v5h5",
    "M9 12h8",
    "M9 16h6"
  ],
  warehouse: [
    "M4 9l8-5 8 5-8 5z",
    "M4 9v7l8 5 8-5V9",
    "M12 14v7"
  ],
  production: [
    "M12 3v3",
    "M12 18v3",
    "M4.8 7.2l2.1 2.1",
    "M17.1 16.7l2.1 2.1",
    "M3 12h3",
    "M18 12h3",
    "M7.2 19.2l2.1-2.1",
    "M16.7 6.9l2.1-2.1",
    "M8 12a4 4 0 1 0 8 0 4 4 0 0 0-8 0z"
  ],
  delivery: [
    "M3 7h11v10H3z",
    "M14 10h4l3 3v4h-7z",
    "M6.5 19a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z",
    "M17.5 19a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"
  ],
  finance: [
    "M4 19V5",
    "M4 19h16",
    "M8 15l3-4 3 2 5-7"
  ],
  system: [
    "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
    "M19.4 15a8 8 0 0 0 .1-5.8l-2.2.4a6.4 6.4 0 0 0-1.1-1.9l1.2-1.9a8 8 0 0 0-5.1-2.9l-.8 2.1a6.8 6.8 0 0 0-2.2 0l-.8-2.1a8 8 0 0 0-5.1 2.9l1.2 1.9a6.4 6.4 0 0 0-1.1 1.9l-2.2-.4a8 8 0 0 0 .1 5.8l2.1-.6a6.6 6.6 0 0 0 1.2 1.7l-1 2a8 8 0 0 0 5.1 2.8l.7-2.1a6.7 6.7 0 0 0 2.4 0l.7 2.1a8 8 0 0 0 5.1-2.8l-1-2a6.6 6.6 0 0 0 1.2-1.7z"
  ],
  plus: ["M12 5v14", "M5 12h14"],
  minus: ["M5 12h14"],
  refresh: ["M20 6v5h-5", "M4 18v-5h5", "M18.5 9A7 7 0 0 0 6.8 6.8L4 11", "M5.5 15a7 7 0 0 0 11.7 2.2L20 13"],
  ledger: ["M6 4h12v16H6z", "M9 8h6", "M9 12h6", "M9 16h4"],
  stocktake: ["M5 5h14v14H5z", "M8 9h8", "M8 13h5", "M8 17h3"],
  close: ["M6 6l12 12", "M18 6L6 18"],
  search: ["M10.5 18a7.5 7.5 0 1 0 0-15 7.5 7.5 0 0 0 0 15z", "M16 16l5 5"],
  warning: ["M12 4l9 16H3z", "M12 9v5", "M12 17h.01"]
};
  • Step 2: Create shared icon component

Create frontend/src/components/AppIcon.vue:

<template>
  <svg class="app-icon" :class="[`app-icon-${tone}`]" viewBox="0 0 24 24" aria-hidden="true">
    <path
      v-for="path in paths"
      :key="path"
      :d="path"
      fill="none"
      stroke="currentColor"
      stroke-linecap="round"
      stroke-linejoin="round"
      :stroke-width="strokeWidth"
    />
  </svg>
</template>

<script setup>
import { computed } from "vue";
import { ICON_PATHS } from "../utils/iconMap";

const props = defineProps({
  name: {
    type: String,
    required: true
  },
  tone: {
    type: String,
    default: "default"
  },
  strokeWidth: {
    type: [Number, String],
    default: 1.8
  }
});

const paths = computed(() => ICON_PATHS[props.name] || ICON_PATHS.dashboard);
</script>
  • Step 3: Add icon styles

Append to frontend/src/styles/main.css:

.app-icon {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  color: currentColor;
}

.app-icon-muted {
  color: var(--ui-ink-muted);
}

.app-icon-primary {
  color: var(--ui-steel);
}

.app-icon-success {
  color: var(--ui-green);
}

.app-icon-warning {
  color: var(--ui-amber);
}

.app-icon-danger {
  color: var(--ui-red);
}
  • Step 4: Replace sidebar raw icon output in App.vue

In frontend/src/App.vue, import the component:

import AppIcon from "./components/AppIcon.vue";

Update the navigation item data to carry icon names:

const navStages = [
  { key: "dashboard", icon: "dashboard", label: "经营总览" },
  { key: "master-data", icon: "masterData", label: "基础资料" },
  { key: "orders", icon: "order", label: "订单与计划" },
  { key: "procurement", icon: "warehouse", label: "采购与库存" },
  { key: "production", icon: "production", label: "生产执行" },
  { key: "delivery", icon: "delivery", label: "发货概览" },
  { key: "finance", icon: "finance", label: "财务分析" },
  { key: "system", icon: "system", label: "系统管理" }
];

Where nav buttons render their icon, replace text/emoji icon output with:

<AppIcon :name="stage.icon" />
  • Step 5: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser check:

Open http://127.0.0.1:5173/
Confirm all sidebar main menu icons render with one consistent line style.
Confirm no duplicate-looking menu icons remain in the main sidebar.
  • Step 6: Commit
git add frontend/src/utils/iconMap.js frontend/src/components/AppIcon.vue frontend/src/App.vue frontend/src/styles/main.css
git commit -m "style: unify app icon system"

If no .git directory exists, record:

Commit skipped because current workspace is not a git repository.

Task 3: Production-Grade App Shell And Responsive Sidebar

Files:

  • Modify: frontend/src/App.vue

  • Modify: frontend/src/styles/main.css

  • Step 1: Add app shell state

In frontend/src/App.vue, ensure the shell has a collapsed state that can be toggled and also activated by narrow viewport.

Use this script shape:

import { computed, onMounted, onBeforeUnmount, ref } from "vue";

const sidebarCollapsed = ref(false);
const narrowShell = ref(false);

function updateShellViewportState() {
  narrowShell.value = window.innerWidth < 1024;
  if (narrowShell.value) {
    sidebarCollapsed.value = true;
  }
}

function toggleSidebar() {
  sidebarCollapsed.value = !sidebarCollapsed.value;
}

onMounted(() => {
  updateShellViewportState();
  window.addEventListener("resize", updateShellViewportState);
});

onBeforeUnmount(() => {
  window.removeEventListener("resize", updateShellViewportState);
});

const shellClass = computed(() => ({
  "app-shell-collapsed": sidebarCollapsed.value,
  "app-shell-narrow": narrowShell.value
}));

Apply :class="shellClass" to the outer authenticated shell container.

  • Step 2: Replace permanent long nav descriptions

Keep the existing title attributes for accessibility, but do not render long descriptions as visible button text. The button visible text must be only the module name:

<button class="nav-stage" :title="stage.description" type="button">
  <AppIcon :name="stage.icon" />
  <span class="nav-stage-label">{{ stage.label }}</span>
  <span v-if="stage.children?.length" class="nav-stage-caret"></span>
</button>
  • Step 3: Add shell CSS

Append to frontend/src/styles/main.css:

.app-shell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: var(--ui-sidebar-width) minmax(0, 1fr);
  background: var(--ui-bg-canvas);
}

.app-shell.app-shell-collapsed {
  grid-template-columns: var(--ui-sidebar-collapsed-width) minmax(0, 1fr);
}

.app-sidebar {
  width: var(--ui-sidebar-width);
  min-width: 0;
  background:
    linear-gradient(180deg, rgba(22, 34, 49, 0.96), rgba(17, 30, 45, 0.98)),
    radial-gradient(circle at 20% 0%, rgba(37, 99, 235, 0.25), transparent 32%);
  border-right: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 18px 0 50px rgba(15, 23, 42, 0.16);
}

.app-shell-collapsed .app-sidebar {
  width: var(--ui-sidebar-collapsed-width);
}

.nav-stage {
  position: relative;
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr) 16px;
  align-items: center;
  gap: 12px;
  min-height: 46px;
  padding: 0 14px;
  border-radius: 14px;
  color: rgba(226, 237, 248, 0.78);
  transition: background var(--ui-transition-fast), color var(--ui-transition-fast), transform var(--ui-transition-fast);
}

.nav-stage:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.08);
  transform: translateX(2px);
}

.nav-stage-active {
  color: #fff;
  background: linear-gradient(90deg, rgba(37, 99, 235, 0.34), rgba(14, 165, 233, 0.12));
  box-shadow: inset 3px 0 0 var(--ui-cyan);
}

.app-shell-collapsed .nav-stage {
  grid-template-columns: 24px;
  justify-content: center;
  padding: 0;
}

.app-shell-collapsed .nav-stage-label,
.app-shell-collapsed .nav-stage-caret {
  display: none;
}

.app-main {
  min-width: 0;
  overflow: hidden;
}

@media (max-width: 768px) {
  .app-shell {
    grid-template-columns: var(--ui-sidebar-collapsed-width) minmax(0, 1fr);
  }

  .app-main {
    min-width: 0;
  }
}

If existing classes use different names, map these rules to the actual shell classes in App.vue rather than duplicating wrappers.

  • Step 4: Verify narrow viewport

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser checks:

At 1440x900, sidebar shows labels and content has normal width.
At 390x844, sidebar collapses to icons and content is not squeezed into a narrow strip.
Top bar remains usable.
  • Step 5: Commit
git add frontend/src/App.vue frontend/src/styles/main.css
git commit -m "style: polish app shell and responsive sidebar"

Task 4: Button, Action, And Helper Text System

Files:

  • Create: frontend/src/components/ActionButton.vue

  • Create: frontend/src/components/FieldHint.vue

  • Modify: frontend/src/styles/main.css

  • Modify: frontend/src/components/FormDrawer.vue

  • Modify: selected high-frequency views when migrating buttons

  • Step 1: Create shared action button

Create frontend/src/components/ActionButton.vue:

<template>
  <button
    class="action-button"
    :class="[`action-button-${variant}`, `action-button-${size}`, { 'action-button-icon-only': iconOnly }]"
    :type="type"
    :disabled="disabled || loading"
  >
    <AppIcon v-if="icon" :name="icon" :tone="iconTone" />
    <span v-if="!iconOnly"><slot /></span>
    <span v-if="loading" class="action-button-spinner" aria-hidden="true"></span>
  </button>
</template>

<script setup>
import { computed } from "vue";
import AppIcon from "./AppIcon.vue";

const props = defineProps({
  icon: { type: String, default: "" },
  variant: { type: String, default: "secondary" },
  size: { type: String, default: "md" },
  type: { type: String, default: "button" },
  disabled: { type: Boolean, default: false },
  loading: { type: Boolean, default: false },
  iconOnly: { type: Boolean, default: false }
});

const iconTone = computed(() => {
  if (props.variant === "danger") return "danger";
  if (props.variant === "primary") return "primary";
  if (props.variant === "success") return "success";
  if (props.variant === "warning") return "warning";
  return "muted";
});
</script>
  • Step 2: Create field hint component

Create frontend/src/components/FieldHint.vue:

<template>
  <span class="field-hint" tabindex="0">
    <AppIcon name="warning" stroke-width="1.6" />
    <span class="field-hint-popover">{{ text }}</span>
  </span>
</template>

<script setup>
import AppIcon from "./AppIcon.vue";

defineProps({
  text: {
    type: String,
    required: true
  }
});
</script>
  • Step 3: Add button and helper styles

Append to frontend/src/styles/main.css:

.action-button,
.primary-button,
.ghost-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 38px;
  border-radius: 12px;
  border: 1px solid transparent;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: transform var(--ui-transition-fast), box-shadow var(--ui-transition-fast), background var(--ui-transition-fast), border-color var(--ui-transition-fast);
}

.action-button:hover,
.primary-button:hover,
.ghost-button:hover {
  transform: translateY(-1px);
}

.action-button:active,
.primary-button:active,
.ghost-button:active {
  transform: translateY(0);
}

.action-button-primary,
.primary-button {
  color: #fff;
  background: linear-gradient(135deg, var(--ui-steel), var(--ui-cyan));
  box-shadow: 0 12px 28px rgba(37, 99, 235, 0.22);
}

.action-button-secondary,
.ghost-button {
  color: var(--ui-ink-soft);
  background: rgba(255, 255, 255, 0.86);
  border-color: var(--ui-line);
}

.action-button-danger,
.danger-ghost {
  color: var(--ui-red);
  background: rgba(254, 242, 242, 0.92);
  border-color: rgba(220, 38, 38, 0.22);
}

.action-button-sm {
  min-height: 32px;
  padding: 0 10px;
  font-size: 12px;
}

.action-button-md {
  padding: 0 14px;
  font-size: 13px;
}

.action-button-lg {
  min-height: 44px;
  padding: 0 18px;
  font-size: 14px;
}

.field-hint {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-left: 6px;
  color: var(--ui-ink-muted);
  cursor: help;
}

.field-hint-popover {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 10px);
  z-index: 30;
  width: max-content;
  max-width: 280px;
  transform: translateX(-50%) translateY(4px);
  opacity: 0;
  pointer-events: none;
  padding: 10px 12px;
  border-radius: 12px;
  color: #fff;
  background: rgba(15, 23, 42, 0.92);
  box-shadow: var(--ui-shadow-float);
  font-size: 12px;
  line-height: 1.5;
  transition: opacity var(--ui-transition-fast), transform var(--ui-transition-fast);
}

.field-hint:hover .field-hint-popover,
.field-hint:focus-visible .field-hint-popover {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
  • Step 4: Update drawer header close button

Modify frontend/src/components/FormDrawer.vue:

<button class="ghost-button icon-button" type="button" aria-label="关闭" @click="emit('close')">
  <AppIcon name="close" />
</button>

Import:

import AppIcon from "./AppIcon.vue";
  • Step 5: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser checks:

Open a purchase order drawer.
Close button renders fully and is not clipped.
Primary and secondary actions have consistent height and radius.
Helper hints appear on hover/focus instead of occupying permanent wide blocks.
  • Step 6: Commit
git add frontend/src/components/ActionButton.vue frontend/src/components/FieldHint.vue frontend/src/components/FormDrawer.vue frontend/src/styles/main.css
git commit -m "style: add shared action and hint components"

Task 5: Chinese-First Status System

Files:

  • Create: frontend/src/utils/statusToneMap.js

  • Create: frontend/src/components/StatusBadge.vue

  • Modify: frontend/src/utils/statusBadges.js

  • Modify: frontend/src/styles/main.css

  • Modify: selected views to use StatusBadge

  • Step 1: Create status tone dictionary

Create frontend/src/utils/statusToneMap.js:

export const STATUS_LABELS = {
  ACTIVE: "启用",
  INACTIVE: "停用",
  CREATED: "已创建",
  RELEASED: "已下达",
  ORDERED: "已下单",
  IN_PROGRESS: "进行中",
  PARTIAL: "部分完成",
  PENDING: "待处理",
  PENDING_QC: "待质检",
  OPEN: "进行中",
  LOCKED: "锁定",
  PAUSED: "暂停",
  READY_TO_RECEIPT: "待成品入库",
  HAS_RETURN: "存在退货",
  PASSED: "已放行",
  PASS: "已放行",
  COMPLETED: "已完成",
  SETTLED: "已结单",
  DONE: "已完成",
  CONFIRMED: "已确认",
  POSTED: "已过账",
  RECEIVED: "已收货",
  REJECTED: "已拒收",
  REJECT: "已拒收",
  SCRAP: "已报废",
  DEPLETED: "已耗尽",
  CLOSED: "已关闭"
};

export const STATUS_TONE_GROUPS = {
  success: ["启用", "正常", "成功", "可用", "已放行", "放行", "已完成", "已完工", "已结单", "已确认", "已过账", "已收货", "已入库", "已审核", "已同步", "已匹配"],
  primary: ["已创建", "已下达", "已下单", "进行中", "部分完成", "小程序清单"],
  warning: ["待处理", "待质检", "待放行", "未开始", "锁定", "锁库", "锁单", "暂停", "返工", "返工中", "待成品入库", "待审核", "待同步", "未同步", "未匹配", "待匹配", "未关联"],
  orange: ["存在退货", "快到期", "即将到期", "超预警"],
  danger: ["停用", "异常", "失败", "报废", "已报废", "拒收", "已拒收", "已耗尽", "已关闭", "已替代", "已覆盖", "已过期", "已驳回", "驳回", "审核失败", "同步失败", "匹配失败"],
  neutral: ["草稿", "无数据", "未维护", "历史记录"]
};

export function normalizeStatusValue(value) {
  return String(value || "").replace(/[:]/g, "").replace(/\s+/g, "").trim();
}

export function getStatusLabel(value) {
  const raw = String(value || "").trim();
  if (!raw || raw === "-") return "-";
  return STATUS_LABELS[raw.toUpperCase()] || raw;
}

export function getStatusTone(value) {
  const label = normalizeStatusValue(getStatusLabel(value));
  if (!label || label === "-") return "neutral";
  const matched = Object.entries(STATUS_TONE_GROUPS).find(([, values]) =>
    values.some((item) => normalizeStatusValue(item) === label)
  );
  return matched?.[0] || "info";
}
  • Step 2: Create status badge component

Create frontend/src/components/StatusBadge.vue:

<template>
  <span class="status-badge" :class="`status-badge-${tone}`">
    <span class="status-badge-dot" aria-hidden="true"></span>
    <span>{{ label }}</span>
  </span>
</template>

<script setup>
import { computed } from "vue";
import { getStatusLabel, getStatusTone } from "../utils/statusToneMap";

const props = defineProps({
  value: {
    type: [String, Number, Boolean],
    default: ""
  },
  text: {
    type: String,
    default: ""
  }
});

const label = computed(() => props.text || getStatusLabel(props.value));
const tone = computed(() => getStatusTone(label.value));
</script>
  • Step 3: Add badge styles

Append to frontend/src/styles/main.css:

.status-badge,
.status-badge-auto {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 24px;
  max-width: 100%;
  padding: 0 10px;
  border-radius: 999px;
  border: 1px solid transparent;
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
}

.status-badge-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 14%, transparent);
}

.status-badge-success,
.status-tone-success {
  color: var(--ui-green);
  background: rgba(22, 163, 74, 0.1);
  border-color: rgba(22, 163, 74, 0.22);
}

.status-badge-primary,
.status-tone-primary {
  color: var(--ui-steel);
  background: rgba(37, 99, 235, 0.1);
  border-color: rgba(37, 99, 235, 0.22);
}

.status-badge-warning,
.status-tone-warning {
  color: var(--ui-amber);
  background: rgba(217, 119, 6, 0.11);
  border-color: rgba(217, 119, 6, 0.24);
}

.status-badge-orange,
.status-tone-orange {
  color: var(--ui-orange);
  background: rgba(234, 88, 12, 0.1);
  border-color: rgba(234, 88, 12, 0.24);
}

.status-badge-danger,
.status-tone-danger {
  color: var(--ui-red);
  background: rgba(220, 38, 38, 0.1);
  border-color: rgba(220, 38, 38, 0.24);
}

.status-badge-neutral,
.status-tone-neutral,
.status-badge-info,
.status-tone-info {
  color: var(--ui-gray);
  background: rgba(100, 116, 139, 0.1);
  border-color: rgba(100, 116, 139, 0.2);
}
  • Step 4: Refactor DOM status enhancer to use dictionary

Modify frontend/src/utils/statusBadges.js so it imports the new dictionary:

import { getStatusLabel, getStatusTone, normalizeStatusValue } from "./statusToneMap";

Where the old code displays text, ensure English status is translated:

function markStatusElement(element, value) {
  const label = getStatusLabel(value);
  const tone = getStatusTone(label);
  const toneClass = tone ? `${STATUS_TONE_CLASS_PREFIX}${tone}` : "";
  clearStatusToneClasses(element, toneClass);
  element.classList.toggle("status-badge-auto", Boolean(tone));
  element.classList.toggle("status-value-cell", element.tagName === "TD" && Boolean(tone));
  if (label && label !== element.textContent.trim()) {
    element.textContent = label;
  }
  if (tone) {
    element.classList.add(toneClass);
  }
}
  • Step 5: Replace obvious inline status cells

Start with these views:

frontend/src/views/ProductionLedgerView.vue
frontend/src/views/DeliveryManagementView.vue
frontend/src/views/QualityInspectionView.vue
frontend/src/views/SystemPermissionView.vue

For each status cell, replace:

<td>{{ row.status }}</td>

with:

<td><StatusBadge :value="row.status" /></td>

Import:

import StatusBadge from "../components/StatusBadge.vue";
  • Step 6: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser checks:

Open /purchase-orders and confirm English statuses are not visible.
Open /production-ledger and confirm 锁单 uses a warning/amber badge consistently.
Open /delivery-management and confirm 已过账 uses a success badge consistently.
Open /system-permissions and confirm 启用/停用 have different colors.
  • Step 7: Commit
git add frontend/src/utils/statusToneMap.js frontend/src/components/StatusBadge.vue frontend/src/utils/statusBadges.js frontend/src/styles/main.css frontend/src/views/ProductionLedgerView.vue frontend/src/views/DeliveryManagementView.vue frontend/src/views/QualityInspectionView.vue frontend/src/views/SystemPermissionView.vue
git commit -m "style: unify Chinese status badges"

Task 6: Data Table System Upgrade

Files:

  • Modify: frontend/src/components/TableControls.vue

  • Modify: frontend/src/components/DrawerDataTable.vue

  • Modify: frontend/src/styles/main.css

  • Modify: high-frequency views using .data-table

  • Step 1: Replace raw sort arrow text

In frontend/src/components/TableControls.vue, update refreshHeaderStates():

if (arrow) {
  arrow.innerHTML = isActive
    ? (props.sortDirection === "asc" ? "↑" : "↓")
    : "";
  arrow.setAttribute("aria-label", isActive ? (props.sortDirection === "asc" ? "升序" : "降序") : "可排序");
  arrow.classList.toggle("sort-arrow-active", isActive);
}

When creating the arrow, use:

const arrow = document.createElement("span");
arrow.className = "sort-arrow";
arrow.setAttribute("aria-hidden", "true");
arrow.textContent = "";
  • Step 2: Add professional table CSS

Append to frontend/src/styles/main.css:

.table-wrap,
.drawer-data-table-wrap {
  position: relative;
  overflow: auto;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-bg-panel-solid);
  box-shadow: var(--ui-shadow-card);
}

.table-wrap::after,
.drawer-data-table-wrap::after {
  content: "";
  position: sticky;
  right: 0;
  top: 0;
  float: right;
  width: 28px;
  height: 1px;
  pointer-events: none;
  box-shadow: -18px 0 24px rgba(15, 23, 42, 0.08);
}

.data-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  table-layout: auto;
  color: var(--ui-ink);
}

.data-table th {
  position: sticky;
  top: 0;
  z-index: 2;
  height: 42px;
  padding: 0 14px;
  color: var(--ui-ink-soft);
  background: linear-gradient(180deg, #f8fafc, #eef3f8);
  border-bottom: 1px solid var(--ui-line-strong);
  font-size: 12px;
  font-weight: 900;
  text-align: left;
  white-space: nowrap;
}

.data-table td {
  height: 44px;
  padding: 0 14px;
  border-bottom: 1px solid rgba(58, 82, 105, 0.08);
  color: var(--ui-ink);
  font-size: 13px;
  vertical-align: middle;
}

.data-table tbody tr {
  transition: background var(--ui-transition-fast), box-shadow var(--ui-transition-fast);
}

.data-table tbody tr:hover td {
  background: rgba(37, 99, 235, 0.045);
}

.sortable-th-button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  border: 0;
  padding: 0;
  color: inherit;
  background: transparent;
  font-weight: inherit;
  cursor: pointer;
}

.sort-arrow {
  display: inline-grid;
  place-items: center;
  width: 16px;
  height: 16px;
  border-radius: 6px;
  color: var(--ui-ink-muted);
  background: rgba(100, 116, 139, 0.08);
}

.sort-arrow:empty::before {
  content: "";
  width: 8px;
  height: 8px;
  border: 1.5px solid currentColor;
  border-left: 0;
  border-bottom: 0;
  transform: rotate(135deg);
  opacity: 0.42;
}

.sortable-th-active .sort-arrow {
  color: #fff;
  background: var(--ui-steel);
}
  • Step 3: Add table density rules

Append:

.compact-table th {
  height: 38px;
}

.compact-table td {
  height: 40px;
  font-size: 12.5px;
}

.data-table .link-button,
.data-table .strong-link {
  color: var(--ui-steel);
  font-weight: 800;
  text-decoration: none;
}

.data-table .link-button:hover,
.data-table .strong-link:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.empty-row {
  height: 96px;
  color: var(--ui-ink-muted);
  text-align: center;
  background: linear-gradient(135deg, rgba(248, 250, 252, 0.9), rgba(241, 245, 249, 0.9));
}
  • Step 4: Improve drawer table compact labels

Modify frontend/src/components/DrawerDataTable.vue header cell:

<th v-for="column in visibleColumns" :key="column.key" :title="column.title || column.label">
  <span class="drawer-table-head-label">{{ column.compactLabel || column.label }}</span>
</th>

Ensure CSS:

.drawer-table-head-label {
  display: inline-block;
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

.drawer-cell-ellipsis {
  display: inline-block;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}
  • Step 5: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser checks:

Open / and confirm no visible ↕ remains in table headers.
Open /purchase-orders and confirm table remains readable.
Open /finance-reports and confirm large table still scrolls horizontally but has cleaner header treatment.
Open a detail drawer with drawer table and confirm headers do not turn vertical or wrap awkwardly.
  • Step 6: Commit
git add frontend/src/components/TableControls.vue frontend/src/components/DrawerDataTable.vue frontend/src/styles/main.css
git commit -m "style: upgrade data table readability"

Task 7: Smart Drawer And Form Section System

Files:

  • Create: frontend/src/components/SmartDrawer.vue

  • Create: frontend/src/components/FormSection.vue

  • Create: frontend/src/components/MetricStrip.vue

  • Modify: frontend/src/components/FormDrawer.vue

  • Modify: frontend/src/styles/main.css

  • Step 1: Create SmartDrawer

Create frontend/src/components/SmartDrawer.vue:

<template>
  <Teleport to="body">
    <div v-if="open" class="smart-drawer-mask" @click.self="emit('close')">
      <aside class="smart-drawer" :class="[`smart-drawer-${size}`]">
        <header class="smart-drawer-head">
          <div class="smart-drawer-title-block">
            <p v-if="eyebrow" class="eyebrow">{{ eyebrow }}</p>
            <h3>{{ title }}</h3>
            <p v-if="description" class="smart-drawer-description">{{ description }}</p>
          </div>
          <div class="smart-drawer-actions">
            <slot name="head-actions" />
            <button class="ghost-button icon-button" type="button" aria-label="关闭" @click="emit('close')">
              <AppIcon name="close" />
            </button>
          </div>
        </header>
        <div class="smart-drawer-body">
          <slot />
        </div>
        <footer v-if="$slots.footer" class="smart-drawer-footer">
          <slot name="footer" />
        </footer>
      </aside>
    </div>
  </Teleport>
</template>

<script setup>
import { toRef } from "vue";
import AppIcon from "./AppIcon.vue";
import { useBodyScrollLock } from "../composables/useBodyScrollLock";

const props = defineProps({
  open: { type: Boolean, default: false },
  eyebrow: { type: String, default: "" },
  title: { type: String, required: true },
  description: { type: String, default: "" },
  size: { type: String, default: "lg" }
});

const emit = defineEmits(["close"]);

useBodyScrollLock(toRef(props, "open"));
</script>
  • Step 2: Create FormSection

Create frontend/src/components/FormSection.vue:

<template>
  <section class="form-section">
    <header v-if="title || description || $slots.actions" class="form-section-head">
      <div>
        <h4 v-if="title">{{ title }}</h4>
        <p v-if="description">{{ description }}</p>
      </div>
      <div v-if="$slots.actions" class="form-section-actions">
        <slot name="actions" />
      </div>
    </header>
    <div class="form-section-grid" :class="`form-section-grid-${columns}`">
      <slot />
    </div>
  </section>
</template>

<script setup>
defineProps({
  title: { type: String, default: "" },
  description: { type: String, default: "" },
  columns: { type: Number, default: 2 }
});
</script>
  • Step 3: Create MetricStrip

Create frontend/src/components/MetricStrip.vue:

<template>
  <div class="metric-strip">
    <article v-for="metric in metrics" :key="metric.label" class="metric-strip-item">
      <span>{{ metric.label }}</span>
      <strong>{{ metric.value }}</strong>
      <small v-if="metric.hint">{{ metric.hint }}</small>
    </article>
  </div>
</template>

<script setup>
defineProps({
  metrics: {
    type: Array,
    default: () => []
  }
});
</script>
  • Step 4: Add drawer and form CSS

Append to frontend/src/styles/main.css:

.smart-drawer-mask {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  justify-content: flex-end;
  padding: 14px;
  background: rgba(15, 23, 42, 0.34);
  backdrop-filter: blur(8px);
}

.smart-drawer {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  width: min(1080px, calc(100vw - 28px));
  height: calc(100vh - 28px);
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.66);
  border-radius: 24px;
  background: rgba(248, 250, 252, 0.96);
  box-shadow: var(--ui-shadow-float);
  animation: smartDrawerIn var(--ui-transition-normal);
}

.smart-drawer-xl {
  width: min(1280px, calc(100vw - 28px));
}

.smart-drawer-fullscreen {
  width: calc(100vw - 28px);
}

@keyframes smartDrawerIn {
  from {
    opacity: 0;
    transform: translateX(18px) scale(0.99);
  }
  to {
    opacity: 1;
    transform: translateX(0) scale(1);
  }
}

.smart-drawer-head,
.smart-drawer-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 22px;
  background: rgba(255, 255, 255, 0.86);
  border-bottom: 1px solid var(--ui-line);
}

.smart-drawer-footer {
  justify-content: flex-end;
  border-top: 1px solid var(--ui-line);
  border-bottom: 0;
  box-shadow: 0 -12px 28px rgba(15, 23, 42, 0.06);
}

.smart-drawer-body {
  min-height: 0;
  overflow: auto;
  padding: 20px 22px;
}

.smart-drawer-title-block h3 {
  margin: 0;
  color: var(--ui-ink);
  font-size: 22px;
  letter-spacing: -0.02em;
}

.smart-drawer-description {
  max-width: 760px;
  margin: 6px 0 0;
  color: var(--ui-ink-muted);
  font-size: 13px;
  line-height: 1.5;
}

.form-section {
  padding: 18px;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius-lg);
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
}

.form-section + .form-section {
  margin-top: 16px;
}

.form-section-head {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}

.form-section-head h4 {
  margin: 0;
  font-size: 15px;
  color: var(--ui-ink);
}

.form-section-head p {
  margin: 4px 0 0;
  color: var(--ui-ink-muted);
  font-size: 12px;
}

.form-section-grid {
  display: grid;
  gap: 14px;
}

.form-section-grid-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.form-section-grid-3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.form-section label {
  display: grid;
  gap: 7px;
  color: var(--ui-ink-soft);
  font-size: 12px;
  font-weight: 800;
}

.form-section input,
.form-section select,
.form-section textarea {
  min-height: 38px;
  width: 100%;
  border: 1px solid var(--ui-line);
  border-radius: 12px;
  padding: 0 12px;
  color: var(--ui-ink);
  background: rgba(255, 255, 255, 0.96);
}

.form-section textarea {
  min-height: 84px;
  padding: 10px 12px;
  resize: vertical;
}

.metric-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px;
}

.metric-strip-item {
  padding: 12px;
  border: 1px solid var(--ui-line);
  border-radius: 14px;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(241, 245, 249, 0.9));
}

.metric-strip-item span,
.metric-strip-item small {
  display: block;
  color: var(--ui-ink-muted);
  font-size: 12px;
}

.metric-strip-item strong {
  display: block;
  margin-top: 4px;
  color: var(--ui-ink);
  font-size: 18px;
}

@media (max-width: 768px) {
  .smart-drawer-mask {
    padding: 0;
  }

  .smart-drawer {
    width: 100vw;
    height: 100vh;
    border-radius: 0;
  }

  .form-section-grid-2,
  .form-section-grid-3 {
    grid-template-columns: 1fr;
  }
}
  • Step 5: Keep FormDrawer backward compatible

Do not replace all drawers at once. Update FormDrawer.vue only with improved header and body classes. Future page tasks will migrate complex drawers to SmartDrawer.

  • Step 6: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser checks:

Open existing drawers and confirm no drawer became invisible.
At 390x844, drawer becomes full-screen instead of a narrow broken side panel.
At 1440x900, drawer has stable width and fixed action area.
  • Step 7: Commit
git add frontend/src/components/SmartDrawer.vue frontend/src/components/FormSection.vue frontend/src/components/MetricStrip.vue frontend/src/components/FormDrawer.vue frontend/src/styles/main.css
git commit -m "style: add smart drawer form system"

Task 8: Searchable Select And Long Option Upgrade

Files:

  • Modify: frontend/src/components/GlobalSearchableSelect.vue

  • Modify: frontend/src/components/StockLotTagSelect.vue

  • Modify: frontend/src/styles/main.css

  • Step 1: Parse option metadata for display

In frontend/src/components/GlobalSearchableSelect.vue, replace collectOptions with metadata-aware parsing:

function splitOptionLabel(label) {
  const parts = cleanOptionLabel(label).split(" · ").filter(Boolean);
  return {
    title: parts[0] || label,
    subtitle: parts.slice(1, 3).join(" · "),
    meta: parts.slice(3).join(" · ")
  };
}

function collectOptions(selectEl) {
  return Array.from(selectEl.options || []).map((option, index) => {
    const label = cleanOptionLabel(option.textContent);
    return {
      index,
      value: option.value,
      label,
      display: splitOptionLabel(label),
      disabled: Boolean(option.disabled || option.parentElement?.disabled),
      selected: index === selectEl.selectedIndex
    };
  });
}
  • Step 2: Update option template

Replace the option button content with:

<span class="global-select-option-main">{{ option.display.title || option.label || option.value || "空选项" }}</span>
<small v-if="option.display.subtitle" class="global-select-option-sub">{{ option.display.subtitle }}</small>
<em v-if="option.display.meta" class="global-select-option-meta">{{ option.display.meta }}</em>
  • Step 3: Add select panel styles

Append to frontend/src/styles/main.css:

.global-select-layer {
  z-index: 2000;
}

.global-select-input-box {
  border-radius: 12px;
  box-shadow: var(--ui-shadow-card);
}

.global-select-search-input {
  border: 1px solid var(--ui-line-strong);
  border-radius: 12px;
  padding: 0 36px 0 12px;
  color: var(--ui-ink);
  background: #fff;
}

.global-select-panel {
  overflow: hidden;
  border: 1px solid var(--ui-line-strong);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.98);
  box-shadow: var(--ui-shadow-float);
}

.global-select-option-list {
  max-height: inherit;
  overflow: auto;
  padding: 6px;
}

.global-select-option {
  display: grid;
  gap: 3px;
  width: 100%;
  border: 0;
  border-radius: 12px;
  padding: 10px 12px;
  color: var(--ui-ink);
  background: transparent;
  text-align: left;
}

.global-select-option:hover,
.global-select-option-active {
  background: rgba(37, 99, 235, 0.08);
}

.global-select-option-selected {
  color: var(--ui-steel);
  background: rgba(37, 99, 235, 0.12);
}

.global-select-option-main {
  font-weight: 900;
}

.global-select-option-sub,
.global-select-option-meta {
  color: var(--ui-ink-muted);
  font-size: 12px;
  font-style: normal;
  line-height: 1.35;
}

.global-select-option-disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
  • Step 4: Align StockLotTagSelect dropdown behavior

Modify frontend/src/components/StockLotTagSelect.vue so every selected lot closes the dropdown and requires the user to click the input again to add another lot:

function selectLot(lot) {
  if (!lot || isSelected(lot) || isDisabled(lot)) {
    return;
  }
  emit("update:modelValue", [...props.modelValue, lot.lot_no]);
  closeDropdown();
}

Keep searchable behavior and selected tags.

  • Step 5: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser checks:

Open purchase order drawer and click 原材料 select.
Confirm long options display as title/subtitle/meta instead of one huge line.
Open a warehouse stock lot multi-select.
Select one lot and confirm dropdown closes.
Click input again and confirm dropdown reopens for next selection.
  • Step 6: Commit
git add frontend/src/components/GlobalSearchableSelect.vue frontend/src/components/StockLotTagSelect.vue frontend/src/styles/main.css
git commit -m "style: improve searchable select option readability"

Task 9: Warehouse Page Visual Upgrade

Files:

  • Modify: frontend/src/views/InventoryLedgerView.vue

  • Modify: frontend/src/styles/main.css

  • Use: AppIcon.vue, StatusBadge.vue, SmartDrawer.vue, MetricStrip.vue where practical

  • Step 1: Rebuild warehouse selector as cards

In frontend/src/views/InventoryLedgerView.vue, update the warehouse selector markup to this shape while preserving existing click handlers and active warehouse state:

<div class="warehouse-switchboard">
  <button
    v-for="warehouse in warehouseTabs"
    :key="warehouse.key"
    class="warehouse-tab-card"
    :class="{ 'warehouse-tab-card-active': activeWarehouse === warehouse.key }"
    type="button"
    @click="selectWarehouse(warehouse.key)"
  >
    <AppIcon :name="warehouse.icon || 'warehouse'" />
    <span>{{ warehouse.label }}</span>
    <strong>{{ warehouse.count }} </strong>
  </button>
</div>

If current data uses another variable name, map to that variable without changing its business value.

  • Step 2: Rebuild operation zone

Use this structure for 入库/出库/工具 buttons:

<section class="warehouse-operation-zone">
  <div class="warehouse-operation-group warehouse-operation-in">
    <span class="warehouse-operation-title">入库</span>
    <button v-for="operation in inboundOperations" :key="operation.key" class="warehouse-operation-button" type="button" @click="openOperation(operation)">
      <AppIcon name="plus" tone="success" />
      <span>{{ operation.label }}</span>
    </button>
  </div>
  <div class="warehouse-operation-group warehouse-operation-out">
    <span class="warehouse-operation-title">出库</span>
    <button v-for="operation in outboundOperations" :key="operation.key" class="warehouse-operation-button" type="button" @click="openOperation(operation)">
      <AppIcon name="minus" tone="danger" />
      <span>{{ operation.label }}</span>
    </button>
  </div>
  <div class="warehouse-operation-group warehouse-operation-tools">
    <button class="warehouse-tool-button" type="button" @click="openLedger">
      <AppIcon name="ledger" />
      <span>流水</span>
    </button>
    <button class="warehouse-tool-button warehouse-tool-primary" type="button" @click="openProductionWorkOrderInbound">
      <AppIcon name="production" />
      <span>生产台账入库</span>
    </button>
  </div>
</section>
  • Step 3: Add warehouse CSS

Append to frontend/src/styles/main.css:

.warehouse-switchboard {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px;
  margin: 14px 0;
}

.warehouse-tab-card {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  min-height: 52px;
  border: 1px solid var(--ui-line);
  border-radius: 16px;
  padding: 0 14px;
  color: var(--ui-ink-soft);
  background: rgba(255, 255, 255, 0.78);
  box-shadow: 0 10px 28px rgba(15, 23, 42, 0.04);
}

.warehouse-tab-card-active {
  color: var(--ui-steel);
  border-color: rgba(37, 99, 235, 0.28);
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(219, 234, 254, 0.62));
  box-shadow: 0 16px 36px rgba(37, 99, 235, 0.13);
}

.warehouse-tab-card span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 900;
}

.warehouse-tab-card strong {
  color: inherit;
  font-size: 12px;
}

.warehouse-operation-zone {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: 12px;
  margin-bottom: 14px;
  padding: 14px;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius-lg);
  background: rgba(255, 255, 255, 0.76);
  box-shadow: var(--ui-shadow-card);
}

.warehouse-operation-group {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
}

.warehouse-operation-title {
  flex: 0 0 auto;
  min-width: 42px;
  color: var(--ui-ink-muted);
  font-size: 12px;
  font-weight: 900;
}

.warehouse-operation-button,
.warehouse-tool-button {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 34px;
  border: 1px solid var(--ui-line);
  border-radius: 999px;
  padding: 0 12px;
  color: var(--ui-ink-soft);
  background: rgba(255, 255, 255, 0.92);
  font-size: 12px;
  font-weight: 900;
}

.warehouse-operation-in .warehouse-operation-button {
  color: var(--ui-green);
  border-color: rgba(22, 163, 74, 0.18);
  background: rgba(22, 163, 74, 0.08);
}

.warehouse-operation-out .warehouse-operation-button {
  color: var(--ui-red);
  border-color: rgba(220, 38, 38, 0.18);
  background: rgba(220, 38, 38, 0.07);
}

.warehouse-tool-primary {
  color: #fff;
  background: linear-gradient(135deg, var(--ui-steel), var(--ui-cyan));
  border-color: transparent;
}

@media (max-width: 1080px) {
  .warehouse-operation-zone {
    grid-template-columns: 1fr;
  }
}
  • Step 4: Reduce permanent explanatory text

In InventoryLedgerView.vue, replace long visible operation descriptions with compact title attributes or FieldHint. Keep operation label visible. Example:

<button class="warehouse-operation-button" :title="operation.description" type="button" @click="openOperation(operation)">
  <AppIcon :name="operation.direction === 'in' ? 'plus' : 'minus'" />
  <span>{{ operation.label }}</span>
</button>
  • Step 5: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser checks:

Open /inventory-ledger.
Confirm warehouse cards are readable and active warehouse is obvious.
Confirm 入库 and 出库 operations are visually separated.
Confirm 流水 and 生产台账入库 are visible near operation tools.
Confirm no operation label contains the word bar.
Confirm table remains usable at 1440x900 and 390x844.
  • Step 6: Commit
git add frontend/src/views/InventoryLedgerView.vue frontend/src/styles/main.css
git commit -m "style: refine warehouse operation experience"

Task 10: Procurement, Sales, Production, Delivery Page Pass

Files:

  • Modify: frontend/src/views/PurchaseOrderView.vue

  • Modify: frontend/src/views/PurchaseReceiptView.vue

  • Modify: frontend/src/views/QualityInspectionView.vue

  • Modify: frontend/src/views/SalesPlanningView.vue

  • Modify: frontend/src/views/ProductionLedgerView.vue

  • Modify: frontend/src/views/OperationReportView.vue

  • Modify: frontend/src/views/DeliveryManagementView.vue

  • Modify: frontend/src/styles/main.css

  • Step 1: Standardize page header rhythm

For each listed view, ensure the top panel follows this shape:

<section class="page-hero-card">
  <div>
    <p class="eyebrow">{{ pageEyebrow }}</p>
    <h2>{{ pageTitle }}</h2>
    <p class="page-hero-subtitle">{{ pageSubtitle }}</p>
  </div>
  <div class="page-hero-actions">
    <button class="ghost-button" type="button" @click="loadData">刷新</button>
    <button v-if="canCreate" class="primary-button" type="button" @click="openCreateDrawer">{{ createLabel }}</button>
  </div>
</section>

Use existing variables and handlers. Do not add new business actions.

  • Step 2: Add page hero CSS

Append to frontend/src/styles/main.css:

.page-hero-card {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18px;
  margin-bottom: 14px;
  padding: 18px 20px;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius-xl);
  background:
    linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(241, 245, 249, 0.88)),
    radial-gradient(circle at 100% 0%, rgba(37, 99, 235, 0.12), transparent 34%);
  box-shadow: var(--ui-shadow-card);
}

.page-hero-card h2 {
  margin: 0;
  color: var(--ui-ink);
  font-size: 22px;
  letter-spacing: -0.02em;
}

.page-hero-subtitle {
  margin: 6px 0 0;
  color: var(--ui-ink-muted);
  font-size: 13px;
}

.page-hero-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 10px;
}

@media (max-width: 768px) {
  .page-hero-card {
    display: grid;
  }

  .page-hero-actions {
    justify-content: flex-start;
  }
}
  • Step 3: Migrate create/edit drawers gradually

For each create/edit drawer in the listed views, keep the existing form fields and handlers but wrap field clusters in FormSection where it improves readability. Example for purchase order:

<FormSection title="采购基础信息" :columns="3">
  <label>
    <span>采购单号</span>
    <input v-model.trim="purchaseForm.order_no" type="text" required />
  </label>
  <label>
    <span>供应商</span>
    <select v-model="purchaseForm.supplier_id" required>
      <option disabled value="">请选择供应商</option>
      <option v-for="supplier in suppliers" :key="supplier.supplier_id" :value="supplier.supplier_id">
        {{ supplier.supplier_name }}
      </option>
    </select>
  </label>
  <label>
    <span>预计到货</span>
    <input v-model="purchaseForm.expected_arrival_date" type="date" />
  </label>
</FormSection>

Import:

import FormSection from "../components/FormSection.vue";
  • Step 4: Convert status cells

For each listed page, replace status plain text with StatusBadge. Example:

<td><StatusBadge :value="row.status" /></td>
  • Step 5: Convert noisy helper blocks

Replace visible helper paragraphs longer than 48 characters with one of:

<FieldHint text="完整说明文字放在这里,用户需要时悬停查看。" />

or a compact title:

<span class="panel-tag" :title="longDescription">规则说明</span>

Do not delete the meaning. Only move it out of permanent page flow.

  • Step 6: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser checks:

Open /purchase-orders and create drawer. Confirm all existing fields still exist.
Open /order-list and create drawer. Confirm all existing fields still exist.
Open /production-ledger and confirm status colors are clear.
Open /delivery-management and confirm grouped delivery table remains readable.
Open /operation-reports and confirm date filters look integrated, not bolted on.
  • Step 7: Commit
git add frontend/src/views/PurchaseOrderView.vue frontend/src/views/PurchaseReceiptView.vue frontend/src/views/QualityInspectionView.vue frontend/src/views/SalesPlanningView.vue frontend/src/views/ProductionLedgerView.vue frontend/src/views/OperationReportView.vue frontend/src/views/DeliveryManagementView.vue frontend/src/styles/main.css
git commit -m "style: polish high frequency business pages"

Task 11: Finance Large Table And System Permission Polish

Files:

  • Modify: frontend/src/views/FinanceReportView.vue

  • Modify: frontend/src/views/SystemPermissionView.vue

  • Modify: frontend/src/components/OrgMindMap.vue

  • Modify: frontend/src/components/OrgPermissionTree.vue

  • Modify: frontend/src/styles/main.css

  • Step 1: Add financial spreadsheet mode styles

Append to frontend/src/styles/main.css:

.finance-sheet-table {
  border-collapse: separate;
  border-spacing: 0;
  background: #fff;
}

.finance-sheet-table th {
  position: sticky;
  top: 0;
  z-index: 3;
  background: linear-gradient(180deg, #f8fafc, #e2e8f0);
  color: var(--ui-ink-soft);
  white-space: nowrap;
  font-size: 12px;
  font-weight: 900;
}

.finance-sheet-table td {
  color: var(--ui-ink);
  white-space: nowrap;
}

.finance-sticky-cell {
  position: sticky;
  left: 0;
  z-index: 2;
  background: #fff;
  box-shadow: 10px 0 18px rgba(15, 23, 42, 0.06);
}

.finance-board-toolbar {
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius-lg);
  background: rgba(255, 255, 255, 0.86);
  box-shadow: var(--ui-shadow-card);
}
  • Step 2: Improve finance filter row

In FinanceReportView.vue, wrap date filters and refresh actions in a compact filter bar:

<div class="filter-ribbon">
  <label>
    <span>开始日期</span>
    <input v-model="dateFrom" type="date" />
  </label>
  <label>
    <span>结束日期</span>
    <input v-model="dateTo" type="date" />
  </label>
  <button class="ghost-button" type="button" @click="expenseDrawerOpen = true">费用登记</button>
  <button class="primary-button" type="button" :disabled="loadingBoard" @click="loadBoard">刷新核算表</button>
</div>
  • Step 3: Add filter ribbon CSS

Append:

.filter-ribbon {
  display: flex;
  align-items: end;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 14px;
  padding: 14px;
  border: 1px solid var(--ui-line);
  border-radius: var(--ui-radius-lg);
  background: rgba(255, 255, 255, 0.86);
  box-shadow: var(--ui-shadow-card);
}

.filter-ribbon label {
  display: grid;
  gap: 6px;
  min-width: 180px;
  color: var(--ui-ink-soft);
  font-size: 12px;
  font-weight: 900;
}

.filter-ribbon input,
.filter-ribbon select {
  min-height: 38px;
  border: 1px solid var(--ui-line);
  border-radius: 12px;
  padding: 0 12px;
  background: #fff;
}
  • Step 4: Polish system permission tabs

In SystemPermissionView.vue, keep the existing two tab model but style as segmented tabs:

<div class="admin-tab-switcher">
  <button :class="{ active: activeTab === 'org' }" type="button" @click="activeTab = 'org'">组织架构</button>
  <button :class="{ active: activeTab === 'role' }" type="button" @click="activeTab = 'role'">角色维护</button>
</div>
  • Step 5: Add admin tab CSS

Append:

.admin-tab-switcher {
  display: inline-flex;
  gap: 6px;
  padding: 6px;
  border: 1px solid var(--ui-line);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.76);
}

.admin-tab-switcher button {
  min-height: 34px;
  border: 0;
  border-radius: 12px;
  padding: 0 14px;
  color: var(--ui-ink-soft);
  background: transparent;
  font-weight: 900;
}

.admin-tab-switcher button.active {
  color: #fff;
  background: linear-gradient(135deg, var(--ui-steel), var(--ui-cyan));
  box-shadow: 0 10px 24px rgba(37, 99, 235, 0.18);
}
  • Step 6: Verify

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built

Browser checks:

Open /finance-reports and confirm filters are not stretched to huge widths.
Confirm financial table remains horizontally scrollable and key columns stay readable.
Open /system-permissions and confirm tabs do not crowd the brain map or tree.
Confirm org map controls are visually consistent with other buttons.
  • Step 7: Commit
git add frontend/src/views/FinanceReportView.vue frontend/src/views/SystemPermissionView.vue frontend/src/components/OrgMindMap.vue frontend/src/components/OrgPermissionTree.vue frontend/src/styles/main.css
git commit -m "style: polish finance and permission interfaces"

Task 12: Final UI Verification And Regression Sweep

Files:

  • Modify only if verification finds issues.

  • Test: frontend build and browser screenshots.

  • Step 1: Build

Run:

cd /Users/souplearn/Gitlab/py/ForgeFlow-ERP/frontend
npm run build

Expected:

✓ built
  • Step 2: Desktop browser sweep

Use browser plugin at 1440x900:

http://127.0.0.1:5173/
http://127.0.0.1:5173/customer-management
http://127.0.0.1:5173/supplier-management
http://127.0.0.1:5173/supplier-material-lite
http://127.0.0.1:5173/product-spec-lite
http://127.0.0.1:5173/order-list
http://127.0.0.1:5173/purchase-orders
http://127.0.0.1:5173/purchase-receipts
http://127.0.0.1:5173/quality-inspection
http://127.0.0.1:5173/inventory-ledger
http://127.0.0.1:5173/production-ledger
http://127.0.0.1:5173/operation-reports
http://127.0.0.1:5173/delivery-management
http://127.0.0.1:5173/finance-reports
http://127.0.0.1:5173/system-permissions
http://127.0.0.1:5173/system-extension

Expected:

No page is squeezed by sidebar.
No obvious raw English status is visible.
No table header shows raw ↕ text.
No drawer close/check button is clipped.
No form field is hidden by footer.
No long helper paragraph dominates the page.
  • Step 3: Narrow browser sweep

Use browser plugin at 390x844:

http://127.0.0.1:5173/
http://127.0.0.1:5173/inventory-ledger
http://127.0.0.1:5173/purchase-orders
http://127.0.0.1:5173/order-list
http://127.0.0.1:5173/system-permissions

Expected:

Sidebar collapses.
Main content remains usable.
Tables scroll inside their wrappers.
Drawers are full-screen or near full-screen.
Primary action buttons remain visible.
  • Step 4: Interaction checks

Perform these checks without saving destructive data:

Open purchase order create drawer.
Open sales order create drawer.
Open warehouse 客料入库 drawer.
Open warehouse stock lot multi-select and select one item.
Open finance expense drawer.
Switch system permission tabs.
Switch org brain map/tree mode.

Expected:

Existing fields are still present.
Dropdowns are searchable.
Multi-select closes after one selection and reopens on next click.
Status badges remain colored.
Buttons have consistent height and feedback.
  • Step 5: Run UI audit helper in browser console

On /inventory-ledger, /purchase-orders, and /finance-reports, run:

import("/src/utils/uiAudit.js").then((m) => m.printUiAuditSnapshot());

Expected:

visibleEnglishStatuses is empty or contains only internal hidden values.
tables do not report hasRawSortText as true.
longHelperBlocks contains no large permanent business explanations.
  • Step 6: Final commit
git add frontend/src
git commit -m "style: complete ERP frontend visual upgrade"

If no .git directory exists, record:

Final commit skipped because current workspace is not a git repository.

Self-Review

  • Spec coverage: The plan covers global visual direction, five requested design skills, shell, icons, buttons, helper text, status colors, tables, drawers, searchable selects, warehouse page, high-frequency pages, finance, system permissions, desktop/narrow verification.
  • Scope boundary: The plan keeps business fields, workflows, API payloads, and backend behavior unchanged.
  • No placeholder scan: The plan contains concrete file paths, code snippets, verification commands, and expected results.
  • Type consistency: New components use stable props: AppIcon.name, StatusBadge.value, SmartDrawer.open/title/size, FormSection.title/columns, MetricStrip.metrics.
  • Execution recommendation: Use subagent-driven development. Split by primitive/component tasks first, then page passes, because the table/status/drawer work can be reviewed independently before touching high-frequency business pages.