第一次提交:微信小程序前端最新完整代码

This commit is contained in:
XingQue
2026-06-12 21:55:06 +08:00
commit 84be8489b4
330 changed files with 74263 additions and 0 deletions

31
.eslintrc.js Normal file
View File

@@ -0,0 +1,31 @@
/*
* Eslint config file
* Documentation: https://eslint.org/docs/user-guide/configuring/
* Install the Eslint extension before using this feature.
*/
module.exports = {
env: {
es6: true,
browser: true,
node: true,
},
ecmaFeatures: {
modules: true,
},
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
globals: {
wx: true,
App: true,
Page: true,
getCurrentPages: true,
getApp: true,
Component: true,
requirePlugin: true,
requireMiniProgram: true,
},
// extends: 'eslint:recommended',
rules: {},
}

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
miniprogram_npm/
project.private.config.json
.DS_Store
*.log

3
.vs/ProjectSettings.json Normal file
View File

@@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}

View File

@@ -0,0 +1,7 @@
{
"ExpandedNodes": [
""
],
"SelectedNode": "\\app.js",
"PreviewInSolutionExplorer": false
}

Binary file not shown.

View File

@@ -0,0 +1,37 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\\u5FAE\u4FE1\u5F00\u53D1\u5DE5\u5177\\\u4EE3\u7801\\a_long_dianjing\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\\u5FAE\u4FE1\u5F00\u53D1\u5DE5\u5177\\\u4EE3\u7801\\a_long_dianjing\\app.js||{14D17961-FE51-464D-9111-C4AF11D7D99A}",
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:app.js||{14D17961-FE51-464D-9111-C4AF11D7D99A}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 0,
"Children": [
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "app.js",
"DocumentMoniker": "C:\\\u5FAE\u4FE1\u5F00\u53D1\u5DE5\u5177\\\u4EE3\u7801\\a_long_dianjing\\app.js",
"RelativeDocumentMoniker": "app.js",
"ToolTip": "C:\\\u5FAE\u4FE1\u5F00\u53D1\u5DE5\u5177\\\u4EE3\u7801\\a_long_dianjing\\app.js",
"RelativeToolTip": "app.js",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001646|",
"WhenOpened": "2026-01-30T05:12:55.655Z",
"EditorCaption": ""
}
]
}
]
}
]
}

BIN
.vs/slnx.sqlite Normal file

Binary file not shown.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 GoEasy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,44 @@
# 大改备份与恢复说明
改造日期2026-06-09
## 如何一键恢复改造前(若已手动备份)
`_backup_pre_redesign/` 下对应文件夹覆盖回项目即可(见各子目录)。
## 本次改造改动文件清单
### 新增
- `utils/primary-role.js` — 三端固定 primaryRole
- `utils/avatar.js` — 统一默认头像
### TabBar
- `custom-tab-bar/index.js`
- `custom-tab-bar/index.wxml`
- `custom-tab-bar/index.wxss`
### 打手合一中心(主战场)
- `pages/dashouduan/dashouduan.js`
- `pages/dashouduan/dashouduan.wxml`
- `pages/dashouduan/dashouduan.wxss`
- `pages/dashouduan/dashouduan.json`
### 启动与 IM
- `app.js`
- `utils/chat-core.js`
### 老板端 / 认证
- `pages/wode/wode.wxml`
- `pages/renzheng/renzheng.js`
- `utils/role-tab-bar.js`
### 废弃独立 Tab路由保留自动跳转打手中心
- `pages/guanshiduan/guanshiduan.js` — onLoad redirect
- `pages/zuzhangduan/zuzhangduan.js` — onLoad redirect
## 改造要点
1. 去掉 TabBar「切换身份」
2. 三端固定normal / dashou / shangjia
3. 管事+组长功能合并进 dashouduan
4. 刷新按钮一次调用dashouxinxi + guanshixinxi + zuzhangxinxi
5. IM 打手端统一 Ds 前缀连接

417
_backup_pre_redesign/app.js Normal file
View File

@@ -0,0 +1,417 @@
// app.js - 稳健生产版本(修复 GoEasy 初始化错误 + 配置100%加载)
import GoEasy from './static/lib/goeasy-2.13.24.esm.min';
const ChatCore = require('./utils/chat-core');
import { check } from './utils/phone-auth';
// ===== 角色 → 默认首页(与 custom-tab-bar 完全一致) =====
const roleDefaultPage = {
normal: '/pages/index/index',
dashou: '/pages/dashouduan/dashouduan',
shangjia: '/pages/shangjiaduan/shangjiaduan',
guanshi: '/pages/guanshiduan/guanshiduan',
zuzhang: '/pages/zuzhangduan/zuzhangduan',
kaoheguan: '/pages/kaoheguan/kaoheguan'
};
App({
globalData: {
hasShownPopupOnColdStart: false,
apiBaseUrl: 'https://www.abas.asia/hqhd',
ossImageUrl: '',
morentouxiang: '',
dashouguize: '',
shangpinliebiao: [],
shangpinzhuanqu: [],
shangpinleixing: [],
shangpinlunbo: [],
shangpingonggao: '',
lunbozhanwei: '/images/lunbozhanwei.jpg',
dingdanTiaoshu: {
daifuwu: 0,
fuwuzhong: 0,
yiwancheng: 0,
yituikuan: 0
},
dashouqun: '',
dashouqunid: '',
guanshiqun: '',
guanshiqunid: '',
appId:'wx0e4be86faac4a8d1',
shangjiastatus: 0,
dashoustatus: 0,
guanshistatus: 0,
dashouNicheng: '',
zhanghaoStatus: null,
dashouzhuangtai: null,
yongjin: null,
zonge: null,
yajin: null,
chenghao: '',
jinfen: null,
clumber: [{
huiyuanid: '',
huiyuanming: '',
daoqi: ''
}],
chengjiaoliang: null,
zaixianZhuangtai: null,
guanshi: {
nicheng: '',
uid: '',
touxiang: '',
gszhstatus: '',
yaoqingzongshu: 0,
fenyongzonge: '0.00',
fenyongtixian: '0.00'
},
shangjia: {
sjzhzhuangtai: '',
sjyue: '',
fadanzong: null,
tuikuanzong: null,
riliushui: '',
yueliushui: ''
},
liaotian_liebiao: [],
xshenfen: 1,
goEasyConfig: null,
kefuConfig: {
link: '',
enterpriseId: ''
},
cosConfig: {
bucket: '',
region: 'ap-shanghai',
uploadPathPrefix: 'order/'
},
goEasyConnection: {
status: 'disconnected',
userId: '',
identityType: '',
lastConnectTime: 0,
autoReconnect: true,
reconnectAttempts: 0,
maxReconnectAttempts: 5,
heartbeatInterval: null,
cacheKeys: {
savedConnection: 'savedGoEasyConnection',
userId: 'goEasyUserId',
identityType: 'currentGoEasyIdentity',
connectTime: 'goEasyConnectTime'
},
config: {
heartbeatInterval: 20000,
reconnectDelay: 3000,
offlineTimeout: 30000,
cacheValidityHours: 12
}
},
messageManager: {
unreadTotal: 0,
unreadMap: {},
latestMessages: [],
notificationQueue: [],
notificationVisible: false,
lastNotificationTime: 0,
notificationCooldown: 3000,
tabBarIndex: 2,
showTabBarBadge: true,
customTabBar: true,
tabBarBadgeText: 0,
soundEnabled: true,
vibrationEnabled: true,
doNotDisturb: false,
doNotDisturbStart: '22:00',
doNotDisturbEnd: '08:00',
notificationStyle: {
position: 'top',
duration: 3000,
backgroundColor: '#00f7ff',
textColor: '#ffffff',
borderRadius: '16rpx',
boxShadow: '0 10rpx 30rpx rgba(0, 247, 255, 0.3)'
},
currentNotification: null,
cacheKeys: {
messageSettings: 'messageSettings',
unreadTotal: 'messageUnreadTotal',
latestMessages: 'latestMessages',
notificationMuted: 'notificationMuted'
}
},
pageState: {
currentPage: '',
isInChatPage: false,
currentChatId: '',
lastPageUpdate: 0
},
eventListeners: {},
debugMode: false,
currentUser: null,
currentRole: 'normal'
},
// ========== 核心启动流程 ==========
async onLaunch() {
// ---------- ① 立即隐藏“返回首页”按钮 ----------
wx.hideHomeButton();
wx.onAppRoute(() => {
wx.hideHomeButton();
});
// ---------- ② 恢复角色并跳转 ----------
const savedRole = wx.getStorageSync('currentRole') || 'normal';
this.globalData.currentRole = savedRole;
const targetPage = roleDefaultPage[savedRole] || '/pages/index/index';
if (savedRole !== 'normal' || targetPage !== '/pages/index/index') {
setTimeout(() => {
wx.reLaunch({ url: targetPage });
}, 0);
}
// ---------- ③ 立刻应用本地缓存配置 ----------
const cachedConfig = this.readConfigFromStorage();
if (cachedConfig) {
this.applyDynamicConfig(cachedConfig);
}
// ---------- ④ 初始化基础设施 ----------
this.initGoEasyWithConfig(); // 只有appkey有效时才真正初始化
ChatCore.initGlobalMessageSystem(this);
this.initCurrentUser();
// ---------- ⑤ 建立连接 ----------
const saved = this.getSavedConnection();
console.log('【启动】缓存身份:', saved ? saved.identityType : '无',
'userId:', saved ? saved.userId : '无');
setTimeout(() => {
this.ensureConnection();
}, 300);
// ---------- ⑥ 后台静默获取最新配置 ----------
this.fetchConfigSafely()
.then(latestConfig => {
this.saveConfigToStorage(latestConfig);
this.applyDynamicConfig(latestConfig);
// 重新尝试初始化 GoEasy如果之前因缺少appkey而跳过现在会成功
this.initGoEasyWithConfig();
console.log('远程配置更新完成');
})
.catch(err => {
console.warn('远程配置获取失败,继续使用本地缓存', err);
});
// ---------- ⑦ 手机号认证检查 ----------
try {
const needAuth = await check();
if (needAuth) {
wx.reLaunch({ url: '/pages/phone-auth/phone-auth' });
return;
}
} catch (e) {}
},
// ========== 连接管理方法 ==========
getSavedConnection() {
try {
const saved = wx.getStorageSync(
this.globalData.goEasyConnection.cacheKeys.savedConnection
);
return saved || null;
} catch (e) {
return null;
}
},
ensureConnection() {
const conn = this.globalData.goEasyConnection;
const userId = wx.getStorageSync(conn.cacheKeys.userId);
if (userId && conn.status !== 'connected') {
console.log('尝试建立GoEasy连接userId:', userId);
// 此处可调用实际的连接逻辑,目前仅做标记
}
},
// ========== 配置缓存读写 ==========
CONFIG_CACHE_KEY: 'app_dynamic_config',
readConfigFromStorage() {
try {
const data = wx.getStorageSync(this.CONFIG_CACHE_KEY);
if (data && typeof data === 'object') {
return data;
}
} catch (e) {}
return null;
},
saveConfigToStorage(config) {
try {
if (config && typeof config === 'object') {
wx.setStorageSync(this.CONFIG_CACHE_KEY, config);
}
} catch (e) {}
},
// ========== 安全获取远程配置(永不报错) ==========
fetchConfigSafely() {
return new Promise((resolve) => {
wx.request({
url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq',
method: 'POST',
header: { 'content-type': 'application/json' },
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 0 && res.data.data) {
resolve(res.data.data);
} else {
console.warn('远程配置接口返回异常,使用已有配置');
resolve(this.readConfigFromStorage() || {});
}
},
fail: (err) => {
console.warn('远程配置网络请求失败,使用已有配置', err);
resolve(this.readConfigFromStorage() || {});
}
});
});
},
// ========== 原有方法(保留,仅增强) ==========
fetchDynamicConfig() {
return new Promise((resolve, reject) => {
wx.request({
url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq',
method: 'POST',
header: { 'content-type': 'application/json' },
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 0) {
resolve(res.data.data);
} else {
reject(new Error(res.data?.msg || '接口返回错误'));
}
},
fail: reject
});
});
},
applyDynamicConfig(config) {
if (!config || typeof config !== 'object') return;
if (config.cos) {
this.globalData.ossImageUrl = config.cos.ossImageUrl || '';
this.globalData.cosConfig = {
bucket: config.cos.bucket || '',
region: config.cos.region || 'ap-shanghai',
uploadPathPrefix: config.cos.uploadPathPrefix || 'order/'
};
}
if (config.goEasy) {
this.globalData.goEasyConfig = {
host: config.goEasy.host || 'hangzhou.goeasy.io',
appkey: config.goEasy.appkey || ''
};
}
if (config.otherConfig) {
this.globalData.morentouxiang = config.otherConfig.morentouxiang || '';
this.globalData.dashouguize = config.otherConfig.dashouguize || '';
}
if (config.kefu) {
this.globalData.kefuConfig = {
link: config.kefu.link || '',
enterpriseId: config.kefu.enterpriseId || ''
};
}
},
// 🔥 关键修复:只在有效 appkey 时初始化,且避免重复初始化
initGoEasyWithConfig() {
const cfg = this.globalData.goEasyConfig;
if (!cfg || !cfg.appkey) {
console.warn('GoEasy appkey 未配置,聊天功能将在配置就绪后自动恢复');
return;
}
// 如果已经初始化过,就不再重复创建实例
if (wx.goEasy && wx.GoEasy) {
console.log('GoEasy 已初始化,跳过');
return;
}
try {
wx.goEasy = GoEasy.getInstance({
host: cfg.host || 'hangzhou.goeasy.io',
appkey: cfg.appkey,
modules: ['im', 'pubsub']
});
wx.GoEasy = GoEasy;
console.log('GoEasy 初始化成功');
} catch (error) {
console.error('GoEasy 初始化失败:', error);
}
},
initCurrentUser() {
const uid = wx.getStorageSync('uid');
if (uid) {
this.globalData.currentUser = {
id: uid,
name: '用户' + uid.substring(0, 6),
avatar: this.globalData.ossImageUrl + this.globalData.morentouxiang
};
}
},
emitEvent(eventName, data) {
if (this.globalData.eventListeners[eventName]) {
this.globalData.eventListeners[eventName].forEach(callback => {
try { callback(data); } catch (error) { console.error(error); }
});
}
},
/** 切换当前角色并通知 custom-tab-bar 立即刷新Storage + globalData + 事件) */
setCurrentRole(role) {
if (!role) return;
this.globalData.currentRole = role;
wx.setStorageSync('currentRole', role);
this.emitEvent('currentRoleChanged', { role });
},
on(eventName, callback) {
if (!this.globalData.eventListeners[eventName]) {
this.globalData.eventListeners[eventName] = [];
}
this.globalData.eventListeners[eventName].push(callback);
},
off(eventName, callback) {
if (this.globalData.eventListeners[eventName]) {
const index = this.globalData.eventListeners[eventName].indexOf(callback);
if (index > -1) {
this.globalData.eventListeners[eventName].splice(index, 1);
}
}
}
});

View File

@@ -0,0 +1,268 @@
// utils/chat-core.js - 最终角标稳定版
import GoEasy from '../static/lib/goeasy-2.13.24.esm.min';
import { jianquanxian } from './imAuth/jianquanxian';
let _convListener = null;
let _privateListener = null;
let _groupListener = null;
let _notificationHandler = null;
function initGlobalMessageSystem(app) {
app.ensureConnection = () => ensureConnection(app);
app.loadConversations = () => loadConversations(app);
app.disconnectGoEasy = () => disconnectGoEasy(app);
app.switchRoleAndReconnect = (newRole) => switchRoleAndReconnect(app, newRole);
app.updateUnreadCount = (count) => updateTabBarBadge(app, count || 0);
try {
const muted = wx.getStorageSync('notificationMuted');
if (muted !== undefined) app.globalData.messageManager.notificationMuted = muted;
} catch(e) {}
initAppStateListener(app);
initNetworkListener(app);
ensureConnection(app);
}
function getCurrentUserId() {
return (wx.goEasy && wx.goEasy.im && wx.goEasy.im.userId) || null;
}
async function ensureConnection(app) {
const role = app.globalData.currentRole || 'normal';
const uid = wx.getStorageSync('uid');
if (!uid) return;
const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz', kaoheguan: 'Kh' };
const prefix = prefixMap[role] || 'Boss';
const targetUserId = prefix + uid;
const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
if (status === 'connected' || status === 'reconnected') {
const currentUserId = getCurrentUserId();
if (currentUserId === targetUserId) {
registerGlobalListeners(app);
await subscribeAllGroups(app);
loadConversations(app);
registerNotification(app);
// 强制立即更新一次角标,避免切后台后不显示
setTimeout(() => { loadConversations(app); }, 500);
return;
} else {
wx.goEasy.disconnect();
}
}
connectGoEasy(app, targetUserId, role);
}
function connectGoEasy(app, userId, identityType) {
const currentUser = app.globalData.currentUser || {
id: wx.getStorageSync('uid'),
name: '用户' + (wx.getStorageSync('uid') || '').substring(0, 6),
avatar: app.globalData.ossImageUrl + app.globalData.morentouxiang
};
wx.goEasy.connect({
id: userId,
data: {
name: currentUser.name,
avatar: currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
identityType: identityType
},
onSuccess: async () => {
wx.setStorageSync('savedGoEasyConnection', JSON.stringify({
userId, identityType, lastConnectTime: Date.now(), status: 'connected'
}));
registerGlobalListeners(app);
await subscribeAllGroups(app);
loadConversations(app);
registerNotification(app);
},
onFailed: (error) => {
console.error('GoEasy连接失败', error);
if (error.code === 408) {
registerGlobalListeners(app);
loadConversations(app);
registerNotification(app);
} else {
wx.showToast({ title: '连接失败,请重试', icon: 'none' });
}
}
});
}
function subscribeAllGroups(app) {
return new Promise((resolve) => {
if (!wx.goEasy || !wx.goEasy.im) return resolve();
wx.goEasy.im.latestConversations({
onSuccess: (result) => {
const conversations = result?.content?.conversations || result?.conversations || [];
const groupIds = conversations.filter(c => c.type === 'group' && c.groupId).map(c => c.groupId);
if (groupIds.length && wx.goEasy.im.subscribeGroup) {
wx.goEasy.im.subscribeGroup({
groupIds: groupIds,
onSuccess: () => resolve(),
onFailed: () => resolve()
});
} else {
resolve();
}
},
onFailed: () => resolve()
});
});
}
function registerGlobalListeners(app) {
if (!wx.goEasy || !wx.goEasy.im) return;
if (_convListener) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _convListener);
}
if (_privateListener) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _privateListener);
}
if (_groupListener) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _groupListener);
}
_convListener = (result) => {
const unreadTotal = result.unreadTotal || 0;
updateTabBarBadge(app, unreadTotal);
app.emitEvent('conversationsUpdated', result);
};
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _convListener);
_privateListener = () => { loadConversations(app); };
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _privateListener);
_groupListener = () => { loadConversations(app); };
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _groupListener);
}
function loadConversations(app) {
if (!getCurrentUserId()) return;
wx.goEasy.im.latestConversations({
onSuccess: (result) => {
const unreadTotal = result.unreadTotal || 0;
updateTabBarBadge(app, unreadTotal);
app.emitEvent('conversationsUpdated', result);
subscribeAllGroups(app);
},
onFailed: (err) => console.error('加载会话失败', err)
});
}
function updateTabBarBadge(app, unreadCount) {
const badgeText = unreadCount > 0 ? (unreadCount > 99 ? '99+' : String(unreadCount)) : '';
app.globalData.messageManager.unreadTotal = unreadCount;
app.emitEvent('tabBarBadgeChanged', { badgeText, showRedDot: unreadCount > 0 });
// 直接更新组件,确保角标立即显示
try {
const pages = getCurrentPages();
if (pages.length) {
const tabBar = pages[pages.length-1].selectComponent('#custom-tab-bar');
if (tabBar && tabBar.setData) tabBar.setData({ badgeText });
// 兼容 getTabBar 方式
const page = pages[pages.length-1];
if (page.getTabBar) {
const tabBarInstance = page.getTabBar();
if (tabBarInstance && tabBarInstance.setData) tabBarInstance.setData({ badgeText });
}
}
} catch(e) {}
wx.setStorageSync('messageUnreadTotal', unreadCount);
}
function registerNotification(app) {
if (!wx.goEasy || !wx.goEasy.im) return;
if (_notificationHandler) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _notificationHandler);
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _notificationHandler);
}
_notificationHandler = (message) => {
if (app.globalData.messageManager.notificationMuted) return;
const pages = getCurrentPages();
if (pages.length) {
const curPage = pages[pages.length - 1];
if (curPage.route === 'pages/liaotian/liaotian' || curPage.route === 'pages/qunliaotian/qunliaotian') return;
}
const senderName = message.senderData?.name || (message.senderId || '用户');
let content = '';
if (message.type === 'text') content = message.payload?.text || '[文本]';
else if (message.type === 'image') content = '[图片]';
else if (message.type === 'audio') content = '[语音]';
else content = '[新消息]';
if (app.globalData.globalNotification && app.globalData.globalNotification.show) {
app.globalData.globalNotification.show({
senderName, content, message,
notificationType: message.groupId ? 'group' : 'private',
groupId: message.groupId, orderId: message.orderId
});
} else {
wx.showToast({ title: `${senderName}: ${content}`, icon: 'none', duration: 2000 });
}
};
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _notificationHandler);
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _notificationHandler);
}
function disconnectGoEasy(app) {
if (_convListener) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, _convListener);
_convListener = null;
}
if (_privateListener) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _privateListener);
_privateListener = null;
}
if (_groupListener) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _groupListener);
_groupListener = null;
}
if (_notificationHandler) {
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, _notificationHandler);
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, _notificationHandler);
_notificationHandler = null;
}
wx.goEasy.disconnect({ onSuccess: () => {}, onFailed: () => {} });
wx.removeStorageSync('savedGoEasyConnection');
}
async function switchRoleAndReconnect(app, newRole) {
await disconnectGoEasy(app);
app.globalData.currentRole = newRole;
wx.setStorageSync('currentRole', newRole);
ensureConnection(app);
return true;
}
function initAppStateListener(app) {
let isActive = true;
wx.onAppShow(() => {
if (!isActive) return;
const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
if (status !== 'connected' && status !== 'reconnected') {
ensureConnection(app);
} else {
// 连接存在但可能角标未更新,强制拉取并更新
loadConversations(app);
// 额外确保角标组件刷新
setTimeout(() => {
const unread = app.globalData.messageManager.unreadTotal || 0;
updateTabBarBadge(app, unread);
}, 500);
}
});
wx.onAppHide(() => {});
}
function initNetworkListener(app) {
wx.onNetworkStatusChange((res) => {
if (res.isConnected) {
setTimeout(() => {
ensureConnection(app);
}, 1000);
}
});
}
module.exports = { initGlobalMessageSystem };

View File

