瀏覽代碼

P2 打磨四项:焦点环/品牌 Logo 组件/占位页升级/负余额跌色

- P2-1 App.vue 全局 :focus-visible 焦点环(排除 el- 组件防双环)
- P2-2 新增 brand-mark.vue 内联 SVG 放大镜 Logo,替换登录页/机构选择页 ◍ 字符
- P2-3 占位页升级为品牌卡片(边框圆角+青蓝「开发中」徽标+el-empty 居中)
- P2-4 费用页负余额接线跌色 --app-num-down(涨红跌绿,财务域强制)

门禁:vue-tsc 0 错 / eslint 0 错 / build 0 错 / dist 无 mock 特征串
Q 10 小時之前
父節點
當前提交
17d5f974e8

+ 10 - 0
src/App.vue

@@ -35,4 +35,14 @@ body {
   font-family: var(--app-font);
   color: var(--app-color-text);
 }
+/*
+ * 全局焦点环(design-tokens.md §4 可达性基线:焦点环 2px solid 青蓝深 可见)。
+ * 用 :focus-visible(仅键盘 Tab 显示,鼠标点击不误显)+ outline(不占布局、不压控件自身样式)。
+ * 排除 el- 前缀的 Element Plus 控件——EP 自带 --el-focus-color 焦点环,叠加会成双重环;
+ * 此规则兜底非 EP 可聚焦元素(链接 / 侧栏菜单 / 自定义控件)的键盘可达性。
+ */
+:focus-visible:not([class^='el-']) {
+  outline: 2px solid var(--app-brand-teal-deep);
+  outline-offset: 2px;
+}
 </style>

+ 41 - 0
src/components/brand-mark.vue

@@ -0,0 +1,41 @@
+<script setup lang="ts">
+/**
+ * brand-mark.vue —— 品牌标识块(登录页 / 作用域选择页复用;P2-2 替代 unicode ◍ 占位)。
+ *
+ * 设计源:frontend-pc/00-design 的 Logo「宝妈小助手」主元素为**放大镜**(靛蓝框 + 墨蓝手柄 + 青蓝主体)。
+ * 本组件用内联 SVG 抽象该意象:青蓝圆角底(--app-brand-teal)+ 白色放大镜(圆环 + 45° 手柄)。
+ * 离线安全:纯内联 SVG,无外网字体/图片依赖;配色全部走 token,尺寸由 size 控制(圆角随宽等比)。
+ *
+ * 入参:size(块宽高 px,缺省 44;SVG 线条占块宽约 52%)。
+ */
+withDefaults(defineProps<{
+  /** 标识块边长(px) */
+  size?: number;
+}>(), { size: 44 });
+</script>
+
+<template>
+  <span class="brand-mark" :style="{ width: size + 'px', height: size + 'px' }" aria-hidden="true">
+    <svg class="brand-mark__svg" viewBox="0 0 24 24" fill="none">
+      <circle cx="10" cy="10" r="6.2" stroke="currentColor" stroke-width="2.2" />
+      <line x1="14.6" y1="14.6" x2="20.4" y2="20.4" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" />
+    </svg>
+  </span>
+</template>
+
+<style scoped>
+/* 品牌块:青蓝底(token)+ 白色线条 + 浅青光环(呼应原型 .brand-mark) */
+.brand-mark {
+  display: inline-grid;
+  place-items: center;
+  flex: 0 0 auto;
+  border-radius: 22%;
+  background: var(--app-brand-teal);
+  color: #fff;
+  box-shadow: 0 0 0 4px rgba(75, 184, 200, 0.25);
+}
+.brand-mark__svg {
+  width: 52%;
+  height: 52%;
+}
+</style>

+ 7 - 2
src/views/academic/fee/index.vue

