fix(ui): 全局清除残留Emoji并升级性别标签为纯矢量SVG图标,落实TDD规范门禁
这个提交包含在:
@@ -9,11 +9,11 @@
|
||||
### 1.1 风格定位:Cyber-Esports Dark Elegance(暗夜流光电竞风)
|
||||
- **核心气质**:高端电竞质感、沉浸式互动、年轻潮酷、信息轻盈清晰。
|
||||
- **避免的设计反模式(Anti-Patterns to Avoid)**:
|
||||
- ❌ 杜绝花哨刺眼的廉价荧光大撞色;
|
||||
- ❌ 杜绝使用任何系统 Emoji 代替产品功能操作图标(全部采用统一风格的矢量 SVG 图标);
|
||||
- ❌ 杜绝点击无反馈或延迟过长(点击态需在 100ms 内给予 0.98 缩放或透明度微反馈);
|
||||
- ❌ 杜绝按钮或卡片按压时引起页面排版位移(Layout Shift / CLS 必须 < 0.1);
|
||||
- ❌ 杜绝纯依靠单一颜色传递状态(成功/失败/在线必须辅以图标或文字辅助标签)。
|
||||
- [严禁] 杜绝花哨刺眼的廉价荧光大撞色;
|
||||
- [严禁] 杜绝使用任何系统 Emoji 代替产品功能操作图标(全部采用统一风格的矢量 SVG 图标);
|
||||
- [严禁] 杜绝点击无反馈或延迟过长(点击态需在 100ms 内给予 0.98 缩放或透明度微反馈);
|
||||
- [严禁] 杜绝按钮或卡片按压时引起页面排版位移(Layout Shift / CLS 必须 < 0.1);
|
||||
- [严禁] 杜绝纯依靠单一颜色传递状态(成功/失败/在线必须辅以图标或文字辅助标签)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -207,5 +207,21 @@ export const ICONS = {
|
||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||
</svg>
|
||||
`),
|
||||
|
||||
female: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#EC4899" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="9" r="5"></circle>
|
||||
<line x1="12" y1="14" x2="12" y2="21"></line>
|
||||
<line x1="9" y1="18" x2="15" y2="18"></line>
|
||||
</svg>
|
||||
`),
|
||||
|
||||
male: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#60A5FA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="10" cy="14" r="5"></circle>
|
||||
<line x1="19" y1="5" x2="13.6" y2="10.4"></line>
|
||||
<polyline points="15 5 19 5 19 9"></polyline>
|
||||
</svg>
|
||||
`)
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<view class="banner-card" :style="{ background: banner.bg }">
|
||||
<view class="banner-tag">{{ banner.tag }}</view>
|
||||
<view class="banner-title">{{ banner.title }}</view>
|
||||
<view class="banner-sub">点击立即参与 ⚡</view>
|
||||
<view class="banner-sub">点击立即参与</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
@@ -106,7 +106,9 @@
|
||||
<view class="card-body">
|
||||
<view class="row-nickname">
|
||||
<text class="nickname">{{ expert.nickname }}</text>
|
||||
<text class="gender-tag" :class="expert.gender">{{ expert.gender === 'female' ? '♀' : '♂' }}</text>
|
||||
<view class="gender-tag" :class="expert.gender">
|
||||
<image class="gender-svg" :src="expert.gender === 'female' ? ICONS.female : ICONS.male" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="rank-badge">{{ expert.rankTag }}</view>
|
||||
|
||||
@@ -540,17 +542,23 @@ const navTo = (url) => {
|
||||
}
|
||||
|
||||
.gender-tag {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 8rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.female {
|
||||
background: rgba(236, 72, 153, 0.2);
|
||||
color: #F472B6;
|
||||
}
|
||||
&.male {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #60A5FA;
|
||||
}
|
||||
|
||||
.gender-svg {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
74
server/tests/no-emoji.spec.ts
普通文件
74
server/tests/no-emoji.spec.ts
普通文件
@@ -0,0 +1,74 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
describe('UI/UX 规范门禁 (TDD 测试套件: 全面杜绝系统 Emoji)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const uniPagesDir = path.join(rootDir, 'pwuniapp', 'pages');
|
||||
const docsDir = path.join(rootDir, 'docs');
|
||||
|
||||
// 严格 Emoji 正则:匹配所有系统 Emoji、符号表情及特殊 Pictographic 字符
|
||||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||||
|
||||
function getAllFiles(dir: string, extFilter: string[]): string[] {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
let results: string[] = [];
|
||||
const list = fs.readdirSync(dir);
|
||||
for (const item of list) {
|
||||
const fullPath = path.join(dir, item);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
results = results.concat(getAllFiles(fullPath, extFilter));
|
||||
} else if (extFilter.some(ext => item.endsWith(ext))) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('1. 首页 (index.vue) 杜绝系统 Emoji 校验', () => {
|
||||
it('首页横幅与卡片中不应含有 ⚡ 或任何系统 Emoji', () => {
|
||||
const indexVuePath = path.join(uniPagesDir, 'index', 'index.vue');
|
||||
const content = fs.readFileSync(indexVuePath, 'utf-8');
|
||||
|
||||
// 验证不包含 ⚡ 闪电 Emoji
|
||||
expect(content).not.toContain('⚡');
|
||||
|
||||
// 验证不包含裸写的 ♀ / ♂ 字符(防止系统字体回退为彩色系统 Emoji)
|
||||
expect(content).not.toContain('♀');
|
||||
expect(content).not.toContain('♂');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 前端 pages 目录下所有页面全局无 Emoji 校验', () => {
|
||||
it('所有 .vue 页面组件模板与文本中严禁包含任何 Emoji 字符', () => {
|
||||
const vueFiles = getAllFiles(uniPagesDir, ['.vue']);
|
||||
const violations: { file: string; line: number; text: string }[] = [];
|
||||
|
||||
for (const file of vueFiles) {
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
lines.forEach((line, idx) => {
|
||||
if (emojiRegex.test(line)) {
|
||||
violations.push({
|
||||
file: path.relative(rootDir, file),
|
||||
line: idx + 1,
|
||||
text: line.trim()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. UIUX 约束规范文档纯净度校验', () => {
|
||||
it('设计规范文档中严禁使用 ❌ 系统 Emoji', () => {
|
||||
const docFiles = getAllFiles(docsDir, ['.md']);
|
||||
for (const file of docFiles) {
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
expect(content).not.toContain('❌');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户