@@ -0,0 +1,220 @@
// custom-tab-bar/index.js
const app = getApp();
const roleCfg = {
normal: {
name: '点单老板',
defaultPage: '/pages/index/index',
tabList: [
{ pagePath: 'pages/index/index', text: '商城', iconPath: '/images/zhuye.png', selectedIconPath: '/images/zhuye.png' },
{ pagePath: 'pages/fenlei/fenlei', text: '分类', iconPath: '/images/fenlei.png', selectedIconPath: '/images/fenlei.png' },
{ pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' },
{ pagePath: 'pages/wode/wode', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' }
]
},
dashou: {
name: '打手',
defaultPage: '/pages/jiedan/jiedan',
tabList: [
{ pagePath: 'pages/jiedan/jiedan', text: '接单池', iconPath: '/images/jiedanchi.png', selectedIconPath: '/images/jiedanchi.png' },
{ pagePath: 'pages/dashoudingdan/dashoudingdan', text: '订单', iconPath: '/images/dingdan.png', selectedIconPath: '/images/dingdan.png' },
{ pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' },
{ pagePath: 'pages/dashouduan/dashouduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' }
]
},
shangjia: {
name: '商家',
defaultPage: 'pages/sjdingdan/sjdingdan',
tabList: [
{ pagePath: 'pages/sjdingdan/sjdingdan', text: '订单', iconPath: '/images/dingdan.png', selectedIconPath: '/images/dingdan.png' },
{ pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' },
{ pagePath: 'pages/shangjiaduan/shangjiaduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' }
]
},
guanshi: {
name: '管事',
defaultPage: '/pages/guanshiduan/guanshiduan',
tabList: [
{ pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' },
{ pagePath: 'pages/guanshiduan/guanshiduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' }
]
},
zuzhang: {
name: '组长',
defaultPage: '/pages/zuzhangduan/zuzhangduan',
tabList: [
{ pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' },
{ pagePath: 'pages/zuzhangduan/zuzhangduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' }
]
},
kaoheguan: {
name: '考核官',
defaultPage: '/pages/kaoheguan/kaoheguan',
tabList: [
{ pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' },
{ pagePath: 'pages/kaoheguan/kaoheguan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' }
]
}
};
Component({
data: {
selectedIndex: 0,
tabList: [],
currentRole: 'normal',
showRolePicker: false,
availableRoles: [],
badgeText: ''
},
lifetimes: {
attached() {
this.refresh();
this._badgeListener = (data) => {
if (data.badgeText !== undefined) {
this.setData({ badgeText: data.badgeText });
}
};
this._roleListener = () => {
this.refresh();
};
app.on('tabBarBadgeChanged', this._badgeListener);
app.on('currentRoleChanged', this._roleListener);
},
detached() {
if (this._badgeListener) {
app.off('tabBarBadgeChanged', this._badgeListener);
}
if (this._roleListener) {
app.off('currentRoleChanged', this._roleListener);
}
}
},
pageLifetimes: {
show() {
this.refresh();
const globalBadge = app.globalData.messageManager?.tabBarBadgeText || '';
if (this.data.badgeText !== globalBadge) {
this.setData({ badgeText: globalBadge });
}
this._syncUnreadWithDebounce();
}
},
methods: {
_syncUnreadWithDebounce() {
if (this._syncTimer) clearTimeout(this._syncTimer);
this._syncTimer = setTimeout(() => {
if (app && app.loadConversations) {
app.loadConversations();
}
}, 300);
},
refresh() {
const token = wx.getStorageSync('token');
// 未登录时强制点单端,避免清除缓存后 TabBar 仍显示其他身份
let role = wx.getStorageSync('currentRole') || app.globalData.currentRole || 'normal';
if (!token) {
role = 'normal';
app.globalData.currentRole = 'normal';
wx.setStorageSync('currentRole', 'normal');
} else {
app.globalData.currentRole = role;
}
let cfg = roleCfg[role];
if (!cfg) return;
// 打手身份下,如果 isJinpai=1悄悄把“我的”页面换成金牌页面
if (role === 'dashou') {
const isJinpai = wx.getStorageSync('isJinpai') === 1;
if (isJinpai) {
cfg = {
...cfg,
tabList: cfg.tabList.map(item => {
if (item.pagePath === 'pages/dashouduan/dashouduan') {
return { ...item, pagePath: 'pages/jinpaids/jinpaids' };
}
return { ...item };
})
};
}
}
const list = cfg.tabList.map((v, i) => ({ ...v, index: i }));
const pages = getCurrentPages();
const curRoute = pages.length ? pages[pages.length - 1].route : '';
let idx = list.findIndex(t => t.pagePath === curRoute);
if (idx === -1) idx = 0;
const avail = this._getAvail();
this.setData({ currentRole: role, tabList: list, selectedIndex: idx, availableRoles: avail });
},
_getAvail() {
const token = wx.getStorageSync('token');
if (!token) {
return [{ key: 'normal', name: '点单老板' }];
}
const isActive = (key) => Number(wx.getStorageSync(key)) === 1;
const arr = [{ key: 'normal', name: '点单老板' }];
if (isActive('dashoustatus')) arr.push({ key: 'dashou', name: '打手' });
if (isActive('shangjiastatus')) arr.push({ key: 'shangjia', name: '商家' });
if (isActive('guanshistatus')) arr.push({ key: 'guanshi', name: '管事' });
if (isActive('zuzhangstatus')) arr.push({ key: 'zuzhang', name: '组长' });
if (isActive('kaoheguanstatus')) arr.push({ key: 'kaoheguan', name: '考核官' });
return arr;
},
toggleRolePicker() {
this.setData({ showRolePicker: !this.data.showRolePicker });
},
async selectRole(e) {
const newRole = e.currentTarget.dataset.role;
if (newRole === this.data.currentRole) {
this.setData({ showRolePicker: false });
return;
}
this.setData({ showRolePicker: false });
// 1. 立即更新角色并保存
if (app.setCurrentRole) {
app.setCurrentRole(newRole);
} else {
app.globalData.currentRole = newRole;
wx.setStorageSync('currentRole', newRole);
app.emitEvent('currentRoleChanged', { role: newRole });
}
// 2. 🔥 确定跳转页面:所有角色都去“我的”页面
let targetPage = '';
if (newRole === 'dashou') {
// 打手需要根据金牌状态决定跳转
const isJinpai = wx.getStorageSync('isJinpai') === 1;
targetPage = isJinpai ? '/pages/jinpaids/jinpaids' : '/pages/dashouduan/dashouduan';
} else {
// 其他角色直接取 tabList 最后一个页面
const cfg = roleCfg[newRole];
if (cfg && cfg.tabList.length > 0) {
targetPage = '/' + cfg.tabList[cfg.tabList.length - 1].pagePath;
}
}
// 3. 跳转
if (targetPage) {
wx.reLaunch({ url: targetPage });
}
// 4. 后台异步切换连接
app.switchRoleAndReconnect(newRole).catch(err => {
console.error('后台切换连接失败:', err);
});
},
switchTab(e) {
const path = e.currentTarget.dataset.path;
const idx = e.currentTarget.dataset.index;
if (idx === this.data.selectedIndex) return;
wx.redirectTo({ url: '/' + path });
}
}
});

View File

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

View File

@@ -0,0 +1,42 @@
<view class="custom-tab-bar">
<view class="bar-row">
<!-- 左侧椭圆身份按钮 -->
<view class="role-switch-btn" catchtap="toggleRolePicker">
<image class="role-switch-icon" src="/images/role-switch.png" mode="aspectFit" />
<text class="role-switch-text">切换身份</text>
</view>
<!-- 右侧纯黑闪光胶囊 -->
<view class="tab-capsule">
<block wx:for="{{tabList}}" wx:key="index">
<view class="tab-item {{selectedIndex === index ? 'active' : ''}}"
data-path="{{item.pagePath}}"
data-index="{{index}}"
catchtap="switchTab">
<!-- 🔥 根据页面路径判断是否显示角标,不再写死 index -->
<view class="badge-container" wx:if="{{item.pagePath === 'pages/xiaoxi/xiaoxi' && badgeText}}">
<view class="tab-badge">{{badgeText}}</view>
</view>
<image class="tab-icon" src="{{selectedIndex === index ? item.selectedIconPath : item.iconPath}}" mode="aspectFit" />
<text class="tab-text">{{item.text}}</text>
</view>
</block>
</view>
</view>
<!-- 身份选择弹窗 -->
<view wx:if="{{showRolePicker}}" class="picker-mask" catchtap="toggleRolePicker"></view>
<view wx:if="{{showRolePicker}}" class="picker-panel">
<view class="picker-header">
<text class="picker-title">切换身份</text>
<text class="picker-close" catchtap="toggleRolePicker">✕</text>
</view>
<block wx:for="{{availableRoles}}" wx:key="key">
<view class="role-option {{currentRole === item.key ? 'active' : ''}}"
data-role="{{item.key}}" catchtap="selectRole">
<text>{{item.name}}</text>
<text wx:if="{{currentRole === item.key}}" class="check-mark">✓</text>
</view>
</block>
</view>
</view>

View File

@@ -0,0 +1,165 @@
/* custom-tab-bar/index.wxss - 角标浮于右上角,可超出胶囊 */
.custom-tab-bar {
position: fixed;
bottom: 0; left: 0; right: 0;
z-index: 9999;
display: flex;
justify-content: center;
align-items: flex-end;
padding-bottom: env(safe-area-inset-bottom);
background: transparent;
}
.bar-row {
display: flex;
align-items: center;
margin-bottom: 15rpx;
padding: 0 20rpx;
width: 100%;
box-sizing: border-box;
}
/* 左侧椭圆身份按钮 */
.role-switch-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 110rpx;
height: 100rpx;
border-radius: 50rpx;
background: #000000;
border: 1px solid rgba(255,255,255,0.15);
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.8);
margin-right: 20rpx;
flex-shrink: 0;
}
.role-switch-icon {
width: 40rpx;
height: 40rpx;
margin-bottom: 2rpx;
}
.role-switch-text {
font-size: 22rpx;
font-weight: 600;
color: #00f7ff;
letter-spacing: 1rpx;
}
/* 纯黑闪光胶囊移除overflow保证角标可见 */
.tab-capsule {
flex: 1;
display: flex;
align-items: center;
justify-content: space-around;
height: 100rpx;
background: #000000;
border-radius: 50rpx;
border: 1px solid rgba(255,255,255,0.15);
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.8);
padding: 0 20rpx;
position: relative;
}
.tab-capsule::before {
content: '';
position: absolute;
top: 0; left: -100%;
width: 60%; height: 100%;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.25) 20%, rgba(255,255,255,0.35) 50%, rgba(255,255,255,0.25) 80%, transparent 100%);
animation: shine 3s infinite;
z-index: 1;
}
@keyframes shine {
0% { left: -100%; }
25% { left: 150%; }
100% { left: 150%; }
}
.tab-item {
position: relative; /* 角标的定位参考 */
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 70rpx;
}
.tab-icon {
width: 44rpx;
height: 44rpx;
margin-bottom: 4rpx;
filter: brightness(0) invert(1);
opacity: 0.9;
}
.tab-text {
font-size: 20rpx;
color: #aaa;
z-index: 2;
}
.active .tab-icon {
opacity: 1;
filter: brightness(0) invert(1) drop-shadow(0 0 8rpx #00aaff);
}
.active .tab-text {
color: #00aaff;
font-weight: 500;
}
/* 🔴 角标容器:绝对定位到图标右上角,允许超出 */
.badge-container {
position: absolute;
top: -10rpx; /* 往上提,更接近图标右上角 */
right: -15rpx; /* 往右伸超出tab-item */
z-index: 100;
pointer-events: none; /* 不阻挡点击 */
}
.tab-badge {
min-width: 36rpx;
height: 36rpx;
line-height: 36rpx;
padding: 0 10rpx;
background: #ff3b30;
color: #fff;
font-size: 22rpx;
font-weight: bold;
border-radius: 36rpx;
text-align: center;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.3);
border: 1rpx solid rgba(255,255,255,0.4);
}
/* 弹窗部分保持不变 */
.picker-mask {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 10000;
}
.picker-panel {
position: fixed; bottom: 0; left: 0; right: 0;
background: #000000;
border-radius: 30rpx 30rpx 0 0;
z-index: 10001;
max-height: 70vh;
overflow-y: auto;
}
.picker-header {
display: flex; justify-content: space-between; align-items: center;
padding: 30rpx 40rpx 20rpx;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.picker-title { font-size: 32rpx; color: #00f7ff; }
.picker-close { font-size: 36rpx; color: #aaa; padding: 0 10rpx; }
.role-option {
display: flex; justify-content: space-between; align-items: center;
padding: 28rpx 40rpx;
border-bottom: 1px solid rgba(255,255,255,0.05);
color: #ccc; font-size: 28rpx;
}
.role-option.active { color: #00f7ff; }
.check-mark { font-size: 32rpx; color: #00f7ff; }
page {
padding-bottom: 100rpx;
}

415
app.js Normal file
View File

@@ -0,0 +1,415 @@
// app.js - 稳健生产版本(修复 GoEasy 初始化错误 + 配置100%加载)
import GoEasy from './static/lib/goeasy-2.13.24.esm.min';
const ChatCore = require('./utils/chat-core');
import { check } from './utils/phone-auth';
import { getPrimaryRole, lockPrimaryRole, migrateLegacyCenterRole, PRIMARY_DEFAULT_PAGES } from './utils/primary-role';
// ===== 三端固定首页(与 custom-tab-bar 一致) =====
const roleDefaultPage = PRIMARY_DEFAULT_PAGES;
App({
globalData: {
hasShownPopupOnColdStart: false,
apiBaseUrl: 'https://www.abas.asia/hqhd',
ossImageUrl: '',
morentouxiang: '',
dashouguize: '',
shangpinliebiao: [],
shangpinzhuanqu: [],
shangpinleixing: [],
shangpinlunbo: [],
shangpingonggao: '',
lunbozhanwei: '/images/lunbozhanwei.jpg',
dingdanTiaoshu: {
daifuwu: 0,
fuwuzhong: 0,
yiwancheng: 0,
yituikuan: 0
},
dashouqun: '',
dashouqunid: '',
guanshiqun: '',
guanshiqunid: '',
appId:'wx0e4be86faac4a8d1',
shangjiastatus: 0,
dashoustatus: 0,
guanshistatus: 0,
dashouNicheng: '',
zhanghaoStatus: null,
dashouzhuangtai: null,
yongjin: null,
zonge: null,
yajin: null,
chenghao: '',
jinfen: null,
clumber: [{
huiyuanid: '',
huiyuanming: '',
daoqi: ''
}],
chengjiaoliang: null,
zaixianZhuangtai: null,
guanshi: {
nicheng: '',
uid: '',
touxiang: '',
gszhstatus: '',
yaoqingzongshu: 0,
fenyongzonge: '0.00',
fenyongtixian: '0.00'
},
shangjia: {
sjzhzhuangtai: '',
sjyue: '',
fadanzong: null,
tuikuanzong: null,
riliushui: '',
yueliushui: ''
},
liaotian_liebiao: [],
xshenfen: 1,
goEasyConfig: null,
kefuConfig: {
link: '',
enterpriseId: ''
},
cosConfig: {
bucket: '',
region: 'ap-shanghai',
uploadPathPrefix: 'order/'
},
goEasyConnection: {
status: 'disconnected',
userId: '',
identityType: '',
lastConnectTime: 0,
autoReconnect: true,
reconnectAttempts: 0,
maxReconnectAttempts: 5,
heartbeatInterval: null,
cacheKeys: {
savedConnection: 'savedGoEasyConnection',
userId: 'goEasyUserId',
identityType: 'currentGoEasyIdentity',
connectTime: 'goEasyConnectTime'
},
config: {
heartbeatInterval: 20000,
reconnectDelay: 3000,
offlineTimeout: 30000,
cacheValidityHours: 12
}
},
messageManager: {
unreadTotal: 0,
unreadMap: {},
latestMessages: [],
notificationQueue: [],
notificationVisible: false,
lastNotificationTime: 0,
notificationCooldown: 3000,
tabBarIndex: 2,
showTabBarBadge: true,
customTabBar: true,
tabBarBadgeText: 0,
soundEnabled: true,
vibrationEnabled: true,
doNotDisturb: false,
doNotDisturbStart: '22:00',
doNotDisturbEnd: '08:00',
notificationStyle: {
position: 'top',
duration: 3000,
backgroundColor: '#00f7ff',
textColor: '#ffffff',
borderRadius: '16rpx',
boxShadow: '0 10rpx 30rpx rgba(0, 247, 255, 0.3)'
},
currentNotification: null,
cacheKeys: {
messageSettings: 'messageSettings',
unreadTotal: 'messageUnreadTotal',
latestMessages: 'latestMessages',
notificationMuted: 'notificationMuted'
}
},
pageState: {
currentPage: '',
isInChatPage: false,
currentChatId: '',
lastPageUpdate: 0
},
eventListeners: {},
debugMode: false,
currentUser: null,
currentRole: 'normal',
primaryRole: 'normal'
},
// ========== 核心启动流程 ==========
async onLaunch() {
// ---------- ① 立即隐藏“返回首页”按钮 ----------
wx.hideHomeButton();
wx.onAppRoute((res) => {
wx.hideHomeButton();
const path = (res && res.path) || '';
if (path === 'pages/guanshiduan/guanshiduan' || path === 'pages/zuzhangduan/zuzhangduan') {
migrateLegacyCenterRole(this);
lockPrimaryRole('dashou', this);
}
});
// ---------- ② 恢复主端并跳转(旧管事/组长标识自动归打手端) ----------
migrateLegacyCenterRole(this);
const savedRole = getPrimaryRole(this);
lockPrimaryRole(savedRole, this);
const targetPage = roleDefaultPage[savedRole] || '/pages/index/index';
if (savedRole !== 'normal') {
setTimeout(() => {
wx.reLaunch({ url: targetPage });
}, 0);
}
// ---------- ③ 立刻应用本地缓存配置 ----------
const cachedConfig = this.readConfigFromStorage();
if (cachedConfig) {
this.applyDynamicConfig(cachedConfig);
}
// ---------- ④ 初始化基础设施 ----------
this.initGoEasyWithConfig(); // 只有appkey有效时才真正初始化
ChatCore.initGlobalMessageSystem(this);
this.initCurrentUser();
// ---------- ⑤ 建立连接 ----------
const saved = this.getSavedConnection();
console.log('【启动】缓存身份:', saved ? saved.identityType : '无',
'userId:', saved ? saved.userId : '无');
setTimeout(() => {
if (this.startImWhenReady) this.startImWhenReady();
else if (this.ensureConnection) this.ensureConnection();
}, 300);
// ---------- ⑥ 后台静默获取最新配置 ----------
this.fetchConfigSafely()
.then(latestConfig => {
this.saveConfigToStorage(latestConfig);
this.applyDynamicConfig(latestConfig);
this.initGoEasyWithConfig();
this.initCurrentUser();
if (this.startImWhenReady) this.startImWhenReady();
else if (this.connectForCurrentRole) this.connectForCurrentRole();
console.log('远程配置更新完成');
})
.catch(err => {
console.warn('远程配置获取失败,继续使用本地缓存', err);
});
// ---------- ⑦ 手机号认证检查 ----------
try {
const needAuth = await check();
if (needAuth) {
wx.reLaunch({ url: '/pages/phone-auth/phone-auth' });
return;
}
} catch (e) {}
},
// ========== 连接管理方法 ==========
getSavedConnection() {
try {
const saved = wx.getStorageSync(
this.globalData.goEasyConnection.cacheKeys.savedConnection
);
return saved || null;
} catch (e) {
return null;
}
},
// ========== 配置缓存读写 ==========
CONFIG_CACHE_KEY: 'app_dynamic_config',
readConfigFromStorage() {
try {
const data = wx.getStorageSync(this.CONFIG_CACHE_KEY);
if (data && typeof data === 'object') {
return data;
}
} catch (e) {}
return null;
},
saveConfigToStorage(config) {
try {
if (config && typeof config === 'object') {
wx.setStorageSync(this.CONFIG_CACHE_KEY, config);
}
} catch (e) {}
},
// ========== 安全获取远程配置(永不报错) ==========
fetchConfigSafely() {
return new Promise((resolve) => {
wx.request({
url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq',
method: 'POST',
header: { 'content-type': 'application/json' },
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 0 && res.data.data) {
resolve(res.data.data);
} else {
console.warn('远程配置接口返回异常,使用已有配置');
resolve(this.readConfigFromStorage() || {});
}
},
fail: (err) => {
console.warn('远程配置网络请求失败,使用已有配置', err);
resolve(this.readConfigFromStorage() || {});
}
});
});
},
// ========== 原有方法(保留,仅增强) ==========
fetchDynamicConfig() {
return new Promise((resolve, reject) => {
wx.request({
url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq',
method: 'POST',
header: { 'content-type': 'application/json' },
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 0) {
resolve(res.data.data);
} else {
reject(new Error(res.data?.msg || '接口返回错误'));
}
},
fail: reject
});
});
},
applyDynamicConfig(config) {
if (!config || typeof config !== 'object') return;
if (config.cos) {
this.globalData.ossImageUrl = config.cos.ossImageUrl || '';
this.globalData.cosConfig = {
bucket: config.cos.bucket || '',
region: config.cos.region || 'ap-shanghai',
uploadPathPrefix: config.cos.uploadPathPrefix || 'order/'
};
}
if (config.goEasy) {
this.globalData.goEasyConfig = {
host: config.goEasy.host || 'hangzhou.goeasy.io',
appkey: config.goEasy.appkey || ''
};
}
if (config.otherConfig) {
this.globalData.morentouxiang = config.otherConfig.morentouxiang || '';
this.globalData.dashouguize = config.otherConfig.dashouguize || '';
}
if (config.kefu) {
this.globalData.kefuConfig = {
link: config.kefu.link || '',
enterpriseId: config.kefu.enterpriseId || ''
};
}
},
// 🔥 关键修复:只在有效 appkey 时初始化,且避免重复初始化
initGoEasyWithConfig() {
const cfg = this.globalData.goEasyConfig;
if (!cfg || !cfg.appkey) {
console.warn('GoEasy appkey 未配置,聊天功能将在配置就绪后自动恢复');
return;
}
// 如果已经初始化过,就不再重复创建实例
if (wx.goEasy && wx.GoEasy) {
console.log('GoEasy 已初始化,跳过');
return;
}
try {
wx.goEasy = GoEasy.getInstance({
host: cfg.host || 'hangzhou.goeasy.io',
appkey: cfg.appkey,
modules: ['im', 'pubsub']
});
wx.GoEasy = GoEasy;
console.log('GoEasy 初始化成功');
if (this.startImWhenReady && wx.getStorageSync('uid')) {
setTimeout(() => this.startImWhenReady(), 100);
}
} catch (error) {
console.error('GoEasy 初始化失败:', error);
}
},
initCurrentUser() {
const uid = wx.getStorageSync('uid');
if (uid) {
this.globalData.currentUser = {
id: uid,
name: '用户' + uid.substring(0, 6),
avatar: this.globalData.ossImageUrl + this.globalData.morentouxiang
};
}
},
emitEvent(eventName, data) {
if (this.globalData.eventListeners[eventName]) {
this.globalData.eventListeners[eventName].forEach(callback => {
try { callback(data); } catch (error) { console.error(error); }
});
}
},
/** 切换当前角色并通知 custom-tab-bar 立即刷新Storage + globalData + 事件) */
setCurrentRole(role) {
if (!role) return;
this.globalData.currentRole = role;
wx.setStorageSync('currentRole', role);
this.emitEvent('currentRoleChanged', { role });
},
on(eventName, callback) {
if (!this.globalData.eventListeners[eventName]) {
this.globalData.eventListeners[eventName] = [];
}
this.globalData.eventListeners[eventName].push(callback);
},
off(eventName, callback) {
if (this.globalData.eventListeners[eventName]) {
const index = this.globalData.eventListeners[eventName].indexOf(callback);
if (index > -1) {
this.globalData.eventListeners[eventName].splice(index, 1);
}
}
}
});

107
app.json Normal file
View File

@@ -0,0 +1,107 @@
{
"pages": [
"pages/index/index",
"pages/fenlei/fenlei",
"pages/xuanren/xuanren",
"pages/xiaoxi/xiaoxi",
"pages/wode/wode",
"pages/shangpinxiangqing/shangpinxiangqing",
"pages/tijiao/tijiao",
"pages/xiugai/xiugai",
"pages/dashouduan/dashouduan",
"pages/shangjiaduan/shangjiaduan",
"pages/guanshiduan/guanshiduan",
"pages/dingdan/dingdan",
"pages/dingdanxiangqing/dingdanxiangqing",
"pages/dashouguize/dashouguize",
"pages/dashouxiugai/dashouxiugai",
"pages/dashoupaihang/dashoupaihang",
"pages/dashouchongzhi/dashouchongzhi",
"pages/dashouxiaoxi/dashouxiaoxi",
"pages/dashoudingdan/dashoudingdan",
"pages/jiedan/jiedan",
"pages/tixian/tixian",
"pages/yaoqingdashou/yaoqingdashou",
"pages/wodedashou/wodedashou",
"pages/czjilu/czjilu",
"pages/guanshipaihang/guanshipaihang",
"pages/sjpaidan/sjpaidan",
"pages/sjdingdan/sjdingdan",
"pages/sjxiaoxi/sjxiaoxi",
"pages/sjpaihang/sjpaihang",
"pages/sjchongzhi/sjchongzhi",
"pages/sjddxq/sjddxq",
"pages/dsddxq/dsddxq",
"pages/liaotian/liaotian",
"pages/jisufd/jisufd",
"pages/jiedanchi/jiedanchi",
"pages/haibao/haibao",
"pages/jinpaids/jinpaids",
"pages/zuzhangduan/zuzhangduan",
"pages/guanzhual/guanzhual",
"pages/zzfhjilu/zzfhjilu",
"pages/yqguanshi/yqguanshi",
"pages/jiedanchi2/jiedanchi2",
"pages/qunliaotian/qunliaotian",
"pages/kefuliaotian/kefuliaotian",
"pages/renzheng/renzheng",
"components/global-notification/global-notification",
"components/popup-notice/popup-notice",
"components/order-card/order-card",
"components/order-sender/order-sender",
"pages/cfss/cfss/cfss",
"pages/cfss/components/fakuan-list/fakuan-list",
"pages/cfss/components/jifen-list/jifen-list",
"pages/cfss/components/fakuan-pay/fakuan-pay",
"pages/phone-auth/phone-auth",
"components/chenghao-tag/chenghao-tag",
"pages/kaoheguan/kaoheguan",
"pages/kaohe_dafen/kaohe_dafen",
"pages/kaohe_jilu/kaohe_jilu",
"pages/kaohe_zhongxin/kaohe_zhongxin",
"pages/kaohe_jinpai/kaohe_jinpai",
"pages/peihuDingdan/peihuDingdan",
"pages/tixian/components/mode1/mode1",
"pages/tixian/components/mode2/mode2"
],
"usingComponents": {
"popup-notice": "/components/popup-notice/popup-notice",
"custom-tab-bar": "/custom-tab-bar/index"
},
"window": {
"navigationBarTextStyle": "black",
"navigationStyle": "default",
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "星阙网络"
},
"tabBar": {
"custom": true,
"list": [
{
"pagePath": "pages/jiedanchi/jiedanchi",
"text": "接单池",
"iconPath": "/images/jiedanchi.png",
"selectedIconPath": "/images/jiedanchi.png"
},
{
"pagePath": "pages/jiedanchi2/jiedanchi2",
"text": "接单池",
"iconPath": "/images/jiedanchi.png",
"selectedIconPath": "/images/jiedanchi.png"
}
]
},
"style": "v2",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}

10
app.wxss Normal file
View File

@@ -0,0 +1,10 @@
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 200rpx 0;
box-sizing: border-box;
}

View File

@@ -0,0 +1,73 @@
// components/chenghao-tag/chenghao-tag.js
const app = getApp();
Component({
properties: {
mingcheng: { type: String, value: '' },
texiaoJson: { type: Object, value: {} }
},
data: {
// 默认值宽152高52六边形无背景图无动画白色字
width: 152,
height: 52,
shapeClass: 'liubianxing', // 保证至少不是矩形
animationClass: '',
bgStyle: '',
textColor: '#FFFFFF',
textSize: 22,
imageUrl: ''
},
lifetimes: {
attached() {
const cfg = this.properties.texiaoJson || {};
// 1. 尺寸稍大一些一行能放3~4个
const width = cfg.width || 152;
const height = cfg.height || 52;
// 2. 背景处理:优先背景图,否则用渐变/纯色
let bgStyle = '';
let imageUrl = '';
if (cfg.image_url) {
const ossImageUrl = app.globalData.ossImageUrl || '';
imageUrl = cfg.image_url.startsWith('http')
? cfg.image_url
: ossImageUrl + cfg.image_url;
} else if (cfg.bg_gradient) {
bgStyle = `background: ${cfg.bg_gradient};`;
} else if (cfg.bg_color) {
bgStyle = `background: ${cfg.bg_color};`;
} else {
// 默认金橙渐变
bgStyle = `background: linear-gradient(135deg, #FFD700, #FF8C00);`;
}
// 3. 形状(后端传入 shape 字段,默认 liubianxing
const shapeClass = cfg.shape || 'liubianxing';
// 4. 动画
const animationClass = cfg.animation || '';
// 5. 文字
const textColor = cfg.text_color || '#FFFFFF';
const textSize = cfg.text_size || 22;
this.setData({
width, height,
bgStyle, imageUrl,
shapeClass, animationClass,
textColor, textSize
});
}
},
methods: {
// 背景图加载失败时回退为纯色背景
onImageError() {
this.setData({ imageUrl: '' });
// 如果 bgStyle 为空,给个默认背景
if (!this.data.bgStyle) {
this.setData({ bgStyle: 'background: linear-gradient(135deg, #FFD700, #FF8C00);' });
}
}
}
});

View File

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

View File

@@ -0,0 +1,20 @@
<!-- components/chenghao-tag/chenghao-tag.wxml -->
<view
class="tag-root {{shapeClass}} {{animationClass}}"
style="width: {{width}}rpx; height: {{height}}rpx; {{bgStyle}}"
>
<!-- 背景图(如果有) -->
<image
wx:if="{{imageUrl}}"
class="tag-bg-image"
src="{{imageUrl}}"
mode="aspectFill"
binderror="onImageError"
></image>
<!-- 文字层 -->
<text
class="tag-text"
style="color: {{textColor}}; font-size: {{textSize}}rpx;"
>{{mingcheng}}</text>
</view>

View File

@@ -0,0 +1,90 @@
/* components/chenghao-tag/chenghao-tag.wxss */
/* ========== 根容器 ========== */
.tag-root {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
border-radius: 4rpx;
overflow: hidden;
flex-shrink: 0;
margin: 4rpx;
box-sizing: border-box;
}
/* ========== 背景图 ========== */
.tag-bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
border-radius: inherit;
}
/* ========== 文字层 ========== */
.tag-text {
position: relative;
z-index: 2;
font-weight: bold;
white-space: nowrap;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5);
padding: 0 10rpx;
}
/* ========== 多边形形状 ========== */
.tag-root.liubianxing {
clip-path: polygon(12% 0%, 88% 0%, 100% 50%, 88% 100%, 12% 100%, 0% 50%);
}
.tag-root.lingxing {
clip-path: polygon(50% 0%, 85% 12%, 100% 50%, 85% 88%, 50% 100%, 15% 88%, 0% 50%, 15% 12%);
}
.tag-root.wujiaoxing {
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
.tag-root.dunpai {
clip-path: polygon(18% 0%, 82% 0%, 100% 18%, 95% 50%, 100% 82%, 82% 100%, 18% 100%, 0% 82%, 5% 50%, 0% 18%);
}
.tag-root.jiantou {
clip-path: polygon(0% 0%, 85% 0%, 100% 50%, 85% 100%, 0% 100%, 12% 50%);
}
/* ========== 动画 ========== */
/* 流光扫描 */
.tag-root.shine::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 60%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent);
transform: skewX(-20deg);
animation: shine-anim 2s infinite;
z-index: 1;
pointer-events: none;
}
@keyframes shine-anim {
0% { left: -100%; }
100% { left: 200%; }
}
/* 呼吸光晕 */
.tag-root.pulse {
animation: pulse-anim 1.5s ease-in-out infinite;
}
@keyframes pulse-anim {
0%, 100% { filter: brightness(1); transform: scale(1); }
50% { filter: brightness(1.2); transform: scale(1.04); }
}
/* 外发光闪烁 */
.tag-root.glow {
animation: glow-anim 2s ease-in-out infinite;
}
@keyframes glow-anim {
0%, 100% { box-shadow: 0 0 8rpx rgba(255, 215, 0, 0.4); }
50% { box-shadow: 0 0 20rpx rgba(255, 215, 0, 0.8), 0 0 40rpx rgba(255, 215, 0, 0.4); }
}

View File

@@ -0,0 +1,368 @@
const { resolveAvatarUrl } = require('../../utils/avatar.js');
Component({
properties: {
position: {
type: String,
value: 'top'
},
duration: {
type: Number,
value: 3000
},
showProgress: {
type: Boolean,
value: true
},
showMuteButton: {
type: Boolean,
value: true
},
showTime: {
type: Boolean,
value: true
},
swipeToClose: {
type: Boolean,
value: true
},
swipeThreshold: {
type: Number,
value: 50
}
},
data: {
show: false,
title: '',
message: '',
avatar: '',
notificationData: null,
progress: 100,
progressInterval: null,
positionClass: 'top',
// 滑动相关
touchStartY: 0,
touchStartX: 0,
currentY: 0,
currentX: 0,
isSwiping: false,
swipingClass: '',
swipeStyle: '',
swipeDistance: 0,
// 静音状态
isMuted: false,
// 自动隐藏计时器
autoHideTimeout: null
},
lifetimes: {
attached() {
this.loadMuteSetting();
this.setData({
positionClass: this.properties.position
});
this.setupDirectEventListeners();
},
detached() {
this.clearTimers();
}
},
methods: {
setupDirectEventListeners() {
const app = getApp();
if (app) {
const self = this;
const globalNotification = {
show: (data) => {
if (self && self.showNotification) {
self.showNotification(data);
}
},
hide: () => {
if (self && self.hideNotification) {
self.hideNotification();
}
},
isMuted: () => {
return self.data.isMuted;
}
};
app.globalData.globalNotification = globalNotification;
}
},
loadMuteSetting() {
try {
const isMuted = wx.getStorageSync('notificationMuted') || false;
this.setData({ isMuted });
} catch (error) {
console.warn('加载静音设置失败:', error);
}
},
saveMuteSetting(isMuted) {
try {
wx.setStorageSync('notificationMuted', isMuted);
} catch (error) {
console.warn('保存静音设置失败:', error);
}
},
showNotification(data) {
if (!data) return;
if (this.data.isMuted) return;
this.clearTimers();
const app = getApp();
let avatar = resolveAvatarUrl(
data.avatar || data.message?.senderData?.avatar,
app
);
this.setData({
show: true,
title: data.senderName || '新消息',
message: data.content || '[消息内容]',
avatar: avatar,
notificationData: data,
progress: 100,
swipingClass: '',
swipeStyle: ''
});
if (this.properties.showProgress) {
this.startProgressBar(this.properties.duration);
}
this.data.autoHideTimeout = setTimeout(() => {
this.hideNotification();
}, this.properties.duration);
},
hideNotification() {
this.clearTimers();
this.setData({
show: false,
progress: 100,
swipingClass: '',
swipeStyle: ''
});
this.triggerEvent('hide');
},
startProgressBar(duration) {
const interval = 100;
const steps = duration / interval;
const stepValue = 100 / steps;
let currentProgress = 100;
this.data.progressInterval = setInterval(() => {
currentProgress -= stepValue;
if (currentProgress <= 0) {
currentProgress = 0;
this.setData({ progress: currentProgress });
clearInterval(this.data.progressInterval);
} else {
this.setData({ progress: currentProgress });
}
}, interval);
},
clearTimers() {
if (this.data.autoHideTimeout) {
clearTimeout(this.data.autoHideTimeout);
this.data.autoHideTimeout = null;
}
if (this.data.progressInterval) {
clearInterval(this.data.progressInterval);
this.data.progressInterval = null;
}
},
onTouchStart(e) {
if (!this.properties.swipeToClose) return;
const touch = e.touches[0];
this.setData({
touchStartY: touch.clientY,
touchStartX: touch.clientX,
currentY: touch.clientY,
currentX: touch.clientX,
isSwiping: false
});
this.clearTimers();
},
onTouchMove(e) {
if (!this.properties.swipeToClose) return;
const touch = e.touches[0];
const deltaY = touch.clientY - this.data.touchStartY;
const deltaX = touch.clientX - this.data.touchStartX;
const absDeltaY = Math.abs(deltaY);
const absDeltaX = Math.abs(deltaX);
if (absDeltaY > 10 && absDeltaY > absDeltaX) {
this.setData({
isSwiping: true,
currentY: touch.clientY,
swipingClass: deltaY < 0 ? 'swiping-up' : 'swiping',
swipeDistance: Math.abs(deltaY),
swipeStyle: `--swipe-distance: ${Math.abs(deltaY)}rpx;`
});
e.stopPropagation();
}
},
onTouchEnd(e) {
if (!this.properties.swipeToClose || !this.data.isSwiping) {
this.restartAutoHide();
return;
}
const deltaY = this.data.currentY - this.data.touchStartY;
const absDeltaY = Math.abs(deltaY);
if (absDeltaY > this.properties.swipeThreshold) {
this.hideNotification();
} else {
this.setData({
swipingClass: '',
swipeStyle: 'transition: transform 0.3s ease; transform: translateY(0);'
});
setTimeout(() => {
this.setData({
swipingClass: '',
swipeStyle: ''
});
}, 300);
this.restartAutoHide();
}
this.setData({ isSwiping: false });
},
restartAutoHide() {
const remainingTime = (this.data.progress / 100) * this.properties.duration;
if (remainingTime > 0) {
this.data.autoHideTimeout = setTimeout(() => {
this.hideNotification();
}, remainingTime);
}
},
onTap() {
const { notificationData } = this.data;
if (!notificationData) return;
this.triggerEvent('tap', notificationData);
this.hideNotification();
this.navigateToChatPage(notificationData);
},
onClose(e) {
e.stopPropagation();
this.triggerEvent('close');
this.hideNotification();
},
onMute(e) {
e.stopPropagation();
const newMutedState = !this.data.isMuted;
this.setData({ isMuted: newMutedState });
this.saveMuteSetting(newMutedState);
const app = getApp();
if (app && app.toggleDoNotDisturb) {
app.toggleDoNotDisturb(newMutedState);
}
this.triggerEvent('mute', { muted: newMutedState });
wx.showToast({
title: newMutedState ? '已开启免打扰' : '已关闭免打扰',
icon: 'success',
duration: 1500
});
if (newMutedState) {
this.hideNotification();
}
},
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60 * 1000) return '刚刚';
if (diff < 60 * 60 * 1000) return Math.floor(diff / (60 * 1000)) + '分钟前';
if (date.toDateString() === now.toDateString()) {
return date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0');
}
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === yesterday.toDateString()) {
return '昨天 ' + date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0');
}
return (date.getMonth() + 1) + '月' + date.getDate() + '日';
},
navigateToChatPage(data) {
if (!data || !data.message) {
console.warn('通知数据无效,无法跳转');
return;
}
const msg = data.message;
let path = '';
let queryParam = null;
// 根据 notificationType 或消息字段判断
if (data.notificationType === 'cs' || msg.teamId) {
// 客服消息
const teamId = data.teamId || msg.teamId;
path = '/pages/kefuliaotian/kefuliaotian';
queryParam = { teamId: teamId };
} else if (data.notificationType === 'group' || msg.groupId) {
// 群聊消息
const groupId = data.groupId || msg.groupId;
const orderId = data.orderId || msg.orderId || '';
const groupName = data.groupName || '订单群聊';
const groupAvatar = data.groupAvatar || '';
const isCross = data.isCross || msg.isCross || 0;
path = '/pages/qunliaotian/qunliaotian';
queryParam = {
groupId,
orderId,
groupName,
groupAvatar,
isCross
};
} else {
// 默认私聊
const toUserId = data.senderId || msg.senderId;
const toName = data.senderName || '用户';
const toAvatar = data.avatar || '';
path = '/pages/liaotian/liaotian';
queryParam = {
toUserId,
toName,
toAvatar
};
}
if (path && queryParam) {
const encoded = encodeURIComponent(JSON.stringify(queryParam));
wx.navigateTo({
url: `${path}?data=${encoded}`,
fail: (err) => {
console.error('通知跳转失败:', err);
wx.switchTab({ url: '/pages/xiaoxi/xiaoxi' });
}
});
} else {
wx.switchTab({ url: '/pages/xiaoxi/xiaoxi' });
}
}
}
});

