chore: 抢单端UI回退前备份当前完整工作区

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-29 17:59:23 +08:00
parent 6e2f7bc39f
commit e8fb32c1fe
53 changed files with 3410 additions and 1065 deletions

View File

@@ -6,7 +6,7 @@ const PILL_SHAPES = ['pill', 'rectangle', 'rounded'];
Component({
properties: {
mingcheng: { type: String, value: '' },
texiaoJson: { type: Object, value: {} },
texiaoJson: { type: null, value: null },
},
data: {
inlineStyle: '',
@@ -29,7 +29,20 @@ Component({
},
methods: {
_applyConfig(raw) {
const cfg = raw || {};
let cfg = raw || {};
if (typeof cfg === 'string') {
try {
cfg = JSON.parse(cfg);
} catch (e) {
cfg = {};
}
}
if (Array.isArray(cfg)) {
cfg = cfg[0] || {};
}
if (!cfg || typeof cfg !== 'object') {
cfg = {};
}
const ossImageUrl = app.globalData.ossImageUrl || '';
// 无 shape身份装饰标签 / 自定义色圆角标;有 shape考核称号等特殊形状

View File

@@ -1,4 +1,4 @@
const { resolveAvatarUrl } = require('../../utils/avatar.js');
const { resolveAvatarUrl, getDefaultAvatarUrl } = require('../../utils/avatar.js');
Component({
properties: {
@@ -53,7 +53,8 @@ Component({
// 静音状态
isMuted: false,
formattedTime: '',
// 自动隐藏计时器
autoHideTimeout: null
},
@@ -123,7 +124,7 @@ Component({
let avatar = resolveAvatarUrl(
data.avatar || data.message?.senderData?.avatar,
app
);
) || getDefaultAvatarUrl(app);
this.setData({
show: true,
@@ -131,6 +132,7 @@ Component({
message: data.content || '[消息内容]',
avatar: avatar,
notificationData: data,
formattedTime: this.formatTime(data.timestamp),
progress: 100,
swipingClass: '',
swipeStyle: ''
@@ -149,6 +151,7 @@ Component({
this.clearTimers();
this.setData({
show: false,
formattedTime: '',
progress: 100,
swipingClass: '',
swipeStyle: ''
@@ -265,6 +268,13 @@ Component({
this.triggerEvent('close');
this.hideNotification();
},
onAvatarError() {
const def = getDefaultAvatarUrl(getApp());
if (def && this.data.avatar !== def) {
this.setData({ avatar: def });
}
},
onMute(e) {
e.stopPropagation();

View File

@@ -15,14 +15,15 @@
<!-- 通知内容 -->
<view class="notification-content">
<!-- 左侧头像 -->
<image class="notification-avatar" src="{{avatar}}" mode="aspectFill"></image>
<image class="notification-avatar" src="{{avatar}}" mode="aspectFill" binderror="onAvatarError"></image>
<!-- 中间内容区域 -->
<view class="notification-body">
<view class="notification-tag">新消息</view>
<view class="notification-title">{{title}}</view>
<view class="notification-message">{{message}}</view>
<view class="notification-time" wx:if="{{showTime}}">
{{formatTime(notificationData ? notificationData.timestamp : '')}}
<view class="notification-time" wx:if="{{show && showTime && formattedTime}}">
{{formattedTime}}
</view>
</view>

View File

@@ -1,29 +1,40 @@
.global-notification {
position: fixed;
left: 20rpx;
right: 20rpx;
top: 0rpx;
left: 24rpx;
right: 24rpx;
top: calc(env(safe-area-inset-top) + 108rpx);
z-index: 99999;
opacity: 0;
transform: translateY(-120%);
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform: translateY(-160%);
transition: all 0.42s cubic-bezier(0.34, 1.2, 0.64, 1);
pointer-events: none;
overflow: hidden;
border-radius: 24rpx;
background: linear-gradient(135deg,
rgba(41, 47, 61, 0.98) 0%,
rgba(31, 36, 48, 0.98) 100%);
backdrop-filter: blur(30rpx) saturate(180%);
-webkit-backdrop-filter: blur(30rpx) saturate(180%);
border: 1rpx solid rgba(255, 255, 255, 0.08);
box-shadow:
0 20rpx 60rpx rgba(0, 0, 0, 0.4),
0 0 0 1rpx rgba(255, 255, 255, 0.03),
inset 0 1rpx 0 rgba(255, 255, 255, 0.1);
min-height: 140rpx;
border-radius: 28rpx;
background: linear-gradient(145deg,
rgba(28, 32, 42, 0.97) 0%,
rgba(22, 26, 36, 0.98) 100%);
backdrop-filter: blur(40rpx) saturate(180%);
-webkit-backdrop-filter: blur(40rpx) saturate(180%);
border: 1rpx solid rgba(255, 255, 255, 0.1);
box-shadow:
0 24rpx 64rpx rgba(0, 0, 0, 0.45),
0 0 0 1rpx rgba(255, 255, 255, 0.04),
inset 0 1rpx 0 rgba(255, 255, 255, 0.12);
min-height: 148rpx;
touch-action: pan-y;
}
.global-notification::before {
content: '';
position: absolute;
left: 0;
top: 24rpx;
bottom: 24rpx;
width: 6rpx;
border-radius: 0 6rpx 6rpx 0;
background: linear-gradient(180deg, #07c160 0%, #D4AF37 100%);
}
.global-notification.show {
opacity: 1;
transform: translateY(0);
@@ -58,8 +69,8 @@
.notification-content {
display: flex;
align-items: center;
padding: 32rpx;
min-height: 140rpx;
padding: 28rpx 28rpx 28rpx 36rpx;
min-height: 148rpx;
position: relative;
}
@@ -84,6 +95,18 @@
justify-content: center;
}
.notification-tag {
display: inline-block;
align-self: flex-start;
font-size: 20rpx;
color: #07c160;
background: rgba(7, 193, 96, 0.15);
padding: 4rpx 14rpx;
border-radius: 20rpx;
margin-bottom: 8rpx;
font-weight: 500;
}
.notification-title {
font-size: 34rpx;
font-weight: 600;

View File

@@ -0,0 +1,126 @@
import { openCustomerServiceChat } from '../../utils/kefu-nav.js';
Component({
properties: {
bottom: { type: String, value: '200rpx' },
},
data: {
mode: 'full',
sessionHidden: false,
csUnread: 0,
floatY: 520,
},
lifetimes: {
attached() {
this._syncFromGlobal();
this._initFloatY();
this._convHandler = () => this.syncCsUnread();
const app = getApp();
if (app.on) app.on('conversationsUpdated', this._convHandler);
if (app.on) app.on('unreadCountChanged', this._convHandler);
this.syncCsUnread();
},
detached() {
const app = getApp();
if (this._convHandler && app.off) {
app.off('conversationsUpdated', this._convHandler);
app.off('unreadCountChanged', this._convHandler);
}
},
},
pageLifetimes: {
show() {
this._syncFromGlobal();
this.syncCsUnread();
},
},
methods: {
_initFloatY() {
const app = getApp();
if (app.globalData.kefuFloatY != null) {
this.setData({ floatY: app.globalData.kefuFloatY });
return;
}
try {
const sys = wx.getSystemInfoSync();
const defaultY = Math.floor((sys.windowHeight || 600) * 0.55);
this.setData({ floatY: defaultY });
} catch (e) {
this.setData({ floatY: 520 });
}
},
_syncFromGlobal() {
const app = getApp();
const sessionHidden = !!app.globalData.kefuFloatSessionHidden;
const mode = app.globalData.kefuFloatMode || 'full';
this.setData({ sessionHidden, mode });
},
onFloatMove(e) {
const y = e.detail && e.detail.y;
if (y == null) return;
const app = getApp();
app.globalData.kefuFloatY = y;
},
syncCsUnread() {
if (this.data.sessionHidden) return;
if (!wx.goEasy?.im?.latestConversations) return;
wx.goEasy.im.latestConversations({
onSuccess: (res) => {
const list = res.content?.conversations || res.conversations || [];
let unread = 0;
list.forEach((c) => {
if (c.type === 'cs' || c.teamId === 'support_team') {
unread += c.unread || 0;
}
});
if (unread !== this.data.csUnread) {
this.setData({ csUnread: unread });
}
},
onFailed: () => {},
});
},
onContact() {
openCustomerServiceChat();
},
onHideTap(e) {
if (e && e.stopPropagation) e.stopPropagation();
const app = getApp();
app.globalData.kefuFloatMode = 'mini';
this.setData({ mode: 'mini' });
},
onExpand(e) {
if (e && e.stopPropagation) e.stopPropagation();
const app = getApp();
app.globalData.kefuFloatMode = 'full';
this.setData({ mode: 'full' });
},
onSessionDismiss(e) {
if (e && e.stopPropagation) e.stopPropagation();
wx.showModal({
title: '隐藏联系客服',
content: '本次使用期间不再显示此按钮。清空后台重新进入小程序后会再次出现。',
confirmText: '确定隐藏',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
const app = getApp();
app.globalData.kefuFloatSessionHidden = true;
this.setData({ sessionHidden: true });
}
},
});
},
},
});

View File

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

View File

@@ -0,0 +1,47 @@
<movable-area class="kefu-movable-area" wx:if="{{!sessionHidden}}">
<movable-view
class="kefu-movable-view"
direction="vertical"
y="{{floatY}}"
damping="40"
friction="2"
bindchange="onFloatMove"
>
<block wx:if="{{mode === 'full'}}">
<view class="kefu-wrap">
<view class="kefu-float kefu-float--full">
<view class="kefu-unread-badge" wx:if="{{csUnread > 0}}">{{csUnread > 99 ? '99+' : csUnread}}</view>
<view class="kefu-float-main" catchtap="onContact">
<text class="kefu-float-icon">💬</text>
<view class="kefu-text-col">
<text class="kefu-float-text">联系客服</text>
<text class="kefu-float-sub" wx:if="{{csUnread > 0}}">客服有新消息</text>
<text class="kefu-float-sub kefu-float-sub--muted" wx:else>咨询订单 / 账号问题</text>
</view>
</view>
<view class="kefu-float-close" catchtap="onHideTap">
<text>×</text>
</view>
</view>
<view class="kefu-permanent-row" catchtap="onSessionDismiss">
<text class="kefu-permanent-label">本次不再显示</text>
<text class="kefu-permanent-x">×</text>
</view>
</view>
</block>
<block wx:else>
<view class="kefu-wrap kefu-wrap--mini">
<view class="kefu-float kefu-float--mini" catchtap="onContact" bindlongpress="onExpand">
<view class="kefu-unread-badge kefu-unread-badge--mini" wx:if="{{csUnread > 0}}">{{csUnread > 99 ? '99+' : csUnread}}</view>
<text class="kefu-float-mini-icon">💬</text>
<text class="kefu-mini-label">联系客服</text>
</view>
<view class="kefu-permanent-row kefu-permanent-row--mini" catchtap="onSessionDismiss">
<text class="kefu-permanent-x">×</text>
<text class="kefu-permanent-label">本次隐藏</text>
</view>
</view>
</block>
</movable-view>
</movable-area>

View File

@@ -0,0 +1,179 @@
.kefu-movable-area {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 900;
}
.kefu-movable-view {
width: auto;
height: auto;
pointer-events: auto;
position: absolute;
right: 24rpx;
top: 0;
}
.kefu-wrap {
display: flex;
flex-direction: column;
align-items: flex-end;
pointer-events: auto;
}
.kefu-float {
position: relative;
}
.kefu-float--full {
display: flex;
align-items: stretch;
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
border-radius: 20rpx;
box-shadow: 0 8rpx 28rpx rgba(7, 193, 96, 0.35);
overflow: visible;
}
.kefu-unread-badge {
position: absolute;
top: -12rpx;
right: -8rpx;
min-width: 36rpx;
height: 36rpx;
line-height: 36rpx;
padding: 0 8rpx;
background: #fa5151;
color: #fff;
font-size: 20rpx;
text-align: center;
border-radius: 18rpx;
border: 2rpx solid #fff;
z-index: 2;
}
.kefu-unread-badge--mini {
top: -8rpx;
right: -4rpx;
}
.kefu-float-main {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
flex: 1;
}
.kefu-float-main:active {
opacity: 0.88;
}
.kefu-float-icon {
font-size: 36rpx;
margin-right: 14rpx;
flex-shrink: 0;
}
.kefu-text-col {
display: flex;
flex-direction: column;
min-width: 0;
}
.kefu-float-text {
font-size: 28rpx;
color: #fff;
font-weight: 600;
white-space: nowrap;
}
.kefu-float-sub {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.95);
margin-top: 4rpx;
}
.kefu-float-sub--muted {
color: rgba(255, 255, 255, 0.75);
}
.kefu-float-close {
width: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.12);
color: rgba(255, 255, 255, 0.95);
font-size: 38rpx;
line-height: 1;
}
.kefu-float-close:active {
background: rgba(0, 0, 0, 0.2);
}
.kefu-permanent-row {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 10rpx;
padding: 8rpx 12rpx;
background: rgba(255, 255, 255, 0.92);
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.kefu-permanent-row:active {
opacity: 0.85;
}
.kefu-permanent-row--mini {
margin-top: 8rpx;
padding: 6rpx 14rpx;
}
.kefu-permanent-label {
font-size: 22rpx;
color: #888;
margin-right: 8rpx;
}
.kefu-permanent-x {
font-size: 28rpx;
color: #bbb;
line-height: 1;
}
.kefu-wrap--mini {
align-items: flex-end;
}
.kefu-float--mini {
width: auto;
min-width: 160rpx;
padding: 16rpx 24rpx;
border-radius: 48rpx;
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
box-shadow: 0 6rpx 20rpx rgba(7, 193, 96, 0.3);
display: flex;
flex-direction: column;
align-items: center;
}
.kefu-float--mini:active {
transform: scale(0.96);
}
.kefu-float-mini-icon {
font-size: 36rpx;
}
.kefu-mini-label {
font-size: 22rpx;
color: #fff;
font-weight: 500;
margin-top: 4rpx;
white-space: nowrap;
}

View File

@@ -1,16 +1,87 @@
import request from '../../utils/request';
const app = getApp();
function mapIdentityForApi() {
const role = app.globalData.currentRole || wx.getStorageSync('currentRole') || 'normal';
if (role === 'dashou') return 'dashou';
if (role === 'shangjia') return 'shangjia';
return 'normal';
}
Component({
properties: {
visible: Boolean,
showDetail: { type: Boolean, value: false }
visible: { type: Boolean, value: false },
groupId: { type: String, value: '' },
orderId: { type: String, value: '' },
},
data: {},
data: {
keyword: '',
loading: false,
orders: [],
filteredOrders: [],
},
observers: {
visible(v) {
if (v) {
this.setData({ keyword: '' });
this.loadOrders('');
}
},
},
methods: {
close() {
this.triggerEvent('close');
}
}
});
},
onSearchInput(e) {
const keyword = (e.detail.value || '').trim();
this.setData({ keyword });
if (this._searchTimer) clearTimeout(this._searchTimer);
this._searchTimer = setTimeout(() => {
this.loadOrders(keyword);
}, 350);
},
async loadOrders(keyword) {
const { groupId, orderId } = this.properties;
if (!groupId && !orderId) return;
this.setData({ loading: true });
try {
const res = await request({
url: '/dingdan/ltpddd',
method: 'POST',
data: {
groupId,
dingdan_id: orderId,
keyword: keyword || '',
identityType: mapIdentityForApi(),
},
});
const body = res?.data || {};
const list = body.data?.list || [];
this.setData({
orders: list,
filteredOrders: list,
loading: false,
});
} catch (e) {
console.warn('加载历史订单失败', e);
this.setData({ loading: false, orders: [], filteredOrders: [] });
wx.showToast({ title: '加载订单失败', icon: 'none' });
}
},
onSelectOrder(e) {
const index = e.currentTarget.dataset.index;
const order = this.data.filteredOrders[index];
if (!order) return;
this.triggerEvent('send', { order });
this.close();
},
},
});

View File

@@ -1,12 +1,28 @@
<view class="order-sender-mask" wx:if="{{visible}}" catchtap="close"></view>
<view class="order-sender {{visible ? 'show' : ''}}">
<view class="header">
<text class="title">提示</text>
<text class="title">选择订单发送</text>
<text class="close" bindtap="close">✕</text>
</view>
<view class="content">
<view class="icon">📋</view>
<text class="notice">此功能暂未开放</text>
<text class="sub">敬请期待</text>
<view class="search-row">
<input class="search-input" placeholder="搜索订单号/内容" value="{{keyword}}" bindinput="onSearchInput" confirm-type="search" />
</view>
</view>
<scroll-view scroll-y class="order-list" enable-flex>
<view class="loading-tip" wx:if="{{loading}}">加载中...</view>
<block wx:for="{{filteredOrders}}" wx:key="dingdan_id">
<view class="order-item" data-index="{{index}}" bindtap="onSelectOrder">
<view class="order-item-top">
<text class="order-id">{{item.dingdan_id}}</text>
<text class="order-status">{{item.zhuangtaiText}}</text>
</view>
<text class="order-desc">{{item.jieshao || '暂无介绍'}}</text>
<view class="order-item-bottom">
<text class="order-price">¥{{item.jine || '0'}}</text>
<text class="order-time">{{item.create_time}}</text>
</view>
<text class="order-send-hint">点击发送订单卡片</text>
</view>
</block>
<view class="empty-tip" wx:if="{{!loading && !filteredOrders.length}}">暂无相关订单</view>
</scroll-view>
</view>

View File

@@ -1,45 +1,140 @@
.order-sender-mask {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 2000;
}
.order-sender {
position: fixed; bottom: 0; left: 0; right: 0;
height: 360rpx;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
z-index: 2001;
transform: translateY(100%);
transition: 0.25s;
display: flex;
flex-direction: column;
}
.order-sender.show { transform: translateY(0); }
.header {
display: flex; justify-content: space-between;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.title { font-size: 32rpx; font-weight: 600; }
.close { font-size: 44rpx; color: #999; padding: 0 12rpx; }
.content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 40rpx;
}
.icon { font-size: 80rpx; margin-bottom: 20rpx; }
.notice {
font-size: 34rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
}
.sub {
font-size: 24rpx;
color: #999;
}
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
}
.order-sender {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 72vh;
max-height: 900rpx;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
z-index: 2001;
transform: translateY(100%);
transition: transform 0.25s;
display: flex;
flex-direction: column;
}
.order-sender.show {
transform: translateY(0);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.title {
font-size: 32rpx;
font-weight: 600;
}
.close {
font-size: 44rpx;
color: #999;
padding: 0 12rpx;
}
.search-row {
padding: 16rpx 24rpx;
flex-shrink: 0;
}
.search-input {
background: #f5f5f5;
border-radius: 32rpx;
padding: 16rpx 28rpx;
font-size: 28rpx;
}
.order-list {
flex: 1;
height: 0;
padding: 0 24rpx 24rpx;
box-sizing: border-box;
}
.loading-tip,
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 60rpx 0;
}
.order-item {
background: #fafafa;
border-radius: 16rpx;
padding: 20rpx 24rpx;
margin-bottom: 16rpx;
border: 1rpx solid #eee;
}
.order-item:active {
background: #f0f7ff;
}
.order-item-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.order-id {
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.order-status {
font-size: 22rpx;
color: #07c160;
background: #e8f8ee;
padding: 4rpx 14rpx;
border-radius: 20rpx;
}
.order-desc {
font-size: 26rpx;
color: #666;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 8rpx;
}
.order-item-bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-price {
font-size: 28rpx;
color: #ff6b00;
font-weight: 600;
}
.order-time {
font-size: 22rpx;
color: #aaa;
}
.order-send-hint {
display: block;
font-size: 22rpx;
color: #007aff;
margin-top: 10rpx;
}

View File

@@ -9,7 +9,7 @@
<text class="pindao-channel-no">{{channelNo}}</text>
</view>
<scroll-view class="pindao-scroll" scroll-y enhanced show-scrollbar="{{false}}">
<view wx:if="{{images.length === 0}}" class="pindao-empty">暂无频道内容</view>
<view wx:if="{{!images || images.length === 0}}" class="pindao-empty">暂无频道内容</view>
<image
wx:for="{{images}}"
wx:key="index"