@@ -285,7 +285,7 @@ const ops: FeeOp[] = [
 
     <!-- ② 仪表盘 -->
     <el-row v-if="dash" :gutter="12" class="fee-page__dash">
-      <el-col :span="4"><div class="stat-card"><div class="stat-card__label">账户余额</div><div class="stat-card__value">¥ {{ formatMoney(dash.balance) }}</div></div></el-col>
+      <el-col :span="4"><div class="stat-card"><div class="stat-card__label">账户余额</div><div class="stat-card__value" :class="{ 'stat-card__value--neg': Number(dash.balance) < 0 }">¥ {{ formatMoney(dash.balance) }}</div></div></el-col>
       <el-col :span="4"><div class="stat-card"><div class="stat-card__label">累计充值</div><div class="stat-card__value">¥ {{ formatMoney(dash.totalIncome) }}</div></div></el-col>
       <el-col :span="4"><div class="stat-card"><div class="stat-card__label">累计支出</div><div class="stat-card__value">¥ {{ formatMoney(dash.totalExpenditure) }}</div></div></el-col>
       <el-col :span="4"><div class="stat-card"><div class="stat-card__label">特殊费用</div><div class="stat-card__value">¥ {{ formatMoney(dash.specialExpenses) }}</div></div></el-col>
@@ -294,7 +294,7 @@ const ops: FeeOp[] = [
           <template #header>校区余额分布</template>
           <div v-for="d in dash.balanceDistribution" :key="d.campusId" class="fee-page__dist">
             <span>{{ campusName(d.campusId) }}</span>
-            <span class="fee-page__dist-num">¥ {{ formatMoney(d.balance) }}</span>
+            <span class="fee-page__dist-num" :class="{ 'fee-page__dist-num--neg': Number(d.balance) < 0 }">¥ {{ formatMoney(d.balance) }}</span>
           </div>
         </el-card>
       </el-col>
@@ -430,6 +430,11 @@ const ops: FeeOp[] = [
   color: var(--app-color-text);
   font-variant-numeric: tabular-nums;
 }
+/* 负余额 = 赤字(跌色 --app-num-down,财务域强制涨红跌绿;后端允许扣成负数) */
+.stat-card__value--neg,
+.fee-page__dist-num--neg {
+  color: var(--app-num-down);
+}
 .fee-page__tabs {
   margin-top: 4px;
 }

+ 34 - 7
src/views/common/placeholder.vue

@@ -4,8 +4,10 @@
  *
  * 为何占位:这些菜单项(题库/练习分析/用户角色/任务监控/操作日志)属教学/系统域,
  *   尚未落地 mock 端点与后端契约;为避免菜单点击后打未覆盖端点弹 "mock 未覆盖" toast,
- *   占位页**不发任何请求**,仅静态展示"建设中"。后续对应域补 mock 端点后替换本页。
+ *   占位页**不发任何请求**,仅静态展示"开发中"。后续对应域补 mock 端点后替换本页。
  *
+ * 视觉(P2-3 统一"开发中"卡片,token §3 卡片口径):白卡 + 边框 + 圆角,居中;
+ *   el-empty 承载说明,mark 用页签前 2 字(墨蓝块)点明"哪个模块",配"开发中"角标。
  * 入参:title(页签名)/ desc(可选说明)。
  */
 defineProps<{
@@ -18,11 +20,14 @@ defineProps<{
 
 <template>
   <div class="placeholder">
-    <el-empty :description="desc || '该模块功能建设中,敬请期待'">
-      <template #image>
-        <div class="placeholder__mark">{{ title.slice(0, 2) }}</div>
-      </template>
-    </el-empty>
+    <div class="placeholder__card">
+      <span class="placeholder__badge">开发中</span>
+      <el-empty :description="desc || '该模块功能建设中,敬请期待'">
+        <template #image>
+          <div class="placeholder__mark">{{ title.slice(0, 2) }}</div>
+        </template>
+      </el-empty>
+    </div>
   </div>
 </template>
 
@@ -31,6 +36,28 @@ defineProps<{
   width: 100%;
   padding: 48px 0;
 }
+/* 品牌卡片(token §3:卡片 8 圆角、border、内容居中;无阴影——阴影仅浮层用) */
+.placeholder__card {
+  position: relative;
+  max-width: 560px;
+  margin: 0 auto;
+  background: var(--app-color-card);
+  border: 1px solid var(--app-color-border);
+  border-radius: 8px;
+  padding: 40px 24px 32px;
+}
+/* "开发中"角标(teal 浅底深字,主色系点缀) */
+.placeholder__badge {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  background: var(--app-color-teal-soft);
+  color: var(--app-color-teal-ink);
+  border-radius: 4px;
+  padding: 2px 10px;
+  font-size: var(--app-font-sm);
+  font-weight: 600;
+}
 .placeholder__mark {
   width: 64px;
   height: 64px;
@@ -40,7 +67,7 @@ defineProps<{
   justify-content: center;
   font-size: 20px;
   font-weight: 600;
-  color: #fff;
+  color: var(--app-side-text);
   background: var(--app-brand-navy);
 }
 </style>

+ 3 - 11
src/views/login/index.vue

@@ -20,6 +20,7 @@
  * SFC 骨架顺序(README §3.2):imports → props/emits(无)→ store 注入 → 状态 → 生命周期 → 方法。
  */
 import { toDataURL } from 'qrcode';
+import BrandMark from '@/components/brand-mark.vue';
 import { issue, login } from '@/api/auth/login';
 import type { MembershipView } from '@/api/auth/login';
 import { fetchWechatQrcode, pollWechatQrcodeStatus } from '@/api/auth/wechat';
@@ -251,7 +252,7 @@ onUnmounted(() => {
     <!-- 左:品牌区(墨蓝;窄屏隐藏)—— 对齐原型 .login-brand -->
     <aside class="login-brand">
       <div class="brand-logo">
-        <div class="brand-mark">◍</div>
+        <BrandMark :size="44" />
         <div class="brand-text"><b>源思维 · 统一后台</b><span>教务 × 教学 · 多机构多校区</span></div>
       </div>
       <p class="login-slogan">一个账号,<br>跨校区、跨角色<em>无缝切换</em>。</p>
@@ -357,16 +358,7 @@ onUnmounted(() => {
   position: relative;
   z-index: 1;
 }
-.brand-mark {
-  width: 44px;
-  height: 44px;
-  border-radius: 10px;
-  background: var(--app-brand-teal);
-  display: grid;
-  place-items: center;
-  font-size: 22px;
-  box-shadow: 0 0 0 4px rgba(75, 184, 200, 0.25);
-}
+/* 品牌块视觉(.brand-mark)由 @/components/brand-mark.vue 自带,此处不再重复定义 */
 .brand-text b {
   font-size: 17px;
   font-weight: 600;

+ 3 - 12
src/views/org/scope-select.vue

@@ -18,6 +18,7 @@
  *   票据 5min 有效、单次消费;本页未签发前刷新会丢失过渡态,需回登录页重走①。
  */
 import { issue } from '@/api/auth/login';
+import BrandMark from '@/components/brand-mark.vue';
 import { useLogin } from '@/store/login';
 
 const login = useLogin();
@@ -105,7 +106,7 @@ async function handleLogout(): Promise<void> {
     <div class="scope-card">
       <!-- 欢迎区(对齐原型:brand-mark + 欢迎语 + 副标题) -->
       <div class="scope-welcome">
-        <div class="brand-mark">◍</div>
+        <BrandMark :size="38" />
         <div class="scope-welcome__text">
           <b>欢迎回来,{{ welcomeName }}</b>
           <div class="scope-welcome__sub">请选择进入的机构 / 校区与角色</div>
@@ -187,17 +188,7 @@ async function handleLogout(): Promise<void> {
   gap: 12px;
   margin-bottom: 6px;
 }
-.brand-mark {
-  width: 38px;
-  height: 38px;
-  border-radius: 8px;
-  background: var(--app-brand-teal);
-  display: grid;
-  place-items: center;
-  font-size: 19px;
-  flex: 0 0 auto;
-  box-shadow: 0 0 0 4px rgba(75, 184, 200, 0.25);
-}
+/* 品牌块视觉(.brand-mark)由 @/components/brand-mark.vue 自带,此处不再重复定义 */
 .scope-welcome__text b {
   font-size: 16px;
   font-weight: 600;