View File

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

View File

@@ -0,0 +1,49 @@
<view
class="global-notification {{show ? 'show' : ''}} {{positionClass}} {{swipingClass}}"
bindtap="onTap"
bindtouchstart="onTouchStart"
bindtouchmove="onTouchMove"
bindtouchend="onTouchEnd"
style="{{swipeStyle}}"
data-notification-id="{{notificationData && notificationData.id ? notificationData.id : ''}}">
<!-- 滑动关闭指示器 -->
<view class="notification-swipe-indicator {{isSwiping ? 'active' : ''}}">
<view class="swipe-line"></view>
</view>
<!-- 通知内容 -->
<view class="notification-content">
<!-- 左侧头像 -->
<image class="notification-avatar" src="{{avatar}}" mode="aspectFill"></image>
<!-- 中间内容区域 -->
<view class="notification-body">
<view class="notification-title">{{title}}</view>
<view class="notification-message">{{message}}</view>
<view class="notification-time" wx:if="{{showTime}}">
{{formatTime(notificationData ? notificationData.timestamp : '')}}
</view>
</view>
<!-- 右侧操作区域 -->
<view class="notification-actions">
<!-- 关闭按钮 -->
<view class="notification-close" catchtap="onClose">
<text class="close-icon">×</text>
</view>
<!-- 免打扰按钮 -->
<view class="notification-mute {{isMuted ? 'muted' : ''}}" catchtap="onMute" wx:if="{{showMuteButton}}">
<text class="mute-icon">
{{isMuted ? '🔕' : '🔔'}}
</text>
</view>
</view>
</view>
<!-- 进度条 -->
<view class="notification-progress" wx:if="{{showProgress}}">
<view class="progress-bar {{progress < 30 ? 'warning' : ''}}"></view>
</view>
</view>

View File

@@ -0,0 +1,215 @@
.global-notification {
position: fixed;
left: 20rpx;
right: 20rpx;
top: 0rpx;
z-index: 99999;
opacity: 0;
transform: translateY(-120%);
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
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;
touch-action: pan-y;
}
.global-notification.show {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.notification-swipe-indicator {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
}
.swipe-line {
width: 40rpx;
height: 4rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 2rpx;
transition: all 0.3s;
}
.notification-swipe-indicator.active .swipe-line {
background: rgba(255, 255, 255, 0.6);
transform: scaleX(1.2);
}
.notification-content {
display: flex;
align-items: center;
padding: 32rpx;
min-height: 140rpx;
position: relative;
}
.notification-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 44rpx;
margin-right: 24rpx;
flex-shrink: 0;
border: 3rpx solid rgba(212, 175, 55, 0.6);
box-shadow:
0 8rpx 24rpx rgba(212, 175, 55, 0.2),
inset 0 1rpx 0 rgba(255, 255, 255, 0.3);
background-color: rgba(0, 0, 0, 0.2);
}
.notification-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.notification-title {
font-size: 34rpx;
font-weight: 600;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: linear-gradient(135deg, #D4AF37 0%, #FFD700 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
}
.notification-message {
font-size: 28rpx;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
color: rgba(255, 255, 255, 0.95);
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5);
}
.notification-time {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
margin-top: 4rpx;
font-weight: 300;
}
.notification-actions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-left: 20rpx;
gap: 16rpx;
}
.notification-close,
.notification-mute {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.08);
border: 1rpx solid rgba(255, 255, 255, 0.1);
transition: all 0.3s;
flex-shrink: 0;
box-shadow:
0 4rpx 12rpx rgba(0, 0, 0, 0.2),
inset 0 1rpx 0 rgba(255, 255, 255, 0.05);
}
.notification-close:active,
.notification-mute:active {
background: rgba(255, 255, 255, 0.15);
transform: scale(0.92);
}
.close-icon {
font-size: 32rpx;
font-weight: 300;
color: rgba(255, 255, 255, 0.9);
line-height: 1;
}
.mute-icon {
font-size: 28rpx;
line-height: 0;
color: rgba(255, 255, 255, 0.9);
}
.notification-mute.muted .mute-icon {
color: #D4AF37;
}
.notification-progress {
height: 6rpx;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
border-radius: 0 0 24rpx 24rpx;
}
.progress-bar {
height: 100%;
width: 100%;
background: linear-gradient(90deg,
#D4AF37 0%,
#FFD700 50%,
#D4AF37 100%);
background-size: 200% 100%;
transition: width 0.1s linear;
box-shadow: 0 0 20rpx rgba(212, 175, 55, 0.4);
animation: progressShine 2s linear infinite;
}
@keyframes progressShine {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.progress-bar.warning {
background: linear-gradient(90deg,
#ff3b30 0%,
#ff6b6b 50%,
#ff3b30 100%);
animation: progressShine 1s linear infinite;
}
.global-notification.swiping {
opacity: 0.7;
transform: translateY(calc(var(--swipe-distance, 0) * -1));
}
.global-notification.swiping-up {
opacity: 0.3;
transform: translateY(calc(var(--swipe-distance, 0) * -1));
}
.global-notification:active:not(.swiping) {
transform: scale(0.995) translateY(0);
transition: transform 0.1s;
}

View File

@@ -0,0 +1,102 @@
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
/**
* 组件的属性列表
*/
properties: {
extClass: {
type: String,
value: ''
},
title: {
type: String,
value: ''
},
background: {
type: String,
value: ''
},
color: {
type: String,
value: ''
},
back: {
type: Boolean,
value: true
},
loading: {
type: Boolean,
value: false
},
homeButton: {
type: Boolean,
value: false,
},
animated: {
// 显示隐藏的时候opacity动画效果
type: Boolean,
value: true
},
show: {
// 显示隐藏导航隐藏的时候navigation-bar的高度占位还在
type: Boolean,
value: true,
observer: '_showChange'
},
// back为true的时候返回的页面深度
delta: {
type: Number,
value: 1
},
},
/**
* 组件的初始数据
*/
data: {
displayStyle: ''
},
lifetimes: {
attached() {
const rect = wx.getMenuButtonBoundingClientRect()
const platform = (wx.getDeviceInfo() || wx.getSystemInfoSync()).platform
const isAndroid = platform === 'android'
const isDevtools = platform === 'devtools'
const { windowWidth, safeArea: { top = 0, bottom = 0 } = {} } = wx.getWindowInfo() || wx.getSystemInfoSync()
this.setData({
ios: !isAndroid,
innerPaddingRight: `padding-right: ${windowWidth - rect.left}px`,
leftWidth: `width: ${windowWidth - rect.left}px`,
safeAreaTop: isDevtools || isAndroid ? `height: calc(var(--height) + ${top}px); padding-top: ${top}px` : ``
})
},
},
/**
* 组件的方法列表
*/
methods: {
_showChange(show) {
const animated = this.data.animated
let displayStyle = ''
if (animated) {
displayStyle = `opacity: ${show ? '1' : '0'
};transition:opacity 0.5s;`
} else {
displayStyle = `display: ${show ? '' : 'none'}`
}
this.setData({
displayStyle
})
},
back() {
const data = this.data
if (data.delta) {
wx.navigateBack({
delta: data.delta
})
}
this.triggerEvent('back', { delta: data.delta }, {})
}
},
})

View File

@@ -0,0 +1,5 @@
{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {}
}

View File

@@ -0,0 +1,64 @@
<view class="weui-navigation-bar {{extClass}}">
<view class="weui-navigation-bar__inner {{ios ? 'ios' : 'android'}}" style="color: {{color}}; background: {{background}}; {{displayStyle}}; {{innerPaddingRight}}; {{safeAreaTop}}">
<!-- 左侧按钮 -->
<view class='weui-navigation-bar__left' style="{{leftWidth}}">
<block wx:if="{{back || homeButton}}">
<!-- 返回上一页 -->
<block wx:if="{{back}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_goback">
<view
bindtap="back"
class="weui-navigation-bar__btn_goback_wrapper"
hover-class="weui-active"
hover-stay-time="100"
aria-role="button"
aria-label="返回"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_goback"></view>
</view>
</view>
</block>
<!-- 返回首页 -->
<block wx:if="{{homeButton}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_home">
<view
bindtap="home"
class="weui-navigation-bar__btn_home_wrapper"
hover-class="weui-active"
aria-role="button"
aria-label="首页"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_home"></view>
</view>
</view>
</block>
</block>
<block wx:else>
<slot name="left"></slot>
</block>
</view>
<!-- 标题 -->
<view class='weui-navigation-bar__center'>
<view wx:if="{{loading}}" class="weui-navigation-bar__loading" aria-role="alert">
<view
class="weui-loading"
aria-role="img"
aria-label="加载中"
></view>
</view>
<block wx:if="{{title}}">
<text>{{title}}</text>
</block>
<block wx:else>
<slot name="center"></slot>
</block>
</view>
<!-- 右侧留空 -->
<view class='weui-navigation-bar__right'>
<slot name="right"></slot>
</view>
</view>
</view>

View File

@@ -0,0 +1,96 @@
.weui-navigation-bar {
--weui-FG-0:rgba(0,0,0,.9);
--height: 44px;
--left: 16px;
}
.weui-navigation-bar .android {
--height: 48px;
}
.weui-navigation-bar {
overflow: hidden;
color: var(--weui-FG-0);
flex: none;
}
.weui-navigation-bar__inner {
position: relative;
top: 0;
left: 0;
height: calc(var(--height) + env(safe-area-inset-top));
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: env(safe-area-inset-top);
width: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__left {
position: relative;
padding-left: var(--left);
display: flex;
flex-direction: row;
align-items: flex-start;
height: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__btn_goback_wrapper {
padding: 11px 18px 11px 16px;
margin: -11px -18px -11px -16px;
}
.weui-navigation-bar__btn_goback_wrapper.weui-active {
opacity: 0.5;
}
.weui-navigation-bar__btn_goback {
font-size: 12px;
width: 12px;
height: 24px;
-webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--weui-FG-0);
}
.weui-navigation-bar__center {
font-size: 17px;
text-align: center;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-weight: bold;
flex: 1;
height: 100%;
}
.weui-navigation-bar__loading {
margin-right: 4px;
align-items: center;
}
.weui-loading {
font-size: 16px;
width: 16px;
height: 16px;
display: block;
background: transparent url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='80px' height='80px' viewBox='0 0 80 80' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Eloading%3C/title%3E%3Cdefs%3E%3ClinearGradient x1='94.0869141%25' y1='0%25' x2='94.0869141%25' y2='90.559082%25' id='linearGradient-1'%3E%3Cstop stop-color='%23606060' stop-opacity='0' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3ClinearGradient x1='100%25' y1='8.67370605%25' x2='100%25' y2='90.6286621%25' id='linearGradient-2'%3E%3Cstop stop-color='%23606060' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' opacity='0.9'%3E%3Cg%3E%3Cpath d='M40,0 C62.09139,0 80,17.90861 80,40 C80,62.09139 62.09139,80 40,80 L40,73 C58.2253967,73 73,58.2253967 73,40 C73,21.7746033 58.2253967,7 40,7 L40,0 Z' fill='url(%23linearGradient-1)'%3E%3C/path%3E%3Cpath d='M40,0 L40,7 C21.7746033,7 7,21.7746033 7,40 C7,58.2253967 21.7746033,73 40,73 L40,80 C17.90861,80 0,62.09139 0,40 C0,17.90861 17.90861,0 40,0 Z' fill='url(%23linearGradient-2)'%3E%3C/path%3E%3Ccircle id='Oval' fill='%23606060' cx='40.5' cy='3.5' r='3.5'%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A") no-repeat;
background-size: 100%;
margin-left: 0;
animation: loading linear infinite 1s;
}
@keyframes loading {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,102 @@
const app = getApp();
Component({
properties: {
orderId: { type: String, value: '' }
},
data: {
order: null,
loading: false,
errorMsg: '',
// 跨平台字段
isCross: false, // 是否跨平台订单
dispatchType: 0, // 1-我方派单2-对方派单
partnerClubId: '', // 对方俱乐部ID
},
lifetimes: {
attached() {
if (this.properties.orderId) {
this.fetchOrderDetail();
}
}
},
methods: {
// 获取订单详情(含跨平台字段)
fetchOrderDetail() {
if (!this.properties.orderId) return;
this.setData({ loading: true, errorMsg: '' });
const baseUrl = app.globalData.apiBaseUrl;
wx.request({
url: `${baseUrl}/dingdan/order_detail/`, // 需后端返回 is_cross, dispatch_type, partner_club_id
method: 'POST',
data: { order_id: this.properties.orderId },
header: { 'content-type': 'application/json' },
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 0) {
const order = res.data.data;
// 处理图片URL
if (order.tupian && !order.tupian.startsWith('http')) {
order.tupian = app.globalData.ossImageUrl + order.tupian;
}
this.setData({
order,
loading: false,
isCross: order.is_cross === 1,
dispatchType: order.dispatch_type || 0,
partnerClubId: order.partner_club_id || '',
});
// 通知父页面状态
this.triggerEvent('statusready', {
isCross: this.data.isCross,
dispatchType: this.data.dispatchType,
partnerClubId: this.data.partnerClubId
});
} else {
this.setData({ errorMsg: res.data?.msg || '获取订单失败', loading: false });
}
},
fail: () => {
this.setData({ errorMsg: '网络请求失败', loading: false });
}
});
},
// 发送订单卡片消息(原有)
sendOrderMessage() {
if (!this.data.order) return;
this.triggerEvent('sendorder', { order: this.data.order });
},
// 跨平台消息转发(供父页面调用)
forwardMessage(payload) {
const that = this;
const baseUrl = app.globalData.apiBaseUrl;
return new Promise((resolve, reject) => {
wx.request({
url: `${baseUrl}/dingdan/kptxxts`,
method: 'POST',
data: {
order_id: that.properties.orderId,
message_type: payload.type || 'text',
content: payload.content || '',
image_url: payload.imageUrl || '', // 图片等
},
header: { 'content-type': 'application/json' },
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 0) {
resolve(res.data);
} else {
reject(res.data);
}
},
fail: (err) => {
reject(err);
}
});
});
}
}
});

View File

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

View File

@@ -0,0 +1,20 @@
<view class="order-card" wx:if="{{order}}">
<view class="card-header">
<text class="card-title">关联订单</text>
<text class="card-id">{{order.dingdan_id}}</text>
<!-- 跨平台标记 -->
<text class="cross-tag" wx:if="{{isCross}}">跨平台</text>
</view>
<view class="card-body" bindtap="sendOrderMessage">
<image class="card-img" src="{{order.tupian || '/images/default-goods.png'}}" mode="aspectFill" />
<view class="card-info">
<text class="card-jieshao">{{order.jieshao || '暂无介绍'}}</text>
<text class="card-jine">¥{{order.jine}}</text>
</view>
<view class="card-send-btn">发送</view>
</view>
<view wx:if="{{errorMsg}}" class="card-error">{{errorMsg}}</view>
</view>
<view class="order-card" wx:elif="{{loading}}">
<text class="loading-text">加载订单信息...</text>
</view>

View File

@@ -0,0 +1,39 @@
.order-card {
background: #fff;
margin: 15rpx 20rpx;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
}
.card-header {
display: flex;
align-items: center;
padding: 16rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.card-title { font-size: 26rpx; color: #666; }
.card-id { font-size: 24rpx; color: #999; margin-left: 15rpx; }
.cross-tag {
margin-left: auto;
background: #ff9900;
color: #fff;
font-size: 20rpx;
padding: 2rpx 12rpx;
border-radius: 8rpx;
}
.card-body {
display: flex;
align-items: center;
padding: 20rpx;
}
.card-img { width: 100rpx; height: 100rpx; border-radius: 8rpx; margin-right: 15rpx; }
.card-info { flex: 1; }
.card-jieshao { font-size: 28rpx; color: #333; }
.card-jine { font-size: 26rpx; color: #e74c3c; }
.card-send-btn {
width: 80rpx; height: 50rpx; line-height: 50rpx;
text-align: center; background: #07c160; color: #fff;
border-radius: 8rpx; font-size: 24rpx;
}
.card-error { padding: 20rpx; color: #e74c3c; text-align: center; }
.loading-text { display: block; padding: 20rpx; text-align: center; color: #999; }

View File

@@ -0,0 +1,16 @@
const app = getApp();
Component({
properties: {
visible: Boolean,
showDetail: { type: Boolean, value: false }
},
data: {},
methods: {
close() {
this.triggerEvent('close');
}
}
});

View File

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

View File

@@ -0,0 +1,12 @@
<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="close" bindtap="close">✕</text>
</view>
<view class="content">
<view class="icon">📋</view>
<text class="notice">此功能暂未开放</text>
<text class="sub">敬请期待</text>
</view>
</view>

View File

@@ -0,0 +1,45 @@
.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;
}

View File

@@ -0,0 +1,325 @@
// components/popup-notice/popup-notice.js
import PopupService from '../../services/popupService.js';
Component({
data: {
// 原有全屏弹窗数据
visible: false,
title: '',
images: [],
processedImages: [],
hasImages: false,
hasTextOnly: false,
textContent: '',
duration: 0,
remainingSeconds: 0,
muteChecked: false,
showMuteCheckbox: false,
isFullscreen: false,
timer: null,
// 悬浮窗相关数据
isMinimized: false,
floatX: 20,
floatY: 200,
savedRemainingSeconds: 0,
floatAvatarUrl: '',
// 🆕 自定义预览数据
showCustomPreview: false, // 是否显示自定义预览遮罩
previewUrls: [], // 当前预览的图片URL列表
currentPreviewIndex: 0, // 当前预览图片索引
},
lifetimes: {
detached() {
this.clearTimer();
}
},
methods: {
// ========== 原有所有方法(完全不变,一字不漏) ==========
async show(config, onCloseCallback) {
const { title, images, duration, showMuteCheckbox, serverTime } = config;
this.onCloseCallback = onCloseCallback;
let safeImages = JSON.parse(JSON.stringify(images || []));
const hasImages = safeImages.length > 0;
let hasTextOnly = false, textContent = '';
if (!hasImages) {
hasTextOnly = true;
textContent = config.text || '暂无内容';
}
this.setData({
visible: true,
isMinimized: false,
title: title || '',
images: safeImages,
hasImages,
hasTextOnly,
textContent,
duration: duration || 0,
remainingSeconds: duration || 0,
muteChecked: false,
showMuteCheckbox: showMuteCheckbox || false,
isFullscreen: false,
processedImages: [],
});
if (duration > 0) {
this.startCountdown();
}
if (hasImages) {
const processed = [];
const screenWidth = wx.getSystemInfoSync().windowWidth;
const containerWidth = screenWidth * 0.9;
const imageWidth = containerWidth - 60;
for (let i = 0; i < safeImages.length; i++) {
const img = safeImages[i];
const fullUrl = this.getFullImageUrl(img.url);
let imageHeight = 200;
try {
const res = await this.getImageInfoPromise(fullUrl);
const realHeight = (imageWidth * res.height) / res.width;
imageHeight = realHeight;
} catch (e) {
console.error('获取图片信息失败', fullUrl, e);
}
processed.push({
url: fullUrl,
text: img.text,
height: imageHeight,
});
}
this.setData({ processedImages: processed });
}
},
getImageInfoPromise(url) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src: url,
success: resolve,
fail: reject,
});
});
},
hide(muteToday = false) {
this.clearTimer();
this.setData({
visible: false,
isMinimized: false
});
if (this.onCloseCallback) {
this.onCloseCallback({ muteToday });
}
},
startCountdown() {
this.clearTimer();
this.timer = setInterval(() => {
let sec = this.data.remainingSeconds;
if (sec <= 1) {
this.clearTimer();
this.setData({ remainingSeconds: 0 });
} else {
this.setData({ remainingSeconds: sec - 1 });
}
}, 1000);
},
clearTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
onCloseTap() {
if (this.data.remainingSeconds > 0) {
wx.showToast({
title: `请阅读${this.data.remainingSeconds}秒后再关闭`,
icon: 'none'
});
return;
}
if (this.data.showMuteCheckbox && this.data.muteChecked) {
wx.showModal({
title: '提示',
content: '今天将不再收到本页面的任何公告,确认吗?',
success: (res) => {
if (res.confirm) this.hide(true);
},
});
} else {
this.hide(false);
}
},
onMuteChange(e) {
this.setData({ muteChecked: e.detail.value.length > 0 });
},
// 🆕 替换原有的 onPreviewImage改用自定义预览不触发 onShow
onCustomPreview(e) {
const index = e.currentTarget.dataset.index;
const urls = this.data.processedImages.map(img => img.url);
this.setData({
showCustomPreview: true,
previewUrls: urls,
currentPreviewIndex: index !== undefined ? index : 0
});
},
// 🆕 关闭自定义预览
onCloseCustomPreview() {
this.setData({
showCustomPreview: false,
previewUrls: [],
currentPreviewIndex: 0
});
},
// 🆕 swiper 切换事件
onPreviewSwiperChange(e) {
this.setData({
currentPreviewIndex: e.detail.current
});
},
// 保留原 onPreviewImage 方法以防外部调用,但不再使用
onPreviewImage(e) {
// 重定向到自定义预览
this.onCustomPreview(e);
},
onImageError(e) {
console.error('图片加载失败:', e.currentTarget.dataset.url);
},
toggleFullscreen() {
this.setData({ isFullscreen: !this.data.isFullscreen });
},
getFullImageUrl(relativeUrl) {
if (!relativeUrl) return '';
if (relativeUrl.startsWith('http')) return relativeUrl;
const app = getApp();
let base = app.globalData.ossImageUrl || '';
base = base.replace(/\/+$/, '');
const clean = relativeUrl.replace(/^\/+/, '');
return base ? base + '/' + clean : clean;
},
preventMove() {},
onMinimize() {
const remaining = this.data.remainingSeconds;
this.clearTimer();
const app = getApp();
let avatarUrl = '';
if (app.globalData.morentouxiang) {
avatarUrl = app.globalData.morentouxiang;
} else {
// 使用内嵌的默认头像 base64灰色圆形确保绝对显示不依赖外部图片
avatarUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI1MCIgZmlsbD0iI2NjY2NjYyIvPjxwYXRoIGQ9Ik01MCAyNWMtMTMuOCAwLTI1IDExLjItMjUgMjUgMCAzLjIgMS4yIDYuMSAzLjIgOC4zQzM0LjQgNzAuNyA0Mi4xIDc1IDUwIDc1czE1LjYtNC4zIDIxLjgtMTEuN0M3My44IDU2LjEgNzUgNTMuMiA3NSA1MGMwLTEzLjgtMTEuMi0yNS0yNS0yNXoiIGZpbGw9IiNmZmZmZmYiLz48L3N2Zz4=';
}
if (avatarUrl && !avatarUrl.startsWith('http') && !avatarUrl.startsWith('data:')) {
const ossBase = app.globalData.ossImageUrl || '';
avatarUrl = ossBase.replace(/\/+$/, '') + '/' + avatarUrl.replace(/^\/+/, '');
}
this.setData({
visible: false,
isMinimized: true,
savedRemainingSeconds: remaining,
floatX: 20,
floatY: 200,
floatAvatarUrl: avatarUrl
});
},
onFloatChange(e) {
this.setData({
floatX: e.detail.x,
floatY: e.detail.y
});
},
onFloatTap() {
this.setData({
visible: true,
isMinimized: false,
remainingSeconds: this.data.savedRemainingSeconds
});
if (this.data.duration > 0 && this.data.savedRemainingSeconds > 0) {
this.startCountdown();
}
},
onFloatAvatarError() {
this.setData({
floatAvatarUrl: '/images/avatar-placeholder.png'
});
},
cleanup() {
this.clearTimer();
this.setData({
visible: false,
isMinimized: false
});
PopupService.reset();
},
onFloatTouchStart(e) {
const touch = e.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
this.startFloatX = this.data.floatX;
this.startFloatY = this.data.floatY;
this.hasMoved = false;
return false;
},
onFloatTouchMove(e) {
const touch = e.touches[0];
const deltaX = touch.clientX - this.touchStartX;
const deltaY = touch.clientY - this.touchStartY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
this.hasMoved = true;
}
let newX = this.startFloatX + deltaX;
let newY = this.startFloatY + deltaY;
const systemInfo = wx.getSystemInfoSync();
const screenWidth = systemInfo.windowWidth;
const screenHeight = systemInfo.windowHeight;
const estimatedSize = 60;
newX = Math.max(0, Math.min(newX, screenWidth - estimatedSize));
newY = Math.max(0, Math.min(newY, screenHeight - estimatedSize));
this.setData({ floatX: newX, floatY: newY });
return false;
},
onFloatTouchEnd(e) {
if (!this.hasMoved) {
this.onFloatTap();
}
return false;
},
// 🆕 阻止事件冒泡(用于预览遮罩内部点击不关闭)
stopPropagation() {}
}
});

View File

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

View File

@@ -0,0 +1,111 @@
<!-- components/popup-notice/popup-notice.wxml -->
<!-- 全屏弹窗遮罩层(完全不变) -->
<view class="popup-mask" wx:if="{{visible}}" catchtouchmove="preventMove">
<view class="popup-container {{isFullscreen ? 'fullscreen' : ''}}">
<!-- 右上角操作按钮组 -->
<view class="action-buttons">
<!-- 最小化按钮 -->
<view class="minimize-btn" bindtap="onMinimize">
<text class="btn-icon"></text>
</view>
<!-- 全屏切换按钮 -->
<view class="fullscreen-btn" bindtap="toggleFullscreen">
<text class="btn-icon">{{isFullscreen ? '✕' : '⛶'}}</text>
</view>
</view>
<!-- 标题 -->
<view wx:if="{{title}}" class="popup-title">{{title}}</view>
<!-- 可滚动内容区 -->
<view class="scroll-wrapper">
<scroll-view scroll-y class="scroll-view" enhanced show-scrollbar="{{true}}">
<view wx:if="{{hasTextOnly}}" class="text-content">{{textContent}}</view>
<view wx:else>
<view wx:for="{{processedImages}}" wx:key="index" class="image-block">
<view wx:if="{{item.text}}" class="block-text">{{item.text}}</view>
<view class="image-padding">
<image
class="block-image"
src="{{item.url}}"
style="width:100%; height:{{item.height}}px; display:block;"
data-index="{{index}}"
data-url="{{item.url}}"
bindtap="onCustomPreview"
binderror="onImageError"
lazy-load="{{false}}"
/>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部固定区 -->
<view class="bottom-fixed">
<view wx:if="{{remainingSeconds > 0}}" class="countdown-tip">
请阅读 {{remainingSeconds}} 秒后关闭
</view>
<view wx:if="{{remainingSeconds === 0 && showMuteCheckbox}}" class="mute-row">
<checkbox-group bindchange="onMuteChange">
<checkbox value="mute" color="#000" />
<text class="mute-label">今日不再提示</text>
</checkbox-group>
</view>
<!-- 关闭按钮 -->
<view
class="close-btn {{remainingSeconds > 0 ? 'close-btn-disabled' : ''}}"
bindtap="{{remainingSeconds > 0 ? '' : 'onCloseTap'}}"
>
<text class="close-btn-text">{{remainingSeconds > 0 ? '阅读中...' : '关闭'}}</text>
<view class="close-btn-sweep" wx:if="{{remainingSeconds === 0}}"></view>
</view>
</view>
</view>
</view>
<!-- 悬浮窗(自定义触摸拖动) -->
<view
class="floating-avatar-wrapper"
wx:if="{{isMinimized}}"
style="left: {{floatX}}px; top: {{floatY}}px;"
catchtouchstart="onFloatTouchStart"
catchtouchmove="onFloatTouchMove"
catchtouchend="onFloatTouchEnd"
>
<image
class="floating-avatar"
src="{{floatAvatarUrl}}"
mode="aspectFill"
binderror="onFloatAvatarError"
/>
</view>
<!-- 🆕 自定义图片预览遮罩(全屏,不触发页面生命周期) -->
<view
class="custom-preview-mask"
wx:if="{{showCustomPreview}}"
catchtouchmove="preventMove"
bindtap="onCloseCustomPreview"
>
<view class="preview-header">
<text class="preview-index">{{currentPreviewIndex + 1}} / {{previewUrls.length}}</text>
<view class="preview-close" bindtap="onCloseCustomPreview">×</view>
</view>
<swiper
class="preview-swiper"
current="{{currentPreviewIndex}}"
bindchange="onPreviewSwiperChange"
indicator-dots="{{false}}"
>
<swiper-item wx:for="{{previewUrls}}" wx:key="index">
<image
src="{{item}}"
mode="aspectFit"
class="preview-image"
catchtap="stopPropagation"
/>
</swiper-item>
</swiper>
</view>

View File

