小程序迁移 静态页面完成!!!

This commit is contained in:
Neo
2026-03-18 05:14:35 +08:00
parent 72bb11b34f
commit 075caf067f
124 changed files with 10407 additions and 2738 deletions

View File

@@ -1,3 +1,3 @@
<view class="ai-inline-icon ai-color-{{color}}">
<image src="/assets/icons/ai-robot-inline.svg" mode="aspectFit" />
<image class="ai-inline-icon-img" src="/assets/icons/ai-robot-inline.svg" mode="aspectFit" />
</view>

View File

@@ -1,6 +1,6 @@
<view class="ai-title-badge ai-color-{{color}}">
<view class="ai-title-badge-icon">
<image src="/assets/icons/ai-robot-badge.svg" mode="aspectFit" />
<image class="ai-title-badge-icon-img" src="/assets/icons/ai-robot-badge.svg" mode="aspectFit" />
</view>
<text>{{label}}</text>
</view>

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,30 @@
Component({
properties: {
tag: {
type: String,
value: '',
},
category: {
type: String,
value: 'primary',
},
emoji: {
type: String,
value: '',
},
title: {
type: String,
value: '',
},
source: {
type: String,
value: '',
},
content: {
type: String,
value: '',
},
},
data: {},
methods: {},
})

View File

@@ -0,0 +1,14 @@
<view class="clue-item">
<view class="clue-tag clue-tag-{{category}}">
<text class="clue-tag-text">{{tag}}</text>
</view>
<view class="clue-content">
<view class="clue-text-container">
<text class="clue-text"><text class="clue-emoji">{{emoji}}</text> {{title}}</text>
<text class="clue-source">{{source}}</text>
</view>
</view>
<view class="clue-desc" wx:if="{{content}}">
<text class="clue-desc-text">{{content}}</text>
</view>
</view>

View File

