feat(tabbar): 底部菜单增加框架自带原生tabBar,首屏对齐微官网门户并适配安全区间距
这个提交包含在:
@@ -0,0 +1,45 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
describe('uni-app 框架自带原生 tabBar 规范测试 (TDD Red -> Green)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../../mpuniapp');
|
||||
const pagesJsonPath = path.join(rootDir, 'pages.json');
|
||||
|
||||
it('1. pages.json 中必须配置合法且符合 uni-app 原生规范的 tabBar', () => {
|
||||
expect(fs.existsSync(pagesJsonPath)).toBe(true);
|
||||
const content = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
// 去除 json 中的注释
|
||||
const cleanJson = content.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, '$1');
|
||||
const config = JSON.parse(cleanJson);
|
||||
|
||||
expect(config.tabBar).toBeDefined();
|
||||
expect(Array.isArray(config.tabBar.list)).toBe(true);
|
||||
expect(config.tabBar.list.length).toBeGreaterThanOrEqual(2);
|
||||
expect(config.tabBar.list.length).toBeLessThanOrEqual(5);
|
||||
|
||||
// 关键规范:tabBar.list[0].pagePath 必须与 pages[0].path 一致,原生 tabBar 才能在首屏正常加载
|
||||
expect(config.tabBar.list[0].pagePath).toBe(config.pages[0].path);
|
||||
expect(config.tabBar.list[0].text).toBe('官网');
|
||||
|
||||
// 校验颜色 Token
|
||||
expect(config.tabBar.color.toLowerCase()).toBe('#667085');
|
||||
expect(config.tabBar.selectedColor.toLowerCase()).toBe('#0b5cff');
|
||||
});
|
||||
|
||||
it('2. tabBar 中声明的所有图标资源文件必须真实存在', () => {
|
||||
const content = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
const cleanJson = content.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, '$1');
|
||||
const config = JSON.parse(cleanJson);
|
||||
|
||||
for (const item of config.tabBar.list) {
|
||||
if (item.iconPath) {
|
||||
const iconFile = path.join(rootDir, item.iconPath);
|
||||
expect(fs.existsSync(iconFile)).toBe(true);
|
||||
}
|
||||
if (item.selectedIconPath) {
|
||||
const selIconFile = path.join(rootDir, item.selectedIconPath);
|
||||
expect(fs.existsSync(selIconFile)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
4699
mpuniapp/pages.json
4699
mpuniapp/pages.json
文件差异内容过多而无法显示
加载差异
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="portal-page" :class="pageRtlClass">
|
||||
<template>
|
||||
<view class="portal-page" :class="pageRtlClass" :style="pageTheme">
|
||||
<view class="locale-bar">
|
||||
<view class="locale-selector-container">
|
||||
<i18n-selector :model-value="currentLang" @change="switchLang" />
|
||||
@@ -196,6 +196,25 @@ import {
|
||||
import I18nSelector from '../../components/i18n-selector/i18n-selector.uvue';
|
||||
import AudioPronounce from '../../components/audio-pronounce/audio-pronounce.uvue';
|
||||
import { releaseAudioPlayer } from './useAudioPlayer.uts';
|
||||
import { utsLightColors, utsRadius, utsSpacing } from '../../common/tokens.uts';
|
||||
|
||||
const pageTheme: Record<string, string> = {
|
||||
'--oc-primary': utsLightColors.primary,
|
||||
'--oc-primary-soft': utsLightColors.primarySoft,
|
||||
'--oc-ai': utsLightColors.ai,
|
||||
'--oc-bg': utsLightColors.bg,
|
||||
'--oc-surface': utsLightColors.surface,
|
||||
'--oc-border': utsLightColors.border,
|
||||
'--oc-text': utsLightColors.text,
|
||||
'--oc-text-2': utsLightColors.text2,
|
||||
'--oc-radius-btn': utsRadius.button,
|
||||
'--oc-radius-card': utsRadius.card,
|
||||
'--oc-space-4': utsSpacing.space4,
|
||||
'--oc-space-8': utsSpacing.space8,
|
||||
'--oc-space-12': utsSpacing.space12,
|
||||
'--oc-space-16': utsSpacing.space16,
|
||||
'--oc-space-24': utsSpacing.space24,
|
||||
};
|
||||
|
||||
const defaultAvatar = 'https://cdn.omnicard.ai/assets/default-avatar.png';
|
||||
|
||||
@@ -639,60 +658,68 @@ onUnload(() => {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
/* 2. 快捷动作条 */
|
||||
/* 2. 快捷动作条 (4列规整网格,触控区 >= 44x44) */
|
||||
.quick-action-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--oc-space-16, 16px);
|
||||
}
|
||||
.action-item {
|
||||
flex: 1 1 20%;
|
||||
min-width: 64px;
|
||||
min-height: 52px;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E3E7ED;
|
||||
border-radius: 12px;
|
||||
margin: 0 4px;
|
||||
width: calc(25% - 6px);
|
||||
min-height: 56px;
|
||||
background-color: var(--oc-bg, #FFFFFF);
|
||||
border: 1px solid var(--oc-border, #E3E7ED);
|
||||
border-radius: var(--oc-radius-btn, 12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 4px;
|
||||
box-sizing: border-box;
|
||||
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.action-item:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.action-icon-box {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.action-icon-text {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #0B5CFF;
|
||||
color: var(--oc-primary, #0B5CFF);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.action-item-label {
|
||||
font-size: 11px;
|
||||
color: #101828;
|
||||
color: var(--oc-text, #101828);
|
||||
}
|
||||
|
||||
/* 3. Bento Grid 网格 */
|
||||
/* 3. Bento Grid 网格 (便当盒美学) */
|
||||
.bento-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.bento-card {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E3E7ED;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
background-color: var(--oc-bg, #FFFFFF);
|
||||
border: 1px solid var(--oc-border, #E3E7ED);
|
||||
border-radius: var(--oc-radius-card, 16px);
|
||||
padding: var(--oc-space-16, 16px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||
transition: transform 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.bento-card:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
.bento-col-1 {
|
||||
width: 48.5%;
|
||||
width: calc(50% - 6px);
|
||||
}
|
||||
.bento-col-2 {
|
||||
width: 100%;
|
||||
@@ -700,13 +727,13 @@ onUnload(() => {
|
||||
.bento-tag {
|
||||
align-self: flex-start;
|
||||
padding: 2px 8px;
|
||||
background-color: #F7F8FA;
|
||||
background-color: var(--oc-surface, #F7F8FA);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.bento-tag-text {
|
||||
font-size: 11px;
|
||||
color: #667085;
|
||||
color: var(--oc-text-2, #667085);
|
||||
font-weight: 500;
|
||||
}
|
||||
.bento-cover {
|
||||
@@ -714,56 +741,73 @@ onUnload(() => {
|
||||
height: 120px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #F7F8FA;
|
||||
background-color: var(--oc-surface, #F7F8FA);
|
||||
}
|
||||
.bento-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
color: var(--oc-text, #101828);
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.3;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.bento-content {
|
||||
font-size: 13px;
|
||||
color: #667085;
|
||||
line-height: 1.4;
|
||||
color: var(--oc-text-2, #667085);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 底部垫高与常驻主 CTA */
|
||||
/* 底部垫高与常驻主 CTA (视口安全区 safe-area-inset-bottom 适配) */
|
||||
.portal-bottom-spacer {
|
||||
height: 80px;
|
||||
height: calc(140px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
.ai-entry {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 96px;
|
||||
bottom: calc(80px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 8px 12px;
|
||||
color: #7C5CFF;
|
||||
font-size: 12px;
|
||||
padding: 8px 14px;
|
||||
color: var(--oc-ai, #7C5CFF);
|
||||
background-color: var(--oc-bg, #FFFFFF);
|
||||
border: 1px solid var(--oc-border, #E3E7ED);
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 2px 8px rgba(124, 92, 255, 0.12);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.portal-cta-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 12px 16px 24px 16px;
|
||||
background-color: #FFFFFF;
|
||||
border-top: 1px solid #E3E7ED;
|
||||
bottom: var(--window-bottom, 0px);
|
||||
padding: 12px var(--oc-space-16, 16px) calc(16px + env(safe-area-inset-bottom, 0px)) var(--oc-space-16, 16px);
|
||||
background-color: var(--oc-bg, #FFFFFF);
|
||||
border-top: 1px solid var(--oc-border, #E3E7ED);
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
}
|
||||
.primary-cta-btn {
|
||||
height: 48px;
|
||||
background-color: #0B5CFF;
|
||||
border-radius: 12px;
|
||||
min-height: 44px;
|
||||
background-color: var(--oc-primary, #0B5CFF);
|
||||
border-radius: var(--oc-radius-btn, 12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
border-width: 0;
|
||||
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.primary-cta-btn:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.primary-cta-btn-text {
|
||||
color: #FFFFFF;
|
||||
color: var(--oc-bg, #FFFFFF);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 987 B |
二进制
mpuniapp/static/tab-portal.png
普通文件
二进制
mpuniapp/static/tab-portal.png
普通文件
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 1.0 KiB |
在新工单中引用
屏蔽一个用户