@@ -0,0 +1,295 @@
/* components/popup-notice/popup-notice.wxss */
/* ========== 全屏弹窗样式(原有,完全不变) ========== */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.popup-container {
width: 90%;
height: 85vh;
background: white;
border-radius: 32rpx;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.popup-container.fullscreen {
width: 100%;
height: 100%;
border-radius: 0;
}
.action-buttons {
position: absolute;
top: 20rpx;
right: 20rpx;
display: flex;
gap: 16rpx;
z-index: 10;
}
.minimize-btn,
.fullscreen-btn {
width: 60rpx;
height: 60rpx;
background: rgba(0, 0, 0, 0.4);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4rpx);
transition: all 0.2s;
}
.minimize-btn:active,
.fullscreen-btn:active {
background: rgba(0, 0, 0, 0.6);
transform: scale(0.9);
}
.btn-icon {
font-size: 36rpx;
color: white;
font-weight: bold;
line-height: 1;
}
.popup-title {
font-size: 40rpx;
font-weight: bold;
color: #000;
text-align: center;
padding: 30rpx 30rpx 20rpx;
border-bottom: 2rpx solid #eee;
background: white;
}
.scroll-wrapper {
flex: 1;
overflow: hidden;
width: 100%;
}
.scroll-view {
height: 100%;
width: 100%;
padding: 0;
}
.text-content {
font-size: 36rpx;
font-weight: bold;
color: #000;
line-height: 1.6;
padding: 30rpx;
text-align: left;
}
.image-block {
display: flex;
flex-direction: column;
width: 100%;
margin: 0;
padding: 0;
}
.block-text {
font-size: 36rpx;
font-weight: bold;
color: #000;
padding: 16rpx 30rpx 4rpx 30rpx;
text-align: left;
line-height: 1.4;
}
.image-padding {
padding: 0 30rpx;
box-sizing: border-box;
width: 100%;
}
.block-image {
width: 100%;
display: block;
background: #f5f5f5;
border-radius: 16rpx;
}
.bottom-fixed {
background: white;
border-top: 1rpx solid #eee;
padding: 20rpx 0 30rpx 0;
width: 100%;
box-sizing: border-box;
}
.countdown-tip {
text-align: center;
font-size: 28rpx;
color: #ff6600;
margin-bottom: 20rpx;
}
.mute-row {
display: flex;
justify-content: center;
margin-bottom: 20rpx;
}
.mute-label {
font-size: 28rpx;
color: #555;
margin-left: 12rpx;
}
/* 🔥 关闭按钮(替换原有 button增加扫光 */
.close-btn {
width: 600rpx;
height: 100rpx;
background: #1a1a1a;
border-radius: 60rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
margin: 0 auto;
}
.close-btn:active {
opacity: 0.85;
transform: scale(0.98);
}
.close-btn-disabled {
opacity: 0.6;
pointer-events: none;
}
.close-btn-text {
color: white;
font-size: 40rpx;
font-weight: bold;
position: relative;
z-index: 2;
}
.close-btn-sweep {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent);
transform: skewX(-20deg);
animation: sweep 2.5s infinite ease-in-out;
z-index: 1;
pointer-events: none;
}
@keyframes sweep {
0% { left: -100%; }
40% { left: 100%; }
100% { left: 100%; }
}
/* ========== 悬浮窗样式(纯圆形,适配自定义触摸拖动) ========== */
.floating-avatar-wrapper {
position: fixed;
width: 120rpx;
height: 120rpx;
z-index: 10001;
transform: translateZ(0);
will-change: left, top;
display: flex;
align-items: center;
justify-content: center;
}
.floating-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
border: 2rpx solid rgba(255, 215, 0, 0.6);
background: #f0f0f0;
display: block;
transform: translateZ(0);
}
/* 以下原有 movable-view 相关样式已弃用,但保留以维持代码完整(用户要求不删代码) */
.floating-area {
display: none;
}
.floating-view {
display: none;
}
.float-avatar-wrapper {
display: none;
}
.float-text {
display: none;
}
/* ========== 自定义图片预览遮罩样式(全屏,不触发 onShow ========== */
.custom-preview-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.95);
z-index: 100000;
display: flex;
flex-direction: column;
}
.preview-header {
height: 100rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
color: white;
font-size: 32rpx;
background: rgba(0, 0, 0, 0.3);
}
.preview-index {
color: white;
}
.preview-close {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
color: white;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
}
.preview-swiper {
flex: 1;
width: 100%;
height: 100%;
}
.preview-image {
width: 100%;
height: 100%;
}

109
custom-tab-bar/index.js Normal file
View File

@@ -0,0 +1,109 @@
// custom-tab-bar/index.js — 三端固定,无身份切换
const app = getApp();
import { getTabRole } from '../utils/primary-role.js';
const roleCfg = {
normal: {
name: '点单老板',
tabList: [
{ pagePath: 'pages/index/index', text: '商城', iconPath: '/images/zhuye.png', selectedIconPath: '/images/zhuye.png' },
{ pagePath: 'pages/fenlei/fenlei', text: '分类', iconPath: '/images/fenlei.png', selectedIconPath: '/images/fenlei.png' },
{ pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' },
{ pagePath: 'pages/wode/wode', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' },
],
},
dashou: {
name: '打手',
tabList: [
{ pagePath: 'pages/jiedan/jiedan', text: '接单池', iconPath: '/images/jiedanchi.png', selectedIconPath: '/images/jiedanchi.png' },
{ pagePath: 'pages/dashoudingdan/dashoudingdan', text: '订单', iconPath: '/images/dingdan.png', selectedIconPath: '/images/dingdan.png' },
{ pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' },
{ pagePath: 'pages/dashouduan/dashouduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' },
],
},
shangjia: {
name: '商家',
tabList: [
{ pagePath: 'pages/sjdingdan/sjdingdan', text: '订单', iconPath: '/images/dingdan.png', selectedIconPath: '/images/dingdan.png' },
{ pagePath: 'pages/xiaoxi/xiaoxi', text: '消息', iconPath: '/images/xiaoxi.png', selectedIconPath: '/images/xiaoxi.png' },
{ pagePath: 'pages/shangjiaduan/shangjiaduan', text: '我的', iconPath: '/images/wode.png', selectedIconPath: '/images/wode.png' },
],
},
};
Component({
data: {
selectedIndex: 0,
tabList: [],
currentRole: 'normal',
badgeText: '',
},
lifetimes: {
attached() {
this.refresh();
this._badgeListener = (data) => {
if (data.badgeText !== undefined) {
this.setData({ badgeText: data.badgeText });
}
};
this._roleListener = () => this.refresh();
app.on('tabBarBadgeChanged', this._badgeListener);
app.on('currentRoleChanged', this._roleListener);
},
detached() {
if (this._badgeListener) app.off('tabBarBadgeChanged', this._badgeListener);
if (this._roleListener) app.off('currentRoleChanged', this._roleListener);
},
},
pageLifetimes: {
show() {
this.refresh();
const globalBadge = app.globalData.messageManager?.tabBarBadgeText || '';
if (this.data.badgeText !== globalBadge) {
this.setData({ badgeText: globalBadge });
}
this._syncUnreadWithDebounce();
},
},
methods: {
_syncUnreadWithDebounce() {
if (this._syncTimer) clearTimeout(this._syncTimer);
this._syncTimer = setTimeout(() => {
if (app && app.loadConversations) app.loadConversations();
}, 300);
},
refresh() {
const token = wx.getStorageSync('token');
let role = getTabRole(app);
if (!token) {
role = 'normal';
}
app.globalData.currentRole = role;
wx.setStorageSync('currentRole', role);
let cfg = roleCfg[role];
if (!cfg) {
cfg = roleCfg.normal;
role = 'normal';
}
const list = cfg.tabList.map((v, i) => ({ ...v, index: i }));
const pages = getCurrentPages();
const curRoute = pages.length ? pages[pages.length - 1].route : '';
let idx = list.findIndex((t) => t.pagePath === curRoute);
if (idx === -1) idx = 0;
this.setData({ currentRole: role, tabList: list, selectedIndex: idx });
},
switchTab(e) {
const path = e.currentTarget.dataset.path;
const idx = e.currentTarget.dataset.index;
if (idx === this.data.selectedIndex) return;
wx.redirectTo({ url: '/' + path });
},
},
});

View File

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

24
custom-tab-bar/index.wxml Normal file
View File

@@ -0,0 +1,24 @@
<view class="custom-tab-bar">
<view class="bar-row">
<view class="tab-capsule">
<block wx:for="{{tabList}}" wx:key="index">
<view
class="tab-item {{selectedIndex === index ? 'active' : ''}}"
data-path="{{item.pagePath}}"
data-index="{{index}}"
catchtap="switchTab"
>
<view class="badge-container" wx:if="{{item.pagePath === 'pages/xiaoxi/xiaoxi' && badgeText}}">
<view class="tab-badge">{{badgeText}}</view>
</view>
<image
class="tab-icon"
src="{{selectedIndex === index ? item.selectedIconPath : item.iconPath}}"
mode="aspectFit"
/>
<text class="tab-text">{{item.text}}</text>
</view>
</block>
</view>
</view>
</view>

123
custom-tab-bar/index.wxss Normal file
View File

@@ -0,0 +1,123 @@
/* custom-tab-bar — 三端固定,全宽 Tab 胶囊 */
.custom-tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
display: flex;
justify-content: center;
align-items: flex-end;
padding-bottom: env(safe-area-inset-bottom);
background: transparent;
}
.bar-row {
display: flex;
align-items: center;
margin-bottom: 15rpx;
padding: 0 24rpx;
width: 100%;
box-sizing: border-box;
}
.tab-capsule {
flex: 1;
display: flex;
align-items: center;
justify-content: space-around;
height: 100rpx;
background: #000000;
border-radius: 50rpx;
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.8);
padding: 0 20rpx;
position: relative;
}
.tab-capsule::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 60%;
height: 100%;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.25) 20%,
rgba(255, 255, 255, 0.35) 50%,
rgba(255, 255, 255, 0.25) 80%,
transparent 100%
);
animation: shine 3s infinite;
z-index: 1;
}
@keyframes shine {
0% { left: -100%; }
25% { left: 150%; }
100% { left: 150%; }
}
.tab-item {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 70rpx;
}
.tab-icon {
width: 44rpx;
height: 44rpx;
margin-bottom: 4rpx;
filter: brightness(0) invert(1);
opacity: 0.9;
}
.tab-text {
font-size: 20rpx;
color: #aaa;
z-index: 2;
}
.active .tab-icon {
opacity: 1;
filter: brightness(0) invert(1) drop-shadow(0 0 8rpx #00aaff);
}
.active .tab-text {
color: #00aaff;
font-weight: 500;
}
.badge-container {
position: absolute;
top: -10rpx;
right: -15rpx;
z-index: 100;
pointer-events: none;
}
.tab-badge {
min-width: 36rpx;
height: 36rpx;
line-height: 36rpx;
padding: 0 10rpx;
background: #ff3b30;
color: #fff;
font-size: 22rpx;
font-weight: bold;
border-radius: 36rpx;
text-align: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
border: 1rpx solid rgba(255, 255, 255, 0.4);
}
page {
padding-bottom: 100rpx;
}

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 60 40" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1">
<path d="M60 20 Q60 35 45 32 Q35 40 20 30 Q10 35 0 25" />
<path d="M20 30 Q16 34 12 30 Q16 26 20 30" fill="rgba(255,255,255,0.15)"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 60 40" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1">
<path d="M0 20 Q0 5 15 8 Q25 0 40 10 Q50 5 60 15" />
<path d="M40 10 Q44 6 48 10 Q44 14 40 10" fill="rgba(255,255,255,0.15)"/>
</svg>

After

Width:  |  Height:  |  Size: 223 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 100 80" fill="none" stroke="#b8943a" stroke-width="1.3">
<path d="M100 40 Q100 65 82 68 Q70 80 45 72 Q30 80 15 65 Q0 60 0 40" />
<path d="M45 72 Q40 77 35 72 Q40 67 45 72" fill="rgba(184,148,58,0.1)"/>
<path d="M82 68 Q78 72 74 68 Q78 64 82 68" fill="rgba(184,148,58,0.1)"/>
<path d="M95 45 Q95 58 88 62" stroke-width="0.8" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 100 80" fill="none" stroke="#b8943a" stroke-width="1.3">
<path d="M0 40 Q0 15 18 12 Q30 0 55 8 Q70 0 85 15 Q100 20 100 40" />
<path d="M55 8 Q60 3 65 8 Q60 13 55 8" fill="rgba(184,148,58,0.1)"/>
<path d="M18 12 Q22 8 26 12 Q22 16 18 12" fill="rgba(184,148,58,0.1)"/>
<path d="M5 35 Q5 22 12 18" stroke-width="0.8" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 120 60" fill="none" stroke="#b8943a" stroke-width="1.2">
<path d="M0 30 Q0 10 20 8 Q30 0 50 5 Q70 0 85 10 Q100 8 110 20 Q120 15 120 35" />
<path d="M20 8 Q25 3 30 8 Q25 13 20 8" fill="rgba(184,148,58,0.12)"/>
<path d="M85 10 Q90 5 95 10 Q90 15 85 10" fill="rgba(184,148,58,0.12)"/>
</svg>

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 120 60" fill="none" stroke="#b8943a" stroke-width="1.2">
<path d="M120 30 Q120 10 100 8 Q90 0 70 5 Q50 0 35 10 Q20 8 10 20 Q0 15 0 35" />
<path d="M100 8 Q95 3 90 8 Q95 13 100 8" fill="rgba(184,148,58,0.12)"/>
<path d="M35 10 Q30 5 25 10 Q30 15 35 10" fill="rgba(184,148,58,0.12)"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

BIN
images/dingdan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
images/fenlei.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
images/gonggao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
images/goumai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
images/jiedanchi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
images/jieshao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
images/jinpaidashou.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
images/role-switch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
images/shuaxin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
images/sousuo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
images/tiaoxuan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
images/tuichu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
images/weixinzhifu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
images/wode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/xiaoxi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
images/yituikuan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
images/yiwancheng.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
images/zhuanqu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
images/zhuye.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

100
pages/cfss/cfss/cfss.js Normal file
View File

@@ -0,0 +1,100 @@
// pages/cfss/cfss.js
import request from '../../../utils/request.js'; // 向上4级就是根目录
// 如果有 COS
const app = getApp();
Page({
data: {
// 统计数字(由接口 /yonghu/cffktjhq 返回)
stats: {
fakuan: { total: 0, daijiaona: 0, shensuzhong: 0, yijiaona: 0, yibohui: 0 },
jifen: { total: 0, daichuli: 0, yichuli: 0, daichuli_0: 0, shensuzhong_3: 0, yichufa_1: 0, yibohui_2: 0 }
},
// 当前选中的主标签:'fakuan' 或 'jifen'
activeTab: 'fakuan',
// 子标签状态值
activeFakuanStatus: 1, // 罚款默认展示“待缴纳”
activeJifenStatus: 0, // 积分默认展示“待处理”
// 搜索关键词(订单号)
searchDingdan: '',
// 刷新触发器,传给子组件
trigger: {
fakuan: 0,
jifen: 0
}
},
onLoad() {
this.fetchTongji();
},
onShow() {
// 每次显示页面都重新拉取统计,确保数字最新
this.fetchTongji();
},
// 获取综合统计
async fetchTongji() {
try {
const res = await request({
url: '/yonghu/cffktjhq',
method: 'POST'
});
if (res.statusCode === 200 && res.data.code === 0) {
this.setData({ stats: res.data.data });
}
} catch (error) {
console.error('获取处罚统计失败', error);
}
},
// 切换主Tab罚款 / 积分)
switchMainTab(e) {
const tab = e.currentTarget.dataset.tab;
if (tab === this.data.activeTab) return;
this.setData({ activeTab: tab });
},
// 切换罚款状态子标签
switchFakuanStatus(e) {
const status = Number(e.currentTarget.dataset.status);
if (status === this.data.activeFakuanStatus) return;
this.setData({ activeFakuanStatus: status });
this.bumpTrigger('fakuan'); // 通知罚款列表组件刷新
},
// 切换积分状态子标签
switchJifenStatus(e) {
const status = Number(e.currentTarget.dataset.status);
if (status === this.data.activeJifenStatus) return;
this.setData({ activeJifenStatus: status });
this.bumpTrigger('jifen'); // 通知积分列表组件刷新
},
// 搜索输入
onSearchInput(e) {
this.setData({ searchDingdan: e.detail.value });
},
// 执行搜索(按键盘搜索按钮或点击搜索图标)
onSearchConfirm() {
this.bumpTrigger(this.data.activeTab);
},
// 清空搜索关键字
clearSearch() {
this.setData({ searchDingdan: '' });
this.bumpTrigger(this.data.activeTab);
},
// 递增触发器,子组件通过 observer 响应
bumpTrigger(tab) {
const key = `trigger.${tab}`;
this.setData({ [key]: this.data.trigger[tab] + 1 });
}
});

10
pages/cfss/cfss/cfss.json Normal file
View File

@@ -0,0 +1,10 @@
{
"usingComponents": {
"fakuan-list": "/pages/cfss/components/fakuan-list/fakuan-list",
"jifen-list": "/pages/cfss/components/jifen-list/jifen-list"
},
"navigationBarTitleText": "处罚中心",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f7f8fa"
}

68
pages/cfss/cfss/cfss.wxml Normal file
View File

@@ -0,0 +1,68 @@
<!-- pages/cfss/cfss.wxml -->
<view class="page-container">
<!-- 统计卡片 -->
<view class="stats-card">
<view class="stats-number-row">
<view class="number-block">
<text class="big-num green">{{ stats.fakuan.daijiaona }}</text>
<text class="num-label">待缴罚款</text>
</view>
<view class="number-block">
<text class="big-num orange">{{ stats.jifen.daichuli }}</text>
<text class="num-label">待处理积分</text>
</view>
</view>
<view class="stats-detail-row">
<text>罚款:已缴{{ stats.fakuan.yijiaona }} · 申诉中{{ stats.fakuan.shensuzhong }} · 成功{{ stats.fakuan.yibohui }}</text>
<text>积分:已处理{{ stats.jifen.yichuli }}</text>
</view>
</view>
<!-- 主Tab -->
<view class="tab-row">
<view class="tab-item {{ activeTab === 'fakuan' ? 'active' : '' }}" bindtap="switchMainTab" data-tab="fakuan">
罚款
</view>
<view class="tab-item {{ activeTab === 'jifen' ? 'active' : '' }}" bindtap="switchMainTab" data-tab="jifen">
积分处罚
</view>
</view>
<!-- 子状态标签(横向滑动) -->
<block wx:if="{{ activeTab === 'fakuan' }}">
<scroll-view class="sub-tabs" scroll-x enable-flex>
<view class="sub-tag {{ activeFakuanStatus === 1 ? 'sub-active' : '' }}" bindtap="switchFakuanStatus" data-status="1">待缴纳 {{ stats.fakuan.daijiaona }}</view>
<view class="sub-tag {{ activeFakuanStatus === 3 ? 'sub-active' : '' }}" bindtap="switchFakuanStatus" data-status="3">申诉中 {{ stats.fakuan.shensuzhong }}</view>
<view class="sub-tag {{ activeFakuanStatus === 2 ? 'sub-active' : '' }}" bindtap="switchFakuanStatus" data-status="2">已缴纳 {{ stats.fakuan.yijiaona }}</view>
<view class="sub-tag {{ activeFakuanStatus === 4 ? 'sub-active' : '' }}" bindtap="switchFakuanStatus" data-status="4">申诉成功 {{ stats.fakuan.yibohui }}</view>
</scroll-view>
</block>
<block wx:else>
<scroll-view class="sub-tabs" scroll-x enable-flex>
<view class="sub-tag {{ activeJifenStatus === 0 ? 'sub-active' : '' }}" bindtap="switchJifenStatus" data-status="0">待处理 {{ stats.jifen.daichuli }}</view>
<view class="sub-tag {{ activeJifenStatus === 1 ? 'sub-active' : '' }}" bindtap="switchJifenStatus" data-status="1">已处理 {{ stats.jifen.yichuli }}</view>
</scroll-view>
</block>
<!-- 搜索栏 -->
<view class="search-bar">
<input class="search-input" placeholder="输入订单号搜索" value="{{ searchDingdan }}" bindinput="onSearchInput" bindconfirm="onSearchConfirm" confirm-type="search"/>
<view class="search-btn" bindtap="onSearchConfirm">搜索</view>
<view wx:if="{{ searchDingdan }}" class="clear-btn" bindtap="clearSearch">✕</view>
</view>
<!-- 列表区域(动态显示对应组件) -->
<view wx:if="{{ activeTab === 'fakuan' }}" class="list-area">
<fakuan-list
status="{{ activeFakuanStatus }}"
searchDingdan="{{ searchDingdan }}"
trigger="{{ trigger.fakuan }}"
/>
</view>
<view wx:else class="list-area">
<jifen-list
status="{{ activeJifenStatus }}"
trigger="{{ trigger.jifen }}"
/>
</view>
</view>

121
pages/cfss/cfss/cfss.wxss Normal file
View File

@@ -0,0 +1,121 @@
/* pages/cfss/cfss.wxss */
page { background: #f7f8fa; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
.page-container { padding-bottom: 30rpx; }
/* 统计卡片 */
.stats-card {
margin: 24rpx 28rpx;
padding: 30rpx 24rpx;
background: #fff;
border-radius: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
}
.stats-number-row {
display: flex;
justify-content: space-around;
margin-bottom: 20rpx;
}
.number-block {
display: flex;
flex-direction: column;
align-items: center;
}
.big-num {
font-size: 56rpx;
font-weight: 700;
}
.green { color: #2E7D32; }
.orange { color: #E67E22; }
.num-label {
font-size: 24rpx;
color: #888;
margin-top: 8rpx;
}
.stats-detail-row {
border-top: 1rpx solid #eee;
padding-top: 18rpx;
}
.stats-detail-row text {
display: block;
font-size: 22rpx;
color: #aaa;
margin-bottom: 4rpx;
}
/* 主Tab */
.tab-row {
display: flex;
margin: 16rpx 28rpx;
background: #fff;
border-radius: 18rpx;
overflow: hidden;
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.02);
}
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
transition: 0.2s;
}
.tab-item.active {
color: #2E7D32;
font-weight: 600;
background: #F0F9F0;
}
/* 子状态标签 */
.sub-tabs {
white-space: nowrap;
margin: 8rpx 28rpx 16rpx;
}
.sub-tag {
display: inline-block;
padding: 10rpx 26rpx;
margin-right: 14rpx;
background: #fff;
border-radius: 30rpx;
font-size: 24rpx;
color: #888;
box-shadow: 0 1rpx 4rpx rgba(0,0,0,0.02);
}
.sub-tag.sub-active {
background: #E8F5E9;
color: #2E7D32;
font-weight: 600;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
margin: 16rpx 28rpx;
background: #fff;
border-radius: 20rpx;
padding: 12rpx 22rpx;
box-shadow: 0 1rpx 6rpx rgba(0,0,0,0.02);
}
.search-input {
flex: 1;
font-size: 26rpx;
color: #333;
}
.search-btn {
margin-left: 18rpx;
padding: 12rpx 30rpx;
background: #2E7D32;
color: #fff;
border-radius: 16rpx;
font-size: 24rpx;
}
.clear-btn {
margin-left: 16rpx;
font-size: 30rpx;
color: #aaa;
}
.list-area {
margin-top: 10rpx;
}

View File

@@ -0,0 +1,317 @@
// components/fakuan-list/fakuan-list.js
import request from '../../../../utils/request.js';
const app = getApp();
const MEIYE_TIAOSHU = 5;
Component({
properties: {
status: { type: Number, value: 1, observer: 'reload' },
searchDingdan: { type: String, value: '', observer: 'reload' },
trigger: { type: Number, value: 0, observer: 'reload' }
},
data: {
list: [],
page: 1,
hasMore: true,
loading: false,
loadingMore: false,
ossImageUrl: app.globalData.ossImageUrl || '',
// 详情弹窗
showDetail: false,
detailItem: null,
// 申诉弹窗
showShensu: false,
shensuLiyou: '',
shensuTupian: [],
uploading: false,
uploadProgress: { current: 0, total: 0 },
uploadProgressWidth: '0%',
// 支付弹窗
showPay: false,
payFadan: null
},
lifetimes: {
attached() { this.reload(); }
},
methods: {
// ==================== 数据加载 ====================
reload() {
this.setData({ page: 1, list: [], hasMore: true });
this.loadData();
},
async loadData(isLoadMore = false) {
// 防止重复请求
if (this.data.loading || this.data.loadingMore) return;
if (!isLoadMore) {
this.setData({ loading: true });
} else {
// 加载更多时,如果后端已经告知没有更多数据,不发送请求
if (!this.data.hasMore) {
wx.showToast({ title: '没有更多了', icon: 'none', duration: 1000 });
return;
}
this.setData({ loadingMore: true });
}
try {
const res = await request({
url: '/yonghu/dsfklbhq',
method: 'POST',
data: {
page: this.data.page,
page_size: MEIYE_TIAOSHU,
zhuangtai: this.data.status,
sousuo_dingdan_id: this.data.searchDingdan
}
});
if (res.statusCode === 200 && res.data.code === 0) {
const data = res.data.data;
const newList = isLoadMore ? this.data.list.concat(data.list || []) : (data.list || []);
this.setData({
list: newList,
hasMore: data.has_more || false, // 以后端返回为准
page: isLoadMore ? this.data.page + 1 : 2, // 首次加载后页码为2
loading: false,
loadingMore: false
});
} else {
wx.showToast({ title: res.data?.msg || '加载失败', icon: 'none' });
this.setData({ loading: false, loadingMore: false });
}
} catch (e) {
wx.showToast({ title: '网络请求失败', icon: 'none' });
this.setData({ loading: false, loadingMore: false });
}
},
loadMore() {
if (this.data.list.length === 0) return;
this.loadData(true);
},
// ==================== 详情弹窗 ====================
openDetail(e) {
const item = e.currentTarget.dataset.item;
this.setData({ showDetail: true, detailItem: item });
},
closeDetail() {
this.setData({ showDetail: false, detailItem: null });
},
// ==================== 申诉逻辑 ====================
openShensu() {
const item = this.data.detailItem;
if (item.bohuiliyou) {
wx.showToast({ title: '该罚单申诉已被驳回', icon: 'none' });
return;
}
const existingImgs = (item.shensu_tupian || []).map(url => this.getFullUrl(url));
this.setData({
showShensu: true,
shensuLiyou: item.shensuliyou || '',
shensuTupian: existingImgs,
uploading: false,
uploadProgress: { current: 0, total: 0 },
uploadProgressWidth: '0%'
});
},
closeShensu() {
this.setData({ showShensu: false });
},
onShensuInput(e) {
this.setData({ shensuLiyou: e.detail.value.slice(0, 500) });
},
chooseImage() {
const remain = 9 - this.data.shensuTupian.length;
if (remain <= 0) return;
wx.chooseMedia({
count: remain,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const newImgs = [...this.data.shensuTupian, ...res.tempFiles.map(f => f.tempFilePath)];
this.setData({ shensuTupian: newImgs.slice(0, 9) });
}
});
},
deleteShensuImg(e) {
const arr = this.data.shensuTupian;
arr.splice(e.currentTarget.dataset.index, 1);
this.setData({ shensuTupian: arr });
},
previewImage(e) {
wx.previewImage({ current: e.currentTarget.dataset.url, urls: [e.currentTarget.dataset.url] });
},
previewLocalImg(e) {
const idx = e.currentTarget.dataset.index;
wx.previewImage({ current: this.data.shensuTupian[idx], urls: this.data.shensuTupian });
},
// ==================== 提交流程 ====================
async submitShensu() {
if (!this.data.shensuLiyou.trim()) {
wx.showToast({ title: '请输入申诉理由', icon: 'none' });
return;
}
wx.showLoading({ title: '提交中...' });
try {
const newLocalPaths = this.data.shensuTupian.filter(path => !path.startsWith('http'));
let finalUrls = [];
if (newLocalPaths.length > 0) {
finalUrls = await this.piliangShangchuanShensuTupian(newLocalPaths);
if (!finalUrls) {
wx.hideLoading();
return;
}
}
const oldRemoteRelative = this.data.shensuTupian
.filter(path => path.startsWith('http'))
.map(url => url.replace(this.data.ossImageUrl, '').replace(/^\//, ''));
const allUrls = oldRemoteRelative.concat(finalUrls);
await this.doSubmitShensu(allUrls);
} catch (err) {
console.error('提交申诉失败', err);
wx.showToast({ title: err.message || '提交失败', icon: 'none' });
} finally {
wx.hideLoading();
}
},
async doSubmitShensu(tupianUrls) {
const res = await request({
url: '/yonghu/fkss',
method: 'POST',
data: {
fadan_id: this.data.detailItem.id,
shensu_liyou: this.data.shensuLiyou,
shensu_tupian_urls: tupianUrls
}
});
if (res.statusCode === 200 && res.data.code === 0) {
wx.showToast({ title: '申诉已提交', icon: 'success' });
this.setData({ showShensu: false, showDetail: false });
this.reload();
} else {
throw new Error(res.data?.msg || '提交失败');
}
},
// ==================== 图片上传到COS ====================
async piliangShangchuanShensuTupian(localPaths) {
const total = localPaths.length;
if (!total) return [];
this.setData({ uploading: true, uploadProgress: { current: 0, total: total }, uploadProgressWidth: '0%' });
let tokenData;
try {
const credRes = await request({
url: '/dingdan/dsscpz',
method: 'POST',
data: {
dingdan_id: this.data.detailItem.guanliandingdan_id || '',
fadan_id: this.data.detailItem.id,
yongtu: 'fakuan'
}
});
if (credRes.data.code !== 0) throw new Error(credRes.data.msg || '凭证获取失败');
tokenData = credRes.data.data;
} catch (e) {
wx.showToast({ title: '获取上传凭证失败', icon: 'none' });
this.setData({ uploading: false });
return null;
}
const COS = require('../../../../utils/cos-wx-sdk-v5.js');
const credentials = tokenData.credentials || tokenData;
const cos = new COS({
SimpleUploadMethod: 'putObject',
getAuthorization: (options, callback) => {
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
SecurityToken: credentials.sessionToken || '',
StartTime: tokenData.startTime,
ExpiredTime: tokenData.expiredTime
});
}
});
const bucket = tokenData.bucket || 'julebu-1361527063';
const region = tokenData.region || 'ap-shanghai';
const yonghuid = app.globalData.userInfo?.yonghuid || wx.getStorageSync('uid') || '0000000';
const fadanId = this.data.detailItem.id;
const keys = [];
for (let i = 0; i < total; i++) {
keys.push(`fakuan/dashoufakuan/dashoufakuanshensu/${yonghuid}_${fadanId}_${Date.now() + i}.jpg`);
}
for (let i = 0; i < total; i++) {
try {
await new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => resolve(), 2000);
cos.uploadFile({
Bucket: bucket,
Region: region,
Key: keys[i],
FilePath: localPaths[i],
onProgress: (info) => {
if (Math.round(info.percent * 100) === 100) {
clearTimeout(timeoutId);
resolve();
}
}
}, (err) => {
clearTimeout(timeoutId);
if (err) reject(err);
else resolve();
});
});
} catch (err) {
wx.showToast({ title: `${i + 1} 张上传失败`, icon: 'none' });
this.setData({ uploading: false });
return null;
}
const done = i + 1;
this.setData({
uploadProgress: { current: done, total: total },
uploadProgressWidth: `${((done / total) * 100).toFixed(0)}%`
});
}
this.setData({ uploading: false });
return keys;
},
// ==================== 缴纳支付 ====================
startPay() {
this.setData({
showDetail: false,
showPay: true,
payFadan: this.data.detailItem
});
},
onPayClose() {
this.setData({ showPay: false, payFadan: null });
},
onPaySuccess() {
this.setData({ showPay: false, payFadan: null });
this.reload();
},
// ==================== 工具方法 ====================
getFullUrl(url) {
if (!url) return '';
if (url.startsWith('http')) return url;
return this.data.ossImageUrl + url;
}
}
});

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"fakuan-pay": "/pages/cfss/components/fakuan-pay/fakuan-pay"
}
}

View File

