微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
8
apps/miniprogram - 副本/miniprogram/pages/apply/apply.json
Normal file
8
apps/miniprogram - 副本/miniprogram/pages/apply/apply.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "申请访问权限",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
146
apps/miniprogram - 副本/miniprogram/pages/apply/apply.ts
Normal file
146
apps/miniprogram - 副本/miniprogram/pages/apply/apply.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { request } from "../../utils/request"
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
siteCode: "",
|
||||
role: "",
|
||||
phone: "",
|
||||
employeeNumber: "",
|
||||
nickname: "",
|
||||
submitting: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const { statusBarHeight } = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight })
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._checkAccess()
|
||||
},
|
||||
|
||||
/** 校验用户身份:无 token 跳登录,非 new/rejected 跳对应页 */
|
||||
async _checkAccess() {
|
||||
const token = wx.getStorageSync("token")
|
||||
if (!token) {
|
||||
wx.reLaunch({ url: "/pages/login/login" })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await request({
|
||||
url: "/api/xcx/me",
|
||||
method: "GET",
|
||||
needAuth: true,
|
||||
})
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.authUser = {
|
||||
userId: data.user_id,
|
||||
status: data.status,
|
||||
nickname: data.nickname,
|
||||
}
|
||||
wx.setStorageSync("userId", data.user_id)
|
||||
wx.setStorageSync("userStatus", data.status)
|
||||
|
||||
switch (data.status) {
|
||||
case "new":
|
||||
break
|
||||
case "rejected":
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
break
|
||||
case "approved":
|
||||
wx.reLaunch({ url: "/pages/mvp/mvp" })
|
||||
break
|
||||
case "pending":
|
||||
wx.reLaunch({ url: "/pages/reviewing/reviewing" })
|
||||
break
|
||||
case "disabled":
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// 网络错误不阻塞,允许用户继续填表
|
||||
}
|
||||
},
|
||||
|
||||
onBack() {
|
||||
wx.navigateBack({ fail: () => wx.reLaunch({ url: "/pages/login/login" }) })
|
||||
},
|
||||
|
||||
/* 原生 input 的 bindinput 事件 */
|
||||
onSiteCodeInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ siteCode: e.detail.value })
|
||||
},
|
||||
|
||||
onRoleInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ role: e.detail.value })
|
||||
},
|
||||
|
||||
onPhoneInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ phone: e.detail.value })
|
||||
},
|
||||
|
||||
onEmployeeNumberInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ employeeNumber: e.detail.value })
|
||||
},
|
||||
|
||||
onNicknameInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ nickname: e.detail.value })
|
||||
},
|
||||
|
||||
async onSubmit() {
|
||||
if (this.data.submitting) return
|
||||
|
||||
const { siteCode, role, phone, nickname, employeeNumber } = this.data
|
||||
|
||||
if (!siteCode.trim()) {
|
||||
wx.showToast({ title: "请输入球房ID", icon: "none" })
|
||||
return
|
||||
}
|
||||
if (!role.trim()) {
|
||||
wx.showToast({ title: "请输入申请身份", icon: "none" })
|
||||
return
|
||||
}
|
||||
if (!/^\d{11}$/.test(phone)) {
|
||||
wx.showToast({ title: "请输入11位手机号", icon: "none" })
|
||||
return
|
||||
}
|
||||
if (!nickname.trim()) {
|
||||
wx.showToast({ title: "请输入昵称", icon: "none" })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ submitting: true })
|
||||
|
||||
try {
|
||||
await request({
|
||||
url: "/api/xcx/apply",
|
||||
method: "POST",
|
||||
data: {
|
||||
site_code: siteCode.trim(),
|
||||
applied_role_text: role.trim(),
|
||||
phone,
|
||||
employee_number: employeeNumber.trim() || undefined,
|
||||
nickname: nickname.trim(),
|
||||
},
|
||||
needAuth: true,
|
||||
})
|
||||
|
||||
wx.showToast({ title: "申请已提交", icon: "success" })
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({ url: "/pages/reviewing/reviewing" })
|
||||
}, 800)
|
||||
} catch (err: any) {
|
||||
const msg =
|
||||
err?.data?.detail ||
|
||||
(err?.statusCode === 409
|
||||
? "您已有待审核的申请"
|
||||
: err?.statusCode === 422
|
||||
? "表单信息有误,请检查"
|
||||
: "提交失败,请稍后重试")
|
||||
wx.showToast({ title: msg, icon: "none" })
|
||||
} finally {
|
||||
this.setData({ submitting: false })
|
||||
}
|
||||
},
|
||||
})
|
||||
107
apps/miniprogram - 副本/miniprogram/pages/apply/apply.wxml
Normal file
107
apps/miniprogram - 副本/miniprogram/pages/apply/apply.wxml
Normal file
@@ -0,0 +1,107 @@
|
||||
<!-- pages/apply/apply.wxml — 按 H5 原型结构迁移 -->
|
||||
<view class="page" style="padding-top: {{statusBarHeight}}px;">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="navbar">
|
||||
<view class="navbar-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="35rpx" color="#4b4b4b" />
|
||||
</view>
|
||||
<text class="navbar-title">申请访问权限</text>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="main">
|
||||
<!-- 欢迎卡片 + 审核流程(整合) -->
|
||||
<view class="welcome-card">
|
||||
<view class="welcome-header">
|
||||
<view class="welcome-icon-box">
|
||||
<t-icon name="check-circle-filled" size="42rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="welcome-text">
|
||||
<text class="welcome-title">欢迎加入球房运营助手</text>
|
||||
<text class="welcome-desc">请填写申请信息,等待管理员审核</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 审核流程步骤条 -->
|
||||
<view class="steps-bar">
|
||||
<view class="steps-row">
|
||||
<view class="step-item">
|
||||
<view class="step-circle step-circle--active">1</view>
|
||||
<text class="step-label step-label--active">提交申请</text>
|
||||
</view>
|
||||
<view class="step-line"></view>
|
||||
<view class="step-item">
|
||||
<view class="step-circle">2</view>
|
||||
<text class="step-label">等待审核</text>
|
||||
</view>
|
||||
<view class="step-line"></view>
|
||||
<view class="step-item">
|
||||
<view class="step-circle">3</view>
|
||||
<text class="step-label">审核通过</text>
|
||||
</view>
|
||||
<view class="step-line"></view>
|
||||
<view class="step-item">
|
||||
<view class="step-circle">4</view>
|
||||
<text class="step-label">开始使用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-card">
|
||||
<!-- 球房ID -->
|
||||
<view class="form-item form-item--border">
|
||||
<view class="form-label">
|
||||
<text class="required">*</text>
|
||||
<text>球房ID</text>
|
||||
</view>
|
||||
<input class="form-input" type="text" placeholder="请输入球房ID" value="{{siteCode}}" maxlength="5" bindinput="onSiteCodeInput" />
|
||||
</view>
|
||||
<!-- 申请身份 -->
|
||||
<view class="form-item form-item--border">
|
||||
<view class="form-label">
|
||||
<text class="required">*</text>
|
||||
<text>申请身份</text>
|
||||
</view>
|
||||
<input class="form-input" type="text" placeholder="请输入申请身份(如:助教、店长等)" value="{{role}}" bindinput="onRoleInput" />
|
||||
</view>
|
||||
<!-- 手机号 -->
|
||||
<view class="form-item form-item--border">
|
||||
<view class="form-label">
|
||||
<text class="required">*</text>
|
||||
<text>手机号</text>
|
||||
</view>
|
||||
<input class="form-input" type="number" placeholder="请输入手机号" value="{{phone}}" maxlength="11" bindinput="onPhoneInput" />
|
||||
</view>
|
||||
<!-- 编号(选填) -->
|
||||
<view class="form-item form-item--border">
|
||||
<view class="form-label">
|
||||
<text>编号</text>
|
||||
<text class="optional-tag">选填</text>
|
||||
</view>
|
||||
<input class="form-input" type="text" placeholder="请输入编号" value="{{employeeNumber}}" maxlength="50" bindinput="onEmployeeNumberInput" />
|
||||
</view>
|
||||
<!-- 昵称 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="required">*</text>
|
||||
<text>昵称</text>
|
||||
</view>
|
||||
<input class="form-input" type="text" placeholder="请输入昵称" value="{{nickname}}" maxlength="50" bindinput="onNicknameInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-area">
|
||||
<text class="form-tip">请认真填写,信息不完整可能导致审核不通过</text>
|
||||
<view class="submit-btn {{submitting ? 'submit-btn--disabled' : ''}}" bindtap="onSubmit">
|
||||
<t-loading wx:if="{{submitting}}" theme="circular" size="32rpx" color="#fff" />
|
||||
<text wx:else class="submit-btn-text">提交申请</text>
|
||||
</view>
|
||||
<text class="bottom-tip">审核通常需要 1-3 个工作日</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<dev-fab />
|
||||
271
apps/miniprogram - 副本/miniprogram/pages/apply/apply.wxss
Normal file
271
apps/miniprogram - 副本/miniprogram/pages/apply/apply.wxss
Normal file
@@ -0,0 +1,271 @@
|
||||
/* pages/apply/apply.wxss — H5 px × 2 × 0.875 精确转换 */
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 50%, #ecfeff 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ---- 顶部导航栏 h-11=44px→78rpx ---- */
|
||||
.navbar {
|
||||
height: 78rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-bottom: 1rpx solid rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
.navbar-back {
|
||||
position: absolute;
|
||||
left: 28rpx;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
/* text-base=16px→28rpx */
|
||||
.navbar-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #242424;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
/* ---- 主体内容 p-4=16px→28rpx ---- */
|
||||
.main {
|
||||
flex: 1;
|
||||
padding: 28rpx;
|
||||
padding-bottom: 380rpx;
|
||||
}
|
||||
|
||||
/* ---- 欢迎卡片 p-5=20px→36rpx, rounded-2xl=16px→28rpx ---- */
|
||||
.welcome-card {
|
||||
background: linear-gradient(135deg, #0052d9, #60a5fa);
|
||||
border-radius: 28rpx;
|
||||
padding: 36rpx;
|
||||
margin-bottom: 28rpx;
|
||||
box-shadow: 0 14rpx 36rpx rgba(0, 82, 217, 0.2);
|
||||
}
|
||||
|
||||
/* gap-4=16px→28rpx, mb-4=16px→28rpx */
|
||||
.welcome-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
/* w-12 h-12=48px→84rpx, rounded-xl=12px→22rpx */
|
||||
.welcome-icon-box {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
min-width: 84rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
/* text-lg=18px→32rpx */
|
||||
.welcome-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* text-sm=14px→24rpx */
|
||||
.welcome-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ---- 审核流程步骤条 p-4=16px→28rpx, rounded-xl=12px→22rpx ---- */
|
||||
.steps-bar {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 22rpx;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.steps-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
/* w-7 h-7=28px→50rpx, text-xs=12px→22rpx */
|
||||
.step-circle {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.step-circle--active {
|
||||
background: #ffffff;
|
||||
color: #0052d9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* text-xs=12px→22rpx */
|
||||
.step-label {
|
||||
font-size: 18rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.step-label--active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* h-0.5=2px→4rpx, mx-2=8px→14rpx */
|
||||
.step-line {
|
||||
flex: 1;
|
||||
height: 4rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
margin: 0 10rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* ---- 表单卡片 rounded-2xl=16px→28rpx ---- */
|
||||
.form-card {
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* px-5=20px→36rpx, py-4=16px→28rpx+2rpx 视觉补偿 */
|
||||
.form-item {
|
||||
padding: 30rpx 36rpx;
|
||||
}
|
||||
|
||||
.form-item--border {
|
||||
border-bottom: 2rpx solid #f3f3f3;
|
||||
}
|
||||
|
||||
/* text-sm=14px→24rpx, mb-2=8px→14rpx */
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
margin-bottom: 14rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #242424;
|
||||
}
|
||||
|
||||
/* text-sm=14px→24rpx */
|
||||
.required {
|
||||
color: #e34d59;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* text-xs=12px→22rpx */
|
||||
.optional-tag {
|
||||
font-size: 20rpx;
|
||||
color: #a6a6a6;
|
||||
font-weight: 400;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
/* px-4=16px→28rpx, py-3=12px→22rpx, rounded-xl=12px→22rpx, text-sm=14px→24rpx
|
||||
小程序 input 组件内部有压缩,py 加 4rpx 补偿到视觉等高 */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 28rpx;
|
||||
background: #f8f8f8;
|
||||
border-radius: 22rpx;
|
||||
border: 2rpx solid #f3f3f3;
|
||||
font-size: 24rpx;
|
||||
font-weight: 300;
|
||||
color: #242424;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: #c5c5c5;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ---- 表单提示(移入底部固定区) ---- */
|
||||
.form-tip {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 20rpx;
|
||||
color: #a6a6a6;
|
||||
margin-bottom: 18rpx;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ---- 底部提交 p-4=16px→28rpx, pb-8=32px→56rpx ---- */
|
||||
.bottom-area {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 28rpx;
|
||||
padding-bottom: calc(56rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-top: 2rpx solid #f3f3f3;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* py-4=16px→28rpx (用padding代替固定高度), rounded-xl=12px→22rpx, text-base=16px→28rpx */
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 28rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0052d9, #3b82f6);
|
||||
border-radius: 22rpx;
|
||||
box-shadow: 0 10rpx 28rpx rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* text-base=16px→28rpx */
|
||||
.submit-btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* text-xs=12px→22rpx, mt-3=12px→22rpx */
|
||||
.bottom-tip {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 20rpx;
|
||||
color: #c5c5c5;
|
||||
margin-top: 22rpx;
|
||||
font-weight: 300;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"navigationBarTitleText": "助教看板",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"filter-dropdown": "/components/filter-dropdown/filter-dropdown",
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"board-tab-bar": "/components/board-tab-bar/board-tab-bar",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// 助教看板页 — 排序×技能×时间三重筛选,4 种维度卡片
|
||||
// TODO: 联调时替换 mock 数据为真实 API 调用
|
||||
export {}
|
||||
|
||||
/** 排序维度 → 卡片模板映射 */
|
||||
type DimType = 'perf' | 'salary' | 'sv' | 'task'
|
||||
|
||||
const SORT_TO_DIM: Record<string, DimType> = {
|
||||
perf_desc: 'perf',
|
||||
perf_asc: 'perf',
|
||||
salary_desc: 'salary',
|
||||
salary_asc: 'salary',
|
||||
sv_desc: 'sv',
|
||||
task_desc: 'task',
|
||||
}
|
||||
|
||||
const SORT_OPTIONS = [
|
||||
{ value: 'perf_desc', text: '定档业绩最高' },
|
||||
{ value: 'perf_asc', text: '定档业绩最低' },
|
||||
{ value: 'salary_desc', text: '工资最高' },
|
||||
{ value: 'salary_asc', text: '工资最低' },
|
||||
{ value: 'sv_desc', text: '客源储值最高' },
|
||||
{ value: 'task_desc', text: '任务完成最多' },
|
||||
]
|
||||
|
||||
const SKILL_OPTIONS = [
|
||||
{ value: 'all', text: '不限' },
|
||||
{ value: 'chinese', text: '🎱 中式/追分' },
|
||||
{ value: 'snooker', text: '斯诺克' },
|
||||
{ value: 'mahjong', text: '🀄 麻将/棋牌' },
|
||||
{ value: 'karaoke', text: '🎤 团建/K歌' },
|
||||
]
|
||||
|
||||
const TIME_OPTIONS = [
|
||||
{ value: 'month', text: '本月' },
|
||||
{ value: 'quarter', text: '本季度' },
|
||||
{ value: 'last_month', text: '上月' },
|
||||
{ value: 'last_3m', text: '前3个月(不含本月)' },
|
||||
{ value: 'last_quarter', text: '上季度' },
|
||||
{ value: 'last_6m', text: '最近6个月(不含本月,不支持客源储值最高)' },
|
||||
]
|
||||
|
||||
/** 等级 → 样式类映射 */
|
||||
const LEVEL_CLASS: Record<string, string> = {
|
||||
'星级': 'level--star',
|
||||
'高级': 'level--high',
|
||||
'中级': 'level--mid',
|
||||
'初级': 'level--low',
|
||||
}
|
||||
|
||||
/** 技能 → 样式类映射 */
|
||||
const SKILL_CLASS: Record<string, string> = {
|
||||
'🎱': 'skill--chinese',
|
||||
'斯': 'skill--snooker',
|
||||
'🀄': 'skill--mahjong',
|
||||
'🎤': 'skill--karaoke',
|
||||
}
|
||||
|
||||
interface CoachItem {
|
||||
id: string
|
||||
name: string
|
||||
initial: string
|
||||
avatarGradient: string
|
||||
level: string
|
||||
levelClass: string
|
||||
skills: Array<{ text: string; cls: string }>
|
||||
topCustomers: string[]
|
||||
// 定档业绩维度
|
||||
perfHours: string
|
||||
perfHoursBefore?: string
|
||||
perfGap?: string
|
||||
perfReached: boolean
|
||||
// 工资维度
|
||||
salary: string
|
||||
salaryPerfHours: string
|
||||
salaryPerfBefore?: string
|
||||
// 客源储值维度
|
||||
svAmount: string
|
||||
svCustomerCount: string
|
||||
svConsume: string
|
||||
// 任务维度
|
||||
taskRecall: string
|
||||
taskCallback: string
|
||||
}
|
||||
|
||||
/** Mock 数据(忠于 H5 原型 6 位助教) */
|
||||
const MOCK_COACHES: CoachItem[] = [
|
||||
{
|
||||
id: 'c1', name: '小燕', initial: '小',
|
||||
avatarGradient: 'avatar--blue',
|
||||
level: '星级', levelClass: LEVEL_CLASS['星级'],
|
||||
skills: [{ text: '🎱', cls: SKILL_CLASS['🎱'] }],
|
||||
topCustomers: ['💖 王先生', '💖 李女士', '💛 赵总'],
|
||||
perfHours: '86.2h', perfHoursBefore: '92.0h', perfGap: '距升档 13.8h', perfReached: false,
|
||||
salary: '¥12,680', salaryPerfHours: '86.2h', salaryPerfBefore: '92.0h',
|
||||
svAmount: '¥45,200', svCustomerCount: '18', svConsume: '¥8,600',
|
||||
taskRecall: '18', taskCallback: '14',
|
||||
},
|
||||
{
|
||||
id: 'c2', name: '泡芙', initial: '泡',
|
||||
avatarGradient: 'avatar--green',
|
||||
level: '高级', levelClass: LEVEL_CLASS['高级'],
|
||||
skills: [{ text: '斯', cls: SKILL_CLASS['斯'] }],
|
||||
topCustomers: ['💖 陈先生', '💛 刘女士', '💛 黄总'],
|
||||
perfHours: '72.5h', perfHoursBefore: '78.0h', perfGap: '距升档 7.5h', perfReached: false,
|
||||
salary: '¥10,200', salaryPerfHours: '72.5h', salaryPerfBefore: '78.0h',
|
||||
svAmount: '¥38,600', svCustomerCount: '15', svConsume: '¥6,200',
|
||||
taskRecall: '15', taskCallback: '13',
|
||||
},
|
||||
{
|
||||
id: 'c3', name: 'Amy', initial: 'A',
|
||||
avatarGradient: 'avatar--pink',
|
||||
level: '星级', levelClass: LEVEL_CLASS['星级'],
|
||||
skills: [{ text: '🎱', cls: SKILL_CLASS['🎱'] }, { text: '斯', cls: SKILL_CLASS['斯'] }],
|
||||
topCustomers: ['💖 张先生', '💛 周女士', '💛 吴总'],
|
||||
perfHours: '68.0h', perfHoursBefore: '72.5h', perfGap: '距升档 32.0h', perfReached: false,
|
||||
salary: '¥9,800', salaryPerfHours: '68.0h', salaryPerfBefore: '72.5h',
|
||||
svAmount: '¥32,100', svCustomerCount: '14', svConsume: '¥5,800',
|
||||
taskRecall: '12', taskCallback: '13',
|
||||
},
|
||||
{
|
||||
id: 'c4', name: 'Mia', initial: 'M',
|
||||
avatarGradient: 'avatar--amber',
|
||||
level: '中级', levelClass: LEVEL_CLASS['中级'],
|
||||
skills: [{ text: '🀄', cls: SKILL_CLASS['🀄'] }],
|
||||
topCustomers: ['💛 赵先生', '💛 吴女士', '💛 孙总'],
|
||||
perfHours: '55.0h', perfGap: '距升档 5.0h', perfReached: false,
|
||||
salary: '¥7,500', salaryPerfHours: '55.0h',
|
||||
svAmount: '¥28,500', svCustomerCount: '12', svConsume: '¥4,100',
|
||||
taskRecall: '10', taskCallback: '10',
|
||||
},
|
||||
{
|
||||
id: 'c5', name: '糖糖', initial: '糖',
|
||||
avatarGradient: 'avatar--purple',
|
||||
level: '初级', levelClass: LEVEL_CLASS['初级'],
|
||||
skills: [{ text: '🎱', cls: SKILL_CLASS['🎱'] }],
|
||||
topCustomers: ['💛 钱先生', '💛 孙女士', '💛 周总'],
|
||||
perfHours: '42.0h', perfHoursBefore: '45.0h', perfReached: true,
|
||||
salary: '¥6,200', salaryPerfHours: '42.0h', salaryPerfBefore: '45.0h',
|
||||
svAmount: '¥22,000', svCustomerCount: '10', svConsume: '¥3,500',
|
||||
taskRecall: '8', taskCallback: '10',
|
||||
},
|
||||
{
|
||||
id: 'c6', name: '露露', initial: '露',
|
||||
avatarGradient: 'avatar--cyan',
|
||||
level: '中级', levelClass: LEVEL_CLASS['中级'],
|
||||
skills: [{ text: '🎤', cls: SKILL_CLASS['🎤'] }],
|
||||
topCustomers: ['💛 郑先生', '💛 冯女士', '💛 陈总'],
|
||||
perfHours: '38.0h', perfGap: '距升档 22.0h', perfReached: false,
|
||||
salary: '¥5,100', salaryPerfHours: '38.0h',
|
||||
svAmount: '¥18,300', svCustomerCount: '9', svConsume: '¥2,800',
|
||||
taskRecall: '6', taskCallback: '9',
|
||||
},
|
||||
]
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
activeTab: 'coach' as 'finance' | 'customer' | 'coach',
|
||||
|
||||
selectedSort: 'perf_desc',
|
||||
sortOptions: SORT_OPTIONS,
|
||||
selectedSkill: 'all',
|
||||
skillOptions: SKILL_OPTIONS,
|
||||
selectedTime: 'month',
|
||||
timeOptions: TIME_OPTIONS,
|
||||
|
||||
/** 当前维度类型,控制卡片模板 */
|
||||
dimType: 'perf' as DimType,
|
||||
|
||||
coaches: [] as CoachItem[],
|
||||
allCoaches: [] as CoachItem[],
|
||||
|
||||
/** 筛选栏可见性(滚动隐藏/显示) */
|
||||
filterBarVisible: true,
|
||||
},
|
||||
|
||||
_lastScrollTop: 0,
|
||||
_scrollAcc: 0,
|
||||
_scrollDir: null as 'up' | 'down' | null,
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadData()
|
||||
setTimeout(() => wx.stopPullDownRefresh(), 500)
|
||||
},
|
||||
|
||||
/** 滚动隐藏/显示筛选栏 */
|
||||
onPageScroll(e: { scrollTop: number }) {
|
||||
const y = e.scrollTop
|
||||
const delta = y - this._lastScrollTop
|
||||
this._lastScrollTop = y
|
||||
|
||||
if (y <= 8) {
|
||||
if (!this.data.filterBarVisible) this.setData({ filterBarVisible: true })
|
||||
this._scrollAcc = 0
|
||||
this._scrollDir = null
|
||||
return
|
||||
}
|
||||
|
||||
if (Math.abs(delta) <= 2) return
|
||||
const dir = delta > 0 ? 'down' : 'up'
|
||||
if (dir !== this._scrollDir) {
|
||||
this._scrollDir = dir
|
||||
this._scrollAcc = 0
|
||||
}
|
||||
this._scrollAcc += Math.abs(delta)
|
||||
|
||||
const threshold = dir === 'up' ? 12 : 24
|
||||
if (this._scrollAcc < threshold) return
|
||||
|
||||
const visible = dir === 'up'
|
||||
if (this.data.filterBarVisible !== visible) {
|
||||
this.setData({ filterBarVisible: visible })
|
||||
}
|
||||
this._scrollAcc = 0
|
||||
},
|
||||
|
||||
loadData() {
|
||||
this.setData({ pageState: 'loading' })
|
||||
setTimeout(() => {
|
||||
const data = MOCK_COACHES
|
||||
if (!data || data.length === 0) {
|
||||
this.setData({ pageState: 'empty' })
|
||||
return
|
||||
}
|
||||
this.setData({ allCoaches: data, coaches: data, pageState: 'normal' })
|
||||
}, 400)
|
||||
},
|
||||
|
||||
onTabChange(e: WechatMiniprogram.TouchEvent) {
|
||||
const tab = e.currentTarget.dataset.tab as string
|
||||
if (tab === 'finance') {
|
||||
wx.switchTab({ url: '/pages/board-finance/board-finance' })
|
||||
} else if (tab === 'customer') {
|
||||
wx.navigateTo({ url: '/pages/board-customer/board-customer' })
|
||||
}
|
||||
},
|
||||
|
||||
onSortChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
const val = e.detail.value
|
||||
this.setData({
|
||||
selectedSort: val,
|
||||
dimType: SORT_TO_DIM[val] || 'perf',
|
||||
})
|
||||
},
|
||||
|
||||
onSkillChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
this.setData({ selectedSkill: e.detail.value })
|
||||
},
|
||||
|
||||
onTimeChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
this.setData({ selectedTime: e.detail.value })
|
||||
},
|
||||
|
||||
onCoachTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id as string
|
||||
wx.navigateTo({ url: '/pages/coach-detail/coach-detail?id=' + id })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
<!-- 助教看板页 — 忠于 H5 原型结构 -->
|
||||
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="70rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-empty description="暂无助教数据" />
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:else>
|
||||
<!-- 顶部看板 Tab -->
|
||||
<view class="board-tabs">
|
||||
<view class="board-tab" data-tab="finance" bindtap="onTabChange">
|
||||
<text>财务</text>
|
||||
</view>
|
||||
<view class="board-tab" data-tab="customer" bindtap="onTabChange">
|
||||
<text>客户</text>
|
||||
</view>
|
||||
<view class="board-tab board-tab--active" data-tab="coach">
|
||||
<text>助教</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-bar {{filterBarVisible ? '' : 'filter-bar--hidden'}}">
|
||||
<view class="filter-bar-inner">
|
||||
<view class="filter-item filter-item--wide">
|
||||
<filter-dropdown
|
||||
label="定档业绩最高"
|
||||
options="{{sortOptions}}"
|
||||
value="{{selectedSort}}"
|
||||
bind:change="onSortChange"
|
||||
/>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<filter-dropdown
|
||||
label="不限"
|
||||
options="{{skillOptions}}"
|
||||
value="{{selectedSkill}}"
|
||||
bind:change="onSkillChange"
|
||||
/>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<filter-dropdown
|
||||
label="本月"
|
||||
options="{{timeOptions}}"
|
||||
value="{{selectedTime}}"
|
||||
bind:change="onTimeChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 助教列表 -->
|
||||
<view class="coach-list">
|
||||
<view
|
||||
class="coach-card"
|
||||
wx:for="{{coaches}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onCoachTap"
|
||||
>
|
||||
<view class="card-row">
|
||||
<!-- 头像 -->
|
||||
<view class="card-avatar {{item.avatarGradient}}">
|
||||
<text class="avatar-text">{{item.initial}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 信息区 -->
|
||||
<view class="card-info">
|
||||
<!-- 第一行:姓名 + 等级 + 技能 + 右侧指标 -->
|
||||
<view class="card-name-row">
|
||||
<text class="card-name">{{item.name}}</text>
|
||||
<text class="level-tag {{item.levelClass}}">{{item.level}}</text>
|
||||
<text
|
||||
class="skill-tag {{skill.cls}}"
|
||||
wx:for="{{item.skills}}"
|
||||
wx:for-item="skill"
|
||||
wx:key="text"
|
||||
>{{skill.text}}</text>
|
||||
|
||||
<!-- 定档业绩维度 -->
|
||||
<view class="card-right" wx:if="{{dimType === 'perf'}}">
|
||||
<text class="right-text">定档 <text class="right-highlight">{{item.perfHours}}</text></text>
|
||||
<text class="right-sub" wx:if="{{item.perfHoursBefore}}">折前 <text class="right-sub-val">{{item.perfHoursBefore}}</text></text>
|
||||
</view>
|
||||
|
||||
<!-- 工资维度 -->
|
||||
<view class="card-right" wx:elif="{{dimType === 'salary'}}">
|
||||
<text class="salary-tag">预估</text>
|
||||
<text class="salary-amount">{{item.salary}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 客源储值维度 -->
|
||||
<view class="card-right" wx:elif="{{dimType === 'sv'}}">
|
||||
<text class="right-sub">储值</text>
|
||||
<text class="salary-amount">{{item.svAmount}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 任务维度 -->
|
||||
<view class="card-right" wx:elif="{{dimType === 'task'}}">
|
||||
<text class="right-text">召回 <text class="right-highlight">{{item.taskRecall}}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:客户列表 + 右侧补充 -->
|
||||
<view class="card-bottom-row">
|
||||
<view class="customer-list">
|
||||
<block wx:for="{{item.topCustomers}}" wx:for-item="cust" wx:for-index="custIdx" wx:key="*this">
|
||||
<text class="customer-item" wx:if="{{dimType !== 'sv' || custIdx < 2}}">{{cust}}</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 定档业绩:距升档/已达标 -->
|
||||
<text class="bottom-right bottom-right--warning" wx:if="{{dimType === 'perf' && !item.perfReached}}">{{item.perfGap}}</text>
|
||||
<text class="bottom-right bottom-right--success" wx:elif="{{dimType === 'perf' && item.perfReached}}">✅ 已达标</text>
|
||||
|
||||
<!-- 工资:定档/折前 -->
|
||||
<view class="bottom-right-group" wx:elif="{{dimType === 'salary'}}">
|
||||
<text class="bottom-perf">定档 <text class="bottom-perf-val">{{item.salaryPerfHours}}</text></text>
|
||||
<text class="bottom-sub" wx:if="{{item.salaryPerfBefore}}">折前 <text class="bottom-sub-val">{{item.salaryPerfBefore}}</text></text>
|
||||
</view>
|
||||
|
||||
<!-- 客源储值:客户数 | 消耗 -->
|
||||
<view class="bottom-right-group" wx:elif="{{dimType === 'sv'}}">
|
||||
<text class="bottom-sub">客户 <text class="bottom-perf-val">{{item.svCustomerCount}}</text>人</text>
|
||||
<text class="bottom-divider">|</text>
|
||||
<text class="bottom-sub">消耗 <text class="bottom-perf-val">{{item.svConsume}}</text></text>
|
||||
</view>
|
||||
|
||||
<!-- 任务:回访数 -->
|
||||
<text class="bottom-sub" wx:elif="{{dimType === 'task'}}">回访 <text class="bottom-perf-val">{{item.taskCallback}}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区(为自定义导航栏留空间) -->
|
||||
<view class="safe-bottom"></view>
|
||||
</block>
|
||||
|
||||
<!-- 自定义底部导航栏 -->
|
||||
<board-tab-bar active="board" />
|
||||
|
||||
<!-- AI 悬浮按钮 — 在导航栏上方 -->
|
||||
<ai-float-button bottom="{{220}}" />
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,330 @@
|
||||
/* 助教看板页 — H5 px × 2 × 0.875 精确转换 */
|
||||
|
||||
/* ===== 三态 ===== */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
/* ===== 看板 Tab py-3=12px→22rpx, text-sm=14px→26rpx(视觉校准+2) ===== */
|
||||
.board-tabs {
|
||||
display: flex;
|
||||
background: #ffffff;
|
||||
border-bottom: 2rpx solid #eeeeee;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.board-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #8b8b8b;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.board-tab--active {
|
||||
color: #0052d9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* w-24px→42rpx, h-3px→5rpx(H5 实际渲染偏细) */
|
||||
.board-tab--active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 42rpx;
|
||||
height: 5rpx;
|
||||
background: linear-gradient(90deg, #0052d9, #5b9cf8);
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
|
||||
/* ===== 筛选区域 px-4=16px→28rpx, py-2=8px→14rpx ===== */
|
||||
.filter-bar {
|
||||
background: #f3f3f3;
|
||||
padding: 14rpx 28rpx;
|
||||
position: sticky;
|
||||
top: 70rpx;
|
||||
z-index: 15;
|
||||
border-bottom: 2rpx solid #eeeeee;
|
||||
transition: transform 220ms ease, opacity 220ms ease;
|
||||
}
|
||||
|
||||
.filter-bar--hidden {
|
||||
opacity: 0;
|
||||
transform: translateY(-110%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* p-1.5=6px→10rpx, gap-2=8px→14rpx, rounded-lg=8px→14rpx */
|
||||
.filter-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 14rpx;
|
||||
padding: 10rpx;
|
||||
border: 2rpx solid #eeeeee;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-item--wide {
|
||||
flex: 1.8;
|
||||
}
|
||||
|
||||
/* ===== 助教列表 p-4=16px→28rpx, space-y-3=12px→20rpx ===== */
|
||||
.coach-list {
|
||||
padding: 24rpx 28rpx;
|
||||
}
|
||||
|
||||
/* ===== 助教卡片 p-4=16px→28rpx, rounded-2xl=16px→28rpx ===== */
|
||||
.coach-card {
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 30rpx 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.coach-card:active {
|
||||
opacity: 0.96;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* gap-3=12px→20rpx(视觉校准紧凑) */
|
||||
.card-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* ===== 头像 w-11 h-11=44px→78rpx, text-base=16px→28rpx ===== */
|
||||
.card-avatar {
|
||||
width: 78rpx;
|
||||
height: 78rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 头像渐变色(忠于 H5 原型 6 种) */
|
||||
.avatar--blue { background: linear-gradient(135deg, #60a5fa, #6366f1); }
|
||||
.avatar--green { background: linear-gradient(135deg, #4ade80, #10b981); }
|
||||
.avatar--pink { background: linear-gradient(135deg, #f472b6, #f43f5e); }
|
||||
.avatar--amber { background: linear-gradient(135deg, #fbbf24, #f97316); }
|
||||
.avatar--purple { background: linear-gradient(135deg, #a78bfa, #8b5cf6); }
|
||||
.avatar--cyan { background: linear-gradient(135deg, #22d3ee, #14b8a6); }
|
||||
|
||||
/* ===== 信息区 ===== */
|
||||
.card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* gap-1.5=6px→10rpx */
|
||||
.card-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
/* text-base=16px→28rpx */
|
||||
.card-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #242424;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 等级标签 px-1.5=6px→10rpx, py-0.5=2px→4rpx, text-xs=12px→22rpx ===== */
|
||||
.level-tag {
|
||||
padding: 4rpx 10rpx;
|
||||
font-size: 22rpx;
|
||||
border-radius: 6rpx;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.level--star {
|
||||
background: linear-gradient(to right, #fbbf24, #f97316);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.level--high {
|
||||
background: linear-gradient(to right, #a78bfa, #8b5cf6);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.level--mid {
|
||||
background: linear-gradient(to right, #60a5fa, #6366f1);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.level--low {
|
||||
background: linear-gradient(to right, #9ca3af, #6b7280);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ===== 技能标签 ===== */
|
||||
.skill-tag {
|
||||
padding: 4rpx 10rpx;
|
||||
font-size: 22rpx;
|
||||
border-radius: 6rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skill--chinese { background: rgba(0, 82, 217, 0.1); color: #0052d9; }
|
||||
.skill--snooker { background: rgba(0, 168, 112, 0.1); color: #00a870; }
|
||||
.skill--mahjong { background: rgba(237, 123, 47, 0.1); color: #ed7b2f; }
|
||||
.skill--karaoke { background: rgba(227, 77, 89, 0.1); color: #e34d59; }
|
||||
|
||||
/* ===== 卡片右侧指标(ml-auto 推到右边) ===== */
|
||||
.card-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* text-xs=12px→22rpx — "定档"标签文字,普通粗细 */
|
||||
.right-text {
|
||||
font-size: 22rpx;
|
||||
color: #8b8b8b;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* text-sm=14px→24rpx — 数值加粗 */
|
||||
.right-highlight {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
/* "折前"更淡更细 */
|
||||
.right-sub {
|
||||
font-size: 20rpx;
|
||||
color: #c5c5c5;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.right-sub-val {
|
||||
color: #8b8b8b;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 工资维度 */
|
||||
.salary-tag {
|
||||
padding: 4rpx 10rpx;
|
||||
background: rgba(237, 123, 47, 0.1);
|
||||
color: #ed7b2f;
|
||||
font-size: 22rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
/* text-lg=18px→32rpx — 储值维度缩小避免挤压客户列表 */
|
||||
.salary-amount {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #242424;
|
||||
}
|
||||
|
||||
/* ===== 第二行 mt-1.5=6px→12rpx, text-xs=12px→22rpx ===== */
|
||||
.card-bottom-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* gap-2=8px→12rpx */
|
||||
.customer-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.customer-item {
|
||||
font-size: 22rpx;
|
||||
color: #a6a6a6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bottom-right {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bottom-right--warning {
|
||||
color: #ed7b2f;
|
||||
}
|
||||
|
||||
.bottom-right--success {
|
||||
color: #00a870;
|
||||
}
|
||||
|
||||
.bottom-right-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bottom-perf {
|
||||
font-size: 22rpx;
|
||||
color: #4b4b4b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bottom-perf-val {
|
||||
font-weight: 700;
|
||||
color: #4b4b4b;
|
||||
}
|
||||
|
||||
.bottom-sub {
|
||||
font-size: 22rpx;
|
||||
color: #8b8b8b;
|
||||
}
|
||||
|
||||
.bottom-sub-val {
|
||||
color: #8b8b8b;
|
||||
}
|
||||
|
||||
.bottom-divider {
|
||||
font-size: 22rpx;
|
||||
color: #c5c5c5;
|
||||
}
|
||||
|
||||
/* ===== 底部安全区(自定义导航栏高度 100rpx + safe-area) ===== */
|
||||
.safe-bottom {
|
||||
height: 200rpx;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"navigationBarTitleText": "客户看板",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"filter-dropdown": "/components/filter-dropdown/filter-dropdown",
|
||||
"heart-icon": "/components/heart-icon/heart-icon",
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"board-tab-bar": "/components/board-tab-bar/board-tab-bar",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// 客户看板页 — 8 个维度查看前 100 名客户
|
||||
// TODO: 联调时替换 mock 数据为真实 API 调用
|
||||
export {}
|
||||
|
||||
/** 维度类型 → 卡片模板映射 */
|
||||
type DimType = 'recall' | 'potential' | 'balance' | 'recharge' | 'recent' | 'spend60' | 'freq60' | 'loyal'
|
||||
|
||||
const DIMENSION_TO_DIM: Record<string, DimType> = {
|
||||
recall: 'recall',
|
||||
potential: 'potential',
|
||||
balance: 'balance',
|
||||
recharge: 'recharge',
|
||||
recent: 'recent',
|
||||
spend60: 'spend60',
|
||||
freq60: 'freq60',
|
||||
loyal: 'loyal',
|
||||
}
|
||||
|
||||
const DIMENSION_OPTIONS = [
|
||||
{ value: 'recall', text: '最应召回' },
|
||||
{ value: 'potential', text: '最大消费潜力' },
|
||||
{ value: 'balance', text: '最高余额' },
|
||||
{ value: 'recharge', text: '最近充值' },
|
||||
{ value: 'recent', text: '最近到店' },
|
||||
{ value: 'spend60', text: '最高消费 近60天' },
|
||||
{ value: 'freq60', text: '最频繁 近60天' },
|
||||
{ value: 'loyal', text: '最专一 近60天' },
|
||||
]
|
||||
|
||||
const PROJECT_OPTIONS = [
|
||||
{ value: 'all', text: '全部' },
|
||||
{ value: 'chinese', text: '🎱 中式/追分' },
|
||||
{ value: 'snooker', text: '斯诺克' },
|
||||
{ value: 'mahjong', text: '🀄 麻将/棋牌' },
|
||||
{ value: 'karaoke', text: '🎤 团建/K歌' },
|
||||
]
|
||||
|
||||
interface AssistantInfo {
|
||||
name: string
|
||||
cls: string // assistant--assignee / assistant--abandoned / assistant--normal
|
||||
heartScore: number // 0-10,heart-icon 组件用
|
||||
badge?: string // 跟 / 弃
|
||||
badgeCls?: string // assistant-badge--follow / assistant-badge--drop
|
||||
}
|
||||
|
||||
interface CustomerItem {
|
||||
id: string
|
||||
name: string
|
||||
initial: string
|
||||
avatarCls: string
|
||||
// 召回维度
|
||||
idealDays: number
|
||||
elapsedDays: number
|
||||
overdueDays: number
|
||||
visits30d: number
|
||||
balance: string
|
||||
recallIndex: string
|
||||
// 消费潜力维度
|
||||
potentialTags: Array<{ text: string; theme: string }>
|
||||
spend30d: string
|
||||
avgVisits: string
|
||||
avgSpend: string
|
||||
// 余额维度
|
||||
lastVisit: string
|
||||
monthlyConsume: string
|
||||
availableMonths: string
|
||||
// 充值维度
|
||||
lastRecharge: string
|
||||
rechargeAmount: string
|
||||
recharges60d: string
|
||||
currentBalance: string
|
||||
// 消费60天
|
||||
spend60d: string
|
||||
visits60d: string
|
||||
highSpendTag: boolean
|
||||
// 频率60天
|
||||
avgInterval: string
|
||||
weeklyVisits: Array<{ val: number; pct: number }>
|
||||
// 专一度
|
||||
intimacy: string
|
||||
coachName: string
|
||||
coachRatio: string
|
||||
topCoachName: string
|
||||
topCoachHeart: number
|
||||
topCoachScore: string
|
||||
coachDetails: Array<{
|
||||
name: string
|
||||
cls: string
|
||||
heartScore: number
|
||||
badge?: string
|
||||
avgDuration: string
|
||||
serviceCount: string
|
||||
coachSpend: string
|
||||
relationIdx: number
|
||||
}>
|
||||
// 最近到店
|
||||
visitFreq: string
|
||||
daysAgo: number
|
||||
// 助教
|
||||
assistants: AssistantInfo[]
|
||||
}
|
||||
|
||||
/** Mock 数据(忠于 H5 原型 3 位客户) */
|
||||
const MOCK_CUSTOMERS: CustomerItem[] = [
|
||||
{
|
||||
id: 'u1', name: '王先生', initial: '王', avatarCls: 'avatar--amber',
|
||||
idealDays: 7, elapsedDays: 15, overdueDays: 8,
|
||||
visits30d: 3, balance: '¥2,680', recallIndex: '9.2',
|
||||
potentialTags: [
|
||||
{ text: '高频', theme: 'primary' },
|
||||
{ text: '高客单', theme: 'warning' },
|
||||
],
|
||||
spend30d: '¥4,200', avgVisits: '6.2次', avgSpend: '¥680',
|
||||
lastVisit: '3天前', monthlyConsume: '¥3,500', availableMonths: '0.8月',
|
||||
lastRecharge: '2月15日', rechargeAmount: '¥5,000', recharges60d: '2次', currentBalance: '¥2,680',
|
||||
spend60d: '¥8,400', visits60d: '12', highSpendTag: true,
|
||||
avgInterval: '5.0天', intimacy: '92',
|
||||
topCoachName: '小燕', topCoachHeart: 9.2, topCoachScore: '9.2',
|
||||
coachDetails: [
|
||||
{ name: '小燕', cls: 'assistant--assignee', heartScore: 9.2, badge: '跟', avgDuration: '2.3h', serviceCount: '14', coachSpend: '¥4,200', relationIdx: 9.2 },
|
||||
{ name: '泡芙', cls: 'assistant--normal', heartScore: 6.5, avgDuration: '1.5h', serviceCount: '8', coachSpend: '¥2,100', relationIdx: 7.2 },
|
||||
],
|
||||
weeklyVisits: [
|
||||
{ val: 2, pct: 60 }, { val: 2, pct: 60 }, { val: 1, pct: 30 }, { val: 3, pct: 100 },
|
||||
{ val: 2, pct: 60 }, { val: 3, pct: 100 }, { val: 1, pct: 30 }, { val: 3, pct: 100 },
|
||||
], coachName: '小燕', coachRatio: '78%',
|
||||
visitFreq: '6.2次/月',
|
||||
daysAgo: 3,
|
||||
assistants: [
|
||||
{ name: '小燕', cls: 'assistant--assignee', heartScore: 9.2, badge: '跟', badgeCls: 'assistant-badge--follow' },
|
||||
{ name: '泡芙', cls: 'assistant--normal', heartScore: 6.5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'u2', name: '李女士', initial: '李', avatarCls: 'avatar--pink',
|
||||
idealDays: 10, elapsedDays: 22, overdueDays: 12,
|
||||
visits30d: 1, balance: '¥8,200', recallIndex: '8.5',
|
||||
potentialTags: [
|
||||
{ text: '高余额', theme: 'success' },
|
||||
{ text: '低频', theme: 'gray' },
|
||||
],
|
||||
spend30d: '¥1,800', avgVisits: '2.1次', avgSpend: '¥860',
|
||||
lastVisit: '12天前', monthlyConsume: '¥1,800', availableMonths: '4.6月',
|
||||
lastRecharge: '1月20日', rechargeAmount: '¥10,000', recharges60d: '1次', currentBalance: '¥8,200',
|
||||
spend60d: '¥3,600', visits60d: '4', highSpendTag: false,
|
||||
avgInterval: '15.0天', intimacy: '68',
|
||||
topCoachName: 'Amy', topCoachHeart: 8.5, topCoachScore: '8.5',
|
||||
coachDetails: [
|
||||
{ name: 'Amy', cls: 'assistant--assignee', heartScore: 8.5, badge: '跟', avgDuration: '1.8h', serviceCount: '10', coachSpend: '¥3,600', relationIdx: 8.5 },
|
||||
{ name: '小燕', cls: 'assistant--abandoned', heartScore: 4.2, badge: '弃', avgDuration: '0.8h', serviceCount: '3', coachSpend: '¥600', relationIdx: 4.2 },
|
||||
],
|
||||
weeklyVisits: [
|
||||
{ val: 1, pct: 40 }, { val: 1, pct: 40 }, { val: 0, pct: 10 }, { val: 1, pct: 40 },
|
||||
{ val: 1, pct: 40 }, { val: 0, pct: 10 }, { val: 1, pct: 40 }, { val: 1, pct: 40 },
|
||||
], coachName: 'Amy', coachRatio: '62%',
|
||||
visitFreq: '2.1次/月',
|
||||
daysAgo: 12,
|
||||
assistants: [
|
||||
{ name: 'Amy', cls: 'assistant--assignee', heartScore: 8.5, badge: '跟', badgeCls: 'assistant-badge--follow' },
|
||||
{ name: '小燕', cls: 'assistant--abandoned', heartScore: 4.2, badge: '弃', badgeCls: 'assistant-badge--drop' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'u3', name: '张先生', initial: '张', avatarCls: 'avatar--blue',
|
||||
idealDays: 5, elapsedDays: 8, overdueDays: 3,
|
||||
visits30d: 5, balance: '¥1,200', recallIndex: '7.8',
|
||||
potentialTags: [
|
||||
{ text: '高频', theme: 'primary' },
|
||||
],
|
||||
spend30d: '¥3,500', avgVisits: '8.0次', avgSpend: '¥440',
|
||||
lastVisit: '1天前', monthlyConsume: '¥3,200', availableMonths: '0.4月',
|
||||
lastRecharge: '3月1日', rechargeAmount: '¥3,000', recharges60d: '3次', currentBalance: '¥1,200',
|
||||
spend60d: '¥7,000', visits60d: '16', highSpendTag: true,
|
||||
avgInterval: '3.8天', intimacy: '95',
|
||||
topCoachName: '泡芙', topCoachHeart: 9.5, topCoachScore: '9.5',
|
||||
coachDetails: [
|
||||
{ name: '泡芙', cls: 'assistant--assignee', heartScore: 9.5, badge: '跟', avgDuration: '2.1h', serviceCount: '11', coachSpend: '¥3,300', relationIdx: 9.5 },
|
||||
{ name: '小燕', cls: 'assistant--normal', heartScore: 6.8, avgDuration: '1.3h', serviceCount: '6', coachSpend: '¥1,800', relationIdx: 6.8 },
|
||||
{ name: 'Amy', cls: 'assistant--abandoned', heartScore: 3.2, badge: '弃', avgDuration: '0.5h', serviceCount: '2', coachSpend: '¥400', relationIdx: 3.2 },
|
||||
],
|
||||
weeklyVisits: [
|
||||
{ val: 2, pct: 50 }, { val: 3, pct: 75 }, { val: 2, pct: 50 }, { val: 4, pct: 100 },
|
||||
{ val: 3, pct: 75 }, { val: 3, pct: 75 }, { val: 2, pct: 50 }, { val: 3, pct: 75 },
|
||||
], coachName: '泡芙', coachRatio: '85%',
|
||||
visitFreq: '8.0次/月',
|
||||
daysAgo: 1,
|
||||
assistants: [
|
||||
{ name: '泡芙', cls: 'assistant--assignee', heartScore: 9.5, badge: '跟', badgeCls: 'assistant-badge--follow' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
activeTab: 'customer' as 'finance' | 'customer' | 'coach',
|
||||
|
||||
selectedDimension: 'recall',
|
||||
dimensionOptions: DIMENSION_OPTIONS,
|
||||
selectedProject: 'all',
|
||||
projectOptions: PROJECT_OPTIONS,
|
||||
|
||||
/** 当前维度类型,控制卡片模板 */
|
||||
dimType: 'recall' as DimType,
|
||||
|
||||
customers: [] as CustomerItem[],
|
||||
allCustomers: [] as CustomerItem[],
|
||||
totalCount: 0,
|
||||
|
||||
/** 筛选栏可见性 */
|
||||
filterBarVisible: true,
|
||||
},
|
||||
|
||||
_lastScrollTop: 0,
|
||||
_scrollAcc: 0,
|
||||
_scrollDir: null as 'up' | 'down' | null,
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadData()
|
||||
setTimeout(() => wx.stopPullDownRefresh(), 500)
|
||||
},
|
||||
|
||||
/** 滚动隐藏/显示筛选栏 */
|
||||
onPageScroll(e: { scrollTop: number }) {
|
||||
const y = e.scrollTop
|
||||
const delta = y - this._lastScrollTop
|
||||
this._lastScrollTop = y
|
||||
|
||||
if (y <= 8) {
|
||||
if (!this.data.filterBarVisible) this.setData({ filterBarVisible: true })
|
||||
this._scrollAcc = 0
|
||||
this._scrollDir = null
|
||||
return
|
||||
}
|
||||
|
||||
if (Math.abs(delta) <= 2) return
|
||||
const dir = delta > 0 ? 'down' : 'up'
|
||||
if (dir !== this._scrollDir) {
|
||||
this._scrollDir = dir
|
||||
this._scrollAcc = 0
|
||||
}
|
||||
this._scrollAcc += Math.abs(delta)
|
||||
|
||||
const threshold = dir === 'up' ? 12 : 24
|
||||
if (this._scrollAcc < threshold) return
|
||||
|
||||
const visible = dir === 'up'
|
||||
if (this.data.filterBarVisible !== visible) {
|
||||
this.setData({ filterBarVisible: visible })
|
||||
}
|
||||
this._scrollAcc = 0
|
||||
},
|
||||
|
||||
loadData() {
|
||||
this.setData({ pageState: 'loading' })
|
||||
setTimeout(() => {
|
||||
const data = MOCK_CUSTOMERS
|
||||
if (!data || data.length === 0) {
|
||||
this.setData({ pageState: 'empty' })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
allCustomers: data,
|
||||
customers: data,
|
||||
totalCount: data.length,
|
||||
pageState: 'normal',
|
||||
})
|
||||
}, 400)
|
||||
},
|
||||
|
||||
onTabChange(e: WechatMiniprogram.TouchEvent) {
|
||||
const tab = e.currentTarget.dataset.tab as string
|
||||
if (tab === 'finance') {
|
||||
wx.switchTab({ url: '/pages/board-finance/board-finance' })
|
||||
} else if (tab === 'coach') {
|
||||
wx.navigateTo({ url: '/pages/board-coach/board-coach' })
|
||||
}
|
||||
},
|
||||
|
||||
onDimensionChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
const val = e.detail.value
|
||||
this.setData({
|
||||
selectedDimension: val,
|
||||
dimType: DIMENSION_TO_DIM[val] || 'recall',
|
||||
})
|
||||
},
|
||||
|
||||
onProjectChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
this.setData({ selectedProject: e.detail.value })
|
||||
},
|
||||
|
||||
onCustomerTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id as string
|
||||
wx.navigateTo({ url: '/pages/customer-detail/customer-detail?id=' + id })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,301 @@
|
||||
<!-- 客户看板页 — 忠于 H5 原型,8 维度卡片模板 -->
|
||||
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="70rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-empty description="暂无客户数据" />
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:else>
|
||||
<!-- 顶部看板 Tab -->
|
||||
<view class="board-tabs">
|
||||
<view class="board-tab" data-tab="finance" bindtap="onTabChange">
|
||||
<text>财务</text>
|
||||
</view>
|
||||
<view class="board-tab board-tab--active" data-tab="customer">
|
||||
<text>客户</text>
|
||||
</view>
|
||||
<view class="board-tab" data-tab="coach" bindtap="onTabChange">
|
||||
<text>助教</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-bar {{filterBarVisible ? '' : 'filter-bar--hidden'}}">
|
||||
<view class="filter-bar-inner">
|
||||
<view class="filter-item filter-item--wide">
|
||||
<filter-dropdown
|
||||
label="最应召回"
|
||||
options="{{dimensionOptions}}"
|
||||
value="{{selectedDimension}}"
|
||||
bind:change="onDimensionChange"
|
||||
/>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<filter-dropdown
|
||||
label="全部"
|
||||
options="{{projectOptions}}"
|
||||
value="{{selectedProject}}"
|
||||
bind:change="onProjectChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表头部 -->
|
||||
<view class="list-header">
|
||||
<view class="list-header-left">
|
||||
<text class="list-header-title">客户列表</text>
|
||||
<text class="list-header-sub">· 前100名</text>
|
||||
</view>
|
||||
<text class="list-header-count">共{{totalCount}}名客户</text>
|
||||
</view>
|
||||
|
||||
<!-- 客户列表 -->
|
||||
<view class="customer-list">
|
||||
<view
|
||||
class="customer-card"
|
||||
wx:for="{{customers}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onCustomerTap"
|
||||
>
|
||||
<!-- ===== 卡片头部:头像 + 姓名 + 右侧指标 ===== -->
|
||||
<view class="card-header">
|
||||
<view class="card-avatar {{item.avatarCls}}">
|
||||
<text class="avatar-text">{{item.initial}}</text>
|
||||
</view>
|
||||
<text class="card-name" wx:if="{{dimType !== 'freq60'}}">{{item.name}}</text>
|
||||
<!-- 最频繁:姓名 + 下方小字垂直排列 -->
|
||||
<view class="card-name-group" wx:if="{{dimType === 'freq60'}}">
|
||||
<text class="card-name">{{item.name}}</text>
|
||||
<view class="card-name-sub">
|
||||
<text class="mid-text" style="white-space: nowrap;">平均间隔 <text class="mid-primary">{{item.avgInterval}}</text></text>
|
||||
<text class="mid-text" style="margin-left: 16rpx; white-space: nowrap;">60天消费 <text class="mid-dark">{{item.spend60d}}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-header-spacer"></view>
|
||||
|
||||
<!-- 最应召回:理想/已过/超期 -->
|
||||
<view class="header-metrics" wx:if="{{dimType === 'recall'}}">
|
||||
<text class="metric-gray">理想 <text class="metric-dark">{{item.idealDays}}天</text></text>
|
||||
<text class="metric-gray">已过 <text class="metric-error">{{item.elapsedDays}}天</text></text>
|
||||
<view class="overdue-tag {{item.overdueDays > 7 ? 'overdue-tag--danger' : 'overdue-tag--warn'}}">
|
||||
<text>超期 {{item.overdueDays}}天</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最大消费潜力:频率/客单/余额标签 -->
|
||||
<view class="header-metrics" wx:elif="{{dimType === 'potential'}}">
|
||||
<view class="potential-tag potential-tag--{{tag.theme}}" wx:for="{{item.potentialTags}}" wx:for-item="tag" wx:key="text">
|
||||
<text>{{tag.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最高余额:最近到店/理想 -->
|
||||
<view class="header-metrics" wx:elif="{{dimType === 'balance'}}">
|
||||
<text class="metric-gray">最近到店 <text class="metric-dark">{{item.lastVisit}}</text></text>
|
||||
<text class="metric-gray">理想 <text class="metric-dark">{{item.idealDays}}天</text></text>
|
||||
</view>
|
||||
|
||||
<!-- 最近充值:最近到店/理想 -->
|
||||
<view class="header-metrics" wx:elif="{{dimType === 'recharge'}}">
|
||||
<text class="metric-gray">最近到店 <text class="metric-dark">{{item.lastVisit}}</text></text>
|
||||
<text class="metric-gray">理想 <text class="metric-dark">{{item.idealDays}}天</text></text>
|
||||
</view>
|
||||
|
||||
<!-- 最频繁近60天:右上角大字到店次数 -->
|
||||
<view class="header-metrics header-metrics--freq" wx:elif="{{dimType === 'freq60'}}">
|
||||
<view class="freq-big-num">
|
||||
<text class="freq-big-val">{{item.visits60d}}</text>
|
||||
<text class="freq-big-unit">次</text>
|
||||
</view>
|
||||
<text class="freq-big-label">60天到店</text>
|
||||
</view>
|
||||
|
||||
<!-- 最专一近60天:右上角 ❤️ 助教名 + 关系指数 -->
|
||||
<view class="header-metrics header-metrics--loyal" wx:elif="{{dimType === 'loyal'}}">
|
||||
<heart-icon score="{{item.topCoachHeart}}" />
|
||||
<text class="loyal-top-name">{{item.topCoachName}}</text>
|
||||
<text class="loyal-top-score">{{item.topCoachScore}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 最高消费近60天:高消费标签 -->
|
||||
<view class="header-metrics" wx:elif="{{dimType === 'spend60'}}">
|
||||
<view class="potential-tag potential-tag--warning" wx:if="{{item.highSpendTag}}">
|
||||
<text>高消费</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近到店:右上角大字 X天前到店 -->
|
||||
<view class="header-metrics header-metrics--recent" wx:elif="{{dimType === 'recent'}}">
|
||||
<view class="recent-big-num">
|
||||
<text class="recent-big-val">{{item.daysAgo}}</text>
|
||||
<text class="recent-big-unit">天前到店</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 卡片中间行:维度特定数据 ===== -->
|
||||
|
||||
<!-- 最应召回:30天到店 / 余额 / 召回指数 -->
|
||||
<view class="card-mid-row" wx:if="{{dimType === 'recall'}}">
|
||||
<text class="mid-text">30天到店 <text class="mid-dark">{{item.visits30d}}次</text></text>
|
||||
<text class="mid-text mid-ml">余额 <text class="mid-dark">{{item.balance}}</text></text>
|
||||
<text class="mid-text mid-right">召回指数 <text class="mid-primary-bold">{{item.recallIndex}}</text></text>
|
||||
</view>
|
||||
|
||||
<!-- 最大消费潜力:4 列网格(30天消费用橙色大字,和最高余额的余额值一致) -->
|
||||
<view class="card-grid card-grid--4" wx:elif="{{dimType === 'potential'}}">
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">30天消费</text>
|
||||
<text class="grid-val grid-val--warning grid-val--lg">{{item.spend30d}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">月均到店</text>
|
||||
<text class="grid-val">{{item.avgVisits}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">余额</text>
|
||||
<text class="grid-val grid-val--success">{{item.balance}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">次均消费</text>
|
||||
<text class="grid-val">{{item.avgSpend}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最高余额:3 列网格 -->
|
||||
<view class="card-grid card-grid--3" wx:elif="{{dimType === 'balance'}}">
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">余额</text>
|
||||
<text class="grid-val grid-val--warning grid-val--lg">{{item.balance}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">月均消耗</text>
|
||||
<text class="grid-val">{{item.monthlyConsume}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">可用</text>
|
||||
<text class="grid-val grid-val--success">{{item.availableMonths}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近充值:4 列网格 -->
|
||||
<view class="card-grid card-grid--4" wx:elif="{{dimType === 'recharge'}}">
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">最后充值</text>
|
||||
<text class="grid-val">{{item.lastRecharge}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">充值</text>
|
||||
<text class="grid-val grid-val--success">{{item.rechargeAmount}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">60天充值</text>
|
||||
<text class="grid-val">{{item.recharges60d}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">当前余额</text>
|
||||
<text class="grid-val grid-val--warning">{{item.currentBalance}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最高消费近60天:3 列网格 -->
|
||||
<view class="card-grid card-grid--3" wx:elif="{{dimType === 'spend60'}}">
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">近60天消费</text>
|
||||
<text class="grid-val grid-val--warning grid-val--lg">{{item.spend60d}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">到店次数</text>
|
||||
<text class="grid-val">{{item.visits60d}}</text>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<text class="grid-label">次均消费</text>
|
||||
<text class="grid-val">{{item.avgSpend}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最频繁近60天:无中间行(数据已在头部) -->
|
||||
|
||||
<!-- 最频繁:迷你柱状图(8 周) -->
|
||||
<view class="mini-chart" wx:if="{{dimType === 'freq60'}}">
|
||||
<view class="mini-chart-header">
|
||||
<text class="mini-chart-label">8周前</text>
|
||||
<text class="mini-chart-label">本周</text>
|
||||
</view>
|
||||
<view class="mini-chart-bars">
|
||||
<view class="mini-bar-col" wx:for="{{item.weeklyVisits}}" wx:for-item="wv" wx:for-index="wIdx" wx:key="wIdx">
|
||||
<view class="mini-bar" style="height:{{wv.pct}}%;opacity:{{0.2 + wv.pct * 0.006}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mini-chart-nums">
|
||||
<text class="mini-chart-num {{wIdx === item.weeklyVisits.length - 1 ? 'mini-chart-num--active' : ''}}" wx:for="{{item.weeklyVisits}}" wx:for-item="wv" wx:for-index="wIdx" wx:key="wIdx">{{wv.val}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最专一近60天:助教服务明细表 -->
|
||||
<view class="loyal-table" wx:elif="{{dimType === 'loyal'}}">
|
||||
<!-- 表头 -->
|
||||
<view class="loyal-row loyal-row--header">
|
||||
<text class="loyal-col loyal-col--name">助教</text>
|
||||
<text class="loyal-col">次均时长</text>
|
||||
<text class="loyal-col">服务次数</text>
|
||||
<text class="loyal-col">助教消费</text>
|
||||
<text class="loyal-col">关系指数</text>
|
||||
</view>
|
||||
<!-- 数据行 -->
|
||||
<view class="loyal-row" wx:for="{{item.coachDetails}}" wx:for-item="cd" wx:key="name">
|
||||
<view class="loyal-col loyal-col--name">
|
||||
<heart-icon score="{{cd.heartScore}}" />
|
||||
<text class="loyal-coach-name {{cd.cls}}">{{cd.name}}</text>
|
||||
<text class="assistant-badge assistant-badge--follow" wx:if="{{cd.badge === '跟'}}">跟</text>
|
||||
<text class="assistant-badge assistant-badge--drop" wx:elif="{{cd.badge === '弃'}}">弃</text>
|
||||
</view>
|
||||
<text class="loyal-col loyal-val">{{cd.avgDuration}}</text>
|
||||
<text class="loyal-col loyal-val">{{cd.serviceCount}}</text>
|
||||
<text class="loyal-col loyal-val">{{cd.coachSpend}}</text>
|
||||
<text class="loyal-col {{cd.relationIdx >= 8 ? 'loyal-val--primary' : 'loyal-val--gray'}}">{{cd.relationIdx}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近到店:理想间隔 / 60天到店 / 次均消费 -->
|
||||
<view class="card-mid-row" wx:elif="{{dimType === 'recent'}}">
|
||||
<text class="mid-text">理想间隔 <text class="mid-dark">{{item.idealDays}}天</text></text>
|
||||
<text class="mid-text mid-ml">60天到店 <text class="mid-dark">{{item.visits60d}}天</text></text>
|
||||
<text class="mid-text mid-ml">次均消费 <text class="mid-dark">{{item.avgSpend}}</text></text>
|
||||
</view>
|
||||
|
||||
<!-- ===== 卡片底部:助教行(最专一维度不显示,因为助教信息在表格中) ===== -->
|
||||
<view class="card-assistant-row" wx:if="{{item.assistants && item.assistants.length > 0 && dimType !== 'loyal'}}">
|
||||
<text class="assistant-label">助教:</text>
|
||||
<block wx:for="{{item.assistants}}" wx:for-item="ast" wx:for-index="astIdx" wx:key="name">
|
||||
<text class="assistant-sep" wx:if="{{astIdx > 0}}">|</text>
|
||||
<view class="assistant-tag">
|
||||
<heart-icon score="{{ast.heartScore}}" />
|
||||
<text class="assistant-name {{ast.cls}}">{{ast.name}}</text>
|
||||
<text class="assistant-badge assistant-badge--follow" wx:if="{{ast.badge === '跟'}}">跟</text>
|
||||
<text class="assistant-badge assistant-badge--drop" wx:elif="{{ast.badge === '弃'}}">弃</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区 -->
|
||||
<view class="safe-bottom"></view>
|
||||
</block>
|
||||
|
||||
<!-- 自定义底部导航栏 -->
|
||||
<board-tab-bar active="board" />
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{220}}" />
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,637 @@
|
||||
/* 客户看板页 — 忠于 H5 原型,87.5% 缩放 */
|
||||
|
||||
/* ===== 三态 ===== */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
/* ===== 看板 Tab(对齐 board-coach 规范) ===== */
|
||||
.board-tabs {
|
||||
display: flex;
|
||||
background: #ffffff;
|
||||
border-bottom: 2rpx solid #eeeeee;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.board-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #8b8b8b;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.board-tab--active {
|
||||
color: #0052d9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.board-tab--active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 42rpx;
|
||||
height: 5rpx;
|
||||
background: linear-gradient(90deg, #0052d9, #5b9cf8);
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
|
||||
/* ===== 筛选区域(对齐 board-coach 规范) ===== */
|
||||
.filter-bar {
|
||||
background: #f3f3f3;
|
||||
padding: 14rpx 28rpx;
|
||||
position: sticky;
|
||||
top: 70rpx;
|
||||
z-index: 15;
|
||||
border-bottom: 2rpx solid #eeeeee;
|
||||
transition: transform 220ms ease, opacity 220ms ease;
|
||||
}
|
||||
|
||||
.filter-bar--hidden {
|
||||
opacity: 0;
|
||||
transform: translateY(-110%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.filter-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 14rpx;
|
||||
padding: 10rpx;
|
||||
border: 2rpx solid #eeeeee;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-item--wide {
|
||||
flex: 1.8;
|
||||
}
|
||||
|
||||
/* ===== 列表头部 ===== */
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 28rpx 12rpx;
|
||||
}
|
||||
|
||||
.list-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.list-header-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #242424;
|
||||
}
|
||||
|
||||
.list-header-sub {
|
||||
font-size: 24rpx;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
.list-header-count {
|
||||
font-size: 24rpx;
|
||||
color: #c5c5c5;
|
||||
}
|
||||
|
||||
/* ===== 客户列表 ===== */
|
||||
.customer-list {
|
||||
padding: 0 28rpx 24rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* ===== 客户卡片 ===== */
|
||||
.customer-card {
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 30rpx 28rpx 26rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.customer-card:active {
|
||||
opacity: 0.96;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* ===== 卡片头部 ===== */
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.card-avatar {
|
||||
width: 66rpx;
|
||||
height: 66rpx;
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #ffffff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 头像渐变色 */
|
||||
.avatar--amber { background: linear-gradient(135deg, #fbbf24, #f97316); }
|
||||
.avatar--pink { background: linear-gradient(135deg, #f472b6, #f43f5e); }
|
||||
.avatar--blue { background: linear-gradient(135deg, #60a5fa, #6366f1); }
|
||||
.avatar--green { background: linear-gradient(135deg, #4ade80, #10b981); }
|
||||
.avatar--purple { background: linear-gradient(135deg, #a78bfa, #8b5cf6); }
|
||||
.avatar--cyan { background: linear-gradient(135deg, #22d3ee, #14b8a6); }
|
||||
.avatar--rose { background: linear-gradient(135deg, #fb7185, #e11d48); }
|
||||
.avatar--indigo { background: linear-gradient(135deg, #818cf8, #4f46e5); }
|
||||
|
||||
.card-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #242424;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 最频繁:姓名+小字垂直排列 */
|
||||
.card-name-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-name-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 2rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 头部右侧指标区 */
|
||||
.header-metrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 最频繁维度:右上角大字到店次数 */
|
||||
.header-metrics--freq {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.freq-big-num {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.freq-big-val {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #0052d9;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.freq-big-unit {
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
color: #a6a6a6;
|
||||
margin-left: 2rpx;
|
||||
}
|
||||
|
||||
.freq-big-label {
|
||||
font-size: 20rpx;
|
||||
color: #a6a6a6;
|
||||
margin-top: -2rpx;
|
||||
}
|
||||
|
||||
/* 最近到店维度:右上角大字 X天前到店 */
|
||||
.header-metrics--recent {
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.recent-big-num {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.recent-big-val {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #00a870;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.recent-big-unit {
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
.metric-gray {
|
||||
font-size: 22rpx;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
.metric-dark {
|
||||
color: #393939;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-error {
|
||||
color: #e34d59;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 超期标签 */
|
||||
.overdue-tag {
|
||||
padding: 4rpx 10rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.overdue-tag--danger {
|
||||
background: rgba(227, 77, 89, 0.1);
|
||||
color: #e34d59;
|
||||
}
|
||||
|
||||
.overdue-tag--warn {
|
||||
background: rgba(237, 123, 47, 0.1);
|
||||
color: #ed7b2f;
|
||||
}
|
||||
|
||||
/* 消费潜力标签 */
|
||||
.potential-tag {
|
||||
padding: 4rpx 10rpx;
|
||||
font-size: 22rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.potential-tag--primary {
|
||||
background: rgba(0, 82, 217, 0.1);
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
.potential-tag--warning {
|
||||
background: rgba(237, 123, 47, 0.1);
|
||||
color: #ed7b2f;
|
||||
}
|
||||
|
||||
.potential-tag--success {
|
||||
background: rgba(0, 168, 112, 0.1);
|
||||
color: #00a870;
|
||||
}
|
||||
|
||||
.potential-tag--gray {
|
||||
background: #eeeeee;
|
||||
color: #777777;
|
||||
}
|
||||
|
||||
/* ===== 卡片中间行(flex 布局,左对齐名字位置) ===== */
|
||||
.card-mid-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6rpx 0 4rpx 80rpx;
|
||||
}
|
||||
|
||||
.mid-text {
|
||||
font-size: 24rpx;
|
||||
color: #c5c5c5;
|
||||
}
|
||||
|
||||
.mid-dark {
|
||||
color: #393939;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mid-primary {
|
||||
color: #0052d9;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mid-primary-bold {
|
||||
color: #0052d9;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mid-ml {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.mid-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mid-error {
|
||||
color: #e34d59;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ===== 网格布局 ===== */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
gap: 12rpx;
|
||||
padding: 6rpx 0 4rpx 80rpx;
|
||||
}
|
||||
|
||||
.card-grid--3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-grid--4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.grid-label {
|
||||
font-size: 18rpx;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
.grid-val {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #393939;
|
||||
}
|
||||
|
||||
.grid-val--success {
|
||||
color: #00a870;
|
||||
}
|
||||
|
||||
.grid-val--warning {
|
||||
color: #ed7b2f;
|
||||
}
|
||||
|
||||
.grid-val--lg {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ===== 迷你柱状图(最频繁维度) ===== */
|
||||
.mini-chart {
|
||||
padding: 8rpx 0 4rpx 80rpx;
|
||||
}
|
||||
|
||||
.mini-chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.mini-chart-label {
|
||||
font-size: 18rpx;
|
||||
color: #c5c5c5;
|
||||
}
|
||||
|
||||
.mini-chart-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 6rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.mini-bar-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mini-bar {
|
||||
width: 100%;
|
||||
background: rgba(0, 82, 217, 0.3);
|
||||
border-radius: 4rpx 4rpx 0 0;
|
||||
min-height: 4rpx;
|
||||
}
|
||||
|
||||
.mini-chart-nums {
|
||||
display: flex;
|
||||
gap: 6rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.mini-chart-num {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 18rpx;
|
||||
color: #c5c5c5;
|
||||
}
|
||||
|
||||
.mini-chart-num--active {
|
||||
color: #0052d9;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ===== 助教行 ===== */
|
||||
.card-assistant-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
margin-top: 10rpx;
|
||||
margin-left: 80rpx;
|
||||
padding-top: 10rpx;
|
||||
border-top: 2rpx solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.assistant-label {
|
||||
font-size: 22rpx;
|
||||
color: #a6a6a6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.assistant-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.assistant-heart {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
.assistant-name {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.assistant-name.assistant--assignee {
|
||||
color: #e34d59;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.assistant-name.assistant--abandoned {
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
.assistant-name.assistant--normal {
|
||||
color: #242424;
|
||||
}
|
||||
|
||||
.assistant-sep {
|
||||
font-size: 20rpx;
|
||||
color: #c5c5c5;
|
||||
margin: 0 6rpx;
|
||||
}
|
||||
|
||||
/* 跟/弃 badge — 忠于 H5 原型:白字 + 渐变背景 + 阴影 */
|
||||
.assistant-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 28rpx;
|
||||
height: 24rpx;
|
||||
padding: 0 8rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 18rpx;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5rpx;
|
||||
margin-left: 4rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.assistant-badge--follow {
|
||||
background: linear-gradient(135deg, #e34d59 0%, #f26a76 100%);
|
||||
border: 1rpx solid rgba(227, 77, 89, 0.28);
|
||||
box-shadow: 0 2rpx 6rpx rgba(227, 77, 89, 0.28);
|
||||
}
|
||||
|
||||
.assistant-badge--drop {
|
||||
background: linear-gradient(135deg, #d4d4d4 0%, #b6b6b6 100%);
|
||||
border: 1rpx solid rgba(120, 120, 120, 0.18);
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
/* ===== 最专一维度:助教服务明细表 ===== */
|
||||
.loyal-table {
|
||||
padding: 6rpx 0 4rpx 80rpx;
|
||||
border-left: 4rpx solid #eeeeee;
|
||||
margin-left: 80rpx;
|
||||
padding-left: 14rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.loyal-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
padding: 6rpx 0;
|
||||
}
|
||||
|
||||
.loyal-row--header {
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.loyal-row--header .loyal-col {
|
||||
font-size: 20rpx;
|
||||
color: #c5c5c5;
|
||||
}
|
||||
|
||||
.loyal-col {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.loyal-col--name {
|
||||
width: 140rpx;
|
||||
flex: none;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.loyal-coach-name {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loyal-coach-name.assistant--assignee {
|
||||
color: #e34d59;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.loyal-coach-name.assistant--abandoned {
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
.loyal-coach-name.assistant--normal {
|
||||
color: #242424;
|
||||
}
|
||||
|
||||
.loyal-val {
|
||||
font-weight: 600;
|
||||
color: #393939;
|
||||
}
|
||||
|
||||
.loyal-val--primary {
|
||||
font-weight: 700;
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
.loyal-val--gray {
|
||||
color: #8b8b8b;
|
||||
}
|
||||
|
||||
/* 最专一头部右侧 */
|
||||
.header-metrics--loyal {
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.loyal-top-name {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #e34d59;
|
||||
}
|
||||
|
||||
.loyal-top-score {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
/* ===== 底部安全区 ===== */
|
||||
.safe-bottom {
|
||||
height: 200rpx;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"navigationBarTitleText": "看板",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"metric-card": "/components/metric-card/metric-card",
|
||||
"filter-dropdown": "/components/filter-dropdown/filter-dropdown",
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
// 财务看板页 — 忠于 H5 原型结构,内联 mock 数据
|
||||
// TODO: 联调时替换 mock 数据为真实 API 调用
|
||||
|
||||
/** 目录板块定义 */
|
||||
interface TocItem {
|
||||
emoji: string
|
||||
title: string
|
||||
sectionId: string
|
||||
}
|
||||
|
||||
/** 指标解释映射 */
|
||||
const tipContents: Record<string, { title: string; content: string }> = {
|
||||
occurrence: {
|
||||
title: '发生额/正价',
|
||||
content: '所有消费项目按标价计算的总金额,不扣除任何优惠。\n\n即"如果没有任何折扣,客户应付多少"。',
|
||||
},
|
||||
discount: {
|
||||
title: '总优惠',
|
||||
content: '包含会员折扣、赠送卡抵扣、团购差价等所有优惠金额。\n\n优惠越高,实际收入越低。',
|
||||
},
|
||||
confirmed: {
|
||||
title: '成交/确认收入',
|
||||
content: '发生额减去总优惠后的实际入账金额。\n\n成交收入 = 发生额 - 总优惠',
|
||||
},
|
||||
cashIn: {
|
||||
title: '实收/现金流入',
|
||||
content: '实际到账的现金,包含消费直接支付和储值充值。\n\n往期为已结算金额,本期为截至当前的发生额。',
|
||||
},
|
||||
cashOut: {
|
||||
title: '现金支出',
|
||||
content: '包含人工、房租、水电、进货等所有经营支出。',
|
||||
},
|
||||
balance: {
|
||||
title: '现金结余',
|
||||
content: '现金流入减去现金支出。\n\n现金结余 = 现金流入 - 现金支出',
|
||||
},
|
||||
rechargeActual: {
|
||||
title: '储值卡充值实收',
|
||||
content: '会员储值卡首充和续费的实际到账金额。\n\n不含赠送金额。',
|
||||
},
|
||||
firstCharge: {
|
||||
title: '首充',
|
||||
content: '新会员首次充值的金额。',
|
||||
},
|
||||
renewCharge: {
|
||||
title: '续费',
|
||||
content: '老会员续费充值的金额。',
|
||||
},
|
||||
consume: {
|
||||
title: '消耗',
|
||||
content: '会员使用储值卡消费的金额。',
|
||||
},
|
||||
cardBalance: {
|
||||
title: '储值卡总余额',
|
||||
content: '所有储值卡的剩余可用余额。',
|
||||
},
|
||||
allCardBalance: {
|
||||
title: '全类别会员卡余额合计',
|
||||
content: '储值卡 + 赠送卡(酒水卡、台费卡、抵用券)的总余额。\n\n仅供经营参考,非财务属性。',
|
||||
},
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'normal' as 'loading' | 'empty' | 'normal',
|
||||
|
||||
/** 时间筛选 */
|
||||
selectedTime: 'month',
|
||||
timeOptions: [
|
||||
{ value: 'month', text: '本月' },
|
||||
{ value: 'lastMonth', text: '上月' },
|
||||
{ value: 'week', text: '本周' },
|
||||
{ value: 'lastWeek', text: '上周' },
|
||||
{ value: 'quarter3', text: '前3个月 不含本月' },
|
||||
{ value: 'quarter', text: '本季度' },
|
||||
{ value: 'lastQuarter', text: '上季度' },
|
||||
{ value: 'half6', text: '最近6个月不含本月' },
|
||||
],
|
||||
|
||||
/** 区域筛选 */
|
||||
selectedArea: 'all',
|
||||
areaOptions: [
|
||||
{ value: 'all', text: '全部区域' },
|
||||
{ value: 'hall', text: '大厅' },
|
||||
{ value: 'hallA', text: 'A区' },
|
||||
{ value: 'hallB', text: 'B区' },
|
||||
{ value: 'hallC', text: 'C区' },
|
||||
{ value: 'mahjong', text: '麻将房' },
|
||||
{ value: 'teamBuilding', text: '团建房' },
|
||||
],
|
||||
|
||||
/** 环比开关 */
|
||||
compareEnabled: false,
|
||||
|
||||
/** 目录导航 */
|
||||
tocVisible: false,
|
||||
tocItems: [
|
||||
{ emoji: '📈', title: '经营一览', sectionId: 'section-overview' },
|
||||
{ emoji: '💳', title: '预收资产', sectionId: 'section-recharge' },
|
||||
{ emoji: '💰', title: '应计收入确认', sectionId: 'section-revenue' },
|
||||
{ emoji: '🧾', title: '现金流入', sectionId: 'section-cashflow' },
|
||||
{ emoji: '📤', title: '现金流出', sectionId: 'section-expense' },
|
||||
{ emoji: '🎱', title: '助教分析', sectionId: 'section-coach' },
|
||||
] as TocItem[],
|
||||
currentSectionIndex: 0,
|
||||
scrollIntoView: '',
|
||||
|
||||
/** 提示弹窗 */
|
||||
tipVisible: false,
|
||||
tipTitle: '',
|
||||
tipContent: '',
|
||||
|
||||
/** 经营一览 */
|
||||
overview: {
|
||||
occurrence: '¥823,456',
|
||||
occurrenceCompare: '12.5%',
|
||||
discount: '-¥113,336',
|
||||
discountCompare: '3.2%',
|
||||
discountRate: '13.8%',
|
||||
discountRateCompare: '1.5%',
|
||||
confirmedRevenue: '¥710,120',
|
||||
confirmedCompare: '8.7%',
|
||||
cashIn: '¥698,500',
|
||||
cashInCompare: '5.3%',
|
||||
cashOut: '¥472,300',
|
||||
cashOutCompare: '2.1%',
|
||||
cashBalance: '¥226,200',
|
||||
cashBalanceCompare: '15.2%',
|
||||
balanceRate: '32.4%',
|
||||
balanceRateCompare: '3.8%',
|
||||
},
|
||||
|
||||
/** 预收资产 */
|
||||
recharge: {
|
||||
actualIncome: '¥352,800',
|
||||
actualCompare: '18.5%',
|
||||
firstCharge: '¥188,500',
|
||||
firstChargeCompare: '12.3%',
|
||||
renewCharge: '¥164,300',
|
||||
renewChargeCompare: '8.7%',
|
||||
consumed: '¥238,200',
|
||||
consumedCompare: '5.2%',
|
||||
cardBalance: '¥642,600',
|
||||
cardBalanceCompare: '11.4%',
|
||||
giftRows: [
|
||||
{
|
||||
label: '新增', total: '¥108,600', totalCompare: '9.8%',
|
||||
wine: '¥43,200', wineCompare: '11.2%',
|
||||
table: '¥54,100', tableCompare: '8.5%',
|
||||
coupon: '¥11,300', couponCompare: '6.3%',
|
||||
},
|
||||
{
|
||||
label: '消费', total: '¥75,800', totalCompare: '7.2%',
|
||||
wine: '¥32,100', wineCompare: '8.1%',
|
||||
table: '¥32,800', tableCompare: '6.5%',
|
||||
coupon: '¥10,900', couponCompare: '5.8%',
|
||||
},
|
||||
{
|
||||
label: '余额', total: '¥243,900', totalCompare: '4.5%',
|
||||
wine: '¥118,500', wineCompare: '5.2%',
|
||||
table: '¥109,200', tableCompare: '3.8%',
|
||||
coupon: '¥16,200', couponCompare: '2.5%',
|
||||
},
|
||||
],
|
||||
allCardBalance: '¥586,500',
|
||||
allCardBalanceCompare: '6.2%',
|
||||
},
|
||||
|
||||
/** 应计收入确认 */
|
||||
revenue: {
|
||||
structureRows: [
|
||||
{ name: '开台与包厢', amount: '¥358,600', discount: '-¥45,200', booked: '¥313,400', bookedCompare: '9.2%' },
|
||||
{ name: 'A区', amount: '¥118,200', discount: '-¥11,600', booked: '¥106,600', bookedCompare: '12.1%', isSub: true },
|
||||
{ name: 'B区', amount: '¥95,800', discount: '-¥11,200', booked: '¥84,600', bookedCompare: '8.5%', isSub: true },
|
||||
{ name: 'C区', amount: '¥72,600', discount: '-¥11,100', booked: '¥61,500', bookedCompare: '6.3%', isSub: true },
|
||||
{ name: '团建区', amount: '¥48,200', discount: '-¥6,800', booked: '¥41,400', bookedCompare: '5.8%', isSub: true },
|
||||
{ name: '麻将区', amount: '¥23,800', discount: '-¥4,500', booked: '¥19,300', bookedCompare: '-2.1%', isSub: true },
|
||||
{ name: '助教', desc: '基础课', amount: '¥232,500', discount: '-', booked: '¥232,500', bookedCompare: '15.3%' },
|
||||
{ name: '助教', desc: '激励课', amount: '¥112,800', discount: '-', booked: '¥112,800', bookedCompare: '8.2%' },
|
||||
{ name: '食品酒水', amount: '¥119,556', discount: '-¥68,136', booked: '¥51,420', bookedCompare: '6.5%' },
|
||||
],
|
||||
priceItems: [
|
||||
{ name: '开台消费', value: '¥358,600', compare: '9.2%' },
|
||||
{ name: '酒水商品', value: '¥186,420', compare: '18.5%' },
|
||||
{ name: '包厢费用', value: '¥165,636', compare: '12.1%' },
|
||||
{ name: '助教服务', value: '¥112,800', compare: '15.3%' },
|
||||
],
|
||||
totalOccurrence: '¥823,456',
|
||||
totalOccurrenceCompare: '12.5%',
|
||||
discountItems: [
|
||||
{ name: '会员折扣', value: '-¥45,200', compare: '3.1%' },
|
||||
{ name: '赠送卡抵扣', value: '-¥42,016', compare: '2.5%' },
|
||||
{ name: '团购差价', value: '-¥26,120', compare: '5.2%' },
|
||||
],
|
||||
confirmedTotal: '¥710,120',
|
||||
confirmedTotalCompare: '8.7%',
|
||||
channelItems: [
|
||||
{ name: '储值卡结算冲销', value: '¥238,200', compare: '11.2%' },
|
||||
{ name: '现金/线上支付', value: '¥345,800', compare: '7.8%' },
|
||||
{ name: '团购核销确认收入', desc: '团购成交价', value: '¥126,120', compare: '5.3%' },
|
||||
],
|
||||
},
|
||||
|
||||
/** 现金流入 */
|
||||
cashflow: {
|
||||
consumeItems: [
|
||||
{ name: '纸币现金', desc: '柜台现金收款', value: '¥85,600', compare: '12.3%', isDown: true },
|
||||
{ name: '线上收款', desc: '微信/支付宝/刷卡 已扣除平台服务费', value: '¥260,200', compare: '8.5%', isDown: false },
|
||||
{ name: '团购平台', desc: '美团/抖音回款 已扣除平台服务费', value: '¥126,120', compare: '15.2%', isDown: false },
|
||||
],
|
||||
rechargeItems: [
|
||||
{ name: '会员充值到账', desc: '首充/续费实收', value: '¥352,800', compare: '18.5%' },
|
||||
],
|
||||
total: '¥824,720',
|
||||
totalCompare: '10.2%',
|
||||
},
|
||||
|
||||
/** 现金流出 */
|
||||
expense: {
|
||||
operationItems: [
|
||||
{ name: '食品饮料', value: '¥108,200', compare: '4.5%', isDown: false },
|
||||
{ name: '耗材', value: '¥21,850', compare: '2.1%', isDown: true },
|
||||
{ name: '报销', value: '¥10,920', compare: '6.8%', isDown: false },
|
||||
],
|
||||
fixedItems: [
|
||||
{ name: '房租', value: '¥125,000', compare: '持平', isFlat: true },
|
||||
{ name: '水电', value: '¥24,200', compare: '3.2%', isFlat: false },
|
||||
{ name: '物业', value: '¥11,500', compare: '持平', isFlat: true },
|
||||
{ name: '人员工资', value: '¥112,000', compare: '持平', isFlat: true },
|
||||
],
|
||||
coachItems: [
|
||||
{ name: '基础课分成', value: '¥116,250', compare: '8.2%', isDown: false },
|
||||
{ name: '激励课分成', value: '¥23,840', compare: '5.6%', isDown: false },
|
||||
{ name: '充值提成', value: '¥12,640', compare: '12.3%', isDown: false },
|
||||
{ name: '额外奖金', value: '¥11,500', compare: '3.1%', isDown: true },
|
||||
],
|
||||
platformItems: [
|
||||
{ name: '汇来米', value: '¥10,680', compare: '1.5%' },
|
||||
{ name: '美团', value: '¥11,240', compare: '2.8%' },
|
||||
{ name: '抖音', value: '¥10,580', compare: '3.5%' },
|
||||
],
|
||||
total: '¥600,400',
|
||||
totalCompare: '2.1%',
|
||||
},
|
||||
|
||||
/** 助教分析 */
|
||||
coachAnalysis: {
|
||||
basic: {
|
||||
totalPay: '¥232,500',
|
||||
totalPayCompare: '15.3%',
|
||||
totalShare: '¥116,250',
|
||||
totalShareCompare: '15.3%',
|
||||
avgHourly: '¥25/h',
|
||||
avgHourlyCompare: '4.2%',
|
||||
rows: [
|
||||
{ level: '初级', pay: '¥68,600', payCompare: '12.5%', share: '¥34,300', shareCompare: '12.5%', hourly: '¥20/h', hourlyCompare: '持平', hourlyFlat: true },
|
||||
{ level: '中级', pay: '¥82,400', payCompare: '18.2%', share: '¥41,200', shareCompare: '18.2%', hourly: '¥25/h', hourlyCompare: '8.7%' },
|
||||
{ level: '高级', pay: '¥57,800', payCompare: '14.6%', share: '¥28,900', shareCompare: '14.6%', hourly: '¥30/h', hourlyCompare: '持平', hourlyFlat: true },
|
||||
{ level: '星级', pay: '¥23,700', payCompare: '3.2%', payDown: true, share: '¥11,850', shareCompare: '3.2%', shareDown: true, hourly: '¥35/h', hourlyCompare: '持平', hourlyFlat: true },
|
||||
],
|
||||
},
|
||||
incentive: {
|
||||
totalPay: '¥112,800',
|
||||
totalPayCompare: '8.2%',
|
||||
totalShare: '¥33,840',
|
||||
totalShareCompare: '8.2%',
|
||||
avgHourly: '¥15/h',
|
||||
avgHourlyCompare: '2.1%',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// mock 数据已内联,直接显示
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
setTimeout(() => wx.stopPullDownRefresh(), 500)
|
||||
},
|
||||
|
||||
/** 看板 Tab 切换 */
|
||||
onTabChange(e: WechatMiniprogram.TouchEvent) {
|
||||
const tab = e.currentTarget.dataset.tab as string
|
||||
if (tab === 'customer') {
|
||||
wx.navigateTo({ url: '/pages/board-customer/board-customer' })
|
||||
} else if (tab === 'coach') {
|
||||
wx.navigateTo({ url: '/pages/board-coach/board-coach' })
|
||||
}
|
||||
},
|
||||
|
||||
/** 时间筛选变更 */
|
||||
onTimeChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
this.setData({ selectedTime: e.detail.value })
|
||||
},
|
||||
|
||||
/** 区域筛选变更 */
|
||||
onAreaChange(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
|
||||
this.setData({ selectedArea: e.detail.value })
|
||||
},
|
||||
|
||||
/** 环比开关切换 */
|
||||
toggleCompare() {
|
||||
this.setData({ compareEnabled: !this.data.compareEnabled })
|
||||
},
|
||||
|
||||
/** 目录导航开关 */
|
||||
toggleToc() {
|
||||
this.setData({ tocVisible: !this.data.tocVisible })
|
||||
},
|
||||
|
||||
closeToc() {
|
||||
this.setData({ tocVisible: false })
|
||||
},
|
||||
|
||||
/** 目录项点击 → 滚动到对应板块 */
|
||||
onTocItemTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const index = e.currentTarget.dataset.index as number
|
||||
const sectionId = this.data.tocItems[index]?.sectionId
|
||||
if (sectionId) {
|
||||
this.setData({
|
||||
tocVisible: false,
|
||||
currentSectionIndex: index,
|
||||
scrollIntoView: sectionId,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 帮助图标点击 → 弹出说明 */
|
||||
onHelpTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const key = e.currentTarget.dataset.key as string
|
||||
const tip = tipContents[key]
|
||||
if (tip) {
|
||||
this.setData({
|
||||
tipVisible: true,
|
||||
tipTitle: tip.title,
|
||||
tipContent: tip.content,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 关闭提示弹窗 */
|
||||
closeTip() {
|
||||
this.setData({ tipVisible: false })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,734 @@
|
||||
<!-- 财务看板页 — 忠于 H5 原型结构 -->
|
||||
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-empty description="暂无财务数据" />
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:else>
|
||||
<!-- 顶部看板 Tab 导航 -->
|
||||
<view class="board-tabs">
|
||||
<view class="board-tab board-tab--active" data-tab="finance">
|
||||
<text>财务</text>
|
||||
</view>
|
||||
<view class="board-tab" data-tab="customer" bindtap="onTabChange">
|
||||
<text>客户</text>
|
||||
</view>
|
||||
<view class="board-tab" data-tab="coach" bindtap="onTabChange">
|
||||
<text>助教</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<view class="filter-bar">
|
||||
<view class="filter-bar-inner">
|
||||
<!-- 目录按钮 -->
|
||||
<view class="toc-btn" bindtap="toggleToc">
|
||||
<t-icon name="view-list" size="40rpx" color="#ffffff" />
|
||||
</view>
|
||||
|
||||
<!-- 时间筛选 -->
|
||||
<view class="filter-item">
|
||||
<filter-dropdown
|
||||
label="本月"
|
||||
options="{{timeOptions}}"
|
||||
value="{{selectedTime}}"
|
||||
bind:change="onTimeChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 区域筛选 -->
|
||||
<view class="filter-item">
|
||||
<filter-dropdown
|
||||
label="全部区域"
|
||||
options="{{areaOptions}}"
|
||||
value="{{selectedArea}}"
|
||||
bind:change="onAreaChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 环比开关 -->
|
||||
<view class="compare-switch" bindtap="toggleCompare">
|
||||
<text class="compare-label">环比</text>
|
||||
<view class="compare-toggle {{compareEnabled ? 'compare-toggle--active' : ''}}">
|
||||
<view class="compare-toggle-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 滚动内容区 -->
|
||||
<scroll-view
|
||||
class="board-content"
|
||||
scroll-y
|
||||
scroll-into-view="{{scrollIntoView}}"
|
||||
scroll-with-animation
|
||||
>
|
||||
|
||||
<!-- ===== 板块 1: 经营一览(深色) ===== -->
|
||||
<view id="section-overview" class="card-section section-dark">
|
||||
<view class="card-header-dark">
|
||||
<text class="card-header-emoji">📈</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-dark">经营一览</text>
|
||||
<text class="card-header-desc-dark">快速了解收入与现金流的整体健康度</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收入概览 -->
|
||||
<view class="sub-section-label">
|
||||
<text class="sub-label-text">收入概览</text>
|
||||
<text class="sub-label-desc">记账口径收入与优惠</text>
|
||||
</view>
|
||||
|
||||
<view class="overview-grid-3">
|
||||
<view class="overview-cell">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">发生额/正价</text>
|
||||
<view class="help-icon-light" data-key="occurrence" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-white">{{overview.occurrence}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.occurrenceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">总优惠</text>
|
||||
<view class="help-icon-light" data-key="discount" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-red">{{overview.discount}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-down">↓{{overview.discountCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">优惠占比</text>
|
||||
</view>
|
||||
<text class="cell-value-gray">{{overview.discountRate}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-down">↓{{overview.discountRateCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成交/确认收入 -->
|
||||
<view class="confirmed-row">
|
||||
<view class="confirmed-left">
|
||||
<text class="confirmed-label">成交/确认收入</text>
|
||||
<view class="help-icon-light" data-key="confirmed" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<view class="confirmed-right">
|
||||
<text class="confirmed-value">{{overview.confirmedRevenue}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.confirmedCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-divider-light"></view>
|
||||
|
||||
<!-- 现金流水概览 -->
|
||||
<view class="sub-section-label">
|
||||
<text class="sub-label-text">现金流水概览</text>
|
||||
<text class="sub-label-desc">往期为已结算 本期为截至当前的发生额</text>
|
||||
</view>
|
||||
|
||||
<view class="overview-grid-2">
|
||||
<view class="overview-cell-bg">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">实收/现金流入</text>
|
||||
<view class="help-icon-light" data-key="cashIn" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-white-sm">{{overview.cashIn}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.cashInCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell-bg">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">现金支出</text>
|
||||
<view class="help-icon-light" data-key="cashOut" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-gray-sm">{{overview.cashOut}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.cashOutCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell-bg">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">现金结余</text>
|
||||
<view class="help-icon-light" data-key="balance" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-white-sm">{{overview.cashBalance}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.cashBalanceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-cell-bg">
|
||||
<view class="cell-label-row">
|
||||
<text class="cell-label-light">结余率</text>
|
||||
</view>
|
||||
<text class="cell-value-white-sm">{{overview.balanceRate}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up">↑{{overview.balanceRateCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 洞察 -->
|
||||
<view class="ai-insight-section">
|
||||
<view class="ai-insight-header">
|
||||
<view class="ai-insight-icon">🤖</view>
|
||||
<text class="ai-insight-title">AI 智能洞察</text>
|
||||
</view>
|
||||
<view class="ai-insight-body">
|
||||
<text class="ai-insight-line"><text class="ai-insight-dim">优惠率Top:</text>团购(5.2%) / 大客户(3.1%) / 赠送卡(2.5%)</text>
|
||||
<text class="ai-insight-line"><text class="ai-insight-dim">差异最大:</text>酒水(+18%) / 台桌(-5%) / 包厢(+12%)</text>
|
||||
<text class="ai-insight-line"><text class="ai-insight-dim">建议关注:</text>充值高但消耗低,会员活跃度需提升</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 2: 预收资产 ===== -->
|
||||
<view id="section-recharge" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">💳</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">预收资产</text>
|
||||
<text class="card-header-desc-light">会员卡充值与余额 掌握资金沉淀</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 储值卡统计 -->
|
||||
<view class="section-body">
|
||||
<text class="card-section-title">储值卡统计</text>
|
||||
<view class="table-bordered">
|
||||
<!-- 行1:储值卡充值实收 -->
|
||||
<view class="table-row table-row--highlight">
|
||||
<view class="table-row-left">
|
||||
<text class="table-row-label-bold">储值卡充值实收</text>
|
||||
<view class="help-icon-dark" data-key="rechargeActual" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<view class="table-row-right">
|
||||
<text class="table-row-value-lg">{{recharge.actualIncome}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{recharge.actualCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 行2:首充/续费/消耗 三列 -->
|
||||
<view class="table-row-grid3">
|
||||
<view class="grid-cell">
|
||||
<view class="cell-label-row-sm">
|
||||
<text class="cell-label-sm">首充</text>
|
||||
<view class="help-icon-dark-sm" data-key="firstCharge" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-sm">{{recharge.firstCharge}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{recharge.firstChargeCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<view class="cell-label-row-sm">
|
||||
<text class="cell-label-sm">续费</text>
|
||||
<view class="help-icon-dark-sm" data-key="renewCharge" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-sm">{{recharge.renewCharge}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{recharge.renewChargeCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="grid-cell">
|
||||
<view class="cell-label-row-sm">
|
||||
<text class="cell-label-sm">消耗</text>
|
||||
<view class="help-icon-dark-sm" data-key="consume" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<text class="cell-value-sm">{{recharge.consumed}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{recharge.consumedCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 行3:储值卡总余额 -->
|
||||
<view class="table-row table-row--footer">
|
||||
<view class="table-row-left">
|
||||
<text class="table-row-label-bold">储值卡总余额</text>
|
||||
<view class="help-icon-dark" data-key="cardBalance" bindtap="onHelpTap">?</view>
|
||||
</view>
|
||||
<view class="table-row-right">
|
||||
<text class="table-row-value-lg">{{recharge.cardBalance}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{recharge.cardBalanceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 赠送卡统计详情 -->
|
||||
<text class="card-section-title" style="margin-top: 28rpx;">赠送卡统计详情</text>
|
||||
<view class="table-bordered">
|
||||
<!-- 表头 -->
|
||||
<view class="gift-table-header">
|
||||
<text class="gift-col gift-col--name">类型</text>
|
||||
<text class="gift-col">酒水卡</text>
|
||||
<text class="gift-col">台费卡</text>
|
||||
<text class="gift-col">抵用券</text>
|
||||
</view>
|
||||
<!-- 新增行 -->
|
||||
<view class="gift-table-row" wx:for="{{recharge.giftRows}}" wx:key="label">
|
||||
<view class="gift-col gift-col--name">
|
||||
<text class="gift-row-label">{{item.label}}</text>
|
||||
<text class="gift-row-total">{{item.total}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.totalCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gift-col">
|
||||
<text class="gift-col-val">{{item.wine}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.wineCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gift-col">
|
||||
<text class="gift-col-val">{{item.table}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.tableCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gift-col">
|
||||
<text class="gift-col-val">{{item.coupon}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.couponCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 全类别会员卡余额合计 -->
|
||||
<view class="total-balance-row">
|
||||
<view class="total-balance-left">
|
||||
<text class="total-balance-label">全类别会员卡余额合计</text>
|
||||
<view class="help-icon-dark" data-key="allCardBalance" bindtap="onHelpTap">?</view>
|
||||
<text class="total-balance-note">仅经营参考,非财务属性</text>
|
||||
</view>
|
||||
<view class="total-balance-right">
|
||||
<text class="total-balance-value">{{recharge.allCardBalance}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{recharge.allCardBalanceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 3: 应计收入确认 ===== -->
|
||||
<view id="section-revenue" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">💰</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">【记账】应计收入确认</text>
|
||||
<text class="card-header-desc-light">从发生额到入账收入的全流程</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-body">
|
||||
<!-- 收入结构 -->
|
||||
<view class="sub-title-row">
|
||||
<text class="sub-title-text">收入结构</text>
|
||||
<text class="sub-title-desc">按业务查看各项应计收入的构成</text>
|
||||
</view>
|
||||
<view class="table-bordered">
|
||||
<!-- 表头 -->
|
||||
<view class="rev-table-header">
|
||||
<text class="rev-col rev-col--name">项目</text>
|
||||
<text class="rev-col">发生额</text>
|
||||
<text class="rev-col">优惠</text>
|
||||
<text class="rev-col">入账</text>
|
||||
</view>
|
||||
<!-- 数据行 -->
|
||||
<block wx:for="{{revenue.structureRows}}" wx:key="name">
|
||||
<view class="rev-table-row {{item.isSub ? 'rev-table-row--sub' : ''}}">
|
||||
<view class="rev-col rev-col--name">
|
||||
<text class="{{item.isSub ? 'rev-name-sub' : 'rev-name'}}">{{item.name}}</text>
|
||||
<text class="rev-name-desc" wx:if="{{item.desc}}">{{item.desc}}</text>
|
||||
</view>
|
||||
<text class="rev-col rev-val">{{item.amount}}</text>
|
||||
<text class="rev-col rev-val {{item.discount !== '-' ? 'rev-val--red' : 'rev-val--muted'}}">{{item.discount}}</text>
|
||||
<view class="rev-col">
|
||||
<text class="rev-val {{item.isSub ? '' : 'rev-val--bold'}}">{{item.booked}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled && item.bookedCompare}}">
|
||||
<text class="compare-text-up-xs">↑{{item.bookedCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 收入确认(损益链) -->
|
||||
<view class="sub-title-row" style="margin-top: 28rpx;">
|
||||
<text class="sub-title-text">收入确认</text>
|
||||
<text class="sub-title-desc">从正价到收款方式的损益链</text>
|
||||
</view>
|
||||
<view class="table-bordered">
|
||||
<!-- 项目正价 标题 -->
|
||||
<view class="flow-header">
|
||||
<text class="flow-header-title">项目正价</text>
|
||||
<text class="flow-header-desc">即标价测算</text>
|
||||
</view>
|
||||
<!-- 正价明细(左侧竖线) -->
|
||||
<view class="flow-detail-list">
|
||||
<view class="flow-detail-item" wx:for="{{revenue.priceItems}}" wx:key="name">
|
||||
<text class="flow-detail-name">{{item.name}}</text>
|
||||
<view class="flow-detail-right">
|
||||
<text class="flow-detail-val">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 发生额合计 -->
|
||||
<view class="flow-total-row">
|
||||
<view class="flow-total-left">
|
||||
<text class="flow-total-label">发生额</text>
|
||||
<text class="flow-total-desc">即上列正价合计</text>
|
||||
</view>
|
||||
<view class="flow-total-right">
|
||||
<text class="flow-total-value">{{revenue.totalOccurrence}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{revenue.totalOccurrenceCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 优惠扣减 -->
|
||||
<view class="flow-header flow-header--deduct">
|
||||
<text class="flow-header-title">优惠扣减</text>
|
||||
</view>
|
||||
<view class="flow-detail-list">
|
||||
<view class="flow-detail-item" wx:for="{{revenue.discountItems}}" wx:key="name">
|
||||
<text class="flow-detail-name">{{item.name}}</text>
|
||||
<view class="flow-detail-right">
|
||||
<text class="flow-detail-val flow-detail-val--red">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-down-xs">↓{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 成交收入 -->
|
||||
<view class="flow-total-row flow-total-row--accent">
|
||||
<view class="flow-total-left">
|
||||
<text class="flow-total-label">成交收入</text>
|
||||
<text class="flow-total-desc">发生额 - 优惠</text>
|
||||
</view>
|
||||
<view class="flow-total-right">
|
||||
<text class="flow-total-value">{{revenue.confirmedTotal}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{revenue.confirmedTotalCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 收款渠道 -->
|
||||
<view class="flow-header">
|
||||
<text class="flow-header-title">收款渠道明细</text>
|
||||
</view>
|
||||
<view class="flow-detail-list">
|
||||
<view class="flow-detail-item" wx:for="{{revenue.channelItems}}" wx:key="name">
|
||||
<view class="flow-detail-name-group">
|
||||
<text class="flow-detail-name">{{item.name}}</text>
|
||||
<text class="flow-detail-name-desc" wx:if="{{item.desc}}">{{item.desc}}</text>
|
||||
</view>
|
||||
<view class="flow-detail-right">
|
||||
<text class="flow-detail-val">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 4: 现金流入 ===== -->
|
||||
<view id="section-cashflow" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">🧾</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">【现金流水】流入</text>
|
||||
<text class="card-header-desc-light">实际到账的资金来源明细</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-body">
|
||||
<!-- 消费收入 -->
|
||||
<text class="flow-group-label">消费收入</text>
|
||||
<view class="flow-item-list">
|
||||
<view class="flow-item" wx:for="{{cashflow.consumeItems}}" wx:key="name">
|
||||
<view class="flow-item-left">
|
||||
<text class="flow-item-name">{{item.name}}</text>
|
||||
<text class="flow-item-desc" wx:if="{{item.desc}}">{{item.desc}}</text>
|
||||
</view>
|
||||
<view class="flow-item-right">
|
||||
<text class="flow-item-value">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.isDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.isDown ? '↓' : '↑'}}{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 充值收入 -->
|
||||
<text class="flow-group-label" style="margin-top: 20rpx;">充值收入</text>
|
||||
<view class="flow-item-list">
|
||||
<view class="flow-item" wx:for="{{cashflow.rechargeItems}}" wx:key="name">
|
||||
<view class="flow-item-left">
|
||||
<text class="flow-item-name">{{item.name}}</text>
|
||||
<text class="flow-item-desc" wx:if="{{item.desc}}">{{item.desc}}</text>
|
||||
</view>
|
||||
<view class="flow-item-right">
|
||||
<text class="flow-item-value">{{item.value}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 合计 -->
|
||||
<view class="flow-sum-row">
|
||||
<text class="flow-sum-label">现金流入合计</text>
|
||||
<view class="flow-sum-right">
|
||||
<text class="flow-sum-value">{{cashflow.total}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{cashflow.totalCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 5: 现金流出 ===== -->
|
||||
<view id="section-expense" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">📤</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">【现金流水】流出</text>
|
||||
<text class="card-header-desc-light">清晰呈现各类开销与结构</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-body">
|
||||
<!-- 进货与运营 3列 -->
|
||||
<text class="expense-group-label">进货与运营</text>
|
||||
<view class="expense-grid-3">
|
||||
<view class="expense-cell" wx:for="{{expense.operationItems}}" wx:key="name">
|
||||
<text class="expense-cell-label">{{item.name}}</text>
|
||||
<text class="expense-cell-value">{{item.value}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.isDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.isDown ? '↓' : '↑'}}{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 固定支出 2×2 -->
|
||||
<text class="expense-group-label">固定支出</text>
|
||||
<view class="expense-grid-2">
|
||||
<view class="expense-cell" wx:for="{{expense.fixedItems}}" wx:key="name">
|
||||
<text class="expense-cell-label">{{item.name}}</text>
|
||||
<text class="expense-cell-value">{{item.value}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.isFlat ? 'compare-text-flat-xs' : 'compare-text-up-xs'}}">{{item.isFlat ? '' : '↑'}}{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 助教薪资 2×2 -->
|
||||
<text class="expense-group-label">助教薪资</text>
|
||||
<view class="expense-grid-2">
|
||||
<view class="expense-cell" wx:for="{{expense.coachItems}}" wx:key="name">
|
||||
<text class="expense-cell-label">{{item.name}}</text>
|
||||
<text class="expense-cell-value">{{item.value}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.isDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.isDown ? '↓' : '↑'}}{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 平台服务费 3列 -->
|
||||
<text class="expense-group-label">平台服务费</text>
|
||||
<text class="expense-group-note">服务费在流水流入时,平台已经扣除。不产生支出流水。</text>
|
||||
<view class="expense-grid-3">
|
||||
<view class="expense-cell" wx:for="{{expense.platformItems}}" wx:key="name">
|
||||
<text class="expense-cell-label">{{item.name}}</text>
|
||||
<text class="expense-cell-value">{{item.value}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{item.compare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支出合计 -->
|
||||
<view class="flow-sum-row">
|
||||
<text class="flow-sum-label">支出合计</text>
|
||||
<view class="flow-sum-right">
|
||||
<text class="flow-sum-value">{{expense.total}}</text>
|
||||
<view class="compare-row-inline" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-sm">↑{{expense.totalCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 板块 6: 助教分析 ===== -->
|
||||
<view id="section-coach" class="card-section">
|
||||
<view class="card-header-light">
|
||||
<text class="card-header-emoji">🎱</text>
|
||||
<view class="card-header-text">
|
||||
<text class="card-header-title-light">助教分析</text>
|
||||
<text class="card-header-desc-light">全部助教服务收入与分成的平均值</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-body">
|
||||
<!-- 基础课 -->
|
||||
<text class="card-section-title">助教 <text class="card-section-title-sub">(基础课)</text></text>
|
||||
<view class="table-bordered">
|
||||
<view class="coach-fin-header">
|
||||
<text class="coach-fin-col coach-fin-col--name">级别</text>
|
||||
<text class="coach-fin-col">客户支付</text>
|
||||
<text class="coach-fin-col">球房抽成</text>
|
||||
<text class="coach-fin-col">小时平均</text>
|
||||
</view>
|
||||
<!-- 合计行 -->
|
||||
<view class="coach-fin-row coach-fin-row--total">
|
||||
<text class="coach-fin-col coach-fin-col--name coach-fin-bold">合计</text>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-bold">{{coachAnalysis.basic.totalPay}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.basic.totalPayCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-bold">{{coachAnalysis.basic.totalShare}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.basic.totalShareCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val-sm">{{coachAnalysis.basic.avgHourly}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.basic.avgHourlyCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 明细行 -->
|
||||
<view class="coach-fin-row" wx:for="{{coachAnalysis.basic.rows}}" wx:key="level">
|
||||
<text class="coach-fin-col coach-fin-col--name">{{item.level}}</text>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val">{{item.pay}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.payDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.payDown ? '↓' : '↑'}}{{item.payCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val">{{item.share}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.shareDown ? 'compare-text-down-xs' : 'compare-text-up-xs'}}">{{item.shareDown ? '↓' : '↑'}}{{item.shareCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val-sm">{{item.hourly}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="{{item.hourlyFlat ? 'compare-text-flat-xs' : 'compare-text-up-xs'}}">{{item.hourlyFlat ? '' : '↑'}}{{item.hourlyCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 激励课 -->
|
||||
<text class="card-section-title" style="margin-top: 28rpx;">助教 <text class="card-section-title-sub">(激励课)</text></text>
|
||||
<view class="table-bordered">
|
||||
<view class="coach-fin-header">
|
||||
<text class="coach-fin-col coach-fin-col--name">级别</text>
|
||||
<text class="coach-fin-col">客户支付</text>
|
||||
<text class="coach-fin-col">球房抽成</text>
|
||||
<text class="coach-fin-col">小时平均</text>
|
||||
</view>
|
||||
<view class="coach-fin-row">
|
||||
<text class="coach-fin-col coach-fin-col--name coach-fin-bold">合计</text>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-bold">{{coachAnalysis.incentive.totalPay}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.incentive.totalPayCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-bold">{{coachAnalysis.incentive.totalShare}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.incentive.totalShareCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coach-fin-col">
|
||||
<text class="coach-fin-val-sm">{{coachAnalysis.incentive.avgHourly}}</text>
|
||||
<view class="compare-row" wx:if="{{compareEnabled}}">
|
||||
<text class="compare-text-up-xs">↑{{coachAnalysis.incentive.avgHourlyCompare}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区 -->
|
||||
<view class="safe-bottom"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- ===== 目录导航遮罩 ===== -->
|
||||
<view class="toc-overlay" wx:if="{{tocVisible}}" catchtap="closeToc"></view>
|
||||
|
||||
<!-- ===== 目录导航面板 ===== -->
|
||||
<view class="toc-panel {{tocVisible ? 'toc-panel--show' : ''}}">
|
||||
<view class="toc-header">
|
||||
<text class="toc-header-text">📊 财务看板导航</text>
|
||||
</view>
|
||||
<view class="toc-list">
|
||||
<view
|
||||
class="toc-item {{currentSectionIndex === index ? 'toc-item--active' : ''}}"
|
||||
wx:for="{{tocItems}}"
|
||||
wx:key="sectionId"
|
||||
data-index="{{index}}"
|
||||
bindtap="onTocItemTap"
|
||||
>
|
||||
<text class="toc-item-emoji">{{item.emoji}}</text>
|
||||
<text class="toc-item-text">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 指标说明弹窗 ===== -->
|
||||
<view class="tip-overlay" wx:if="{{tipVisible}}" catchtap="closeTip"></view>
|
||||
<view class="tip-toast {{tipVisible ? 'tip-toast--show' : ''}}">
|
||||
<view class="tip-toast-header">
|
||||
<text class="tip-toast-title">{{tipTitle}}</text>
|
||||
<view class="tip-toast-close" bindtap="closeTip">
|
||||
<t-icon name="close" size="36rpx" color="#8b8b8b" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="tip-toast-content">{{tipContent}}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{200}}" />
|
||||
|
||||
<dev-fab />
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "对话历史",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { mockChatHistory } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
/** 带展示标签的对话历史项 */
|
||||
interface ChatHistoryDisplay {
|
||||
id: string
|
||||
title: string
|
||||
lastMessage: string
|
||||
timestamp: string
|
||||
customerName?: string
|
||||
/** 格式化后的时间标签 */
|
||||
timeLabel: string
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态:loading / empty / normal */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 对话历史列表 */
|
||||
list: [] as ChatHistoryDisplay[],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
/** 加载数据 */
|
||||
loadData() {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API 调用
|
||||
const sorted = sortByTimestamp(mockChatHistory)
|
||||
const list: ChatHistoryDisplay[] = sorted.map((item) => ({
|
||||
...item,
|
||||
timeLabel: this.formatTime(item.timestamp),
|
||||
}))
|
||||
|
||||
this.setData({
|
||||
list,
|
||||
pageState: list.length === 0 ? 'empty' : 'normal',
|
||||
})
|
||||
}, 400)
|
||||
},
|
||||
|
||||
/** 格式化时间为相对标签 */
|
||||
formatTime(timestamp: string): string {
|
||||
const now = new Date()
|
||||
const target = new Date(timestamp)
|
||||
const diffMs = now.getTime() - target.getTime()
|
||||
const diffMin = Math.floor(diffMs / 60000)
|
||||
const diffHour = Math.floor(diffMs / 3600000)
|
||||
const diffDay = Math.floor(diffMs / 86400000)
|
||||
|
||||
if (diffMin < 1) return '刚刚'
|
||||
if (diffMin < 60) return `${diffMin}分钟前`
|
||||
if (diffHour < 24) return `${diffHour}小时前`
|
||||
if (diffDay < 7) return `${diffDay}天前`
|
||||
|
||||
const month = target.getMonth() + 1
|
||||
const day = target.getDate()
|
||||
return `${month}月${day}日`
|
||||
},
|
||||
|
||||
/** 点击对话记录 → 跳转 chat 页面 */
|
||||
onItemTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({ url: '/pages/chat/chat?historyId=' + id })
|
||||
},
|
||||
|
||||
/** 下拉刷新 */
|
||||
onPullDownRefresh() {
|
||||
this.loadData()
|
||||
setTimeout(() => wx.stopPullDownRefresh(), 600)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-empty description="暂无对话记录" />
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- 对话列表 -->
|
||||
<view class="chat-list">
|
||||
<view
|
||||
class="chat-item"
|
||||
wx:for="{{list}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onItemTap"
|
||||
>
|
||||
<view class="chat-icon-box">
|
||||
<t-icon name="chat" size="40rpx" color="#ffffff" />
|
||||
</view>
|
||||
<view class="chat-content">
|
||||
<view class="chat-top">
|
||||
<text class="chat-title text-ellipsis">{{item.title}}</text>
|
||||
<text class="chat-time">{{item.timeLabel}}</text>
|
||||
</view>
|
||||
<view class="chat-bottom">
|
||||
<text class="chat-summary text-ellipsis" wx:if="{{item.customerName}}">{{item.customerName}} · {{item.lastMessage}}</text>
|
||||
<text class="chat-summary text-ellipsis" wx:else>{{item.lastMessage}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<view class="list-footer">
|
||||
<text class="footer-text">— 已加载全部记录 —</text>
|
||||
</view>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{120}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,89 @@
|
||||
/* ========== 加载态 & 空态 ========== */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
/* ========== 对话列表 ========== */
|
||||
.chat-list {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
gap: 24rpx;
|
||||
border-bottom: 1rpx solid var(--color-gray-1, #f3f3f3);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.chat-item:active {
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
}
|
||||
|
||||
/* 图标容器 */
|
||||
.chat-icon-box {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, var(--color-primary, #0052d9), #4d8cf5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 82, 217, 0.2);
|
||||
}
|
||||
|
||||
/* 内容区 */
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-13, #242424);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-time {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.chat-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-summary {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* ========== 底部提示 ========== */
|
||||
.list-footer {
|
||||
text-align: center;
|
||||
padding: 32rpx 0 64rpx;
|
||||
}
|
||||
.footer-text {
|
||||
font-size: 20rpx;
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
}
|
||||
7
apps/miniprogram - 副本/miniprogram/pages/chat/chat.json
Normal file
7
apps/miniprogram - 副本/miniprogram/pages/chat/chat.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "AI 助手",
|
||||
"usingComponents": {
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
168
apps/miniprogram - 副本/miniprogram/pages/chat/chat.ts
Normal file
168
apps/miniprogram - 副本/miniprogram/pages/chat/chat.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
// pages/chat/chat.ts — AI 对话页
|
||||
import { mockChatMessages } from '../../utils/mock-data'
|
||||
import type { ChatMessage } from '../../utils/mock-data'
|
||||
import { simulateStreamOutput } from '../../utils/chat'
|
||||
|
||||
/** 将 referenceCard.data (Record<string,string>) 转为数组供 WXML wx:for 渲染 */
|
||||
function toDataList(data?: Record<string, string>): Array<{ key: string; value: string }> {
|
||||
if (!data) return []
|
||||
return Object.keys(data).map((k) => ({ key: k, value: data[k] }))
|
||||
}
|
||||
|
||||
/** 为消息列表中的 referenceCard 补充 dataList 字段 */
|
||||
function enrichMessages(msgs: ChatMessage[]) {
|
||||
return msgs.map((m) => ({
|
||||
...m,
|
||||
referenceCard: m.referenceCard
|
||||
? { ...m.referenceCard, dataList: toDataList(m.referenceCard.data) }
|
||||
: undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Mock AI 回复模板 */
|
||||
const mockAIReplies = [
|
||||
'根据数据分析,这位客户近期消费频次有所下降,建议安排一次主动回访,了解具体原因。',
|
||||
'好的,我已经记录了你的需求。下次回访时我会提醒你。',
|
||||
'这位客户偏好中式台球,最近对斯诺克也产生了兴趣,可以推荐相关课程。',
|
||||
]
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态 */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 消息列表 */
|
||||
messages: [] as Array<ChatMessage & { referenceCard?: ChatMessage['referenceCard'] & { dataList?: Array<{ key: string; value: string }> } }>,
|
||||
/** 输入框内容 */
|
||||
inputText: '',
|
||||
/** AI 正在流式回复 */
|
||||
isStreaming: false,
|
||||
/** 流式输出中的内容 */
|
||||
streamingContent: '',
|
||||
/** 滚动锚点 */
|
||||
scrollToId: '',
|
||||
/** 页面顶部引用卡片(从其他页面跳转时) */
|
||||
referenceCard: null as { title: string; summary: string } | null,
|
||||
/** 客户 ID */
|
||||
customerId: '',
|
||||
},
|
||||
|
||||
/** 消息计数器,用于生成唯一 ID */
|
||||
_msgCounter: 0,
|
||||
|
||||
onLoad(options) {
|
||||
const customerId = options?.customerId || ''
|
||||
this.setData({ customerId })
|
||||
this.loadMessages(customerId)
|
||||
},
|
||||
|
||||
/** 加载消息(Mock) */
|
||||
loadMessages(customerId: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API 调用
|
||||
const messages = enrichMessages(mockChatMessages)
|
||||
this._msgCounter = messages.length
|
||||
|
||||
// 如果携带 customerId,显示引用卡片
|
||||
const referenceCard = customerId
|
||||
? { title: '客户详情', summary: `正在查看客户 ${customerId} 的相关信息` }
|
||||
: null
|
||||
|
||||
const isEmpty = messages.length === 0 && !referenceCard
|
||||
|
||||
this.setData({
|
||||
pageState: isEmpty ? 'empty' : 'normal',
|
||||
messages,
|
||||
referenceCard,
|
||||
})
|
||||
|
||||
// 滚动到底部
|
||||
this.scrollToBottom()
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/** 输入框内容变化 */
|
||||
onInputChange(e: WechatMiniprogram.Input) {
|
||||
this.setData({ inputText: e.detail.value })
|
||||
},
|
||||
|
||||
/** 发送消息 */
|
||||
onSendMessage() {
|
||||
const text = this.data.inputText.trim()
|
||||
if (!text || this.data.isStreaming) return
|
||||
|
||||
this._msgCounter++
|
||||
const userMsg = {
|
||||
id: `msg-user-${this._msgCounter}`,
|
||||
role: 'user' as const,
|
||||
content: text,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const messages = [...this.data.messages, userMsg]
|
||||
this.setData({
|
||||
messages,
|
||||
inputText: '',
|
||||
pageState: 'normal',
|
||||
})
|
||||
|
||||
this.scrollToBottom()
|
||||
|
||||
// 模拟 AI 回复
|
||||
setTimeout(() => {
|
||||
this.triggerAIReply()
|
||||
}, 300)
|
||||
},
|
||||
|
||||
/** 触发 AI 流式回复 */
|
||||
triggerAIReply() {
|
||||
this._msgCounter++
|
||||
const aiMsgId = `msg-ai-${this._msgCounter}`
|
||||
const replyText = mockAIReplies[this._msgCounter % mockAIReplies.length]
|
||||
|
||||
// 先添加空的 AI 消息占位
|
||||
const aiMsg = {
|
||||
id: aiMsgId,
|
||||
role: 'assistant' as const,
|
||||
content: '',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const messages = [...this.data.messages, aiMsg]
|
||||
this.setData({
|
||||
messages,
|
||||
isStreaming: true,
|
||||
streamingContent: '',
|
||||
})
|
||||
|
||||
this.scrollToBottom()
|
||||
|
||||
// 流式输出
|
||||
const aiIndex = messages.length - 1
|
||||
simulateStreamOutput(replyText, (partial: string) => {
|
||||
const key = `messages[${aiIndex}].content`
|
||||
this.setData({
|
||||
[key]: partial,
|
||||
streamingContent: partial,
|
||||
})
|
||||
this.scrollToBottom()
|
||||
}).then(() => {
|
||||
this.setData({
|
||||
isStreaming: false,
|
||||
streamingContent: '',
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/** 滚动到底部 */
|
||||
scrollToBottom() {
|
||||
// 使用 nextTick 确保 DOM 更新后再滚动
|
||||
setTimeout(() => {
|
||||
this.setData({ scrollToId: '' })
|
||||
setTimeout(() => {
|
||||
this.setData({ scrollToId: 'scroll-bottom' })
|
||||
}, 50)
|
||||
}, 50)
|
||||
},
|
||||
})
|
||||
125
apps/miniprogram - 副本/miniprogram/pages/chat/chat.wxml
Normal file
125
apps/miniprogram - 副本/miniprogram/pages/chat/chat.wxml
Normal file
@@ -0,0 +1,125 @@
|
||||
<!-- pages/chat/chat.wxml — AI 对话页 -->
|
||||
|
||||
<!-- 加载态 -->
|
||||
<view class="loading-container" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="48rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<view class="chat-page" wx:elif="{{pageState === 'normal' || pageState === 'empty'}}">
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view
|
||||
class="message-list"
|
||||
scroll-y
|
||||
scroll-into-view="{{scrollToId}}"
|
||||
scroll-with-animation
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
>
|
||||
<!-- 引用卡片(从其他页面跳转时显示) -->
|
||||
<view class="reference-card" wx:if="{{referenceCard}}">
|
||||
<view class="reference-header">
|
||||
<t-icon name="file-copy" size="32rpx" color="var(--color-gray-7)" />
|
||||
<text class="reference-source">来源:{{referenceCard.title}}</text>
|
||||
</view>
|
||||
<text class="reference-summary">{{referenceCard.summary}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 空对话提示 -->
|
||||
<view class="empty-hint" wx:if="{{pageState === 'empty' && messages.length === 0}}">
|
||||
<view class="empty-icon">🤖</view>
|
||||
<text class="empty-text">你好,我是 AI 助手</text>
|
||||
<text class="empty-sub">有什么可以帮你的?</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息气泡列表 -->
|
||||
<block wx:for="{{messages}}" wx:key="id">
|
||||
<!-- 用户消息:右对齐蓝色 -->
|
||||
<view
|
||||
class="message-row message-user"
|
||||
wx:if="{{item.role === 'user'}}"
|
||||
id="msg-{{item.id}}"
|
||||
>
|
||||
<view class="bubble bubble-user">
|
||||
<text class="bubble-text">{{item.content}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 消息:左对齐白色 -->
|
||||
<view
|
||||
class="message-row message-assistant"
|
||||
wx:else
|
||||
id="msg-{{item.id}}"
|
||||
>
|
||||
<view class="ai-avatar">
|
||||
<text class="ai-avatar-emoji">🤖</text>
|
||||
</view>
|
||||
<view class="bubble-wrapper">
|
||||
<view class="bubble bubble-assistant">
|
||||
<text class="bubble-text">{{item.content}}</text>
|
||||
</view>
|
||||
<!-- 引用卡片(AI 消息内联) -->
|
||||
<view class="inline-ref-card" wx:if="{{item.referenceCard}}">
|
||||
<view class="inline-ref-header">
|
||||
<text class="inline-ref-type">{{item.referenceCard.type === 'customer' ? '👤 客户' : '📋 记录'}}</text>
|
||||
<text class="inline-ref-title">{{item.referenceCard.title}}</text>
|
||||
</view>
|
||||
<text class="inline-ref-summary">{{item.referenceCard.summary}}</text>
|
||||
<view class="inline-ref-data">
|
||||
<view
|
||||
class="ref-data-item"
|
||||
wx:for="{{item.referenceCard.dataList}}"
|
||||
wx:for-item="entry"
|
||||
wx:key="key"
|
||||
>
|
||||
<text class="ref-data-key">{{entry.key}}</text>
|
||||
<text class="ref-data-value">{{entry.value}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- AI 正在输入指示器 -->
|
||||
<view class="message-row message-assistant" wx:if="{{isStreaming && !streamingContent}}" id="msg-typing">
|
||||
<view class="ai-avatar">
|
||||
<text class="ai-avatar-emoji">🤖</text>
|
||||
</view>
|
||||
<view class="bubble bubble-assistant typing-bubble">
|
||||
<view class="typing-dots">
|
||||
<view class="dot dot-1"></view>
|
||||
<view class="dot dot-2"></view>
|
||||
<view class="dot dot-3"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部占位,确保最后一条消息不被输入框遮挡 -->
|
||||
<view class="scroll-bottom-spacer" id="scroll-bottom"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入区域 -->
|
||||
<view class="input-bar safe-area-bottom">
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="chat-input"
|
||||
value="{{inputText}}"
|
||||
placeholder="输入消息..."
|
||||
placeholder-class="input-placeholder"
|
||||
confirm-type="send"
|
||||
bindinput="onInputChange"
|
||||
bindconfirm="onSendMessage"
|
||||
disabled="{{isStreaming}}"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="send-btn {{inputText.length > 0 && !isStreaming ? 'send-btn-active' : 'send-btn-disabled'}}"
|
||||
bindtap="onSendMessage"
|
||||
>
|
||||
<t-icon name="send" size="40rpx" color="{{inputText.length > 0 && !isStreaming ? '#ffffff' : 'var(--color-gray-6)'}}" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<dev-fab />
|
||||
289
apps/miniprogram - 副本/miniprogram/pages/chat/chat.wxss
Normal file
289
apps/miniprogram - 副本/miniprogram/pages/chat/chat.wxss
Normal file
@@ -0,0 +1,289 @@
|
||||
/* pages/chat/chat.wxss — AI 对话页样式 */
|
||||
|
||||
/* 加载态 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 页面容器 */
|
||||
.chat-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: var(--color-gray-1);
|
||||
}
|
||||
|
||||
/* ========== 消息列表 ========== */
|
||||
.message-list {
|
||||
flex: 1;
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scroll-bottom-spacer {
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
/* ========== 引用卡片(页面顶部) ========== */
|
||||
.reference-card {
|
||||
background-color: var(--color-gray-2, #eeeeee);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.reference-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.reference-source {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.reference-summary {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-9);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ========== 空对话提示 ========== */
|
||||
.empty-hint {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: var(--font-lg);
|
||||
color: var(--color-gray-13);
|
||||
font-weight: 500;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.empty-sub {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
/* ========== 消息行 ========== */
|
||||
.message-row {
|
||||
display: flex;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message-assistant {
|
||||
justify-content: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
/* ========== AI 头像 ========== */
|
||||
.ai-avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--color-primary), #4d8ff7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-avatar-emoji {
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ========== 气泡 ========== */
|
||||
.bubble-wrapper {
|
||||
max-width: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 20rpx 28rpx;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bubble-text {
|
||||
font-size: var(--font-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 用户气泡:蓝色,右上角方角 */
|
||||
.bubble-user {
|
||||
max-width: 80%;
|
||||
background-color: var(--color-primary);
|
||||
border-radius: 32rpx 8rpx 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.bubble-user .bubble-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* AI 气泡:白色,左上角方角 */
|
||||
.bubble-assistant {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8rpx 32rpx 32rpx 32rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.bubble-assistant .bubble-text {
|
||||
color: var(--color-gray-13);
|
||||
}
|
||||
|
||||
/* ========== AI 引用卡片(内联) ========== */
|
||||
.inline-ref-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20rpx 24rpx;
|
||||
border-left: 6rpx solid var(--color-primary);
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.inline-ref-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.inline-ref-type {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.inline-ref-title {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-13);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.inline-ref-summary {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-8);
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inline-ref-data {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx 24rpx;
|
||||
}
|
||||
|
||||
.ref-data-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.ref-data-key {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.ref-data-value {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-13);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ========== 打字指示器 ========== */
|
||||
.typing-bubble {
|
||||
padding: 20rpx 32rpx;
|
||||
}
|
||||
|
||||
.typing-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-gray-6);
|
||||
animation: typingBounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.dot-1 { animation-delay: 0s; }
|
||||
.dot-2 { animation-delay: 0.2s; }
|
||||
.dot-3 { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes typingBounce {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* ========== 底部输入区域 ========== */
|
||||
.input-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
background-color: #ffffff;
|
||||
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
background-color: var(--color-gray-1);
|
||||
border-radius: 48rpx;
|
||||
padding: 16rpx 28rpx;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
width: 100%;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-13);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: var(--color-gray-6);
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
/* 发送按钮 */
|
||||
.send-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.send-btn-active {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.send-btn-disabled {
|
||||
background-color: var(--color-gray-1);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"navigationBarTitleText": "助教详情",
|
||||
"usingComponents": {
|
||||
"banner": "/components/banner/banner",
|
||||
"note-modal": "/components/note-modal/note-modal",
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import { mockCoaches } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
/** 助教详情(含绩效、备注等扩展数据) */
|
||||
interface CoachDetail {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
level: string
|
||||
skills: string[]
|
||||
workYears: number
|
||||
customerCount: number
|
||||
/** 绩效指标 */
|
||||
performance: {
|
||||
monthlyHours: number
|
||||
monthlySalary: number
|
||||
customerBalance: number
|
||||
tasksCompleted: number
|
||||
}
|
||||
/** 收入明细 */
|
||||
income: {
|
||||
thisMonth: IncomeItem[]
|
||||
lastMonth: IncomeItem[]
|
||||
}
|
||||
/** 备注列表 */
|
||||
notes: NoteItem[]
|
||||
}
|
||||
|
||||
interface IncomeItem {
|
||||
label: string
|
||||
amount: string
|
||||
color: string
|
||||
}
|
||||
|
||||
interface NoteItem {
|
||||
id: string
|
||||
content: string
|
||||
timestamp: string
|
||||
score: number
|
||||
customerName: string
|
||||
}
|
||||
|
||||
/** 内联 Mock 数据:助教详情扩展 */
|
||||
const mockCoachDetail: CoachDetail = {
|
||||
id: 'coach-001',
|
||||
name: '小燕',
|
||||
avatar: '/assets/images/avatar-default.png',
|
||||
level: '星级',
|
||||
skills: ['中🎱', '🎯 斯诺克'],
|
||||
workYears: 3,
|
||||
customerCount: 68,
|
||||
performance: {
|
||||
monthlyHours: 87.5,
|
||||
monthlySalary: 6950,
|
||||
customerBalance: 86200,
|
||||
tasksCompleted: 38,
|
||||
},
|
||||
income: {
|
||||
thisMonth: [
|
||||
{ label: '基础课时费', amount: '¥3,500', color: 'primary' },
|
||||
{ label: '激励课时费', amount: '¥1,800', color: 'success' },
|
||||
{ label: '充值提成', amount: '¥1,200', color: 'warning' },
|
||||
{ label: '酒水提成', amount: '¥450', color: 'purple' },
|
||||
],
|
||||
lastMonth: [
|
||||
{ label: '基础课时费', amount: '¥3,800', color: 'primary' },
|
||||
{ label: '激励课时费', amount: '¥1,900', color: 'success' },
|
||||
{ label: '充值提成', amount: '¥1,100', color: 'warning' },
|
||||
{ label: '酒水提成', amount: '¥400', color: 'purple' },
|
||||
],
|
||||
},
|
||||
notes: [
|
||||
{ id: 'n1', content: '本月表现优秀,客户好评率高', timestamp: '2026-03-05 14:30', score: 9, customerName: '管理员' },
|
||||
{ id: 'n2', content: '需要加强斯诺克教学技巧', timestamp: '2026-02-28 10:00', score: 7, customerName: '管理员' },
|
||||
{ id: 'n3', content: '客户王先生反馈服务态度很好', timestamp: '2026-02-20 16:45', score: 8, customerName: '王先生' },
|
||||
],
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态 */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 助教 ID */
|
||||
coachId: '',
|
||||
/** 助教详情 */
|
||||
detail: null as CoachDetail | null,
|
||||
/** Banner 指标 */
|
||||
bannerMetrics: [] as Array<{ label: string; value: string }>,
|
||||
/** 绩效指标卡片 */
|
||||
perfCards: [] as Array<{ label: string; value: string; unit: string; sub: string; bgClass: string; valueColor: string }>,
|
||||
/** 收入明细 Tab */
|
||||
incomeTab: 'this' as 'this' | 'last',
|
||||
/** 当前收入明细 */
|
||||
currentIncome: [] as IncomeItem[],
|
||||
/** 当前收入合计 */
|
||||
incomeTotal: '',
|
||||
/** 排序后的备注列表 */
|
||||
sortedNotes: [] as NoteItem[],
|
||||
/** 备注弹窗 */
|
||||
noteModalVisible: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options?.id || ''
|
||||
this.setData({ coachId: id })
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
loadData(id: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API 调用 GET /api/coaches/:id
|
||||
const basicCoach = mockCoaches.find((c) => c.id === id)
|
||||
const detail: CoachDetail = basicCoach
|
||||
? { ...mockCoachDetail, id: basicCoach.id, name: basicCoach.name }
|
||||
: mockCoachDetail
|
||||
|
||||
if (!detail) {
|
||||
this.setData({ pageState: 'empty' })
|
||||
return
|
||||
}
|
||||
|
||||
const bannerMetrics = [
|
||||
{ label: '工龄', value: `${detail.workYears}年` },
|
||||
{ label: '客户', value: `${detail.customerCount}人` },
|
||||
]
|
||||
|
||||
const perfCards = [
|
||||
{ label: '本月定档业绩', value: `${detail.performance.monthlyHours}`, unit: 'h', sub: '折算前 89.0h', bgClass: 'perf-blue', valueColor: 'text-primary' },
|
||||
{ label: '本月工资(预估)', value: `¥${detail.performance.monthlySalary.toLocaleString()}`, unit: '', sub: '含预估部分', bgClass: 'perf-green', valueColor: 'text-success' },
|
||||
{ label: '客源储值余额', value: `¥${detail.performance.customerBalance.toLocaleString()}`, unit: '', sub: `${detail.customerCount}位客户合计`, bgClass: 'perf-orange', valueColor: 'text-warning' },
|
||||
{ label: '本月任务完成', value: `${detail.performance.tasksCompleted}`, unit: '个', sub: '覆盖 22 位客户', bgClass: 'perf-purple', valueColor: 'text-purple' },
|
||||
]
|
||||
|
||||
const sorted = sortByTimestamp(detail.notes || [], 'timestamp') as NoteItem[]
|
||||
|
||||
this.setData({
|
||||
pageState: 'normal',
|
||||
detail,
|
||||
bannerMetrics,
|
||||
perfCards,
|
||||
sortedNotes: sorted,
|
||||
})
|
||||
|
||||
this.switchIncomeTab('this')
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/** 切换收入明细 Tab */
|
||||
switchIncomeTab(tab: 'this' | 'last') {
|
||||
const detail = this.data.detail
|
||||
if (!detail) return
|
||||
|
||||
const items = tab === 'this' ? detail.income.thisMonth : detail.income.lastMonth
|
||||
// 计算合计(从格式化金额中提取数字)
|
||||
const total = items.reduce((sum, item) => {
|
||||
const num = parseFloat(item.amount.replace(/[¥,]/g, ''))
|
||||
return sum + (isNaN(num) ? 0 : num)
|
||||
}, 0)
|
||||
|
||||
this.setData({
|
||||
incomeTab: tab,
|
||||
currentIncome: items,
|
||||
incomeTotal: `¥${total.toLocaleString()}`,
|
||||
})
|
||||
},
|
||||
|
||||
/** 点击收入 Tab */
|
||||
onIncomeTabTap(e: WechatMiniprogram.CustomEvent) {
|
||||
const tab = e.currentTarget.dataset.tab as 'this' | 'last'
|
||||
this.switchIncomeTab(tab)
|
||||
},
|
||||
|
||||
/** 打开备注弹窗 */
|
||||
onAddNote() {
|
||||
this.setData({ noteModalVisible: true })
|
||||
},
|
||||
|
||||
/** 备注弹窗确认 */
|
||||
onNoteConfirm(e: WechatMiniprogram.CustomEvent<{ score: number; content: string }>) {
|
||||
const { score, content } = e.detail
|
||||
// TODO: 替换为真实 API 调用 POST /api/xcx/notes
|
||||
const newNote: NoteItem = {
|
||||
id: `n-${Date.now()}`,
|
||||
content,
|
||||
timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||
score,
|
||||
customerName: '我',
|
||||
}
|
||||
const notes = [newNote, ...this.data.sortedNotes]
|
||||
this.setData({
|
||||
noteModalVisible: false,
|
||||
sortedNotes: notes,
|
||||
})
|
||||
wx.showToast({ title: '备注已保存', icon: 'success' })
|
||||
},
|
||||
|
||||
/** 备注弹窗取消 */
|
||||
onNoteCancel() {
|
||||
this.setData({ noteModalVisible: false })
|
||||
},
|
||||
|
||||
/** 返回 */
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
/** 问问助手 */
|
||||
onStartChat() {
|
||||
const id = this.data.coachId || this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/chat/chat?coachId=${id}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,142 @@
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-icon name="info-circle" size="120rpx" color="#c5c5c5" />
|
||||
<text class="empty-text">未找到助教信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- Banner 区域 -->
|
||||
<view class="banner-area">
|
||||
<view class="banner-bg"></view>
|
||||
<view class="banner-overlay">
|
||||
<!-- 导航栏 -->
|
||||
<view class="banner-nav">
|
||||
<view class="nav-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="48rpx" color="#ffffff" />
|
||||
</view>
|
||||
<text class="nav-title">助教详情</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<!-- 助教基本信息 -->
|
||||
<view class="coach-header">
|
||||
<view class="avatar-box">
|
||||
<image class="avatar-img" src="{{detail.avatar}}" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="info-middle">
|
||||
<view class="name-row">
|
||||
<text class="coach-name">{{detail.name}}</text>
|
||||
<t-tag variant="light" size="small" theme="warning">{{detail.level}}</t-tag>
|
||||
</view>
|
||||
<view class="skill-row">
|
||||
<text class="skill-tag" wx:for="{{detail.skills}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-right-stats">
|
||||
<view class="right-stat">
|
||||
<text class="right-stat-label">工龄</text>
|
||||
<text class="right-stat-value">{{detail.workYears}}年</text>
|
||||
</view>
|
||||
<view class="right-stat">
|
||||
<text class="right-stat-label">客户</text>
|
||||
<text class="right-stat-value">{{detail.customerCount}}人</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="main-content">
|
||||
<!-- 绩效概览 -->
|
||||
<view class="card">
|
||||
<text class="section-title title-blue">绩效概览</text>
|
||||
<view class="perf-grid">
|
||||
<view class="perf-card {{item.bgClass}}" wx:for="{{perfCards}}" wx:key="label">
|
||||
<text class="perf-label">{{item.label}}</text>
|
||||
<view class="perf-value-row">
|
||||
<text class="perf-value {{item.valueColor}}">{{item.value}}</text>
|
||||
<text class="perf-unit" wx:if="{{item.unit}}">{{item.unit}}</text>
|
||||
</view>
|
||||
<text class="perf-sub">{{item.sub}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收入明细 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-green">收入明细</text>
|
||||
<view class="income-tabs">
|
||||
<text class="income-tab {{incomeTab === 'this' ? 'active' : ''}}"
|
||||
data-tab="this" bindtap="onIncomeTabTap">本月</text>
|
||||
<text class="income-tab {{incomeTab === 'last' ? 'active' : ''}}"
|
||||
data-tab="last" bindtap="onIncomeTabTap">上月</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="income-list">
|
||||
<view class="income-item" wx:for="{{currentIncome}}" wx:key="label">
|
||||
<view class="income-dot dot-{{item.color}}"></view>
|
||||
<text class="income-label">{{item.label}}</text>
|
||||
<text class="income-amount">{{item.amount}}</text>
|
||||
</view>
|
||||
<view class="income-total">
|
||||
<text class="income-total-label">合计{{incomeTab === 'this' ? '(预估)' : ''}}</text>
|
||||
<text class="income-total-value">{{incomeTotal}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注列表 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-orange">备注记录</text>
|
||||
<text class="header-hint">共 {{sortedNotes.length}} 条</text>
|
||||
</view>
|
||||
<view class="note-list" wx:if="{{sortedNotes.length > 0}}">
|
||||
<view class="note-item" wx:for="{{sortedNotes}}" wx:key="id">
|
||||
<view class="note-top">
|
||||
<text class="note-author">{{item.tagLabel}}</text>
|
||||
<text class="note-time">{{item.createdAt}}</text>
|
||||
</view>
|
||||
<text class="note-content">{{item.content}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="note-empty" wx:else>
|
||||
<t-icon name="info-circle" size="80rpx" color="#dcdcdc" />
|
||||
<text class="empty-hint">暂无备注</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar safe-area-bottom">
|
||||
<view class="btn-chat" bindtap="onStartChat">
|
||||
<t-icon name="chat" size="40rpx" color="#ffffff" />
|
||||
<text>问问助手</text>
|
||||
</view>
|
||||
<view class="btn-note" bindtap="onAddNote">
|
||||
<t-icon name="edit-1" size="40rpx" color="#242424" />
|
||||
<text>添加备注</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注弹窗 -->
|
||||
<note-modal
|
||||
visible="{{noteModalVisible}}"
|
||||
customerName="{{detail.name}}"
|
||||
bind:confirm="onNoteConfirm"
|
||||
bind:cancel="onNoteCancel"
|
||||
/>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{120}}" customerId="{{detail.id}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,431 @@
|
||||
/* 加载态 & 空态 */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* ========== Banner ========== */
|
||||
.banner-area {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.banner-bg {
|
||||
width: 100%;
|
||||
height: 380rpx;
|
||||
background: linear-gradient(135deg, #ff6b4a, #ff8a65);
|
||||
display: block;
|
||||
}
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.banner-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.nav-back {
|
||||
padding: 8rpx;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
/* 助教头部信息 */
|
||||
.coach-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 16rpx 40rpx 32rpx;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.info-middle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.coach-name {
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
.skill-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.skill-tag {
|
||||
padding: 4rpx 16rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.info-right-stats {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.right-stat {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.right-stat-label {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.right-stat-value {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ========== 主体内容 ========== */
|
||||
.main-content {
|
||||
padding: 32rpx;
|
||||
padding-bottom: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
/* ========== 通用卡片 ========== */
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-xl, 32rpx);
|
||||
padding: 40rpx;
|
||||
box-shadow: var(--shadow-lg, 0 8rpx 32rpx rgba(0, 0, 0, 0.06));
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
position: relative;
|
||||
padding-left: 20rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.card-header .section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.section-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
.title-blue::before {
|
||||
background: linear-gradient(180deg, #0052d9, #5b9cf8);
|
||||
}
|
||||
.title-green::before {
|
||||
background: linear-gradient(180deg, #00a870, #4cd964);
|
||||
}
|
||||
.title-orange::before {
|
||||
background: linear-gradient(180deg, #ed7b2f, #ffc107);
|
||||
}
|
||||
.header-hint {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* ========== 绩效概览 ========== */
|
||||
.perf-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.perf-card {
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 24rpx;
|
||||
border: 1rpx solid transparent;
|
||||
}
|
||||
.perf-blue {
|
||||
background: linear-gradient(135deg, #eff6ff, #eef2ff);
|
||||
border-color: rgba(191, 219, 254, 0.5);
|
||||
}
|
||||
.perf-green {
|
||||
background: linear-gradient(135deg, #ecfdf5, #d1fae5);
|
||||
border-color: rgba(167, 243, 208, 0.5);
|
||||
}
|
||||
.perf-orange {
|
||||
background: linear-gradient(135deg, #fff7ed, #fef3c7);
|
||||
border-color: rgba(253, 186, 116, 0.5);
|
||||
}
|
||||
.perf-purple {
|
||||
background: linear-gradient(135deg, #faf5ff, #ede9fe);
|
||||
border-color: rgba(196, 181, 253, 0.5);
|
||||
}
|
||||
.perf-label {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.perf-value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
}
|
||||
.perf-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.perf-unit {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.perf-sub {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* ========== 收入明细 ========== */
|
||||
.income-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.income-tab {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 100rpx;
|
||||
color: var(--color-gray-7, #8b8b8b);
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
}
|
||||
.income-tab.active {
|
||||
color: var(--color-primary, #0052d9);
|
||||
background: var(--color-primary-light, #ecf2fe);
|
||||
font-weight: 500;
|
||||
}
|
||||
.income-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.income-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.income-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot-primary {
|
||||
background: var(--color-primary, #0052d9);
|
||||
}
|
||||
.dot-success {
|
||||
background: var(--color-success, #00a870);
|
||||
}
|
||||
.dot-warning {
|
||||
background: var(--color-warning, #ed7b2f);
|
||||
}
|
||||
.dot-purple {
|
||||
background: #7c3aed;
|
||||
}
|
||||
.income-label {
|
||||
flex: 1;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
}
|
||||
.income-amount {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.income-total {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
}
|
||||
.income-total-label {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
}
|
||||
.income-total-value {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 700;
|
||||
color: var(--color-success, #00a870);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ========== 备注列表 ========== */
|
||||
.note-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.note-item {
|
||||
background: #fafafa;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
padding: 24rpx;
|
||||
}
|
||||
.note-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.note-author {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.note-time {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.note-content {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.note-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.note-star {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.note-score-value {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-warning, #ed7b2f);
|
||||
font-weight: 500;
|
||||
}
|
||||
.note-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.empty-hint {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
}
|
||||
|
||||
/* ========== 底部操作栏 ========== */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 128rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 0 32rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
.btn-chat {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #0052d9, #3b82f6);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
.btn-note {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
}
|
||||
|
||||
/* ========== 颜色工具类 ========== */
|
||||
.text-primary {
|
||||
color: var(--color-primary, #0052d9) !important;
|
||||
}
|
||||
.text-success {
|
||||
color: var(--color-success, #00a870) !important;
|
||||
}
|
||||
.text-warning {
|
||||
color: var(--color-warning, #ed7b2f) !important;
|
||||
}
|
||||
.text-purple {
|
||||
color: #7c3aed !important;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"navigationBarTitleText": "客户详情",
|
||||
"usingComponents": {
|
||||
"heart-icon": "/components/heart-icon/heart-icon",
|
||||
"star-rating": "/components/star-rating/star-rating",
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { mockCustomers, mockCustomerDetail } from '../../utils/mock-data'
|
||||
import type { CustomerDetail, ConsumptionRecord } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态 */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 客户 ID */
|
||||
customerId: '',
|
||||
/** 客户详情 */
|
||||
detail: null as CustomerDetail | null,
|
||||
/** 排序后的消费记录 */
|
||||
sortedRecords: [] as ConsumptionRecord[],
|
||||
/** AI 洞察 */
|
||||
aiInsight: {
|
||||
summary: '高价值 VIP 客户,月均到店 4-5 次,偏好夜场中式台球,近期对斯诺克产生兴趣。社交属性强,常带固定球搭子,有拉新能力。储值余额充足,对促销活动响应积极。',
|
||||
strategies: [
|
||||
{ color: 'green', text: '最后到店距今 12 天,超出理想间隔 7 天,建议尽快安排助教主动联系召回' },
|
||||
{ color: 'amber', text: '客户提到想练斯诺克走位,可推荐斯诺克专项课程包,结合储值优惠提升客单价' },
|
||||
{ color: 'pink', text: '社交属性强,可邀请参加门店球友赛事活动,带动球搭子到店消费' },
|
||||
],
|
||||
},
|
||||
/** 维客线索 */
|
||||
clues: [
|
||||
{ category: '客户基础', categoryColor: 'primary', text: '🎂 生日 3月15日 · VIP会员 · 注册2年', source: '系统' },
|
||||
{ category: '消费习惯', categoryColor: 'success', text: '🌙 常来夜场 · 月均4-5次', source: '系统' },
|
||||
{ category: '消费习惯', categoryColor: 'success', text: '💰 高客单价', source: '系统', detail: '近60天场均消费 ¥420,高于门店均值 ¥180;偏好夜场时段,酒水附加消费占比 35%' },
|
||||
{ category: '玩法偏好', categoryColor: 'purple', text: '🎱 偏爱中式 · 斯诺克进阶中', source: '系统' },
|
||||
{ category: '促销接受', categoryColor: 'warning', text: '🍷 爱点酒水套餐 · 对储值活动敏感', source: '系统', detail: '最近3次到店均点了酒水套餐;上次 ¥5000 储值活动当天即充值,对满赠类活动响应率高' },
|
||||
{ category: '社交关系', categoryColor: 'pink', text: '👥 常带朋友 · 固定球搭子2人', source: '系统', detail: '近60天 80% 的到店为多人局,常与「李哥」「阿杰」同行;曾介绍2位新客办卡' },
|
||||
{ category: '重要反馈', categoryColor: 'error', text: '⚠️ 上次提到想练斯诺克走位,对球桌维护质量比较在意,建议优先安排VIP房', source: '小燕' },
|
||||
],
|
||||
/** Banner 统计 */
|
||||
bannerStats: {
|
||||
balance: '¥8,600',
|
||||
spend60d: '¥2,800',
|
||||
idealInterval: '7天',
|
||||
daysSinceVisit: '12天',
|
||||
},
|
||||
/** 助教任务 */
|
||||
coachTasks: [
|
||||
{
|
||||
name: '小燕',
|
||||
level: '高级助教',
|
||||
levelColor: 'pink',
|
||||
taskType: '高优先召回',
|
||||
taskColor: 'red',
|
||||
lastService: '02-20 21:30 · 2.5h',
|
||||
bgClass: 'coach-card-red',
|
||||
metrics: [
|
||||
{ label: '近60天次数', value: '18次', color: 'primary' },
|
||||
{ label: '总时长', value: '17h', color: '' },
|
||||
{ label: '次均时长', value: '0.9h', color: 'warning' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '泡芙',
|
||||
level: '中级助教',
|
||||
levelColor: 'purple',
|
||||
taskType: '关系构建',
|
||||
taskColor: 'pink',
|
||||
lastService: '02-15 14:00 · 1.5h',
|
||||
bgClass: 'coach-card-pink',
|
||||
metrics: [
|
||||
{ label: '近60天次数', value: '12次', color: 'primary' },
|
||||
{ label: '总时长', value: '11h', color: '' },
|
||||
{ label: '次均时长', value: '0.9h', color: 'warning' },
|
||||
],
|
||||
},
|
||||
],
|
||||
/** 最喜欢的助教 */
|
||||
favoriteCoaches: [
|
||||
{
|
||||
name: '小燕',
|
||||
emoji: '❤️',
|
||||
relationIndex: '0.92',
|
||||
indexColor: 'success',
|
||||
bgClass: 'fav-card-pink',
|
||||
stats: [
|
||||
{ label: '基础', value: '12h', color: 'primary' },
|
||||
{ label: '激励', value: '5h', color: 'warning' },
|
||||
{ label: '上课', value: '18次', color: '' },
|
||||
{ label: '充值', value: '¥5,000', color: 'success' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '泡芙',
|
||||
emoji: '💛',
|
||||
relationIndex: '0.78',
|
||||
indexColor: 'warning',
|
||||
bgClass: 'fav-card-amber',
|
||||
stats: [
|
||||
{ label: '基础', value: '8h', color: 'primary' },
|
||||
{ label: '激励', value: '3h', color: 'warning' },
|
||||
{ label: '上课', value: '12次', color: '' },
|
||||
{ label: '充值', value: '¥3,000', color: 'success' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options?.id || ''
|
||||
this.setData({ customerId: id })
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
loadData(id: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API 调用
|
||||
// 先从 mockCustomers 查找基本信息,再用 mockCustomerDetail 补充
|
||||
const customer = mockCustomers.find((c) => c.id === id)
|
||||
const detail = customer
|
||||
? { ...mockCustomerDetail, id: customer.id, name: customer.name, heartScore: customer.heartScore, tags: customer.tags }
|
||||
: mockCustomerDetail
|
||||
|
||||
if (!detail) {
|
||||
this.setData({ pageState: 'empty' })
|
||||
return
|
||||
}
|
||||
|
||||
const sorted = sortByTimestamp(detail.consumptionRecords || [], 'date') as ConsumptionRecord[]
|
||||
this.setData({
|
||||
pageState: 'normal',
|
||||
detail,
|
||||
sortedRecords: sorted,
|
||||
})
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/** 发起对话 */
|
||||
onStartChat() {
|
||||
const id = this.data.customerId || this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/chat/chat?customerId=${id}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
|
||||
/** 添加备注 */
|
||||
onAddNote() {
|
||||
wx.showToast({ title: '备注功能开发中', icon: 'none' })
|
||||
},
|
||||
|
||||
/** 查看服务记录 */
|
||||
onViewServiceRecords() {
|
||||
const id = this.data.customerId || this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/customer-service-records/customer-service-records?customerId=${id}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
|
||||
/** 返回 */
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,215 @@
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-icon name="info-circle" size="120rpx" color="#c5c5c5" />
|
||||
<text class="empty-text">未找到客户信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- Banner 区域 -->
|
||||
<view class="banner-area">
|
||||
<view class="banner-bg"></view>
|
||||
<view class="banner-overlay">
|
||||
<!-- 导航栏 -->
|
||||
<view class="banner-nav">
|
||||
<view class="nav-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="48rpx" color="#ffffff" />
|
||||
</view>
|
||||
<text class="nav-title">客户详情</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<!-- 客户基本信息 -->
|
||||
<view class="customer-header">
|
||||
<view class="avatar-box">
|
||||
<text class="avatar-text">{{detail.name[0] || '?'}}</text>
|
||||
</view>
|
||||
<view class="info-right">
|
||||
<view class="name-row">
|
||||
<text class="customer-name">{{detail.name}}</text>
|
||||
<t-tag wx:for="{{detail.tags}}" wx:key="*this"
|
||||
variant="light" size="small" theme="warning"
|
||||
class="customer-tag">{{item}}</t-tag>
|
||||
</view>
|
||||
<view class="sub-info">
|
||||
<text class="phone">{{detail.phone}}</text>
|
||||
<text class="member-id">VIP20231215</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Banner 统计指标 -->
|
||||
<view class="banner-stats">
|
||||
<view class="stat-item stat-border">
|
||||
<text class="stat-value stat-green">{{bannerStats.balance}}</text>
|
||||
<text class="stat-label">储值余额</text>
|
||||
</view>
|
||||
<view class="stat-item stat-border">
|
||||
<text class="stat-value">{{bannerStats.spend60d}}</text>
|
||||
<text class="stat-label">60天消费</text>
|
||||
</view>
|
||||
<view class="stat-item stat-border">
|
||||
<text class="stat-value">{{bannerStats.idealInterval}}</text>
|
||||
<text class="stat-label">理想间隔</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value stat-amber">{{bannerStats.daysSinceVisit}}</text>
|
||||
<text class="stat-label">距今到店</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="main-content">
|
||||
<!-- AI 智能洞察 -->
|
||||
<view class="ai-insight-card">
|
||||
<view class="ai-insight-header">
|
||||
<text class="ai-icon-emoji">🤖</text>
|
||||
<text class="ai-insight-label">AI 智能洞察</text>
|
||||
</view>
|
||||
<text class="ai-insight-summary">{{aiInsight.summary}}</text>
|
||||
<view class="ai-strategy-box">
|
||||
<text class="strategy-title">📋 当前推荐策略</text>
|
||||
<view class="strategy-list">
|
||||
<view class="strategy-item" wx:for="{{aiInsight.strategies}}" wx:key="index">
|
||||
<view class="strategy-dot dot-{{item.color}}"></view>
|
||||
<text class="strategy-text">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 维客线索 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-green">维客线索</text>
|
||||
<view class="ai-badge-box">
|
||||
<text class="ai-badge-emoji">🤖</text>
|
||||
<text class="ai-badge-text">AI智能洞察</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="clue-list">
|
||||
<view class="clue-item {{item.detail ? 'clue-with-detail' : ''}}" wx:for="{{clues}}" wx:key="index">
|
||||
<view class="clue-main">
|
||||
<view class="clue-category clue-cat-{{item.categoryColor}}">
|
||||
<text>{{item.category}}</text>
|
||||
</view>
|
||||
<view class="clue-content">
|
||||
<text class="clue-text">{{item.text}}</text>
|
||||
<text class="clue-source">By:{{item.source}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="clue-detail" wx:if="{{item.detail}}">{{item.detail}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 助教任务 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-blue">助教任务</text>
|
||||
<text class="header-hint">当前进行中</text>
|
||||
</view>
|
||||
<view class="coach-task-list">
|
||||
<view class="coach-task-card {{item.bgClass}}" wx:for="{{coachTasks}}" wx:key="index">
|
||||
<view class="coach-task-top">
|
||||
<view class="coach-name-row">
|
||||
<text class="coach-name">{{item.name}}</text>
|
||||
<text class="coach-level level-{{item.levelColor}}">{{item.level}}</text>
|
||||
</view>
|
||||
<text class="coach-task-type type-{{item.taskColor}}">{{item.taskType}}</text>
|
||||
</view>
|
||||
<text class="coach-last-service">上次服务:{{item.lastService}}</text>
|
||||
<view class="coach-metrics">
|
||||
<view class="coach-metric" wx:for="{{item.metrics}}" wx:for-item="m" wx:key="label">
|
||||
<text class="metric-label">{{m.label}}</text>
|
||||
<text class="metric-value {{m.color ? 'text-' + m.color : ''}}">{{m.value}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最喜欢的助教 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-pink">最喜欢的助教</text>
|
||||
<text class="header-hint">近60天</text>
|
||||
</view>
|
||||
<view class="fav-coach-list">
|
||||
<view class="fav-coach-card {{item.bgClass}}" wx:for="{{favoriteCoaches}}" wx:key="index">
|
||||
<view class="fav-coach-top">
|
||||
<view class="fav-coach-name">
|
||||
<text class="fav-emoji">{{item.emoji}}</text>
|
||||
<text class="fav-name">{{item.name}}</text>
|
||||
</view>
|
||||
<view class="fav-index">
|
||||
<text class="fav-index-label">关系指数</text>
|
||||
<text class="fav-index-value text-{{item.indexColor}}">{{item.relationIndex}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="fav-period">近60天</text>
|
||||
<view class="fav-stats">
|
||||
<view class="fav-stat" wx:for="{{item.stats}}" wx:for-item="s" wx:key="label">
|
||||
<text class="fav-stat-label">{{s.label}}</text>
|
||||
<text class="fav-stat-value {{s.color ? 'text-' + s.color : ''}}">{{s.value}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消费记录 -->
|
||||
<view class="card">
|
||||
<view class="card-header" bindtap="onViewServiceRecords">
|
||||
<text class="section-title title-orange">消费记录</text>
|
||||
<t-icon name="chevron-right" size="40rpx" color="#a6a6a6" />
|
||||
</view>
|
||||
<view class="record-list" wx:if="{{sortedRecords.length > 0}}">
|
||||
<view class="record-item" wx:for="{{sortedRecords}}" wx:key="id">
|
||||
<view class="record-header">
|
||||
<view class="record-project">
|
||||
<view class="record-dot"></view>
|
||||
<text class="record-project-name">{{item.project}}</text>
|
||||
</view>
|
||||
<text class="record-date">{{item.date}}</text>
|
||||
</view>
|
||||
<view class="record-body">
|
||||
<view class="record-info">
|
||||
<text class="record-coach" wx:if="{{item.coachName !== '-'}}">助教:{{item.coachName}}</text>
|
||||
<text class="record-duration" wx:if="{{item.duration > 0}}">{{item.duration}}分钟</text>
|
||||
</view>
|
||||
<text class="record-amount">¥{{item.amount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="record-empty" wx:else>
|
||||
<t-icon name="info-circle" size="80rpx" color="#dcdcdc" />
|
||||
<text class="empty-hint">暂无消费记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar safe-area-bottom">
|
||||
<view class="btn-chat" bindtap="onStartChat">
|
||||
<t-icon name="chat" size="40rpx" color="#ffffff" />
|
||||
<text>问问助手</text>
|
||||
</view>
|
||||
<view class="btn-note" bindtap="onAddNote">
|
||||
<t-icon name="edit-1" size="40rpx" color="#242424" />
|
||||
<text>备注</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{200}}" customerId="{{detail.id}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,680 @@
|
||||
/* 加载态 & 空态 */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* ========== Banner ========== */
|
||||
.banner-area {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.banner-bg {
|
||||
width: 100%;
|
||||
height: 480rpx;
|
||||
background: linear-gradient(135deg, #1a1a2e, #c9a84c);
|
||||
display: block;
|
||||
}
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.banner-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.nav-back {
|
||||
padding: 8rpx;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
/* 客户头部信息 */
|
||||
.customer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
padding: 16rpx 40rpx 24rpx;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.avatar-text {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
.info-right {
|
||||
flex: 1;
|
||||
}
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.customer-name {
|
||||
font-size: var(--font-xl, 40rpx);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
.customer-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sub-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.phone,
|
||||
.member-id {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* Banner 统计 */
|
||||
.banner-stats {
|
||||
margin: 0 40rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
}
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
.stat-border {
|
||||
border-right: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
.stat-green {
|
||||
color: #6ee7b7;
|
||||
}
|
||||
.stat-amber {
|
||||
color: #fcd34d;
|
||||
}
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* ========== 主体内容 ========== */
|
||||
.main-content {
|
||||
padding: 32rpx;
|
||||
padding-bottom: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
/* ========== AI 智能洞察卡片 ========== */
|
||||
.ai-insight-card {
|
||||
border-radius: var(--radius-xl, 32rpx);
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
padding: 40rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
.ai-insight-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.ai-icon-small {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--radius-md, 16rpx);
|
||||
padding: 8rpx;
|
||||
}
|
||||
.ai-icon-emoji {
|
||||
font-size: 40rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.ai-insight-label {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.ai-insight-summary {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.ai-strategy-box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 24rpx;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.strategy-title {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
.strategy-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.strategy-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.strategy-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
margin-top: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot-green {
|
||||
background: #6ee7b7;
|
||||
}
|
||||
.dot-amber {
|
||||
background: #fcd34d;
|
||||
}
|
||||
.dot-pink {
|
||||
background: #f9a8d4;
|
||||
}
|
||||
.strategy-text {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ========== 通用卡片 ========== */
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-xl, 32rpx);
|
||||
padding: 40rpx;
|
||||
box-shadow: var(--shadow-lg, 0 8rpx 32rpx rgba(0, 0, 0, 0.06));
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
position: relative;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
.section-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
.title-green::before {
|
||||
background: linear-gradient(180deg, #00a870, #4cd964);
|
||||
}
|
||||
.title-blue::before {
|
||||
background: linear-gradient(180deg, #0052d9, #5b9cf8);
|
||||
}
|
||||
.title-orange::before {
|
||||
background: linear-gradient(180deg, #ed7b2f, #ffc107);
|
||||
}
|
||||
.title-pink::before {
|
||||
background: linear-gradient(180deg, #e851a4, #f5a0c0);
|
||||
}
|
||||
.header-hint {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* AI 徽章 */
|
||||
.ai-badge-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
background: var(--color-primary-light, #ecf2fe);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.ai-badge-emoji {
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.ai-badge-text {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-primary, #0052d9);
|
||||
}
|
||||
|
||||
/* ========== 维客线索 ========== */
|
||||
.clue-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.clue-item {
|
||||
background: #fafafa;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
padding: 24rpx;
|
||||
}
|
||||
.clue-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.clue-category {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
.clue-cat-primary {
|
||||
background: rgba(0, 82, 217, 0.1);
|
||||
color: var(--color-primary, #0052d9);
|
||||
}
|
||||
.clue-cat-success {
|
||||
background: rgba(0, 168, 112, 0.1);
|
||||
color: var(--color-success, #00a870);
|
||||
}
|
||||
.clue-cat-purple {
|
||||
background: rgba(124, 58, 237, 0.1);
|
||||
color: #7c3aed;
|
||||
}
|
||||
.clue-cat-warning {
|
||||
background: rgba(237, 123, 47, 0.1);
|
||||
color: var(--color-warning, #ed7b2f);
|
||||
}
|
||||
.clue-cat-pink {
|
||||
background: rgba(236, 72, 153, 0.1);
|
||||
color: #ec4899;
|
||||
}
|
||||
.clue-cat-error {
|
||||
background: rgba(227, 77, 89, 0.1);
|
||||
color: var(--color-error, #e34d59);
|
||||
}
|
||||
.clue-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-height: 80rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.clue-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-13, #242424);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.clue-source {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
align-self: flex-end;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.clue-detail {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-7, #8b8b8b);
|
||||
line-height: 1.6;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
/* ========== 助教任务 ========== */
|
||||
.coach-task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.coach-task-card {
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 24rpx;
|
||||
}
|
||||
.coach-card-red {
|
||||
background: linear-gradient(135deg, rgba(254, 226, 226, 0.8), rgba(255, 228, 230, 0.6));
|
||||
border: 1rpx solid rgba(252, 165, 165, 0.6);
|
||||
}
|
||||
.coach-card-pink {
|
||||
background: linear-gradient(135deg, rgba(252, 231, 243, 0.8), rgba(250, 232, 255, 0.6));
|
||||
border: 1rpx solid rgba(249, 168, 212, 0.6);
|
||||
}
|
||||
.coach-task-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.coach-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.coach-name {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.coach-level {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 100rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.level-pink {
|
||||
background: #fce7f3;
|
||||
color: #be185d;
|
||||
}
|
||||
.level-purple {
|
||||
background: #f3e8ff;
|
||||
color: #7c3aed;
|
||||
}
|
||||
.coach-task-type {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 100rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.type-red {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.type-pink {
|
||||
background: #fce7f3;
|
||||
color: #be185d;
|
||||
}
|
||||
.coach-last-service {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-7, #8b8b8b);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.coach-metrics {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.coach-metric {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: var(--radius-md, 16rpx);
|
||||
padding: 12rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
.metric-label {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.metric-value {
|
||||
display: block;
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
|
||||
/* ========== 最喜欢的助教 ========== */
|
||||
.fav-coach-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.fav-coach-card {
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 32rpx;
|
||||
}
|
||||
.fav-card-pink {
|
||||
background: linear-gradient(135deg, rgba(252, 231, 243, 0.8), rgba(255, 228, 230, 0.6));
|
||||
border: 1rpx solid rgba(249, 168, 212, 0.6);
|
||||
}
|
||||
.fav-card-amber {
|
||||
background: linear-gradient(135deg, rgba(254, 243, 199, 0.8), rgba(254, 249, 195, 0.6));
|
||||
border: 1rpx solid rgba(252, 211, 77, 0.6);
|
||||
}
|
||||
.fav-coach-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.fav-coach-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.fav-emoji {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
.fav-name {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.fav-index {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.fav-index-label {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-7, #8b8b8b);
|
||||
}
|
||||
.fav-index-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.fav-period {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-7, #8b8b8b);
|
||||
margin-bottom: 12rpx;
|
||||
padding-left: 4rpx;
|
||||
}
|
||||
.fav-stats {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.fav-stat {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: var(--radius-md, 16rpx);
|
||||
padding: 16rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
.fav-stat-label {
|
||||
display: block;
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-7, #8b8b8b);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.fav-stat-value {
|
||||
display: block;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
|
||||
/* ========== 消费记录 ========== */
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.record-item {
|
||||
background: #fafafa;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
overflow: hidden;
|
||||
}
|
||||
.record-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
background: linear-gradient(90deg, #eff6ff, #eef2ff);
|
||||
border-bottom: 1rpx solid rgba(219, 234, 254, 0.5);
|
||||
}
|
||||
.record-project {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.record-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary, #0052d9);
|
||||
}
|
||||
.record-project-name {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-primary, #0052d9);
|
||||
}
|
||||
.record-date {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-8, #777777);
|
||||
}
|
||||
.record-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
.record-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.record-coach {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-8, #777777);
|
||||
}
|
||||
.record-duration {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.record-amount {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.record-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.empty-hint {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
}
|
||||
|
||||
/* ========== 底部操作栏 ========== */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 128rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 0 32rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
.btn-chat {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #0052d9, #3b82f6);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
.btn-note {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
}
|
||||
|
||||
/* ========== 颜色工具类 ========== */
|
||||
.text-primary {
|
||||
color: var(--color-primary, #0052d9) !important;
|
||||
}
|
||||
.text-success {
|
||||
color: var(--color-success, #00a870) !important;
|
||||
}
|
||||
.text-warning {
|
||||
color: var(--color-warning, #ed7b2f) !important;
|
||||
}
|
||||
.text-error {
|
||||
color: var(--color-error, #e34d59) !important;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "服务记录",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { mockCustomerDetail, mockCustomers } from '../../utils/mock-data'
|
||||
import type { ConsumptionRecord } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
/** 服务记录(含月份分组信息) */
|
||||
interface ServiceRecord extends ConsumptionRecord {
|
||||
/** 格式化后的日期,如 "2月5日" */
|
||||
dateLabel: string
|
||||
/** 时间段,如 "15:00 - 17:00" */
|
||||
timeRange: string
|
||||
/** 时长文本,如 "2.0h" */
|
||||
durationText: string
|
||||
/** 课程类型标签 */
|
||||
typeLabel: string
|
||||
/** 类型样式 class */
|
||||
typeClass: string
|
||||
/** 台号/房间 */
|
||||
tableNo: string
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态 */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 客户 ID */
|
||||
customerId: '',
|
||||
/** 客户名 */
|
||||
customerName: '',
|
||||
/** 客户名首字 */
|
||||
customerInitial: '',
|
||||
/** 客户电话 */
|
||||
customerPhone: '139****5678',
|
||||
/** 累计服务次数 */
|
||||
totalServiceCount: 0,
|
||||
/** 关系指数 */
|
||||
relationIndex: '0.85',
|
||||
/** 当前月份标签 */
|
||||
monthLabel: '',
|
||||
/** 当前年 */
|
||||
currentYear: 2026,
|
||||
/** 当前月 */
|
||||
currentMonth: 2,
|
||||
/** 最小年月(数据起始) */
|
||||
minYearMonth: 202601,
|
||||
/** 最大年月(当前月) */
|
||||
maxYearMonth: 202602,
|
||||
/** 是否可切换上月 */
|
||||
canPrev: true,
|
||||
/** 是否可切换下月 */
|
||||
canNext: false,
|
||||
/** 月度统计 */
|
||||
monthCount: '6次',
|
||||
monthHours: '11.5h',
|
||||
monthRelation: '0.85',
|
||||
/** 当前月的服务记录 */
|
||||
records: [] as ServiceRecord[],
|
||||
/** 所有记录(原始) */
|
||||
allRecords: [] as ConsumptionRecord[],
|
||||
/** 是否还有更多 */
|
||||
hasMore: false,
|
||||
/** 加载更多中 */
|
||||
loadingMore: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options?.customerId || options?.id || ''
|
||||
this.setData({ customerId: id })
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
/** 加载数据 */
|
||||
loadData(id: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API 调用
|
||||
const customer = mockCustomers.find((c) => c.id === id)
|
||||
const detail = customer
|
||||
? { ...mockCustomerDetail, id: customer.id, name: customer.name }
|
||||
: mockCustomerDetail
|
||||
|
||||
const allRecords = sortByTimestamp(detail.consumptionRecords || [], 'date') as ConsumptionRecord[]
|
||||
const name = detail.name || '客户'
|
||||
|
||||
this.setData({
|
||||
customerName: name,
|
||||
customerInitial: name[0] || '?',
|
||||
allRecords,
|
||||
totalServiceCount: allRecords.length,
|
||||
})
|
||||
|
||||
this.updateMonthView()
|
||||
}, 400)
|
||||
},
|
||||
|
||||
/** 根据当前月份筛选并更新视图 */
|
||||
updateMonthView() {
|
||||
const { currentYear, currentMonth, allRecords } = this.data
|
||||
const monthLabel = `${currentYear}年${currentMonth}月`
|
||||
|
||||
// 筛选当月记录
|
||||
const monthPrefix = `${currentYear}-${String(currentMonth).padStart(2, '0')}`
|
||||
const monthRecords = allRecords.filter((r) => r.date.startsWith(monthPrefix))
|
||||
|
||||
// 转换为展示格式
|
||||
const records: ServiceRecord[] = monthRecords.map((r) => {
|
||||
const d = new Date(r.date)
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
return {
|
||||
...r,
|
||||
dateLabel: `${month}月${day}日`,
|
||||
timeRange: this.generateTimeRange(r.duration),
|
||||
durationText: (r.duration / 60).toFixed(1) + 'h',
|
||||
typeLabel: this.getTypeLabel(r.project),
|
||||
typeClass: this.getTypeClass(r.project),
|
||||
tableNo: this.getTableNo(r.id),
|
||||
}
|
||||
})
|
||||
|
||||
// 月度统计
|
||||
const totalMinutes = monthRecords.reduce((sum, r) => sum + r.duration, 0)
|
||||
const monthCount = monthRecords.length + '次'
|
||||
const monthHours = (totalMinutes / 60).toFixed(1) + 'h'
|
||||
|
||||
// 边界判断
|
||||
const yearMonth = currentYear * 100 + currentMonth
|
||||
const canPrev = yearMonth > this.data.minYearMonth
|
||||
const canNext = yearMonth < this.data.maxYearMonth
|
||||
|
||||
const isEmpty = records.length === 0 && allRecords.length === 0
|
||||
|
||||
this.setData({
|
||||
monthLabel,
|
||||
records,
|
||||
monthCount,
|
||||
monthHours,
|
||||
canPrev,
|
||||
canNext,
|
||||
pageState: isEmpty ? 'empty' : 'normal',
|
||||
})
|
||||
},
|
||||
|
||||
/** 生成模拟时间段 */
|
||||
generateTimeRange(durationMin: number): string {
|
||||
const startHour = 14 + Math.floor(Math.random() * 6)
|
||||
const endMin = startHour * 60 + durationMin
|
||||
const endHour = Math.floor(endMin / 60)
|
||||
const endMinute = endMin % 60
|
||||
return `${startHour}:00 - ${endHour}:${String(endMinute).padStart(2, '0')}`
|
||||
},
|
||||
|
||||
/** 课程类型标签 */
|
||||
getTypeLabel(project: string): string {
|
||||
if (project.includes('小组')) return '小组课'
|
||||
if (project.includes('1v1')) return '基础课'
|
||||
if (project.includes('充值')) return '充值'
|
||||
if (project.includes('斯诺克')) return '斯诺克'
|
||||
return '基础课'
|
||||
},
|
||||
|
||||
/** 课程类型样式 */
|
||||
getTypeClass(project: string): string {
|
||||
if (project.includes('充值')) return 'type-tip'
|
||||
if (project.includes('小组')) return 'type-vip'
|
||||
if (project.includes('斯诺克')) return 'type-vip'
|
||||
return 'type-basic'
|
||||
},
|
||||
|
||||
/** 模拟台号 */
|
||||
getTableNo(id: string): string {
|
||||
const tables = ['A12号台', '3号台', 'VIP1号房', '5号台', 'VIP2号房', '8号台']
|
||||
const idx = parseInt(id.replace(/\D/g, '') || '0', 10) % tables.length
|
||||
return tables[idx]
|
||||
},
|
||||
|
||||
/** 切换到上一月 */
|
||||
onPrevMonth() {
|
||||
if (!this.data.canPrev) return
|
||||
let { currentYear, currentMonth } = this.data
|
||||
currentMonth--
|
||||
if (currentMonth < 1) {
|
||||
currentMonth = 12
|
||||
currentYear--
|
||||
}
|
||||
this.setData({ currentYear, currentMonth })
|
||||
this.updateMonthView()
|
||||
},
|
||||
|
||||
/** 切换到下一月 */
|
||||
onNextMonth() {
|
||||
if (!this.data.canNext) return
|
||||
let { currentYear, currentMonth } = this.data
|
||||
currentMonth++
|
||||
if (currentMonth > 12) {
|
||||
currentMonth = 1
|
||||
currentYear++
|
||||
}
|
||||
this.setData({ currentYear, currentMonth })
|
||||
this.updateMonthView()
|
||||
},
|
||||
|
||||
/** 下拉刷新 */
|
||||
onPullDownRefresh() {
|
||||
this.loadData(this.data.customerId)
|
||||
setTimeout(() => wx.stopPullDownRefresh(), 600)
|
||||
},
|
||||
|
||||
/** 触底加载 */
|
||||
onReachBottom() {
|
||||
// Mock 阶段数据有限,不做分页
|
||||
if (this.data.loadingMore || !this.data.hasMore) return
|
||||
this.setData({ loadingMore: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ loadingMore: false, hasMore: false })
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/** 返回上一页 */
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-empty description="暂无服务记录" />
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- Banner 区域 -->
|
||||
<view class="banner-area">
|
||||
<view class="banner-bg"></view>
|
||||
<view class="banner-overlay">
|
||||
<!-- 导航栏 -->
|
||||
<view class="banner-nav">
|
||||
<view class="nav-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="48rpx" color="#ffffff" />
|
||||
</view>
|
||||
<text class="nav-title">服务记录</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<!-- 客户信息 -->
|
||||
<view class="customer-header">
|
||||
<view class="avatar-box">
|
||||
<text class="avatar-text">{{customerInitial}}</text>
|
||||
</view>
|
||||
<view class="info-right">
|
||||
<view class="name-row">
|
||||
<text class="customer-name">{{customerName}}</text>
|
||||
<text class="phone-text">{{customerPhone}}</text>
|
||||
</view>
|
||||
<view class="sub-stats">
|
||||
<text class="sub-stat">累计服务 <text class="stat-highlight">{{totalServiceCount}}</text> 次</text>
|
||||
<text class="sub-stat">关系指数 <text class="stat-highlight">{{relationIndex}}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 月份切换 -->
|
||||
<view class="month-switcher">
|
||||
<view class="month-btn {{canPrev ? '' : 'disabled'}}" bindtap="onPrevMonth">
|
||||
<t-icon name="chevron-left" size="32rpx" color="{{canPrev ? '#777777' : '#dcdcdc'}}" />
|
||||
</view>
|
||||
<text class="month-label">{{monthLabel}}</text>
|
||||
<view class="month-btn {{canNext ? '' : 'disabled'}}" bindtap="onNextMonth">
|
||||
<t-icon name="chevron-right" size="32rpx" color="{{canNext ? '#777777' : '#dcdcdc'}}" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 月度统计概览 -->
|
||||
<view class="month-summary">
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">本月服务</text>
|
||||
<text class="summary-value">{{monthCount}}</text>
|
||||
</view>
|
||||
<view class="summary-divider"></view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">服务时长</text>
|
||||
<text class="summary-value value-primary">{{monthHours}}</text>
|
||||
</view>
|
||||
<view class="summary-divider"></view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">关系指数</text>
|
||||
<text class="summary-value value-warning">{{monthRelation}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 记录列表 -->
|
||||
<view class="records-container">
|
||||
<view class="record-card" wx:for="{{records}}" wx:key="id">
|
||||
<view class="record-top">
|
||||
<view class="record-date-time">
|
||||
<text class="record-date">{{item.dateLabel}}</text>
|
||||
<text class="record-time">{{item.timeRange}}</text>
|
||||
</view>
|
||||
<text class="record-duration">{{item.durationText}}</text>
|
||||
</view>
|
||||
<view class="record-bottom">
|
||||
<text class="record-table">{{item.tableNo}}</text>
|
||||
<text class="record-type {{item.typeClass}}">{{item.typeLabel}}</text>
|
||||
<text class="record-income">¥{{item.amount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无当月记录 -->
|
||||
<view class="no-month-data" wx:if="{{records.length === 0}}">
|
||||
<text class="no-month-text">本月暂无服务记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<view class="list-footer" wx:if="{{records.length > 0}}">
|
||||
<text class="footer-text">— 已加载全部记录 —</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="loading-more" wx:if="{{loadingMore}}">
|
||||
<t-loading theme="circular" size="40rpx" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{120}}" customerId="{{customerId}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,283 @@
|
||||
/* ========== 加载态 & 空态 ========== */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
/* ========== Banner ========== */
|
||||
.banner-area {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.banner-bg {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
background: linear-gradient(135deg, #ff6b4a, #ff8a65);
|
||||
display: block;
|
||||
}
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.banner-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.nav-back {
|
||||
padding: 8rpx;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
/* 客户头部 */
|
||||
.customer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 8rpx 40rpx 32rpx;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
.info-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.customer-name {
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
.phone-text {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.sub-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.sub-stat {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.stat-highlight {
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
}
|
||||
|
||||
/* ========== 月份切换 ========== */
|
||||
.month-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 48rpx;
|
||||
background: #ffffff;
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
}
|
||||
.month-btn {
|
||||
padding: 12rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.month-btn.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.month-label {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
|
||||
/* ========== 月度统计 ========== */
|
||||
.month-summary {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #ffffff;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
}
|
||||
.summary-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.summary-label {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.summary-value {
|
||||
display: block;
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.value-primary {
|
||||
color: var(--color-primary, #0052d9);
|
||||
}
|
||||
.value-warning {
|
||||
color: var(--color-warning, #ed7b2f);
|
||||
}
|
||||
.summary-divider {
|
||||
width: 1rpx;
|
||||
height: 64rpx;
|
||||
background: var(--color-gray-2, #eeeeee);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* ========== 记录列表 ========== */
|
||||
.records-container {
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: 160rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.04);
|
||||
border: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.record-card:active {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.record-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.record-date-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.record-date {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.record-time {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.record-duration {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: var(--color-success, #00a870);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.record-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.record-table {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-primary, #0052d9);
|
||||
background: var(--color-primary-light, #ecf2fe);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.record-type {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.type-basic {
|
||||
background: rgba(0, 168, 112, 0.1);
|
||||
color: var(--color-success, #00a870);
|
||||
}
|
||||
.type-vip {
|
||||
background: rgba(0, 82, 217, 0.1);
|
||||
color: var(--color-primary, #0052d9);
|
||||
}
|
||||
.type-tip {
|
||||
background: rgba(237, 123, 47, 0.1);
|
||||
color: var(--color-warning, #ed7b2f);
|
||||
}
|
||||
.record-income {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13, #242424);
|
||||
margin-left: auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 无当月数据 */
|
||||
.no-month-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
.no-month-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* 底部提示 */
|
||||
.list-footer {
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
.footer-text {
|
||||
font-size: 20rpx;
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.loading-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "开发调试面板",
|
||||
"usingComponents": {}
|
||||
}
|
||||
182
apps/miniprogram - 副本/miniprogram/pages/dev-tools/dev-tools.ts
Normal file
182
apps/miniprogram - 副本/miniprogram/pages/dev-tools/dev-tools.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 开发调试面板页面
|
||||
*
|
||||
* 功能:
|
||||
* - 展示当前用户上下文(角色、权限、绑定、门店)
|
||||
* - 一键切换角色(后端真实修改 user_site_roles + 重签 token)
|
||||
* - 一键切换用户状态(后端真实修改 users.status + 重签 token)
|
||||
* - 页面跳转列表(点击跳转到任意已注册页面)
|
||||
*/
|
||||
import { request } from "../../utils/request"
|
||||
|
||||
// 页面列表分三段:正在迁移、已完成、未完成
|
||||
const MIGRATING_PAGES = [
|
||||
{ path: "pages/board-finance/board-finance", name: "财务看板" },
|
||||
]
|
||||
|
||||
const DONE_PAGES = [
|
||||
{ path: "pages/no-permission/no-permission", name: "无权限" },
|
||||
{ path: "pages/login/login", name: "登录" },
|
||||
{ path: "pages/apply/apply", name: "申请" },
|
||||
{ path: "pages/reviewing/reviewing", name: "审核中" },
|
||||
{ path: "pages/board-coach/board-coach", name: "助教看板" },
|
||||
{ path: "pages/board-customer/board-customer", name: "客户看板" },
|
||||
]
|
||||
|
||||
const TODO_PAGES = [
|
||||
{ path: "pages/task-list/task-list", name: "任务列表" },
|
||||
{ path: "pages/my-profile/my-profile", name: "个人中心" },
|
||||
{ path: "pages/task-detail/task-detail", name: "任务详情" },
|
||||
{ path: "pages/task-detail-callback/task-detail-callback", name: "任务-回访" },
|
||||
{ path: "pages/task-detail-priority/task-detail-priority", name: "任务-优先级" },
|
||||
{ path: "pages/task-detail-relationship/task-detail-relationship", name: "任务-关系" },
|
||||
{ path: "pages/notes/notes", name: "备忘录" },
|
||||
{ path: "pages/performance/performance", name: "业绩总览" },
|
||||
{ path: "pages/performance-records/performance-records", name: "业绩明细" },
|
||||
{ path: "pages/customer-detail/customer-detail", name: "客户详情" },
|
||||
{ path: "pages/customer-service-records/customer-service-records", name: "客户服务记录" },
|
||||
{ path: "pages/coach-detail/coach-detail", name: "助教详情" },
|
||||
{ path: "pages/chat/chat", name: "AI 对话" },
|
||||
{ path: "pages/chat-history/chat-history", name: "对话历史" },
|
||||
{ path: "pages/index/index", name: "首页" },
|
||||
{ path: "pages/mvp/mvp", name: "MVP" },
|
||||
{ path: "pages/logs/logs", name: "日志" },
|
||||
]
|
||||
|
||||
const ROLE_LIST = [
|
||||
{ code: "coach", name: "助教" },
|
||||
{ code: "staff", name: "员工" },
|
||||
{ code: "site_admin", name: "店铺管理员" },
|
||||
{ code: "tenant_admin", name: "租户管理员" },
|
||||
]
|
||||
|
||||
const STATUS_LIST = ["new", "pending", "approved", "rejected", "disabled"]
|
||||
|
||||
Page({
|
||||
data: {
|
||||
ctx: null as any,
|
||||
loading: true,
|
||||
pages: [] as typeof TODO_PAGES,
|
||||
migratingPages: MIGRATING_PAGES,
|
||||
donePages: DONE_PAGES,
|
||||
todoPages: TODO_PAGES,
|
||||
roles: ROLE_LIST,
|
||||
statuses: STATUS_LIST,
|
||||
currentRole: "",
|
||||
rolesText: "-",
|
||||
permissionsText: "-",
|
||||
bindingText: "-",
|
||||
message: "",
|
||||
messageType: "",
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadContext()
|
||||
},
|
||||
|
||||
/** 加载当前用户调试上下文 */
|
||||
async loadContext() {
|
||||
// 没有 token 时不发请求,避免 401 → 刷新 → 跳转的无限循环
|
||||
const token = wx.getStorageSync("token")
|
||||
if (!token) {
|
||||
this.setData({ loading: false })
|
||||
this.showMsg("未登录,请先通过 dev-login 获取 token", "error")
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true, message: "" })
|
||||
try {
|
||||
const ctx = await request({ url: "/api/xcx/dev-context", method: "GET" })
|
||||
const rolesText = ctx.roles?.length ? ctx.roles.join(", ") : "-"
|
||||
const permissionsText = ctx.permissions?.length ? ctx.permissions.join(", ") : "-"
|
||||
let bindingText = "-"
|
||||
if (ctx.binding) {
|
||||
const b = ctx.binding
|
||||
bindingText = `${b.binding_type} (助教:${b.assistant_id || '-'} 员工:${b.staff_id || '-'})`
|
||||
}
|
||||
// 当前角色取第一个(通常只有一个)
|
||||
const currentRole = ctx.roles?.length ? ctx.roles[0] : ""
|
||||
this.setData({ ctx, rolesText, permissionsText, bindingText, currentRole, loading: false })
|
||||
} catch (err: any) {
|
||||
this.setData({ loading: false })
|
||||
// 401 说明 token 无效或是受限令牌,不触发重试
|
||||
const detail = err?.data?.detail || "网络错误"
|
||||
this.showMsg("获取上下文失败: " + detail, "error")
|
||||
}
|
||||
},
|
||||
|
||||
/** 切换角色 */
|
||||
async switchRole(e: any) {
|
||||
const code = e.currentTarget.dataset.code
|
||||
if (code === this.data.currentRole) return
|
||||
wx.showLoading({ title: "切换中..." })
|
||||
try {
|
||||
const res = await request({
|
||||
url: "/api/xcx/dev-switch-role",
|
||||
method: "POST",
|
||||
data: { role_code: code },
|
||||
})
|
||||
// 保存新 token
|
||||
this.saveTokens(res)
|
||||
this.showMsg(`已切换为 ${code}`, "success")
|
||||
// 重新加载上下文
|
||||
this.loadContext()
|
||||
} catch (err: any) {
|
||||
this.showMsg("切换角色失败: " + (err?.data?.detail || "网络错误"), "error")
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
/** 切换用户状态 */
|
||||
async switchStatus(e: any) {
|
||||
const status = e.currentTarget.dataset.status
|
||||
if (status === this.data.ctx?.status) return
|
||||
wx.showLoading({ title: "切换中..." })
|
||||
try {
|
||||
const res = await request({
|
||||
url: "/api/xcx/dev-switch-status",
|
||||
method: "POST",
|
||||
data: { status },
|
||||
})
|
||||
// 保存新 token
|
||||
this.saveTokens(res)
|
||||
this.showMsg(`状态已切换为 ${status}`, "success")
|
||||
this.loadContext()
|
||||
} catch (err: any) {
|
||||
this.showMsg("切换状态失败: " + (err?.data?.detail || "网络错误"), "error")
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
/** 跳转到指定页面 */
|
||||
goPage(e: any) {
|
||||
const url = "/" + e.currentTarget.dataset.url
|
||||
// 使用 reLaunch 确保能跳转到任意页面(包括 tabBar 页面)
|
||||
wx.reLaunch({ url })
|
||||
},
|
||||
|
||||
/** 保存后端返回的新 token */
|
||||
saveTokens(res: any) {
|
||||
if (res.access_token && res.refresh_token) {
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.token = res.access_token
|
||||
app.globalData.refreshToken = res.refresh_token
|
||||
wx.setStorageSync("token", res.access_token)
|
||||
wx.setStorageSync("refreshToken", res.refresh_token)
|
||||
if (res.user_id) {
|
||||
wx.setStorageSync("userId", res.user_id)
|
||||
}
|
||||
if (res.user_status) {
|
||||
wx.setStorageSync("userStatus", res.user_status)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** 显示操作提示 */
|
||||
showMsg(msg: string, type: "success" | "error") {
|
||||
this.setData({ message: msg, messageType: type })
|
||||
setTimeout(() => this.setData({ message: "" }), 3000)
|
||||
},
|
||||
})
|
||||
109
apps/miniprogram - 副本/miniprogram/pages/dev-tools/dev-tools.wxml
Normal file
109
apps/miniprogram - 副本/miniprogram/pages/dev-tools/dev-tools.wxml
Normal file
@@ -0,0 +1,109 @@
|
||||
<!--
|
||||
开发调试面板 — 页面跳转、角色切换、状态切换、绑定切换、上下文展示
|
||||
-->
|
||||
<view class="container">
|
||||
|
||||
<!-- 当前上下文信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">当前上下文</view>
|
||||
<view class="info-card" wx:if="{{ctx}}">
|
||||
<view class="info-row"><text class="label">user_id</text><text class="value">{{ctx.user_id}}</text></view>
|
||||
<view class="info-row"><text class="label">openid</text><text class="value ellipsis">{{ctx.openid || '-'}}</text></view>
|
||||
<view class="info-row"><text class="label">状态</text><text class="value tag tag-{{ctx.status}}">{{ctx.status}}</text></view>
|
||||
<view class="info-row"><text class="label">昵称</text><text class="value">{{ctx.nickname || '-'}}</text></view>
|
||||
<view class="info-row"><text class="label">门店</text><text class="value">{{ctx.site_name || '-'}} ({{ctx.site_id || '-'}})</text></view>
|
||||
<view class="info-row"><text class="label">角色</text><text class="value">{{rolesText}}</text></view>
|
||||
<view class="info-row"><text class="label">权限</text><text class="value ellipsis">{{permissionsText}}</text></view>
|
||||
<view class="info-row"><text class="label">绑定</text><text class="value">{{bindingText}}</text></view>
|
||||
</view>
|
||||
<view class="info-card" wx:else>
|
||||
<text class="hint">{{loading ? '加载中...' : '未登录或无法获取上下文'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 角色切换 -->
|
||||
<view class="section">
|
||||
<view class="section-title">角色切换</view>
|
||||
<view class="btn-group">
|
||||
<view
|
||||
wx:for="{{roles}}"
|
||||
wx:key="code"
|
||||
class="btn {{currentRole === item.code ? 'btn-active' : ''}}"
|
||||
bindtap="switchRole"
|
||||
data-code="{{item.code}}"
|
||||
>{{item.name}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户状态切换 -->
|
||||
<view class="section">
|
||||
<view class="section-title">用户状态切换</view>
|
||||
<view class="btn-group">
|
||||
<view
|
||||
wx:for="{{statuses}}"
|
||||
wx:key="*this"
|
||||
class="btn {{ctx.status === item ? 'btn-active' : ''}}"
|
||||
bindtap="switchStatus"
|
||||
data-status="{{item}}"
|
||||
>{{item}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 页面跳转 -->
|
||||
<view class="section">
|
||||
<view class="section-title">🔧 正在迁移</view>
|
||||
<view class="page-list">
|
||||
<view
|
||||
wx:for="{{migratingPages}}"
|
||||
wx:key="path"
|
||||
class="page-item page-item--migrating"
|
||||
bindtap="goPage"
|
||||
data-url="{{item.path}}"
|
||||
>
|
||||
<text class="page-name">{{item.name}}</text>
|
||||
<text class="page-path">/{{item.path}}</text>
|
||||
</view>
|
||||
<view class="page-item page-item--empty" wx:if="{{migratingPages.length === 0}}">
|
||||
<text class="hint">暂无</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">✅ 已完成</view>
|
||||
<view class="page-list">
|
||||
<view
|
||||
wx:for="{{donePages}}"
|
||||
wx:key="path"
|
||||
class="page-item page-item--done"
|
||||
bindtap="goPage"
|
||||
data-url="{{item.path}}"
|
||||
>
|
||||
<text class="page-name">{{item.name}}</text>
|
||||
<text class="page-path">/{{item.path}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">⏳ 未完成</view>
|
||||
<view class="page-list">
|
||||
<view
|
||||
wx:for="{{todoPages}}"
|
||||
wx:key="path"
|
||||
class="page-item page-item--todo"
|
||||
bindtap="goPage"
|
||||
data-url="{{item.path}}"
|
||||
>
|
||||
<text class="page-name">{{item.name}}</text>
|
||||
<text class="page-path">/{{item.path}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作提示 -->
|
||||
<view class="section" wx:if="{{message}}">
|
||||
<view class="message {{messageType}}">{{message}}</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
171
apps/miniprogram - 副本/miniprogram/pages/dev-tools/dev-tools.wxss
Normal file
171
apps/miniprogram - 副本/miniprogram/pages/dev-tools/dev-tools.wxss
Normal file
@@ -0,0 +1,171 @@
|
||||
.container {
|
||||
padding: 24rpx;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
padding-left: 8rpx;
|
||||
border-left: 6rpx solid #1890ff;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
width: 120rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 480rpx;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.tag-approved { background: #e6f7e6; color: #52c41a; }
|
||||
.tag-pending { background: #fff7e6; color: #faad14; }
|
||||
.tag-new { background: #e6f7ff; color: #1890ff; }
|
||||
.tag-rejected { background: #fff1f0; color: #ff4d4f; }
|
||||
.tag-disabled { background: #f5f5f5; color: #999; }
|
||||
|
||||
/* 按钮组 */
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 16rpx 28rpx;
|
||||
background: #fff;
|
||||
border: 2rpx solid #d9d9d9;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 页面列表 */
|
||||
.page-list {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.page-item {
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.page-item:active {
|
||||
background: #f0f5ff;
|
||||
}
|
||||
|
||||
.page-item--migrating {
|
||||
border-left: 6rpx solid #faad14;
|
||||
}
|
||||
|
||||
.page-item--done {
|
||||
border-left: 6rpx solid #52c41a;
|
||||
}
|
||||
|
||||
.page-item--todo {
|
||||
border-left: 6rpx solid #d9d9d9;
|
||||
}
|
||||
|
||||
.page-item--empty {
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.page-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-path {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
font-family: monospace;
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* 消息提示 */
|
||||
.message {
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #e6f7e6;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
66
apps/miniprogram - 副本/miniprogram/pages/index/index.js
Normal file
66
apps/miniprogram - 副本/miniprogram/pages/index/index.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// pages/index/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
5
apps/miniprogram - 副本/miniprogram/pages/index/index.json
Normal file
5
apps/miniprogram - 副本/miniprogram/pages/index/index.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-button": "tdesign-miniprogram/button/button"
|
||||
}
|
||||
}
|
||||
54
apps/miniprogram - 副本/miniprogram/pages/index/index.ts
Normal file
54
apps/miniprogram - 副本/miniprogram/pages/index/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// index.ts
|
||||
// 获取应用实例
|
||||
const app = getApp<IAppOption>()
|
||||
const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
|
||||
|
||||
Component({
|
||||
data: {
|
||||
motto: 'Hello World',
|
||||
userInfo: {
|
||||
avatarUrl: defaultAvatarUrl,
|
||||
nickName: '',
|
||||
},
|
||||
hasUserInfo: false,
|
||||
canIUseGetUserProfile: wx.canIUse('getUserProfile'),
|
||||
canIUseNicknameComp: wx.canIUse('input.type.nickname'),
|
||||
},
|
||||
methods: {
|
||||
// 事件处理函数
|
||||
bindViewTap() {
|
||||
wx.navigateTo({
|
||||
url: '../logs/logs',
|
||||
})
|
||||
},
|
||||
onChooseAvatar(e: any) {
|
||||
const { avatarUrl } = e.detail
|
||||
const { nickName } = this.data.userInfo
|
||||
this.setData({
|
||||
"userInfo.avatarUrl": avatarUrl,
|
||||
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
|
||||
})
|
||||
},
|
||||
onInputChange(e: any) {
|
||||
const nickName = e.detail.value
|
||||
const { avatarUrl } = this.data.userInfo
|
||||
this.setData({
|
||||
"userInfo.nickName": nickName,
|
||||
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
|
||||
})
|
||||
},
|
||||
getUserProfile() {
|
||||
// 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗
|
||||
wx.getUserProfile({
|
||||
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
|
||||
success: (res) => {
|
||||
console.log(res)
|
||||
this.setData({
|
||||
userInfo: res.userInfo,
|
||||
hasUserInfo: true
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
30
apps/miniprogram - 副本/miniprogram/pages/index/index.wxml
Normal file
30
apps/miniprogram - 副本/miniprogram/pages/index/index.wxml
Normal file
@@ -0,0 +1,30 @@
|
||||
<!--index.wxml-->
|
||||
<scroll-view class="scrollarea" scroll-y type="list">
|
||||
<view class="container">
|
||||
<view class="userinfo">
|
||||
<block wx:if="{{canIUseNicknameComp && !hasUserInfo}}">
|
||||
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
|
||||
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
|
||||
</button>
|
||||
<view class="nickname-wrapper">
|
||||
<text class="nickname-label">昵称</text>
|
||||
<input type="nickname" class="nickname-input" placeholder="请输入昵称" bind:change="onInputChange" />
|
||||
</view>
|
||||
</block>
|
||||
<block wx:elif="{{!hasUserInfo}}">
|
||||
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
|
||||
<view wx:else> 请使用2.10.4及以上版本基础库 </view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
|
||||
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
|
||||
</block>
|
||||
</view>
|
||||
<view class="usermotto">
|
||||
<text class="user-motto">{{motto}}</text>
|
||||
</view>
|
||||
<t-button theme="primary">按钮</t-button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<dev-fab />
|
||||
62
apps/miniprogram - 副本/miniprogram/pages/index/index.wxss
Normal file
62
apps/miniprogram - 副本/miniprogram/pages/index/index.wxss
Normal file
@@ -0,0 +1,62 @@
|
||||
/**index.wxss**/
|
||||
page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.scrollarea {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.userinfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #aaa;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.userinfo-avatar {
|
||||
overflow: hidden;
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
margin: 20rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.usermotto {
|
||||
margin-top: 200px;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
padding: 0;
|
||||
width: 56px !important;
|
||||
border-radius: 8px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.nickname-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
border-top: .5px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: .5px solid rgba(0, 0, 0, 0.1);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.nickname-label {
|
||||
width: 105px;
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
flex: 1;
|
||||
}
|
||||
8
apps/miniprogram - 副本/miniprogram/pages/login/login.json
Normal file
8
apps/miniprogram - 副本/miniprogram/pages/login/login.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
112
apps/miniprogram - 副本/miniprogram/pages/login/login.ts
Normal file
112
apps/miniprogram - 副本/miniprogram/pages/login/login.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { request } from "../../utils/request"
|
||||
import { API_BASE } from "../../utils/config"
|
||||
|
||||
/** develop 环境使用 dev-login 跳过微信 code2Session */
|
||||
const isDevMode = API_BASE.startsWith("http://127.0.0.1")
|
||||
|
||||
Page({
|
||||
data: {
|
||||
agreed: false,
|
||||
loading: false,
|
||||
/** 系统状态栏高度(px),用于自定义导航栏顶部偏移 */
|
||||
statusBarHeight: 20,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const sysInfo = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: sysInfo.statusBarHeight || 20 })
|
||||
},
|
||||
|
||||
onAgreeChange() {
|
||||
this.setData({ agreed: !this.data.agreed })
|
||||
},
|
||||
|
||||
async onLogin() {
|
||||
if (!this.data.agreed) {
|
||||
wx.showToast({ title: "请先同意用户协议", icon: "none" })
|
||||
return
|
||||
}
|
||||
if (this.data.loading) return
|
||||
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
let data: any
|
||||
|
||||
if (isDevMode) {
|
||||
// 开发模式:直接调用 dev-login,无需 wx.login
|
||||
// 不传 status,让后端保留用户当前状态(首次创建默认 new)
|
||||
data = await request({
|
||||
url: "/api/xcx/dev-login",
|
||||
method: "POST",
|
||||
data: { openid: "dev_test_openid" },
|
||||
needAuth: false,
|
||||
})
|
||||
} else {
|
||||
// 正式/体验:走微信 code2Session 流程
|
||||
const loginRes = await new Promise<WechatMiniprogram.LoginSuccessCallbackResult>(
|
||||
(resolve, reject) => {
|
||||
wx.login({
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
data = await request({
|
||||
url: "/api/xcx/login",
|
||||
method: "POST",
|
||||
data: { code: loginRes.code },
|
||||
needAuth: false,
|
||||
})
|
||||
}
|
||||
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.token = data.access_token
|
||||
app.globalData.refreshToken = data.refresh_token
|
||||
wx.setStorageSync("token", data.access_token)
|
||||
wx.setStorageSync("refreshToken", data.refresh_token)
|
||||
|
||||
// 持久化用户身份信息
|
||||
app.globalData.authUser = {
|
||||
userId: data.user_id,
|
||||
status: data.user_status,
|
||||
}
|
||||
wx.setStorageSync("userId", data.user_id)
|
||||
wx.setStorageSync("userStatus", data.user_status)
|
||||
|
||||
// 根据 user_status 路由
|
||||
switch (data.user_status) {
|
||||
case "approved":
|
||||
wx.reLaunch({ url: "/pages/mvp/mvp" })
|
||||
break
|
||||
case "pending":
|
||||
wx.reLaunch({ url: "/pages/reviewing/reviewing" })
|
||||
break
|
||||
case "new":
|
||||
// 全新用户,跳转申请页填表
|
||||
wx.reLaunch({ url: "/pages/apply/apply" })
|
||||
break
|
||||
case "rejected":
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
break
|
||||
case "disabled":
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
break
|
||||
default:
|
||||
wx.reLaunch({ url: "/pages/apply/apply" })
|
||||
break
|
||||
}
|
||||
} catch (err: any) {
|
||||
const msg =
|
||||
err?.statusCode === 403
|
||||
? "账号已被禁用"
|
||||
: err?.statusCode === 401
|
||||
? "登录凭证无效,请重试"
|
||||
: "登录失败,请稍后重试"
|
||||
wx.showToast({ title: msg, icon: "none" })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
})
|
||||
74
apps/miniprogram - 副本/miniprogram/pages/login/login.wxml
Normal file
74
apps/miniprogram - 副本/miniprogram/pages/login/login.wxml
Normal file
@@ -0,0 +1,74 @@
|
||||
<!--pages/login/login.wxml — 忠于 H5 原型,使用 TDesign 组件 -->
|
||||
<view class="page" style="padding-top: {{statusBarHeight}}px;">
|
||||
<!-- 装饰元素:模拟 blur 圆形 -->
|
||||
<view class="deco-circle deco-circle--1"></view>
|
||||
<view class="deco-circle deco-circle--2"></view>
|
||||
<view class="deco-circle deco-circle--3"></view>
|
||||
|
||||
<!-- 顶部区域 - Logo 和名称 -->
|
||||
<view class="hero">
|
||||
<!-- Logo:浮动动画 -->
|
||||
<view class="logo-wrap float-animation">
|
||||
<view class="logo-box">
|
||||
<image class="logo-icon" src="/assets/icons/logo-billiard.svg" mode="aspectFit" />
|
||||
</view>
|
||||
<!-- 装饰点 -->
|
||||
<view class="logo-dot logo-dot--tr"></view>
|
||||
<view class="logo-dot logo-dot--bl"></view>
|
||||
</view>
|
||||
|
||||
<!-- 应用名称 -->
|
||||
<text class="app-name">球房运营助手</text>
|
||||
<text class="app-desc">为台球厅提升运营效率的内部管理工具</text>
|
||||
|
||||
<!-- 功能亮点 -->
|
||||
<view class="features">
|
||||
<view class="feature-item">
|
||||
<view class="feature-icon feature-icon--primary">
|
||||
<t-icon name="task" size="35rpx" color="#0052d9" />
|
||||
</view>
|
||||
<text class="feature-text">任务管理</text>
|
||||
</view>
|
||||
<view class="feature-item">
|
||||
<view class="feature-icon feature-icon--success">
|
||||
<t-icon name="chart-bar" size="35rpx" color="#00a870" />
|
||||
</view>
|
||||
<text class="feature-text">数据看板</text>
|
||||
</view>
|
||||
<view class="feature-item">
|
||||
<view class="feature-icon feature-icon--warning">
|
||||
<t-icon name="chat" size="35rpx" color="#ed7b2f" />
|
||||
</view>
|
||||
<text class="feature-text">智能助手</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部区域 - 登录按钮和协议 -->
|
||||
<view class="bottom-area">
|
||||
<!-- 微信登录按钮 -->
|
||||
<view class="login-btn-wrap {{(!agreed || loading) ? 'login-btn--disabled' : 'login-btn--active'}}" bindtap="onLogin">
|
||||
<image wx:if="{{!loading}}" class="wechat-icon" src="/assets/icons/icon-wechat.svg" mode="aspectFit" />
|
||||
<t-loading wx:if="{{loading}}" theme="circular" size="32rpx" color="#fff" />
|
||||
<text class="login-btn-text">使用微信登录</text>
|
||||
</view>
|
||||
|
||||
<!-- 协议勾选 -->
|
||||
<view class="agreement" bindtap="onAgreeChange">
|
||||
<view class="checkbox {{agreed ? 'checkbox--checked' : ''}}">
|
||||
<t-icon wx:if="{{agreed}}" name="check" size="18rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="agreement-text-wrap">
|
||||
<text class="agreement-text">我已阅读并同意</text>
|
||||
<text class="link">《用户协议》</text>
|
||||
<text class="agreement-text">和</text>
|
||||
<text class="link">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部说明 -->
|
||||
<text class="footer-tip">仅限球房内部员工使用</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<dev-fab />
|
||||
281
apps/miniprogram - 副本/miniprogram/pages/login/login.wxss
Normal file
281
apps/miniprogram - 副本/miniprogram/pages/login/login.wxss
Normal file
@@ -0,0 +1,281 @@
|
||||
/* pages/login/login.wxss — 忠于 H5 原型,TDesign 组件定制 */
|
||||
|
||||
.page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 50%, #ecfeff 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
/* padding-top 由 JS statusBarHeight 动态设置,box-sizing 确保 padding 包含在 100vh 内 */
|
||||
}
|
||||
|
||||
/* ---- 装饰圆形 ---- */
|
||||
.deco-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.deco-circle--1 {
|
||||
width: 176rpx;
|
||||
height: 176rpx;
|
||||
background: radial-gradient(circle, rgba(0, 82, 217, 0.12) 0%, transparent 70%);
|
||||
top: 140rpx;
|
||||
left: 56rpx;
|
||||
opacity: 0.8;
|
||||
animation: pulse-soft 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.deco-circle--2 {
|
||||
width: 210rpx;
|
||||
height: 210rpx;
|
||||
background: radial-gradient(circle, rgba(103, 232, 249, 0.12) 0%, transparent 70%);
|
||||
top: 280rpx;
|
||||
right: 42rpx;
|
||||
opacity: 0.8;
|
||||
animation: pulse-soft 3s ease-in-out infinite 1s;
|
||||
}
|
||||
|
||||
.deco-circle--3 {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
background: radial-gradient(circle, rgba(96, 165, 250, 0.12) 0%, transparent 70%);
|
||||
bottom: 280rpx;
|
||||
left: 84rpx;
|
||||
opacity: 0.8;
|
||||
animation: pulse-soft 3s ease-in-out infinite 0.5s;
|
||||
}
|
||||
|
||||
/* ---- Hero 区域 ---- */
|
||||
.hero {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 56rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ---- Logo ---- */
|
||||
.logo-wrap {
|
||||
position: relative;
|
||||
margin-bottom: 42rpx;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
width: 168rpx;
|
||||
height: 168rpx;
|
||||
border-radius: 42rpx;
|
||||
background: linear-gradient(135deg, #0052d9, #60a5fa);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 14rpx 42rpx rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 98rpx;
|
||||
height: 98rpx;
|
||||
}
|
||||
|
||||
.logo-dot {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.logo-dot--tr {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
background: #22d3ee;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
box-shadow: 0 4rpx 10rpx rgba(34, 211, 238, 0.4);
|
||||
}
|
||||
|
||||
.logo-dot--bl {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
background: #93c5fd;
|
||||
bottom: -14rpx;
|
||||
left: -14rpx;
|
||||
box-shadow: 0 4rpx 10rpx rgba(147, 197, 253, 0.4);
|
||||
}
|
||||
|
||||
/* ---- 应用名称 ---- */
|
||||
.app-name {
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
color: #242424;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8b8b8b;
|
||||
text-align: center;
|
||||
line-height: 1.625;
|
||||
margin-bottom: 56rpx;
|
||||
}
|
||||
|
||||
/* ---- 功能亮点 ---- */
|
||||
.features {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 28rpx;
|
||||
padding: 36rpx 18rpx;
|
||||
margin-bottom: 42rpx;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feature-icon--primary { background: rgba(0, 82, 217, 0.1); }
|
||||
.feature-icon--success { background: rgba(0, 168, 112, 0.1); }
|
||||
.feature-icon--warning { background: rgba(237, 123, 47, 0.1); }
|
||||
|
||||
.feature-text {
|
||||
font-size: 22rpx;
|
||||
color: #5e5e5e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---- 底部操作区 ---- */
|
||||
.bottom-area {
|
||||
width: 100%;
|
||||
padding: 0 56rpx 70rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* TDesign Button 替换为原生 view 按钮,避免 TDesign 默认样式干扰 */
|
||||
.login-btn-wrap {
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
border-radius: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.login-btn-text {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 未勾选协议 → 灰色禁用态(忠于原型 btn-disabled) */
|
||||
.login-btn--disabled {
|
||||
background: #dcdcdc;
|
||||
}
|
||||
.login-btn--disabled .login-btn-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 勾选协议 → 蓝色渐变(忠于原型 from-primary to-blue-500) */
|
||||
.login-btn--active {
|
||||
background: linear-gradient(135deg, #0052d9, #3b82f6);
|
||||
box-shadow: 0 10rpx 28rpx rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
|
||||
/* ---- 协议勾选 ---- */
|
||||
.agreement {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
min-width: 28rpx;
|
||||
min-height: 28rpx;
|
||||
border: 2rpx solid #dcdcdc;
|
||||
border-radius: 6rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 4rpx;
|
||||
margin-right: 10rpx;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.checkbox--checked {
|
||||
background: #0052d9;
|
||||
border-color: #0052d9;
|
||||
}
|
||||
|
||||
.agreement-text-wrap {
|
||||
flex: 1;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
font-size: 22rpx;
|
||||
color: #8b8b8b;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0052d9;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
/* ---- 底部提示 ---- */
|
||||
.footer-tip {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #c5c5c5;
|
||||
margin-top: 42rpx;
|
||||
}
|
||||
|
||||
/* ---- 动画 ---- */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-14rpx); }
|
||||
}
|
||||
|
||||
.float-animation {
|
||||
animation: float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-soft {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
4
apps/miniprogram - 副本/miniprogram/pages/mvp/mvp.json
Normal file
4
apps/miniprogram - 副本/miniprogram/pages/mvp/mvp.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "MVP验证"
|
||||
}
|
||||
45
apps/miniprogram - 副本/miniprogram/pages/mvp/mvp.ts
Normal file
45
apps/miniprogram - 副本/miniprogram/pages/mvp/mvp.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// MVP 全链路验证页面
|
||||
// 从后端 API 读取 test."xcx-test" 表 ti 列第一行并显示
|
||||
|
||||
import { API_BASE } from "../../utils/config"
|
||||
|
||||
Page({
|
||||
data: {
|
||||
tiValue: "加载中...",
|
||||
error: "",
|
||||
loading: true,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.fetchData()
|
||||
},
|
||||
|
||||
fetchData() {
|
||||
this.setData({ loading: true, error: "" })
|
||||
|
||||
wx.request({
|
||||
url: `${API_BASE}/api/xcx-test`,
|
||||
method: "GET",
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const data = res.data as { ti: string }
|
||||
this.setData({
|
||||
tiValue: data.ti,
|
||||
loading: false,
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
error: `请求失败: ${res.statusCode}`,
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
this.setData({
|
||||
error: `网络错误: ${err.errMsg}`,
|
||||
loading: false,
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
19
apps/miniprogram - 副本/miniprogram/pages/mvp/mvp.wxml
Normal file
19
apps/miniprogram - 副本/miniprogram/pages/mvp/mvp.wxml
Normal file
@@ -0,0 +1,19 @@
|
||||
<!--MVP 全链路验证页面-->
|
||||
<view class="container">
|
||||
<view class="title">小程序 MVP 验证</view>
|
||||
<view class="desc">数据来源: test_zqyy_app → test."xcx-test" → ti</view>
|
||||
|
||||
<view class="result" wx:if="{{!error}}">
|
||||
<text class="value">{{tiValue}}</text>
|
||||
</view>
|
||||
|
||||
<view class="error" wx:if="{{error}}">
|
||||
<text>{{error}}</text>
|
||||
</view>
|
||||
|
||||
<view class="retry" bindtap="fetchData">
|
||||
<text>点击刷新</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<dev-fab />
|
||||
48
apps/miniprogram - 副本/miniprogram/pages/mvp/mvp.wxss
Normal file
48
apps/miniprogram - 副本/miniprogram/pages/mvp/mvp.wxss
Normal file
@@ -0,0 +1,48 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.result {
|
||||
background: #f0f9ff;
|
||||
border: 2rpx solid #0ea5e9;
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx 80rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc2626;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.retry {
|
||||
padding: 20rpx 40rpx;
|
||||
background: #0ea5e9;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { mockUserProfile } from '../../utils/mock-data'
|
||||
import { getMenuRoute, navigateTo } from '../../utils/router'
|
||||
|
||||
// TODO: 联调时替换为真实 API 获取用户信息
|
||||
Page({
|
||||
data: {
|
||||
userInfo: mockUserProfile,
|
||||
},
|
||||
|
||||
onMenuTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const key = e.currentTarget.dataset.key as string
|
||||
const route = getMenuRoute(key)
|
||||
if (route) {
|
||||
navigateTo(route)
|
||||
}
|
||||
},
|
||||
|
||||
onLogout() {
|
||||
wx.showModal({
|
||||
title: '确认退出',
|
||||
content: '确认退出当前账号吗?',
|
||||
confirmColor: '#e34d59',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
wx.clearStorageSync()
|
||||
wx.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
<!-- 我的页面 -->
|
||||
<view class="page-my-profile">
|
||||
<!-- 用户信息区域 -->
|
||||
<view class="user-card">
|
||||
<image class="avatar" src="{{userInfo.avatar}}" mode="aspectFill" />
|
||||
<view class="user-info">
|
||||
<view class="name-row">
|
||||
<text class="name">{{userInfo.name}}</text>
|
||||
<text class="role-tag">{{userInfo.role}}</text>
|
||||
</view>
|
||||
<text class="store-name">{{userInfo.storeName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="menu-list">
|
||||
<view class="menu-item" bind:tap="onMenuTap" data-key="notes">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon icon-notes">
|
||||
<t-icon name="edit-1" size="40rpx" color="#0052d9" />
|
||||
</view>
|
||||
<text class="menu-text">备注记录</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
|
||||
<view class="menu-item" bind:tap="onMenuTap" data-key="chat-history">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon icon-chat">
|
||||
<t-icon name="chat" size="40rpx" color="#00a870" />
|
||||
</view>
|
||||
<text class="menu-text">助手对话记录</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
|
||||
<view class="menu-item menu-item--last" bind:tap="onLogout">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon icon-logout">
|
||||
<t-icon name="poweroff" size="40rpx" color="#e34d59" />
|
||||
</view>
|
||||
<text class="menu-text">退出账号</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 悬浮按钮 — TabBar 页面 bottom 200rpx -->
|
||||
<ai-float-button visible="{{true}}" bottom="200" />
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,102 @@
|
||||
.page-my-profile {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-page, #f3f3f3);
|
||||
}
|
||||
|
||||
/* 用户信息卡片 */
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
padding: 48rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary, #242424);
|
||||
}
|
||||
|
||||
.role-tag {
|
||||
padding: 4rpx 16rpx;
|
||||
background: rgba(0, 82, 217, 0.1);
|
||||
color: #0052d9;
|
||||
font-size: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text-placeholder, #8b8b8b);
|
||||
}
|
||||
|
||||
/* 菜单列表 */
|
||||
.menu-list {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f3f3f3;
|
||||
}
|
||||
|
||||
.menu-item--last {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-notes {
|
||||
background: rgba(0, 82, 217, 0.1);
|
||||
}
|
||||
|
||||
.icon-chat {
|
||||
background: rgba(0, 168, 112, 0.1);
|
||||
}
|
||||
|
||||
.icon-logout {
|
||||
background: rgba(227, 77, 89, 0.1);
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text-primary, #242424);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "无权限",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// pages/no-permission/no-permission.ts
|
||||
// 无权限页面 — 账号已禁用或无访问权限时展示
|
||||
// onShow 时查询最新状态,状态变化时自动跳转
|
||||
|
||||
import { request } from "../../utils/request"
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const { statusBarHeight = 0 } = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight })
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._checkStatus()
|
||||
},
|
||||
|
||||
/** 查询最新用户状态,非 disabled 时自动跳转 */
|
||||
async _checkStatus() {
|
||||
const token = wx.getStorageSync("token")
|
||||
if (!token) {
|
||||
wx.reLaunch({ url: "/pages/login/login" })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await request({
|
||||
url: "/api/xcx/me",
|
||||
method: "GET",
|
||||
needAuth: true,
|
||||
})
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.authUser = {
|
||||
userId: data.user_id,
|
||||
status: data.status,
|
||||
nickname: data.nickname,
|
||||
}
|
||||
wx.setStorageSync("userId", data.user_id)
|
||||
wx.setStorageSync("userStatus", data.status)
|
||||
|
||||
switch (data.status) {
|
||||
case "disabled":
|
||||
case "rejected":
|
||||
break // 留在当前页
|
||||
case "approved":
|
||||
wx.reLaunch({ url: "/pages/mvp/mvp" })
|
||||
break
|
||||
case "pending":
|
||||
wx.reLaunch({ url: "/pages/reviewing/reviewing" })
|
||||
break
|
||||
case "new":
|
||||
wx.reLaunch({ url: "/pages/apply/apply" })
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// 网络错误不阻塞
|
||||
}
|
||||
},
|
||||
|
||||
/** 更换登录账号:清除凭证后跳转登录页 */
|
||||
onSwitchAccount() {
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.token = undefined
|
||||
app.globalData.refreshToken = undefined
|
||||
app.globalData.authUser = undefined
|
||||
wx.removeStorageSync("token")
|
||||
wx.removeStorageSync("refreshToken")
|
||||
wx.removeStorageSync("userId")
|
||||
wx.removeStorageSync("userStatus")
|
||||
wx.reLaunch({ url: "/pages/login/login" })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
<!-- pages/no-permission/no-permission.wxml — 按 H5 原型结构迁移 -->
|
||||
<view class="page" style="padding-top: {{statusBarHeight}}px;">
|
||||
<!-- 十字纹背景图案(H5 bg-pattern,error 色) -->
|
||||
<view class="bg-pattern"></view>
|
||||
<!-- 顶部渐变装饰(红色主题) -->
|
||||
<view class="top-gradient"></view>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="content">
|
||||
<!-- 图标区域 -->
|
||||
<view class="icon-area">
|
||||
<!-- 背景光晕 -->
|
||||
<view class="icon-glow"></view>
|
||||
<!-- 主图标 -->
|
||||
<view class="icon-box">
|
||||
<image class="icon-main-img" src="/assets/icons/icon-forbidden.svg" mode="aspectFit" />
|
||||
</view>
|
||||
<!-- 装饰点 -->
|
||||
<view class="icon-dot dot-1"></view>
|
||||
<view class="icon-dot dot-2"></view>
|
||||
</view>
|
||||
|
||||
<!-- 标题区域 -->
|
||||
<view class="title-area">
|
||||
<text class="main-title">无访问权限</text>
|
||||
<text class="sub-title">很抱歉,您的访问申请未通过审核,或当前账号无访问权限</text>
|
||||
</view>
|
||||
|
||||
<!-- 原因说明卡片 -->
|
||||
<view class="reason-card">
|
||||
<view class="reason-header">
|
||||
<view class="reason-icon-box">
|
||||
<t-icon name="error-circle-filled" size="35rpx" color="#e34d59" />
|
||||
</view>
|
||||
<view class="reason-header-text">
|
||||
<text class="reason-title">可能的原因</text>
|
||||
<view class="reason-list">
|
||||
<text class="reason-item">• 申请信息不完整或不符合要求</text>
|
||||
<text class="reason-item">• 非本店授权员工账号</text>
|
||||
<text class="reason-item">• 账号权限已被管理员收回</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 联系管理员 -->
|
||||
<view class="reason-footer">
|
||||
<text class="reason-footer-label">请联系管理员</text>
|
||||
<text class="reason-footer-value">厉超</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 帮助提示 -->
|
||||
<view class="contact-hint">
|
||||
<t-icon name="help-circle-filled" size="28rpx" color="#a6a6a6" />
|
||||
<text class="contact-text">如有疑问,请联系管理员重新申请</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-area">
|
||||
<view class="switch-btn" bindtap="onSwitchAccount">
|
||||
<t-icon name="logout" size="28rpx" color="#5e5e5e" />
|
||||
<text class="switch-btn-text">更换登录账号</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,241 @@
|
||||
/* pages/no-permission/no-permission.wxss — 原 H5 值 × 0.875 统一缩放 */
|
||||
|
||||
.page {
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f8fafc;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bg-pattern {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-image:
|
||||
repeating-linear-gradient(0deg, transparent, transparent 58rpx, rgba(227,77,89,0.03) 58rpx, rgba(227,77,89,0.03) 62rpx, transparent 62rpx, transparent 120rpx),
|
||||
repeating-linear-gradient(90deg, transparent, transparent 58rpx, rgba(227,77,89,0.03) 58rpx, rgba(227,77,89,0.03) 62rpx, transparent 62rpx, transparent 120rpx);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 512 × 0.875 = 448 */
|
||||
.top-gradient {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 448rpx;
|
||||
background: linear-gradient(to bottom, rgba(227,77,89,0.10), transparent);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* padding 64 × 0.875 = 56 */
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 56rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* mb 64 × 0.875 = 56 */
|
||||
.icon-area {
|
||||
position: relative;
|
||||
margin-bottom: 56rpx;
|
||||
}
|
||||
|
||||
/* 320 × 0.875 = 280, offset -48 × 0.875 = -42 */
|
||||
.icon-glow {
|
||||
position: absolute;
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
border-radius: 50%;
|
||||
top: -42rpx;
|
||||
left: -42rpx;
|
||||
background: radial-gradient(circle, rgba(227,77,89,0.18) 0%, rgba(227,77,89,0.06) 50%, transparent 75%);
|
||||
}
|
||||
|
||||
/* 224 × 0.875 = 196, radius 48 × 0.875 = 42 */
|
||||
.icon-box {
|
||||
position: relative;
|
||||
width: 196rpx;
|
||||
height: 196rpx;
|
||||
border-radius: 42rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #fb7185, #ef4444);
|
||||
box-shadow: 0 14rpx 42rpx rgba(227,77,89,0.3);
|
||||
}
|
||||
|
||||
/* 112 × 0.875 = 98 */
|
||||
.icon-main-img {
|
||||
width: 98rpx;
|
||||
height: 98rpx;
|
||||
}
|
||||
|
||||
.icon-dot { position: absolute; border-radius: 50%; }
|
||||
|
||||
/* 32 × 0.875 = 28, offset -16 × 0.875 = -14 */
|
||||
.dot-1 {
|
||||
width: 28rpx; height: 28rpx;
|
||||
top: -14rpx; right: -14rpx;
|
||||
background: #e34d59; opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 24 × 0.875 = 21 → 22, offset -8 × 0.875 = -7 → -8, -24 × 0.875 = -21 → -22 */
|
||||
.dot-2 {
|
||||
width: 22rpx; height: 22rpx;
|
||||
bottom: -8rpx; left: -22rpx;
|
||||
background: #fda4af; opacity: 0.6;
|
||||
}
|
||||
|
||||
/* mb 64 × 0.875 = 56 */
|
||||
.title-area {
|
||||
text-align: center;
|
||||
margin-bottom: 56rpx;
|
||||
}
|
||||
|
||||
/* 48 × 0.875 = 42, mb 24 × 0.875 = 21 → 22 */
|
||||
.main-title {
|
||||
display: block;
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
color: #4b4b4b;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
/* 28 × 0.875 = 24.5 → 24, max-w 640 × 0.875 = 560 */
|
||||
.sub-title {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #8b8b8b;
|
||||
line-height: 1.625;
|
||||
max-width: 490rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* max-w 550 × 0.875 ≈ 482, radius 32 × 0.875 = 28, padding 40 × 0.875 = 35 → 36, mb 48 × 0.875 = 42 */
|
||||
.reason-card {
|
||||
width: 100%;
|
||||
max-width: 482rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 36rpx;
|
||||
margin-bottom: 42rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* gap/mb 32 × 0.875 = 28 */
|
||||
.reason-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 28rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
/* 80 × 0.875 = 70, radius 24 × 0.875 = 21 → 22 */
|
||||
.reason-icon-box {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
min-width: 70rpx;
|
||||
background: rgba(227,77,89,0.1);
|
||||
border-radius: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reason-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 28 × 0.875 = 24, mb 8 × 0.875 = 7 → 8 */
|
||||
.reason-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #242424;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
/* gap 8 × 0.875 = 7 → 8 */
|
||||
.reason-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
/* 24 × 0.875 = 21 → 22 */
|
||||
.reason-item {
|
||||
font-size: 22rpx;
|
||||
color: #a6a6a6;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* mt/pt 32 × 0.875 = 28 */
|
||||
.reason-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 28rpx;
|
||||
padding-top: 28rpx;
|
||||
border-top: 2rpx solid #f3f3f3;
|
||||
}
|
||||
|
||||
.reason-footer-label {
|
||||
font-size: 22rpx;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
.reason-footer-value {
|
||||
font-size: 24rpx;
|
||||
color: #242424;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* gap 16 × 0.875 = 14, mb 64 × 0.875 = 56 */
|
||||
.contact-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
margin-bottom: 56rpx;
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
font-size: 22rpx;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
/* px 64 × 0.875 = 56, pb 96 × 0.875 = 84 */
|
||||
.bottom-area {
|
||||
padding: 0 56rpx 84rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* padding 28 × 0.875 = 24.5 → 24, gap 16 × 0.875 = 14, radius 24 × 0.875 = 21 → 22 */
|
||||
.switch-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14rpx;
|
||||
width: 100%;
|
||||
padding: 24rpx 0;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #eeeeee;
|
||||
border-radius: 22rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.switch-btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #5e5e5e;
|
||||
}
|
||||
8
apps/miniprogram - 副本/miniprogram/pages/notes/notes.json
Normal file
8
apps/miniprogram - 副本/miniprogram/pages/notes/notes.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
45
apps/miniprogram - 副本/miniprogram/pages/notes/notes.ts
Normal file
45
apps/miniprogram - 副本/miniprogram/pages/notes/notes.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { mockNotes } from '../../utils/mock-data'
|
||||
import type { Note } from '../../utils/mock-data'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: true,
|
||||
error: false,
|
||||
notes: [] as Note[],
|
||||
/** 系统状态栏高度(px),用于自定义导航栏顶部偏移 */
|
||||
statusBarHeight: 20,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 获取真实状态栏高度,兼容所有机型(iPhone 刘海屏、安卓异形屏等)
|
||||
const sysInfo = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: sysInfo.statusBarHeight || 20 })
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
loadData() {
|
||||
this.setData({ loading: true, error: false })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API 调用 GET /api/xcx/notes
|
||||
try {
|
||||
this.setData({
|
||||
loading: false,
|
||||
notes: mockNotes,
|
||||
})
|
||||
} catch {
|
||||
this.setData({ loading: false, error: true })
|
||||
}
|
||||
}, 400)
|
||||
},
|
||||
|
||||
/** 返回上一页 */
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
/** 错误态重试 */
|
||||
onRetry() {
|
||||
this.loadData()
|
||||
},
|
||||
})
|
||||
45
apps/miniprogram - 副本/miniprogram/pages/notes/notes.wxml
Normal file
45
apps/miniprogram - 副本/miniprogram/pages/notes/notes.wxml
Normal file
@@ -0,0 +1,45 @@
|
||||
<!-- 自定义顶部导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-bar-inner">
|
||||
<view class="nav-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="40rpx" color="#4b4b4b" />
|
||||
</view>
|
||||
<text class="nav-title">备注</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{loading}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 错误态 -->
|
||||
<view class="page-error" wx:elif="{{error}}">
|
||||
<text class="error-text">加载失败,请点击重试</text>
|
||||
<view class="retry-btn" bindtap="onRetry">
|
||||
<text>重试</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空数据态 -->
|
||||
<view class="page-empty" wx:elif="{{notes.length === 0}}">
|
||||
<t-icon name="edit-1" size="120rpx" color="#dcdcdc" />
|
||||
<text class="empty-text">暂无备注记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 正常态 — 备注列表 -->
|
||||
<view class="note-list" wx:else>
|
||||
<view
|
||||
class="note-card"
|
||||
wx:for="{{notes}}"
|
||||
wx:key="id"
|
||||
>
|
||||
<text class="note-content">{{item.content}}</text>
|
||||
<view class="note-bottom">
|
||||
<text class="note-tag {{item.tagType === 'coach' ? 'tag-coach' : 'tag-customer'}}">{{item.tagLabel}}</text>
|
||||
<text class="note-time">{{item.createdAt}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<dev-fab />
|
||||
130
apps/miniprogram - 副本/miniprogram/pages/notes/notes.wxss
Normal file
130
apps/miniprogram - 副本/miniprogram/pages/notes/notes.wxss
Normal file
@@ -0,0 +1,130 @@
|
||||
/* ========== 自定义导航栏 ========== */
|
||||
.nav-bar {
|
||||
/* padding-top 由 JS 动态设置(wx.getSystemInfoSync().statusBarHeight) */
|
||||
background-color: #ffffff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.nav-bar-inner {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border-bottom: 2rpx solid #eeeeee;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.nav-back {
|
||||
position: absolute;
|
||||
left: 32rpx;
|
||||
padding: 8rpx;
|
||||
}
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #242424;
|
||||
}
|
||||
|
||||
/* ========== 页面背景 ========== */
|
||||
page {
|
||||
background-color: #f3f3f3;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ========== 加载态 / 空态 / 错误态 ========== */
|
||||
.page-loading,
|
||||
.page-empty,
|
||||
.page-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.empty-text,
|
||||
.error-text {
|
||||
font-size: 28rpx;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
.retry-btn {
|
||||
margin-top: 16rpx;
|
||||
padding: 12rpx 48rpx;
|
||||
background-color: #0052d9;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.retry-btn text {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ========== 备注列表 ========== */
|
||||
.note-list {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
/* 卡片间距:H5 原型 space-y-3 = 24rpx */
|
||||
.note-card + .note-card {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
/* ========== 备注卡片 ========== */
|
||||
/* H5: bg-white rounded-2xl p-4 shadow-sm */
|
||||
.note-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 32rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 备注正文 */
|
||||
/* H5: text-sm text-gray-13 leading-relaxed mb-3 */
|
||||
.note-content {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #242424;
|
||||
line-height: 1.625;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* 底部行:标签 + 时间 */
|
||||
/* H5: flex items-center justify-between */
|
||||
.note-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* ========== 标签样式 ========== */
|
||||
/* H5: .note-tag { padding: 4px 10px; font-size: 12px; border-radius: 8px; border: 1px solid; } */
|
||||
.note-tag {
|
||||
padding: 8rpx 20rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid;
|
||||
}
|
||||
|
||||
/* 客户标签 */
|
||||
/* H5: .tag-customer { background: linear-gradient(135deg, #ecf2fe, #e8eeff); color: #0052d9; border-color: #c5d4f7; } */
|
||||
.tag-customer {
|
||||
background: linear-gradient(135deg, #ecf2fe, #e8eeff);
|
||||
color: #0052d9;
|
||||
border-color: #c5d4f7;
|
||||
}
|
||||
|
||||
/* 助教标签 */
|
||||
/* H5: .tag-coach { background: linear-gradient(135deg, #e8faf0, #e0f7ea); color: #00a870; border-color: #b3e6d0; } */
|
||||
.tag-coach {
|
||||
background: linear-gradient(135deg, #e8faf0, #e0f7ea);
|
||||
color: #00a870;
|
||||
border-color: #b3e6d0;
|
||||
}
|
||||
|
||||
/* 时间 */
|
||||
/* H5: text-xs text-gray-6 */
|
||||
.note-time {
|
||||
font-size: 24rpx;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationBarTitleText": "业绩明细",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { mockPerformanceRecords } from '../../utils/mock-data'
|
||||
import type { PerformanceRecord } from '../../utils/mock-data'
|
||||
|
||||
/** 按日期分组后的展示结构 */
|
||||
interface DateGroup {
|
||||
date: string
|
||||
totalHours: string
|
||||
totalIncome: string
|
||||
records: RecordItem[]
|
||||
}
|
||||
|
||||
interface RecordItem {
|
||||
id: string
|
||||
customerName: string
|
||||
avatarChar: string
|
||||
avatarGradient: string
|
||||
timeRange: string
|
||||
hours: string
|
||||
courseType: string
|
||||
courseTypeClass: string
|
||||
location: string
|
||||
income: string
|
||||
}
|
||||
|
||||
const GRADIENT_POOL = [
|
||||
'from-blue', 'from-pink', 'from-teal', 'from-green',
|
||||
'from-orange', 'from-purple', 'from-violet', 'from-amber',
|
||||
]
|
||||
|
||||
/** 根据名字首字生成稳定的渐变色 */
|
||||
function nameToGradient(name: string): string {
|
||||
const code = name.charCodeAt(0) || 0
|
||||
return GRADIENT_POOL[code % GRADIENT_POOL.length]
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
|
||||
/** Banner */
|
||||
coachName: '小燕',
|
||||
coachLevel: '星级',
|
||||
storeName: '球会名称店',
|
||||
|
||||
/** 月份切换 */
|
||||
currentYear: 2026,
|
||||
currentMonth: 2,
|
||||
monthLabel: '2026年2月',
|
||||
canGoPrev: true,
|
||||
canGoNext: false,
|
||||
|
||||
/** 统计概览 */
|
||||
totalCount: '0笔',
|
||||
totalHours: '0h',
|
||||
totalIncome: '¥0',
|
||||
|
||||
/** 按日期分组的记录 */
|
||||
dateGroups: [] as DateGroup[],
|
||||
|
||||
/** 所有记录(用于筛选) */
|
||||
allRecords: [] as PerformanceRecord[],
|
||||
|
||||
/** 分页 */
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadData(() => wx.stopPullDownRefresh())
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (!this.data.hasMore) return
|
||||
this.setData({ page: this.data.page + 1 })
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
loadData(cb?: () => void) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API,按月份请求
|
||||
const allRecords = mockPerformanceRecords
|
||||
|
||||
// 模拟按日期分组的服务记录
|
||||
const dateGroups: DateGroup[] = [
|
||||
{
|
||||
date: '2月7日',
|
||||
totalHours: '6.0h',
|
||||
totalIncome: '¥510',
|
||||
records: [
|
||||
{ id: 'r1', customerName: '王先生', avatarChar: '王', avatarGradient: nameToGradient('王'), timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '3号台', income: '¥160' },
|
||||
{ id: 'r2', customerName: '李女士', avatarChar: '李', avatarGradient: nameToGradient('李'), timeRange: '16:00-18:00', hours: '2.0h', courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP1号房', income: '¥190' },
|
||||
{ id: 'r3', customerName: '陈女士', avatarChar: '陈', avatarGradient: nameToGradient('陈'), timeRange: '10:00-12:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: '¥160' },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2月6日',
|
||||
totalHours: '3.5h',
|
||||
totalIncome: '¥280',
|
||||
records: [
|
||||
{ id: 'r4', customerName: '张先生', avatarChar: '张', avatarGradient: nameToGradient('张'), timeRange: '19:00-21:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '5号台', income: '¥160' },
|
||||
{ id: 'r5', customerName: '刘先生', avatarChar: '刘', avatarGradient: nameToGradient('刘'), timeRange: '15:30-17:00', hours: '1.5h', courseType: '打赏课', courseTypeClass: 'tag-tip', location: '打赏', income: '¥120' },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2月5日',
|
||||
totalHours: '4.0h',
|
||||
totalIncome: '¥320',
|
||||
records: [
|
||||
{ id: 'r6', customerName: '陈女士', avatarChar: '陈', avatarGradient: nameToGradient('陈'), timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: '¥160' },
|
||||
{ id: 'r7', customerName: '赵先生', avatarChar: '赵', avatarGradient: nameToGradient('赵'), timeRange: '14:00-16:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '7号台', income: '¥160' },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2月4日',
|
||||
totalHours: '4.0h',
|
||||
totalIncome: '¥350',
|
||||
records: [
|
||||
{ id: 'r8', customerName: '孙先生', avatarChar: '孙', avatarGradient: nameToGradient('孙'), timeRange: '19:00-21:00', hours: '2.0h', courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP2号房', income: '¥190' },
|
||||
{ id: 'r9', customerName: '吴女士', avatarChar: '吴', avatarGradient: nameToGradient('吴'), timeRange: '15:00-17:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '1号台', income: '¥160' },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2月3日',
|
||||
totalHours: '3.5h',
|
||||
totalIncome: '¥280',
|
||||
records: [
|
||||
{ id: 'r10', customerName: '郑先生', avatarChar: '郑', avatarGradient: nameToGradient('郑'), timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '4号台', income: '¥160' },
|
||||
{ id: 'r11', customerName: '黄女士', avatarChar: '黄', avatarGradient: nameToGradient('黄'), timeRange: '14:30-16:00', hours: '1.5h', courseType: '打赏课', courseTypeClass: 'tag-tip', location: '打赏', income: '¥120' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
this.setData({
|
||||
pageState: dateGroups.length > 0 ? 'normal' : 'empty',
|
||||
allRecords,
|
||||
dateGroups,
|
||||
totalCount: '32笔',
|
||||
totalHours: '59.0h',
|
||||
totalIncome: '¥4,720',
|
||||
hasMore: false,
|
||||
})
|
||||
|
||||
cb?.()
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/** 切换月份 */
|
||||
switchMonth(e: WechatMiniprogram.TouchEvent) {
|
||||
const direction = e.currentTarget.dataset.direction as 'prev' | 'next'
|
||||
let { currentYear, currentMonth } = this.data
|
||||
|
||||
if (direction === 'prev') {
|
||||
currentMonth--
|
||||
if (currentMonth < 1) {
|
||||
currentMonth = 12
|
||||
currentYear--
|
||||
}
|
||||
} else {
|
||||
currentMonth++
|
||||
if (currentMonth > 12) {
|
||||
currentMonth = 1
|
||||
currentYear++
|
||||
}
|
||||
}
|
||||
|
||||
// 不能超过当前月
|
||||
const now = new Date()
|
||||
const nowYear = now.getFullYear()
|
||||
const nowMonth = now.getMonth() + 1
|
||||
const canGoNext = currentYear < nowYear || (currentYear === nowYear && currentMonth < nowMonth)
|
||||
|
||||
this.setData({
|
||||
currentYear,
|
||||
currentMonth,
|
||||
monthLabel: `${currentYear}年${currentMonth}月`,
|
||||
canGoNext,
|
||||
canGoPrev: true,
|
||||
})
|
||||
|
||||
this.loadData()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<block wx:else>
|
||||
<!-- Banner -->
|
||||
<view class="banner-section">
|
||||
<view class="banner-bg"></view>
|
||||
<view class="banner-content">
|
||||
<view class="coach-info">
|
||||
<view class="coach-avatar">
|
||||
<text class="avatar-emoji">👤</text>
|
||||
</view>
|
||||
<view class="coach-meta">
|
||||
<view class="coach-name-row">
|
||||
<text class="coach-name">{{coachName}}</text>
|
||||
<text class="coach-level-tag">{{coachLevel}}</text>
|
||||
</view>
|
||||
<text class="coach-store">{{storeName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 月份切换 -->
|
||||
<view class="month-switcher">
|
||||
<view class="month-btn {{canGoPrev ? '' : 'month-btn-disabled'}}" data-direction="prev" bindtap="switchMonth">
|
||||
<t-icon name="chevron-left" size="32rpx" />
|
||||
</view>
|
||||
<text class="month-label">{{monthLabel}}</text>
|
||||
<view class="month-btn {{canGoNext ? '' : 'month-btn-disabled'}}" data-direction="next" bindtap="switchMonth">
|
||||
<t-icon name="chevron-right" size="32rpx" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计概览 -->
|
||||
<view class="stats-overview">
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">总记录</text>
|
||||
<text class="stat-value">{{totalCount}}</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">总业绩时长</text>
|
||||
<text class="stat-value stat-primary">{{totalHours}}</text>
|
||||
<text class="stat-hint">预估</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">收入</text>
|
||||
<text class="stat-value stat-success">{{totalIncome}}</text>
|
||||
<text class="stat-hint">预估</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:if="{{pageState === 'empty'}}">
|
||||
<t-icon name="chart-bar" size="120rpx" color="#dcdcdc" />
|
||||
<text class="empty-text">暂无数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 记录列表 -->
|
||||
<view class="records-container" wx:elif="{{pageState === 'normal'}}">
|
||||
<view class="records-card">
|
||||
<block wx:for="{{dateGroups}}" wx:key="date">
|
||||
<!-- 日期分隔线 -->
|
||||
<view class="date-divider">
|
||||
<text class="dd-date">{{item.date}}</text>
|
||||
<view class="dd-line"></view>
|
||||
<text class="dd-stats">时长 {{item.totalHours}} · 预估收入 {{item.totalIncome}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 该日期下的记录 -->
|
||||
<view class="record-item" wx:for="{{item.records}}" wx:for-item="rec" wx:key="id">
|
||||
<view class="record-avatar avatar-{{rec.avatarGradient}}">
|
||||
<text>{{rec.avatarChar}}</text>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<view class="record-top">
|
||||
<view class="record-name-time">
|
||||
<text class="record-name">{{rec.customerName}}</text>
|
||||
<text class="record-time">{{rec.timeRange}}</text>
|
||||
</view>
|
||||
<text class="record-hours">{{rec.hours}}</text>
|
||||
</view>
|
||||
<view class="record-bottom">
|
||||
<view class="record-tags">
|
||||
<text class="course-tag {{rec.courseTypeClass}}">{{rec.courseType}}</text>
|
||||
<text class="record-location">{{rec.location}}</text>
|
||||
</view>
|
||||
<text class="record-income">我的预估收入 <text class="record-income-val">{{rec.income}}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{120}}" />
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,347 @@
|
||||
/* ============================================
|
||||
* 加载态 / 空态
|
||||
* ============================================ */
|
||||
.page-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-6);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* Banner
|
||||
* ============================================ */
|
||||
.banner-section {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-bg {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
background: linear-gradient(135deg, #ff6b4a, #ff8a65);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16rpx 40rpx 32rpx;
|
||||
}
|
||||
|
||||
.coach-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.coach-avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-emoji {
|
||||
font-size: 56rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.coach-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.coach-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.coach-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.coach-level-tag {
|
||||
padding: 4rpx 16rpx;
|
||||
background: rgba(251, 191, 36, 0.3);
|
||||
color: #fef3c7;
|
||||
border-radius: 24rpx;
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
|
||||
.coach-store {
|
||||
font-size: var(--font-xs);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* 月份切换
|
||||
* ============================================ */
|
||||
.month-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 48rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
background: #ffffff;
|
||||
border-bottom: 2rpx solid var(--color-gray-2);
|
||||
}
|
||||
|
||||
.month-btn {
|
||||
padding: 12rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.month-btn-disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.month-label {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* 统计概览
|
||||
* ============================================ */
|
||||
.stats-overview {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx;
|
||||
background: #ffffff;
|
||||
border-bottom: 2rpx solid var(--color-gray-2);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 20rpx;
|
||||
color: var(--color-gray-6);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.stat-primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.stat-success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.stat-hint {
|
||||
font-size: 20rpx;
|
||||
color: var(--color-warning);
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 2rpx;
|
||||
height: 80rpx;
|
||||
background: var(--color-gray-2);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* 记录列表
|
||||
* ============================================ */
|
||||
.records-container {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.records-card {
|
||||
background: #ffffff;
|
||||
border-radius: 32rpx;
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.date-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 20rpx 32rpx 8rpx;
|
||||
}
|
||||
|
||||
.dd-date {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-7);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dd-line {
|
||||
flex: 1;
|
||||
height: 2rpx;
|
||||
background: var(--color-gray-4);
|
||||
}
|
||||
|
||||
.dd-stats {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-6);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
|
||||
.record-avatar {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 头像渐变色 */
|
||||
.avatar-from-blue { background: linear-gradient(135deg, #60a5fa, #6366f1); }
|
||||
.avatar-from-pink { background: linear-gradient(135deg, #f472b6, #f43f5e); }
|
||||
.avatar-from-teal { background: linear-gradient(135deg, #2dd4bf, #10b981); }
|
||||
.avatar-from-green { background: linear-gradient(135deg, #4ade80, #14b8a6); }
|
||||
.avatar-from-orange { background: linear-gradient(135deg, #fb923c, #f59e0b); }
|
||||
.avatar-from-purple { background: linear-gradient(135deg, #c084fc, #8b5cf6); }
|
||||
.avatar-from-violet { background: linear-gradient(135deg, #a78bfa, #7c3aed); }
|
||||
.avatar-from-amber { background: linear-gradient(135deg, #fbbf24, #eab308); }
|
||||
|
||||
.record-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.record-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.record-name-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.record-name {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-13);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-6);
|
||||
}
|
||||
|
||||
.record-hours {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
font-variant-numeric: tabular-nums;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.record-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.course-tag {
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-basic {
|
||||
background: #ecfdf5;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.tag-vip {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.tag-tip {
|
||||
background: #fffbeb;
|
||||
color: #a16207;
|
||||
}
|
||||
|
||||
.record-location {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.record-income {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-income-val {
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-9);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationBarTitleText": "业绩详情",
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"metric-card": "/components/metric-card/metric-card",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// TODO: 联调时替换为真实 API 调用
|
||||
|
||||
/** 业绩明细项(本月/上月) */
|
||||
interface IncomeItem {
|
||||
icon: string
|
||||
label: string
|
||||
desc: string
|
||||
value: string
|
||||
}
|
||||
|
||||
/** 服务记录(按日期分组后的展示结构) */
|
||||
interface ServiceRecord {
|
||||
customerName: string
|
||||
avatarChar: string
|
||||
avatarGradient: string
|
||||
timeRange: string
|
||||
hours: string
|
||||
courseType: string
|
||||
courseTypeClass: string
|
||||
location: string
|
||||
income: string
|
||||
}
|
||||
|
||||
interface DateGroup {
|
||||
date: string
|
||||
records: ServiceRecord[]
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
|
||||
/** Banner 数据 */
|
||||
coachName: '小燕',
|
||||
coachRole: '助教',
|
||||
storeName: '广州朗朗桌球',
|
||||
monthlyIncome: '¥6,206',
|
||||
lastMonthIncome: '¥16,880',
|
||||
|
||||
/** 收入档位 */
|
||||
currentTier: {
|
||||
basicRate: 80,
|
||||
incentiveRate: 95,
|
||||
},
|
||||
nextTier: {
|
||||
basicRate: 90,
|
||||
incentiveRate: 114,
|
||||
},
|
||||
upgradeHoursNeeded: 15,
|
||||
upgradeBonus: 800,
|
||||
|
||||
/** 本月业绩明细 */
|
||||
incomeItems: [] as IncomeItem[],
|
||||
monthlyTotal: '¥6,950.5',
|
||||
|
||||
/** 服务记录 */
|
||||
thisMonthRecords: [] as DateGroup[],
|
||||
thisMonthRecordsExpanded: false,
|
||||
/** 默认显示前 N 条日期组 */
|
||||
visibleRecordGroups: 2,
|
||||
|
||||
/** 新客列表 */
|
||||
newCustomers: [] as Array<{ name: string; avatarChar: string; gradient: string; lastService: string; count: number }>,
|
||||
newCustomerExpanded: false,
|
||||
|
||||
/** 常客列表 */
|
||||
regularCustomers: [] as Array<{ name: string; avatarChar: string; gradient: string; hours: number; income: string; count: number }>,
|
||||
regularCustomerExpanded: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
loadData() {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API
|
||||
const incomeItems: IncomeItem[] = [
|
||||
{ icon: '🎱', label: '基础课', desc: '80元/h × 75h', value: '¥6,000' },
|
||||
{ icon: '⭐', label: '激励课', desc: '95.05元/h × 10h', value: '¥950.5' },
|
||||
{ icon: '💰', label: '充值激励', desc: '客户充值返佣', value: '¥500' },
|
||||
{ icon: '🏆', label: 'TOP3 销冠奖', desc: '全店业绩前三名奖励', value: '继续努力' },
|
||||
]
|
||||
|
||||
const gradients = [
|
||||
'from-blue', 'from-pink', 'from-teal', 'from-green',
|
||||
'from-orange', 'from-purple', 'from-violet', 'from-amber',
|
||||
]
|
||||
|
||||
// 模拟服务记录按日期分组
|
||||
const thisMonthRecords: DateGroup[] = [
|
||||
{
|
||||
date: '2月7日',
|
||||
records: [
|
||||
{ customerName: '王先生', avatarChar: '王', avatarGradient: gradients[0], timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '3号台', income: '¥160' },
|
||||
{ customerName: '李女士', avatarChar: '李', avatarGradient: gradients[1], timeRange: '16:00-18:00', hours: '2.0h', courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP1号房', income: '¥190' },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2月6日',
|
||||
records: [
|
||||
{ customerName: '张先生', avatarChar: '张', avatarGradient: gradients[2], timeRange: '19:00-21:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '5号台', income: '¥160' },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2月5日',
|
||||
records: [
|
||||
{ customerName: '陈女士', avatarChar: '陈', avatarGradient: gradients[2], timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: '¥160' },
|
||||
{ customerName: '赵先生', avatarChar: '赵', avatarGradient: gradients[5], timeRange: '14:00-16:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '7号台', income: '¥160' },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2月4日',
|
||||
records: [
|
||||
{ customerName: '孙先生', avatarChar: '孙', avatarGradient: gradients[6], timeRange: '19:00-21:00', hours: '2.0h', courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP2号房', income: '¥190' },
|
||||
{ customerName: '吴女士', avatarChar: '吴', avatarGradient: gradients[2], timeRange: '15:00-17:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '1号台', income: '¥160' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const newCustomers = [
|
||||
{ name: '王先生', avatarChar: '王', gradient: gradients[0], lastService: '2月7日', count: 2 },
|
||||
{ name: '李女士', avatarChar: '李', gradient: gradients[1], lastService: '2月7日', count: 1 },
|
||||
{ name: '刘先生', avatarChar: '刘', gradient: gradients[4], lastService: '2月6日', count: 1 },
|
||||
]
|
||||
|
||||
const regularCustomers = [
|
||||
{ name: '张先生', avatarChar: '张', gradient: gradients[2], hours: 12, income: '¥960', count: 6 },
|
||||
{ name: '陈女士', avatarChar: '陈', gradient: gradients[2], hours: 10, income: '¥800', count: 5 },
|
||||
{ name: '赵先生', avatarChar: '赵', gradient: gradients[5], hours: 8, income: '¥640', count: 4 },
|
||||
{ name: '孙先生', avatarChar: '孙', gradient: gradients[6], hours: 6, income: '¥570', count: 3 },
|
||||
]
|
||||
|
||||
this.setData({
|
||||
pageState: 'normal',
|
||||
incomeItems,
|
||||
thisMonthRecords,
|
||||
newCustomers,
|
||||
regularCustomers,
|
||||
})
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/** 展开/收起本月服务记录 */
|
||||
toggleThisMonthRecords() {
|
||||
this.setData({ thisMonthRecordsExpanded: !this.data.thisMonthRecordsExpanded })
|
||||
},
|
||||
|
||||
/** 查看全部 → 跳转业绩明细 */
|
||||
goToRecords() {
|
||||
wx.navigateTo({ url: '/pages/performance-records/performance-records' })
|
||||
},
|
||||
|
||||
/** 展开/收起新客列表 */
|
||||
toggleNewCustomer() {
|
||||
this.setData({ newCustomerExpanded: !this.data.newCustomerExpanded })
|
||||
},
|
||||
|
||||
/** 展开/收起常客列表 */
|
||||
toggleRegularCustomer() {
|
||||
this.setData({ regularCustomerExpanded: !this.data.regularCustomerExpanded })
|
||||
},
|
||||
|
||||
/** 点击客户卡片 → 跳转任务详情 */
|
||||
onCustomerTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const { name } = e.currentTarget.dataset
|
||||
wx.navigateTo({
|
||||
url: `/pages/task-detail/task-detail?customerName=${name}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
|
||||
/** 点击收入概览卡片 → 跳转业绩记录 */
|
||||
onIncomeCardTap() {
|
||||
wx.navigateTo({ url: '/pages/performance-records/performance-records' })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,272 @@
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<block wx:elif="{{pageState === 'empty'}}">
|
||||
<view class="page-empty">
|
||||
<t-icon name="chart-bar" size="120rpx" color="#dcdcdc" />
|
||||
<text class="empty-text">暂无业绩数据</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<!-- Banner -->
|
||||
<view class="banner-section">
|
||||
<!-- banner 背景用 CSS 渐变替代图片 -->
|
||||
<view class="banner-bg-gradient"></view>
|
||||
<view class="banner-content">
|
||||
<!-- 个人信息 -->
|
||||
<view class="coach-info">
|
||||
<view class="coach-avatar">
|
||||
<text class="avatar-emoji">👤</text>
|
||||
</view>
|
||||
<view class="coach-meta">
|
||||
<view class="coach-name-row">
|
||||
<text class="coach-name">{{coachName}}</text>
|
||||
<text class="coach-role-tag">{{coachRole}}</text>
|
||||
</view>
|
||||
<text class="coach-store">{{storeName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心收入数据 -->
|
||||
<view class="income-overview" bindtap="onIncomeCardTap">
|
||||
<view class="income-card">
|
||||
<text class="income-label">本月预计收入</text>
|
||||
<text class="income-value">{{monthlyIncome}}</text>
|
||||
</view>
|
||||
<view class="income-card">
|
||||
<text class="income-label">上月收入</text>
|
||||
<text class="income-value income-highlight">{{lastMonthIncome}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收入情况 -->
|
||||
<view class="section-card">
|
||||
<view class="section-title">
|
||||
<view class="title-dot dot-primary"></view>
|
||||
<text>收入情况</text>
|
||||
</view>
|
||||
|
||||
<!-- 当前档位 -->
|
||||
<view class="tier-card tier-current">
|
||||
<view class="tier-badge badge-current">当前档位</view>
|
||||
<view class="tier-row">
|
||||
<view class="tier-icon-label">
|
||||
<text class="tier-emoji">📊</text>
|
||||
<text class="tier-label tier-label-green">当前档位</text>
|
||||
</view>
|
||||
<view class="tier-rates">
|
||||
<view class="rate-item">
|
||||
<view class="rate-value-row">
|
||||
<text class="rate-value rate-green">{{currentTier.basicRate}}</text>
|
||||
<text class="rate-unit rate-green-light">元/h</text>
|
||||
</view>
|
||||
<text class="rate-desc rate-green-light">基础课到手</text>
|
||||
</view>
|
||||
<view class="rate-divider"></view>
|
||||
<view class="rate-item">
|
||||
<view class="rate-value-row">
|
||||
<text class="rate-value rate-green">{{currentTier.incentiveRate}}</text>
|
||||
<text class="rate-unit rate-green-light">元/h</text>
|
||||
</view>
|
||||
<text class="rate-desc rate-green-light">激励课到手</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下一阶段 -->
|
||||
<view class="tier-card tier-next">
|
||||
<view class="tier-badge badge-next">下一阶段</view>
|
||||
<view class="tier-row">
|
||||
<view class="tier-icon-label">
|
||||
<text class="tier-emoji">🎯</text>
|
||||
<text class="tier-label tier-label-yellow">下一阶段</text>
|
||||
</view>
|
||||
<view class="tier-rates">
|
||||
<view class="rate-item">
|
||||
<view class="rate-value-row">
|
||||
<text class="rate-value rate-yellow">{{nextTier.basicRate}}</text>
|
||||
<text class="rate-unit rate-yellow-light">元/h</text>
|
||||
</view>
|
||||
<text class="rate-desc rate-yellow-light">基础课到手</text>
|
||||
</view>
|
||||
<view class="rate-divider rate-divider-yellow"></view>
|
||||
<view class="rate-item">
|
||||
<view class="rate-value-row">
|
||||
<text class="rate-value rate-yellow">{{nextTier.incentiveRate}}</text>
|
||||
<text class="rate-unit rate-yellow-light">元/h</text>
|
||||
</view>
|
||||
<text class="rate-desc rate-yellow-light">激励课到手</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 升级提示 -->
|
||||
<view class="upgrade-hint">
|
||||
<view class="upgrade-left">
|
||||
<text class="upgrade-emoji">⏱️</text>
|
||||
<view class="upgrade-text">
|
||||
<text class="upgrade-label">距离下一阶段</text>
|
||||
<view class="upgrade-hours">
|
||||
<text>需完成 </text>
|
||||
<text class="upgrade-hours-num">{{upgradeHoursNeeded}}</text>
|
||||
<text> 小时</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="upgrade-bonus">
|
||||
<text class="bonus-label">到达即得</text>
|
||||
<text class="bonus-value">{{upgradeBonus}}元</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 本月业绩 -->
|
||||
<view class="section-card">
|
||||
<view class="section-title">
|
||||
<view class="title-dot dot-success"></view>
|
||||
<text>本月业绩 预估</text>
|
||||
</view>
|
||||
|
||||
<view class="income-list">
|
||||
<view class="income-row" wx:for="{{incomeItems}}" wx:key="label">
|
||||
<view class="income-row-left">
|
||||
<view class="income-icon-box">
|
||||
<text>{{item.icon}}</text>
|
||||
</view>
|
||||
<view class="income-info">
|
||||
<text class="income-item-label">{{item.label}}</text>
|
||||
<text class="income-item-desc">{{item.desc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="income-item-value">{{item.value}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 合计 -->
|
||||
<view class="income-total">
|
||||
<text class="total-label">本月合计 预估</text>
|
||||
<text class="total-value">{{monthlyTotal}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 服务记录明细 -->
|
||||
<view class="service-records-section">
|
||||
<view class="service-records-header">
|
||||
<text class="service-records-emoji">📋</text>
|
||||
<text class="service-records-title">我的服务记录明细</text>
|
||||
</view>
|
||||
|
||||
<block wx:for="{{thisMonthRecords}}" wx:key="date" wx:if="{{thisMonthRecordsExpanded || index < visibleRecordGroups}}">
|
||||
<view class="date-divider">
|
||||
<text class="dd-date">{{item.date}}</text>
|
||||
</view>
|
||||
<view class="record-item" wx:for="{{item.records}}" wx:for-item="rec" wx:key="customerName">
|
||||
<view class="record-avatar avatar-{{rec.avatarGradient}}">
|
||||
<text>{{rec.avatarChar}}</text>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<view class="record-top">
|
||||
<view class="record-name-time">
|
||||
<text class="record-name">{{rec.customerName}}</text>
|
||||
<text class="record-time">{{rec.timeRange}}</text>
|
||||
</view>
|
||||
<text class="record-hours">{{rec.hours}}</text>
|
||||
</view>
|
||||
<view class="record-bottom">
|
||||
<view class="record-tags">
|
||||
<text class="course-tag {{rec.courseTypeClass}}">{{rec.courseType}}</text>
|
||||
<text class="record-location">{{rec.location}}</text>
|
||||
</view>
|
||||
<text class="record-income">我的预估收入 <text class="record-income-val">{{rec.income}}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view class="records-toggle" bindtap="toggleThisMonthRecords" wx:if="{{thisMonthRecords.length > visibleRecordGroups}}">
|
||||
<text>{{thisMonthRecordsExpanded ? '收起' : '展开更多'}}</text>
|
||||
<t-icon name="{{thisMonthRecordsExpanded ? 'chevron-up' : 'chevron-down'}}" size="28rpx" />
|
||||
</view>
|
||||
|
||||
<view class="records-view-all" bindtap="goToRecords">
|
||||
<text>查看全部</text>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#0052d9" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的新客 -->
|
||||
<view class="section-card">
|
||||
<view class="section-title">
|
||||
<view class="title-dot dot-cyan"></view>
|
||||
<text>我的新客</text>
|
||||
</view>
|
||||
|
||||
<view class="customer-list">
|
||||
<view
|
||||
class="customer-item"
|
||||
wx:for="{{newCustomers}}"
|
||||
wx:key="name"
|
||||
wx:if="{{newCustomerExpanded || index < 2}}"
|
||||
data-name="{{item.name}}"
|
||||
bindtap="onCustomerTap"
|
||||
>
|
||||
<view class="customer-avatar avatar-{{item.gradient}}">
|
||||
<text>{{item.avatarChar}}</text>
|
||||
</view>
|
||||
<view class="customer-info">
|
||||
<text class="customer-name">{{item.name}}</text>
|
||||
<text class="customer-detail">最近服务: {{item.lastService}} · {{item.count}}次</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="toggle-btn" bindtap="toggleNewCustomer" wx:if="{{newCustomers.length > 2}}">
|
||||
<text>{{newCustomerExpanded ? '收起 ↑' : '查看更多 ↓'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的常客 -->
|
||||
<view class="section-card">
|
||||
<view class="section-title">
|
||||
<view class="title-dot dot-pink"></view>
|
||||
<text>我的常客</text>
|
||||
</view>
|
||||
|
||||
<view class="customer-list">
|
||||
<view
|
||||
class="customer-item"
|
||||
wx:for="{{regularCustomers}}"
|
||||
wx:key="name"
|
||||
wx:if="{{regularCustomerExpanded || index < 2}}"
|
||||
data-name="{{item.name}}"
|
||||
bindtap="onCustomerTap"
|
||||
>
|
||||
<view class="customer-avatar avatar-{{item.gradient}}">
|
||||
<text>{{item.avatarChar}}</text>
|
||||
</view>
|
||||
<view class="customer-info">
|
||||
<text class="customer-name">{{item.name}}</text>
|
||||
<text class="customer-detail">{{item.count}}次 · {{item.hours}}h · {{item.income}}</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#c5c5c5" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="toggle-btn" bindtap="toggleRegularCustomer" wx:if="{{regularCustomers.length > 2}}">
|
||||
<text>{{regularCustomerExpanded ? '收起 ↑' : '查看更多 ↓'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{120}}" />
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,657 @@
|
||||
/* ============================================
|
||||
* 加载态 / 空态
|
||||
* ============================================ */
|
||||
.page-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-6);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* Banner
|
||||
* ============================================ */
|
||||
.banner-section {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-bg-gradient {
|
||||
width: 100%;
|
||||
height: 480rpx;
|
||||
background: linear-gradient(135deg, #0052d9, #0080ff);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 40rpx 32rpx;
|
||||
}
|
||||
|
||||
.coach-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 40rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.coach-avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-emoji {
|
||||
font-size: 56rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.coach-meta {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.coach-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.coach-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.coach-role-tag {
|
||||
padding: 4rpx 16rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 24rpx;
|
||||
font-size: var(--font-xs);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.coach-store {
|
||||
font-size: var(--font-sm);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 收入概览卡片 */
|
||||
.income-overview {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.income-card {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.income-label {
|
||||
display: block;
|
||||
font-size: var(--font-xs);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.income-value {
|
||||
display: block;
|
||||
font-size: var(--font-2xl);
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.income-highlight {
|
||||
color: #a7f3d0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* 通用 Section 卡片
|
||||
* ============================================ */
|
||||
.section-card {
|
||||
background: #ffffff;
|
||||
border-radius: 32rpx;
|
||||
padding: 32rpx;
|
||||
margin: 24rpx 24rpx 0;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13);
|
||||
}
|
||||
|
||||
.title-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot-primary { background: var(--color-primary); }
|
||||
.dot-success { background: var(--color-success); }
|
||||
.dot-cyan { background: #06b6d4; }
|
||||
.dot-pink { background: #ec4899; }
|
||||
|
||||
/* ============================================
|
||||
* 收入档位
|
||||
* ============================================ */
|
||||
.tier-card {
|
||||
position: relative;
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.tier-current {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
||||
border: 2rpx solid #86efac;
|
||||
}
|
||||
|
||||
.tier-next {
|
||||
background: linear-gradient(135deg, #fefce8 0%, #fef9c3 100%);
|
||||
border: 2rpx solid #fde047;
|
||||
}
|
||||
|
||||
.tier-badge {
|
||||
position: absolute;
|
||||
top: -16rpx;
|
||||
right: 24rpx;
|
||||
padding: 4rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.badge-current {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
}
|
||||
|
||||
.badge-next {
|
||||
background: linear-gradient(135deg, #eab308 0%, #ca8a04 100%);
|
||||
}
|
||||
|
||||
.tier-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tier-icon-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.tier-emoji {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.tier-label {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tier-label-green { color: #15803d; }
|
||||
.tier-label-yellow { color: #a16207; }
|
||||
|
||||
.tier-rates {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.rate-item {
|
||||
text-align: center;
|
||||
width: 128rpx;
|
||||
}
|
||||
|
||||
.rate-value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.rate-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rate-unit {
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
|
||||
.rate-desc {
|
||||
font-size: 20rpx;
|
||||
margin-top: 4rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rate-green { color: #15803d; }
|
||||
.rate-green-light { color: #16a34a; }
|
||||
.rate-yellow { color: #a16207; }
|
||||
.rate-yellow-light { color: #ca8a04; }
|
||||
|
||||
.rate-divider {
|
||||
width: 2rpx;
|
||||
height: 64rpx;
|
||||
background: #bbf7d0;
|
||||
}
|
||||
|
||||
.rate-divider-yellow {
|
||||
background: #fef08a;
|
||||
}
|
||||
|
||||
/* 升级提示 */
|
||||
.upgrade-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(to right, #eff6ff, #eef2ff);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
border: 2rpx solid #bfdbfe;
|
||||
}
|
||||
|
||||
.upgrade-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.upgrade-emoji {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.upgrade-label {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-9);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.upgrade-hours {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 600;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.upgrade-hours-num {
|
||||
font-size: var(--font-base);
|
||||
}
|
||||
|
||||
.upgrade-bonus {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: linear-gradient(to right, #fbbf24, #f97316);
|
||||
color: #ffffff;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.bonus-label {
|
||||
font-size: 20rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.bonus-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* 本月业绩明细
|
||||
* ============================================ */
|
||||
.income-list {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.income-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 2rpx solid var(--color-gray-1);
|
||||
}
|
||||
|
||||
.income-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.income-row-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.income-icon-box {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
background: var(--color-gray-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.income-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.income-item-label {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-13);
|
||||
}
|
||||
|
||||
.income-item-desc {
|
||||
font-size: 20rpx;
|
||||
color: var(--color-gray-5);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.income-item-value {
|
||||
font-size: var(--font-base);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.income-total {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 16rpx;
|
||||
border-top: 2rpx solid var(--color-gray-1);
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: var(--color-success);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* 服务记录
|
||||
* ============================================ */
|
||||
.service-records-section {
|
||||
margin-top: 32rpx;
|
||||
margin: 32rpx -32rpx -32rpx;
|
||||
padding: 32rpx;
|
||||
background: rgba(243, 243, 243, 0.7);
|
||||
border-radius: 0 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.service-records-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.service-records-emoji {
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.service-records-title {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13);
|
||||
}
|
||||
|
||||
.date-divider {
|
||||
padding: 20rpx 0 8rpx;
|
||||
}
|
||||
|
||||
.dd-date {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-7);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.record-avatar {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 头像渐变色 */
|
||||
.avatar-from-blue { background: linear-gradient(135deg, #60a5fa, #6366f1); }
|
||||
.avatar-from-pink { background: linear-gradient(135deg, #f472b6, #f43f5e); }
|
||||
.avatar-from-teal { background: linear-gradient(135deg, #2dd4bf, #10b981); }
|
||||
.avatar-from-green { background: linear-gradient(135deg, #4ade80, #14b8a6); }
|
||||
.avatar-from-orange { background: linear-gradient(135deg, #fb923c, #f59e0b); }
|
||||
.avatar-from-purple { background: linear-gradient(135deg, #c084fc, #8b5cf6); }
|
||||
.avatar-from-violet { background: linear-gradient(135deg, #a78bfa, #7c3aed); }
|
||||
.avatar-from-amber { background: linear-gradient(135deg, #fbbf24, #eab308); }
|
||||
|
||||
.record-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.record-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.record-name-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.record-name {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-13);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-6);
|
||||
}
|
||||
|
||||
.record-hours {
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
font-variant-numeric: tabular-nums;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.record-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.course-tag {
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-basic {
|
||||
background: #ecfdf5;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.tag-vip {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.tag-tip {
|
||||
background: #fffbeb;
|
||||
color: #a16207;
|
||||
}
|
||||
|
||||
.record-location {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.record-income {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-income-val {
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-9);
|
||||
}
|
||||
|
||||
.records-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 16rpx 0;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
.records-view-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
padding: 12rpx 0;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* 新客 / 常客列表
|
||||
* ============================================ */
|
||||
.customer-list {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.customer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 2rpx solid var(--color-gray-1);
|
||||
}
|
||||
|
||||
.customer-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.customer-avatar {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.customer-name {
|
||||
display: block;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-13);
|
||||
}
|
||||
|
||||
.customer-detail {
|
||||
display: block;
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-6);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12rpx 0;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationBarTitleText": "审核状态",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { request } from "../../utils/request"
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
loading: true,
|
||||
status: "pending" as "pending" | "rejected",
|
||||
application: null as null | {
|
||||
id: number
|
||||
site_code: string
|
||||
role_type: string
|
||||
phone: string
|
||||
status: string
|
||||
reject_reason: string | null
|
||||
created_at: string
|
||||
},
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const { statusBarHeight } = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight })
|
||||
this.fetchStatus()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.fetchStatus()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchStatus().finally(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
async fetchStatus() {
|
||||
try {
|
||||
const data = await request({
|
||||
url: "/api/xcx/me",
|
||||
method: "GET",
|
||||
needAuth: true,
|
||||
})
|
||||
|
||||
// 同步 globalData 和 Storage
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.authUser = {
|
||||
userId: data.user_id,
|
||||
status: data.status,
|
||||
nickname: data.nickname,
|
||||
}
|
||||
wx.setStorageSync("userId", data.user_id)
|
||||
wx.setStorageSync("userStatus", data.status)
|
||||
|
||||
// 已通过 → 跳转主页
|
||||
if (data.status === "approved") {
|
||||
wx.reLaunch({ url: "/pages/mvp/mvp" })
|
||||
return
|
||||
}
|
||||
|
||||
// 已禁用 → 跳转无权限页
|
||||
if (data.status === "disabled") {
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
return
|
||||
}
|
||||
|
||||
// 已拒绝 → 跳转无权限页
|
||||
if (data.status === "rejected") {
|
||||
wx.reLaunch({ url: "/pages/no-permission/no-permission" })
|
||||
return
|
||||
}
|
||||
|
||||
// 全新用户(尚未提交申请)→ 跳回申请页
|
||||
if (data.status === "new") {
|
||||
wx.reLaunch({ url: "/pages/apply/apply" })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
loading: false,
|
||||
status: data.status,
|
||||
application: data.latest_application || null,
|
||||
})
|
||||
} catch (err: any) {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: "获取状态失败", icon: "none" })
|
||||
}
|
||||
},
|
||||
|
||||
onSwitchAccount() {
|
||||
const app = getApp<IAppOption>()
|
||||
app.globalData.token = undefined
|
||||
app.globalData.refreshToken = undefined
|
||||
app.globalData.authUser = undefined
|
||||
wx.removeStorageSync("token")
|
||||
wx.removeStorageSync("refreshToken")
|
||||
wx.removeStorageSync("userId")
|
||||
wx.removeStorageSync("userStatus")
|
||||
wx.reLaunch({ url: "/pages/login/login" })
|
||||
},
|
||||
})
|
||||
116
apps/miniprogram - 副本/miniprogram/pages/reviewing/reviewing.wxml
Normal file
116
apps/miniprogram - 副本/miniprogram/pages/reviewing/reviewing.wxml
Normal file
@@ -0,0 +1,116 @@
|
||||
<!-- pages/reviewing/reviewing.wxml — 按 H5 原型结构迁移 -->
|
||||
<view class="page" style="padding-top: {{statusBarHeight}}px;">
|
||||
<!-- 十字纹背景图案(H5 bg-pattern) -->
|
||||
<view class="bg-pattern"></view>
|
||||
<!-- 顶部渐变装饰 -->
|
||||
<view class="top-gradient top-gradient--{{status}}"></view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view class="content content--loading" wx:if="{{loading}}">
|
||||
<view class="loading-box">
|
||||
<t-loading theme="circular" size="42rpx" text="加载中..." />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="content" wx:else>
|
||||
<!-- 图标区域 -->
|
||||
<view class="icon-area float-animation">
|
||||
<!-- 背景光晕 -->
|
||||
<view class="icon-glow icon-glow--{{status}}"></view>
|
||||
<!-- 主图标 -->
|
||||
<view class="icon-box icon-box--{{status}}">
|
||||
<image wx:if="{{status === 'pending'}}" class="icon-main-img" src="/assets/icons/icon-clock-circle.svg" mode="aspectFit" />
|
||||
<t-icon wx:else name="close-circle" size="98rpx" color="#fff" />
|
||||
</view>
|
||||
<!-- 装饰点 -->
|
||||
<view class="icon-dot icon-dot--{{status}} dot-1 pulse-soft"></view>
|
||||
<view class="icon-dot icon-dot--{{status}} dot-2 pulse-soft"></view>
|
||||
</view>
|
||||
|
||||
<!-- 标题区域 -->
|
||||
<view class="title-area">
|
||||
<text class="main-title">{{status === 'pending' ? '申请审核中' : '申请未通过'}}</text>
|
||||
<text class="sub-title" wx:if="{{status === 'pending'}}">您的访问申请已提交成功,正在等待管理员审核,请耐心等待</text>
|
||||
<text class="sub-title" wx:else>很抱歉,您的申请未通过审核</text>
|
||||
</view>
|
||||
|
||||
<!-- 进度提示卡片(仅审核中显示) -->
|
||||
<view class="progress-card" wx:if="{{status === 'pending'}}">
|
||||
<view class="progress-header">
|
||||
<view class="progress-icon-box">
|
||||
<t-icon name="info-circle-filled" size="35rpx" color="#ed7b2f" />
|
||||
</view>
|
||||
<view class="progress-header-text">
|
||||
<text class="progress-title">审核进度</text>
|
||||
<text class="progress-desc">通常需要 1-3 个工作日</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 进度步骤 -->
|
||||
<view class="progress-steps">
|
||||
<view class="step-group">
|
||||
<view class="step-dot step-dot--done">
|
||||
<t-icon name="check" size="28rpx" color="#fff" />
|
||||
</view>
|
||||
<text class="step-label step-label--done">已提交</text>
|
||||
</view>
|
||||
<view class="step-line">
|
||||
<view class="step-line-fill"></view>
|
||||
</view>
|
||||
<view class="step-group">
|
||||
<view class="step-dot step-dot--active">
|
||||
<view class="step-dot-inner pulse-soft"></view>
|
||||
</view>
|
||||
<text class="step-label step-label--active">审核中</text>
|
||||
</view>
|
||||
<view class="step-line"></view>
|
||||
<view class="step-group">
|
||||
<view class="step-dot step-dot--pending"></view>
|
||||
<text class="step-label step-label--pending">通过</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拒绝原因卡片 -->
|
||||
<view class="reject-card" wx:if="{{status === 'rejected' && application && application.reject_reason}}">
|
||||
<view class="reject-header">
|
||||
<t-icon name="error-circle-filled" size="32rpx" color="#e34d59" />
|
||||
<text class="reject-title">拒绝原因</text>
|
||||
</view>
|
||||
<text class="reject-reason">{{application.reject_reason}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 申请信息摘要 -->
|
||||
<view class="info-card" wx:if="{{application}}">
|
||||
<text class="info-card-title">申请信息</text>
|
||||
<view class="info-row">
|
||||
<text class="info-label">球房ID</text>
|
||||
<text class="info-value">{{application.site_code}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">申请身份</text>
|
||||
<text class="info-value">{{application.role_type}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">手机号</text>
|
||||
<text class="info-value">{{application.phone}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系提示 -->
|
||||
<view class="contact-hint">
|
||||
<t-icon name="chat" size="28rpx" color="#a6a6a6" />
|
||||
<text class="contact-text">如有疑问,请联系管理员</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮区域 -->
|
||||
<view class="bottom-area" wx:if="{{!loading}}">
|
||||
<view class="switch-btn" bindtap="onSwitchAccount">
|
||||
<t-icon name="logout" size="28rpx" color="#5e5e5e" />
|
||||
<text class="switch-btn-text">更换登录账号</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<dev-fab />
|
||||
427
apps/miniprogram - 副本/miniprogram/pages/reviewing/reviewing.wxss
Normal file
427
apps/miniprogram - 副本/miniprogram/pages/reviewing/reviewing.wxss
Normal file
@@ -0,0 +1,427 @@
|
||||
/* pages/reviewing/reviewing.wxss — 按 H5 原型精确转换 */
|
||||
|
||||
/* 一屏页面:height + box-sizing 确保 padding 不增加总高度 */
|
||||
.page {
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f8fafc;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* H5: bg-pattern 十字纹背景 — 用 WXML view + repeating-linear-gradient 模拟 */
|
||||
/* 背景纹理间距不缩放,保持原值 */
|
||||
.bg-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
repeating-linear-gradient(0deg, transparent, transparent 58rpx, rgba(0,82,217,0.03) 58rpx, rgba(0,82,217,0.03) 62rpx, transparent 62rpx, transparent 120rpx),
|
||||
repeating-linear-gradient(90deg, transparent, transparent 58rpx, rgba(0,82,217,0.03) 58rpx, rgba(0,82,217,0.03) 62rpx, transparent 62rpx, transparent 120rpx);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---- 顶部渐变装饰 ---- */
|
||||
.top-gradient {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 448rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* H5: from-warning/10 = rgba(237,123,47,0.10) */
|
||||
.top-gradient--pending {
|
||||
background: linear-gradient(to bottom, rgba(237, 123, 47, 0.10), transparent);
|
||||
}
|
||||
|
||||
.top-gradient--rejected {
|
||||
background: linear-gradient(to bottom, rgba(227, 77, 89, 0.1), transparent);
|
||||
}
|
||||
|
||||
/* ---- 主体内容 ---- */
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 56rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.content--loading {
|
||||
justify-content: flex-start;
|
||||
padding-top: 350rpx;
|
||||
}
|
||||
|
||||
/* ---- 加载中 ---- */
|
||||
.loading-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ---- 图标区域 ---- */
|
||||
.icon-area {
|
||||
position: relative;
|
||||
margin-bottom: 56rpx;
|
||||
}
|
||||
|
||||
/* H5: w-32 h-32 bg-warning/20 rounded-full blur-xl — 小程序不支持 filter:blur,用更大尺寸 + radial-gradient 模拟 */
|
||||
.icon-glow {
|
||||
position: absolute;
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
border-radius: 50%;
|
||||
top: -42rpx;
|
||||
left: -42rpx;
|
||||
}
|
||||
|
||||
.icon-glow--pending {
|
||||
background: radial-gradient(circle, rgba(237, 123, 47, 0.18) 0%, rgba(237, 123, 47, 0.06) 50%, transparent 75%);
|
||||
}
|
||||
|
||||
.icon-glow--rejected {
|
||||
background: radial-gradient(circle, rgba(227, 77, 89, 0.18) 0%, rgba(227, 77, 89, 0.06) 50%, transparent 75%);
|
||||
}
|
||||
|
||||
/* H5: w-28 h-28 = 112px = 224rpx, rounded-3xl = 48rpx */
|
||||
.icon-box {
|
||||
position: relative;
|
||||
width: 196rpx;
|
||||
height: 196rpx;
|
||||
border-radius: 42rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* H5: bg-gradient-to-br from-amber-400 to-orange-500, shadow-xl shadow-warning/30 */
|
||||
.icon-box--pending {
|
||||
background: linear-gradient(135deg, #f59e0b, #f97316);
|
||||
box-shadow: 0 14rpx 42rpx rgba(237, 123, 47, 0.3);
|
||||
}
|
||||
|
||||
.icon-box--rejected {
|
||||
background: linear-gradient(135deg, #f87171, #e34d59);
|
||||
box-shadow: 0 14rpx 42rpx rgba(227, 77, 89, 0.3);
|
||||
}
|
||||
|
||||
/* 主图标图片 — H5: w-14 h-14 = 56px = 112rpx */
|
||||
.icon-main-img {
|
||||
width: 98rpx;
|
||||
height: 98rpx;
|
||||
}
|
||||
|
||||
.icon-dot {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* dot-1: bg-warning = #ed7b2f; dot-2: bg-amber-300 = #fcd34d */
|
||||
.icon-dot--pending.dot-1 { background: #ed7b2f; }
|
||||
.icon-dot--pending.dot-2 { background: #fcd34d; }
|
||||
.icon-dot--rejected { background: #e34d59; }
|
||||
|
||||
/* H5: -top-2 -right-2 w-4 h-4 = 32rpx */
|
||||
.dot-1 {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
top: -14rpx;
|
||||
right: -14rpx;
|
||||
}
|
||||
|
||||
/* H5: -bottom-1 -left-3 w-3 h-3 = 24rpx, opacity 0.7, animation-delay 0.5s */
|
||||
.dot-2 {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
bottom: -8rpx;
|
||||
left: -22rpx;
|
||||
opacity: 0.7;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
/* ---- 标题区域 ---- */
|
||||
/* H5: text-center mb-8 = 64rpx */
|
||||
.title-area {
|
||||
text-align: center;
|
||||
margin-bottom: 56rpx;
|
||||
}
|
||||
|
||||
/* H5: text-2xl = 48rpx, font-bold = 700, text-gray-10 = #4b4b4b, mb-3 = 24rpx */
|
||||
.main-title {
|
||||
display: block;
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
color: #4b4b4b;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
/* H5: text-sm = 28rpx, text-gray-7 = #8b8b8b, leading-relaxed = 1.625, max-w-xs ≈ 640rpx */
|
||||
.sub-title {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #8b8b8b;
|
||||
line-height: 1.625;
|
||||
max-width: 560rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ---- 进度提示卡片 ---- */
|
||||
/* H5: w-full max-w-sm bg-white rounded-2xl p-5 shadow-lg shadow-gray-200/50 mb-6 */
|
||||
.progress-card {
|
||||
width: 100%;
|
||||
max-width: 482rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 36rpx;
|
||||
margin-bottom: 42rpx;
|
||||
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* H5: flex items-center gap-4 = 32rpx mb-4 = 32rpx */
|
||||
.progress-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
/* H5: w-10 h-10 = 80rpx, bg-warning/10, rounded-xl = 24rpx */
|
||||
.progress-icon-box {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
min-width: 70rpx;
|
||||
background: rgba(237, 123, 47, 0.1);
|
||||
border-radius: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.progress-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* H5: text-sm = 28rpx, font-medium = 500, text-gray-13 = #242424 */
|
||||
.progress-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #242424;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
/* H5: text-xs = 24rpx, text-gray-6 = #a6a6a6 */
|
||||
.progress-desc {
|
||||
font-size: 22rpx;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
/* ---- 进度步骤 ---- */
|
||||
/* H5: flex items-center gap-3 = 24rpx */
|
||||
.progress-steps {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 22rpx;
|
||||
}
|
||||
|
||||
.step-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* H5: w-6 h-6 = 48rpx */
|
||||
.step-dot {
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-dot--done { background: #00a870; }
|
||||
.step-dot--active { background: rgba(237, 123, 47, 0.2); }
|
||||
.step-dot--pending { background: #f3f3f3; }
|
||||
|
||||
/* H5: w-2 h-2 = 16rpx */
|
||||
.step-dot-inner {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
background: #ed7b2f;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* H5: text-xs = 24rpx */
|
||||
.step-label {
|
||||
font-size: 22rpx;
|
||||
color: #c5c5c5;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.step-label--done { color: #5e5e5e; }
|
||||
.step-label--active { color: #a6a6a6; }
|
||||
.step-label--pending { color: #c5c5c5; }
|
||||
|
||||
/* H5: flex-1 h-0.5 = 4rpx, bg-gray-200 = #eeeeee, gap-3 = 24rpx 两侧 */
|
||||
.step-line {
|
||||
flex: 1;
|
||||
height: 4rpx;
|
||||
background: #eeeeee;
|
||||
border-radius: 2rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* H5: w-1/2 bg-warning */
|
||||
.step-line-fill {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: #ed7b2f;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
/* ---- 拒绝原因卡片 ---- */
|
||||
.reject-card {
|
||||
width: 100%;
|
||||
background: #fff5f5;
|
||||
border: 2rpx solid rgba(227, 77, 89, 0.15);
|
||||
border-radius: 28rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.reject-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
|
||||
.reject-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #e34d59;
|
||||
}
|
||||
|
||||
.reject-reason {
|
||||
font-size: 22rpx;
|
||||
color: #5e5e5e;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
/* ---- 申请信息卡片 ---- */
|
||||
.info-card {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 22rpx;
|
||||
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.info-card-title {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #242424;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14rpx 0;
|
||||
border-bottom: 2rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
|
||||
.info-label {
|
||||
font-size: 24rpx;
|
||||
color: #8b8b8b;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 24rpx;
|
||||
color: #242424;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---- 联系提示 ---- */
|
||||
/* H5: flex items-center gap-2 text-gray-6 mb-8 */
|
||||
.contact-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
margin-bottom: 56rpx;
|
||||
}
|
||||
|
||||
/* H5: text-xs = 24rpx */
|
||||
.contact-text {
|
||||
font-size: 22rpx;
|
||||
color: #a6a6a6;
|
||||
}
|
||||
|
||||
/* ---- 底部按钮区域 ---- */
|
||||
/* H5: px-8 = 64rpx, pb-12 = 96rpx */
|
||||
.bottom-area {
|
||||
padding: 0 56rpx 84rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 原生按钮替代 TDesign — H5: w-full py-3.5=56rpx bg-white border border-gray-200 rounded-xl */
|
||||
.switch-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14rpx;
|
||||
width: 100%;
|
||||
padding: 24rpx 0;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #eeeeee;
|
||||
border-radius: 22rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.switch-btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #5e5e5e;
|
||||
}
|
||||
|
||||
/* ---- 动画 ---- */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-18rpx); }
|
||||
}
|
||||
|
||||
.float-animation {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-soft {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.pulse-soft {
|
||||
animation: pulse-soft 2s ease-in-out infinite;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigationBarTitleText": "任务详情",
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"note-modal": "/components/note-modal/note-modal",
|
||||
"star-rating": "/components/star-rating/star-rating",
|
||||
"heart-icon": "/components/heart-icon/heart-icon",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { mockTaskDetails } from '../../utils/mock-data'
|
||||
import type { TaskDetail, Note } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
detail: null as TaskDetail | null,
|
||||
sortedNotes: [] as Note[],
|
||||
noteModalVisible: false,
|
||||
/** 回访专属:话术列表 */
|
||||
talkingPoints: [
|
||||
'赵姐您好!上次打球感觉怎么样?新到的球杆手感还习惯吗?这周末您有空的话,可以提前帮您预留老位置~',
|
||||
'赵姐,最近店里新进了一批斯诺克专用巧克粉,手感特别好,下次来的时候可以试试~',
|
||||
'赵姐好呀,上次您说想学几个高级杆法,我最近整理了一些教学视频,要不要发给您先看看?',
|
||||
'赵姐,这周六下午VIP包厢有空位,要不要帮您提前预留?可以叫上朋友一起来打球~',
|
||||
'赵姐您好,我们下个月有个会员积分兑换活动,您的积分可以换不少好东西,到时候提醒您哦~',
|
||||
],
|
||||
/** 近期服务记录 */
|
||||
serviceRecords: [
|
||||
{ table: 'VIP2号房', type: '基础课', typeClass: 'basic', duration: '2.0h', income: '¥190', drinks: '🍷 红牛x2 花生米x1', date: '2026-02-04 15:00' },
|
||||
{ table: '8号台', type: '激励课', typeClass: 'incentive', duration: '1.5h', income: '¥120', drinks: '🍷 可乐x2', date: '2026-01-30 16:30' },
|
||||
{ table: 'VIP2号房', type: '基础课', typeClass: 'basic', duration: '2.5h', income: '¥200', drinks: '🍷 百威x3 薯条x1', date: '2026-01-25 14:00' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options?.id || ''
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
loadData(id: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API
|
||||
const detail = mockTaskDetails.find((t) => t.id === id) || mockTaskDetails[0]
|
||||
if (!detail) {
|
||||
this.setData({ pageState: 'empty' })
|
||||
return
|
||||
}
|
||||
const sorted = sortByTimestamp(detail.notes || []) as Note[]
|
||||
this.setData({ pageState: 'normal', detail, sortedNotes: sorted })
|
||||
}, 500)
|
||||
},
|
||||
|
||||
onAddNote() {
|
||||
this.setData({ noteModalVisible: true })
|
||||
},
|
||||
|
||||
onNoteConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
const { score, content } = e.detail
|
||||
wx.showToast({ title: '备注已保存', icon: 'success' })
|
||||
this.setData({ noteModalVisible: false })
|
||||
const newNote: Note = {
|
||||
id: `note-${Date.now()}`,
|
||||
content,
|
||||
tagType: 'customer',
|
||||
tagLabel: `客户:${this.data.detail?.customerName || ''}`,
|
||||
createdAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
||||
}
|
||||
this.setData({ sortedNotes: [newNote, ...this.data.sortedNotes] })
|
||||
},
|
||||
|
||||
onNoteCancel() {
|
||||
this.setData({ noteModalVisible: false })
|
||||
},
|
||||
|
||||
/** 问问助手 */
|
||||
onAskAssistant() {
|
||||
const customerId = this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/chat/chat?customerId=${customerId}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
|
||||
/** 记录回访 */
|
||||
onRecordCallback() {
|
||||
this.setData({ noteModalVisible: true })
|
||||
},
|
||||
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
/** 查看全部服务记录 */
|
||||
onViewAllRecords() {
|
||||
const customerId = this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/customer-service-records/customer-service-records?customerId=${customerId}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,203 @@
|
||||
<wxs src="../../utils/format.wxs" module="fmt" />
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-icon name="info-circle" size="120rpx" color="#c5c5c5" />
|
||||
<text class="empty-text">未找到任务信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- Banner - 青绿色主题 -->
|
||||
<view class="banner-area banner-teal">
|
||||
<view class="banner-nav">
|
||||
<view class="nav-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="48rpx" color="#ffffff" />
|
||||
</view>
|
||||
<text class="nav-title">任务详情</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view class="customer-info">
|
||||
<view class="avatar-box">
|
||||
<text class="avatar-text">{{detail.customerName[0] || '?'}}</text>
|
||||
</view>
|
||||
<view class="info-right">
|
||||
<view class="name-row">
|
||||
<text class="customer-name">{{detail.customerName}}</text>
|
||||
<text class="task-type-tag">客户回访</text>
|
||||
</view>
|
||||
<view class="sub-info">
|
||||
<text class="phone">135****6677</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="main-content">
|
||||
<!-- ① 维客线索(回访页面排第一) -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-green">维客线索</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="clue-list">
|
||||
<view class="clue-item">
|
||||
<view class="clue-tag clue-tag-primary">客户<text>\n</text>基础</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">👩 女性 · VIP会员 · 入会1年半 · 忠实老客户</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="clue-item">
|
||||
<view class="clue-tag clue-tag-success">消费<text>\n</text>习惯</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">☀️ 偏好周末下午 · 月均6-8次</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="clue-item clue-item-detail">
|
||||
<view class="clue-tag clue-tag-success">消费<text>\n</text>习惯</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">💰 高客单价 · 爱点酒水</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
<text class="clue-detail">场均消费 ¥420,高于门店均值 ¥180;酒水小食附加消费占比 40%,偏好VIP包厢</text>
|
||||
</view>
|
||||
<view class="clue-item clue-item-detail">
|
||||
<view class="clue-tag clue-tag-purple">玩法<text>\n</text>偏好</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">🎱 斯诺克爱好者 · 技术中上 · 喜欢研究杆法</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
<text class="clue-detail">斯诺克占比 85%,偶尔玩中式八球;对高级杆法有浓厚兴趣</text>
|
||||
</view>
|
||||
<view class="clue-item clue-item-detail">
|
||||
<view class="clue-tag clue-tag-error">重要<text>\n</text>反馈</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">⭐ 上次服务好评,新球杆手感满意</text>
|
||||
<text class="clue-source">By:小燕</text>
|
||||
</view>
|
||||
<text class="clue-detail">2月4日到店时表示新球杆手感很好,希望下次能提前预留VIP包厢</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ② 与我的关系(含近期服务记录) -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-blue">与我的关系</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="relationship-row">
|
||||
<view class="rel-tag rel-tag-pink">
|
||||
<heart-icon score="{{detail.heartScore}}" />
|
||||
<text>{{detail.heartScore > 8.5 ? '非常好' : detail.heartScore > 7 ? '良好' : detail.heartScore > 5 ? '一般' : '待发展'}}</text>
|
||||
</view>
|
||||
<view class="rel-bar">
|
||||
<view class="rel-bar-fill" style="width: {{detail.heartScore * 10}}%"></view>
|
||||
</view>
|
||||
<text class="rel-score">{{fmt.toFixed(detail.heartScore / 10, 2)}}</text>
|
||||
</view>
|
||||
<text class="card-desc">长期合作关系良好,共有 45 次服务记录。客户多次指定您服务,评价均为 5 星。</text>
|
||||
|
||||
<!-- 近期服务记录(嵌入关系卡片内) -->
|
||||
<view class="svc-section">
|
||||
<text class="svc-title">📋 近期服务记录</text>
|
||||
<view class="svc-record" wx:for="{{serviceRecords}}" wx:key="date">
|
||||
<view class="svc-row-top">
|
||||
<view class="svc-tags">
|
||||
<text class="svc-table">{{item.table}}</text>
|
||||
<text class="svc-type svc-type-{{item.typeClass}}">{{item.type}}</text>
|
||||
<text class="svc-duration">{{item.duration}}</text>
|
||||
</view>
|
||||
<text class="svc-income">{{item.income}}</text>
|
||||
</view>
|
||||
<view class="svc-row-bottom">
|
||||
<text class="svc-drinks">{{item.drinks}}</text>
|
||||
<text class="svc-date">{{item.date}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="svc-more" bindtap="onViewAllRecords">
|
||||
<text>查看全部服务记录 →</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ③ 任务建议 -->
|
||||
<view class="card">
|
||||
<text class="section-title title-purple">任务建议</text>
|
||||
<view class="suggestion-box suggestion-teal">
|
||||
<view class="suggestion-header">
|
||||
<text class="suggestion-icon">📞 常规回访要点</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<text class="suggestion-desc">该客户上次到店是 3 天前,关系良好,进行常规关怀回访:</text>
|
||||
<view class="suggestion-list">
|
||||
<text class="suggestion-item">• 询问上次体验是否满意,是否有改进建议</text>
|
||||
<text class="suggestion-item">• 告知近期新到的斯诺克相关设备或活动</text>
|
||||
<text class="suggestion-item">• 提前预约下次到店时间,提供专属服务</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 话术参考(竖线样式) -->
|
||||
<view class="talking-section">
|
||||
<view class="talking-header">
|
||||
<text class="talking-title">💬 话术参考</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="talking-list">
|
||||
<text class="talking-item" wx:for="{{talkingPoints}}" wx:key="index">"{{item}}"</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ④ 我给TA的备注 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-blue">我给TA的备注</text>
|
||||
<text class="note-count">{{sortedNotes.length}} 条备注</text>
|
||||
</view>
|
||||
<block wx:if="{{sortedNotes.length > 0}}">
|
||||
<view class="note-item" wx:for="{{sortedNotes}}" wx:key="id">
|
||||
<view class="note-top">
|
||||
<text class="note-date">{{item.createdAt}}</text>
|
||||
<text class="note-tag-inline {{item.tagType === 'coach' ? 'tag-coach-inline' : 'tag-customer-inline'}}">{{item.tagLabel}}</text>
|
||||
</view>
|
||||
<text class="note-content">{{item.content}}</text>
|
||||
</view>
|
||||
</block>
|
||||
<view class="note-empty" wx:else>
|
||||
<t-icon name="edit-1" size="80rpx" color="#dcdcdc" />
|
||||
<text class="empty-hint">暂无备注</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar safe-area-bottom">
|
||||
<view class="btn-ask btn-ask-teal" bindtap="onAskAssistant">
|
||||
<t-icon name="chat" size="40rpx" color="#ffffff" />
|
||||
<text>问问助手</text>
|
||||
</view>
|
||||
<view class="btn-note" bindtap="onAddNote">
|
||||
<t-icon name="edit-1" size="40rpx" color="#242424" />
|
||||
<text>备注</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<note-modal
|
||||
visible="{{noteModalVisible}}"
|
||||
customerName="{{detail.customerName}}"
|
||||
initialScore="{{0}}"
|
||||
initialContent=""
|
||||
bind:confirm="onNoteConfirm"
|
||||
bind:cancel="onNoteCancel"
|
||||
/>
|
||||
|
||||
<ai-float-button bottom="{{200}}" customerId="{{detail.id}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,458 @@
|
||||
/* 加载态 & 空态 */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* Banner - 青绿色 */
|
||||
.banner-area {
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
padding-bottom: 48rpx;
|
||||
}
|
||||
.banner-teal {
|
||||
background: linear-gradient(135deg, #14b8a6 0%, #06b6d4 100%);
|
||||
}
|
||||
.banner-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.nav-back { padding: 8rpx; }
|
||||
.nav-title {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 500;
|
||||
}
|
||||
.nav-placeholder { width: 48rpx; }
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
padding: 16rpx 40rpx 0;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.avatar-text {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
.info-right { flex: 1; }
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.customer-name {
|
||||
font-size: var(--font-xl, 40rpx);
|
||||
font-weight: 600;
|
||||
}
|
||||
.task-type-tag {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
padding: 4rpx 16rpx;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.sub-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.phone {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 主体 */
|
||||
.main-content {
|
||||
padding: 32rpx;
|
||||
padding-bottom: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
/* 卡片 */
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-xl, 32rpx);
|
||||
padding: 40rpx;
|
||||
box-shadow: var(--shadow-lg, 0 8rpx 32rpx rgba(0,0,0,0.06));
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.title-green { border-left: 6rpx solid #00a870; padding-left: 16rpx; }
|
||||
.title-blue { border-left: 6rpx solid #0052d9; padding-left: 16rpx; }
|
||||
.title-purple { border-left: 6rpx solid #7c3aed; padding-left: 16rpx; margin-bottom: 32rpx; }
|
||||
|
||||
.ai-badge {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-primary, #0052d9);
|
||||
background: var(--color-primary-light, #ecf2fe);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
|
||||
/* 维客线索 */
|
||||
.clue-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.clue-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
}
|
||||
.clue-item-detail {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.clue-tag {
|
||||
flex-shrink: 0;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
border-radius: 8rpx;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.clue-tag-primary { background: rgba(0, 82, 217, 0.1); color: #0052d9; }
|
||||
.clue-tag-success { background: rgba(0, 168, 112, 0.1); color: #00a870; }
|
||||
.clue-tag-purple { background: rgba(124, 58, 237, 0.1); color: #7c3aed; }
|
||||
.clue-tag-error { background: rgba(227, 77, 89, 0.1); color: #e34d59; }
|
||||
|
||||
.clue-body {
|
||||
flex: 1;
|
||||
min-height: 80rpx;
|
||||
position: relative;
|
||||
}
|
||||
.clue-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-13, #242424);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.clue-source {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
.clue-detail {
|
||||
width: 100%;
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-8, #777777);
|
||||
line-height: 1.6;
|
||||
margin-top: 8rpx;
|
||||
padding-left: 104rpx;
|
||||
}
|
||||
|
||||
/* 关系区域 */
|
||||
.relationship-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.rel-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rel-tag-pink { background: linear-gradient(135deg, #ec4899, #f43f5e); }
|
||||
.rel-bar {
|
||||
flex: 1;
|
||||
height: 12rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: 100rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rel-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #f9a8d4, #f43f5e);
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.rel-score {
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 700;
|
||||
color: #ec4899;
|
||||
}
|
||||
.card-desc {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-8, #777777);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* 近期服务记录 */
|
||||
.svc-section {
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.svc-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
margin-bottom: 24rpx;
|
||||
display: block;
|
||||
}
|
||||
.svc-record {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
}
|
||||
.svc-record:last-of-type { border-bottom: none; }
|
||||
.svc-row-top, .svc-row-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.svc-row-top { margin-bottom: 8rpx; }
|
||||
.svc-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.svc-table {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
background: #ffffff;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.svc-type {
|
||||
font-size: 22rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
.svc-type-basic { background: #0052d9; }
|
||||
.svc-type-incentive { background: #ed7b2f; }
|
||||
.svc-duration {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.svc-income {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.svc-drinks {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.svc-date {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.svc-more {
|
||||
text-align: center;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.svc-more text {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-primary, #0052d9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 任务建议 */
|
||||
.suggestion-box {
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 32rpx;
|
||||
border: 1rpx solid;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.suggestion-teal {
|
||||
background: linear-gradient(135deg, #f0fdfa, #ecfeff);
|
||||
border-color: #99f6e4;
|
||||
}
|
||||
.suggestion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.suggestion-icon {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: #0d9488;
|
||||
}
|
||||
.suggestion-desc {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
.suggestion-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.suggestion-item {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 话术参考(竖线样式) */
|
||||
.talking-section {
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 32rpx;
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
}
|
||||
.talking-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.talking-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.talking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.talking-item {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
padding-left: 24rpx;
|
||||
border-left: 4rpx solid rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
|
||||
/* 备注 */
|
||||
.note-count {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.note-item {
|
||||
padding: 28rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.note-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.note-date {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.note-content {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.note-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.empty-hint {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 128rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 0 32rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
.btn-ask {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
}
|
||||
.btn-ask-teal {
|
||||
background: linear-gradient(135deg, #14b8a6, #06b6d4);
|
||||
box-shadow: 0 8rpx 24rpx rgba(20, 184, 166, 0.3);
|
||||
}
|
||||
.btn-note {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigationBarTitleText": "任务详情",
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"note-modal": "/components/note-modal/note-modal",
|
||||
"star-rating": "/components/star-rating/star-rating",
|
||||
"heart-icon": "/components/heart-icon/heart-icon",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { mockTaskDetails } from '../../utils/mock-data'
|
||||
import type { TaskDetail, Note } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
detail: null as TaskDetail | null,
|
||||
sortedNotes: [] as Note[],
|
||||
noteModalVisible: false,
|
||||
/** 话术列表(竖线样式) */
|
||||
talkingPoints: [
|
||||
'张哥,好久没见您来打球了,最近忙吗?店里这周六有个球友聚会活动,想邀请您来玩,顺便认识一些新球友~',
|
||||
'张哥好呀,最近工作还顺利吧?周末有空的话过来放松一下,我帮您约几个水平差不多的球友一起切磋~',
|
||||
'张哥,店里最近新上了几款精酿啤酒,打完球来一杯特别爽,周末要不要来试试?',
|
||||
'张哥,上次您说想练练组合球,我最近研究了几个不错的训练方法,下次来的时候教您~',
|
||||
'张哥您好,这个月会员充值有额外赠送活动,力度挺大的,要不要了解一下?',
|
||||
],
|
||||
/** 近期服务记录 */
|
||||
serviceRecords: [
|
||||
{ table: '5号台', type: '基础课', typeClass: 'basic', duration: '2.0h', income: '¥160', drinks: '🍷 雪花x2 矿泉水x1', date: '2026-02-06 19:00' },
|
||||
{ table: 'A08号台', type: '激励课', typeClass: 'incentive', duration: '1.5h', income: '¥150', drinks: '🍷 百威x1', date: '2026-01-20 20:30' },
|
||||
{ table: '3号台', type: '基础课', typeClass: 'basic', duration: '2.0h', income: '¥160', drinks: '🍷 可乐x2 红牛x1', date: '2026-01-05 21:00' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options?.id || ''
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
loadData(id: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API
|
||||
const detail = mockTaskDetails.find((t) => t.id === id) || mockTaskDetails[1]
|
||||
if (!detail) {
|
||||
this.setData({ pageState: 'empty' })
|
||||
return
|
||||
}
|
||||
const sorted = sortByTimestamp(detail.notes || []) as Note[]
|
||||
this.setData({ pageState: 'normal', detail, sortedNotes: sorted })
|
||||
}, 500)
|
||||
},
|
||||
|
||||
onAddNote() {
|
||||
this.setData({ noteModalVisible: true })
|
||||
},
|
||||
|
||||
onNoteConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
const { score, content } = e.detail
|
||||
wx.showToast({ title: '备注已保存', icon: 'success' })
|
||||
this.setData({ noteModalVisible: false })
|
||||
const newNote: Note = {
|
||||
id: `note-${Date.now()}`,
|
||||
content,
|
||||
tagType: 'customer',
|
||||
tagLabel: `客户:${this.data.detail?.customerName || ''}`,
|
||||
createdAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
||||
}
|
||||
this.setData({ sortedNotes: [newNote, ...this.data.sortedNotes] })
|
||||
},
|
||||
|
||||
onNoteCancel() {
|
||||
this.setData({ noteModalVisible: false })
|
||||
},
|
||||
|
||||
onAskAssistant() {
|
||||
const customerId = this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/chat/chat?customerId=${customerId}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
|
||||
/** 记录联系 */
|
||||
onRecordContact() {
|
||||
this.setData({ noteModalVisible: true })
|
||||
},
|
||||
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onViewAllRecords() {
|
||||
const customerId = this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/customer-service-records/customer-service-records?customerId=${customerId}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,204 @@
|
||||
<wxs src="../../utils/format.wxs" module="fmt" />
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-icon name="info-circle" size="120rpx" color="#c5c5c5" />
|
||||
<text class="empty-text">未找到任务信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- Banner - 橙色主题 -->
|
||||
<view class="banner-area banner-orange">
|
||||
<view class="banner-nav">
|
||||
<view class="nav-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="48rpx" color="#ffffff" />
|
||||
</view>
|
||||
<text class="nav-title">任务详情</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view class="customer-info">
|
||||
<view class="avatar-box">
|
||||
<text class="avatar-text">{{detail.customerName[0] || '?'}}</text>
|
||||
</view>
|
||||
<view class="info-right">
|
||||
<view class="name-row">
|
||||
<text class="customer-name">{{detail.customerName}}</text>
|
||||
<text class="task-type-tag">优先召回</text>
|
||||
</view>
|
||||
<view class="sub-info">
|
||||
<text class="phone">139****1234</text>
|
||||
<block wx:if="{{detail.daysAbsent}}">
|
||||
<text class="absent-info">⚠️ {{detail.daysAbsent}}天未到店</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="main-content">
|
||||
<!-- ① 维客线索 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-green">维客线索</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="clue-list">
|
||||
<view class="clue-item">
|
||||
<view class="clue-tag clue-tag-primary">客户<text>\n</text>基础</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">👤 普通会员 · 注册10个月 · 近期活跃度下降</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="clue-item">
|
||||
<view class="clue-tag clue-tag-success">消费<text>\n</text>习惯</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">🌙 偏好夜场 20:00-23:00 · 之前月均3-4次</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="clue-item clue-item-detail">
|
||||
<view class="clue-tag clue-tag-success">消费<text>\n</text>习惯</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">📉 频率下降 · 爱组局</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
<text class="clue-detail">场均消费 ¥220,近月到店仅 1 次(之前月均 3-4 次);喜欢和朋友组局</text>
|
||||
</view>
|
||||
<view class="clue-item clue-item-detail">
|
||||
<view class="clue-tag clue-tag-purple">玩法<text>\n</text>偏好</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">🎱 中式八球为主 · 喜欢组局对战 · 想练组合球</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
<text class="clue-detail">中式八球占比 90%,技术水平中等;喜欢 3-4 人组局对战</text>
|
||||
</view>
|
||||
<view class="clue-item clue-item-detail">
|
||||
<view class="clue-tag clue-tag-error">重要<text>\n</text>反馈</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">⚠️ 换了工作,下班时间不固定</text>
|
||||
<text class="clue-source">By:小燕</text>
|
||||
</view>
|
||||
<text class="clue-detail">2月3日沟通时提到最近换了工作,周末可能更方便</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ② 与我的关系(含近期服务记录) -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-blue">与我的关系</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="relationship-row">
|
||||
<view class="rel-tag rel-tag-amber">
|
||||
<heart-icon score="{{detail.heartScore}}" />
|
||||
<text>{{detail.heartScore > 8.5 ? '非常好' : detail.heartScore > 7 ? '良好' : detail.heartScore > 5 ? '一般' : '待发展'}}</text>
|
||||
</view>
|
||||
<view class="rel-bar">
|
||||
<view class="rel-bar-fill rel-bar-amber" style="width: {{detail.heartScore * 10}}%"></view>
|
||||
</view>
|
||||
<text class="rel-score rel-score-amber">{{fmt.toFixed(detail.heartScore / 10, 2)}}</text>
|
||||
</view>
|
||||
<text class="card-desc">最近 2 个月互动较少,仅有 3 次服务记录。客户对您的印象中等,有提升空间。</text>
|
||||
|
||||
<view class="svc-section">
|
||||
<text class="svc-title">📋 近期服务记录</text>
|
||||
<view class="svc-record" wx:for="{{serviceRecords}}" wx:key="date">
|
||||
<view class="svc-row-top">
|
||||
<view class="svc-tags">
|
||||
<text class="svc-table">{{item.table}}</text>
|
||||
<text class="svc-type svc-type-{{item.typeClass}}">{{item.type}}</text>
|
||||
<text class="svc-duration">{{item.duration}}</text>
|
||||
</view>
|
||||
<text class="svc-income">{{item.income}}</text>
|
||||
</view>
|
||||
<view class="svc-row-bottom">
|
||||
<text class="svc-drinks">{{item.drinks}}</text>
|
||||
<text class="svc-date">{{item.date}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="svc-more" bindtap="onViewAllRecords">
|
||||
<text>查看全部服务记录 →</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ③ 任务建议 -->
|
||||
<view class="card">
|
||||
<text class="section-title title-orange">任务建议</text>
|
||||
<view class="suggestion-box suggestion-orange">
|
||||
<view class="suggestion-header">
|
||||
<text class="suggestion-icon">💡 建议执行</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<text class="suggestion-desc">该客户消费频率从月均 4 次下降到近月仅 1 次,需要关注原因:</text>
|
||||
<view class="suggestion-list">
|
||||
<text class="suggestion-item">• 了解是否工作变动或搬家导致不便</text>
|
||||
<text class="suggestion-item">• 询问对门店服务是否有改进建议</text>
|
||||
<text class="suggestion-item">• 推荐近期的会员优惠活动</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="talking-section">
|
||||
<view class="talking-header">
|
||||
<text class="talking-title">💬 话术参考</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="talking-list">
|
||||
<text class="talking-item" wx:for="{{talkingPoints}}" wx:key="index">"{{item}}"</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ④ 我给TA的备注 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-blue">我给TA的备注</text>
|
||||
<text class="note-count">{{sortedNotes.length}} 条备注</text>
|
||||
</view>
|
||||
<block wx:if="{{sortedNotes.length > 0}}">
|
||||
<view class="note-item" wx:for="{{sortedNotes}}" wx:key="id">
|
||||
<view class="note-top">
|
||||
<text class="note-date">{{item.createdAt}}</text>
|
||||
<text class="note-tag-inline {{item.tagType === 'coach' ? 'tag-coach-inline' : 'tag-customer-inline'}}">{{item.tagLabel}}</text>
|
||||
</view>
|
||||
<text class="note-content">{{item.content}}</text>
|
||||
</view>
|
||||
</block>
|
||||
<view class="note-empty" wx:else>
|
||||
<t-icon name="edit-1" size="80rpx" color="#dcdcdc" />
|
||||
<text class="empty-hint">暂无备注</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar safe-area-bottom">
|
||||
<view class="btn-ask btn-ask-orange" bindtap="onAskAssistant">
|
||||
<t-icon name="chat" size="40rpx" color="#ffffff" />
|
||||
<text>问问助手</text>
|
||||
</view>
|
||||
<view class="btn-note" bindtap="onAddNote">
|
||||
<t-icon name="edit-1" size="40rpx" color="#242424" />
|
||||
<text>备注</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<note-modal
|
||||
visible="{{noteModalVisible}}"
|
||||
customerName="{{detail.customerName}}"
|
||||
initialScore="{{0}}"
|
||||
initialContent=""
|
||||
bind:confirm="onNoteConfirm"
|
||||
bind:cancel="onNoteCancel"
|
||||
/>
|
||||
|
||||
<ai-float-button bottom="{{200}}" customerId="{{detail.id}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,459 @@
|
||||
/* 加载态 & 空态 */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* Banner - 橙色 */
|
||||
.banner-area {
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
padding-bottom: 48rpx;
|
||||
}
|
||||
.banner-orange {
|
||||
background: linear-gradient(135deg, #f97316 0%, #f59e0b 100%);
|
||||
}
|
||||
.banner-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.nav-back { padding: 8rpx; }
|
||||
.nav-title {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 500;
|
||||
}
|
||||
.nav-placeholder { width: 48rpx; }
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
padding: 16rpx 40rpx 0;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.avatar-text {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
.info-right { flex: 1; }
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.customer-name {
|
||||
font-size: var(--font-xl, 40rpx);
|
||||
font-weight: 600;
|
||||
}
|
||||
.task-type-tag {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
padding: 4rpx 16rpx;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.sub-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.phone {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.absent-info {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* 主体 */
|
||||
.main-content {
|
||||
padding: 32rpx;
|
||||
padding-bottom: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
/* 卡片 */
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-xl, 32rpx);
|
||||
padding: 40rpx;
|
||||
box-shadow: var(--shadow-lg, 0 8rpx 32rpx rgba(0,0,0,0.06));
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.title-green { border-left: 6rpx solid #00a870; padding-left: 16rpx; }
|
||||
.title-blue { border-left: 6rpx solid #0052d9; padding-left: 16rpx; }
|
||||
.title-orange { border-left: 6rpx solid #ed7b2f; padding-left: 16rpx; margin-bottom: 32rpx; }
|
||||
|
||||
.ai-badge {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-primary, #0052d9);
|
||||
background: var(--color-primary-light, #ecf2fe);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
|
||||
/* 维客线索 */
|
||||
.clue-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.clue-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
}
|
||||
.clue-item-detail { flex-wrap: wrap; }
|
||||
.clue-tag {
|
||||
flex-shrink: 0;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
border-radius: 8rpx;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.clue-tag-primary { background: rgba(0, 82, 217, 0.1); color: #0052d9; }
|
||||
.clue-tag-success { background: rgba(0, 168, 112, 0.1); color: #00a870; }
|
||||
.clue-tag-purple { background: rgba(124, 58, 237, 0.1); color: #7c3aed; }
|
||||
.clue-tag-error { background: rgba(227, 77, 89, 0.1); color: #e34d59; }
|
||||
.clue-body {
|
||||
flex: 1;
|
||||
min-height: 80rpx;
|
||||
position: relative;
|
||||
}
|
||||
.clue-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-13, #242424);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.clue-source {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
.clue-detail {
|
||||
width: 100%;
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-8, #777777);
|
||||
line-height: 1.6;
|
||||
margin-top: 8rpx;
|
||||
padding-left: 104rpx;
|
||||
}
|
||||
|
||||
/* 关系区域 - 琥珀色变体 */
|
||||
.relationship-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.rel-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rel-tag-amber { background: linear-gradient(135deg, #f59e0b, #eab308); }
|
||||
.rel-bar {
|
||||
flex: 1;
|
||||
height: 12rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: 100rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rel-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.rel-bar-amber { background: linear-gradient(90deg, #fcd34d, #eab308); }
|
||||
.rel-score {
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 700;
|
||||
}
|
||||
.rel-score-amber { color: #f59e0b; }
|
||||
.card-desc {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-8, #777777);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* 近期服务记录 */
|
||||
.svc-section {
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.svc-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
margin-bottom: 24rpx;
|
||||
display: block;
|
||||
}
|
||||
.svc-record {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
}
|
||||
.svc-record:last-of-type { border-bottom: none; }
|
||||
.svc-row-top, .svc-row-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.svc-row-top { margin-bottom: 8rpx; }
|
||||
.svc-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.svc-table {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
background: #ffffff;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.svc-type {
|
||||
font-size: 22rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
.svc-type-basic { background: #0052d9; }
|
||||
.svc-type-incentive { background: #ed7b2f; }
|
||||
.svc-duration {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.svc-income {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.svc-drinks {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.svc-date {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.svc-more {
|
||||
text-align: center;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.svc-more text {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-primary, #0052d9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 任务建议 - 橙色 */
|
||||
.suggestion-box {
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 32rpx;
|
||||
border: 1rpx solid;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.suggestion-orange {
|
||||
background: linear-gradient(135deg, #fff7ed, #fffbeb);
|
||||
border-color: #fed7aa;
|
||||
}
|
||||
.suggestion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.suggestion-icon {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: #ed7b2f;
|
||||
}
|
||||
.suggestion-desc {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
.suggestion-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.suggestion-item {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 话术参考(竖线样式) */
|
||||
.talking-section {
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 32rpx;
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
}
|
||||
.talking-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.talking-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.talking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.talking-item {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
padding-left: 24rpx;
|
||||
border-left: 4rpx solid rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
|
||||
/* 备注 */
|
||||
.note-count {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.note-item {
|
||||
padding: 28rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.note-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.note-date {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.note-content {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.note-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.empty-hint {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 128rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 0 32rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
.btn-ask {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
}
|
||||
.btn-ask-orange {
|
||||
background: linear-gradient(135deg, #f97316, #f59e0b);
|
||||
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
.btn-note {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigationBarTitleText": "任务详情",
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"note-modal": "/components/note-modal/note-modal",
|
||||
"star-rating": "/components/star-rating/star-rating",
|
||||
"heart-icon": "/components/heart-icon/heart-icon",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { mockTaskDetails } from '../../utils/mock-data'
|
||||
import type { TaskDetail, Note } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
detail: null as TaskDetail | null,
|
||||
sortedNotes: [] as Note[],
|
||||
noteModalVisible: false,
|
||||
/** 话术列表(竖线样式) */
|
||||
talkingPoints: [
|
||||
'王哥您好,上次打球聊得挺开心的,最近有空来玩吗?可以约几个球友一起~',
|
||||
'王哥,最近店里新到了几副好球杆,知道您是行家,有空来试试手感?',
|
||||
'王哥好呀,周末有个中式八球友谊赛,奖品挺丰富的,要不要来参加?',
|
||||
'王哥,上次您推荐的那个朋友来过了,打得不错,下次可以一起约~',
|
||||
'王哥您好,我们最近推出了老带新活动,您带朋友来都有优惠,了解一下?',
|
||||
],
|
||||
/** 近期服务记录 */
|
||||
serviceRecords: [
|
||||
{ table: '6号台', type: '基础课', typeClass: 'basic', duration: '1.5h', income: '¥150', drinks: '🍷 可乐x2', date: '2026-02-08 15:00' },
|
||||
{ table: 'VIP1号房', type: '激励课', typeClass: 'incentive', duration: '2.0h', income: '¥200', drinks: '🍷 红牛x1 花生米x1', date: '2026-02-01 16:00' },
|
||||
{ table: '3号台', type: '基础课', typeClass: 'basic', duration: '1.5h', income: '¥150', drinks: '🍷 矿泉水x2', date: '2026-01-25 14:30' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options?.id || ''
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
loadData(id: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API
|
||||
const detail = mockTaskDetails.find((t) => t.id === id) || mockTaskDetails[2]
|
||||
if (!detail) {
|
||||
this.setData({ pageState: 'empty' })
|
||||
return
|
||||
}
|
||||
const sorted = sortByTimestamp(detail.notes || []) as Note[]
|
||||
this.setData({ pageState: 'normal', detail, sortedNotes: sorted })
|
||||
}, 500)
|
||||
},
|
||||
|
||||
onAddNote() {
|
||||
this.setData({ noteModalVisible: true })
|
||||
},
|
||||
|
||||
onNoteConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
const { score, content } = e.detail
|
||||
wx.showToast({ title: '备注已保存', icon: 'success' })
|
||||
this.setData({ noteModalVisible: false })
|
||||
const newNote: Note = {
|
||||
id: `note-${Date.now()}`,
|
||||
content,
|
||||
tagType: 'customer',
|
||||
tagLabel: `客户:${this.data.detail?.customerName || ''}`,
|
||||
createdAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
||||
}
|
||||
this.setData({ sortedNotes: [newNote, ...this.data.sortedNotes] })
|
||||
},
|
||||
|
||||
onNoteCancel() {
|
||||
this.setData({ noteModalVisible: false })
|
||||
},
|
||||
|
||||
onAskAssistant() {
|
||||
const customerId = this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/chat/chat?customerId=${customerId}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
|
||||
/** 记录互动 */
|
||||
onRecordInteraction() {
|
||||
this.setData({ noteModalVisible: true })
|
||||
},
|
||||
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onViewAllRecords() {
|
||||
const customerId = this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/customer-service-records/customer-service-records?customerId=${customerId}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,193 @@
|
||||
<wxs src="../../utils/format.wxs" module="fmt" />
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-icon name="info-circle" size="120rpx" color="#c5c5c5" />
|
||||
<text class="empty-text">未找到任务信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- Banner - 粉色主题 -->
|
||||
<view class="banner-area banner-pink">
|
||||
<view class="banner-nav">
|
||||
<view class="nav-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="48rpx" color="#ffffff" />
|
||||
</view>
|
||||
<text class="nav-title">任务详情</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view class="customer-info">
|
||||
<view class="avatar-box">
|
||||
<text class="avatar-text">{{detail.customerName[0] || '?'}}</text>
|
||||
</view>
|
||||
<view class="info-right">
|
||||
<view class="name-row">
|
||||
<text class="customer-name">{{detail.customerName}}</text>
|
||||
<text class="task-type-tag">关系构建</text>
|
||||
</view>
|
||||
<view class="sub-info">
|
||||
<text class="phone">137****8899</text>
|
||||
<block wx:if="{{detail.preferences.length > 0}}">
|
||||
<text class="pref-info">🎱 {{detail.preferences[0]}}</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="main-content">
|
||||
<!-- ① 维客线索 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-green">维客线索</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="clue-list">
|
||||
<view class="clue-item">
|
||||
<view class="clue-tag clue-tag-primary">客户<text>\n</text>基础</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">👤 普通会员 · 注册8个月 · 社交活跃</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="clue-item clue-item-detail">
|
||||
<view class="clue-tag clue-tag-success">消费<text>\n</text>习惯</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">☀️ 偏好周末下午 · 月均3-4次 · 喜欢包厢</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
<text class="clue-detail">{{detail.consumptionHabits || '周末下午为主,偏好包厢'}}</text>
|
||||
</view>
|
||||
<view class="clue-item clue-item-detail">
|
||||
<view class="clue-tag clue-tag-purple">玩法<text>\n</text>偏好</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">🎱 中式台球 · 麻将 · 喜欢组队</text>
|
||||
<text class="clue-source">By:系统</text>
|
||||
</view>
|
||||
<text class="clue-detail">{{detail.socialPreference || '喜欢组队打球,社交型消费者'}}</text>
|
||||
</view>
|
||||
<view class="clue-item">
|
||||
<view class="clue-tag clue-tag-error">重要<text>\n</text>反馈</view>
|
||||
<view class="clue-body">
|
||||
<text class="clue-text">⭐ 对服务满意,希望认识更多球友</text>
|
||||
<text class="clue-source">By:小燕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ② 与我的关系(含近期服务记录) -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-blue">与我的关系</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="relationship-row">
|
||||
<view class="rel-tag rel-tag-pink">
|
||||
<heart-icon score="{{detail.heartScore}}" />
|
||||
<text>{{detail.heartScore > 8.5 ? '非常好' : detail.heartScore > 7 ? '良好' : detail.heartScore > 5 ? '一般' : '待发展'}}</text>
|
||||
</view>
|
||||
<view class="rel-bar">
|
||||
<view class="rel-bar-fill" style="width: {{detail.heartScore * 10}}%"></view>
|
||||
</view>
|
||||
<text class="rel-score">{{fmt.toFixed(detail.heartScore / 10, 2)}}</text>
|
||||
</view>
|
||||
<text class="card-desc">{{detail.aiAnalysis.summary}}</text>
|
||||
|
||||
<view class="svc-section">
|
||||
<text class="svc-title">📋 近期服务记录</text>
|
||||
<view class="svc-record" wx:for="{{serviceRecords}}" wx:key="date">
|
||||
<view class="svc-row-top">
|
||||
<view class="svc-tags">
|
||||
<text class="svc-table">{{item.table}}</text>
|
||||
<text class="svc-type svc-type-{{item.typeClass}}">{{item.type}}</text>
|
||||
<text class="svc-duration">{{item.duration}}</text>
|
||||
</view>
|
||||
<text class="svc-income">{{item.income}}</text>
|
||||
</view>
|
||||
<view class="svc-row-bottom">
|
||||
<text class="svc-drinks">{{item.drinks}}</text>
|
||||
<text class="svc-date">{{item.date}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="svc-more" bindtap="onViewAllRecords">
|
||||
<text>查看全部服务记录 →</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ③ 任务建议 -->
|
||||
<view class="card">
|
||||
<text class="section-title title-pink-left">任务建议</text>
|
||||
<view class="suggestion-box suggestion-pink">
|
||||
<view class="suggestion-header">
|
||||
<text class="suggestion-icon">💝 关系构建重点</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="suggestion-list">
|
||||
<text class="suggestion-item" wx:for="{{detail.aiAnalysis.suggestions}}" wx:key="index">• {{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="talking-section">
|
||||
<view class="talking-header">
|
||||
<text class="talking-title">💬 话术参考</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="talking-list">
|
||||
<text class="talking-item" wx:for="{{talkingPoints}}" wx:key="index">"{{item}}"</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ④ 我给TA的备注 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-blue">我给TA的备注</text>
|
||||
<text class="note-count">{{sortedNotes.length}} 条备注</text>
|
||||
</view>
|
||||
<block wx:if="{{sortedNotes.length > 0}}">
|
||||
<view class="note-item" wx:for="{{sortedNotes}}" wx:key="id">
|
||||
<view class="note-top">
|
||||
<text class="note-date">{{item.createdAt}}</text>
|
||||
<text class="note-tag-inline {{item.tagType === 'coach' ? 'tag-coach-inline' : 'tag-customer-inline'}}">{{item.tagLabel}}</text>
|
||||
</view>
|
||||
<text class="note-content">{{item.content}}</text>
|
||||
</view>
|
||||
</block>
|
||||
<view class="note-empty" wx:else>
|
||||
<t-icon name="edit-1" size="80rpx" color="#dcdcdc" />
|
||||
<text class="empty-hint">快点击下方备注按钮,添加客人备注!</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar safe-area-bottom">
|
||||
<view class="btn-ask btn-ask-pink" bindtap="onAskAssistant">
|
||||
<t-icon name="chat" size="40rpx" color="#ffffff" />
|
||||
<text>问问助手</text>
|
||||
</view>
|
||||
<view class="btn-note" bindtap="onAddNote">
|
||||
<t-icon name="edit-1" size="40rpx" color="#242424" />
|
||||
<text>备注</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<note-modal
|
||||
visible="{{noteModalVisible}}"
|
||||
customerName="{{detail.customerName}}"
|
||||
initialScore="{{0}}"
|
||||
initialContent=""
|
||||
bind:confirm="onNoteConfirm"
|
||||
bind:cancel="onNoteCancel"
|
||||
/>
|
||||
|
||||
<ai-float-button bottom="{{200}}" customerId="{{detail.id}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,443 @@
|
||||
/* 加载态 & 空态 */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* Banner - 粉色 */
|
||||
.banner-area {
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
padding-bottom: 48rpx;
|
||||
}
|
||||
.banner-pink {
|
||||
background: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%);
|
||||
}
|
||||
.banner-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.nav-back { padding: 8rpx; }
|
||||
.nav-title {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 500;
|
||||
}
|
||||
.nav-placeholder { width: 48rpx; }
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
padding: 16rpx 40rpx 0;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.avatar-text {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
.info-right { flex: 1; }
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.customer-name {
|
||||
font-size: var(--font-xl, 40rpx);
|
||||
font-weight: 600;
|
||||
}
|
||||
.task-type-tag {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
padding: 4rpx 16rpx;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.sub-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.phone {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.pref-info {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* 主体 */
|
||||
.main-content {
|
||||
padding: 32rpx;
|
||||
padding-bottom: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
/* 卡片 */
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-xl, 32rpx);
|
||||
padding: 40rpx;
|
||||
box-shadow: var(--shadow-lg, 0 8rpx 32rpx rgba(0,0,0,0.06));
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.title-green { border-left: 6rpx solid #00a870; padding-left: 16rpx; }
|
||||
.title-blue { border-left: 6rpx solid #0052d9; padding-left: 16rpx; }
|
||||
.title-pink-left { border-left: 6rpx solid #ec4899; padding-left: 16rpx; margin-bottom: 32rpx; }
|
||||
|
||||
.ai-badge {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-primary, #0052d9);
|
||||
background: var(--color-primary-light, #ecf2fe);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
|
||||
/* 维客线索 */
|
||||
.clue-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.clue-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
}
|
||||
.clue-item-detail { flex-wrap: wrap; }
|
||||
.clue-tag {
|
||||
flex-shrink: 0;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
border-radius: 8rpx;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.clue-tag-primary { background: rgba(0, 82, 217, 0.1); color: #0052d9; }
|
||||
.clue-tag-success { background: rgba(0, 168, 112, 0.1); color: #00a870; }
|
||||
.clue-tag-purple { background: rgba(124, 58, 237, 0.1); color: #7c3aed; }
|
||||
.clue-tag-error { background: rgba(227, 77, 89, 0.1); color: #e34d59; }
|
||||
.clue-body {
|
||||
flex: 1;
|
||||
min-height: 80rpx;
|
||||
position: relative;
|
||||
}
|
||||
.clue-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-13, #242424);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.clue-source {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 22rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
.clue-detail {
|
||||
width: 100%;
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-8, #777777);
|
||||
line-height: 1.6;
|
||||
margin-top: 8rpx;
|
||||
padding-left: 104rpx;
|
||||
}
|
||||
|
||||
/* 关系区域 */
|
||||
.relationship-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.rel-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rel-tag-pink { background: linear-gradient(135deg, #ec4899, #f43f5e); }
|
||||
.rel-bar {
|
||||
flex: 1;
|
||||
height: 12rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: 100rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rel-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #f9a8d4, #f43f5e);
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.rel-score {
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 700;
|
||||
color: #ec4899;
|
||||
}
|
||||
.card-desc {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-8, #777777);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* 近期服务记录 */
|
||||
.svc-section {
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.svc-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
margin-bottom: 24rpx;
|
||||
display: block;
|
||||
}
|
||||
.svc-record {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
}
|
||||
.svc-record:last-of-type { border-bottom: none; }
|
||||
.svc-row-top, .svc-row-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.svc-row-top { margin-bottom: 8rpx; }
|
||||
.svc-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.svc-table {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
background: #ffffff;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.svc-type {
|
||||
font-size: 22rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
.svc-type-basic { background: #0052d9; }
|
||||
.svc-type-incentive { background: #ed7b2f; }
|
||||
.svc-duration {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.svc-income {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.svc-drinks { font-size: 22rpx; color: var(--color-gray-6, #a6a6a6); }
|
||||
.svc-date { font-size: 22rpx; color: var(--color-gray-6, #a6a6a6); }
|
||||
.svc-more { text-align: center; margin-top: 24rpx; }
|
||||
.svc-more text {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-primary, #0052d9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 任务建议 - 粉色 */
|
||||
.suggestion-box {
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 32rpx;
|
||||
border: 1rpx solid;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.suggestion-pink {
|
||||
background: linear-gradient(135deg, #fdf2f8, #fff1f2);
|
||||
border-color: #fbcfe8;
|
||||
}
|
||||
.suggestion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.suggestion-icon {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: #ec4899;
|
||||
}
|
||||
.suggestion-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.suggestion-item {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 话术参考(竖线样式) */
|
||||
.talking-section {
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 32rpx;
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
}
|
||||
.talking-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.talking-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.talking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.talking-item {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
padding-left: 24rpx;
|
||||
border-left: 4rpx solid rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
|
||||
/* 备注 */
|
||||
.note-count {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.note-item {
|
||||
padding: 28rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.note-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.note-date {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.note-content {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.note-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.empty-hint {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 128rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 0 32rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
.btn-ask {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
}
|
||||
.btn-ask-pink {
|
||||
background: linear-gradient(135deg, #ec4899, #f43f5e);
|
||||
box-shadow: 0 8rpx 24rpx rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
.btn-note {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigationBarTitleText": "任务详情",
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"note-modal": "/components/note-modal/note-modal",
|
||||
"star-rating": "/components/star-rating/star-rating",
|
||||
"heart-icon": "/components/heart-icon/heart-icon",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { mockTaskDetails } from '../../utils/mock-data'
|
||||
import type { TaskDetail, Note } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态 */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 任务详情 */
|
||||
detail: null as TaskDetail | null,
|
||||
/** 排序后的备注列表 */
|
||||
sortedNotes: [] as Note[],
|
||||
/** 备注弹窗 */
|
||||
noteModalVisible: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options?.id || ''
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
loadData(id: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API 调用
|
||||
const detail = mockTaskDetails.find((t) => t.id === id) || mockTaskDetails[0]
|
||||
if (!detail) {
|
||||
this.setData({ pageState: 'empty' })
|
||||
return
|
||||
}
|
||||
|
||||
const sorted = sortByTimestamp(detail.notes || []) as Note[]
|
||||
this.setData({
|
||||
pageState: 'normal',
|
||||
detail,
|
||||
sortedNotes: sorted,
|
||||
})
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/** 点击"添加备注" */
|
||||
onAddNote() {
|
||||
this.setData({ noteModalVisible: true })
|
||||
},
|
||||
|
||||
/** 备注弹窗确认 */
|
||||
onNoteConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
const { score, content } = e.detail
|
||||
wx.showToast({ title: '备注已保存', icon: 'success' })
|
||||
this.setData({ noteModalVisible: false })
|
||||
|
||||
// 模拟添加到列表
|
||||
const newNote: Note = {
|
||||
id: `note-${Date.now()}`,
|
||||
content,
|
||||
tagType: 'customer',
|
||||
tagLabel: `客户:${this.data.detail?.customerName || ''}`,
|
||||
createdAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
||||
}
|
||||
const notes = [newNote, ...this.data.sortedNotes]
|
||||
this.setData({ sortedNotes: notes })
|
||||
},
|
||||
|
||||
/** 备注弹窗取消 */
|
||||
onNoteCancel() {
|
||||
this.setData({ noteModalVisible: false })
|
||||
},
|
||||
|
||||
/** 放弃任务 */
|
||||
onAbandon() {
|
||||
wx.showModal({
|
||||
title: '放弃任务',
|
||||
content: '确定要放弃该客户的维护吗?此操作不可撤销。',
|
||||
confirmColor: '#e34d59',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.showToast({ title: '已放弃该客户的维护', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
/** 问问助手 */
|
||||
onAskAssistant() {
|
||||
const customerId = this.data.detail?.id || ''
|
||||
wx.navigateTo({
|
||||
url: `/pages/chat/chat?customerId=${customerId}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
|
||||
/** 标记完成 */
|
||||
onMarkComplete() {
|
||||
wx.showToast({ title: '已标记完成', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
},
|
||||
|
||||
/** 返回 */
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
<wxs src="../../utils/format.wxs" module="fmt" />
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-icon name="info-circle" size="120rpx" color="#c5c5c5" />
|
||||
<text class="empty-text">未找到任务信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- Banner 区域 -->
|
||||
<view class="banner-area banner-red">
|
||||
<view class="banner-nav">
|
||||
<view class="nav-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="48rpx" color="#ffffff" />
|
||||
</view>
|
||||
<text class="nav-title">任务详情</text>
|
||||
<text class="nav-abandon" bindtap="onAbandon">放弃</text>
|
||||
</view>
|
||||
<view class="customer-info">
|
||||
<view class="avatar-box">
|
||||
<text class="avatar-text">{{detail.customerName[0] || '?'}}</text>
|
||||
</view>
|
||||
<view class="info-right">
|
||||
<view class="name-row">
|
||||
<text class="customer-name">{{detail.customerName}}</text>
|
||||
<text class="task-type-tag">{{detail.taskTypeLabel || '高优先召回'}}</text>
|
||||
</view>
|
||||
<view class="sub-info">
|
||||
<text class="phone">138****5678</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="main-content">
|
||||
<!-- 与我的关系 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-pink">与我的关系</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="relationship-row">
|
||||
<view class="rel-tag rel-tag-pink">
|
||||
<heart-icon score="{{detail.heartScore}}" />
|
||||
<text>{{detail.heartScore > 8.5 ? '非常好' : detail.heartScore > 7 ? '良好' : detail.heartScore > 5 ? '一般' : '待发展'}}</text>
|
||||
</view>
|
||||
<view class="rel-bar">
|
||||
<view class="rel-bar-fill" style="width: {{detail.heartScore * 10}}%"></view>
|
||||
</view>
|
||||
<text class="rel-score">{{fmt.toFixed(detail.heartScore / 10, 2)}}</text>
|
||||
</view>
|
||||
<text class="card-desc">{{detail.aiAnalysis.summary}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 任务建议 -->
|
||||
<view class="card">
|
||||
<text class="section-title title-orange">任务建议</text>
|
||||
<view class="suggestion-box">
|
||||
<view class="suggestion-header">
|
||||
<text class="suggestion-icon">💡 建议执行</text>
|
||||
<text class="ai-badge">AI智能洞察</text>
|
||||
</view>
|
||||
<view class="suggestion-list">
|
||||
<view class="suggestion-item" wx:for="{{detail.aiAnalysis.suggestions}}" wx:key="index">
|
||||
<text>• {{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我给TA的备注 -->
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<text class="section-title title-blue">我给TA的备注</text>
|
||||
<text class="note-count">{{sortedNotes.length}} 条备注</text>
|
||||
</view>
|
||||
<block wx:if="{{sortedNotes.length > 0}}">
|
||||
<view class="note-item" wx:for="{{sortedNotes}}" wx:key="id">
|
||||
<view class="note-top">
|
||||
<text class="note-date">{{item.createdAt}}</text>
|
||||
<text class="note-tag-inline {{item.tagType === 'coach' ? 'tag-coach-inline' : 'tag-customer-inline'}}">{{item.tagLabel}}</text>
|
||||
</view>
|
||||
<text class="note-content">{{item.content}}</text>
|
||||
</view>
|
||||
</block>
|
||||
<view class="note-empty" wx:else>
|
||||
<t-icon name="edit-1" size="80rpx" color="#dcdcdc" />
|
||||
<text class="empty-hint">暂无备注</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar safe-area-bottom">
|
||||
<view class="btn-ask" bindtap="onAskAssistant">
|
||||
<t-icon name="chat" size="40rpx" color="#ffffff" />
|
||||
<text>问问助手</text>
|
||||
</view>
|
||||
<view class="btn-note" bindtap="onAddNote">
|
||||
<t-icon name="edit-1" size="40rpx" color="#242424" />
|
||||
<text>备注</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注弹窗 -->
|
||||
<note-modal
|
||||
visible="{{noteModalVisible}}"
|
||||
customerName="{{detail.customerName}}"
|
||||
initialScore="{{0}}"
|
||||
initialContent=""
|
||||
bind:confirm="onNoteConfirm"
|
||||
bind:cancel="onNoteCancel"
|
||||
/>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{200}}" customerId="{{detail.id}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,288 @@
|
||||
/* 加载态 & 空态 */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* Banner */
|
||||
.banner-area {
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
padding-bottom: 48rpx;
|
||||
}
|
||||
.banner-red {
|
||||
background: linear-gradient(135deg, #e34d59 0%, #c62828 100%);
|
||||
}
|
||||
.banner-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.nav-back {
|
||||
padding: 8rpx;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 500;
|
||||
}
|
||||
.nav-abandon {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 客户信息 */
|
||||
.customer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
padding: 16rpx 40rpx 0;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.avatar-text {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
.info-right {
|
||||
flex: 1;
|
||||
}
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.customer-name {
|
||||
font-size: var(--font-xl, 40rpx);
|
||||
font-weight: 600;
|
||||
}
|
||||
.task-type-tag {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
padding: 4rpx 16rpx;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.sub-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.phone {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 主体内容 */
|
||||
.main-content {
|
||||
padding: 32rpx;
|
||||
padding-bottom: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
/* 卡片 */
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-xl, 32rpx);
|
||||
padding: 40rpx;
|
||||
box-shadow: var(--shadow-lg, 0 8rpx 32rpx rgba(0,0,0,0.06));
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.title-pink { border-left: 6rpx solid #ec4899; padding-left: 16rpx; }
|
||||
.title-orange { border-left: 6rpx solid #ed7b2f; padding-left: 16rpx; margin-bottom: 32rpx; }
|
||||
.title-blue { border-left: 6rpx solid #0052d9; padding-left: 16rpx; }
|
||||
.title-green { border-left: 6rpx solid #00a870; padding-left: 16rpx; }
|
||||
|
||||
.ai-badge {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-primary, #0052d9);
|
||||
background: var(--color-primary-light, #ecf2fe);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
|
||||
/* 关系区域 */
|
||||
.relationship-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.rel-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rel-tag-pink {
|
||||
background: linear-gradient(135deg, #ec4899, #f43f5e);
|
||||
}
|
||||
.rel-bar {
|
||||
flex: 1;
|
||||
height: 12rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: 100rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rel-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #f9a8d4, #f43f5e);
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.rel-score {
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 700;
|
||||
color: #ec4899;
|
||||
}
|
||||
.card-desc {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-8, #777777);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 任务建议 */
|
||||
.suggestion-box {
|
||||
background: linear-gradient(135deg, #eff6ff, #eef2ff);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
padding: 32rpx;
|
||||
border: 1rpx solid #dbeafe;
|
||||
}
|
||||
.suggestion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.suggestion-icon {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 500;
|
||||
color: var(--color-primary, #0052d9);
|
||||
}
|
||||
.suggestion-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.suggestion-item {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 备注 */
|
||||
.note-count {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.note-item {
|
||||
padding: 28rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
border: 1rpx solid var(--color-gray-3, #e7e7e7);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.note-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.note-date {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.note-content {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-9, #5e5e5e);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.note-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.empty-hint {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 128rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 0 32rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
.btn-ask {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #0052d9, #3b82f6);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 82, 217, 0.3);
|
||||
}
|
||||
.btn-note {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: var(--color-gray-1, #f3f3f3);
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-lg, 24rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
font-size: var(--font-base, 32rpx);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "任务",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"banner": "/components/banner/banner",
|
||||
"heart-icon": "/components/heart-icon/heart-icon",
|
||||
"hobby-tag": "/components/hobby-tag/hobby-tag",
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button"
|
||||
}
|
||||
}
|
||||
123
apps/miniprogram - 副本/miniprogram/pages/task-list/task-list.ts
Normal file
123
apps/miniprogram - 副本/miniprogram/pages/task-list/task-list.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { mockTasks, mockPerformance } from '../../utils/mock-data'
|
||||
import type { Task } from '../../utils/mock-data'
|
||||
import { getTaskTypeColor } from '../../utils/task'
|
||||
|
||||
/** 任务类型 → 详情页路由映射 */
|
||||
const DETAIL_ROUTE_MAP: Record<string, string> = {
|
||||
callback: '/pages/task-detail-callback/task-detail-callback',
|
||||
priority_recall: '/pages/task-detail-priority/task-detail-priority',
|
||||
relationship: '/pages/task-detail-relationship/task-detail-relationship',
|
||||
}
|
||||
|
||||
/** 为任务附加颜色信息 */
|
||||
function enrichTask(task: Task) {
|
||||
return {
|
||||
...task,
|
||||
typeColor: getTaskTypeColor(task.taskType),
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态:loading / empty / normal */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 任务列表(附带颜色) */
|
||||
tasks: [] as ReturnType<typeof enrichTask>[],
|
||||
/** 任务总数 */
|
||||
taskCount: 0,
|
||||
/** Banner 指标 */
|
||||
bannerMetrics: [] as Array<{ label: string; value: string }>,
|
||||
/** Banner 标题 */
|
||||
bannerTitle: '',
|
||||
/** 是否还有更多数据(模拟分页) */
|
||||
hasMore: true,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// TabBar 页面每次显示时可刷新
|
||||
},
|
||||
|
||||
/** 下拉刷新 */
|
||||
onPullDownRefresh() {
|
||||
this.loadData(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom() {
|
||||
if (!this.data.hasMore) return
|
||||
// Mock:无更多数据
|
||||
this.setData({ hasMore: false })
|
||||
wx.showToast({ title: '没有更多了', icon: 'none' })
|
||||
},
|
||||
|
||||
/** 加载数据 */
|
||||
loadData(cb?: () => void) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
// 模拟网络延迟
|
||||
setTimeout(() => {
|
||||
const pending = mockTasks.filter((t) => t.status === 'pending')
|
||||
const enriched = pending.map(enrichTask)
|
||||
|
||||
const perf = mockPerformance
|
||||
const bannerTitle = `${perf.currentTier}`
|
||||
const bannerMetrics = [
|
||||
{ label: '本月收入', value: `¥${perf.monthlyIncome.toLocaleString()}` },
|
||||
{ label: '今日服务', value: `${perf.todayServiceCount}` },
|
||||
{ label: '距下一档', value: `¥${perf.nextTierGap.toLocaleString()}` },
|
||||
]
|
||||
|
||||
this.setData({
|
||||
pageState: enriched.length > 0 ? 'normal' : 'empty',
|
||||
tasks: enriched,
|
||||
taskCount: enriched.length,
|
||||
bannerTitle,
|
||||
bannerMetrics,
|
||||
hasMore: true,
|
||||
})
|
||||
|
||||
cb?.()
|
||||
}, 600)
|
||||
},
|
||||
|
||||
/** 点击任务卡片 → 跳转详情 */
|
||||
onTaskTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const { id, tasktype } = e.currentTarget.dataset
|
||||
const route = DETAIL_ROUTE_MAP[tasktype] || '/pages/task-detail/task-detail'
|
||||
wx.navigateTo({
|
||||
url: `${route}?id=${id}`,
|
||||
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
|
||||
/** 长按任务卡片 → 操作菜单 */
|
||||
onTaskLongPress(e: WechatMiniprogram.TouchEvent) {
|
||||
const { id, name } = e.currentTarget.dataset
|
||||
wx.showActionSheet({
|
||||
itemList: ['查看详情', '标记完成', '添加备注'],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0: {
|
||||
// 查看详情 — 复用点击逻辑
|
||||
const { tasktype } = e.currentTarget.dataset
|
||||
const route = DETAIL_ROUTE_MAP[tasktype] || '/pages/task-detail/task-detail'
|
||||
wx.navigateTo({ url: `${route}?id=${id}` })
|
||||
break
|
||||
}
|
||||
case 1:
|
||||
wx.showToast({ title: `已标记「${name}」完成`, icon: 'success' })
|
||||
break
|
||||
case 2:
|
||||
wx.showToast({ title: `为「${name}」添加备注`, icon: 'none' })
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
<!-- 任务列表页 -->
|
||||
<view class="page-task-list">
|
||||
|
||||
<!-- ====== 顶部绩效 Banner ====== -->
|
||||
<banner theme="blue" title="{{bannerTitle}}" metrics="{{bannerMetrics}}" />
|
||||
|
||||
<!-- ====== Loading 状态 ====== -->
|
||||
<view class="state-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<view class="loading-placeholder" wx:for="{{[1,2,3]}}" wx:key="*this">
|
||||
<view class="ph-line ph-line--title"></view>
|
||||
<view class="ph-line ph-line--body"></view>
|
||||
<view class="ph-line ph-line--short"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ====== 空状态 ====== -->
|
||||
<view class="state-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-icon name="task" size="160rpx" color="#dcdcdc" />
|
||||
<text class="empty-text">暂无待办任务</text>
|
||||
</view>
|
||||
|
||||
<!-- ====== 正常态:任务列表 ====== -->
|
||||
<view class="task-section" wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- 标题行 -->
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日 客户维护</text>
|
||||
<text class="section-count">共 {{taskCount}} 项</text>
|
||||
</view>
|
||||
|
||||
<!-- 任务卡片列表 -->
|
||||
<view
|
||||
class="task-card"
|
||||
wx:for="{{tasks}}"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
data-name="{{item.customerName}}"
|
||||
data-tasktype="{{item.taskType}}"
|
||||
bindtap="onTaskTap"
|
||||
bindlongpress="onTaskLongPress"
|
||||
>
|
||||
<!-- 左侧彩色边条 -->
|
||||
<view class="card-border" style="background-color: {{item.typeColor}};"></view>
|
||||
|
||||
<view class="card-body">
|
||||
<!-- 第一行:标签 + 客户名 + 爱心 + 备注 -->
|
||||
<view class="card-row-1">
|
||||
<view class="task-type-tag" style="background-color: {{item.typeColor}};">
|
||||
<text class="tag-text">{{item.taskTypeLabel}}</text>
|
||||
</view>
|
||||
<text class="customer-name">{{item.customerName}}</text>
|
||||
<heart-icon score="{{item.heartScore}}" />
|
||||
<text class="note-indicator" wx:if="{{item.hasNote}}">📝</text>
|
||||
<text class="pin-indicator" wx:if="{{item.isPinned}}">📌</text>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:截止时间 -->
|
||||
<view class="card-row-2">
|
||||
<text class="deadline-text">截止:{{item.deadline}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 第三行:喜好标签 -->
|
||||
<view class="card-row-3" wx:if="{{item.hobbies.length > 0}}">
|
||||
<hobby-tag
|
||||
wx:for="{{item.hobbies}}"
|
||||
wx:for-item="hobby"
|
||||
wx:key="*this"
|
||||
type="{{hobby}}"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧箭头 -->
|
||||
<view class="card-arrow">
|
||||
<text class="arrow-icon">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view class="load-more" wx:if="{{!hasMore}}">
|
||||
<text class="load-more-text">— 没有更多了 —</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ====== AI 悬浮按钮 ====== -->
|
||||
<ai-float-button visible="{{pageState !== 'loading'}}" bottom="{{200}}" />
|
||||
</view>
|
||||
|
||||
<dev-fab />
|
||||
173
apps/miniprogram - 副本/miniprogram/pages/task-list/task-list.wxss
Normal file
173
apps/miniprogram - 副本/miniprogram/pages/task-list/task-list.wxss
Normal file
@@ -0,0 +1,173 @@
|
||||
/* 任务列表页样式 */
|
||||
.page-task-list {
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-gray-1);
|
||||
padding-bottom: 180rpx; /* 为 AI 悬浮按钮 + 安全区留空 */
|
||||
}
|
||||
|
||||
/* ====== Loading 骨架屏 ====== */
|
||||
.state-loading {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.loading-placeholder {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.ph-line {
|
||||
height: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
background: linear-gradient(90deg, var(--color-gray-2) 25%, var(--color-gray-1) 50%, var(--color-gray-2) 75%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.ph-line--title {
|
||||
width: 40%;
|
||||
height: 32rpx;
|
||||
}
|
||||
.ph-line--body {
|
||||
width: 80%;
|
||||
}
|
||||
.ph-line--short {
|
||||
width: 55%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
/* ====== 空状态 ====== */
|
||||
.state-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
.empty-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-6);
|
||||
}
|
||||
|
||||
/* ====== 任务区域 ====== */
|
||||
.task-section {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
/* 标题行 */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: var(--font-base);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13);
|
||||
}
|
||||
.section-count {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-6);
|
||||
}
|
||||
|
||||
/* ====== 任务卡片 ====== */
|
||||
.task-card {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
margin-bottom: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.task-card:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* 左侧彩色边条 */
|
||||
.card-border {
|
||||
width: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 卡片主体 */
|
||||
.card-body {
|
||||
flex: 1;
|
||||
padding: 28rpx 24rpx;
|
||||
min-width: 0; /* 防止溢出 */
|
||||
}
|
||||
|
||||
/* 第一行 */
|
||||
.card-row-1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.task-type-tag {
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.tag-text {
|
||||
font-size: var(--font-xs);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
.customer-name {
|
||||
font-size: var(--font-base);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13);
|
||||
}
|
||||
.note-indicator,
|
||||
.pin-indicator {
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
/* 第二行 */
|
||||
.card-row-2 {
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.deadline-text {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-gray-7);
|
||||
}
|
||||
|
||||
/* 第三行:喜好标签 */
|
||||
.card-row-3 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
/* 右侧箭头 */
|
||||
.card-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.arrow-icon {
|
||||
font-size: 40rpx;
|
||||
color: var(--color-gray-5);
|
||||
}
|
||||
|
||||
/* ====== 加载更多 ====== */
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 32rpx 0;
|
||||
}
|
||||
.load-more-text {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-gray-6);
|
||||
}
|
||||
Reference in New Issue
Block a user