Ver código fonte

fix(mobile-oa-h5): skin market neutral tags as barley soft.

Map Element Plus info tones through Vant default and style them as
浅底大麦黄 so unpaid/pending tags stay visible and distinct from solid semantic tags.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 semana atrás
pai
commit
19dffbaa49

+ 460 - 0
docs/demos/2026-10-17-mobile-market-neutral-tag-demo.html

@@ -0,0 +1,460 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1" />
+  <title>市场部 · 中性 Tag 样式对比</title>
+  <style>
+    :root {
+      --brand: #0a9f6d;
+      --brand-dark: #087f57;
+      --brand-soft: #e9f9f2;
+      --barley: #d6a343;
+      --barley-soft: #fff7e8;
+      --text: #18332a;
+      --muted: #6b7f77;
+      --border: #dce9e4;
+      --surface: #fff;
+      --page: #f4f8f6;
+      --danger: #ef4444;
+      --danger-soft: #fef2f2;
+      --success: #10b981;
+      --vant-gray: #969799;
+    }
+    * { box-sizing: border-box; }
+    body {
+      margin: 0;
+      font-family: "PingFang SC", "Noto Sans SC", system-ui, sans-serif;
+      color: var(--text);
+      background:
+        radial-gradient(900px 360px at 0% -10%, #d1fae5 0%, transparent 55%),
+        radial-gradient(700px 320px at 100% 0%, #ecfdf5 0%, transparent 50%),
+        var(--page);
+      line-height: 1.5;
+    }
+    .wrap { max-width: 980px; margin: 0 auto; padding: 24px 16px 72px; }
+    h1 { margin: 0 0 8px; font-size: 24px; letter-spacing: -0.02em; }
+    .lead { margin: 0 0 20px; color: var(--muted); max-width: 720px; }
+    .note {
+      margin: 0 0 24px;
+      padding: 12px 14px;
+      border: 1px solid var(--border);
+      border-radius: 12px;
+      background: #fff;
+      font-size: 13px;
+      color: var(--muted);
+    }
+    .note strong { color: var(--text); }
+    .section { margin-bottom: 28px; }
+    .section h2 {
+      margin: 0 0 10px;
+      font-size: 16px;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+    .section h2 .pill {
+      font-size: 11px;
+      font-weight: 600;
+      padding: 2px 8px;
+      border-radius: 999px;
+      background: var(--brand-soft);
+      color: var(--brand-dark);
+    }
+    .card {
+      border: 1px solid var(--border);
+      border-radius: 16px;
+      background: var(--surface);
+      box-shadow: 0 6px 20px rgb(8 127 87 / 7%);
+      padding: 14px;
+    }
+    .card + .card { margin-top: 12px; }
+    .card-head {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      padding-bottom: 12px;
+      border-bottom: 1px solid #eef4f1;
+      margin-bottom: 12px;
+    }
+    .idx {
+      display: grid;
+      place-items: center;
+      width: 30px;
+      height: 30px;
+      border-radius: 50%;
+      background: var(--brand-soft);
+      color: var(--brand-dark);
+      font-size: 13px;
+      font-weight: 700;
+      flex: 0 0 30px;
+    }
+    .id-block { flex: 1; min-width: 0; }
+    .id-block strong { display: block; font-size: 16px; }
+    .id-block small { color: var(--muted); font-size: 12px; }
+    .row-tags {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+      align-items: center;
+    }
+    .hint {
+      margin: 8px 0 0;
+      font-size: 12px;
+      color: var(--muted);
+    }
+    /* —— shared capsule —— */
+    .tag {
+      display: inline-flex;
+      align-items: center;
+      width: fit-content;
+      max-width: 128px;
+      padding: 5px 12px;
+      border-radius: 999px;
+      font-size: 12px;
+      line-height: 1.3;
+      font-weight: 500;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      border: 1px solid transparent;
+    }
+    /* existing filled tones (keep as-is) */
+    .tag--primary { background: var(--brand); color: #fff; }
+    .tag--success { background: var(--success); color: #fff; }
+    .tag--warning { background: #ff976a; color: #fff; }
+    .tag--danger { background: var(--danger); color: #fff; }
+
+    /* CURRENT problem: Vant default */
+    .tag--current-default {
+      background: var(--vant-gray);
+      color: #fff;
+    }
+
+    /* Option A: soft slate (Element-like info light) */
+    .tag--opt-a {
+      background: #f1f5f9;
+      color: #475569;
+      border-color: #e2e8f0;
+    }
+
+    /* Option B: soft mint-muted (brand family, not solid primary) */
+    .tag--opt-b {
+      background: #eef7f3;
+      color: #3d5c52;
+      border-color: #d5e8df;
+    }
+
+    /* Option C: outline / plain neutral */
+    .tag--opt-c {
+      background: transparent;
+      color: #5b6f67;
+      border-color: #c5d4cd;
+    }
+
+    /* Option D: barley soft (warm pending, distinct from warning orange fill) */
+    .tag--opt-d {
+      background: var(--barley-soft);
+      color: #9a7020;
+      border-color: #f0e0b8;
+    }
+
+    .options {
+      display: grid;
+      gap: 12px;
+    }
+    @media (min-width: 720px) {
+      .options { grid-template-columns: 1fr 1fr; }
+    }
+    .option {
+      position: relative;
+      border: 2px solid var(--border);
+      border-radius: 16px;
+      background: #fff;
+      padding: 14px;
+      cursor: pointer;
+      transition: border-color .15s, box-shadow .15s;
+      text-align: left;
+      font: inherit;
+      color: inherit;
+      width: 100%;
+    }
+    .option:hover { border-color: #b7d4c8; }
+    .option.is-selected {
+      border-color: var(--brand);
+      box-shadow: 0 0 0 3px var(--brand-soft);
+    }
+    .option .opt-title {
+      display: flex;
+      justify-content: space-between;
+      gap: 8px;
+      margin-bottom: 8px;
+      font-weight: 700;
+      font-size: 14px;
+    }
+    .option .opt-title span {
+      font-size: 11px;
+      font-weight: 600;
+      color: var(--brand-dark);
+      background: var(--brand-soft);
+      padding: 2px 8px;
+      border-radius: 999px;
+    }
+    .option .opt-desc {
+      margin: 8px 0 0;
+      font-size: 12px;
+      color: var(--muted);
+    }
+    .compare-existing {
+      margin-top: 10px;
+      padding-top: 10px;
+      border-top: 1px dashed var(--border);
+    }
+    .compare-existing .label {
+      font-size: 11px;
+      color: var(--muted);
+      margin-bottom: 6px;
+    }
+    .web-admin {
+      display: grid;
+      gap: 10px;
+    }
+    @media (min-width: 720px) {
+      .web-admin { grid-template-columns: 1fr 1fr; }
+    }
+    .el-tag {
+      display: inline-flex;
+      align-items: center;
+      height: 24px;
+      padding: 0 9px;
+      border-radius: 4px;
+      font-size: 12px;
+      border: 1px solid transparent;
+    }
+    .el-tag--info {
+      background: #f4f4f5;
+      border-color: #e9e9eb;
+      color: #909399;
+    }
+    .el-tag--success {
+      background: #f0f9eb;
+      border-color: #e1f3d8;
+      color: #67c23a;
+    }
+    .el-tag--warning {
+      background: #fdf6ec;
+      border-color: #faecd8;
+      color: #e6a23c;
+    }
+    .el-tag--primary {
+      background: #ecf5ff;
+      border-color: #d9ecff;
+      color: #409eff;
+    }
+    .el-tag--danger {
+      background: #fef0f0;
+      border-color: #fde2e2;
+      color: #f56c6c;
+    }
+    .pick-bar {
+      position: sticky;
+      bottom: 0;
+      margin: 28px -16px -24px;
+      padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
+      background: rgba(255,255,255,.92);
+      backdrop-filter: blur(8px);
+      border-top: 1px solid var(--border);
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 12px;
+      font-size: 13px;
+    }
+    .pick-bar b { color: var(--brand-dark); }
+    .pick-bar button {
+      border: 0;
+      border-radius: 999px;
+      padding: 8px 16px;
+      background: var(--brand);
+      color: #fff;
+      font-weight: 600;
+      cursor: pointer;
+    }
+    .pick-bar button:disabled {
+      opacity: .45;
+      cursor: not-allowed;
+    }
+  </style>
+</head>
+<body>
+  <div class="wrap">
+    <h1>市场部 · 中性 Tag 样式对比</h1>
+    <p class="lead">
+      上一版把 Element Plus 的 <code>info</code> 映射成了 Vant <code>default</code>(灰底白字),可读但偏「硬」。
+      请点选一种<strong>中性态</strong>样式;实心绿 / 橙 / 红等已有语义色保持不变。
+    </p>
+
+    <div class="note">
+      <strong>web-admin「存储服务」说明:</strong>
+      Element Plus 默认 <code>el-tag type="info"</code> 是<strong>浅灰底 + 灰字</strong>(light),
+      <strong>不是</strong>灰底白字,也<strong>不是</strong>无底白字。
+      存储服务里大量「待缴费 / 待生效 / 草稿」走 <code>info</code>,视觉接近下方「Web 对照」区。
+    </div>
+
+    <section class="section">
+      <h2>现状问题 <span class="pill">Vant default</span></h2>
+      <div class="card">
+        <div class="card-head">
+          <span class="idx">1</span>
+          <span class="id-block">
+            <strong>欧阳妮妮</strong>
+            <small>ZKZX34000194</small>
+          </span>
+          <span class="tag tag--current-default">待缴费</span>
+        </div>
+        <div class="row-tags">
+          <span class="tag tag--current-default">待缴费</span>
+          <span class="tag tag--current-default">待生效</span>
+          <span class="tag tag--current-default">草稿</span>
+          <span class="tag tag--primary">部分已缴</span>
+          <span class="tag tag--success">已缴清</span>
+          <span class="tag tag--warning">催缴中</span>
+        </div>
+        <p class="hint">灰底白字对比度高,但和「语义实心色」挤在一起时显得脏、抢戏。</p>
+      </div>
+    </section>
+
+    <section class="section">
+      <h2>方案选择 <span class="pill">点一张卡片</span></h2>
+      <div class="options" id="options">
+        <button type="button" class="option" data-choice="A" data-label="浅灰底深灰字">
+          <div class="opt-title">方案 A · 浅灰底深灰字 <span>推荐贴近 Web</span></div>
+          <div class="row-tags">
+            <span class="tag tag--opt-a">待缴费</span>
+            <span class="tag tag--opt-a">待生效</span>
+            <span class="tag tag--opt-a">草稿</span>
+          </div>
+          <div class="compare-existing">
+            <div class="label">同屏已有语义色(不变)</div>
+            <div class="row-tags">
+              <span class="tag tag--primary">部分已缴</span>
+              <span class="tag tag--success">已缴清</span>
+              <span class="tag tag--warning">催缴中</span>
+            </div>
+          </div>
+          <p class="opt-desc">接近 Element Plus info light;中性、不抢绿/橙实心标。</p>
+        </button>
+
+        <button type="button" class="option" data-choice="B" data-label="薄荷绿灰底深字">
+          <div class="opt-title">方案 B · 薄荷绿灰底深字 <span>品牌系</span></div>
+          <div class="row-tags">
+            <span class="tag tag--opt-b">待缴费</span>
+            <span class="tag tag--opt-b">待生效</span>
+            <span class="tag tag--opt-b">草稿</span>
+          </div>
+          <div class="compare-existing">
+            <div class="label">同屏已有语义色(不变)</div>
+            <div class="row-tags">
+              <span class="tag tag--primary">部分已缴</span>
+              <span class="tag tag--success">已缴清</span>
+              <span class="tag tag--warning">催缴中</span>
+            </div>
+          </div>
+          <p class="opt-desc">偏三墙绿氛围,但仍是浅底深字,和实心 primary 区分明显。</p>
+        </button>
+
+        <button type="button" class="option" data-choice="C" data-label="描边中性">
+          <div class="opt-title">方案 C · 描边中性 <span>最轻</span></div>
+          <div class="row-tags">
+            <span class="tag tag--opt-c">待缴费</span>
+            <span class="tag tag--opt-c">待生效</span>
+            <span class="tag tag--opt-c">草稿</span>
+          </div>
+          <div class="compare-existing">
+            <div class="label">同屏已有语义色(不变)</div>
+            <div class="row-tags">
+              <span class="tag tag--primary">部分已缴</span>
+              <span class="tag tag--success">已缴清</span>
+              <span class="tag tag--warning">催缴中</span>
+            </div>
+          </div>
+          <p class="opt-desc">无填充描边;层级最低,适合「未开始」态,但在密集列表里可能偏淡。</p>
+        </button>
+
+        <button type="button" class="option is-selected" data-choice="D" data-label="大麦黄浅底">
+          <div class="opt-title">方案 D · 大麦黄浅底 <span>已采纳</span></div>
+          <div class="row-tags">
+            <span class="tag tag--opt-d">待缴费</span>
+            <span class="tag tag--opt-d">待生效</span>
+            <span class="tag tag--opt-d">草稿</span>
+          </div>
+          <div class="compare-existing">
+            <div class="label">同屏已有语义色(不变)</div>
+            <div class="row-tags">
+              <span class="tag tag--primary">部分已缴</span>
+              <span class="tag tag--success">已缴清</span>
+              <span class="tag tag--warning">催缴中</span>
+            </div>
+          </div>
+          <p class="opt-desc">用三墙大麦黄浅底表达「待处理」;需与 warning 实心橙区分(浅底 vs 实心)。</p>
+        </button>
+      </div>
+    </section>
+
+    <section class="section">
+      <h2>Web 对照 <span class="pill">Element Plus info</span></h2>
+      <div class="web-admin">
+        <div class="card">
+          <div class="label" style="font-size:12px;color:var(--muted);margin-bottom:8px">存储服务常见 info(浅底深/灰字)</div>
+          <div class="row-tags">
+            <span class="el-tag el-tag--info">待缴费</span>
+            <span class="el-tag el-tag--info">待生效</span>
+            <span class="el-tag el-tag--info">草稿</span>
+            <span class="el-tag el-tag--success">已缴清</span>
+            <span class="el-tag el-tag--warning">部分已缴</span>
+          </div>
+          <p class="hint">无「无底白字」问题;默认也不是灰底白字。</p>
+        </div>
+        <div class="card">
+          <div class="label" style="font-size:12px;color:var(--muted);margin-bottom:8px">若选方案 A,移动端中性态将更接近 Web</div>
+          <div class="row-tags">
+            <span class="tag tag--opt-a">待缴费</span>
+            <span class="el-tag el-tag--info">待缴费</span>
+          </div>
+          <p class="hint">左:移动方案 A · 右:Web info light(圆角规范不同,色感接近)</p>
+        </div>
+      </div>
+    </section>
+
+    <div class="pick-bar">
+      <div>已选:<b id="picked">未选择</b></div>
+      <button type="button" id="confirm" disabled>确认此方案</button>
+    </div>
+  </div>
+
+  <script>
+    const options = document.getElementById('options')
+    const picked = document.getElementById('picked')
+    const confirmBtn = document.getElementById('confirm')
+    let current = null
+
+    options.addEventListener('click', (e) => {
+      const btn = e.target.closest('.option')
+      if (!btn) return
+      options.querySelectorAll('.option').forEach((el) => el.classList.remove('is-selected'))
+      btn.classList.add('is-selected')
+      current = { choice: btn.dataset.choice, label: btn.dataset.label }
+      picked.textContent = '方案 ' + current.choice + ' · ' + current.label
+      confirmBtn.disabled = false
+      if (window.parent !== window) {
+        window.parent.postMessage({ type: 'brainstorm-select', choice: current.choice, label: current.label }, '*')
+      }
+    })
+
+    confirmBtn.addEventListener('click', () => {
+      if (!current) return
+      alert('已记录偏好:方案 ' + current.choice + '(' + current.label + ')。请回到对话告诉助手你的选择。')
+    })
+  </script>
+</body>
+</html>

+ 40 - 0
docs/superpowers/specs/2026-10-17-mobile-market-neutral-tag-barley-design.md

@@ -0,0 +1,40 @@
+# 市场部移动端中性 Tag · 大麦黄浅底(方案 D)
+
+## 背景
+
+`info`(Element Plus 语义)映射为 Vant `default` 后,中性态呈现「灰底白字」,与实心语义色同屏时观感偏硬。用户选定方案 D:三墙大麦黄浅底。
+
+## 决策
+
+| 项 | 选择 |
+|----|------|
+| 方案 | D · 大麦黄浅底 |
+| 范围 | 仅 `mobile-oa-h5` 市场部(`.market-page` / `.presign-order-page`) |
+| 不改 | `primary` / `success` / `warning` / `danger` 实心色;web-admin |
+| 数据层 | 登记表继续 `tagType: 'info'`;展示层仍 `info` → Vant `default` |
+
+## 视觉 Token
+
+- 背景:`#fff7e8`(`--market` 大麦浅底,与规范 `#fff7e8` 一致)
+- 文字:`#9a7020`
+- 描边:`#f0e0b8`
+- 形状:沿用现有胶囊(`.market-tag` / round)
+
+与 `warning` 区分:中性 = 浅底深字;warning = 实心橙 + 白字。
+
+## 实现
+
+在 `market.css` 覆盖:
+
+```css
+.market-page .van-tag--default,
+.presign-order-page .van-tag--default { ... }
+```
+
+不新增 tone 枚举;`resolveStatusTone('…', 'info')` 继续返回 `default`。
+
+## 验收
+
+- 缴费列表「待缴费」等中性态为大麦黄浅底,非灰底白字
+- 已缴清 / 部分已缴 / 催缴等实心色不变
+- 相关单测通过

+ 1 - 1
mobile-oa-h5/src/market/components/MarketDetailNestedCard.vue

@@ -124,7 +124,7 @@
         <van-tag
           v-else-if="field.tag"
           class="market-tag"
-          :type="field.tagType || resolveStatusTone(field.value)"
+          :type="resolveStatusTone(field.value, field.tagType)"
           round
           :title="field.value"
         >

+ 7 - 1
mobile-oa-h5/src/market/components/MarketDetailSection.vue

@@ -10,7 +10,13 @@
             :model-value="field.value"
             :editable="false"
           />
-          <van-tag v-else-if="field.tag" class="market-tag" :type="field.tagType || resolveStatusTone(field.value)" round :title="field.value">
+          <van-tag
+            v-else-if="field.tag"
+            class="market-tag"
+            :type="resolveStatusTone(field.value, field.tagType)"
+            round
+            :title="field.value"
+          >
             {{ field.value }}
           </van-tag>
           <span v-else>{{ field.value }}</span>

+ 31 - 6
mobile-oa-h5/src/market/components/MarketListCard.vue

@@ -17,7 +17,9 @@
         <strong class="market-list-card__title">{{ card.title }}</strong>
         <small class="market-list-card__subtitle">{{ card.subtitle }}</small>
       </span>
-      <van-tag v-if="card.status" class="market-tag" :type="statusTone" round :title="card.status">{{ card.status }}</van-tag>
+      <van-tag v-if="card.status" class="market-tag" :type="statusTone" round :title="card.status">
+        {{ card.status }}
+      </van-tag>
     </header>
 
     <div class="market-list-card__fields">
@@ -28,7 +30,14 @@
         :class="{ 'is-wide': field.wide }"
       >
         <span>{{ field.label }}</span>
-        <van-tag v-if="field.tag" class="market-tag" :type="field.tagType || resolveStatusTone(field.value)" size="medium" round :title="field.value">{{ field.value }}</van-tag>
+        <van-tag
+          v-if="field.tag"
+          class="market-tag"
+          :type="resolveStatusTone(field.value, field.tagType)"
+          size="medium"
+          round
+          :title="field.value"
+        >{{ field.value }}</van-tag>
         <b v-else :class="{ 'is-money': field.money }">{{ field.value }}</b>
       </div>
     </div>
@@ -100,7 +109,14 @@
           :class="{ 'is-wide': field.wide }"
         >
           <span>{{ field.label }}</span>
-          <van-tag v-if="field.tag" class="market-tag" :type="field.tagType || resolveStatusTone(field.value)" size="medium" round :title="field.value">{{ field.value }}</van-tag>
+          <van-tag
+            v-if="field.tag"
+            class="market-tag"
+            :type="resolveStatusTone(field.value, field.tagType)"
+            size="medium"
+            round
+            :title="field.value"
+          >{{ field.value }}</van-tag>
           <b v-else :class="{ 'is-money': field.money }">{{ field.value }}</b>
         </div>
       </div>
@@ -114,7 +130,14 @@
         :class="{ 'is-wide': field.wide }"
       >
         <span>{{ field.label }}</span>
-        <van-tag v-if="field.tag" class="market-tag" :type="field.tagType || resolveStatusTone(field.value)" size="medium" round :title="field.value">{{ field.value }}</van-tag>
+        <van-tag
+          v-if="field.tag"
+          class="market-tag"
+          :type="resolveStatusTone(field.value, field.tagType)"
+          size="medium"
+          round
+          :title="field.value"
+        >{{ field.value }}</van-tag>
         <b v-else :class="{ 'is-money': field.money }">{{ field.value }}</b>
       </div>
     </div>
@@ -151,7 +174,7 @@
             <van-tag
               v-if="block.status && block.status !== '—'"
               class="market-tag"
-              :type="block.statusTone || resolveStatusTone(block.status)"
+              :type="resolveStatusTone(block.status, block.statusTone)"
               size="medium"
               round
               :title="block.status"
@@ -217,7 +240,9 @@ const FIELD_PREVIEW_COUNT = 4
 const FIELD_FOLD_MODULES = new Set(['protocol', 'payment-chase', 'payment'])
 
 const card = computed(() => buildMarketCard(props.module, props.role, props.item))
-const statusTone = computed(() => card.value.statusTone || resolveStatusTone(card.value.status))
+const statusTone = computed(() =>
+  resolveStatusTone(card.value.status, card.value.statusTone)
+)
 const allFields = computed(() => (Array.isArray(card.value.fields) ? card.value.fields : []))
 const footerFieldCount = computed(() =>
   Array.isArray(card.value.footerFields) ? card.value.footerFields.length : 0

+ 3 - 1
mobile-oa-h5/src/market/components/PaymentDetailTransfersPanel.vue

@@ -46,6 +46,7 @@
 import { ref, watch } from 'vue'
 import { formatMoney } from '../money.js'
 import { resolveStatusTag, paymentScheduleStatus } from '../status-tag-registry.js'
+import { resolveStatusTone } from '../status-tone.js'
 import ScheduleTransferEditor from './ScheduleTransferEditor.vue'
 
 const props = defineProps({
@@ -78,7 +79,8 @@ function scheduleStatusLabel(schedule) {
 }
 
 function scheduleStatusTone(schedule) {
-  return resolveStatusTag(paymentScheduleStatus, schedule?.scheduleStatus).tagType
+  const tag = resolveStatusTag(paymentScheduleStatus, schedule?.scheduleStatus)
+  return resolveStatusTone(tag.label, tag.tagType)
 }
 
 function onScheduleUpdate(index, nextSchedule) {

+ 6 - 1
mobile-oa-h5/src/market/components/PresignOrderCard.vue

@@ -11,7 +11,11 @@
         <strong class="presign-order-card__title">{{ card.title }}</strong>
         <small>母亲姓名</small>
       </span>
-      <van-tag class="market-tag" :type="statusField.tone" round>{{ statusField.value }}</van-tag>
+      <van-tag
+        class="market-tag"
+        :type="resolveStatusTone(statusField.value, statusField.tone)"
+        round
+      >{{ statusField.value }}</van-tag>
       <van-icon name="arrow" class="presign-order-card__arrow" />
     </button>
 
@@ -78,6 +82,7 @@ import {
   buildPresignCardModel,
   presignCardActionEvent
 } from '../presign/presign-list-model.js'
+import { resolveStatusTone } from '../status-tone.js'
 
 /** 默认展示两行(4 项),其余折叠 */
 const FIELD_PREVIEW_COUNT = 4

+ 4 - 1
mobile-oa-h5/src/market/components/PresignTransferCard.vue

@@ -108,6 +108,7 @@ import OaDatePicker from '../../oa/components/OaDatePicker.vue'
 import OaMoneyField from '../../oa/components/OaMoneyField.vue'
 import { removeMarketAttachmentAt } from '../attachment-upload-state.js'
 import { resolveStatusTag, payStatus } from '../status-tag-registry.js'
+import { resolveStatusTone } from '../status-tone.js'
 import PresignAttachmentUploader from './PresignAttachmentUploader.vue'
 import PresignSelectField from './PresignSelectField.vue'
 
@@ -144,7 +145,9 @@ const statusTag = computed(() =>
   resolveStatusTag(payStatus, props.modelValue.payStatus || 'dfj')
 )
 const statusLabel = computed(() => statusTag.value.label)
-const statusTone = computed(() => statusTag.value.tagType)
+const statusTone = computed(() =>
+  resolveStatusTone(statusTag.value.label, statusTag.value.tagType)
+)
 
 function can(action) {
   return !props.disabled && actionSet.value.has(action)

+ 3 - 1
mobile-oa-h5/src/market/components/ProtocolV1PaymentPanel.vue

@@ -288,6 +288,7 @@ import {
 } from '../attachment-upload-policy.js'
 import MarketAttachmentCards from './MarketAttachmentCards.vue'
 import { resolveStatusTag, payStatus } from '../status-tag-registry.js'
+import { resolveStatusTone } from '../status-tone.js'
 
 const props = defineProps({
   payment: { type: Object, default: () => ({}) },
@@ -365,7 +366,8 @@ function statusLabel(value) {
 }
 
 function statusTone(value) {
-  return resolveStatusTag(payStatus, value).tagType
+  const tag = resolveStatusTag(payStatus, value)
+  return resolveStatusTone(tag.label, tag.tagType)
 }
 
 function transferTypeLabel(value) {

+ 19 - 3
mobile-oa-h5/src/market/mobile-parity-ui.test.js

@@ -73,6 +73,11 @@ test('market list visual contract keeps search white, tags filled and actions ri
     styles,
     /\.market-page \.van-tag,\s*\n\.presign-order-page \.van-tag\s*\{[\s\S]*?padding:\s*5px 12px;[\s\S]*?border-radius:\s*999px;/
   )
+  // Neutral info→default: barley soft (not Vant gray-on-white)
+  assert.match(
+    styles,
+    /\.market-page \.van-tag--default,\s*\n\.presign-order-page \.van-tag--default\s*\{[\s\S]*?background:\s*#fff7e8;[\s\S]*?color:\s*#9a7020;[\s\S]*?border:\s*1px solid #f0e0b8;/
+  )
   assert.match(card, /class="market-tag"/)
   assert.doesNotMatch(card, /<van-tag[^>]*\bplain\b/)
   for (const component of [
@@ -205,7 +210,8 @@ test('freight list cards collapse sample blocks by default and allow expand', ()
 
   assert.match(presenter, /sampleBlocks:\s*rows\.map\(freightSampleBlock\)/)
   assert.match(card, /card\.sampleBlocks/)
-  assert.match(card, /block\.statusTone \|\| resolveStatusTone\(block\.status\)/)
+  assert.match(card, /resolveStatusTone\(block\.status, block\.statusTone\)/)
+  assert.match(card, /resolveStatusTone\(card\.value\.status, card\.value\.statusTone\)/)
   assert.match(card, /samplesOpen/)
   assert.match(card, /关联样本(/)
   assert.match(card, /点击展开/)
@@ -539,7 +545,7 @@ test('nested market detail cards use explicit labels instead of API property nam
   const detailSection = source('./components/MarketDetailSection.vue')
 
   assert.match(detailSection, /cards:/)
-  assert.match(detailSection, /field\.tagType \|\| resolveStatusTone\(field\.value\)/)
+  assert.match(detailSection, /resolveStatusTone\(field\.value, field\.tagType\)/)
   assert.doesNotMatch(detailSection, /function flattenFields/)
 })
 
@@ -684,7 +690,7 @@ test('protocol create and edit forms expose every writable web business field',
   assert.equal(bySection.samples, undefined)
   assert.deepEqual(bySection.attachments, [
     'protocolAttachments', 'consentFormUrls', 'healthSurveyFormUrls',
-    'attachmentUrls', 'infectiousDiseaseReportUrls'
+    'infectiousDiseaseReportUrls', 'attachmentUrls'
   ])
   const customer = module.formSections.find((section) => section.key === 'customer')
   assert.deepEqual(
@@ -708,6 +714,9 @@ test('protocol create and edit forms expose every writable web business field',
   assert.match(form, /protocolTotalFollowSamples/)
   assert.match(form, /syncProtocolTotalFromSamples/)
   assert.match(form, /sampleContractSum/)
+  assert.match(form, /refreshSampleCodes/)
+  assert.match(form, /@refresh-sample-codes="refreshSampleCodes"/)
+  assert.match(form, /codeFilter/)
   const samplePanel = source('../market/components/ProtocolSampleTransportPanel.vue')
   assert.match(samplePanel, /添加样本/)
   assert.match(samplePanel, /配置运费/)
@@ -722,6 +731,9 @@ test('protocol create and edit forms expose every writable web business field',
   assert.match(samplePanel, /codeSelectOptions/)
   assert.match(samplePanel, /searchable/)
   assert.match(samplePanel, /market-form-field/)
+  assert.match(samplePanel, /resolveSampleCodeFilter/)
+  assert.match(samplePanel, /refresh-sample-codes/)
+  assert.match(samplePanel, /sampleCodeFilter/)
   const payPanel = source('../market/components/ProtocolSamplePaymentPanel.vue')
   const transferEditor = source('../market/components/ScheduleTransferEditor.vue')
   assert.match(payPanel, /applyPreferentialPriceToPayment/)
@@ -755,6 +767,10 @@ test('protocol create and edit forms expose every writable web business field',
   assert.doesNotMatch(form, /:can-manage-transfers="true"/)
   assert.match(form, /presignDepositHint/)
   assert.match(form, /depositTransfers/)
+  assert.match(form, /PresignTransferCard/)
+  assert.match(form, /showDepositSection/)
+  assert.match(form, /<PresignTransferCard[\s\S]*disabled/)
+  assert.doesNotMatch(form, /class="market-deposit-transfer"/)
   const protocolModule = source('../market/modules/protocol.js')
   assert.match(protocolModule, /意向金(关联预签约)/)
   assert.match(protocolModule, /审核记录/)

+ 4 - 2
mobile-oa-h5/src/market/status-tone.js

@@ -1,12 +1,14 @@
-const VALID_TONES = new Set(['primary', 'success', 'warning', 'danger', 'info'])
+/** Vant Tag types only — no Element-Plus-style `info` (white text / no bg if used raw). */
+const VALID_TONES = new Set(['primary', 'success', 'warning', 'danger', 'default'])
 
 export function resolveStatusTone(statusText, backendTone) {
   const preferred = String(backendTone || '').trim().toLowerCase()
+  if (preferred === 'info') return 'default'
   if (VALID_TONES.has(preferred)) return preferred
   const status = String(statusText || '').trim().toLowerCase()
   if (/不通过|异常|拒绝|失败|逾期|作废|danger|error|reject/.test(status)) return 'danger'
   if (/待|未|部分|到期|过期|进行|warning|pending|processing/.test(status)) return 'warning'
   if (/完成|通过|正常|启用|生效|已缴清|已入库|已采集|已接收|已办理|success|completed/.test(status)) return 'success'
-  if (/停用|默认|info/.test(status)) return 'info'
+  if (/停用|默认|info/.test(status)) return 'default'
   return 'primary'
 }

+ 9 - 1
mobile-oa-h5/src/market/status-tone.test.js

@@ -4,10 +4,18 @@ import test from 'node:test'
 import { resolveStatusTone } from './status-tone.js'
 
 test('prefers a valid backend status tone', () => {
-  assert.equal(resolveStatusTone('待处理', 'info'), 'info')
+  assert.equal(resolveStatusTone('待处理', 'warning'), 'warning')
   assert.equal(resolveStatusTone('失败', 'not-a-tone'), 'danger')
 })
 
+test('maps Element Plus info tone to Vant default so unpaid tags stay visible', () => {
+  // Vant Tag has no `info`; bare van-tag--info is white text on transparent → invisible.
+  // Market CSS then skins van-tag--default as barley soft (#fff7e8 / #9a7020).
+  assert.equal(resolveStatusTone('待缴费', 'info'), 'default')
+  assert.equal(resolveStatusTone('停用'), 'default')
+  assert.equal(resolveStatusTone('', 'info'), 'default')
+})
+
 test('matches negative status before positive substrings', () => {
   assert.equal(resolveStatusTone('不通过'), 'danger')
   assert.equal(resolveStatusTone('通过'), 'success')

+ 11 - 15
mobile-oa-h5/src/styles/market.css

@@ -1214,22 +1214,10 @@ body.van-toast--unclickable .presign-order-page * {
   border-color: #f0c989;
 }
 
-.market-deposit-transfer {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 8px 12px;
-  align-items: center;
+.market-deposit-transfer-list {
+  display: grid;
+  gap: 12px;
   margin-top: 10px;
-  padding: 10px 12px;
-  border: 1px solid #e3f0ea;
-  border-radius: 12px;
-  background: #f7fffb;
-  color: #18332a;
-  font-size: 13px;
-}
-
-.market-deposit-transfer strong {
-  font-weight: 600;
 }
 
 .market-form-field {
@@ -1511,6 +1499,14 @@ body.van-toast--unclickable .presign-order-page * {
   background: var(--market-primary);
 }
 
+/* 中性态(info→default):大麦黄浅底,区别于 Vant 灰底白字与 warning 实心橙 */
+.market-page .van-tag--default,
+.presign-order-page .van-tag--default {
+  background: #fff7e8;
+  color: #9a7020;
+  border: 1px solid #f0e0b8;
+}
+
 @media (min-width: 414px) {
   .market-page__body {
     padding-right: 18px;