@@ -0,0 +1,116 @@
<!-- components/fakuan-list/fakuan-list.wxml -->
<view class="container">
<scroll-view scroll-y class="scroll" bindscrolltolower="loadMore">
<view wx:if="{{ loading && list.length === 0 }}" class="tip">加载中...</view>
<block wx:for="{{ list }}" wx:key="id">
<view class="card" bindtap="openDetail" data-item="{{ item }}">
<view class="card-head">
<text class="time">{{ item.create_time }}</text>
<text class="tag {{ item.status_class }}">{{ item.display_status }}</text>
</view>
<text class="info">金额:¥{{ item.fakuanjine }}</text>
<text class="info">理由:{{ item.chufaliyou }}</text>
<text class="info" wx:if="{{ item.guanliandingdan_id }}">订单:{{ item.guanliandingdan_id }}</text>
</view>
</block>
<!-- ✅ 按钮始终显示,只要列表不为空且没有正在加载 -->
<view wx:if="{{ list.length > 0 && !loadingMore }}" class="load-more-btn" bindtap="loadMore">
<text>点击获取更多</text>
</view>
<view wx:if="{{ loadingMore }}" class="tip">加载中...</view>
<view wx:if="{{ !hasMore && list.length > 0 }}" class="tip">—— 没有更多了 ——</view>
<view wx:if="{{ !loading && list.length === 0 }}" class="empty">暂无罚款记录</view>
</scroll-view>
<!-- 详情弹窗 -->
<view wx:if="{{ showDetail }}" class="modal-mask">
<view class="modal-panel">
<view class="modal-head">
<text class="modal-title">罚款详情</text>
<view class="modal-close" bindtap="closeDetail">✕</view>
</view>
<scroll-view scroll-y class="modal-body">
<view class="row"><text class="lbl">罚款金额</text><text class="val strong">¥{{ detailItem.fakuanjine }}</text></view>
<view class="row"><text class="lbl">当前状态</text><text class="tag {{ detailItem.status_class }}">{{ detailItem.display_status }}</text></view>
<view class="row"><text class="lbl">处罚理由</text><text class="val">{{ detailItem.chufaliyou }}</text></view>
<view wx:if="{{ detailItem.guanliandingdan_id }}" class="row"><text class="lbl">关联订单</text><text class="val">{{ detailItem.guanliandingdan_id }}</text></view>
<view wx:if="{{ detailItem.bohuiliyou }}" class="row"><text class="lbl">驳回理由</text><text class="val">{{ detailItem.bohuiliyou }}</text></view>
<view wx:if="{{ detailItem.shensuliyou }}" class="row"><text class="lbl">申诉理由</text><text class="val">{{ detailItem.shensuliyou }}</text></view>
<!-- 证据图片 -->
<view wx:if="{{ detailItem.zhengju_tupian && detailItem.zhengju_tupian.length }}" class="section">
<text class="sec-title">证据图片</text>
<view class="img-grid">
<block wx:for="{{ detailItem.zhengju_tupian }}" wx:key="index">
<view class="img-box" bindtap="previewImage" data-url="{{ ossImageUrl + item }}">
<image class="img" src="{{ ossImageUrl + item }}" mode="aspectFill" />
</view>
</block>
</view>
</view>
<!-- 申诉图片 -->
<view wx:if="{{ detailItem.shensu_tupian && detailItem.shensu_tupian.length }}" class="section">
<text class="sec-title">申诉图片</text>
<view class="img-grid">
<block wx:for="{{ detailItem.shensu_tupian }}" wx:key="index">
<view class="img-box" bindtap="previewImage" data-url="{{ ossImageUrl + item }}">
<image class="img" src="{{ ossImageUrl + item }}" mode="aspectFill" />
</view>
</block>
</view>
</view>
</scroll-view>
<view class="modal-foot">
<view wx:if="{{ detailItem.zhuangtai === 1 && !detailItem.bohuiliyou }}" class="btn primary" bindtap="openShensu">申诉</view>
<view wx:if="{{ detailItem.zhuangtai === 1 || detailItem.zhuangtai === 3 }}" class="btn primary" bindtap="startPay">立即缴纳</view>
<view class="btn default" bindtap="closeDetail">关闭</view>
</view>
</view>
</view>
<!-- 申诉弹窗 -->
<view wx:if="{{ showShensu }}" class="modal-mask">
<view class="modal-panel">
<view class="modal-head">
<text class="modal-title">提交申诉</text>
<view class="modal-close" bindtap="closeShensu">✕</view>
</view>
<scroll-view scroll-y class="modal-body">
<view class="form-group">
<text class="sec-title">申诉理由</text>
<textarea class="textarea" placeholder="请输入申诉理由" value="{{ shensuLiyou }}" bindinput="onShensuInput" maxlength="500" />
<text class="word-count">{{ shensuLiyou.length }}/500</text>
</view>
<view class="form-group">
<text class="sec-title">上传凭证最多9张</text>
<view class="img-grid">
<block wx:for="{{ shensuTupian }}" wx:key="index">
<view class="img-box">
<image class="img" src="{{ item }}" mode="aspectFill" bindtap="previewLocalImg" data-index="{{ index }}" />
<view class="img-del" catchtap="deleteShensuImg" data-index="{{ index }}">✕</view>
</view>
</block>
<view wx:if="{{ shensuTupian.length < 9 }}" class="img-add" bindtap="chooseImage">+</view>
</view>
<view wx:if="{{ uploading }}" class="progress-box">
<view class="progress-bar"><view class="progress-inner" style="width: {{ uploadProgressWidth }}"></view></view>
<text class="progress-text">{{ uploadProgress.current }}/{{ uploadProgress.total }}</text>
</view>
</view>
</scroll-view>
<view class="modal-foot">
<view class="btn default" bindtap="closeShensu">取消</view>
<view class="btn primary" bindtap="submitShensu">提交申诉</view>
</view>
</view>
</view>
<!-- 支付组件(缴纳罚款) -->
<fakuan-pay
visible="{{ showPay }}"
fadan="{{ payFadan }}"
bind:close="onPayClose"
bind:success="onPaySuccess"
/>
</view>

View File

