feat(fe): 新增 seg-tabs 与 stat-ring 组件

seg-tabs 合并三套长得像但尺寸各不同的横向切换:plan 的 .inline-tabs
(灰槽白块)、community 的 .feed-tab(白药丸+阴影)、record 的 .rf-chip
(白药丸无阴影,高度还差 10rpx)。variant="solid|pill" 二选一,items
同时支持字符串数组和 {key,label} 数组。

stat-ring 是首页门面用的完成度环。用 SVG data-uri 画,和 record 页的
体重折线图同一套路——canvas 在 scroll-view 里的层级、滚动跟随和截图
行为全是坑。中心内容走 slot。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-29 10:36:35 +08:00
parent 91caac1b92
commit 15ff59e6db
9 changed files with 170 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
// 横向切换控件。此前有三套长得像但尺寸各不同的实现:
// plan 的 .inline-tabs(灰槽 + 白块)、community 的 .feed-tab(白药丸 + 橙选中 + 阴影)、
// record 的 .rf-chip(白药丸 + 橙选中 + 无阴影,高度还差 10rpx)
// 这里合成一个:variant="solid" 对应灰槽白块,variant="pill" 对应横滑药丸。
//
// items 支持两种写法:
// ['推荐','关注','经验'] —— current 传下标
// [{key:'',label:'全部'},{key:'weight',label:'体重'}] —— current 传 key
Component({
options: { addGlobalClass: true },
properties: {
items: { type: Array, value: [] },
current: { type: null, value: 0 },
variant: { type: String, value: 'pill' },
},
data: { list: [] },
observers: {
'items,current': function (items, current) {
const byKey = typeof current === 'string';
const list = (items || []).map((it, i) => {
const obj = it && typeof it === 'object';
const key = obj ? it.key : i;
return {
key,
label: obj ? it.label : it,
active: byKey ? key === current : i === Number(current),
};
});
this.setData({ list });
},
},
methods: {
onTap(e) {
const { index, key } = e.currentTarget.dataset;
this.triggerEvent('change', { index: Number(index), key });
},
},
});
+4
View File
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+9
View File
@@ -0,0 +1,9 @@
<scroll-view wx:if="{{variant === 'pill'}}" class="st st-pill" scroll-x="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
<view wx:for="{{list}}" wx:key="key" class="st-item {{item.active ? 'on' : ''}}"
data-index="{{index}}" data-key="{{item.key}}" bindtap="onTap">{{item.label}}</view>
</scroll-view>
<view wx:else class="st st-solid">
<view wx:for="{{list}}" wx:key="key" class="st-item {{item.active ? 'on' : ''}}"
data-index="{{index}}" data-key="{{item.key}}" bindtap="onTap">{{item.label}}</view>
</view>
+21
View File
@@ -0,0 +1,21 @@
.st{white-space:nowrap}
/* 横滑药丸:数量不定、可滚动(社区分类、记录类型筛选) */
.st-pill{padding:0 0 var(--sp-4)}
.st-pill .st-item{
display:inline-block;height:var(--h-sm);line-height:var(--h-sm);padding:0 var(--sp-4);
margin-right:var(--sp-2);border-radius:var(--r-full);border:1rpx solid var(--line);
background:#fff;color:var(--muted);font-size:var(--fs-sm);font-weight:var(--fw-b);
}
.st-pill .st-item.on{color:var(--primary-dark);background:var(--primary-soft);border-color:var(--primary-line)}
/* 灰槽白块:数量固定的互斥视图(计划页三段) */
.st-solid{
display:flex;gap:var(--sp-1);padding:var(--sp-1);border-radius:var(--r-md);
background:#EFE8DE;margin-bottom:var(--sp-4);
}
.st-solid .st-item{
flex:1;height:var(--h-sm);line-height:var(--h-sm);text-align:center;border-radius:var(--r-sm);
color:var(--muted);font-size:var(--fs-md);font-weight:var(--fw-b);
}
.st-solid .st-item.on{background:#fff;color:var(--primary-dark)}
+41
View File
@@ -0,0 +1,41 @@
// 完成度环。用 SVG data-uri 画(和 record 页的体重折线图同一套路),不用 canvas:
// canvas 在 scroll-view 里的层级、滚动跟随和截图行为全是坑。
const TAU = 2 * Math.PI;
const R = 42; // viewBox 100 下的半径,留出描边宽度
function ringSvg(percent, stroke, track, fill) {
const c = TAU * R;
const p = Math.max(0, Math.min(100, Number(percent) || 0));
const off = c * (1 - p / 100);
const circle = (color, dash) =>
'<circle cx="50" cy="50" r="' + R + '" fill="none" stroke="' + color +
'" stroke-width="' + stroke + '" stroke-linecap="round"' + dash + '/>';
return (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
circle(track, '') +
'<g transform="rotate(-90 50 50)">' +
circle(fill, ' stroke-dasharray="' + c.toFixed(1) + '" stroke-dashoffset="' + off.toFixed(1) + '"') +
'</g></svg>'
);
}
Component({
options: { addGlobalClass: true },
properties: {
percent: { type: Number, value: 0 },
size: { type: Number, value: 180 }, // rpx
stroke: { type: Number, value: 8 }, // viewBox 单位,即直径的百分比
track: { type: String, value: '#FFE6BA' },
fill: { type: String, value: '#F5A947' },
},
data: { src: '', boxStyle: '' },
observers: {
'percent,size,stroke,track,fill': function (percent, size, stroke, track, fill) {
const svg = ringSvg(percent, stroke, track, fill);
this.setData({
src: 'data:image/svg+xml,' + encodeURIComponent(svg),
boxStyle: 'width:' + size + 'rpx;height:' + size + 'rpx',
});
},
},
});
+4
View File
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+4
View File
@@ -0,0 +1,4 @@
<view class="ring" style="{{boxStyle}}">
<image class="ring-img" src="{{src}}" mode="widthFix"></image>
<view class="ring-center"><slot></slot></view>
</view>
+6
View File
@@ -0,0 +1,6 @@
.ring{position:relative;flex:none}
.ring-img{width:100%;height:100%;display:block}
.ring-center{
position:absolute;left:0;top:0;right:0;bottom:0;
display:flex;flex-direction:column;align-items:center;justify-content:center;
}