微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 是否显示 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
/** 跳转目标页面 */
|
||||
targetUrl: {
|
||||
type: String,
|
||||
value: '/pages/chat/chat',
|
||||
},
|
||||
/** 可选:携带客户 ID 参数 */
|
||||
customerId: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 距底部距离(rpx),TabBar 页面用 200,非 TabBar 页面用 120 */
|
||||
bottom: {
|
||||
type: Number,
|
||||
value: 200,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTap() {
|
||||
let url = this.data.targetUrl
|
||||
if (this.data.customerId) {
|
||||
url += `?customerId=${this.data.customerId}`
|
||||
}
|
||||
wx.navigateTo({
|
||||
url,
|
||||
fail: () => {
|
||||
wx.showToast({ title: '页面跳转失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
<!-- AI 悬浮对话按钮 — SVG 机器人 + 渐变流动背景 -->
|
||||
<view
|
||||
class="ai-float-btn-container"
|
||||
wx:if="{{visible}}"
|
||||
style="bottom: {{bottom}}rpx"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<view class="ai-float-btn">
|
||||
<!-- 高光叠加层 -->
|
||||
<view class="ai-float-btn-highlight"></view>
|
||||
<!-- 机器人 SVG(小程序用 image 引用) -->
|
||||
<image class="ai-icon-svg" src="/assets/icons/ai-robot.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,57 @@
|
||||
/* AI 悬浮按钮 — 忠于 H5 原型:渐变流动背景 + 机器人 SVG */
|
||||
/* H5: 56px → 56×2×0.875 = 98rpx */
|
||||
|
||||
.ai-float-btn-container {
|
||||
position: fixed;
|
||||
right: 28rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.ai-float-btn {
|
||||
width: 98rpx;
|
||||
height: 98rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* 渐变动画背景 — 忠于 H5 原型 */
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #f5576c 75%, #667eea 100%);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 8s ease infinite;
|
||||
box-shadow: 0 8rpx 40rpx rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.ai-float-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 高光叠加层 */
|
||||
.ai-float-btn-highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(145deg, rgba(255,255,255,0.2) 0%, transparent 50%, rgba(0,0,0,0.1) 100%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 背景渐变流动动画 */
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
25% { background-position: 50% 100%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
75% { background-position: 50% 0%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* SVG 图标 H5: 30px → 52rpx */
|
||||
.ai-icon-svg {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** Banner 主题色 */
|
||||
theme: {
|
||||
type: String,
|
||||
value: 'blue',
|
||||
},
|
||||
/** Banner 标题 */
|
||||
title: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 指标列表 [{label, value}] */
|
||||
metrics: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** 背景图加载失败时降级为纯渐变色(CSS 已处理) */
|
||||
onBgError() {
|
||||
// 背景图加载失败,CSS 渐变色自动降级,无需额外处理
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
<view class="banner banner--{{theme}}">
|
||||
<view class="banner-bg"></view>
|
||||
<view class="banner-overlay">
|
||||
<text class="banner-title">{{title}}</text>
|
||||
<view class="banner-metrics">
|
||||
<view class="metric-item" wx:for="{{metrics}}" wx:key="label">
|
||||
<text class="metric-value">{{item.value}}</text>
|
||||
<text class="metric-label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,66 @@
|
||||
.banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 主题渐变降级(背景图加载失败时) */
|
||||
.banner--blue { background: linear-gradient(135deg, #0052d9, #0080ff); }
|
||||
.banner--red { background: linear-gradient(135deg, #e34d59, #ff6b6b); }
|
||||
.banner--orange { background: linear-gradient(135deg, #ed7b2f, #ffaa44); }
|
||||
.banner--pink { background: linear-gradient(135deg, #d94da0, #ff6bcc); }
|
||||
.banner--teal { background: linear-gradient(135deg, #00a870, #00d68f); }
|
||||
.banner--coral { background: linear-gradient(135deg, #e06c5a, #ff8a7a); }
|
||||
.banner--dark-gold { background: linear-gradient(135deg, #8b6914, #c9a227); }
|
||||
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.banner-metrics {
|
||||
display: flex;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: var(--font-2xl);
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: var(--font-xs);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 自定义底部导航栏 — 非 TabBar 页面模拟系统导航
|
||||
Component({
|
||||
properties: {
|
||||
/** 当前激活的 tab: task / board / my */
|
||||
active: {
|
||||
type: String,
|
||||
value: 'board',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const tab = e.currentTarget.dataset.tab as string
|
||||
if (tab === this.data.active) return
|
||||
|
||||
if (tab === 'task') {
|
||||
wx.switchTab({ url: '/pages/task-list/task-list' })
|
||||
} else if (tab === 'board') {
|
||||
wx.switchTab({ url: '/pages/board-finance/board-finance' })
|
||||
} else if (tab === 'my') {
|
||||
wx.switchTab({ url: '/pages/my-profile/my-profile' })
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- 自定义底部导航栏 — 用于非 TabBar 的看板子页面,SVG icon 忠于 H5 原型 -->
|
||||
<view class="tab-bar">
|
||||
<view
|
||||
class="tab-bar-item {{active === 'task' ? 'tab-bar-item--active' : ''}}"
|
||||
bindtap="onTap"
|
||||
data-tab="task"
|
||||
>
|
||||
<image
|
||||
class="tab-bar-icon"
|
||||
src="{{active === 'task' ? '/assets/icons/tab-task-nav-active.svg' : '/assets/icons/tab-task-nav.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="tab-bar-label">任务</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-bar-item {{active === 'board' ? 'tab-bar-item--active' : ''}}"
|
||||
bindtap="onTap"
|
||||
data-tab="board"
|
||||
>
|
||||
<image
|
||||
class="tab-bar-icon"
|
||||
src="{{active === 'board' ? '/assets/icons/tab-board-nav-active.svg' : '/assets/icons/tab-board-nav.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="tab-bar-label">看板</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-bar-item {{active === 'my' ? 'tab-bar-item--active' : ''}}"
|
||||
bindtap="onTap"
|
||||
data-tab="my"
|
||||
>
|
||||
<image
|
||||
class="tab-bar-icon"
|
||||
src="{{active === 'my' ? '/assets/icons/tab-my-nav-active.svg' : '/assets/icons/tab-my-nav.svg'}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="tab-bar-label">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,39 @@
|
||||
/* 自定义底部导航栏 — 模拟系统 TabBar 外观 */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
height: 100rpx;
|
||||
background: #ffffff;
|
||||
border-top: 1rpx solid #eeeeee;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.tab-bar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.tab-bar-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.tab-bar-label {
|
||||
font-size: 20rpx;
|
||||
color: #8b8b8b;
|
||||
}
|
||||
|
||||
.tab-bar-item--active .tab-bar-label {
|
||||
color: #0052d9;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 开发调试浮动按钮组件
|
||||
*
|
||||
* 仅在 develop 环境下显示,点击跳转到 dev-tools 页面。
|
||||
* 使用 movable-view 实现可拖拽。
|
||||
*/
|
||||
Component({
|
||||
data: {
|
||||
visible: false,
|
||||
x: 580, // 初始位置:右下角附近(rpx 换算后的 px 近似值)
|
||||
y: 1100,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
// 仅 develop 环境显示
|
||||
const accountInfo = wx.getAccountInfoSync()
|
||||
const env = accountInfo.miniProgram.envVersion
|
||||
this.setData({ visible: env === "develop" })
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
goDevTools() {
|
||||
wx.navigateTo({ url: "/pages/dev-tools/dev-tools" })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
开发调试浮动按钮 — 仅 develop 环境渲染
|
||||
可拖拽,点击跳转到 dev-tools 页面
|
||||
-->
|
||||
<movable-area wx:if="{{visible}}" class="fab-area">
|
||||
<movable-view
|
||||
class="fab-btn"
|
||||
direction="all"
|
||||
x="{{x}}"
|
||||
y="{{y}}"
|
||||
bindtap="goDevTools"
|
||||
>
|
||||
<text class="fab-icon">🛠</text>
|
||||
</movable-view>
|
||||
</movable-area>
|
||||
@@ -0,0 +1,29 @@
|
||||
/* 浮动按钮覆盖全屏,不阻挡页面交互 */
|
||||
.fab-area {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.fab-btn {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(24, 144, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.25);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 40rpx;
|
||||
line-height: 96rpx;
|
||||
text-align: center;
|
||||
width: 96rpx;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 筛选标签文字 */
|
||||
label: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 选项列表 */
|
||||
options: {
|
||||
type: Array,
|
||||
value: [] as Array<{ value: string; text: string }>,
|
||||
},
|
||||
/** 当前选中值 */
|
||||
value: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
'value, options'(val: string, opts: Array<{ value: string; text: string }>) {
|
||||
const matched = (opts || []).find((o) => o.value === val)
|
||||
this.setData({ selectedText: matched ? matched.text : '' })
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
expanded: false,
|
||||
selectedText: '',
|
||||
panelTop: 0,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const { value, options } = this.data
|
||||
const matched = (options as Array<{ value: string; text: string }>).find(
|
||||
(o) => o.value === value,
|
||||
)
|
||||
this.setData({ selectedText: matched ? matched.text : '' })
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
if (!this.data.options || (this.data.options as any[]).length === 0) return
|
||||
|
||||
if (!this.data.expanded) {
|
||||
// 展开时计算按钮底部位置,作为面板 top
|
||||
this.createSelectorQuery()
|
||||
.select('.filter-dropdown')
|
||||
.boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
this.setData({
|
||||
panelTop: rect.bottom,
|
||||
expanded: true,
|
||||
})
|
||||
} else {
|
||||
this.setData({ expanded: true })
|
||||
}
|
||||
})
|
||||
.exec()
|
||||
} else {
|
||||
this.setData({ expanded: false })
|
||||
}
|
||||
},
|
||||
|
||||
onSelect(e: WechatMiniprogram.TouchEvent) {
|
||||
const val = e.currentTarget.dataset.value as string
|
||||
const opts = this.data.options as Array<{ value: string; text: string }>
|
||||
const matched = opts.find((o) => o.value === val)
|
||||
this.setData({
|
||||
expanded: false,
|
||||
selectedText: matched ? matched.text : '',
|
||||
})
|
||||
this.triggerEvent('change', { value: val })
|
||||
},
|
||||
|
||||
/** 点击遮罩层关闭 */
|
||||
onMaskTap() {
|
||||
this.setData({ expanded: false })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
<!-- 筛选下拉组件 — 全屏宽度面板 + 遮罩层 -->
|
||||
<view class="filter-dropdown-wrap" wx:if="{{options && options.length > 0}}">
|
||||
<view class="filter-dropdown {{expanded ? 'filter-dropdown--active' : ''}}" bindtap="toggleDropdown">
|
||||
<text class="filter-label">{{selectedText || label}}</text>
|
||||
<t-icon name="caret-down-small" size="32rpx" class="filter-arrow {{expanded ? 'filter-arrow--up' : ''}}" />
|
||||
</view>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<view class="dropdown-mask" wx:if="{{expanded}}" catchtap="onMaskTap" />
|
||||
|
||||
<!-- 全屏宽度下拉面板 -->
|
||||
<view
|
||||
class="dropdown-panel {{expanded ? 'dropdown-panel--show' : ''}}"
|
||||
style="top: {{panelTop}}px"
|
||||
>
|
||||
<view
|
||||
class="dropdown-item {{item.value === value ? 'dropdown-item--active' : ''}}"
|
||||
wx:for="{{options}}"
|
||||
wx:key="value"
|
||||
data-value="{{item.value}}"
|
||||
bindtap="onSelect"
|
||||
>
|
||||
<text>{{item.text}}</text>
|
||||
<t-icon wx:if="{{item.value === value}}" name="check" size="32rpx" color="var(--color-primary)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,107 @@
|
||||
.filter-dropdown-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 触发按钮 */
|
||||
.filter-dropdown {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-md);
|
||||
border: 2rpx solid var(--color-gray-1);
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.filter-dropdown--active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-gray-12);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-dropdown--active .filter-label {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 箭头旋转动画 */
|
||||
.filter-arrow {
|
||||
transition: transform 0.25s ease;
|
||||
color: var(--color-gray-6);
|
||||
}
|
||||
|
||||
.filter-arrow--up {
|
||||
transform: rotate(180deg);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 遮罩层 — 半透明黑色背景,忠于 H5 原型 */
|
||||
.dropdown-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 下拉面板 — 全屏宽度,固定定位,从筛选栏下方展开 */
|
||||
.dropdown-panel {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
border-radius: 0 0 28rpx 28rpx;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transform: translateY(-16rpx);
|
||||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||||
pointer-events: none;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.dropdown-panel--show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 选项项 — 更大的 padding,忠于 H5 原型 */
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 34rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-gray-12, #2c2c2c);
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.dropdown-item:active {
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
}
|
||||
|
||||
.dropdown-item--active {
|
||||
color: var(--color-primary, #0052d9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dropdown-item + .dropdown-item {
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 评分 0-10,超出范围自动 clamp */
|
||||
score: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
heartEmoji: '💙',
|
||||
},
|
||||
|
||||
observers: {
|
||||
score(val: number) {
|
||||
const s = val < 0 ? 0 : val > 10 ? 10 : val
|
||||
let emoji: string
|
||||
if (s > 8.5) {
|
||||
emoji = '💖'
|
||||
} else if (s > 7) {
|
||||
emoji = '🧡'
|
||||
} else if (s > 5) {
|
||||
emoji = '💛'
|
||||
} else {
|
||||
emoji = '💙'
|
||||
}
|
||||
this.setData({ heartEmoji: emoji })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
<text class="heart-icon">{{heartEmoji}}</text>
|
||||
@@ -0,0 +1,6 @@
|
||||
.heart-icon {
|
||||
font-size: 22rpx;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
top: -4rpx;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/** type → Emoji + 标签文字映射 */
|
||||
const TAG_MAP: Record<string, { emoji: string; label: string }> = {
|
||||
chinese: { emoji: '🎱', label: '中式' },
|
||||
snooker: { emoji: '斯', label: '斯诺克' },
|
||||
mahjong: { emoji: '🀅', label: '麻将' },
|
||||
karaoke: { emoji: '🎤', label: 'K歌' },
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
/** 喜好类型 */
|
||||
type: {
|
||||
type: String,
|
||||
value: 'chinese',
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
type(val: string) {
|
||||
const tag = TAG_MAP[val] || { emoji: '❓', label: val }
|
||||
this.setData({ emoji: tag.emoji, label: tag.label })
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
emoji: '🎱',
|
||||
label: '中式',
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const tag = TAG_MAP[this.data.type] || { emoji: '❓', label: this.data.type }
|
||||
this.setData({ emoji: tag.emoji, label: tag.label })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
<view class="hobby-tag">
|
||||
<text class="tag-emoji">{{emoji}}</text>
|
||||
<text class="tag-label">{{label}}</text>
|
||||
</view>
|
||||
@@ -0,0 +1,19 @@
|
||||
.hobby-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: var(--color-gray-1);
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-9);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tag-emoji {
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.tag-label {
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 指标名称 */
|
||||
title: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 指标数值(已格式化字符串) */
|
||||
value: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
/** 单位(元/次/人) */
|
||||
unit: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 环比趋势 */
|
||||
trend: {
|
||||
type: String,
|
||||
value: 'flat', // 'up' | 'down' | 'flat'
|
||||
},
|
||||
/** 环比数值(如 "+12.5%") */
|
||||
trendValue: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 帮助说明文字 */
|
||||
helpText: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
value(val: string | null | undefined) {
|
||||
// null/undefined 显示 "--"
|
||||
this.setData({
|
||||
displayValue: val == null || val === '' ? '--' : val,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
displayValue: '--',
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const val = this.data.value
|
||||
this.setData({
|
||||
displayValue: val == null || val === '' ? '--' : val,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTap() {
|
||||
this.triggerEvent('tap')
|
||||
},
|
||||
onHelpTap(e: WechatMiniprogram.TouchEvent) {
|
||||
// 阻止冒泡到卡片 tap
|
||||
e.stopPropagation?.()
|
||||
this.triggerEvent('helpTap')
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
<view class="metric-card" bindtap="onTap">
|
||||
<view class="metric-header">
|
||||
<text class="metric-title">{{title}}</text>
|
||||
<view class="metric-help" wx:if="{{helpText}}" catchtap="onHelpTap">
|
||||
<t-icon name="help-circle" size="32rpx" color="#a6a6a6" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="metric-body">
|
||||
<text class="metric-value">{{displayValue}}</text>
|
||||
<text class="metric-unit" wx:if="{{unit}}">{{unit}}</text>
|
||||
</view>
|
||||
|
||||
<view class="metric-trend" wx:if="{{trendValue}}">
|
||||
<view class="trend-tag trend-{{trend}}">
|
||||
<t-icon wx:if="{{trend === 'up'}}" name="arrow-up" size="24rpx" />
|
||||
<t-icon wx:elif="{{trend === 'down'}}" name="arrow-down" size="24rpx" />
|
||||
<text wx:else class="trend-flat-icon">-</text>
|
||||
<text class="trend-text">{{trendValue}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,82 @@
|
||||
.metric-card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24rpx;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.metric-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.metric-help {
|
||||
padding: 4rpx;
|
||||
}
|
||||
|
||||
.metric-body {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.metric-unit {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trend-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
|
||||
/* 上升 → 绿色 */
|
||||
.trend-up {
|
||||
color: var(--color-success);
|
||||
background: rgba(0, 168, 112, 0.08);
|
||||
}
|
||||
|
||||
/* 下降 → 红色 */
|
||||
.trend-down {
|
||||
color: var(--color-error);
|
||||
background: rgba(227, 77, 89, 0.08);
|
||||
}
|
||||
|
||||
/* 持平 → 灰色 */
|
||||
.trend-flat {
|
||||
color: var(--color-gray-7);
|
||||
background: rgba(139, 139, 139, 0.08);
|
||||
}
|
||||
|
||||
.trend-flat-icon {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.trend-text {
|
||||
font-size: var(--font-xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-rate": "tdesign-miniprogram/rate/rate",
|
||||
"t-textarea": "tdesign-miniprogram/textarea/textarea",
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 控制弹窗显示/隐藏 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
/** 客户名(弹窗标题显示) */
|
||||
customerName: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
/** 初始评分 0-10 */
|
||||
initialScore: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
/** 初始备注内容 */
|
||||
initialContent: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
'visible, initialScore, initialContent'(visible: boolean) {
|
||||
if (visible) {
|
||||
const clamped = Math.max(0, Math.min(10, this.data.initialScore))
|
||||
this.setData({
|
||||
starValue: clamped / 2,
|
||||
content: this.data.initialContent,
|
||||
score: clamped,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
/** 星星值 0-5(半星制) */
|
||||
starValue: 0,
|
||||
/** 内部评分 0-10 */
|
||||
score: 0,
|
||||
/** 备注内容 */
|
||||
content: '',
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** 星星评分变化 */
|
||||
onRateChange(e: WechatMiniprogram.CustomEvent<{ value: number }>) {
|
||||
const starVal = e.detail.value
|
||||
this.setData({
|
||||
starValue: starVal,
|
||||
score: starVal * 2,
|
||||
})
|
||||
},
|
||||
|
||||
/** 文本内容变化 */
|
||||
onContentChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
this.setData({ content: e.detail.value })
|
||||
},
|
||||
|
||||
/** 确认提交 */
|
||||
onConfirm() {
|
||||
if (!this.data.content.trim()) return
|
||||
this.triggerEvent('confirm', {
|
||||
score: this.data.score,
|
||||
content: this.data.content.trim(),
|
||||
})
|
||||
},
|
||||
|
||||
/** 取消关闭 */
|
||||
onCancel() {
|
||||
this.triggerEvent('cancel')
|
||||
},
|
||||
|
||||
/** 阻止冒泡 */
|
||||
noop() {},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
<view class="modal-mask" wx:if="{{visible}}" catchtap="onCancel">
|
||||
<view class="modal-content" catchtap="noop">
|
||||
<!-- 头部 -->
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">添加备注{{customerName ? ' - ' + customerName : ''}}</text>
|
||||
<view class="modal-close" bindtap="onCancel">
|
||||
<t-icon name="close" size="40rpx" color="#a6a6a6" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评分区域 -->
|
||||
<view class="rating-section">
|
||||
<text class="rating-label">评分</text>
|
||||
<t-rate
|
||||
value="{{starValue}}"
|
||||
count="{{5}}"
|
||||
allow-half="{{true}}"
|
||||
size="48rpx"
|
||||
bind:change="onRateChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 文本输入 -->
|
||||
<view class="textarea-section">
|
||||
<t-textarea
|
||||
value="{{content}}"
|
||||
placeholder="请输入备注内容..."
|
||||
maxlength="{{500}}"
|
||||
autosize="{{true}}"
|
||||
bind:change="onContentChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="modal-footer">
|
||||
<t-button theme="default" size="large" block bindtap="onCancel">取消</t-button>
|
||||
<t-button
|
||||
theme="primary"
|
||||
size="large"
|
||||
block
|
||||
disabled="{{!content.trim()}}"
|
||||
bindtap="onConfirm"
|
||||
>确认</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,68 @@
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border-radius: 48rpx 48rpx 0 0;
|
||||
padding: 40rpx 40rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
}
|
||||
|
||||
.rating-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.rating-label {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.textarea-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.modal-footer t-button {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-rate": "tdesign-miniprogram/rate/rate"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
Component({
|
||||
properties: {
|
||||
/** 评分 0-10,内部转换为 0-5 星 */
|
||||
score: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
},
|
||||
/** 星星尺寸 */
|
||||
size: {
|
||||
type: String,
|
||||
value: '40rpx',
|
||||
},
|
||||
/** 是否只读 */
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
score(val: number) {
|
||||
// score(0-10) → star(0-5)
|
||||
const clamped = Math.max(0, Math.min(10, val))
|
||||
this.setData({ starValue: clamped / 2 })
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
starValue: 0,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const clamped = Math.max(0, Math.min(10, this.data.score))
|
||||
this.setData({ starValue: clamped / 2 })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
<t-rate
|
||||
value="{{starValue}}"
|
||||
count="{{5}}"
|
||||
allow-half
|
||||
size="{{size}}"
|
||||
disabled="{{readonly}}"
|
||||
/>
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
Reference in New Issue
Block a user