@@ -0,0 +1,77 @@
/* 完全复用积分组件的样式 */
page { background: #f5f5f5; }
.container { width: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
.scroll { width: 100%; box-sizing: border-box; padding: 20rpx 24rpx; }
.tip { text-align: center; color: #999; font-size: 24rpx; padding: 30rpx 0; }
.empty { display: flex; flex-direction: column; align-items: center; padding-top: 200rpx; color: #999; font-size: 28rpx; }
.card { width: 100%; box-sizing: border-box; background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
.card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
.time { font-size: 24rpx; color: #999; }
.tag { font-size: 20rpx; padding: 2rpx 14rpx; border-radius: 16rpx; display: inline-block; }
/* 状态标签颜色(与积分组件完全相同) */
.zhuangtai-daijiaona { background: #FFF3E0; color: #E65100; } /* 待缴纳 */
.zhuangtai-yijiaona { background: #E8F5E9; color: #2E7D32; } /* 已缴纳 */
.zhuangtai-shensuzhong { background: #E3F2FD; color: #1565C0; } /* 申诉中 */
.zhuangtai-yibohui { background: #F3E5F5; color: #7B1FA2; } /* 申诉成功 */
.info { display: block; font-size: 26rpx; color: #333; margin-top: 8rpx; }
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 999; display: flex; align-items: flex-end; justify-content: center; }
.modal-panel { width: 100%; box-sizing: border-box; max-height: 85vh; background: #fff; border-radius: 24rpx 24rpx 0 0; display: flex; flex-direction: column; overflow: hidden; }
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 30rpx 16rpx; border-bottom: 1px solid #eee; }
.modal-title { font-size: 32rpx; font-weight: 600; }
.modal-close { width: 44rpx; height: 44rpx; border-radius: 50%; background: #f5f5f5; display: flex; align-items: center; justify-content: center; font-size: 26rpx; color: #888; }
.modal-body { width: 100%; box-sizing: border-box; padding: 20rpx 30rpx; overflow-y: auto; }
.row { display: flex; margin-bottom: 16rpx; }
.lbl { width: 150rpx; font-size: 26rpx; color: #888; flex-shrink: 0; }
.val { flex: 1; font-size: 26rpx; color: #333; word-break: break-all; }
.strong { font-weight: 600; color: #E53935; }
.section { margin: 20rpx 0; }
.sec-title { font-size: 26rpx; color: #888; margin-bottom: 12rpx; display: block; }
.img-grid { width: 100%; box-sizing: border-box; display: flex; flex-wrap: wrap; gap: 12rpx; }
.img-box { position: relative; width: calc((100% - 24rpx) / 3); height: 0; padding-bottom: calc((100% - 24rpx) / 3); }
.img-box .img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border-radius: 10rpx; background: #f0f0f0; }
.img-del { position: absolute; top: -6rpx; right: -6rpx; width: 34rpx; height: 34rpx; background: #e53935; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 18rpx; z-index: 2; }
.img-add { width: calc((100% - 24rpx) / 3); height: 0; padding-bottom: calc((100% - 24rpx) / 3); border: 2rpx dashed #ccc; border-radius: 10rpx; display: flex; align-items: center; justify-content: center; font-size: 48rpx; color: #aaa; position: relative; box-sizing: border-box; }
.form-group { margin-bottom: 24rpx; }
.textarea { width: 100%; box-sizing: border-box; height: 180rpx; background: #f9f9f9; border-radius: 10rpx; padding: 16rpx; font-size: 26rpx; border: 1px solid #eee; }
.word-count { text-align: right; font-size: 22rpx; color: #bbb; margin-top: 6rpx; }
.progress-box { margin-top: 12rpx; }
.progress-bar { height: 10rpx; background: #eee; border-radius: 5rpx; }
.progress-inner { height: 100%; background: #2e7d32; border-radius: 5rpx; }
.progress-text { font-size: 22rpx; color: #2e7d32; text-align: right; margin-top: 4rpx; }
.modal-foot { width: 100%; box-sizing: border-box; display: flex; padding: 16rpx 30rpx 30rpx; gap: 20rpx; border-top: 1px solid #eee; }
.btn { flex: 1; text-align: center; padding: 22rpx 0; border-radius: 16rpx; font-size: 28rpx; font-weight: 600; }
.primary { background: #2e7d32; color: #fff; }
.default { background: #f0f0f0; color: #555; }
.btn:active { opacity: 0.8; }
/* ===== 获取更多按钮 ===== */
.load-more-btn {
width: 80%;
margin: 20rpx auto 30rpx;
padding: 22rpx 0;
text-align: center;
background: #ffffff;
border: 2rpx solid #e0e0e0;
border-radius: 20rpx;
color: #555;
font-size: 26rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.02);
transition: all 0.15s;
}
.load-more-btn:active {
background: #f5f5f5;
border-color: #2e7d32;
color: #2e7d32;
}

View File

@@ -0,0 +1,178 @@
// components/fakuan-list/fakuan-pay/fakuan-pay.js
import request from '../../../../utils/request.js'; // 路径保持您原来的样子
Component({
properties: {
visible: { type: Boolean, value: false, observer: 'onVisibleChange' },
fadan: { type: Object, value: null } // { id, fakuanjine }
},
data: {
step: 'method', // method / balance / confirm / loading
balanceOptions: [],
selectedBalance: null,
loadingText: '',
payLoading: false,
dingdanid: '' // 微信支付内部订单ID
},
methods: {
onVisibleChange(val) {
if (val) {
// 每次打开重置为支付方式选择
this.setData({ step: 'method', balanceOptions: [], selectedBalance: null });
}
},
close() {
this.setData({ visible: false });
this.triggerEvent('close');
},
// 选择支付方式
async chooseMethod(e) {
const method = e.currentTarget.dataset.method;
if (method === 'wx') {
this.wxPay();
} else if (method === 'balance') {
this.fetchBalanceOptions();
}
},
// ========== 微信支付流程 ==========
async wxPay() {
this.setData({ step: 'loading', loadingText: '发起支付中...' });
try {
const res = await request({
url: '/yonghu/fkjn',
method: 'POST',
data: { fadan_id: this.properties.fadan.id }
});
if (res.data.code !== 200) throw new Error(res.data.msg || '下单失败');
// 🔥 唯一修改点:后端返回格式 { code:200, payParams:{...}, dingdanid:'...' }
const payParams = res.data.payParams;
const dingdanid = res.data.dingdanid;
this.setData({ dingdanid });
await this.requestWxPayment(payParams);
} catch (e) {
this.setData({ step: 'method' });
wx.showToast({ title: e.message, icon: 'none' });
}
},
async requestWxPayment(payParams) {
return new Promise((resolve, reject) => {
if (!payParams || !payParams.timeStamp || !payParams.paySign) {
wx.showToast({ title: '支付参数错误', icon: 'none' });
reject(new Error('支付参数错误'));
return;
}
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'MD5',
paySign: payParams.paySign,
success: () => {
this.pollPayStatus();
resolve();
},
fail: (err) => {
this.setData({ step: 'method' });
this.notifyPayFail();
reject(err);
}
});
});
},
async pollPayStatus() {
this.setData({ step: 'loading', loadingText: '确认支付结果...' });
for (let i = 0; i < 10; i++) {
try {
const res = await request({
url: '/yonghu/fkzffc',
method: 'POST',
data: { dingdanid: this.data.dingdanid }
});
if (res.data.code === 200) {
this.triggerEvent('success');
this.close();
wx.showToast({ title: '缴纳成功', icon: 'success' });
return;
}
} catch (e) {}
await new Promise(r => setTimeout(r, 2000));
}
this.setData({ step: 'method' });
wx.showToast({ title: '支付确认超时', icon: 'none' });
},
async notifyPayFail() {
try {
await request({
url: '/yonghu/fkzfsb',
method: 'POST',
data: { dingdanid: this.data.dingdanid }
});
} catch (e) {}
},
// ========== 佣金抵扣流程 ==========
async fetchBalanceOptions() {
this.setData({ step: 'loading', loadingText: '获取可抵扣身份...' });
try {
const res = await request({
url: '/shangpin/czhqdy',
method: 'POST',
data: {
leixing: 4,
fadan_id: this.properties.fadan.id,
jine: this.properties.fadan.fakuanjine
}
});
if (res.data.code !== 200) throw new Error(res.data.msg || '获取失败');
this.setData({ balanceOptions: res.data.data, step: 'balance' });
} catch (e) {
this.setData({ step: 'method' });
wx.showToast({ title: e.message, icon: 'none' });
}
},
selectBalance(e) {
const id = e.currentTarget.dataset.id;
const info = this.data.balanceOptions.find(opt => opt.id === id);
if (!info) return;
this.setData({ selectedBalance: info, step: 'confirm' });
},
backToMethod() {
this.setData({ step: 'method' });
},
async confirmBalance() {
if (!this.data.selectedBalance) return;
this.setData({ step: 'loading', loadingText: '抵扣扣款中...' });
try {
const res = await request({
url: '/shangpin/dsqrgmdh',
method: 'POST',
data: {
leixing: 4,
shenfen_id: this.data.selectedBalance.id,
fadan_id: this.properties.fadan.id
}
});
if (res.data.code !== 200) throw new Error(res.data.msg || '抵扣失败');
this.triggerEvent('success');
this.close();
wx.showToast({ title: '缴纳成功', icon: 'success' });
} catch (e) {
this.setData({ step: 'method' });
wx.showToast({ title: e.message, icon: 'none' });
}
}
}
});

View File

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

View File

@@ -0,0 +1,61 @@
<!-- 支付组件模板 -->
<view wx:if="{{ visible }}" class="pay-mask" catchtouchmove="prevent">
<!-- 支付方式选择 -->
<view wx:if="{{ step === 'method' }}" class="pay-panel">
<view class="pay-head">
<text class="pay-title">选择支付方式</text>
<view class="pay-close" bindtap="close">✕</view>
</view>
<view class="pay-body">
<view class="method-item" bindtap="chooseMethod" data-method="wx">
<text class="method-icon">💳</text>
<text class="method-name">微信支付</text>
</view>
<view class="method-item" bindtap="chooseMethod" data-method="balance">
<text class="method-icon">💰</text>
<text class="method-name">佣金抵扣</text>
</view>
</view>
</view>
<!-- 余额抵扣身份选择 -->
<view wx:if="{{ step === 'balance' }}" class="pay-panel">
<view class="pay-head">
<text class="pay-title">选择抵扣身份</text>
<view class="pay-close" bindtap="close">✕</view>
</view>
<scroll-view scroll-y class="pay-body">
<view wx:for="{{ balanceOptions }}" wx:key="id" class="balance-item" bindtap="selectBalance" data-id="{{ item.id }}">
<text class="balance-name">{{ item.jieshao }}</text>
<text class="balance-amount">需¥{{ item.xuyao }}</text>
</view>
</scroll-view>
</view>
<!-- 余额抵扣确认 -->
<view wx:if="{{ step === 'confirm' }}" class="pay-panel">
<view class="pay-head">
<text class="pay-title">确认抵扣</text>
<view class="pay-close" bindtap="close">✕</view>
</view>
<view class="pay-body">
<view class="confirm-row">
<text>抵扣身份:{{ selectedBalance.jieshao }}</text>
</view>
<view class="confirm-row">
<text>需扣除:¥{{ selectedBalance.xuyao }}</text>
</view>
<text class="confirm-tip">确认后将从您的佣金/分红中扣除</text>
</view>
<view class="pay-foot">
<view class="btn default" bindtap="backToMethod">取消</view>
<view class="btn primary" bindtap="confirmBalance">确认</view>
</view>
</view>
<!-- 支付加载动画 -->
<view wx:if="{{ step === 'loading' }}" class="pay-mask loading-box">
<view class="spinner"></view>
<text class="loading-text">{{ loadingText }}</text>
</view>
</view>

View File

@@ -0,0 +1,59 @@
/* 支付组件样式,与罚款列表统一 */
.pay-mask {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5); z-index: 2000;
display: flex; align-items: flex-end; justify-content: center;
}
.pay-panel {
width: 100%; max-height: 85vh; background: #fff;
border-radius: 24rpx 24rpx 0 0; display: flex; flex-direction: column; overflow: hidden;
}
.pay-head {
display: flex; justify-content: space-between; align-items: center;
padding: 28rpx 30rpx 16rpx; border-bottom: 1px solid #eee;
}
.pay-title { font-size: 32rpx; font-weight: 600; }
.pay-close {
width: 44rpx; height: 44rpx; background: #f5f5f5;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
font-size: 26rpx; color: #888;
}
.pay-body { padding: 20rpx 30rpx; flex: 1; overflow-y: auto; }
.pay-foot {
display: flex; padding: 16rpx 30rpx 30rpx; gap: 20rpx; border-top: 1px solid #eee;
}
.method-item {
display: flex; flex-direction: column; align-items: center;
padding: 40rpx 30rpx; background: #f9f9f9; border-radius: 20rpx; margin-bottom: 20rpx;
}
.method-icon { font-size: 48rpx; margin-bottom: 12rpx; }
.method-name { font-size: 28rpx; color: #333; }
.balance-item {
display: flex; justify-content: space-between; align-items: center;
padding: 24rpx 0; border-bottom: 1rpx solid #f0f0f0;
}
.balance-name { font-size: 28rpx; color: #333; }
.balance-amount { font-size: 28rpx; color: #E53935; }
.confirm-row { font-size: 28rpx; margin-bottom: 16rpx; }
.confirm-tip { font-size: 24rpx; color: #888; display: block; margin-top: 20rpx; }
.loading-box {
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: rgba(0,0,0,0.6);
}
.spinner {
width: 60rpx; height: 60rpx;
border: 4rpx solid rgba(255,255,255,0.3);
border-top-color: #fff; border-radius: 50%;
animation: spin 1s linear infinite; margin-bottom: 20rpx;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text { color: #fff; font-size: 26rpx; }
.btn { flex: 1; text-align: center; padding: 22rpx 0; border-radius: 16rpx; font-size: 28rpx; font-weight: 600; }
.primary { background: #2e7d32; color: #fff; }
.default { background: #f0f0f0; color: #555; }
.btn:active { opacity: 0.8; }

View File

@@ -0,0 +1,220 @@
// components/jifen-list/jifen-list.js
import request from '../../../../utils/request.js';
const app = getApp();
const MEIYE_TIAOSHU = 5;
Component({
properties: {
status: { type: Number, value: 0, observer: 'reload' },
trigger: { type: Number, value: 0, observer: 'reload' }
},
data: {
chufaList: [],
dangqianye: 1,
haiyougengduo: true,
jiazhaozhong: false,
jiazhaigengduo: false,
ossImageUrl: app.globalData.ossImageUrl || '',
showXiangqing: false,
xuanzhongChufa: {},
showShensuModal: false,
shensuLiyou: '',
shensuTupian: [],
shangchuanJindu: 0,
shangchuanZongshu: 0,
jinduWidth: '0%',
isShangchuanzhong: false,
fangdouTimer: null
},
lifetimes: {
attached() { this.reload(); },
detached() { if (this.data.fangdouTimer) clearTimeout(this.data.fangdouTimer); }
},
methods: {
reload() {
this.setData({ dangqianye: 1, chufaList: [], haiyougengduo: true });
this.loadJifenList();
},
async loadJifenList(isLoadMore = false) {
if (this.data.jiazhaozhong || this.data.jiazhaigengduo) return;
if (isLoadMore && !this.data.haiyougengduo) return;
if (isLoadMore) this.setData({ jiazhaigengduo: true });
else this.setData({ jiazhaozhong: true });
try {
const params = { page: this.data.dangqianye, page_size: MEIYE_TIAOSHU, zhuangtai: this.data.status };
const res = await request({ url: '/yonghu/dshqcfjl', method: 'POST', data: params });
if (res.statusCode === 200 && res.data.code === 0) {
const data = res.data.data;
if (data.list && Array.isArray(data.list)) {
const processedList = data.list.map(item => {
let displayTime = '--';
if (item.create_time) displayTime = item.create_time;
let displayStatus = '未知状态', statusClass = 'zhuangtai-weizhi';
const n = Number(item.sqzhuangtai);
if (!isNaN(n)) {
if (n === 0) { displayStatus = '待处理'; statusClass = 'zhuangtai-daichuli'; }
else if (n === 1) { displayStatus = '已处罚'; statusClass = 'zhuangtai-yichufa'; }
else if (n === 2) { displayStatus = '处罚已撤销'; statusClass = 'zhuangtai-yibohui'; }
else if (n === 3) { displayStatus = '申诉中'; statusClass = 'zhuangtai-shensuzhong'; }
}
return {
...item,
display_time: displayTime, display_status: displayStatus, status_class: statusClass,
full_zhengju_tupian: (item.zhengju_tupian || []).map(url => this.getFullImageUrl(url)),
full_shensu_tupian: (item.shensu_tupian || []).map(url => this.getFullImageUrl(url))
};
});
const newList = isLoadMore ? [...this.data.chufaList, ...processedList] : processedList;
this.setData({ chufaList: newList, haiyougengduo: data.has_more === true, dangqianye: this.data.dangqianye + 1 });
} else this.setData({ haiyougengduo: false });
}
} catch (e) { wx.showToast({ title: '加载失败', icon: 'none' }); this.setData({ haiyougengduo: false }); }
finally { if (isLoadMore) this.setData({ jiazhaigengduo: false }); else this.setData({ jiazhaozhong: false }); }
},
onReachBottom() {
if (this.data.fangdouTimer) clearTimeout(this.data.fangdouTimer);
const timer = setTimeout(() => {
if (this.data.jiazhaozhong || this.data.jiazhaigengduo) return;
if (!this.data.haiyougengduo) return;
this.loadJifenList(true);
}, 300);
this.setData({ fangdouTimer: timer });
},
chakanXiangqing(e) { this.setData({ showXiangqing: true, xuanzhongChufa: e.currentTarget.dataset.item }); },
guanbiXiangqing() { this.setData({ showXiangqing: false, xuanzhongChufa: {} }); },
openShensuModal() {
const { xuanzhongChufa } = this.data;
if (xuanzhongChufa.ssliyou || (xuanzhongChufa.shensu_tupian && xuanzhongChufa.shensu_tupian.length > 0)) {
wx.showToast({ title: '您已经提交过申诉,请等待处理结果', icon: 'none' }); return;
}
this.setData({ showShensuModal: true, shensuLiyou: '', shensuTupian: [], shangchuanJindu: 0, shangchuanZongshu: 0, jinduWidth: '0%', isShangchuanzhong: false });
},
closeShensuModal() { this.setData({ showShensuModal: false }); },
onShensuLiyouInput(e) { this.setData({ shensuLiyou: e.detail.value.slice(0, 500) }); },
chooseShensuTupian() {
if (this.data.isShangchuanzhong) return;
const shengyu = 9 - this.data.shensuTupian.length;
if (shengyu <= 0) { wx.showToast({ title: '最多只能上传9张图片', icon: 'none' }); return; }
wx.chooseMedia({ count: shengyu, mediaType: ['image'], sourceType: ['album', 'camera'], success: (res) => {
const newTupian = [...this.data.shensuTupian, ...res.tempFiles.map(f => f.tempFilePath)];
this.setData({ shensuTupian: newTupian.slice(0, 9) });
}});
},
deleteShensuTupian(e) { const arr = [...this.data.shensuTupian]; arr.splice(e.currentTarget.dataset.index, 1); this.setData({ shensuTupian: arr }); },
yulanShensuTupian(e) {
const idx = e.currentTarget.dataset.index;
const urls = this.data.shensuTupian;
if (!urls[idx]) return;
wx.previewImage({ current: urls[idx], urls });
},
yulanTupian(e) {
const current = e.currentTarget.dataset.url;
const urls = e.currentTarget.dataset.urls || [];
const fullCurrent = current.startsWith('http') ? current : this.getFullImageUrl(current);
const fullUrls = urls.map(url => url.startsWith('http') ? url : this.getFullImageUrl(url));
wx.previewImage({ current: fullCurrent, urls: fullUrls.length ? fullUrls : [fullCurrent] });
},
/* ========== 图片上传(完全照抄原 cfss.js ========== */
// 【仅替换此方法,其余代码一个字别动】
getFullImageUrl(relativeUrl) {
if (!relativeUrl) return '';
if (relativeUrl.startsWith('http')) return relativeUrl;
const ossUrl = this.data.ossImageUrl;
if (!ossUrl) return relativeUrl;
// 确保拼接时只有一个斜杠
const base = ossUrl.replace(/\/+$/, '');
const rel = relativeUrl.replace(/^\/+/, '');
return base + '/' + rel;
},
getFileExtension(filePath) {
const lastDot = filePath.lastIndexOf('.'); if (lastDot === -1) return '.jpg';
const ext = filePath.substring(lastDot).toLowerCase();
return ['.jpg','.jpeg','.png','.gif','.bmp','.webp'].includes(ext) ? ext : '.jpg';
},
async getCOSZhengshu() {
const res = await request({ url: '/dingdan/dsscpz', method: 'POST', data: { dingdan_id: this.data.xuanzhongChufa.dingdan_id || '', yongtu: 'chufa' } });
if (res.statusCode === 200 && res.data.code === 0 && res.data.data) return res.data.data;
throw new Error(res?.data?.msg || '获取上传凭证失败');
},
initCOSClient(tokenData) {
const COS = require('../../../../utils/cos-wx-sdk-v5.js');
const credentials = tokenData.credentials || tokenData;
const bucket = tokenData.bucket || 'julebu-1361527063';
const region = tokenData.region || 'ap-shanghai';
const cos = new COS({ SimpleUploadMethod: 'putObject', getAuthorization: (options, callback) => { callback({ TmpSecretId: credentials.tmpSecretId, TmpSecretKey: credentials.tmpSecretKey, SecurityToken: credentials.sessionToken || '', StartTime: tokenData.startTime || Math.floor(Date.now() / 1000), ExpiredTime: tokenData.expiredTime || (Math.floor(Date.now() / 1000) + 7200) }); } });
return { cos, bucket, region };
},
shangchuanDanZhangTupian(filePath, cosKey, cosClient, bucket, region) {
return new Promise(resolve => {
const timeoutId = setTimeout(() => resolve({ status: 'optimistic_success', key: cosKey }), 2000);
cosClient.uploadFile({ Bucket: bucket, Region: region, Key: cosKey, FilePath: filePath, onProgress: (info) => { if (Math.round(info.percent * 100) === 100) { clearTimeout(timeoutId); resolve({ status: 'optimistic_success', key: cosKey }); } } });
});
},
async piliangShangchuanShensuTupian() {
const total = this.data.shensuTupian.length;
if (total === 0) return [];
this.setData({ shangchuanZongshu: total, shangchuanJindu: 0, jinduWidth: '0%', isShangchuanzhong: true });
let yonghuid = '';
const uid = wx.getStorageSync('uid'); if (uid) yonghuid = String(uid).padStart(7, '0');
else if (app.globalData.userInfo?.yonghuid) yonghuid = String(app.globalData.userInfo.yonghuid).padStart(7, '0');
if (!yonghuid) { wx.showToast({ title: '请先登录', icon: 'none' }); this.setData({ isShangchuanzhong: false }); return []; }
const preGeneratedUrls = [];
for (let i = 0; i < total; i++) {
const timestamp = Date.now() + i, random = Math.floor(Math.random() * 10000);
preGeneratedUrls.push(`a_long/chfajltp/dssstp/${yonghuid}_${timestamp}_${random}${this.getFileExtension(this.data.shensuTupian[i])}`);
}
let cosClient, bucket, region;
try { const tokenData = await this.getCOSZhengshu(); const c = this.initCOSClient(tokenData); cosClient = c.cos; bucket = c.bucket; region = c.region; }
catch (e) { wx.showToast({ title: '上传初始化失败', icon: 'none' }); this.setData({ isShangchuanzhong: false }); return []; }
for (let i = 0; i < total; i++) {
await this.shangchuanDanZhangTupian(this.data.shensuTupian[i], preGeneratedUrls[i], cosClient, bucket, region);
const done = i + 1;
this.setData({ shangchuanJindu: done, jinduWidth: `${((done / total) * 100).toFixed(0)}%` });
}
this.setData({ isShangchuanzhong: false });
return preGeneratedUrls;
},
async submitShensu() {
if (!this.data.shensuLiyou.trim()) { wx.showToast({ title: '请输入申诉理由', icon: 'none' }); return; }
if (this.data.shensuTupian.length === 0) { wx.showModal({ title: '提示', content: '未上传任何图片,确定要提交申诉吗?', success: async (res) => { if (res.confirm) await this.tijiaoShensuData([]); } }); return; }
wx.showToast({ title: '开始上传图片...', icon: 'none' });
try {
const urls = await this.piliangShangchuanShensuTupian();
if (!urls || urls.length === 0) throw new Error('图片上传失败');
await this.tijiaoShensuData(urls);
} catch (e) { wx.showToast({ title: e.message || '提交失败', icon: 'none' }); }
},
async tijiaoShensuData(tupianUrls) {
wx.showToast({ title: '提交申诉中...', icon: 'none' });
try {
const res = await request({ url: '/yonghu/dscfss', method: 'POST', data: { chufa_id: this.data.xuanzhongChufa.id, shensu_liyou: this.data.shensuLiyou, shensu_tupian_urls: tupianUrls } });
if (res.statusCode === 200 && res.data.code === 0) {
const newList = this.data.chufaList.map(item => {
if (item.id === this.data.xuanzhongChufa.id) { return { ...item, sqzhuangtai: 3, ssliyou: this.data.shensuLiyou, display_status: '申诉中', status_class: 'zhuangtai-shensuzhong', full_shensu_tupian: tupianUrls.map(url => this.getFullImageUrl(url)) }; }
return item;
});
this.setData({ chufaList: newList, showShensuModal: false, showXiangqing: false });
wx.showToast({ title: '申诉提交成功,请等待处理', icon: 'success' });
} else throw new Error(res.data?.msg || '提交申诉失败');
} catch (e) { wx.showToast({ title: e.message || '提交失败', icon: 'none' }); }
}
}
});

View File

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

View File

@@ -0,0 +1,105 @@
<view class="container">
<scroll-view scroll-y class="scroll" bindscrolltolower="onReachBottom">
<view wx:if="{{ jiazhaozhong && chufaList.length === 0 }}" class="tip">加载中...</view>
<block wx:for="{{ chufaList }}" wx:key="id">
<view class="card" bindtap="chakanXiangqing" data-item="{{ item }}">
<view class="card-head">
<text class="time">{{ item.display_time }}</text>
<text class="tag {{ item.status_class }}">{{ item.display_status }}</text>
</view>
<text class="info">申请人:{{ item.qingqiuid || '--' }}</text>
<text class="info">理由:{{ item.cfliyou || '无' }}</text>
<text class="info" wx:if="{{ item.dingdan_id }}">订单:{{ item.dingdan_id }}</text>
</view>
</block>
<!-- ========== 底部:按钮 / 加载中 / 没有更多 ========== -->
<view wx:if="{{ haiyougengduo && !jiazhaigengduo }}" class="load-more-btn" bindtap="onReachBottom">
<text>点击获取更多</text>
</view>
<view wx:if="{{ jiazhaigengduo }}" class="tip">加载中...</view>
<view wx:if="{{ !haiyougengduo && chufaList.length > 0 }}" class="tip">—— 没有更多了 ——</view>
<view wx:if="{{ !jiazhaozhong && chufaList.length === 0 }}" class="empty">暂无积分处罚记录</view>
</scroll-view>
<!-- 详情弹窗(不动) -->
<view wx:if="{{ showXiangqing }}" class="modal-mask">
<view class="modal-panel">
<view class="modal-head">
<text class="modal-title">处罚详情</text>
<view class="modal-close" bindtap="guanbiXiangqing">✕</view>
</view>
<scroll-view scroll-y class="modal-body">
<view class="row"><text class="lbl">订单编号</text><text class="val">{{ xuanzhongChufa.dingdan_id || '--' }}</text></view>
<view class="row"><text class="lbl">申请人ID</text><text class="val">{{ xuanzhongChufa.qingqiuid || '--' }}</text></view>
<view class="row"><text class="lbl">时间</text><text class="val">{{ xuanzhongChufa.display_time }}</text></view>
<view class="row"><text class="lbl">状态</text><text class="val"><text class="tag {{ xuanzhongChufa.status_class }}">{{ xuanzhongChufa.display_status }}</text></text></view>
<view class="row"><text class="lbl">处罚理由</text><text class="val">{{ xuanzhongChufa.cfliyou || '无' }}</text></view>
<view wx:if="{{ xuanzhongChufa.full_zhengju_tupian.length }}" class="section">
<text class="sec-title">证据图片</text>
<view class="img-grid">
<block wx:for="{{ xuanzhongChufa.full_zhengju_tupian }}" wx:key="index">
<view class="img-box" bindtap="yulanTupian" data-url="{{ item }}" data-urls="{{ xuanzhongChufa.full_zhengju_tupian }}">
<image class="img" src="{{ item }}" mode="aspectFill" />
</view>
</block>
</view>
</view>
<view wx:if="{{ xuanzhongChufa.ssliyou || xuanzhongChufa.full_shensu_tupian.length }}" class="section">
<text class="sec-title">我的申诉</text>
<view wx:if="{{ xuanzhongChufa.ssliyou }}" class="row"><text class="lbl">理由</text><text class="val">{{ xuanzhongChufa.ssliyou }}</text></view>
<view wx:if="{{ xuanzhongChufa.full_shensu_tupian.length }}" class="img-grid">
<block wx:for="{{ xuanzhongChufa.full_shensu_tupian }}" wx:key="index">
<view class="img-box" bindtap="yulanTupian" data-url="{{ item }}" data-urls="{{ xuanzhongChufa.full_shensu_tupian }}">
<image class="img" src="{{ item }}" mode="aspectFill" />
</view>
</block>
</view>
</view>
</scroll-view>
<view class="modal-foot">
<view wx:if="{{ xuanzhongChufa.sqzhuangtai === 0 }}" class="btn primary" bindtap="openShensuModal">申诉</view>
<view class="btn default" bindtap="guanbiXiangqing">关闭</view>
</view>
</view>
</view>
<!-- 申诉弹窗(不动) -->
<view wx:if="{{ showShensuModal }}" class="modal-mask">
<view class="modal-panel">
<view class="modal-head">
<text class="modal-title">提交申诉</text>
<view class="modal-close" bindtap="closeShensuModal">✕</view>
</view>
<scroll-view scroll-y class="modal-body">
<view class="form-group">
<text class="sec-title">申诉理由</text>
<textarea class="textarea" placeholder="请输入申诉理由最多500字" value="{{ shensuLiyou }}" bindinput="onShensuLiyouInput" maxlength="500" />
<text class="word-count">{{ shensuLiyou.length }}/500</text>
</view>
<view class="form-group">
<text class="sec-title">上传凭证最多9张</text>
<view class="img-grid">
<block wx:for="{{ shensuTupian }}" wx:key="index">
<view class="img-box">
<image class="img" src="{{ item }}" mode="aspectFill" bindtap="yulanShensuTupian" data-index="{{ index }}" />
<view class="img-del" catchtap="deleteShensuTupian" data-index="{{ index }}">✕</view>
</view>
</block>
<view wx:if="{{ shensuTupian.length < 9 }}" class="img-add" bindtap="chooseShensuTupian">+</view>
</view>
<view wx:if="{{ shangchuanZongshu > 0 }}" class="progress-box">
<view class="progress-bar"><view class="progress-inner" style="width: {{ jinduWidth }}"></view></view>
<text class="progress-text">{{ shangchuanJindu }}/{{ shangchuanZongshu }}</text>
</view>
</view>
</scroll-view>
<view class="modal-foot">
<view class="btn default" bindtap="closeShensuModal">取消</view>
<view class="btn primary" bindtap="submitShensu">提交申诉</view>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,86 @@
/* 页面与容器 */
page { background: #f5f5f5; }
/* 🔥 组件自身必须占满宽度,且 padding 左右严格相等 */
.container { width: 100%; box-sizing: border-box; display: flex; flex-direction: column; }
/* 🔥 滚动区 padding 左右必须相等,且容器用 box-sizing */
.scroll { width: 100%; box-sizing: border-box; padding: 20rpx 24rpx; }
/* 提示文字 */
.tip { text-align: center; color: #999; font-size: 24rpx; padding: 30rpx 0; }
.empty { display: flex; flex-direction: column; align-items: center; padding-top: 200rpx; color: #999; font-size: 28rpx; }
/* 🔥 卡片 width:100% + box-sizing 使左右边距完全由父容器决定 */
.card { width: 100%; box-sizing: border-box; background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; }
.card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
.time { font-size: 24rpx; color: #999; }
.tag { font-size: 20rpx; padding: 2rpx 14rpx; border-radius: 16rpx; display: inline-block; }
.zhuangtai-daichuli { background: #fff3e0; color: #e65100; }
.zhuangtai-yichufa { background: #e8f5e9; color: #2e7d32; }
.zhuangtai-yibohui { background: #f3e5f5; color: #7b1fa2; }
.zhuangtai-shensuzhong { background: #e3f2fd; color: #1565c0; }
.zhuangtai-weizhi { background: #f0f0f0; color: #888; }
.info { display: block; font-size: 26rpx; color: #333; margin-top: 8rpx; }
/* 弹窗遮罩 */
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 999; display: flex; align-items: flex-end; justify-content: center; }
/* 🔥 弹窗面板 width:100% + box-sizing */
.modal-panel { width: 100%; box-sizing: border-box; max-height: 85vh; background: #fff; border-radius: 24rpx 24rpx 0 0; display: flex; flex-direction: column; overflow: hidden; }
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 30rpx 16rpx; border-bottom: 1px solid #eee; }
.modal-title { font-size: 32rpx; font-weight: 600; }
.modal-close { width: 44rpx; height: 44rpx; border-radius: 50%; background: #f5f5f5; display: flex; align-items: center; justify-content: center; font-size: 26rpx; color: #888; }
/* 🔥 内容区左右 padding 严格相等 */
.modal-body { width: 100%; box-sizing: border-box; padding: 20rpx 30rpx; overflow-y: auto; }
.row { display: flex; margin-bottom: 16rpx; }
.lbl { width: 150rpx; font-size: 26rpx; color: #888; flex-shrink: 0; }
.val { flex: 1; font-size: 26rpx; color: #333; word-break: break-all; }
.section { margin: 20rpx 0; }
.sec-title { font-size: 26rpx; color: #888; margin-bottom: 12rpx; display: block; }
/* 🔥 图片网格gap 均等,无任何额外水平偏移 */
.img-grid { width: 100%; box-sizing: border-box; display: flex; flex-wrap: wrap; gap: 12rpx; }
.img { width: calc((100% - 24rpx) / 3); height: 0; padding-bottom: calc((100% - 24rpx) / 3); border-radius: 10rpx; background: #f0f0f0; display: block; position: relative; }
.img-box { position: relative; width: calc((100% - 24rpx) / 3); height: 0; padding-bottom: calc((100% - 24rpx) / 3); }
.img-box .img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; padding-bottom: 0; }
.img-del { position: absolute; top: -6rpx; right: -6rpx; width: 34rpx; height: 34rpx; background: #e53935; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 18rpx; z-index: 2; }
.img-add { width: calc((100% - 24rpx) / 3); height: 0; padding-bottom: calc((100% - 24rpx) / 3); border: 2rpx dashed #ccc; border-radius: 10rpx; display: flex; align-items: center; justify-content: center; font-size: 52rpx; color: #aaa; box-sizing: border-box; position: relative; }
/* 表单 */
.form-group { margin-bottom: 24rpx; }
.textarea { width: 100%; box-sizing: border-box; height: 180rpx; background: #f9f9f9; border-radius: 10rpx; padding: 16rpx; font-size: 26rpx; border: 1px solid #eee; }
.word-count { text-align: right; font-size: 22rpx; color: #bbb; margin-top: 6rpx; }
.progress-box { margin-top: 12rpx; }
.progress-bar { height: 10rpx; background: #eee; border-radius: 5rpx; }
.progress-inner { height: 100%; background: #2e7d32; border-radius: 5rpx; }
.progress-text { font-size: 22rpx; color: #2e7d32; text-align: right; margin-top: 4rpx; }
/* 🔥 底部按钮区左右 padding 严格均等 */
.modal-foot { width: 100%; box-sizing: border-box; display: flex; padding: 16rpx 30rpx 30rpx; gap: 20rpx; border-top: 1px solid #eee; }
.btn { flex: 1; text-align: center; padding: 22rpx 0; border-radius: 16rpx; font-size: 28rpx; font-weight: 600; }
.primary { background: #2e7d32; color: #fff; }
.default { background: #f0f0f0; color: #555; }
.btn:active { opacity: 0.8; }
/* ===== 获取更多按钮 ===== */
.load-more-btn {
width: 80%;
margin: 20rpx auto 30rpx;
padding: 22rpx 0;
text-align: center;
background: #ffffff;
border: 2rpx solid #e0e0e0;
border-radius: 20rpx;
color: #555;
font-size: 26rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.02);
transition: all 0.15s;
}
.load-more-btn:active {
background: #f5f5f5;
border-color: #2e7d32;
color: #2e7d32;
}

288
pages/czjilu/czjilu.js Normal file
View File

@@ -0,0 +1,288 @@
// 管事会员记录页面逻辑
import request from '../../utils/request.js';
// 每页数据条数常量 - 统一管理,方便修改
const MEIYE_TIAOSHU = 50;
Page({
data: {
// 从全局变量获取的数据
yaoqingzongshu: 0, // 邀请打手总数
fenyongzonge: '0.00', // 分成总额
// 从接口获取的数据
chongzhiDashouShuliang: 0, // 充值打手数量
dashouList: [], // 打手列表数据
zongTiaoshu: 0, // 总条数
// 分页相关
dangqianYe: 1, // 当前页
meiyouGengduo: false, // 没有更多数据
jiazaiZhong: false, // 加载中状态
jiazaiGengduo: false, // 加载更多状态
// UI状态
cuowuTishi: '', // 错误提示
shuaxinKezhi: false, // 刷新按钮是否可点击
shuaxinCd: 2000, // 刷新冷却时间(毫秒)
// 常量(用于模板)
meiyeTiaoshu: MEIYE_TIAOSHU // 每页条数
},
onLoad(options) {
// 从全局变量获取管事数据
this.huoquQuanjubianliang();
// 加载第一页数据
this.jiazaiShuju(true);
},
onShow() {
// 页面显示时检查全局变量是否有更新
this.registerNotificationComponent();
this.huoquQuanjubianliang();
},
// 🆕 新增:注册通知组件
registerNotificationComponent() {
const app = getApp();
const notificationComp = this.selectComponent('#global-notification');
if (notificationComp && notificationComp.showNotification) {
console.log('🏪 商城页面注册通知组件');
app.globalData.globalNotification = {
show: (data) => notificationComp.showNotification(data),
hide: () => notificationComp.hideNotification()
};
}
},
// 获取全局变量数据
huoquQuanjubianliang() {
const app = getApp();
const guanshiData = app.globalData.guanshi;
if (guanshiData) {
this.setData({
yaoqingzongshu: guanshiData.yaoqingzongshu || 0,
fenyongzonge: guanshiData.fenyongzonge || '0.00'
});
}
},
// 加载数据核心方法
async jiazaiShuju(chushihua = false) {
// 防止重复请求
if (this.data.jiazaiZhong) return;
// 如果是初始化加载,重置分页
if (chushihua) {
this.setData({
dangqianYe: 1,
meiyouGengduo: false,
dashouList: [],
zongTiaoshu: 0,
chongzhiDashouShuliang: 0
});
} else {
// 上拉加载更多时,检查是否还有更多数据
if (this.data.meiyouGengduo) return;
// 如果有数据但不足一页,说明后端已经没数据了
if (this.data.dashouList.length > 0 && this.data.dashouList.length < MEIYE_TIAOSHU) {
this.setData({ meiyouGengduo: true });
return;
}
// 如果数据量已经达到总条数,则不请求
if (this.data.dashouList.length >= this.data.zongTiaoshu && this.data.zongTiaoshu > 0) {
this.setData({ meiyouGengduo: true });
return;
}
}
// 设置加载状态
this.setData({
jiazaiZhong: chushihua ? true : false,
jiazaiGengduo: !chushihua ? true : false,
cuowuTishi: ''
});
try {
const { dangqianYe } = this.data;
// 严格按照您的request规范调用
const res = await request({
url: '/shangpin/fenhonghq',
method: 'POST',
data: {
page: dangqianYe,
page_size: MEIYE_TIAOSHU // 使用统一常量
}
});
// 处理返回数据
if (res.data.code === 200) {
const data = res.data.data || {};
const newList = data.list || [];
const total = data.total || 0;
// 处理头像URL拼接
const processedList = this.chuliTouxiangURL(newList);
// 计算是否有更多数据
const hasMoreData = processedList.length === MEIYE_TIAOSHU;
// 更新数据
this.setData({
dashouList: chushihua ? processedList : [...this.data.dashouList, ...processedList],
zongTiaoshu: total,
chongzhiDashouShuliang: data.chongzhi_dashou_shuliang || 0,
meiyouGengduo: !hasMoreData,
dangqianYe: chushihua ? 2 : this.data.dangqianYe + 1
});
// 如果数据为空,显示空状态
if (chushihua && processedList.length === 0) {
wx.showToast({
title: '暂无数据',
icon: 'none',
duration: 1500
});
}
// 如果加载了数据,给个提示
if (processedList.length > 0) {
if (chushihua) {
wx.showToast({
title: `已加载${processedList.length}条记录`,
icon: 'success',
duration: 1000
});
}
}
} else {
this.setData({
cuowuTishi: res.data.msg || '数据加载失败'
});
wx.showToast({
title: res.data.msg || '加载失败',
icon: 'none',
duration: 2000
});
}
} catch (error) {
console.error('加载数据失败:', error);
this.setData({
cuowuTishi: '网络错误,请检查网络连接'
});
wx.showToast({
title: '网络错误',
icon: 'none',
duration: 2000
});
} finally {
this.setData({
jiazaiZhong: false,
jiazaiGengduo: false
});
}
},
// 处理头像URL拼接
chuliTouxiangURL(list) {
const app = getApp();
const ossUrl = app.globalData.ossImageUrl || '';
return list.map(item => {
// 确保有头像URL如果没有则使用默认头像
let touxiangUrl = item.avatar || '';
if (touxiangUrl && !touxiangUrl.startsWith('http')) {
touxiangUrl = ossUrl + touxiangUrl;
}
// 格式化时间
let shijian = item.create_time || '';
if (shijian) {
// 如果是时间戳,转换为日期格式
if (/^\d+$/.test(shijian)) {
const date = new Date(parseInt(shijian));
shijian = `${date.getMonth() + 1}${date.getDate()}${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
} else {
// 如果是字符串,提取日期和时间
try {
const dateObj = new Date(shijian);
shijian = `${dateObj.getMonth() + 1}${dateObj.getDate()}${dateObj.getHours().toString().padStart(2, '0')}:${dateObj.getMinutes().toString().padStart(2, '0')}`;
} catch (e) {
shijian = shijian.substring(0, 10);
}
}
}
return {
...item,
touxiangUrl: touxiangUrl || '/images/default-avatar.png',
shijian: shijian,
fenhong: parseFloat(item.fenhong || 0).toFixed(2)
};
});
},
// 刷新数据(带防重复点击)
shuaxinShuju() {
// 检查是否在冷却中
if (this.data.shuaxinKezhi) {
wx.showToast({
title: `请等待${this.data.shuaxinCd/1000}`,
icon: 'none',
duration: 1000
});
return;
}
// 设置冷却状态
this.setData({ shuaxinKezhi: true });
// 显示刷新动画
this.setData({
jiazaiZhong: true
});
// 刷新数据
this.jiazaiShuju(true).finally(() => {
// 2秒后恢复按钮可点击
setTimeout(() => {
this.setData({ shuaxinKezhi: false });
}, this.data.shuaxinCd);
});
},
// 上拉加载更多
shanglaJiazai() {
// 没有更多数据时不再请求
if (this.data.meiyouGengduo) {
wx.showToast({
title: '已经到底了',
icon: 'none',
duration: 1000
});
return;
}
// 如果正在加载,不再请求
if (this.data.jiazaiGengduo || this.data.jiazaiZhong) {
return;
}
this.jiazaiShuju(false);
},
// 重试加载
chongshiJiazai() {
this.setData({
cuowuTishi: '',
meiyouGengduo: false
});
this.jiazaiShuju(true);
}
});

11
pages/czjilu/czjilu.json Normal file
View File

@@ -0,0 +1,11 @@
{
"usingComponents": {
"global-notification": "/components/global-notification/global-notification"
},
"navigationBarTitleText": "会员记录",
"navigationBarBackgroundColor": "#0a0a0f",
"navigationBarTextStyle": "white",
"backgroundColor": "#0a0a0f",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false
}

119
pages/czjilu/czjilu.wxml Normal file
View File

@@ -0,0 +1,119 @@
<!-- 管事会员记录页面 -->
<view class="page-container">
<!-- 固定顶部区域 - 不动 -->
<view class="fixed-top">
<!-- 顶部统计区域 -->
<view class="tongji-quyu">
<!-- 分红总额 -->
<view class="fenyong-zonge">
<view class="zonge-label">分红总额</view>
<view class="zonge-value">¥{{fenyongzonge}}</view>
<view class="zonge-tip">累计收益</view>
</view>
<!-- 底部统计信息 -->
<view class="tongji-bottom">
<view class="tongji-item">
<view class="tongji-number">{{yaoqingzongshu}}</view>
<view class="tongji-label">邀请打手</view>
</view>
<view class="tongji-divider"></view>
<view class="tongji-item">
<view class="tongji-number">{{chongzhiDashouShuliang}}</view>
<view class="tongji-label">充值打手</view>
</view>
</view>
</view>
<!-- 刷新按钮 -->
<view class="shuaxin-anniu" bindtap="shuaxinShuju">
<text class="shuaxin-icon {{shuaxinKezhi ? 'shuaxin-disabled' : ''}}">↻</text>
</view>
</view>
<!-- 滚动区域 - 只包裹列表部分 -->
<scroll-view
class="scroll-area"
scroll-y
enable-back-to-top
bindscrolltolower="shanglaJiazai">
<!-- 全局加载状态 -->
<view wx:if="{{jiazaiZhong && dashouList.length === 0}}" class="jiazai-quanju">
<view class="jiazai-spinner"></view>
<view class="jiazai-text">加载中...</view>
</view>
<!-- 空数据提示 -->
<view wx:if="{{!jiazaiZhong && dashouList.length === 0}}" class="kong-shuju">
<view class="kong-icon">🚀</view>
<view class="kong-text">暂无会员记录</view>
<view class="kong-tip">快去邀请打手吧</view>
<view class="kong-btn" bindtap="shuaxinShuju">点击刷新</view>
</view>
<!-- 会员记录卡片列表 -->
<block wx:if="{{dashouList.length > 0}}">
<!-- 列表标题 -->
<view class="list-title">
<text class="title-text">充值记录</text>
<text class="title-count">({{dashouList.length}}/{{zongTiaoshu}})</text>
</view>
<!-- 卡片列表 -->
<view class="card-list">
<block wx:for="{{dashouList}}" wx:key="index">
<view class="dashou-card">
<!-- 圆形头像区域 -->
<view class="touxiang-quyu">
<image
class="dashou-touxiang"
src="{{item.touxiangUrl}}"
mode="aspectFill">
</image>
<!-- 头像边框效果 -->
<view class="touxiang-border"></view>
</view>
<!-- 信息区域 -->
<view class="xinxi-quyu">
<view class="nicheng">{{item.nicheng}}</view>
<view class="dashou-id">ID: {{item.yonghuid}}</view>
</view>
<!-- 分红金额区域 -->
<view class="fenhong-quyu">
<view class="fenhong-value">
<text class="fenhong-jiahao">+</text>
<text class="fenhong-shuzi">¥{{item.fenhong}}</text>
</view>
<view class="shijian">{{item.shijian}}</view>
</view>
</view>
</block>
</view>
<!-- 加载更多提示 -->
<view wx:if="{{jiazaiGengduo}}" class="jiazai-gengduo">
<view class="gengduo-spinner"></view>
<text class="gengduo-text">加载更多中...</text>
</view>
<!-- 没有更多数据 -->
<view wx:if="{{meiyouGengduo && dashouList.length > 0}}" class="meiyou-gengduo">
<view class="meiyou-icon">✓</view>
<text class="meiyou-text">已经到底了,共{{dashouList.length}}条记录</text>
</view>
</block>
</scroll-view>
<!-- 错误提示 -->
<view wx:if="{{cuowuTishi}}" class="cuowu-quyu">
<view class="cuowu-icon">⚠️</view>
<view class="cuowu-text">{{cuowuTishi}}</view>
<view class="cuowu-btn" bindtap="chongshiJiazai">重试加载</view>
</view>
</view>
<global-notification id="global-notification" />

506
pages/czjilu/czjilu.wxss Normal file
View File

@@ -0,0 +1,506 @@
/* 管事会员记录页面样式 - 赛博风格 */
/* 页面容器 */
page {
height: 100%;
background: #0a0a0f;
}
.page-container {
height: 100%;
background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 50%, #16213e 100%);
position: relative;
}
/* 固定顶部区域 */
.fixed-top {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%);
padding-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3);
}
/* 顶部统计区域 - 赛博卡片设计 */
.tongji-quyu {
position: relative;
margin: 20rpx 30rpx 0;
padding: 40rpx 30rpx;
background: rgba(20, 20, 35, 0.9);
border-radius: 24rpx;
border: 1px solid rgba(100, 255, 255, 0.1);
box-shadow:
0 10rpx 30rpx rgba(0, 0, 0, 0.4),
0 0 20rpx rgba(100, 255, 255, 0.05) inset,
0 0 0 1px rgba(100, 255, 255, 0.1);
backdrop-filter: blur(10px);
overflow: hidden;
}
.tongji-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(45deg,
rgba(100, 255, 255, 0.03) 0%,
rgba(255, 100, 255, 0.03) 100%);
z-index: 0;
}
/* 分红总额样式 - 赛博霓虹效果 */
.fenyong-zonge {
position: relative;
text-align: center;
padding: 20rpx 0 30rpx;
margin-bottom: 25rpx;
border-bottom: 1px solid rgba(100, 255, 255, 0.1);
z-index: 1;
}
.zonge-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 15rpx;
text-transform: uppercase;
letter-spacing: 2rpx;
}
.zonge-value {
font-size: 60rpx;
font-weight: 800;
color: #00ffea;
margin-bottom: 8rpx;
text-shadow:
0 0 10rpx rgba(0, 255, 234, 0.5),
0 0 20rpx rgba(0, 255, 234, 0.3);
font-family: 'Arial', sans-serif;
}
.zonge-tip {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
}
/* 底部统计信息 */
.tongji-bottom {
display: flex;
justify-content: space-around;
align-items: center;
z-index: 1;
position: relative;
}
.tongji-item {
text-align: center;
flex: 1;
}
.tongji-number {
font-size: 40rpx;
font-weight: 700;
color: #ffffff;
margin-bottom: 6rpx;
text-shadow: 0 0 10rpx rgba(255, 255, 255, 0.3);
}
.tongji-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
.tongji-divider {
width: 1px;
height: 45rpx;
background: linear-gradient(to bottom,
transparent 0%,
rgba(100, 255, 255, 0.3) 50%,
transparent 100%);
}
/* 刷新按钮 - 固定在右上角 */
.shuaxin-anniu {
position: absolute;
top: 60rpx;
right: 60rpx;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
width: 70rpx;
height: 70rpx;
background: rgba(20, 20, 35, 0.95);
border-radius: 50%;
border: 1px solid rgba(100, 255, 255, 0.2);
box-shadow:
0 8rpx 20rpx rgba(0, 0, 0, 0.4),
0 0 20rpx rgba(100, 255, 255, 0.1);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.shuaxin-anniu:active {
transform: scale(0.95);
background: rgba(100, 255, 255, 0.1);
box-shadow:
0 4rpx 10rpx rgba(0, 0, 0, 0.3),
0 0 30rpx rgba(100, 255, 255, 0.2);
}
.shuaxin-icon {
font-size: 32rpx;
color: #00ffea;
transition: all 0.3s ease;
}
.shuaxin-disabled {
color: rgba(255, 255, 255, 0.3);
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 滚动区域 - 只包裹列表部分 */
.scroll-area {
position: fixed;
top: 340rpx; /* 根据固定区域高度调整 */
left: 0;
right: 0;
bottom: 0;
z-index: 50;
}
/* 全局加载状态 */
.jiazai-quanju {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.jiazai-spinner {
width: 80rpx;
height: 80rpx;
border: 4rpx solid rgba(100, 255, 255, 0.1);
border-top: 4rpx solid #00ffea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 30rpx;
box-shadow: 0 0 20rpx rgba(0, 255, 234, 0.3);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.jiazai-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.7);
}
/* 空数据提示 */
.kong-shuju {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 30rpx;
background: rgba(20, 20, 35, 0.5);
border-radius: 24rpx;
border: 1px solid rgba(100, 255, 255, 0.1);
margin: 40rpx 30rpx 0;
}
.kong-icon {
font-size: 100rpx;
margin-bottom: 30rpx;
color: rgba(255, 255, 255, 0.2);
}
.kong-text {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 10rpx;
font-weight: 500;
}
.kong-tip {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 50rpx;
}
.kong-btn {
padding: 20rpx 50rpx;
background: linear-gradient(90deg, #00ffea 0%, #0088ff 100%);
color: #000;
border-radius: 50rpx;
font-size: 28rpx;
font-weight: 600;
box-shadow: 0 8rpx 20rpx rgba(0, 255, 234, 0.3);
}
.kong-btn:active {
transform: scale(0.95);
box-shadow: 0 4rpx 10rpx rgba(0, 255, 234, 0.2);
}
/* 列表标题 */
.list-title {
margin: 0 30rpx 20rpx;
display: flex;
align-items: baseline;
}
.title-text {
font-size: 32rpx;
color: #ffffff;
font-weight: 600;
margin-right: 10rpx;
text-shadow: 0 0 10rpx rgba(255, 255, 255, 0.2);
}
.title-count {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
}
/* 卡片列表 */
.card-list {
margin: 0 30rpx;
}
/* 打手卡片 - 赛博风格设计 */
.dashou-card {
position: relative;
display: flex;
align-items: center;
padding: 30rpx;
margin-bottom: 20rpx;
background: rgba(20, 20, 35, 0.7);
border-radius: 20rpx;
border: 1px solid rgba(100, 255, 255, 0.1);
box-shadow:
0 8rpx 24rpx rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(100, 255, 255, 0.05);
transition: all 0.3s ease;
}
.dashou-card:active {
transform: translateY(-2rpx);
background: rgba(30, 30, 50, 0.8);
box-shadow:
0 12rpx 30rpx rgba(0, 0, 0, 0.4),
0 0 20rpx rgba(100, 255, 255, 0.1);
}
/* 头像区域 - 纯圆形 */
.touxiang-quyu {
position: relative;
margin-right: 30rpx;
flex-shrink: 0;
}
.dashou-touxiang {
width: 100rpx;
height: 100rpx;
border-radius: 50%; /* 确保是纯圆形 */
z-index: 2;
position: relative;
}
.touxiang-border {
position: absolute;
top: -4rpx;
left: -4rpx;
width: 108rpx;
height: 108rpx;
border-radius: 50%;
background: linear-gradient(45deg, #00ffea, #ff00ff);
z-index: 1;
opacity: 0.3;
animation: borderGlow 2s linear infinite;
}
@keyframes borderGlow {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.6; }
}
/* 信息区域 */
.xinxi-quyu {
flex: 1;
min-width: 0; /* 防止内容撑开 */
}
.nicheng {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-shadow: 0 0 10rpx rgba(255, 255, 255, 0.2);
}
.dashou-id {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 1rpx;
}
/* 分红金额区域 */
.fenhong-quyu {
text-align: right;
flex-shrink: 0;
min-width: 180rpx;
}
.fenhong-value {
margin-bottom: 8rpx;
}
.fenhong-jiahao {
font-size: 26rpx;
color: #00ff00;
margin-right: 4rpx;
}
.fenhong-shuzi {
font-size: 32rpx;
font-weight: 700;
color: #00ff00;
text-shadow: 0 0 10rpx rgba(0, 255, 0, 0.5);
}
.shijian {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
}
/* 加载更多提示 */
.jiazai-gengduo {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0 60rpx;
margin: 0 30rpx;
}
.gengduo-spinner {
width: 40rpx;
height: 40rpx;
border: 3rpx solid rgba(100, 255, 255, 0.1);
border-top: 3rpx solid #00ffea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
.gengduo-text {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.6);
}
/* 没有更多数据 */
.meiyou-gengduo {
display: flex;
flex-direction: column;
align-items: center;
padding: 50rpx 0 80rpx;
margin: 0 30rpx;
color: rgba(255, 255, 255, 0.4);
}
.meiyou-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: rgba(0, 255, 234, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #00ffea;
margin-bottom: 20rpx;
border: 1px solid rgba(0, 255, 234, 0.2);
}
.meiyou-text {
font-size: 26rpx;
text-align: center;
}
/* 错误提示 */
.cuowu-quyu {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 30rpx;
background: rgba(40, 20, 20, 0.8);
border-radius: 24rpx;
border: 1px solid rgba(255, 100, 100, 0.2);
margin: 40rpx 30rpx 0;
}
.cuowu-icon {
font-size: 80rpx;
color: #ff5555;
margin-bottom: 30rpx;
text-shadow: 0 0 20rpx rgba(255, 85, 85, 0.5);
}
.cuowu-text {
font-size: 30rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 40rpx;
text-align: center;
line-height: 1.5;
}
.cuowu-btn {
padding: 20rpx 50rpx;
background: linear-gradient(90deg, #ff5555 0%, #ff8800 100%);
color: #fff;
border-radius: 50rpx;
font-size: 28rpx;
font-weight: 600;
box-shadow: 0 8rpx 20rpx rgba(255, 85, 85, 0.3);
}
.cuowu-btn:active {
transform: scale(0.95);
box-shadow: 0 4rpx 10rpx rgba(255, 85, 85, 0.2);
}
/* 调整滚动区域的顶部距离,增加间隔 */
.scroll-area {
position: fixed;
top: 400rpx; /* 从 340rpx 增加到 360rpx增加20rpx的间隔 */
left: 0;
right: 0;
bottom: 0;
z-index: 50;
}
/* 如果觉得还不够,可以再增加列表标题的上边距 */
.list-title {
margin: 10rpx 30rpx 20rpx; /* 从 0 30rpx 20rpx 改为 10rpx 30rpx 20rpx */
display: flex;
align-items: baseline;
}

View File

@@ -0,0 +1,975 @@
// pages/dashou-chongzhi/index.js
import request from '../../utils/request';
// 引入弹窗服务(路径根据实际调整)
import PopupService from '../../services/popupService.js';
Page({
data: {
// ========== 全局数据 ==========
clubmber: [], // 会员列表(注意:全局变量中是 clumber这里保持原样
yajin: 0, // 押金
jifen: 0, // 积分(注意:全局变量中是 jinfen
jifenProgress: 0, // 积分进度条角度
// ========== 会员商品列表 ==========
huiyuanList: [], // 从后端获取的会员列表
currentHuiyuanIndex: 0, // 当前选中的会员索引
currentHuiyuan: {}, // 当前选中的会员详情
// ========== 押金充值相关 ==========
showYajinModal: false, // 显示押金弹窗
yajinAmount: '100', // 押金金额
// ========== 支付相关 ==========
payLoading: false, // 支付加载状态
loadingText: '支付中...',
// ========== 订单ID记录 ==========
currentDingdanid: '', // 当前操作的订单ID
// ========== 计算高度 ==========
huiyuanListHeight: 120,
// ========== 会员详情规则 ==========
detailRules: [
'会员有效期为30天从购买当天开始计算',
'会员期间享受优先接单特权',
'可享受专属客服服务',
'会员到期前3天会有提醒',
'支持随时续费,续费天数叠加'
],
// ========== 新增:跳转参数处理 ==========
// 用于接收从其他页面跳转过来的参数,决定是否自动滚动
jumpParams: {},
// ========== 新增:支付方式选择 ==========
showPayMethodModal: false, // 支付方式选择弹窗
currentBuyType: null, // 当前购买类型1会员 2押金 3积分
currentHuiyuanId: null, // 当前选中的会员ID用于会员购买
currentYajinAmount: null, // 当前押金金额(用于押金购买)
// ========== 新增:余额抵扣身份选择 ==========
showBalanceModal: false, // 余额抵扣身份选择弹窗
balanceOptions: [], // 后端返回的身份列表
selectedBalanceId: null, // 选中的身份ID
// ========== 新增:余额抵扣确认弹窗 ==========
showConfirmModal: false, // 余额抵扣确认弹窗
selectedBalanceInfo: null, // 选中的身份详情(用于确认弹窗)
// ========== 新增:会员详情弹窗 ==========
showMemberModal: false,
currentMemberDetail: null,
},
// ========== 生命周期函数 ==========
onLoad(options) {
//console.log('页面加载,跳转参数:', options);
// ✅ 新增:保存跳转参数
this.setData({
jumpParams: options || {}
});
this.initPage();
},
// pages/tijiao/tijiao.js 中添加 onHide 方法(如果已有则合并内容)
onHide() {
const popupComp = this.selectComponent('#popupNotice');
if (popupComp && popupComp.cleanup) {
popupComp.cleanup();
}
},
onShow() {
// 每次显示页面都刷新数据
this.registerNotificationComponent();
this.loadFromGlobalData();
this.checkAndRefreshData();
// 调用统一弹窗组件检查并展示公告
PopupService.checkAndShow(this, 'dashouchongzhi');
},
// 注册通知组件(原有,保持不变)
registerNotificationComponent() {
const app = getApp();
const notificationComp = this.selectComponent('#global-notification');
if (notificationComp && notificationComp.showNotification) {
app.globalData.globalNotification = {
show: (data) => notificationComp.showNotification(data),
hide: () => notificationComp.hideNotification()
};
}
},
onReady() {
// ✅ 新增:页面渲染完成后处理自动滚动
this.handleAutoScroll();
},
// ========== 初始化页面 ==========
async initPage() {
// 从全局数据加载
this.loadFromGlobalData();
// 获取会员商品列表
await this.fetchHuiyuanList();
// 计算会员列表高度
this.calculateHuiyuanHeight();
},
// ========== 从全局变量加载数据 ==========
loadFromGlobalData() {
const app = getApp();
const globalData = app.globalData || {};
// ✅ 重要:保持字段对应关系
// 全局变量中是 clumber页面中是 clubmber
// 全局变量中是 jinfen页面中是 jifen
this.setData({
clubmber: globalData.clumber || [],
yajin: globalData.yajin || 0,
jifen: globalData.jinfen || 0 // ✅ 注意:这里从 jinfen 读取
});
// 计算积分进度条
this.calculateJifenProgress();
},
// ========== 计算积分进度条角度 ==========
calculateJifenProgress() {
const jifen = this.data.jifen || 0;
const progress = (jifen / 10) * 360; // 满分10分
this.setData({
jifenProgress: progress
});
},
// ========== 计算会员列表高度 ==========
calculateHuiyuanHeight() {
const clubmber = this.data.clubmber || [];
const itemHeight = 100; // 每个会员标签高度
const maxHeight = 300; // 最大高度3个会员
let height = clubmber.length * itemHeight;
height = Math.min(height, maxHeight);
this.setData({
huiyuanListHeight: height
});
},
// ========== 获取会员商品列表 ==========
async fetchHuiyuanList() {
try {
const res = await request({
url: '/shangpin/dshyhq',
method: 'POST',
data: {}
});
if (res.data.code === 200) {
const huiyuanList = res.data.data || [];
// 处理数据,标记已购买的会员
const processedList = this.processHuiyuanList(huiyuanList);
this.setData({
huiyuanList: processedList,
currentHuiyuan: processedList[0] || {}
});
} else {
wx.showToast({
title: res.data.message || '获取会员列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取会员列表失败:', error);
wx.showToast({
title: '网络错误,请重试',
icon: 'none'
});
}
},
// ========== 处理会员列表,标记已购买的会员 ==========
processHuiyuanList(huiyuanList) {
const clubmber = this.data.clubmber || [];
return huiyuanList.map(item => {
// 检查是否已购买注意会员ID字段是 id
const boughtItem = clubmber.find(c => c.huiyuanid === item.id);
return {
...item,
isBought: !!boughtItem,
buyInfo: boughtItem || null
};
});
},
// ========== Swiper切换事件 ==========
onSwiperChange(e) {
const current = e.detail.current;
const huiyuanList = this.data.huiyuanList || [];
this.setData({
currentHuiyuanIndex: current,
currentHuiyuan: huiyuanList[current] || {}
});
},
// ========== 手动选择会员 ==========
selectHuiyuan(e) {
const index = e.currentTarget.dataset.index;
const huiyuanList = this.data.huiyuanList || [];
this.setData({
currentHuiyuanIndex: index,
currentHuiyuan: huiyuanList[index] || {}
});
},
// ========== 格式化到期时间 ==========
formatDaoqiTime(daoqi) {
if (!daoqi) return '';
try {
// 如果是标准日期格式,格式化为 YYYY-MM-DD
if (daoqi.includes(' ')) {
const dateStr = daoqi.split(' ')[0];
const dateParts = dateStr.split('-');
if (dateParts.length === 3) {
return `${dateParts[0]}-${dateParts[1]}-${dateParts[2]}`;
}
}
return daoqi;
} catch (error) {
console.error('格式化到期时间错误:', error);
return daoqi;
}
},
// ========== 计算剩余时间 ==========
formatRemainTime(daoqi) {
if (!daoqi) return '';
try {
const now = new Date();
const end = new Date(daoqi);
const diff = end - now;
if (diff <= 0) return '已过期';
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days > 0) {
return `${days}`;
}
const hours = Math.floor(diff / (1000 * 60 * 60));
if (hours > 0) {
return `${hours}小时`;
}
return '即将到期';
} catch (error) {
console.error('计算剩余时间错误:', error);
return '时间错误';
}
},
// ========== 处理自动滚动 ==========
handleAutoScroll() {
const { jumpParams } = this.data;
if (!jumpParams || !jumpParams.needScroll) return;
setTimeout(() => {
let scrollTop = 0;
if (jumpParams.scrollTo === 'bottom') {
scrollTop = 2000;
} else if (jumpParams.scrollTo === 'member') {
return;
}
if (scrollTop > 0) {
wx.pageScrollTo({
scrollTop: scrollTop,
duration: 300
});
}
}, 800);
},
// ========== 押金充值相关 ==========
showYajinModal() {
this.setData({
showYajinModal: true,
yajinAmount: '100'
});
},
hideYajinModal() {
this.setData({
showYajinModal: false
});
},
onYajinInput(e) {
let value = e.detail.value;
value = value.replace(/[^\d]/g, '');
if (value) {
const num = parseInt(value);
if (num < 1) value = '1';
if (num > 10000) value = '10000';
}
this.setData({
yajinAmount: value
});
},
setYajinAmount(e) {
const amount = e.currentTarget.dataset.amount;
this.setData({
yajinAmount: amount
});
},
async handleYajinPay() {
const amount = this.data.yajinAmount;
if (!amount || amount < 1 || amount > 10000) {
wx.showToast({
title: '请输入1-10000元的金额',
icon: 'none'
});
return;
}
this.showLoading('发起支付中...');
try {
const res = await request({
url: '/shangpin/yajingoumai',
method: 'POST',
data: {
jine: parseInt(amount)
}
});
if (res.data.code === 200) {
const payParams = res.data.payParams;
const dingdanid = res.data.dingdanid;
this.setData({
currentDingdanid: dingdanid
});
await this.wechatPay(payParams, 'yajin');
} else {
wx.showToast({
title: res.data.message || '支付发起失败',
icon: 'none'
});
}
} catch (error) {
console.error('押金支付失败:', error);
wx.showToast({
title: '支付失败,请重试',
icon: 'none'
});
} finally {
this.hideLoading();
}
},
// ========== 积分充值相关 ==========
// 🔥 修改点1增加积分已满的硬拦截不弹任何提示直接返回
onJifenClick(e) {
// 优先判断积分是否已满10分为满分
if (this.data.jifen >= 10) {
// 积分已满,不弹窗,不提示,静默返回
return;
}
// 检查是否有会员
const clubmber = this.data.clubmber || [];
if (clubmber.length === 0) {
wx.showToast({
title: '请先购买会员才能充值积分',
icon: 'none',
duration: 3000
});
return;
}
// 积分未满,正常打开支付方式选择
this.setData({
currentBuyType: 3,
showPayMethodModal: true
});
},
// ========== 会员购买相关 ==========
async handleHuiyuanBuy(e) {
const huiyuanId = e.currentTarget.dataset.id;
if (!huiyuanId) {
wx.showToast({
title: '会员信息错误',
icon: 'none'
});
return;
}
this.showLoading('发起会员支付中...');
try {
const res = await request({
url: '/shangpin/huiyuangm',
method: 'POST',
data: {
huiyuanid: huiyuanId
}
});
if (res.data.code === 200) {
const payParams = res.data.payParams;
const dingdanid = res.data.dingdanid;
this.setData({
currentDingdanid: dingdanid
});
await this.wechatPay(payParams, 'huiyuan', huiyuanId);
} else {
wx.showToast({
title: res.data.message || '会员支付发起失败',
icon: 'none'
});
}
} catch (error) {
console.error('会员支付失败:', error);
wx.showToast({
title: '支付失败,请重试',
icon: 'none'
});
} finally {
this.hideLoading();
}
},
// ========== 积分购买(微信支付)—— 已替换为正确的后端接口 ==========
async handleJifenBuy(e) {
const disabled = e.currentTarget.dataset.disabled;
// 检查积分是否已满
if (disabled === 'true') {
wx.showToast({ title: '积分已满,无需补充', icon: 'none' });
return;
}
// 检查是否有会员
const clubmber = this.data.clubmber || [];
if (clubmber.length === 0) {
wx.showToast({ title: '请先购买会员才能充值积分', icon: 'none', duration: 3000 });
return;
}
this.showLoading('发起积分支付中...');
try {
const res = await request({
url: '/shangpin/jifenbc', // 正确的积分下单接口
method: 'POST',
data: {}
});
if (res.data.code === 200) {
const payParams = res.data.payParams;
const dingdanid = res.data.dingdanid;
this.setData({ currentDingdanid: dingdanid });
await this.wechatPay(payParams, 'jifen');
} else {
wx.showToast({ title: res.data.message || '积分支付发起失败', icon: 'none' });
}
} catch (error) {
wx.showToast({ title: '支付失败,请重试', icon: 'none' });
} finally {
this.hideLoading();
}
},
// ========== 微信支付封装 ==========
async wechatPay(payParams, payType, extraData = null) {
return new Promise((resolve, reject) => {
if (!payParams || !payParams.timeStamp || !payParams.paySign) {
wx.showToast({
title: '支付参数错误',
icon: 'none'
});
reject(new Error('支付参数错误'));
return;
}
this.showLoading('调起支付中...');
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'MD5',
paySign: payParams.paySign,
success: async () => {
this.hideLoading();
wx.showToast({
title: '支付成功!确认中...',
icon: 'none',
duration: 2000
});
try {
await this.startPolling(payType, extraData);
resolve();
} catch (error) {
reject(error);
}
},
fail: async (res) => {
this.hideLoading();
let errorMsg = '支付失败';
if (res.errMsg.includes('cancel')) {
errorMsg = '您取消了支付';
} else if (res.errMsg.includes('fail')) {
errorMsg = '支付失败,请重试';
}
wx.showToast({
title: errorMsg,
icon: 'none'
});
try {
await this.handlePayFailure(payType, extraData);
} catch (error) {
console.error('支付失败通知失败:', error);
}
reject(new Error(res.errMsg));
}
});
});
},
// ========== 开始轮询支付结果 ==========
async startPolling(payType, extraData = null) {
const maxRetries = 10;
const interval = 2000;
const dingdanid = this.data.currentDingdanid;
if (!dingdanid) {
wx.showToast({
title: '订单号缺失',
icon: 'none'
});
return;
}
this.showLoading('确认支付结果中...');
for (let i = 0; i < maxRetries; i++) {
try {
const pollResult = await this.checkPayStatus(payType, dingdanid, extraData);
if (pollResult.success) {
this.hideLoading();
await this.updateAfterPayment(payType, extraData, pollResult.data);
wx.showToast({
title: this.getSuccessMessage(payType),
icon: 'success',
duration: 2000
});
return;
}
await this.sleep(interval);
} catch (error) {
console.error(`轮询失败第${i + 1}次:`, error);
}
}
this.hideLoading();
wx.showToast({
title: '支付确认超时,请联系客服',
icon: 'none'
});
},
// ========== 检查支付状态 ==========
async checkPayStatus(payType, dingdanid, extraData = null) {
let url = '';
let data = { dingdanid: dingdanid };
switch (payType) {
case 'yajin':
url = '/shangpin/yajinlunxun';
break;
case 'jifen':
url = '/shangpin/jifenlunxun';
break;
case 'huiyuan':
url = '/shangpin/huiyuanlx';
data.huiyuanid = extraData;
break;
default:
throw new Error('未知的支付类型');
}
try {
const res = await request({
url,
method: 'POST',
data
});
return {
success: res.data.code === 200,
data: res.data
};
} catch (error) {
console.error('检查支付状态失败:', error);
return {
success: false,
data: null
};
}
},
// ========== 支付失败处理 ==========
async handlePayFailure(payType, extraData = null) {
let url = '';
let data = { dingdanid: this.data.currentDingdanid };
switch (payType) {
case 'yajin':
url = '/shangpin/yajinshibai';
break;
case 'jifen':
url = '/shangpin/jifenshibai';
break;
case 'huiyuan':
url = '/shangpin/huiyuanshibai';
data.huiyuanid = extraData;
break;
default:
return;
}
try {
await request({
url,
method: 'POST',
data
});
} catch (error) {
console.error('支付失败通知失败:', error);
}
},
// ========== 支付成功后更新数据 ==========
async updateAfterPayment(payType, extraData = null, responseData = null) {
const app = getApp();
switch (payType) {
case 'yajin':
if (responseData && responseData.yajin !== undefined) {
app.globalData.yajin = responseData.yajin;
}
await this.refreshYajinData();
break;
case 'jifen':
if (responseData && responseData.jifen !== undefined) {
app.globalData.jinfen = responseData.jifen;
}
await this.refreshJifenData();
break;
case 'huiyuan':
if (responseData && responseData.huiyuan) {
app.globalData.jinfen = responseData.jifen;
await this.updateClubmberData(responseData.huiyuan);
}
await this.refreshHuiyuanData(extraData);
break;
}
this.loadFromGlobalData();
},
// ========== 刷新押金数据 ==========
async refreshYajinData() {
this.loadFromGlobalData();
},
// ========== 刷新积分数据 ==========
async refreshJifenData() {
this.loadFromGlobalData();
},
// ========== 刷新会员数据 ==========
async refreshHuiyuanData(huiyuanId) {
this.loadFromGlobalData();
await this.fetchHuiyuanList();
},
// ========== 更新会员数据到全局变量 ==========
async updateClubmberData(huiyuanData) {
const app = getApp();
const globalClubmber = app.globalData.clumber || [];
const { huiyuanid, huiyuanming, daoqi } = huiyuanData;
const index = globalClubmber.findIndex(item => item.huiyuanid === huiyuanid);
if (index >= 0) {
globalClubmber[index].daoqi = daoqi;
} else {
globalClubmber.push({
huiyuanid: huiyuanid,
huiyuanming: huiyuanming,
daoqi: daoqi
});
}
app.globalData.clumber = globalClubmber;
this.setData({
clubmber: [...globalClubmber]
});
},
// ========== 检查并刷新数据 ==========
async checkAndRefreshData() {
this.loadFromGlobalData();
},
// ========== 工具方法 ==========
getSuccessMessage(payType) {
switch (payType) {
case 'yajin': return '押金充值成功';
case 'jifen': return '积分补充成功';
case 'huiyuan': return '会员购买成功';
default: return '支付成功';
}
},
showLoading(text = '加载中...') {
this.setData({
payLoading: true,
loadingText: text
});
},
hideLoading() {
this.setData({
payLoading: false
});
},
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
// ========== 新增功能(余额抵扣等) ==========
onBuyHuiyuanClick(e) {
const huiyuanId = e.currentTarget.dataset.id;
this.setData({
currentBuyType: 1,
currentHuiyuanId: huiyuanId,
showPayMethodModal: true
});
},
onYajinClick() {
this.showYajinModal();
},
onYajinConfirm() {
const amount = this.data.yajinAmount;
if (!amount || amount < 1 || amount > 10000) {
wx.showToast({ title: '请输入1-10000元的金额', icon: 'none' });
return;
}
this.hideYajinModal();
this.setData({
currentBuyType: 2,
currentYajinAmount: amount,
showPayMethodModal: true
});
},
hidePayMethodModal() {
this.setData({ showPayMethodModal: false });
},
// 🔥 修改点2选择支付方式时再次校验积分状态防止异步过程中积分变化
onPayMethodSelect(e) {
const method = e.currentTarget.dataset.method;
const { currentBuyType, currentHuiyuanId, currentYajinAmount, jifen } = this.data;
this.hidePayMethodModal();
// 如果是积分购买,且积分已满,直接拒绝
if (currentBuyType === 3 && jifen >= 10) {
wx.showToast({
title: '积分已满,无需补充',
icon: 'none'
});
return;
}
if (method === 'wx') {
if (currentBuyType === 1) {
this.handleHuiyuanBuy({ currentTarget: { dataset: { id: currentHuiyuanId } } });
} else if (currentBuyType === 2) {
this.setData({ yajinAmount: currentYajinAmount }, () => {
this.handleYajinPay();
});
} else if (currentBuyType === 3) {
// ✅ 调用已修正的积分支付函数
this.handleJifenBuy({ currentTarget: { dataset: { disabled: 'false' } } });
}
} else if (method === 'balance') {
this.fetchBalanceOptions();
}
},
// 🔥 修改点3请求余额抵扣选项前校验积分状态
async fetchBalanceOptions() {
const { currentBuyType, currentHuiyuanId, currentYajinAmount, jifen } = this.data;
// 如果是积分购买,且积分已满,拒绝发起余额抵扣
if (currentBuyType === 3 && jifen >= 10) {
wx.showToast({
title: '积分已满,无需补充',
icon: 'none'
});
return;
}
this.showLoading('获取抵扣信息...');
try {
const res = await request({
url: '/shangpin/czhqdy',
method: 'POST',
data: {
leixing: currentBuyType,
huiyuan_id: currentBuyType === 1 ? currentHuiyuanId : undefined,
yajin_jine: currentBuyType === 2 ? parseInt(currentYajinAmount) : undefined,
}
});
if (res.data.code === 200) {
const options = res.data.data || [];
this.setData({
balanceOptions: options,
showBalanceModal: true
});
} else {
wx.showToast({ title: res.data.message || '获取失败', icon: 'none' });
}
} catch (error) {
console.error('获取抵扣身份失败:', error);
wx.showToast({ title: '网络错误', icon: 'none' });
} finally {
this.hideLoading();
}
},
hideBalanceModal() {
this.setData({ showBalanceModal: false });
},
onBalanceSelect(e) {
const identityId = e.currentTarget.dataset.id;
const selected = this.data.balanceOptions.find(opt => opt.id === identityId);
if (!selected) return;
this.setData({
selectedBalanceId: identityId,
selectedBalanceInfo: selected,
showConfirmModal: true,
showBalanceModal: false
});
},
hideConfirmModal() {
this.setData({ showConfirmModal: false });
},
async confirmBalancePay() {
const { selectedBalanceId, currentBuyType, currentHuiyuanId, currentYajinAmount } = this.data;
if (!selectedBalanceId) return;
this.hideConfirmModal();
this.showLoading('抵扣支付中...');
try {
const res = await request({
url: '/shangpin/dsqrgmdh',
method: 'POST',
data: {
leixing: currentBuyType,
shenfen_id: selectedBalanceId,
huiyuan_id: currentBuyType === 1 ? currentHuiyuanId : undefined,
yajin_jine: currentBuyType === 2 ? parseInt(currentYajinAmount) : undefined,
}
});
if (res.data.code === 200) {
const responseData = res.data.data || {};
await this.updateAfterPayment(
currentBuyType === 1 ? 'huiyuan' : (currentBuyType === 2 ? 'yajin' : 'jifen'),
currentBuyType === 1 ? currentHuiyuanId : null,
responseData
);
wx.showToast({ title: '购买成功', icon: 'success' });
} else {
wx.showToast({ title: res.data.message || '购买失败', icon: 'none' });
}
} catch (error) {
console.error('抵扣支付失败:', error);
wx.showToast({ title: '网络错误', icon: 'none' });
} finally {
this.hideLoading();
}
},
showMemberDetail(e) {
const item = e.currentTarget.dataset.item;
this.setData({
currentMemberDetail: item,
showMemberModal: true
});
},
hideMemberModal() {
this.setData({ showMemberModal: false });
},
});

View File

@@ -0,0 +1,12 @@
{
"usingComponents": {
"global-notification": "/components/global-notification/global-notification"
},
"navigationBarTitleText": "充值中心",
"navigationBarBackgroundColor": "#0a0e2a",
"navigationBarTextStyle": "white",
"backgroundColor": "#0a0e2a",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50
}

View File

@@ -0,0 +1,514 @@
<!--pages/dashou-chongzhi/index.wxml-->
<view class="page-container">
<!-- ❌ 删除自定义导航栏区块 -->
<!-- ======================= -->
<!-- VIP会员区域 - 放在上面 -->
<!-- ======================= -->
<view class="vip-section">
<view class="section-header">
<view class="header-decoration">
<view class="dec-line"></view>
<view class="dec-diamond"></view>
<text class="dec-text">VIP会员充值</text>
<view class="dec-diamond"></view>
<view class="dec-line"></view>
</view>
<text class="section-subtitle">选择你的会员</text>
</view>
<!-- 会员卡片滑动区域 -->
<swiper
class="vip-swiper"
current="{{currentHuiyuanIndex}}"
circular="{{true}}"
previous-margin="120rpx"
next-margin="120rpx"
duration="500"
easing-function="easeOutCubic"
bindchange="onSwiperChange">
<swiper-item wx:for="{{huiyuanList}}" wx:key="id">
<view class="vip-card {{index === currentHuiyuanIndex ? 'active' : ''}}"
data-index="{{index}}" bindtap="selectHuiyuan">
<!-- 机甲风格边框 -->
<view class="card-frame">
<view class="frame-corner tl"></view>
<view class="frame-corner tr"></view>
<view class="frame-corner bl"></view>
<view class="frame-corner br"></view>
<view class="frame-line top"></view>
<view class="frame-line right"></view>
<view class="frame-line bottom"></view>
<view class="frame-line left"></view>
</view>
<!-- 卡片内容 -->
<view class="card-content">
<view class="vip-badge" wx:if="{{item.isBought}}">
<text class="badge-text">已拥有</text>
</view>
<view class="vip-title">
<text class="title-text">{{item.mingzi}}</text>
<view class="title-underline"></view>
</view>
<view class="vip-price">
<text class="price-symbol">¥</text>
<text class="price-number">{{item.jiage}}</text>
<text class="price-unit">/30天</text>
</view>
<view class="vip-features">
<view class="feature-item" wx:for="{{item.features || ['特权功能', '专属标识', '优先接单']}}"
wx:key="index">
<image class="feature-icon" src="/static/images/check-tech.png"></image>
<text class="feature-text">{{item}}</text>
</view>
</view>
</view>
<!-- 选中状态指示器 -->
<view class="card-indicator" wx:if="{{index === currentHuiyuanIndex}}">
<view class="indicator-triangle"></view>
<view class="indicator-glow"></view>
</view>
</view>
</swiper-item>
</swiper>
<!-- 会员详细介绍 -->
<view class="vip-detail">
<view class="detail-header">
<view class="detail-title">
<image class="detail-icon" src="/static/images/info-tech.png"></image>
<text class="title-text">{{currentHuiyuan.mingzi}} - 会员详情</text>
</view>
<view class="detail-tech"></view>
</view>
<view class="detail-content">
<scroll-view class="detail-scroll" scroll-y>
<text class="detail-text">{{currentHuiyuan.jieshao || '加载中...'}}</text>
<view class="detail-rules">
<view class="rule-item" wx:for="{{detailRules}}" wx:key="index">
<view class="rule-number">{{index + 1}}</view>
<text class="rule-text">{{item}}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 购买区域 - 点击按钮先弹出支付方式选择 -->
<view class="buy-section">
<view class="buy-info">
<view class="price-display">
<!-- 🔥 将“机甲能源”改为“价格” -->
<text class="price-label">价格:</text>
<text class="price-value">¥{{currentHuiyuan.jiage}}</text>
</view>
<text class="buy-desc" wx:if="{{currentHuiyuan.isBought && currentHuiyuan.buyInfo}}">
已激活,<text class="expire-date">{{currentHuiyuan.buyInfo.daoqi}}</text>后到期
</text>
<text class="buy-desc" wx:else>激活会员特权</text>
</view>
<view class="buy-action">
<view class="tech-buy-btn {{currentHuiyuan.isBought ? 'renew' : ''}}"
bindtap="onBuyHuiyuanClick" data-id="{{currentHuiyuan.id}}">
<view class="buy-btn-bg"></view>
<view class="buy-btn-glow"></view>
<text class="buy-btn-text">
{{currentHuiyuan.isBought ? '立即续费' : '立即激活'}}
</text>
<view class="buy-btn-energy">
<view class="energy-dot"></view>
<view class="energy-dot"></view>
<view class="energy-dot"></view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- ======================= -->
<!-- 三卡片区域 - 放在下面 -->
<!-- ======================= -->
<view class="tech-grid">
<!-- 会员区域 - 高度限制最多3个超出可滚动 -->
<view class="tech-card huiyuan-card">
<view class="card-header">
<view class="card-title">
<text class="title-text">我的会员</text>
<view class="title-text">👑</view>
</view>
<view class="card-badge">{{clubmber.length}}</view>
</view>
<scroll-view
class="huiyuan-scroll"
scroll-y
enable-flex
style="height: {{huiyuanListHeight}}rpx">
<view wx:for="{{clubmber}}" wx:key="huiyuanid" class="huiyuan-item" data-item="{{item}}" bindtap="showMemberDetail">
<view class="huiyuan-tag tech-tag">
<view class="tag-triangle"></view>
<text class="tag-name">{{item.huiyuanming}}</text>
<text class="tag-time">到期: {{formatDaoqiTime(item.daoqi)}}</text>
<view class="tag-glow"></view>
</view>
</view>
<view wx:if="{{clubmber.length === 0}}" class="empty-huiyuan">
<image class="empty-icon" src="/static/images/empty-member.png"></image>
<text class="empty-text">暂无会员</text>
</view>
</scroll-view>
<view class="card-footer">
<text class="footer-text">共{{clubmber.length}}个会员</text>
<view class="footer-line"></view>
</view>
</view>
<!-- 押金区域 - 点击充值按钮弹出押金输入弹窗 -->
<view class="tech-card yajin-card">
<view class="card-header">
<view class="card-title">
<text class="title-text">我的押金</text>
<view class="title-text">💰</view>
</view>
<view class="card-corner"></view>
</view>
<view class="yajin-display">
<view class="amount-container">
<text class="amount-symbol">¥</text>
<text class="amount-number">{{yajin}}</text>
</view>
<text class="amount-unit">可用余额</text>
</view>
<view class="tech-line"></view>
<view class="action-container">
<view class="tech-btn yajin-btn" bindtap="onYajinClick">
<view class="btn-bg"></view>
<text class="btn-text">立即充值</text>
<view class="btn-arrow" style="display:none;">➤</view>
<view class="btn-glow"></view>
</view>
</view>
<view class="card-corner corner-bottom"></view>
</view>
<!-- 积分区域 - 点击补充按钮弹出支付方式选择 -->
<view class="tech-card jifen-card">
<view class="card-header">
<view class="card-title">
<text class="title-text">我的积分</text>
<view class="title-text">⭐</view>
</view>
</view>
<view class="jifen-display">
<view class="circle-progress">
<view class="circle-bg"></view>
<view class="circle-mask" style="transform: rotate({{jifenProgress}}deg);"></view>
<view class="circle-fill" style="transform: rotate({{jifenProgress}}deg);"></view>
<view class="circle-center">
<text class="jifen-value">{{jifen}}</text>
<text class="jifen-label">积分</text>
</view>
<view class="circle-dots">
<view class="circle-dot" wx:for="{{10}}" wx:key="index"
style="transform: rotate({{index * 36}}deg);"></view>
</view>
</view>
<view class="jifen-status">
<text class="status-text" wx:if="{{jifen < 10}}">积分不足,影响接单,提现权限</text>
<text class="status-text full" wx:else>积分充足,无需补充</text>
</view>
</view>
<view class="action-container">
<view class="tech-btn jifen-btn" bindtap="onJifenClick" data-disabled="{{jifen >= 10}}">
<view class="btn-bg"></view>
<text class="btn-text">{{jifen >= 10 ? '积分已满' : '立即补充'}}</text>
<view class="btn-energy" wx:if="{{jifen < 10}}"></view>
<view class="btn-glow" wx:if="{{jifen < 10}}"></view>
</view>
</view>
</view>
</view>
<!-- ======================= -->
<!-- 押金充值弹窗(原有,确认事件改为 onYajinConfirm -->
<!-- ======================= -->
<view class="tech-modal" wx:if="{{showYajinModal}}">
<view class="modal-backdrop" bindtap="hideYajinModal"></view>
<view class="modal-container">
<view class="modal-corner tl"></view>
<view class="modal-corner tr"></view>
<view class="modal-corner bl"></view>
<view class="modal-corner br"></view>
<view class="modal-header">
<text class="modal-title">押金充值</text>
<text class="modal-subtitle">充值保证金</text>
<view class="modal-close" bindtap="hideYajinModal">
<text class="close-text">✕</text>
</view>
</view>
<view class="modal-body">
<view class="input-section">
<view class="input-label">
<image class="label-icon" src="/static/images/money-tech.png"></image>
<text class="label-text">充值金额 (1-10000元)</text>
</view>
<view class="input-container">
<view class="input-prefix">¥</view>
<input
class="yajin-input"
type="digit"
placeholder="输入充值金额"
placeholder-class="input-placeholder"
value="{{yajinAmount}}"
bindinput="onYajinInput"
maxlength="5"
focus="{{showYajinModal}}"/>
<view class="input-underline"></view>
</view>
<text class="input-tip">支持1-10000元整数充值</text>
</view>
<view class="quick-section">
<text class="quick-title">快速选择</text>
<view class="quick-buttons">
<view
class="quick-btn {{yajinAmount == '100' ? 'active' : ''}}"
bindtap="setYajinAmount"
data-amount="100">
<text class="quick-text">100元</text>
</view>
<view
class="quick-btn {{yajinAmount == '500' ? 'active' : ''}}"
bindtap="setYajinAmount"
data-amount="500">
<text class="quick-text">500元</text>
</view>
<view
class="quick-btn {{yajinAmount == '1000' ? 'active' : ''}}"
bindtap="setYajinAmount"
data-amount="1000">
<text class="quick-text">1000元</text>
</view>
<view
class="quick-btn {{yajinAmount == '5000' ? 'active' : ''}}"
bindtap="setYajinAmount"
data-amount="5000">
<text class="quick-text">5000元</text>
</view>
</view>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel-btn" bindtap="hideYajinModal">
<text class="btn-text">取消</text>
</view>
<view class="modal-divider"></view>
<view class="modal-btn confirm-btn" bindtap="onYajinConfirm">
<text class="btn-text">确认支付</text>
<view class="btn-sparkle"></view>
</view>
</view>
</view>
</view>
<!-- ====== 支付方式选择弹窗 ====== -->
<view class="tech-modal" wx:if="{{showPayMethodModal}}">
<view class="modal-backdrop" bindtap="hidePayMethodModal"></view>
<view class="modal-container small">
<view class="modal-corner tl"></view>
<view class="modal-corner tr"></view>
<view class="modal-corner bl"></view>
<view class="modal-corner br"></view>
<view class="modal-header">
<text class="modal-title">选择支付方式</text>
<view class="modal-close" bindtap="hidePayMethodModal">
<text class="close-text">✕</text>
</view>
</view>
<view class="modal-body">
<view class="pay-method-list">
<view class="pay-method-item" bindtap="onPayMethodSelect" data-method="wx">
<image class="method-icon" src="/static/images/wechat-pay.png"></image>
<text class="method-name">微信支付</text>
<text class="method-desc">安全快捷</text>
</view>
<view class="pay-method-item" bindtap="onPayMethodSelect" data-method="balance">
<image class="method-icon" src="/static/images/balance-pay.png"></image>
<text class="method-name">余额抵扣</text>
<text class="method-desc">使用佣金或分红</text>
</view>
</view>
</view>
</view>
</view>
<!-- ====== 余额抵扣身份选择弹窗 ====== -->
<view class="tech-modal" wx:if="{{showBalanceModal}}">
<view class="modal-backdrop" bindtap="hideBalanceModal"></view>
<view class="modal-container">
<view class="modal-corner tl"></view>
<view class="modal-corner tr"></view>
<view class="modal-corner bl"></view>
<view class="modal-corner br"></view>
<view class="modal-header">
<text class="modal-title">选择抵扣身份</text>
<view class="modal-close" bindtap="hideBalanceModal">
<text class="close-text">✕</text>
</view>
</view>
<view class="modal-body">
<view class="balance-list">
<view wx:for="{{balanceOptions}}" wx:key="id" class="balance-item"
data-id="{{item.id}}" bindtap="onBalanceSelect">
<view class="balance-info">
<text class="balance-name">{{item.jieshao}}</text>
<text class="balance-amount">需¥{{item.xuyao}}</text>
</view>
<view class="balance-arrow">➤</view>
</view>
</view>
</view>
</view>
</view>
<!-- ====== 余额抵扣确认弹窗 ====== -->
<view class="tech-modal" wx:if="{{showConfirmModal}}">
<view class="modal-backdrop" bindtap="hideConfirmModal"></view>
<view class="modal-container">
<view class="modal-corner tl"></view>
<view class="modal-corner tr"></view>
<view class="modal-corner bl"></view>
<view class="modal-corner br"></view>
<view class="modal-header">
<text class="modal-title">确认抵扣</text>
<view class="modal-close" bindtap="hideConfirmModal">
<text class="close-text">✕</text>
</view>
</view>
<view class="modal-body">
<view class="confirm-detail">
<view class="detail-row">
<text class="detail-label">抵扣身份:</text>
<text class="detail-value">{{selectedBalanceInfo && selectedBalanceInfo.jieshao}}</text>
</view>
<view class="detail-row">
<text class="detail-label">所需金额:</text>
<text class="detail-value">¥{{selectedBalanceInfo && selectedBalanceInfo.xuyao}}</text>
</view>
<view class="detail-row">
<text class="detail-label">实际扣除:</text>
<text class="detail-value">¥{{selectedBalanceInfo && selectedBalanceInfo.xuyao}}</text>
</view>
<view class="detail-tip">确认后将从您的余额中扣除</view>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel-btn" bindtap="hideConfirmModal">
<text class="btn-text">取消</text>
</view>
<view class="modal-divider"></view>
<view class="modal-btn confirm-btn" bindtap="confirmBalancePay">
<text class="btn-text">确认支付</text>
<view class="btn-sparkle"></view>
</view>
</view>
</view>
</view>
<!-- ====== 会员详情弹窗 ====== -->
<view class="tech-modal" wx:if="{{showMemberModal}}">
<view class="modal-backdrop" bindtap="hideMemberModal"></view>
<view class="modal-container">
<view class="modal-corner tl"></view>
<view class="modal-corner tr"></view>
<view class="modal-corner bl"></view>
<view class="modal-corner br"></view>
<view class="modal-header">
<text class="modal-title">会员详情</text>
<view class="modal-close" bindtap="hideMemberModal">
<text class="close-text">✕</text>
</view>
</view>
<view class="modal-body">
<view class="member-detail">
<view class="detail-row">
<text class="detail-label">会员名称:</text>
<text class="detail-value">{{currentMemberDetail && currentMemberDetail.huiyuanming}}</text>
</view>
<view class="detail-row">
<text class="detail-label">到期时间:</text>
<text class="detail-value">{{currentMemberDetail && currentMemberDetail.daoqi}}</text>
</view>
<view class="detail-row">
<text class="detail-label">剩余天数:</text>
<text class="detail-value">{{formatRemainTime(currentMemberDetail && currentMemberDetail.daoqi)}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- ❌ 删除“进入提示弹窗”整个区块 -->
<!-- ======================= -->
<!-- 支付加载动画(原有) -->
<!-- ======================= -->
<view class="tech-loading" wx:if="{{payLoading}}">
<view class="loading-backdrop"></view>
<view class="loading-content">
<view class="loading-spinner">
<view class="spinner-ring ring-1"></view>
<view class="spinner-ring ring-2"></view>
<view class="spinner-ring ring-3"></view>
<view class="spinner-core"></view>
<view class="spinner-energy"></view>
</view>
<text class="loading-text">{{loadingText}}</text>
<text class="loading-subtext">正在连接支付系统...</text>
</view>
</view>
<!-- ======================= -->
<!-- 底部机甲装饰 -->
<!-- ======================= -->
<view class="tech-footer">
<view class="footer-line"></view>
<view class="footer-text">星阙网络 © 7891 科技</view>
</view>
</view>
<!-- 通知组件 -->
<global-notification id="global-notification" />
<!-- 🆕 统一弹窗组件 -->
<popup-notice id="popupNotice" />

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,307 @@
const app = getApp();
import request from '../../utils/request.js';
import { reconnectForRole } from '../../utils/role-tab-bar.js';
Page({
data: {
// 商品类型
shangpinleixing: [],
xuanzhongLeixingId: null,
// 订单类型: normal 普通, peihu 陪护
orderType: 'normal',
// 搜索关键字
searchKeyword: '',
// 状态列表(增加全部)
statusList: [
{ name: '全部', key: 'all', zhuangtaiList: [], color: '#333333' },
{ name: '进行中', key: 'jinxingzhong', zhuangtaiList: [2], color: '#2196F3' },
{ name: '退款中', key: 'tuikuanzhong', zhuangtaiList: [4], color: '#FF9800' },
{ name: '已退款', key: 'yituikuan', zhuangtaiList: [5], color: '#9E9E9E' },
{ name: '退款失败', key: 'tuikuanshibai', zhuangtaiList: [6], color: '#F44336' },
{ name: '已完成', key: 'yiwancheng', zhuangtaiList: [3], color: '#4CAF50' },
{ name: '结算中', key: 'jiesuanzhong', zhuangtaiList: [8], color: '#FF5722' }
],
currentStatusKey: 'all',
// 每个状态独立的数据集
dsDingdanShuju: {
all: { list: [], page: 1, hasMore: true, isLoading: false },
jinxingzhong: { list: [], page: 1, hasMore: true, isLoading: false },
tuikuanzhong: { list: [], page: 1, hasMore: true, isLoading: false },
yituikuan: { list: [], page: 1, hasMore: true, isLoading: false },
tuikuanshibai: { list: [], page: 1, hasMore: true, isLoading: false },
yiwancheng: { list: [], page: 1, hasMore: true, isLoading: false },
jiesuanzhong: { list: [], page: 1, hasMore: true, isLoading: false }
},
currentList: [],
hasMore: true,
isLoading: false,
isLoadingMore: false,
scrollViewRefreshing: false,
defaultImg: '/images/default-order.png',
ossImageUrl: app.globalData.ossImageUrl || '',
},
onLoad() {
wx.setNavigationBarTitle({ title: '我的接单' });
this.loadShangpinLeixing();
this.registerNotificationComponent();
},
onShow() {
this.registerNotificationComponent();
if (wx.getStorageSync('uid')) {
reconnectForRole('dashou');
if (app.startImWhenReady) app.startImWhenReady();
}
if (this.data.currentStatusKey && this.data.xuanzhongLeixingId) {
this.loadCurrentStatusOrders(true);
}
},
registerNotificationComponent() {
const notificationComp = this.selectComponent('#global-notification');
if (notificationComp && notificationComp.showNotification) {
app.globalData.globalNotification = {
show: (data) => notificationComp.showNotification(data),
hide: () => notificationComp.hideNotification()
};
}
},
// 加载商品类型(图片拼接)
async loadShangpinLeixing() {
try {
const res = await request({
url: '/dingdan/dsqdhqddlx',
method: 'POST',
header: { 'content-type': 'application/json' }
});
if (res.data && (res.data.code === 200 || res.data.code === 0)) {
let list = res.data.data.list || res.data.data || [];
if (!Array.isArray(list)) list = [];
const oss = this.data.ossImageUrl;
const processed = list.map(item => ({
...item,
full_tupian_url: item.tupian_url && !item.tupian_url.startsWith('http')
? oss + item.tupian_url
: (item.tupian_url || '/images/default-type.png')
}));
this.setData({
shangpinleixing: processed,
xuanzhongLeixingId: processed[0]?.id || null
});
this.loadCurrentStatusOrders(true);
}
} catch (e) {
console.error('加载商品类型失败', e);
}
},
selectLeixing(e) {
const id = e.currentTarget.dataset.id;
if (id === this.data.xuanzhongLeixingId) return;
this.setData({ xuanzhongLeixingId: id });
this.resetCurrentStatusData();
this.loadCurrentStatusOrders(true);
},
switchOrderType(e) {
const type = e.currentTarget.dataset.type;
if (type === this.data.orderType) return;
this.setData({ orderType: type });
this.resetCurrentStatusData();
this.loadCurrentStatusOrders(true);
},
onSearchInput(e) {
this.setData({ searchKeyword: e.detail.value });
if (this._searchTimer) clearTimeout(this._searchTimer);
this._searchTimer = setTimeout(() => {
this.onSearchConfirm();
}, 500);
},
onSearchConfirm() {
this.resetCurrentStatusData();
this.loadCurrentStatusOrders(true);
},
clearSearch() {
this.setData({ searchKeyword: '' });
this.resetCurrentStatusData();
this.loadCurrentStatusOrders(true);
},
switchStatus(e) {
const key = e.currentTarget.dataset.key;
if (key === this.data.currentStatusKey) return;
this.setData({ currentStatusKey: key });
const tabData = this.data.dsDingdanShuju[key];
if (tabData.list.length === 0 && tabData.hasMore && !tabData.isLoading) {
this.loadCurrentStatusOrders(true);
} else {
this.refreshCurrentListView();
}
},
// 加载订单(核心)
async loadCurrentStatusOrders(isRefresh = false) {
const key = this.data.currentStatusKey;
const tabData = this.data.dsDingdanShuju[key];
if (tabData.isLoading) return;
const page = isRefresh ? 1 : tabData.page;
this.setData({
[`dsDingdanShuju.${key}.isLoading`]: true,
isLoading: true,
isLoadingMore: !isRefresh
});
try {
const apiUrl = this.data.orderType === 'peihu'
? '/dingdan/phddlbhq'
: '/dingdan/dshqdingdan';
const statusItem = this.data.statusList.find(s => s.key === key);
const zhuangtaiList = statusItem.zhuangtaiList.length > 0 ? statusItem.zhuangtaiList : undefined;
const params = {
zhuangtai_list: zhuangtaiList,
page: page,
page_size: 5,
leixing_id: this.data.xuanzhongLeixingId,
keyword: this.data.searchKeyword || undefined
};
const res = await request({
url: apiUrl,
method: 'POST',
data: params,
header: { 'content-type': 'application/json' }
});
const code = res.data.code;
if (code === 200 || code === 0) {
const newList = res.data.data.list || [];
const hasMore = res.data.data.has_more || false;
// ✅ 图片URL拼接核心
const processed = newList.map(item => ({
...item,
zhuangtaiZh: this.getZhuangtaiZh(item.zhuangtai),
zhuangtaiColor: statusItem.color,
tupian: item.tupian
? (item.tupian.startsWith('http') ? item.tupian : this.data.ossImageUrl + item.tupian)
: this.data.defaultImg
}));
const currentList = this.data.dsDingdanShuju[key].list;
const updatedList = isRefresh ? processed : [...currentList, ...processed];
this.setData({
[`dsDingdanShuju.${key}.list`]: updatedList,
[`dsDingdanShuju.${key}.page`]: page,
[`dsDingdanShuju.${key}.hasMore`]: hasMore,
[`dsDingdanShuju.${key}.isLoading`]: false,
isLoading: false,
isLoadingMore: false,
scrollViewRefreshing: false
});
this.refreshCurrentListView();
} else {
throw new Error(res.data.msg || '加载失败');
}
} catch (err) {
console.error('加载订单失败', err);
wx.showToast({ title: err.message || '加载失败', icon: 'none' });
this.setData({
[`dsDingdanShuju.${key}.isLoading`]: false,
isLoading: false,
isLoadingMore: false,
scrollViewRefreshing: false
});
this.refreshCurrentListView();
}
},
refreshCurrentListView() {
const key = this.data.currentStatusKey;
const tabData = this.data.dsDingdanShuju[key];
this.setData({
currentList: tabData.list,
hasMore: tabData.hasMore,
isLoading: tabData.isLoading
});
},
resetCurrentStatusData() {
const key = this.data.currentStatusKey;
this.setData({
[`dsDingdanShuju.${key}.list`]: [],
[`dsDingdanShuju.${key}.page`]: 1,
[`dsDingdanShuju.${key}.hasMore`]: true,
[`dsDingdanShuju.${key}.isLoading`]: false
});
this.refreshCurrentListView();
},
// 下拉刷新(由 scroll-view 触发)
onPullDownRefresh() {
if (this.data.isLoading) {
this.setData({ scrollViewRefreshing: false });
return;
}
this.setData({ scrollViewRefreshing: true });
this.resetCurrentStatusData();
this.loadCurrentStatusOrders(true);
},
// 上拉加载更多(由 scroll-view 触发)
onReachBottom() {
const key = this.data.currentStatusKey;
const tabData = this.data.dsDingdanShuju[key];
if (tabData.isLoading || !tabData.hasMore) return;
this.setData({ [`dsDingdanShuju.${key}.page`]: tabData.page + 1 });
this.loadCurrentStatusOrders(false);
},
// ✅ 手动点击“加载更多”按钮
onLoadMoreTap() {
const key = this.data.currentStatusKey;
const tabData = this.data.dsDingdanShuju[key];
if (tabData.isLoading || !tabData.hasMore) return;
this.setData({ [`dsDingdanShuju.${key}.page`]: tabData.page + 1 });
this.loadCurrentStatusOrders(false);
},
getZhuangtaiZh(zhuangtai) {
const map = {
1: '已下单', 2: '进行中', 3: '已完成',
4: '退款中', 5: '已退款', 6: '退款失败',
7: '指定中', 8: '结算中'
};
return map[zhuangtai] || '未知状态';
},
goToDsDingdanXiangqing(e) {
const item = e.currentTarget.dataset.item;
if (!item) return;
const dataStr = encodeURIComponent(JSON.stringify(item));
if (this.data.orderType === 'peihu') {
wx.navigateTo({
url: `/pages/peihuDingdan/peihuDingdan?dingdanData=${dataStr}`
});
} else {
wx.navigateTo({
url: `/pages/dsddxq/dsddxq?dingdanData=${dataStr}`
});
}
},
onImageError() {}
});

View File

@@ -0,0 +1,8 @@
{
"usingComponents": {
"global-notification": "/components/global-notification/global-notification"
},
"navigationBarTitleText": "我的接单",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50
}

View File

@@ -0,0 +1,112 @@
<view class="ds-page">
<!-- 顶部商品类型滑动 -->
<view class="leixing-area">
<scroll-view class="leixing-scroll" scroll-x enhanced show-scrollbar="{{false}}">
<view class="leixing-container">
<block wx:for="{{shangpinleixing}}" wx:key="id">
<view
class="leixing-item {{xuanzhongLeixingId == item.id ? 'leixing-active' : ''}}"
data-id="{{item.id}}"
bindtap="selectLeixing"
>
<image class="leixing-img" src="{{item.full_tupian_url}}" mode="aspectFill"/>
<text class="leixing-name">{{item.jieshao || '类型'}}</text>
</view>
</block>
</view>
</scroll-view>
</view>
<!-- 筛选行 -->
<view class="filter-row">
<view class="type-switch">
<view class="type-btn {{orderType == 'normal' ? 'type-active' : ''}}" data-type="normal" bindtap="switchOrderType">普通订单</view>
<view class="type-btn {{orderType == 'peihu' ? 'type-active' : ''}}" data-type="peihu" bindtap="switchOrderType">陪护订单</view>
</view>
<view class="search-box">
<image class="search-icon" src="/images/icon_search.svg" mode="aspectFit"/>
<input class="search-input" placeholder="搜索订单ID/介绍" value="{{searchKeyword}}" bindinput="onSearchInput" bindconfirm="onSearchConfirm" confirm-type="search"/>
<view class="search-clear" wx:if="{{searchKeyword}}" bindtap="clearSearch"><text>✕</text></view>
</view>
</view>
<!-- 主体:左右布局 -->
<view class="main-container">
<!-- 左侧状态栏(固定) -->
<view class="left-status">
<block wx:for="{{statusList}}" wx:key="key">
<view class="status-item {{currentStatusKey == item.key ? 'status-active' : ''}}" data-key="{{item.key}}" bindtap="switchStatus">
<text class="status-name">{{item.name}}</text>
<view class="status-dot" wx:if="{{currentStatusKey == item.key}}"></view>
</view>
</block>
</view>
<!-- 右侧包裹:撑出右边距,里面是 scroll-view -->
<view class="right-wrapper">
<scroll-view
class="right-list"
scroll-y enhanced show-scrollbar="{{false}}"
refresher-enabled="{{true}}"
refresher-threshold="80"
refresher-default-style="black"
refresher-background="#f5f6fa"
refresher-triggered="{{scrollViewRefreshing}}"
bindrefresherrefresh="onPullDownRefresh"
bindscrolltolower="onReachBottom"
lower-threshold="50"
>
<view class="refresher-slot" slot="refresher">
<text wx:if="{{scrollViewRefreshing}}" class="refreshing-text">⚡ 正在刷新...</text>
<text wx:else class="pull-text">▼ 下拉刷新</text>
</view>
<!-- 内容容器:仅设置左侧 padding右侧间距由 wrapper 控制 -->
<view class="list-inner">
<view wx:if="{{isLoading && currentList.length === 0}}" class="loading-state">
<view class="loading-spinner"></view>
<text class="loading-tip">加载订单中...</text>
</view>
<view wx:elif="{{!isLoading && currentList.length === 0}}" class="empty-state">
<image class="empty-img" src="/images/empty-order.png" mode="aspectFit"/>
<text class="empty-text">暂无订单</text>
</view>
<block wx:for="{{currentList}}" wx:key="dingdan_id">
<view class="order-card" data-item="{{item}}" bindtap="goToDsDingdanXiangqing">
<image class="card-img" src="{{item.tupian}}" mode="aspectFill" binderror="onImageError"/>
<view class="card-info">
<view class="card-title">{{item.jieshao || '暂无描述'}}</view>
<view class="card-id" wx:if="{{item.dingdan_id}}">ID: {{item.dingdan_id}}</view>
</view>
<view class="card-right">
<view class="card-status" style="color: {{item.zhuangtaiColor}};">{{item.zhuangtaiZh}}</view>
<view class="card-price">¥{{item.jine || '0.00'}}</view>
</view>
</view>
</block>
<view class="load-more" wx:if="{{hasMore && currentList.length > 0 && !isLoadingMore}}">
<text class="load-more-text">上拉加载更多</text>
</view>
<view class="loading-more-state" wx:if="{{isLoadingMore}}">
<view class="mini-spinner"></view>
<text>加载中...</text>
</view>
<view class="manual-load-more" wx:if="{{hasMore && currentList.length > 0 && !isLoadingMore}}">
<button class="load-more-btn" bindtap="onLoadMoreTap" loading="{{false}}">点击加载更多</button>
</view>
<view class="no-more" wx:if="{{!hasMore && currentList.length > 0}}">
<text>—— 没有更多了 ——</text>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
<global-notification id="global-notification" />
<custom-tab-bar />

View File

@@ -0,0 +1,349 @@
/* 页面容器:禁止整体滚动 */
page {
height: 100%;
overflow: hidden;
}
.ds-page {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f6fa;
}
/* 商品类型区 */
.leixing-area {
background: #fff;
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.leixing-scroll {
white-space: nowrap;
}
.leixing-container {
display: inline-flex;
padding: 0 24rpx;
}
.leixing-item {
display: inline-flex;
flex-direction: column;
align-items: center;
margin-right: 32rpx;
padding: 12rpx 20rpx;
border-radius: 28rpx;
transition: all 0.2s;
}
.leixing-item.leixing-active {
background: linear-gradient(135deg, #e8f0fe, #d4e4ff);
box-shadow: 0 4rpx 12rpx rgba(33,150,243,0.15);
}
.leixing-img {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #f0f0f0;
margin-bottom: 8rpx;
}
.leixing-name {
font-size: 24rpx;
color: #333;
max-width: 100rpx;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 筛选行 */
.filter-row {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.type-switch {
display: flex;
background: #f0f2f5;
border-radius: 36rpx;
padding: 4rpx;
margin-right: 24rpx;
}
.type-btn {
padding: 12rpx 32rpx;
font-size: 26rpx;
color: #666;
border-radius: 32rpx;
transition: all 0.2s;
}
.type-btn.type-active {
background: #fff;
color: #1976D2;
font-weight: 600;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.06);
}
.search-box {
flex: 1;
display: flex;
align-items: center;
background: #f0f2f5;
border-radius: 36rpx;
padding: 0 20rpx;
height: 68rpx;
}
.search-icon {
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
opacity: 0.5;
}
.search-input {
flex: 1;
font-size: 26rpx;
color: #333;
}
.search-clear {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ccc;
border-radius: 50%;
margin-left: 8rpx;
}
.search-clear text {
font-size: 24rpx;
color: #fff;
font-weight: bold;
}
/* 主体布局 */
.main-container {
flex: 1;
display: flex;
overflow: hidden;
}
/* 左侧状态栏(固定) */
.left-status {
width: 180rpx;
background: #fff;
border-right: 1rpx solid #f0f0f0;
padding: 28rpx 0;
flex-shrink: 0;
}
.status-item {
padding: 28rpx 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.status-item.status-active {
background: linear-gradient(to right, #e3f2fd, #fff);
}
.status-name {
font-size: 28rpx;
color: #555;
}
.status-active .status-name {
color: #1976D2;
font-weight: 600;
}
.status-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background: #1976D2;
}
/* ✅ 右侧包裹:通过 padding-right 制造右侧间距 */
.right-wrapper {
flex: 1;
padding-right: 30rpx; /* 卡片距离屏幕右侧 30rpx */
box-sizing: border-box;
overflow: hidden;
}
/* 右侧滚动区 */
.right-list {
width: 100%;
height: 100%;
background: #f5f6fa;
}
.refresher-slot {
text-align: center;
padding: 14rpx 0;
font-size: 24rpx;
color: #999;
}
/* 内容容器:仅左侧 padding右侧间距由 wrapper 负责 */
.list-inner {
padding: 24rpx 0 200rpx 20rpx; /* 上、右、下、左 —— 右边无 padding */
box-sizing: border-box;
}
/* 订单卡片(严格限制宽度) */
.order-card {
display: flex;
align-items: center;
background: #fff;
border-radius: 24rpx;
padding: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 6rpx 20rpx rgba(0,0,0,0.04);
box-sizing: border-box;
width: 100%; /* 占满父容器宽度 */
}
.order-card:active {
transform: scale(0.98);
}
.card-img {
width: 130rpx;
height: 130rpx;
border-radius: 18rpx;
background: #f0f0f0;
margin-right: 24rpx;
flex-shrink: 0;
}
.card-info {
flex: 1;
height: 130rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
}
.card-title {
font-size: 28rpx;
color: #222;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.card-id {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
/* ✅ 状态和价格区:压缩宽度,强制不换行 */
.card-right {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
height: 130rpx;
margin-left: 20rpx;
flex-shrink: 0;
max-width: 160rpx; /* 限制最大宽度 */
}
.card-status {
font-size: 24rpx;
font-weight: 500;
padding: 4rpx 12rpx;
background: #f5f5f5;
border-radius: 20rpx;
margin-bottom: 8rpx;
white-space: nowrap;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.card-price {
font-size: 34rpx;
font-weight: 700;
color: #333;
white-space: nowrap;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
/* 加载与空状态 */
.loading-state, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.loading-spinner {
width: 52rpx;
height: 52rpx;
border: 4rpx solid #e0e0e0;
border-top-color: #1976D2;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 24rpx;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-img {
width: 200rpx;
height: 200rpx;
opacity: 0.4;
margin-bottom: 24rpx;
}
.empty-text, .loading-tip {
font-size: 28rpx;
color: #999;
}
.load-more {
text-align: center;
padding: 36rpx 0 20rpx 0;
font-size: 26rpx;
color: #999;
}
.loading-more-state {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
font-size: 26rpx;
color: #999;
}
.mini-spinner {
display: inline-block;
width: 26rpx;
height: 26rpx;
border: 3rpx solid #ccc;
border-top-color: #1976D2;
border-radius: 50%;
animation: spin 0.6s linear infinite;
vertical-align: middle;
margin-right: 10rpx;
}
.manual-load-more {
display: flex;
justify-content: center;
padding: 30rpx 0 10rpx 0;
}
.load-more-btn {
background: #ffffff;
color: #1976D2;
font-size: 28rpx;
font-weight: 500;
border: 2rpx solid #1976D2;
border-radius: 40rpx;
padding: 16rpx 60rpx;
text-align: center;
box-shadow: 0 4rpx 12rpx rgba(33,150,243,0.1);
}
.load-more-btn::after {
border: none;
}
.no-more {
text-align: center;
padding: 40rpx 0;
font-size: 26rpx;
color: #999;
}

Some files were not shown because too many files have changed in this diff Show More