@@ -0,0 +1,122 @@
.clue-item {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 18rpx;
padding: 22rpx;
background: #fafafa;
border-radius: 22rpx;
border: 2rpx solid var(--color-gray-4);
}
.clue-tag {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6rpx;
flex-shrink: 0;
}
.clue-tag-text {
font-size: 22rpx;
line-height: 1.3;
font-weight: 500;
text-align: center;
white-space: pre-line;
}
/* VI 规范 2.1 六种客户标签配色 */
/* 客户基础 — 蓝色 */
.clue-tag-primary {
background: rgba(0, 82, 217, 0.10);
color: #0052d9;
}
/* 消费习惯 — 绿色 */
.clue-tag-success {
background: rgba(0, 168, 112, 0.10);
color: #00a870;
}
/* 玩法偏好 — 橙色 */
.clue-tag-orange {
background: rgba(237, 123, 47, 0.12);
color: #ed7b2f;
}
/* 促销偏好 — 金色 */
.clue-tag-gold {
background: rgba(251, 191, 36, 0.15);
color: #d4920a;
}
/* 社交关系 — 紫色 */
.clue-tag-purple {
background: rgba(123, 97, 255, 0.10);
color: #7b61ff;
}
/* 重要反馈 — 红色 */
.clue-tag-error {
background: rgba(227, 77, 89, 0.10);
color: #e34d59;
}
/* 粉色(社交关系别名) */
.clue-tag-pink {
background: rgba(233, 30, 99, 0.10);
color: #e91e63;
}
/* warning 别名(促销偏好) */
.clue-tag-warning {
background: rgba(251, 191, 36, 0.15);
color: #d4920a;
}
.clue-content {
flex: 1;
min-width: 0;
}
.clue-text-container {
position: relative;
height: 72rpx;
overflow: hidden;
line-height: 36rpx;
}
.clue-emoji {
font-size: 28rpx;
}
.clue-text {
font-size: 26rpx;
line-height: 36rpx;
color: var(--color-gray-13);
word-break: break-all;
}
.clue-source {
position: absolute;
bottom: 0;
right: 0;
font-size: 24rpx;
line-height: 36rpx;
color: var(--color-gray-7);
white-space: nowrap;
background: linear-gradient(to left, #fafafa 70%, transparent);
padding-left: 50rpx;
z-index: 1;
}
.clue-desc {
line-height: 30rpx;
}
.clue-desc-text {
font-size: 22rpx;
color: var(--color-gray-9);
}

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,81 @@
/**
* coach-level-tag 助教等级标签组件
* 基于 VI 设计系统 §5 助教等级配色
*
* 用法:
* <coach-level-tag level="star" />
* <coach-level-tag level="{{detail.level}}" bgColor="rgba(255,255,255,0.15)" shadowColor="rgba(0,0,0,0.6)" />
*
* 属性:
* level: 'star' | 'senior' | 'middle' | 'junior'(英文 key
* bgColor: 半透明背景色,默认 '' (使用 CSS 各等级默认色)
* shadowColor: 文字阴影颜色,默认 '' (使用 CSS 默认 rgba(0,0,0,1))
*/
const LEVEL_MAP: Record<string, { cls: string; label: string }> = {
star: { cls: 'level-star', label: '⭐ 星级' },
senior: { cls: 'level-senior', label: '高级' },
middle: { cls: 'level-middle', label: '中级' },
junior: { cls: 'level-junior', label: '初级' },
// 兼容旧中文 key过渡期保留
'星级': { cls: 'level-star', label: '⭐ 星级' },
'高级': { cls: 'level-senior', label: '高级' },
'中级': { cls: 'level-middle', label: '中级' },
'初级': { cls: 'level-junior', label: '初级' },
}
Component({
properties: {
/** 等级 keystar | senior | middle | junior */
level: {
type: String,
value: '',
observer(this: any, val: string) {
this._updateStyle()
},
},
/** 自定义半透明背景色,例如 "rgba(255,255,255,0.15)",留空使用默认 */
bgColor: {
type: String,
value: '',
observer(this: any) {
this._updateStyle()
},
},
/** 自定义文字阴影颜色,例如 "rgba(0,0,0,0.4)",留空使用默认 */
shadowColor: {
type: String,
value: '',
observer(this: any) {
this._updateStyle()
},
},
},
data: {
cls: 'level-junior',
label: '',
tagStyle: '',
},
methods: {
_updateStyle(this: any) {
const val = this.data.level
const info = LEVEL_MAP[val] ?? { cls: 'level-junior', label: val }
const styles: string[] = []
if (this.data.bgColor) styles.push(`background:${this.data.bgColor}`)
if (this.data.shadowColor) styles.push(`text-shadow:0 0 6rpx ${this.data.shadowColor}`)
this.setData({
cls: info.cls,
label: info.label,
tagStyle: styles.join(';'),
})
},
},
lifetimes: {
attached(this: any) {
this._updateStyle()
},
},
})

View File

@@ -0,0 +1,4 @@
<!-- coach-level-tag 助教等级标签 -->
<view class="coach-level-tag-v">
<text class="coach-level-tag {{cls}}" style="{{tagStyle}}">{{label}}</text>
</view>

View File

@@ -0,0 +1,42 @@
/* coach-level-tag 助教等级标签组件样式
* 基于 VI 设计系统 §5 助教等级配色
*/
.coach-level-tag-v {
line-height: 32rpx;
}
.coach-level-tag {
display: inline-block;
padding: 4rpx 16rpx;
border-radius: 24rpx;
font-size: 22rpx;
flex-shrink: 0;
white-space: nowrap;
font-weight: 700;
text-shadow: 0 0 6rpx rgb(0, 0, 0);
}
/* ⭐ 星级 — 金黄色 */
.level-star {
color: #fbbf24;
background: rgba(251, 191, 36, 0.15);
}
/* 高级 — 粉色 */
.level-senior {
color: #e91e63;
background: rgba(233, 30, 99, 0.12);
}
/* 中级 — 橙色 */
.level-middle {
color: #ed7b2f;
background: rgba(237, 123, 47, 0.12);
}
/* 初级 — 蓝色 */
.level-junior {
color: #0052d9;
background: rgba(0, 82, 217, 0.10);
}

View File

@@ -21,6 +21,11 @@ Component({
type: Boolean,
value: true,
},
/** 是否显示评分区域 */
showRating: {
type: Boolean,
value: true,
},
},
data: {
@@ -45,7 +50,7 @@ Component({
if (val) {
// 打开弹窗时重置
this.setData({
ratingExpanded: !this.data.showExpandBtn, // 有展开按钮时默认收起,无按钮时默认展开
ratingExpanded: this.data.showRating && !this.data.showExpandBtn, // 只有在显示评分且无按钮时默认展开
serviceScore: 0,
returnScore: 0,
content: this.data.initialContent || '',
@@ -60,14 +65,17 @@ Component({
}
},
// 任意评分或内容变化时重新计算 canSave
'serviceScore, returnScore, content'(
'serviceScore, returnScore, content, showRating'(
serviceScore: number,
returnScore: number,
content: string
content: string,
showRating: boolean
) {
this.setData({
canSave: serviceScore > 0 && returnScore > 0 && content.trim().length > 0,
})
// 如果不显示评分,只需要有内容即可保存;否则需要评分和内容都有
const canSave = showRating
? serviceScore > 0 && returnScore > 0 && content.trim().length > 0
: content.trim().length > 0
this.setData({ canSave })
},
},

View File

@@ -4,7 +4,7 @@
<view class="modal-header">
<view class="header-left">
<text class="modal-title">添加备注</text>
<view class="expand-btn" wx:if="{{showExpandBtn}}" bindtap="onToggleExpand" hover-class="expand-btn--hover">
<view class="expand-btn" wx:if="{{showExpandBtn && showRating}}" bindtap="onToggleExpand" hover-class="expand-btn--hover">
<text class="expand-text">{{ratingExpanded ? '收起评价 ▴' : '展开评价 ▾'}}</text>
</view>
</view>
@@ -14,7 +14,7 @@
</view>
<!-- 评分区域 -->
<view class="rating-section" wx:if="{{ratingExpanded}}">
<view class="rating-section" wx:if="{{showRating && ratingExpanded}}">
<!-- 再次服务此客户 - 爱心 -->
<view class="rating-group">
<text class="rating-label">再次服务此客户</text>

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,25 @@
Component({
properties: {
/** 进度条填充百分比 0~100 */
filledPct: { type: Number, value: 0 },
/** 火星位置百分比 0~100夹紧在进度末端 */
clampedSparkPct: { type: Number, value: 0 },
/** 当前档位 0~5控制刻度高亮 */
currentTier: { type: Number, value: 0 },
/**
* 刻度数组(从接口传入,不写死)
* 每项:{ value: number, label: string, left: string, highlight?: boolean }
* left: 百分比字符串,如 '45.45%'
* highlight: 是否加粗高亮(如关键档位)
*/
ticks: { type: Array, value: [] },
/** 高光单次播放触发 */
shineRunning: { type: Boolean, value: false },
/** 火花单次播放触发 */
sparkRunning: { type: Boolean, value: false },
/** 高光动画时长(毫秒),由页面层按进度+速度计算后传入 */
shineDurMs: { type: Number, value: 1000 },
/** 火花动画时长(毫秒) */
sparkDurMs: { type: Number, value: 1400 },
},
})

View File

@@ -0,0 +1,65 @@
<!-- perf-progress-bar 组件
Props:
filledPct : number 进度条填充百分比 0~100
clampedSparkPct : number 火星位置 0~100夹紧在进度末端
currentTier : number 当前档位 0~5控制刻度高亮
ticks : Array<{value: number, label: string, left: string}> 刻度数组(接口传入)
shineRunning : boolean 高光单次播放触发
sparkRunning : boolean 火花单次播放触发
shineDurMs : number 高光动画时长(毫秒)
sparkDurMs : number 火花动画时长(毫秒)
-->
<view class="ppb-wrap">
<!-- 进度条轨道 -->
<view class="ppb-track">
<!-- 底轨背景 -->
<view class="ppb-track-bg"></view>
<!-- 渐变填充条 -->
<view
class="ppb-fill"
style="width: {{filledPct}}%"
wx:if="{{filledPct > 0}}"
>
<!-- 全宽渐变层 -->
<view class="ppb-gradient-bar"></view>
<!-- 高光 -->
<view
class="ppb-shine {{shineRunning ? 'ppb-shine--active' : ''}}"
style="animation-duration: {{shineDurMs}}ms"
></view>
</view>
<!-- 分隔竖线:由 ticks 数组动态渲染跳过第一个0和最后一个 -->
<view
wx:for="{{ticks}}"
wx:key="value"
wx:if="{{index > 0 && index < ticks.length - 1}}"
class="ppb-divider"
style="left: {{item.left}}"
></view>
<!-- 导火索火星 -->
<view
class="ppb-edge-glow {{sparkRunning ? 'ppb-edge-glow--active' : ''}}"
style="left: {{clampedSparkPct}}%; animation-duration: {{sparkDurMs}}ms"
>
<view class="ppb-spark ppb-spark-1 {{sparkRunning ? 'ppb-spark--active' : ''}}" style="animation-duration: {{sparkDurMs}}ms"></view>
<view class="ppb-spark ppb-spark-2 {{sparkRunning ? 'ppb-spark--active' : ''}}" style="animation-duration: {{sparkDurMs}}ms"></view>
<view class="ppb-spark ppb-spark-3 {{sparkRunning ? 'ppb-spark--active' : ''}}" style="animation-duration: {{sparkDurMs}}ms"></view>
<view class="ppb-spark ppb-spark-4 {{sparkRunning ? 'ppb-spark--active' : ''}}" style="animation-duration: {{sparkDurMs}}ms"></view>
<view class="ppb-spark ppb-spark-5 {{sparkRunning ? 'ppb-spark--active' : ''}}" style="animation-duration: {{sparkDurMs}}ms"></view>
<view class="ppb-spark ppb-spark-6 {{sparkRunning ? 'ppb-spark--active' : ''}}" style="animation-duration: {{sparkDurMs}}ms"></view>
</view>
</view>
<!-- 刻度 -->
<view class="ppb-ticks">
<text
wx:for="{{ticks}}"
wx:key="value"
class="ppb-tick {{currentTier >= index ? 'ppb-tick--done' : ''}} {{item.highlight ? 'ppb-tick--highlight' : ''}}"
style="{{index === 0 ? 'left:0' : index === ticks.length-1 ? 'right:0' : 'left:' + item.left + ';transform:translateX(-50%)'}}"
>{{item.label}}</text>
</view>
</view>

View File

@@ -0,0 +1,260 @@
/* perf-progress-bar 组件样式
* 所有 class 以 ppb- 为前缀,避免与页面样式冲突
*/
/* ── 外层容器:为刻度留出底部空间 ── */
.ppb-wrap {
position: relative;
padding-bottom: 10rpx; /* 为刻度行留空间 */
}
/* ── 进度条轨道 ── */
.ppb-track {
position: relative;
width: 100%;
height: 14rpx;
border-radius: 9rpx;
overflow: visible;
}
/* 底轨背景 */
.ppb-track-bg {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
border-radius: 9rpx;
background: var(--ppb-track-bg-color, rgba(255, 255, 255, 0.15));
}
/* 填充条:控制裁剪宽度 */
.ppb-fill {
position: absolute;
top: 0; left: 0; bottom: 0;
border-radius: 9rpx;
overflow: hidden;
transition: width 0.4s cubic-bezier(0.34, 1.2, 0.64, 1);
box-shadow: 0 0 14rpx rgba(239, 68, 68, 0.45);
}
/* 全宽渐变层 */
.ppb-gradient-bar {
position: absolute;
top: 0; left: 0; bottom: 0;
width: 100vw;
background: linear-gradient(
90deg,
#fde68a 0%,
#fbbf24 20%,
#f97316 45%,
#ef4444 70%,
#910a0a 100%
);
}
/* 分隔竖线 */
.ppb-divider {
position: absolute;
top: 0;
width: 8rpx;
height: 100%;
background: var(--ppb-divider-color, #5381D9);
transform: translateX(-50%);
pointer-events: none;
z-index: 2;
}
/* ══════════════════════════════════════════════════════
* 高光动画
* ★ 外观旋钮:
* --ppb-shine-width : 光束宽度(固定 rpx不随进度变化
* --ppb-shine-opacity: 峰值亮度 0~1
* --ppb-shine-color : RGB颜色如 255,220,100=暖黄
* ══════════════════════════════════════════════════════ */
.ppb-shine {
--ppb-shine-width: 120rpx;
--ppb-shine-opacity: 1.0;
--ppb-shine-color: 255, 255, 255;
position: absolute;
top: 0;
left: -130rpx;
width: var(--ppb-shine-width);
height: 100%;
background: linear-gradient(
90deg,
transparent 0%,
rgba(var(--ppb-shine-color), 0.08) 20%,
rgba(var(--ppb-shine-color), var(--ppb-shine-opacity)) 50%,
rgba(var(--ppb-shine-color), 0.08) 80%,
transparent 100%
);
animation: none;
pointer-events: none;
}
.ppb-shine--active {
animation: ppbShine 1s linear 1 forwards;
}
@keyframes ppbShine {
0% { left: -130rpx; opacity: 0; }
5% { opacity: 1; }
95% { opacity: 1; }
100% { left: calc(100% + 10rpx); opacity: 0; }
}
/* ══════════════════════════════════════════════════════
* 导火索效果
* ★ 外观旋钮:
* --ppb-spark-scale : 整体缩放
* --ppb-spark-pole-h: 光柱高度
* ══════════════════════════════════════════════════════ */
.ppb-edge-glow {
--ppb-spark-scale: 0.7;
--ppb-spark-pole-h: 30rpx;
position: absolute;
top: 50%;
transform: translate(-50%, -50%) scale(var(--ppb-spark-scale));
transform-origin: center center;
width: calc(var(--ppb-spark-pole-h) / 2);
height: var(--ppb-spark-pole-h);
border-radius: 999rpx;
background: rgba(255, 255, 255, 1);
opacity: 0.25;
box-shadow: 0 0 4rpx 2rpx rgba(255, 200, 80, 0.35);
animation: none;
pointer-events: none;
overflow: visible;
z-index: 10;
transition: opacity 0.1s, box-shadow 0.1s;
}
.ppb-edge-glow--active {
animation: ppbEdgePulse 1s ease-out 1 forwards;
}
@keyframes ppbEdgePulse {
0% { opacity: 1; box-shadow: 0 0 22rpx 12rpx rgba(255, 255, 255, 0.95); }
15% { opacity: 1; box-shadow: 0 0 26rpx 14rpx rgba(255, 220, 80, 0.90); }
55% { opacity: 0.6; box-shadow: 0 0 12rpx 6rpx rgba(255, 160, 40, 0.60); }
85% { opacity: 0.25; box-shadow: 0 0 4rpx 2rpx rgba(255, 200, 80, 0.35); }
100% { opacity: 0.25; box-shadow: 0 0 4rpx 2rpx rgba(255, 200, 80, 0.35); }
}
/* 火星粒子基础 */
.ppb-spark {
position: absolute;
border-radius: 999rpx;
pointer-events: none;
opacity: 0;
top: 50%;
left: 50%;
animation: none;
}
.ppb-spark--active {
animation-iteration-count: 1;
animation-fill-mode: forwards;
}
/* 粒子1右上亮白 */
.ppb-spark-1 { width: 10rpx; height: 10rpx; background: #ffffff; }
.ppb-spark-1.ppb-spark--active { animation-name: ppbSpark1; animation-timing-function: linear; }
@keyframes ppbSpark1 {
0% { opacity: 0; transform: translate( 0rpx, 0rpx) scale(0.0); }
8% { opacity: 1; transform: translate( 8rpx, -16rpx) scale(1.6); }
25% { opacity: 0.9; transform: translate( 16rpx, -30rpx) scale(1.2); }
45% { opacity: 0.7; transform: translate( 22rpx, -40rpx) scale(0.9); }
62% { opacity: 0.5; transform: translate( 27rpx, -48rpx) scale(0.7); }
76% { opacity: 0.3; transform: translate( 31rpx, -53rpx) scale(0.5); }
87% { opacity: 0.15; transform: translate( 34rpx, -57rpx) scale(0.3); }
100% { opacity: 0; transform: translate( 36rpx, -60rpx) scale(0.1); }
}
/* 粒子2右下橙色 */
.ppb-spark-2 { width: 12rpx; height: 12rpx; background: #fb923c; }
.ppb-spark-2.ppb-spark--active { animation-name: ppbSpark2; animation-timing-function: linear; }
@keyframes ppbSpark2 {
0% { opacity: 0; transform: translate( 0rpx, 0rpx) scale(0.0); }
12% { opacity: 1; transform: translate( 10rpx, 14rpx) scale(1.5); }
28% { opacity: 0.8; transform: translate( 19rpx, 24rpx) scale(1.1); }
46% { opacity: 0.6; transform: translate( 26rpx, 32rpx) scale(0.8); }
62% { opacity: 0.4; transform: translate( 31rpx, 38rpx) scale(0.6); }
76% { opacity: 0.25; transform: translate( 35rpx, 43rpx) scale(0.4); }
88% { opacity: 0.1; transform: translate( 38rpx, 47rpx) scale(0.25); }
100% { opacity: 0; transform: translate( 42rpx, 56rpx) scale(0.1); }
}
/* 粒子3正上黄色 */
.ppb-spark-3 { width: 8rpx; height: 8rpx; background: #fde68a; }
.ppb-spark-3.ppb-spark--active { animation-name: ppbSpark3; animation-timing-function: linear; }
@keyframes ppbSpark3 {
0% { opacity: 0; transform: translate( 0rpx, 0rpx) scale(0.0); }
6% { opacity: 1; transform: translate( 4rpx, -22rpx) scale(1.8); }
20% { opacity: 0.9; transform: translate( 7rpx, -36rpx) scale(1.3); }
38% { opacity: 0.7; transform: translate( 9rpx, -48rpx) scale(1.0); }
55% { opacity: 0.5; transform: translate( 10rpx, -57rpx) scale(0.7); }
70% { opacity: 0.3; transform: translate( 11rpx, -63rpx) scale(0.5); }
84% { opacity: 0.15; transform: translate( 11rpx, -68rpx) scale(0.3); }
100% { opacity: 0; transform: translate( 12rpx, -74rpx) scale(0.1); }
}
/* 粒子4右斜上红橙拖尾 */
.ppb-spark-4 { width: 16rpx; height: 6rpx; background: #ef4444; }
.ppb-spark-4.ppb-spark--active { animation-name: ppbSpark4; animation-timing-function: linear; }
@keyframes ppbSpark4 {
0% { opacity: 0; transform: translate( 0rpx, 0rpx) rotate( 0deg) scale(0.0); }
10% { opacity: 1; transform: translate( 14rpx, -10rpx) rotate(-20deg) scale(1.4); }
26% { opacity: 0.8; transform: translate( 24rpx, -16rpx) rotate(-30deg) scale(1.0); }
44% { opacity: 0.6; transform: translate( 32rpx, -21rpx) rotate(-38deg) scale(0.75); }
60% { opacity: 0.4; transform: translate( 39rpx, -26rpx) rotate(-45deg) scale(0.55); }
74% { opacity: 0.25; transform: translate( 44rpx, -30rpx) rotate(-50deg) scale(0.4); }
87% { opacity: 0.1; transform: translate( 48rpx, -33rpx) rotate(-53deg) scale(0.25); }
100% { opacity: 0; transform: translate( 58rpx, -40rpx) rotate(-55deg) scale(0.1); }
}
/* 粒子5正右黄白 */
.ppb-spark-5 { width: 10rpx; height: 10rpx; background: #fbbf24; }
.ppb-spark-5.ppb-spark--active { animation-name: ppbSpark5; animation-timing-function: linear; }
@keyframes ppbSpark5 {
0% { opacity: 0; transform: translate( 0rpx, 0rpx) scale(0.0); }
5% { opacity: 1; transform: translate( 18rpx, 2rpx) scale(1.7); }
20% { opacity: 0.85; transform: translate( 30rpx, 3rpx) scale(1.2); }
38% { opacity: 0.65; transform: translate( 40rpx, 4rpx) scale(0.9); }
55% { opacity: 0.45; transform: translate( 47rpx, 5rpx) scale(0.7); }
70% { opacity: 0.3; transform: translate( 52rpx, 6rpx) scale(0.5); }
84% { opacity: 0.15; transform: translate( 56rpx, 7rpx) scale(0.3); }
100% { opacity: 0; transform: translate( 60rpx, 8rpx) scale(0.1); }
}
/* 粒子6右下斜淡橙 */
.ppb-spark-6 { width: 14rpx; height: 14rpx; background: #fed7aa; }
.ppb-spark-6.ppb-spark--active { animation-name: ppbSpark6; animation-timing-function: linear; }
@keyframes ppbSpark6 {
0% { opacity: 0; transform: translate( 0rpx, 0rpx) scale(0.0); }
15% { opacity: 0.9; transform: translate( 6rpx, 18rpx) scale(1.5); }
30% { opacity: 0.75; transform: translate( 10rpx, 28rpx) scale(1.1); }
47% { opacity: 0.55; transform: translate( 14rpx, 36rpx) scale(0.85); }
62% { opacity: 0.4; transform: translate( 17rpx, 43rpx) scale(0.65); }
75% { opacity: 0.25; transform: translate( 19rpx, 48rpx) scale(0.5); }
87% { opacity: 0.1; transform: translate( 21rpx, 53rpx) scale(0.35); }
100% { opacity: 0; transform: translate( 24rpx, 64rpx) scale(0.1); }
}
/* ── 刻度 ── */
.ppb-ticks {
position: absolute;
width: 100%;
top: 100%;
margin-top: 6rpx;
height: 24rpx;
}
.ppb-tick {
position: absolute;
font-size: 16rpx;
color: var(--ppb-tick-color, rgba(255, 255, 255, 0.55));
transition: color 0.3s ease, font-weight 0.3s ease;
}
.ppb-tick--done {
color: var(--ppb-tick-done-color, rgba(255, 255, 255, 0.95));
font-weight: 600;
}
.ppb-tick--highlight {
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,32 @@
/**
* service-record-card — 服务记录单条卡片组件
* 用于 task-detail「60天内服务记录」和 customer-service-records 列表
*
* 数据约定(两端统一传原始数字,组件负责格式化):
* hours / hoursRaw — 小时数number组件展示时加 h 后缀
* income — 金额number组件展示时加 ¥ 前缀
*/
Component({
properties: {
/** 时间,如 "2026-02-07 21:30" */
time: { type: String, value: '' },
/** 课程标签文字,如 "基础课" "包厢课" "打赏课" */
courseLabel: { type: String, value: '' },
/** 课程样式 class 后缀basic / vip / tip / recharge */
typeClass: { type: String, value: 'basic' },
/** 卡片类型:默认 courserecharge=充值提成 */
type: { type: String, value: 'course' },
/** 台桌号,如 "A12号台" */
tableNo: { type: String, value: '' },
/** 折算后小时数(原始数字,如 2.5),组件展示时加 h 后缀 */
hours: { type: Number, value: 0 },
/** 折算前小时数(原始数字,如 3.0),组件展示时加 h 后缀 */
hoursRaw: { type: Number, value: 0 },
/** 商品/饮品描述 */
drinks: { type: String, value: '' },
/** 金额(原始数字,如 200组件展示时加 ¥ 前缀 */
income: { type: Number, value: 0 },
/** 是否为预估金额(显示「预估」标签) */
isEstimate: { type: Boolean, value: false },
},
})

View File

@@ -0,0 +1,22 @@
<!-- service-record-card 组件 —— 60天内服务记录单条卡片 -->
<view class="svc-card {{type === 'recharge' ? 'svc-card--recharge' : ''}}" hover-class="svc-card--hover">
<!-- 第一行:左=时间,右=课程+台桌+小时数 -->
<view class="svc-row svc-row1">
<text class="svc-time">{{time}}</text>
<view class="svc-right1">
<text class="svc-course-tag svc-course-{{typeClass}}">{{type === 'recharge' ? '充值' : courseLabel}}</text>
<text class="svc-table-label" wx:if="{{tableNo && type !== 'recharge'}}">{{tableNo}}</text>
<text class="svc-hours" wx:if="{{hours && type !== 'recharge'}}">{{hours}}h</text>
<text class="svc-hours-raw" wx:if="{{hoursRaw && hoursRaw !== hours && type !== 'recharge'}}">折前{{hoursRaw}}h</text>
</view>
</view>
<!-- 第二行:左=商品,右=金额(含预估/提成) -->
<view class="svc-row svc-row2">
<text class="svc-drinks">{{drinks || '—'}}</text>
<view class="svc-income-wrap">
<text class="svc-income-est" wx:if="{{isEstimate && type !== 'recharge'}}">预估</text>
<text class="svc-income-label">{{type === 'recharge' ? '提成' : '到手'}}</text>
<text class="svc-income {{type === 'recharge' ? 'svc-income--recharge' : ''}}">¥{{income}}</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,136 @@
/* service-record-card 组件样式 */
.svc-card {
padding: 22rpx 26rpx;
border-radius: 22rpx;
border: 2rpx solid #eeeeee;
background: linear-gradient(135deg, #fafafa 0%, #fff 100%);
}
.svc-card--hover {
background: #f5f5f5;
}
.svc-card--recharge {
background: linear-gradient(135deg, #fffef0 0%, #fffbeb 100%);
border-color: #fef3c7;
}
/* 行通用 */
.svc-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.svc-row1 {
margin-bottom: 14rpx;
}
/* 第一行左:时间 */
.svc-time {
font-size: 24rpx;
line-height: 32rpx;
color: #8b8b8b;
}
/* 第一行右:课程+台桌+小时 */
.svc-right1 {
display: flex;
align-items: center;
gap: 10rpx;
}
.svc-course-tag {
font-size: 22rpx;
line-height: 29rpx;
font-weight: 500;
padding: 3rpx 12rpx;
border-radius: 8rpx;
}
.svc-course-basic {
background: rgba(0, 168, 112, 0.10);
color: #00a870;
}
.svc-course-vip {
background: rgba(0, 82, 217, 0.10);
color: #0052d9;
}
.svc-course-tip {
background: rgba(237, 123, 47, 0.10);
color: #ed7b2f;
}
.svc-course-recharge {
background: rgba(212, 160, 23, 0.12);
color: #b45309;
}
.svc-table-label {
font-size: 22rpx;
line-height: 29rpx;
color: #5a5a5a;
font-weight: 500;
}
.svc-hours {
font-size: 28rpx;
line-height: 36rpx;
font-weight: 700;
color: #393939;
font-variant-numeric: tabular-nums;
}
.svc-hours-raw {
font-size: 20rpx;
line-height: 29rpx;
color: #b9b9b9;
}
/* 第二行左:商品 */
.svc-drinks {
font-size: 22rpx;
line-height: 29rpx;
color: #8b8b8b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 340rpx;
}
/* 第二行右:金额 */
.svc-income-wrap {
display: flex;
align-items: baseline;
gap: 20rpx;
flex-shrink: 0;
}
.svc-income-label {
font-size: 20rpx;
line-height: 26rpx;
color: #a6a6a6;
}
.svc-income {
font-size: 30rpx;
line-height: 36rpx;
font-weight: 700;
color: #242424;
font-variant-numeric: tabular-nums;
}
.svc-income--recharge {
color: #b45309;
}
.svc-income-est {
font-size: 20rpx;
line-height: 26rpx;
color: #ed7b2f;
font-weight: 500;
margin-right: -14rpx;
}