Compare commits

...
Author SHA1 Message Date
XingQue cc650762f8 feat: 抢单池类型红角标 + 组队 Tab 待组队角标 2026-07-27 17:20:24 +08:00
XingQue 684f392cb6 fix: 冻结保障金取消明细跳转;提现须知补充开放时段 2026-07-27 16:58:59 +08:00
XingQue 77fefbe8aa fix: 组队进原订单群;成交率有样本含0%也展示 2026-07-27 09:21:52 +08:00
XingQueandCursor b9de9ea34e fix: 封禁强制登录页并清会话,修复解封后无请求
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 05:56:30 +08:00
XingQueandCursor 1c6c9a7ac0 feat: 我的页下拉刷新身份;401友好提示;封禁原因展示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 05:40:26 +08:00
XingQueandCursor eef2e95bd3 feat: 打手设置改走点单端编辑页,头像/手机与昵称分接口保存
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 05:33:02 +08:00
XingQueandCursor 0067a63093 fix: 抢单池弹窗等 OSS 就绪再拼图,避免相对 URL 空白
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 05:17:38 +08:00
XingQueandCursor 7eca128bb5 fix: 修复抢单池弹窗图片不显示(scroll-view 高度塌陷)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 05:11:30 +08:00
XingQueandCursor 83a5a0aaf0 fix: 组长海报页 generateMyPoster 支持静默自动生成
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 05:09:20 +08:00
XingQueandCursor 42b585274c feat: 推广海报页进入自动获取二维码并生成海报
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 05:08:53 +08:00
XingQueandCursor cec1b477b4 fix: 客服入驻/工作台按身份展示;登录页去底栏;未登录强制回登录页
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 05:03:16 +08:00
XingQueandCursor e203223922 fix: 登录页只保留一键登录;商家按钮严格按真实身份
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 04:58:02 +08:00
XingQueandCursor fdfd3a9524 fix: 未登录固定进登录页防卡死;普通展示邀请码,清缓存仅一键登录
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 04:52:42 +08:00
XingQueandCursor ada903de07 fix: 未登录一律回打手登录页;无商家身份禁止进入商家端
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 04:48:24 +08:00
XingQueandCursor a44be49ac6 fix: 商家入口按角色显示;消息合并;清缓存不自动登录且隐藏邀请码UI
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 04:44:48 +08:00
XingQueandCursor 283133473a feat: 纯打手端入口,无邀请码自动绑定默认管事
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 04:38:14 +08:00
XingQueandCursor 4b064c6303 checkpoint: 纯打手端改造前完整本地备份(不推远程)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 04:36:08 +08:00
XingQueandCursor 2a8f7c427c feat: 投诉仅限邀请人+人工客服展示+抢单池商家指标与平台标签
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 01:55:45 +08:00
XingQueandCursor 16eecd1278 checkpoint: 冻结资金开工前龙先生小程序本地备份(不推远程)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 18:39:32 +08:00
TermiNexus 09df206fd8 Merge branch 'main' of https://git.gvsds.com/XingQue/xingque 2026-07-10 14:01:55 +08:00
TermiNexus 6f6dac422f 修复了停止轮询后 onShow 不恢复的问题,以及支付状态获取异常的问题 2026-07-10 13:52:48 +08:00
XingQue 782a18006e fix: 总会员/体验会员抢单池前端校验与会员状态同步
- dashou-profile 增强 userHasHuiyuan,支持总会员子类型与 ID 等价比较
- 会员支付成功后从 dashouxinxi 刷新完整 clumber,不再只写总会员 ID
- order-pool 对齐 accept-order:服务端 clumber、hasRequiredMember、抢单校验
- fighter 我的页下拉刷新正确同步管事/组长角色状态
2026-07-10 11:33:34 +08:00
TermiNexus 4b60ebbc10 给所有支付接口加上了支付宝 2026-07-09 22:07:20 +08:00
XingQueandCursor 566aeaa3b7 restore: 恢复橙色逍遥梦UI版本(2ea2860)到主分支
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 00:17:03 +08:00
327 changed files with 41232 additions and 13155 deletions

No files matched your search

+99 -33
View File
@@ -3,6 +3,12 @@ import GoEasy from './static/lib/goeasy-2.13.24.esm.min';
const ChatCore = require('./utils/chat-core');
import { ensurePhoneAuth } from './utils/phone-auth';
import { getPrimaryRole, lockPrimaryRole, migrateLegacyCenterRole, PRIMARY_DEFAULT_PAGES } from './utils/primary-role';
import { isFighterOnly, resolveFighterOnlyLaunchUrl } from './utils/fighter-only';
import { setClubId, getConfiguredClubId, buildClubHeaders, getClubId } from './utils/club-context';
import { CLUB_ID, WX_APP_ID } from './config/club-config';
import { applyMiniappAssetsFromConfig, warmupPindaoConfig } from './utils/miniapp-icons.js';
import { refreshDashouMembership } from './utils/dashou-profile.js';
const { getDefaultAvatarUrl, resolveAvatarFromStorage } = require('./utils/avatar.js');
// 三端固定首页
const roleDefaultPage = PRIMARY_DEFAULT_PAGES;
@@ -36,7 +42,8 @@ App({
guanshiqun: '',
guanshiqunid: '',
appId:'wx0e4be86faac4a8d1',
appId: WX_APP_ID,
clubId: CLUB_ID,
shangjiastatus: 0,
dashoustatus: 0,
@@ -49,11 +56,7 @@ App({
yajin: null,
chenghao: '',
jinfen: null,
clumber: [{
huiyuanid: '',
huiyuanming: '',
daoqi: ''
}],
clumber: [],
chengjiaoliang: null,
zaixianZhuangtai: null,
@@ -80,6 +83,8 @@ App({
xshenfen: 1,
goEasyConfig: null,
chatEnabled: false,
groupInfoMap: {},
kefuConfig: {
link: '',
enterpriseId: ''
@@ -151,6 +156,7 @@ App({
pageState: {
currentPage: '',
isInChatPage: false,
isInGroupChat: false,
currentChatId: '',
lastPageUpdate: 0
},
@@ -159,11 +165,23 @@ App({
debugMode: false,
currentUser: null,
currentRole: 'normal',
primaryRole: 'normal'
},
primaryRole: 'normal',
/** 客服浮钮:full | mini,onLaunch 重置为 full */
kefuFloatMode: 'full',
/** 本次启动内隐藏客服浮钮(不持久化,杀进程后恢复) */
kefuFloatSessionHidden: false,
kefuFloatY: null,
/** 从「我的」跳抢单页时高亮指定单 */
_acceptOrderHighlight: '',
},
// 核心启动流程
async onLaunch() {
this.globalData.kefuFloatMode = 'full';
this.globalData.kefuFloatSessionHidden = false;
try { wx.removeStorageSync('kefuFloatPermanentHidden'); } catch (e) {}
setClubId(getConfiguredClubId(), this);
this.globalData.clubId = CLUB_ID;
// ① 隐藏返回首页按钮
wx.hideHomeButton();
wx.onAppRoute((res) => {
@@ -184,7 +202,7 @@ App({
}
migrateLegacyCenterRole(this);
const savedRole = getPrimaryRole(this);
const savedRole = isFighterOnly() ? 'dashou' : getPrimaryRole(this);
lockPrimaryRole(savedRole, this);
// ②b 冷启动:有 token 时先问后端是否需要手机号认证
@@ -193,19 +211,27 @@ App({
if (!phoneOk) return;
}
const targetPage = roleDefaultPage[savedRole] || '/pages/index/index';
if (savedRole !== 'normal') {
setTimeout(() => {
wx.reLaunch({ url: targetPage });
}, 0);
// ②c 打手端冷启动:同步会员/押金等到 globalData(抢单页依赖 clumber)
if (wx.getStorageSync('token') && savedRole === 'dashou') {
refreshDashouMembership(this).catch(() => {});
}
// ③ 应用本地缓存配置
// ③ 先应用本地缓存配置(必须在 reLaunch 抢单池之前,否则弹窗图片拼不出 OSS)
const cachedConfig = this.readConfigFromStorage();
if (cachedConfig) {
this.applyDynamicConfig(cachedConfig);
}
// 纯打手端:未登录必进登录页;已认证进抢单池
const targetPage = isFighterOnly()
? resolveFighterOnlyLaunchUrl()
: (roleDefaultPage[savedRole] || '/pages/index/index');
if (isFighterOnly() || savedRole !== 'normal') {
setTimeout(() => {
wx.reLaunch({ url: targetPage });
}, 0);
}
// ④ 初始化
this.initGoEasyWithConfig(); // 只有appkey有效时才真正初始化
ChatCore.initGlobalMessageSystem(this);
@@ -214,13 +240,11 @@ App({
this.emitEvent('staffContextChanged', {});
}
// ⑤ 建立连接
const saved = this.getSavedConnection();
console.log('【启动】缓存身份:', saved ? saved.identityType : '无',
'userId:', saved ? saved.userId : '无');
// ⑤ 建立连接(文赫式:全局监听,不反复 disconnect)
setTimeout(() => {
if (this.startImWhenReady) this.startImWhenReady();
else if (this.ensureConnection) this.ensureConnection();
if (this.globalData.chatEnabled && this.ensureConnection) {
this.ensureConnection();
}
}, 300);
// ⑥ 获取远程配置
@@ -228,10 +252,16 @@ App({
.then(latestConfig => {
this.saveConfigToStorage(latestConfig);
this.applyDynamicConfig(latestConfig);
warmupPindaoConfig(this);
try {
const { fetchGonggaoLunbo } = require('./utils/display-config');
fetchGonggaoLunbo(this, 'accept_order').catch(() => {});
} catch (e) {}
this.initGoEasyWithConfig();
this.initCurrentUser();
if (this.startImWhenReady) this.startImWhenReady();
else if (this.connectForCurrentRole) this.connectForCurrentRole();
if (this.globalData.chatEnabled && this.ensureConnection) {
this.ensureConnection();
}
console.log('远程配置更新完成');
})
.catch(err => {
@@ -239,10 +269,25 @@ App({
});
},
/** 从后台切回前台时再次向后端校验 */
/** 从后台切回前台时再次向后端校验,并维持 IM 连接 */
async onShow() {
if (!wx.getStorageSync('token')) return;
await ensurePhoneAuth({ redirect: true });
if (this.globalData.chatEnabled && typeof this.startImWhenReady === 'function') {
this.startImWhenReady();
} else if (this.globalData.chatEnabled && typeof this.ensureConnection === 'function') {
this.globalData.goEasyConnection.autoReconnect = true;
if (typeof this.restoreTabBarBadge === 'function') {
this.restoreTabBarBadge();
}
this.ensureConnection();
if (typeof this.syncConnectionStatus === 'function') {
setTimeout(() => this.syncConnectionStatus(), 800);
}
if (typeof this.loadConversations === 'function') {
setTimeout(() => this.loadConversations(), 300);
}
}
},
// 连接管理方法
@@ -258,11 +303,16 @@ App({
},
// 配置缓存读写
CONFIG_CACHE_KEY: 'app_dynamic_config',
CONFIG_CACHE_KEY_PREFIX: 'app_dynamic_config_',
getConfigCacheKey() {
const cid = getClubId(this) || CLUB_ID;
return this.CONFIG_CACHE_KEY_PREFIX + cid;
},
readConfigFromStorage() {
try {
const data = wx.getStorageSync(this.CONFIG_CACHE_KEY);
const data = wx.getStorageSync(this.getConfigCacheKey());
if (data && typeof data === 'object') {
return data;
}
@@ -273,7 +323,7 @@ App({
saveConfigToStorage(config) {
try {
if (config && typeof config === 'object') {
wx.setStorageSync(this.CONFIG_CACHE_KEY, config);
wx.setStorageSync(this.getConfigCacheKey(), config);
}
} catch (e) {}
},
@@ -284,7 +334,7 @@ App({
wx.request({
url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq',
method: 'POST',
header: { 'content-type': 'application/json' },
header: buildClubHeaders({ 'content-type': 'application/json' }),
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 0 && res.data.data) {
resolve(res.data.data);
@@ -307,7 +357,7 @@ App({
wx.request({
url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq',
method: 'POST',
header: { 'content-type': 'application/json' },
header: buildClubHeaders({ 'content-type': 'application/json' }),
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 0) {
resolve(res.data.data);
@@ -350,6 +400,19 @@ App({
enterpriseId: config.kefu.enterpriseId || ''
};
}
try {
applyMiniappAssetsFromConfig(this, config);
} catch (e) {
console.warn('miniapp assets apply failed', e);
}
if (this.emitEvent) {
this.emitEvent('configApplied', {
ossImageUrl: this.globalData.ossImageUrl,
morentouxiang: this.globalData.morentouxiang,
});
}
},
// 仅在有效 appkey 时初始化
@@ -373,9 +436,10 @@ App({
modules: ['im', 'pubsub']
});
wx.GoEasy = GoEasy;
this.globalData.chatEnabled = true;
console.log('GoEasy 初始化成功');
if (this.startImWhenReady && wx.getStorageSync('uid')) {
setTimeout(() => this.startImWhenReady(), 100);
if (this.ensureConnection && wx.getStorageSync('uid')) {
setTimeout(() => this.ensureConnection(), 100);
}
} catch (error) {
console.error('GoEasy 初始化失败:', error);
@@ -385,10 +449,12 @@ App({
initCurrentUser() {
const uid = wx.getStorageSync('uid');
if (uid) {
const def = getDefaultAvatarUrl(this);
const avatar = resolveAvatarFromStorage(this) || def;
this.globalData.currentUser = {
id: uid,
name: '用户' + uid.substring(0, 6),
avatar: this.globalData.ossImageUrl + this.globalData.morentouxiang
name: wx.getStorageSync('nicheng') || ('用户' + uid.substring(0, 6)),
avatar: avatar || def,
};
}
},
+281 -76
View File
@@ -1,113 +1,318 @@
{
"pages": [
"pages/fighter/fighter",
"pages/accept-order/accept-order",
"pages/team-hall/team-hall",
"pages/fighter-orders/fighter-orders",
"pages/messages/messages",
"pages/index/index",
"pages/category/category",
"pages/pick-user/pick-user",
"pages/messages/messages",
"pages/mine/mine",
"pages/product-detail/product-detail",
"pages/submit/submit",
"pages/edit/edit",
"pages/fighter/fighter",
"pages/merchant/merchant",
"pages/merchant-home/merchant-home",
"pages/merchant-kefu-key/merchant-kefu-key",
"pages/merchant-kefu-list/merchant-kefu-list",
"pages/staff-join/staff-join",
"pages/merchant-staff-audit/merchant-staff-audit",
"pages/merchant-staff-role/merchant-staff-role",
"pages/manager/manager",
"pages/orders/orders",
"pages/order-detail/order-detail",
"pages/fighter-rules/fighter-rules",
"pages/fighter-edit/fighter-edit",
"pages/fighter-rank/fighter-rank",
"pages/fighter-recharge/fighter-recharge",
"pages/fighter-msg/fighter-msg",
"pages/fighter-orders/fighter-orders",
"pages/accept-order/accept-order",
"pages/withdraw/withdraw",
"pages/invite-fighter/invite-fighter",
"pages/my-fighter/my-fighter",
"pages/recharge-log/recharge-log",
"pages/manager-rank/manager-rank",
"pages/merchant-dispatch/merchant-dispatch",
"pages/merchant-orders/merchant-orders",
"pages/merchant-msg/merchant-msg",
"pages/merchant-rank/merchant-rank",
"pages/merchant-recharge/merchant-recharge",
"pages/merchant-order-detail/merchant-order-detail",
"pages/fighter-order-detail/fighter-order-detail",
"pages/chat/chat",
"pages/express-order/express-order",
"pages/order-pool/order-pool",
"pages/poster/poster",
"pages/gold-fighter/gold-fighter",
"pages/leader/leader",
"pages/manager-assign/manager-assign",
"pages/leader-bonus-log/leader-bonus-log",
"pages/invite-manager/invite-manager",
"pages/order-pool2/order-pool2",
"pages/group-chat/group-chat",
"pages/cs-chat/cs-chat",
"pages/verify/verify",
"pages/merchant/merchant",
"components/global-notification/global-notification",
"components/popup-notice/popup-notice",
"components/order-card/order-card",
"components/order-sender/order-sender",
"pages/penalty/penalty/penalty",
"pages/penalty/components/fakuan-list/fakuan-list",
"pages/penalty/components/jifen-list/jifen-list",
"pages/penalty/components/fakuan-pay/fakuan-pay",
"pages/phone-auth/phone-auth",
"components/chenghao-tag/chenghao-tag",
"pages/assessor/assessor",
"pages/assess-score/assess-score",
"pages/assess-log/assess-log",
"pages/assess-center/assess-center",
"pages/assess-gold/assess-gold",
"pages/escort-orders/escort-orders",
"pages/withdraw/components/mode1/mode1",
"pages/withdraw/components/mode2/mode2"
"components/chenghao-tag/chenghao-tag"
],
"subPackages": [
{
"root": "pages/dashouduan",
"pages": ["dashouduan"]
},
{
"root": "pages/guanshiduan",
"pages": ["guanshiduan"]
},
{
"root": "pages/pick-user",
"pages": ["pick-user"]
},
{
"root": "pages/product-detail",
"pages": ["product-detail"]
},
{
"root": "pages/submit",
"pages": ["submit"]
},
{
"root": "pages/alipay-webview",
"pages": ["alipay-webview"]
},
{
"root": "pages/edit",
"pages": ["edit"]
},
{
"root": "pages/merchant-kefu-key",
"pages": ["merchant-kefu-key"]
},
{
"root": "pages/merchant-kefu-list",
"pages": ["merchant-kefu-list"]
},
{
"root": "pages/staff-join",
"pages": ["staff-join"]
},
{
"root": "pages/merchant-staff-audit",
"pages": ["merchant-staff-audit"]
},
{
"root": "pages/merchant-staff-role",
"pages": ["merchant-staff-role"]
},
{
"root": "pages/manager",
"pages": ["manager"]
},
{
"root": "pages/orders",
"pages": ["orders"]
},
{
"root": "pages/order-detail",
"pages": ["order-detail"]
},
{
"root": "pages/fighter-rules",
"pages": ["fighter-rules"]
},
{
"root": "pages/fighter-edit",
"pages": ["fighter-edit"]
},
{
"root": "pages/fighter-rank",
"pages": ["fighter-rank"]
},
{
"root": "pages/fighter-recharge",
"pages": ["fighter-recharge", "fighter-deposit"]
},
{
"root": "pages/fighter-ledger",
"pages": ["fighter-ledger"]
},
{
"root": "pages/fighter-msg",
"pages": ["fighter-msg"]
},
{
"root": "pages/dashou-exam",
"pages": ["dashou-exam"]
},
{
"root": "pages/withdraw",
"pages": ["withdraw", "withdraw-records", "components/mode1/mode1", "components/mode2/mode2"]
},
{
"root": "pages/fund-freeze",
"pages": ["fund-freeze"]
},
{
"root": "pages/role-agreement",
"pages": ["role-agreement"]
},
{
"root": "pages/invite-fighter",
"pages": ["invite-fighter"]
},
{
"root": "pages/my-fighter",
"pages": ["my-fighter"]
},
{
"root": "pages/recharge-log",
"pages": ["recharge-log"]
},
{
"root": "pages/manager-rank",
"pages": ["manager-rank"]
},
{
"root": "pages/merchant-data-stats",
"pages": ["merchant-data-stats"]
},
{
"root": "pages/merchant-dispatch",
"pages": ["merchant-dispatch"]
},
{
"root": "pages/merchant-regular-dispatch",
"pages": ["merchant-regular-dispatch"]
},
{
"root": "pages/merchant-msg",
"pages": ["merchant-msg"]
},
{
"root": "pages/merchant-penalty",
"pages": ["merchant-penalty"]
},
{
"root": "pages/merchant-rank",
"pages": ["merchant-rank"]
},
{
"root": "pages/merchant-recharge",
"pages": ["merchant-recharge"]
},
{
"root": "pages/merchant-order-detail",
"pages": ["merchant-order-detail"]
},
{
"root": "pages/fighter-order-detail",
"pages": ["fighter-order-detail"]
},
{
"root": "pages/chat",
"pages": ["chat"]
},
{
"root": "pages/express-order",
"pages": ["express-order"]
},
{
"root": "pages/order-pool",
"pages": ["order-pool"]
},
{
"root": "pages/poster",
"pages": ["poster"]
},
{
"root": "pages/gold-fighter",
"pages": ["gold-fighter"]
},
{
"root": "pages/leader",
"pages": ["leader"]
},
{
"root": "pages/manager-assign",
"pages": ["manager-assign"]
},
{
"root": "pages/leader-bonus-log",
"pages": ["leader-bonus-log"]
},
{
"root": "pages/invite-manager",
"pages": ["invite-manager"]
},
{
"root": "pages/group-chat",
"pages": ["group-chat"]
},
{
"root": "pages/cs-chat",
"pages": ["cs-chat"]
},
{
"root": "pages/verify",
"pages": ["verify"]
},
{
"root": "pages/penalty",
"pages": [
"penalty/penalty",
"components/fakuan-list/fakuan-list",
"components/jifen-list/jifen-list",
"components/fakuan-pay/fakuan-pay"
]
},
{
"root": "pages/gongdan",
"pages": [
"create/create",
"list/list",
"detail/detail",
"chongzhi-list/chongzhi-list"
]
},
{
"root": "pages/phone-auth",
"pages": ["phone-auth"]
},
{
"root": "pages/assessor",
"pages": ["assessor"]
},
{
"root": "pages/escort-orders",
"pages": ["escort-orders"]
},
{
"root": "pages/assess-center",
"name": "assess",
"pages": ["assess-center"]
},
{
"root": "pages/assess-score",
"pages": ["assess-score"]
},
{
"root": "pages/assess-log",
"pages": ["assess-log"]
},
{
"root": "pages/assess-gold",
"pages": ["assess-gold"]
}
],
"usingComponents": {
"popup-notice": "/components/popup-notice/popup-notice",
"tab-bar": "/tab-bar/index"
},
"window": {
"navigationBarTextStyle": "black",
"navigationStyle": "default",
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "星阙网络"
"navigationBarTitleText": "龙先生电竞"
},
"tabBar": {
"custom": true,
"list": [
{
"pagePath": "pages/order-pool/order-pool",
"pagePath": "pages/accept-order/accept-order",
"text": "接单池",
"iconPath": "/images/order-pool.png",
"selectedIconPath": "/images/order-pool.png"
},
{
"pagePath": "pages/order-pool2/order-pool2",
"text": "接单池",
"iconPath": "/images/order-pool.png",
"selectedIconPath": "/images/order-pool.png"
"pagePath": "pages/team-hall/team-hall",
"text": "组队",
"iconPath": "/images/team-hall.png",
"selectedIconPath": "/images/team-hall.png"
},
{
"pagePath": "pages/fighter-orders/fighter-orders",
"text": "订单",
"iconPath": "/images/orders.png",
"selectedIconPath": "/images/orders.png"
},
{
"pagePath": "pages/messages/messages",
"text": "消息",
"iconPath": "/images/messages.png",
"selectedIconPath": "/images/messages.png"
},
{
"pagePath": "pages/fighter/fighter",
"text": "我的",
"iconPath": "/images/mine.png",
"selectedIconPath": "/images/mine.png"
}
]
},
"style": "v2",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}
}
+26 -35
View File
@@ -2,15 +2,6 @@
/* ========== CSS 变量 ========== */
page {
/* 星阙紫色主题 */
--theme-primary: #9333ea;
--theme-primary-light: #a855f7;
--theme-primary-lighter: #c4b5fd;
--theme-primary-lightest: #ede9fe;
--theme-bg-tint: #f5f3ff;
--theme-gradient: linear-gradient(180deg, #c4b5fd, #a78bfa);
--theme-gradient-bg: linear-gradient(180deg, #c4b5fd 0%, #fff 28%, #f5f3ff 100%);
--color-price: #ff4444;
--color-text-main: #333;
--color-text-gray: #999;
@@ -467,7 +458,7 @@ page {
.tag-warning {
background: rgba(250, 173, 20, 0.1);
color: #a855f7;
color: #faad14;
}
.tag-danger {
@@ -477,7 +468,7 @@ page {
.tag-gold {
background: rgba(201, 169, 98, 0.1);
color: #9333ea;
color: #C9A962;
}
/* ========== 页面容器 ========== */
@@ -524,8 +515,8 @@ page {
}
.btn-primary--gold {
background: linear-gradient(135deg, #9333ea, #a855f7);
box-shadow: 0 8rpx 20rpx rgba(147, 51, 234, 0.3);
background: linear-gradient(135deg, #C9A962, #D4B56A);
box-shadow: 0 8rpx 20rpx rgba(201, 169, 98, 0.3);
}
.btn-primary--green {
@@ -539,8 +530,8 @@ page {
}
.btn-primary--orange {
background: linear-gradient(135deg, #a855f7, #7c3aed);
box-shadow: 0 8rpx 20rpx rgba(124, 58, 237, 0.3);
background: linear-gradient(135deg, #ffb347, #ff8c1a);
box-shadow: 0 8rpx 20rpx rgba(255, 140, 26, 0.3);
}
.btn-primary--purple {
@@ -574,8 +565,8 @@ page {
}
.btn-sm--gold {
background: linear-gradient(135deg, #c4b5fd, #9333ea);
box-shadow: 0 6rpx 16rpx rgba(147, 51, 234, 0.3);
background: linear-gradient(135deg, #ffcc00, #ffaa00);
box-shadow: 0 6rpx 16rpx rgba(255, 170, 0, 0.3);
}
.btn-sm--green {
@@ -613,8 +604,8 @@ page {
}
.btn-ghost--gold {
color: #9333ea;
border-color: #9333ea;
color: #C9A962;
border-color: #C9A962;
}
/* 禁用状态 */
@@ -863,8 +854,8 @@ page {
}
.status-tag--warning {
background: #faf5ff;
color: #a855f7;
background: #fffbe6;
color: #faad14;
}
.status-tag--info {
@@ -971,8 +962,8 @@ page {
}
@keyframes crownFloat {
0%, 100% { transform: translateY(0) rotate(5deg) scale(1); filter: drop-shadow(0 0 10rpx rgba(196, 181, 253, 0.8)); }
50% { transform: translateY(-15rpx) rotate(10deg) scale(1.1); filter: drop-shadow(0 0 25rpx rgba(196, 181, 253, 1)); }
0%, 100% { transform: translateY(0) rotate(5deg) scale(1); filter: drop-shadow(0 0 10rpx rgba(255, 215, 0, 0.8)); }
50% { transform: translateY(-15rpx) rotate(10deg) scale(1.1); filter: drop-shadow(0 0 25rpx rgba(255, 215, 0, 1)); }
}
@keyframes haloRotate {
@@ -1112,14 +1103,14 @@ page {
}
.chat-order-bubble {
background: #f5f3ff;
background: #fff7e0;
padding: 12rpx;
border-radius: 8rpx;
}
.chat-order-label {
font-size: 24rpx;
color: #a855f7;
color: #e6a23c;
font-weight: 600;
}
@@ -1614,7 +1605,7 @@ page {
.rank-card--1 .rank-podium-base { height: 80rpx; }
.rank-podium-gold { background: linear-gradient(180deg, rgba(196, 181, 253, 0.3) 0%, rgba(196, 181, 253, 0.1) 100%); border: 1rpx solid rgba(196, 181, 253, 0.4); }
.rank-podium-gold { background: linear-gradient(180deg, rgba(255, 215, 0, 0.3) 0%, rgba(255, 215, 0, 0.1) 100%); border: 1rpx solid rgba(255, 215, 0, 0.4); }
.rank-podium-silver { background: linear-gradient(180deg, rgba(192, 192, 192, 0.3) 0%, rgba(192, 192, 192, 0.1) 100%); border: 1rpx solid rgba(192, 192, 192, 0.4); }
.rank-podium-bronze { background: linear-gradient(180deg, rgba(205, 127, 50, 0.3) 0%, rgba(205, 127, 50, 0.1) 100%); border: 1rpx solid rgba(205, 127, 50, 0.4); }
@@ -1642,7 +1633,7 @@ page {
font-size: 85rpx;
position: relative;
z-index: 2;
filter: drop-shadow(0 0 10rpx rgba(196, 181, 253, 0.8));
filter: drop-shadow(0 0 10rpx rgba(255, 215, 0, 0.8));
animation: crownFloat 3s ease-in-out infinite;
transform: rotate(5deg);
}
@@ -1654,7 +1645,7 @@ page {
transform: translate(-50%, -50%);
width: 200rpx;
height: 200rpx;
background: radial-gradient(circle, rgba(196, 181, 253, 0.3) 0%, rgba(196, 181, 253, 0.1) 40%, transparent 70%);
background: radial-gradient(circle, rgba(255, 215, 0, 0.3) 0%, rgba(255, 215, 0, 0.1) 40%, transparent 70%);
z-index: 0;
animation: glowPulse 2s ease-in-out infinite;
}
@@ -1686,7 +1677,7 @@ page {
.rank-card--1 .rank-avatar-container { border-width: 6rpx; }
.rank-frame-gold .rank-avatar-container { border-color: rgba(196, 181, 253, 0.3); filter: drop-shadow(0 0 30rpx rgba(196, 181, 253, 0.5)); }
.rank-frame-gold .rank-avatar-container { border-color: rgba(255, 215, 0, 0.3); filter: drop-shadow(0 0 30rpx rgba(255, 215, 0, 0.5)); }
.rank-frame-silver .rank-avatar-container { border-color: rgba(192, 192, 192, 0.3); filter: drop-shadow(0 0 25rpx rgba(192, 192, 192, 0.5)); }
.rank-frame-bronze .rank-avatar-container { border-color: rgba(205, 127, 50, 0.3); filter: drop-shadow(0 0 25rpx rgba(205, 127, 50, 0.5)); }
@@ -1710,7 +1701,7 @@ page {
animation: haloRotate 10s linear infinite;
}
.rank-halo-gold { width: 220rpx; height: 220rpx; background: radial-gradient(circle, rgba(196, 181, 253, 0.4) 0%, rgba(196, 181, 253, 0.2) 30%, transparent 70%); }
.rank-halo-gold { width: 220rpx; height: 220rpx; background: radial-gradient(circle, rgba(255, 215, 0, 0.4) 0%, rgba(255, 215, 0, 0.2) 30%, transparent 70%); }
.rank-card--1 .rank-halo-gold { width: 260rpx; height: 260rpx; }
.rank-halo-silver { width: 200rpx; height: 200rpx; background: radial-gradient(circle, rgba(192, 192, 192, 0.4) 0%, rgba(192, 192, 192, 0.2) 30%, transparent 70%); }
.rank-halo-bronze { width: 200rpx; height: 200rpx; background: radial-gradient(circle, rgba(205, 127, 50, 0.4) 0%, rgba(205, 127, 50, 0.2) 30%, transparent 70%); }
@@ -1748,7 +1739,7 @@ page {
.rank-card--1 .rank-badge { width: 70rpx; height: 70rpx; font-size: 32rpx; bottom: -15rpx; right: -15rpx; }
.rank-badge-gold { background: linear-gradient(135deg, #c4b5fd, #9333ea); }
.rank-badge-gold { background: linear-gradient(135deg, #ffd700, #ff9800); }
.rank-badge-silver { background: linear-gradient(135deg, #c0c0c0, #808080); }
.rank-badge-bronze { background: linear-gradient(135deg, #cd7f32, #a0522d); }
@@ -1763,7 +1754,7 @@ page {
overflow: hidden;
}
.rank-card--1 .rank-user-info-card { border-color: rgba(196, 181, 253, 0.3); background: rgba(20, 15, 5, 0.95); }
.rank-card--1 .rank-user-info-card { border-color: rgba(255, 215, 0, 0.3); background: rgba(20, 15, 5, 0.95); }
.rank-card--2 .rank-user-info-card { border-color: rgba(192, 192, 192, 0.3); background: rgba(25, 25, 35, 0.95); }
.rank-card--3 .rank-user-info-card { border-color: rgba(205, 127, 50, 0.3); background: rgba(30, 20, 10, 0.95); }
@@ -1778,7 +1769,7 @@ page {
text-align: center;
}
.rank-card--1 .rank-user-name { color: #c4b5fd; text-shadow: 0 0 15rpx rgba(196, 181, 253, 0.7); font-size: 34rpx; }
.rank-card--1 .rank-user-name { color: #ffd700; text-shadow: 0 0 15rpx rgba(255, 215, 0, 0.7); font-size: 34rpx; }
.rank-card--2 .rank-user-name { color: #c0c0c0; text-shadow: 0 0 10rpx rgba(192, 192, 192, 0.7); }
.rank-card--3 .rank-user-name { color: #cd7f32; text-shadow: 0 0 10rpx rgba(205, 127, 50, 0.7); }
@@ -1816,7 +1807,7 @@ page {
margin-right: 4rpx;
}
.rank-card--1 .rank-currency { color: #c4b5fd; }
.rank-card--1 .rank-currency { color: #ffd700; }
.rank-card--2 .rank-currency { color: #c0c0c0; }
.rank-card--3 .rank-currency { color: #cd7f32; }
@@ -1827,7 +1818,7 @@ page {
text-shadow: 0 0 10rpx rgba(74, 222, 128, 0.5);
}
.rank-card--1 .rank-amount { color: #c4b5fd; text-shadow: 0 0 15rpx rgba(196, 181, 253, 0.7); }
.rank-card--1 .rank-amount { color: #ffd700; text-shadow: 0 0 15rpx rgba(255, 215, 0, 0.7); }
.rank-card--2 .rank-amount { color: #c0c0c0; text-shadow: 0 0 10rpx rgba(192, 192, 192, 0.7); }
.rank-card--3 .rank-amount { color: #cd7f32; text-shadow: 0 0 10rpx rgba(205, 127, 50, 0.7); }
+88 -42
View File
@@ -1,73 +1,119 @@
// components/chenghao-tag/chenghao-tag.js
const app = getApp();
const PILL_SHAPES = ['pill', 'rectangle', 'rounded'];
Component({
properties: {
mingcheng: { type: String, value: '' },
texiaoJson: { type: Object, value: {} }
texiaoJson: { type: null, value: null },
},
data: {
// 默认值:宽152,高52,六边形,无背景图,无动画,白色字
width: 152,
height: 52,
shapeClass: 'liubianxing', // 保证至少不是矩形
inlineStyle: '',
shapeClass: 'tag-pill',
animationClass: '',
bgStyle: '',
textColor: '#FFFFFF',
textSize: 22,
imageUrl: ''
imageUrl: '',
isPill: true,
},
observers: {
'texiaoJson, mingcheng': function () {
this._applyConfig(this.properties.texiaoJson);
},
},
lifetimes: {
attached() {
const cfg = this.properties.texiaoJson || {};
// 1. 尺寸(稍大一些,一行能放3~4个)
const width = cfg.width || 152;
const height = cfg.height || 52;
this._applyConfig(this.properties.texiaoJson);
},
},
methods: {
_applyConfig(raw) {
let cfg = raw || {};
if (typeof cfg === 'string') {
try {
cfg = JSON.parse(cfg);
} catch (e) {
cfg = {};
}
}
if (Array.isArray(cfg)) {
cfg = cfg[0] || {};
}
if (!cfg || typeof cfg !== 'object') {
cfg = {};
}
const ossImageUrl = app.globalData.ossImageUrl || '';
// 2. 背景处理:优先背景图,否则用渐变/纯色
// 无 shape:身份装饰标签 / 自定义色圆角标;有 shape:考核称号等特殊形状
const isPill = !cfg.shape || PILL_SHAPES.includes(cfg.shape);
if (isPill) {
const height = Number(cfg.height) || 44;
const minWidth = Number(cfg.width) || 72;
const solidColor = cfg.bg_color || cfg.background || cfg.color;
let bgStyle = '';
if (cfg.bg_gradient) {
bgStyle = `background: ${cfg.bg_gradient};`;
} else if (solidColor) {
bgStyle = `background: ${solidColor};`;
} else {
bgStyle = 'background: linear-gradient(135deg, #FFD700, #FF8C00);';
}
const flash = cfg.flash || cfg.animation === 'shine' || cfg.animation === 'glow';
let animationClass = cfg.animation || '';
if (flash && !animationClass) {
animationClass = 'pill-flash';
}
const borderColor = cfg.borderColor || cfg.border_color;
const borderStyle = borderColor ? `border: 2rpx solid ${borderColor};` : '';
const textColor = cfg.text_color || cfg.textColor || '#FFFFFF';
this.setData({
isPill: true,
shapeClass: 'tag-pill',
animationClass,
inlineStyle: `min-width: ${minWidth}rpx; height: ${height}rpx; padding: 0 18rpx; ${bgStyle} ${borderStyle}`,
textColor,
textSize: cfg.text_size || 22,
imageUrl: '',
});
return;
}
const width = Number(cfg.width) || 152;
const height = Number(cfg.height) || 52;
let bgStyle = '';
let imageUrl = '';
if (cfg.image_url) {
const ossImageUrl = app.globalData.ossImageUrl || '';
imageUrl = cfg.image_url.startsWith('http')
? cfg.image_url
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, #c4b5fd, #9333ea);`;
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
isPill: false,
inlineStyle: `width: ${width}rpx; height: ${height}rpx; ${bgStyle}`,
shapeClass: cfg.shape || 'liubianxing',
animationClass: cfg.animation || '',
textColor: cfg.text_color || '#FFFFFF',
textSize: cfg.text_size || 22,
imageUrl,
});
}
},
methods: {
// 背景图加载失败时回退为纯色背景
},
onImageError() {
this.setData({ imageUrl: '' });
// 如果 bgStyle 为空,给个默认背景
if (!this.data.bgStyle) {
this.setData({ bgStyle: 'background: linear-gradient(135deg, #c4b5fd, #9333ea);' });
if (!this.data.inlineStyle.includes('background')) {
this.setData({
inlineStyle: this.data.inlineStyle + ' background: linear-gradient(135deg, #FFD700, #FF8C00);',
});
}
}
}
});
},
},
});
+11 -14
View File
@@ -1,20 +1,17 @@
<!-- components/chenghao-tag/chenghao-tag.wxml -->
<view
class="tag-root {{shapeClass}} {{animationClass}}"
style="width: {{width}}rpx; height: {{height}}rpx; {{bgStyle}}"
<view
class="tag-root {{shapeClass}} {{animationClass}}"
style="{{inlineStyle}}"
>
<!-- 背景图(如果有) -->
<image
wx:if="{{imageUrl}}"
class="tag-bg-image"
src="{{imageUrl}}"
<image
wx:if="{{imageUrl && !isPill}}"
class="tag-bg-image"
src="{{imageUrl}}"
mode="aspectFill"
binderror="onImageError"
></image>
<!-- 文字层 -->
<text
class="tag-text"
/>
<text
class="tag-text"
style="color: {{textColor}}; font-size: {{textSize}}rpx;"
>{{mingcheng}}</text>
</view>
</view>
+29 -2
View File
@@ -12,6 +12,18 @@
margin: 4rpx;
box-sizing: border-box;
}
/* 身份装饰 / 自定义色:圆角标 */
.tag-root.tag-pill {
border-radius: 20rpx;
clip-path: none;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
}
.tag-pill .tag-text {
padding: 0;
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.25);
}
/* ========== 背景图 ========== */
.tag-bg-image {
@@ -85,6 +97,21 @@
animation: glow-anim 2s ease-in-out infinite;
}
@keyframes glow-anim {
0%, 100% { box-shadow: 0 0 8rpx rgba(196, 181, 253, 0.4); }
50% { box-shadow: 0 0 20rpx rgba(196, 181, 253, 0.8), 0 0 40rpx rgba(196, 181, 253, 0.4); }
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); }
}
/* 身份标签闪光 */
.tag-root.pill-flash::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 60%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.55), transparent);
transform: skewX(-20deg);
animation: shine-anim 2.2s infinite;
z-index: 1;
pointer-events: none;
}
@@ -1,4 +1,4 @@
const { resolveAvatarUrl } = require('../../utils/avatar.js');
const { resolveAvatarUrl, getDefaultAvatarUrl } = require('../../utils/avatar.js');
Component({
properties: {
@@ -53,7 +53,8 @@ Component({
// 静音状态
isMuted: false,
formattedTime: '',
// 自动隐藏计时器
autoHideTimeout: null
},
@@ -123,7 +124,7 @@ Component({
let avatar = resolveAvatarUrl(
data.avatar || data.message?.senderData?.avatar,
app
);
) || getDefaultAvatarUrl(app);
this.setData({
show: true,
@@ -131,6 +132,7 @@ Component({
message: data.content || '[消息内容]',
avatar: avatar,
notificationData: data,
formattedTime: this.formatTime(data.timestamp),
progress: 100,
swipingClass: '',
swipeStyle: ''
@@ -149,6 +151,7 @@ Component({
this.clearTimers();
this.setData({
show: false,
formattedTime: '',
progress: 100,
swipingClass: '',
swipeStyle: ''
@@ -265,6 +268,13 @@ Component({
this.triggerEvent('close');
this.hideNotification();
},
onAvatarError() {
const def = getDefaultAvatarUrl(getApp());
if (def && this.data.avatar !== def) {
this.setData({ avatar: def });
}
},
onMute(e) {
e.stopPropagation();
@@ -15,14 +15,15 @@
<!-- 通知内容 -->
<view class="notification-content">
<!-- 左侧头像 -->
<image class="notification-avatar" src="{{avatar}}" mode="aspectFill"></image>
<image class="notification-avatar" src="{{avatar}}" mode="aspectFill" binderror="onAvatarError"></image>
<!-- 中间内容区域 -->
<view class="notification-body">
<view class="notification-tag">新消息</view>
<view class="notification-title">{{title}}</view>
<view class="notification-message">{{message}}</view>
<view class="notification-time" wx:if="{{showTime}}">
{{formatTime(notificationData ? notificationData.timestamp : '')}}
<view class="notification-time" wx:if="{{show && showTime && formattedTime}}">
{{formattedTime}}
</view>
</view>
@@ -1,29 +1,40 @@
.global-notification {
position: fixed;
left: 20rpx;
right: 20rpx;
top: 0rpx;
left: 24rpx;
right: 24rpx;
top: calc(env(safe-area-inset-top) + 108rpx);
z-index: 99999;
opacity: 0;
transform: translateY(-120%);
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform: translateY(-160%);
transition: all 0.42s cubic-bezier(0.34, 1.2, 0.64, 1);
pointer-events: none;
overflow: hidden;
border-radius: 24rpx;
background: linear-gradient(135deg,
rgba(41, 47, 61, 0.98) 0%,
rgba(31, 36, 48, 0.98) 100%);
backdrop-filter: blur(30rpx) saturate(180%);
-webkit-backdrop-filter: blur(30rpx) saturate(180%);
border: 1rpx solid rgba(255, 255, 255, 0.08);
box-shadow:
0 20rpx 60rpx rgba(0, 0, 0, 0.4),
0 0 0 1rpx rgba(255, 255, 255, 0.03),
inset 0 1rpx 0 rgba(255, 255, 255, 0.1);
min-height: 140rpx;
border-radius: 28rpx;
background: linear-gradient(145deg,
rgba(28, 32, 42, 0.97) 0%,
rgba(22, 26, 36, 0.98) 100%);
backdrop-filter: blur(40rpx) saturate(180%);
-webkit-backdrop-filter: blur(40rpx) saturate(180%);
border: 1rpx solid rgba(255, 255, 255, 0.1);
box-shadow:
0 24rpx 64rpx rgba(0, 0, 0, 0.45),
0 0 0 1rpx rgba(255, 255, 255, 0.04),
inset 0 1rpx 0 rgba(255, 255, 255, 0.12);
min-height: 148rpx;
touch-action: pan-y;
}
.global-notification::before {
content: '';
position: absolute;
left: 0;
top: 24rpx;
bottom: 24rpx;
width: 6rpx;
border-radius: 0 6rpx 6rpx 0;
background: linear-gradient(180deg, #07c160 0%, #D4AF37 100%);
}
.global-notification.show {
opacity: 1;
transform: translateY(0);
@@ -58,8 +69,8 @@
.notification-content {
display: flex;
align-items: center;
padding: 32rpx;
min-height: 140rpx;
padding: 28rpx 28rpx 28rpx 36rpx;
min-height: 148rpx;
position: relative;
}
@@ -84,6 +95,18 @@
justify-content: center;
}
.notification-tag {
display: inline-block;
align-self: flex-start;
font-size: 20rpx;
color: #07c160;
background: rgba(7, 193, 96, 0.15);
padding: 4rpx 14rpx;
border-radius: 20rpx;
margin-bottom: 8rpx;
font-weight: 500;
}
.notification-title {
font-size: 34rpx;
font-weight: 600;
+170
View File
@@ -0,0 +1,170 @@
import { openCustomerServiceChat, resolveKefuIconUrl } from '../../utils/kefu-nav.js';
const CS_HOUR_START = 8;
const CS_HOUR_END = 23;
function getCsOnlineStatus() {
const hour = new Date().getHours();
const online = hour >= CS_HOUR_START && hour < CS_HOUR_END;
return {
csOnline: online,
csHoursText: '8:00-23:00',
csStatusText: online ? '在线' : '在线时间',
};
}
Component({
properties: {
bottom: { type: String, value: '200rpx' },
},
data: {
mode: 'full',
sessionHidden: false,
csUnread: 0,
floatY: 520,
iconUrl: '',
csOnline: false,
csHoursText: '8:00-23:00',
csStatusText: '在线时间',
},
lifetimes: {
attached() {
const app = getApp();
this._syncIcon();
this._syncFromGlobal();
this._initFloatY();
this._convHandler = () => this.syncCsUnread();
this._configHandler = () => this._syncIcon();
if (app.on) {
app.on('conversationsUpdated', this._convHandler);
app.on('unreadCountChanged', this._convHandler);
app.on('configApplied', this._configHandler);
}
this.syncCsUnread();
this._syncOnlineStatus();
},
detached() {
const app = getApp();
if (app.off) {
if (this._convHandler) {
app.off('conversationsUpdated', this._convHandler);
app.off('unreadCountChanged', this._convHandler);
}
if (this._configHandler) {
app.off('configApplied', this._configHandler);
}
}
},
},
pageLifetimes: {
show() {
this._syncIcon();
this._syncFromGlobal();
this.syncCsUnread();
this._syncOnlineStatus();
},
},
methods: {
_syncOnlineStatus() {
this.setData(getCsOnlineStatus());
},
_syncIcon() {
const url = resolveKefuIconUrl(getApp());
if (url && url !== this.data.iconUrl) {
this.setData({ iconUrl: url });
}
},
_initFloatY() {
const app = getApp();
if (app.globalData.kefuFloatY != null) {
this.setData({ floatY: app.globalData.kefuFloatY });
return;
}
try {
const sys = wx.getSystemInfoSync();
const defaultY = Math.floor((sys.windowHeight || 600) * 0.55);
this.setData({ floatY: defaultY });
} catch (e) {
this.setData({ floatY: 520 });
}
},
_syncFromGlobal() {
const app = getApp();
const sessionHidden = !!app.globalData.kefuFloatSessionHidden;
const mode = app.globalData.kefuFloatMode || 'full';
this.setData({ sessionHidden, mode });
},
onFloatMove(e) {
const y = e.detail && e.detail.y;
if (y == null) return;
const app = getApp();
app.globalData.kefuFloatY = y;
},
syncCsUnread() {
if (this.data.sessionHidden) return;
if (!wx.goEasy?.im?.latestConversations) return;
wx.goEasy.im.latestConversations({
onSuccess: (res) => {
const list = res.content?.conversations || res.conversations || [];
let unread = 0;
list.forEach((c) => {
if (c.type === 'cs' || c.teamId === 'support_team') {
unread += c.unread || 0;
}
});
if (unread !== this.data.csUnread) {
this.setData({ csUnread: unread });
}
},
onFailed: () => {},
});
},
onIconError() {
this._syncIcon();
},
onContact() {
openCustomerServiceChat();
},
onHideTap(e) {
if (e && e.stopPropagation) e.stopPropagation();
const app = getApp();
app.globalData.kefuFloatMode = 'mini';
this.setData({ mode: 'mini' });
},
onExpand(e) {
if (e && e.stopPropagation) e.stopPropagation();
const app = getApp();
app.globalData.kefuFloatMode = 'full';
this.setData({ mode: 'full' });
},
onSessionDismiss(e) {
if (e && e.stopPropagation) e.stopPropagation();
wx.showModal({
title: '隐藏联系客服',
content: '本次使用期间不再显示此按钮。清空后台重新进入小程序后会再次出现。',
confirmText: '确定隐藏',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
const app = getApp();
app.globalData.kefuFloatSessionHidden = true;
this.setData({ sessionHidden: true });
}
},
});
},
},
});
+4
View File
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+44
View File
@@ -0,0 +1,44 @@
<movable-area class="kefu-movable-area" wx:if="{{!sessionHidden}}">
<movable-view
class="kefu-movable-view"
direction="vertical"
y="{{floatY}}"
damping="40"
friction="2"
bindchange="onFloatMove"
>
<block wx:if="{{mode === 'full'}}">
<view class="kefu-wrap">
<view class="kefu-float kefu-float--full">
<view class="kefu-unread-badge" wx:if="{{csUnread > 0}}">{{csUnread > 99 ? '99+' : csUnread}}</view>
<view class="kefu-float-main" catchtap="onContact">
<image class="kefu-float-icon-img" src="{{iconUrl}}" mode="aspectFit" binderror="onIconError"/>
<view class="kefu-text-col">
<text class="kefu-float-text">联系客服</text>
<text class="kefu-float-sub kefu-float-sub--warn" wx:if="{{csUnread > 0}}">有新消息</text>
<text class="kefu-float-sub {{csOnline ? 'kefu-float-sub--online' : ''}}" wx:else>{{csStatusText}} {{csHoursText}}</text>
</view>
</view>
<view class="kefu-float-close" catchtap="onHideTap">
<text>×</text>
</view>
</view>
<view class="kefu-permanent-row" catchtap="onSessionDismiss">
<text class="kefu-permanent-label">本次不再显示</text>
</view>
</view>
</block>
<block wx:else>
<view class="kefu-wrap kefu-wrap--mini">
<view class="kefu-float kefu-float--mini" catchtap="onContact" bindlongpress="onExpand">
<view class="kefu-unread-badge kefu-unread-badge--mini" wx:if="{{csUnread > 0}}">{{csUnread > 99 ? '99+' : csUnread}}</view>
<image class="kefu-float-mini-icon-img" src="{{iconUrl}}" mode="aspectFit" binderror="onIconError"/>
</view>
<view class="kefu-permanent-row kefu-permanent-row--mini" catchtap="onSessionDismiss">
<text class="kefu-permanent-label">隐藏</text>
</view>
</view>
</block>
</movable-view>
</movable-area>
+186
View File
@@ -0,0 +1,186 @@
.kefu-movable-area {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 900;
}
.kefu-movable-view {
width: auto;
height: auto;
pointer-events: auto;
position: absolute;
right: 20rpx;
top: 0;
}
.kefu-wrap {
display: flex;
flex-direction: column;
align-items: flex-end;
pointer-events: auto;
}
.kefu-float {
position: relative;
}
.kefu-float--full {
display: flex;
align-items: stretch;
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
border-radius: 999rpx;
box-shadow: 0 8rpx 28rpx rgba(7, 193, 96, 0.45);
border: 2rpx solid rgba(255, 255, 255, 0.85);
overflow: visible;
animation: kefu-pulse 2.4s ease-in-out infinite;
}
@keyframes kefu-pulse {
0%, 100% { box-shadow: 0 8rpx 28rpx rgba(7, 193, 96, 0.45); }
50% { box-shadow: 0 8rpx 36rpx rgba(7, 193, 96, 0.65), 0 0 0 8rpx rgba(7, 193, 96, 0.12); }
}
.kefu-unread-badge {
position: absolute;
top: -10rpx;
right: -6rpx;
min-width: 36rpx;
height: 36rpx;
line-height: 36rpx;
padding: 0 8rpx;
background: #fa5151;
color: #fff;
font-size: 20rpx;
font-weight: 700;
text-align: center;
border-radius: 18rpx;
border: 3rpx solid #fff;
z-index: 2;
}
.kefu-unread-badge--mini {
top: -6rpx;
right: -4rpx;
}
.kefu-float-main {
display: flex;
align-items: center;
padding: 18rpx 24rpx;
flex: 1;
}
.kefu-float-main:active {
opacity: 0.88;
}
.kefu-float-icon-img {
width: 44rpx;
height: 44rpx;
margin-right: 12rpx;
flex-shrink: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
padding: 4rpx;
}
.kefu-text-col {
display: flex;
flex-direction: column;
min-width: 0;
}
.kefu-float-text {
font-size: 28rpx;
color: #fff;
font-weight: 700;
white-space: nowrap;
}
.kefu-float-sub {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.92);
margin-top: 2rpx;
font-weight: 600;
}
.kefu-float-sub--online {
color: #e8ffe8;
}
.kefu-float-sub--warn {
color: #fff3e0;
}
.kefu-float-close {
width: 52rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.12);
color: rgba(255, 255, 255, 0.9);
font-size: 34rpx;
line-height: 1;
border-left: 1rpx solid rgba(255, 255, 255, 0.25);
border-radius: 0 999rpx 999rpx 0;
}
.kefu-float-close:active {
background: rgba(0, 0, 0, 0.2);
}
.kefu-permanent-row {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 6rpx;
padding: 4rpx 10rpx;
}
.kefu-permanent-row:active {
opacity: 0.75;
}
.kefu-permanent-row--mini {
margin-top: 4rpx;
padding: 2rpx 8rpx;
}
.kefu-permanent-label {
font-size: 20rpx;
color: #888;
}
.kefu-wrap--mini {
align-items: flex-end;
}
.kefu-float--mini {
width: 96rpx;
height: 96rpx;
padding: 0;
border-radius: 50%;
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.45);
border: 3rpx solid #fff;
display: flex;
align-items: center;
justify-content: center;
animation: kefu-pulse 2.4s ease-in-out infinite;
}
.kefu-float--mini:active {
transform: scale(0.96);
}
.kefu-float-mini-icon-img {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
padding: 4rpx;
}
+1 -1
View File
@@ -15,7 +15,7 @@
.card-id { font-size: 24rpx; color: #999; margin-left: 15rpx; }
.cross-tag {
margin-left: auto;
background: #8b5cf6;
background: #ff9900;
color: #fff;
font-size: 20rpx;
padding: 2rpx 12rpx;
+77 -6
View File
@@ -1,16 +1,87 @@
import request from '../../utils/request';
const app = getApp();
function mapIdentityForApi() {
const role = app.globalData.currentRole || wx.getStorageSync('currentRole') || 'normal';
if (role === 'dashou') return 'dashou';
if (role === 'shangjia') return 'shangjia';
return 'normal';
}
Component({
properties: {
visible: Boolean,
showDetail: { type: Boolean, value: false }
visible: { type: Boolean, value: false },
groupId: { type: String, value: '' },
orderId: { type: String, value: '' },
},
data: {},
data: {
keyword: '',
loading: false,
orders: [],
filteredOrders: [],
},
observers: {
visible(v) {
if (v) {
this.setData({ keyword: '' });
this.loadOrders('');
}
},
},
methods: {
close() {
this.triggerEvent('close');
}
}
});
},
onSearchInput(e) {
const keyword = (e.detail.value || '').trim();
this.setData({ keyword });
if (this._searchTimer) clearTimeout(this._searchTimer);
this._searchTimer = setTimeout(() => {
this.loadOrders(keyword);
}, 350);
},
async loadOrders(keyword) {
const { groupId, orderId } = this.properties;
if (!groupId && !orderId) return;
this.setData({ loading: true });
try {
const res = await request({
url: '/dingdan/ltpddd',
method: 'POST',
data: {
groupId,
dingdan_id: orderId,
keyword: keyword || '',
identityType: mapIdentityForApi(),
},
});
const body = res?.data || {};
const list = body.data?.list || [];
this.setData({
orders: list,
filteredOrders: list,
loading: false,
});
} catch (e) {
console.warn('加载历史订单失败', e);
this.setData({ loading: false, orders: [], filteredOrders: [] });
wx.showToast({ title: '加载订单失败', icon: 'none' });
}
},
onSelectOrder(e) {
const index = e.currentTarget.dataset.index;
const order = this.data.filteredOrders[index];
if (!order) return;
this.triggerEvent('send', { order });
this.close();
},
},
});
+22 -6
View File
@@ -1,12 +1,28 @@
<view class="order-sender-mask" wx:if="{{visible}}" catchtap="close"></view>
<view class="order-sender {{visible ? 'show' : ''}}">
<view class="header">
<text class="title">提示</text>
<text class="title">选择订单发送</text>
<text class="close" bindtap="close">✕</text>
</view>
<view class="content">
<view class="icon">📋</view>
<text class="notice">此功能暂未开放</text>
<text class="sub">敬请期待</text>
<view class="search-row">
<input class="search-input" placeholder="搜索订单号/内容" value="{{keyword}}" bindinput="onSearchInput" confirm-type="search" />
</view>
</view>
<scroll-view scroll-y class="order-list" enable-flex>
<view class="loading-tip" wx:if="{{loading}}">加载中...</view>
<block wx:for="{{filteredOrders}}" wx:key="dingdan_id">
<view class="order-item" data-index="{{index}}" bindtap="onSelectOrder">
<view class="order-item-top">
<text class="order-id">{{item.dingdan_id}}</text>
<text class="order-status">{{item.zhuangtaiText}}</text>
</view>
<text class="order-desc">{{item.jieshao || '暂无介绍'}}</text>
<view class="order-item-bottom">
<text class="order-price">¥{{item.jine || '0'}}</text>
<text class="order-time">{{item.create_time}}</text>
</view>
<text class="order-send-hint">点击发送订单卡片</text>
</view>
</block>
<view class="empty-tip" wx:if="{{!loading && !filteredOrders.length}}">暂无相关订单</view>
</scroll-view>
</view>
+139 -44
View File
@@ -1,45 +1,140 @@
.order-sender-mask {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 2000;
}
.order-sender {
position: fixed; bottom: 0; left: 0; right: 0;
height: 360rpx;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
z-index: 2001;
transform: translateY(100%);
transition: 0.25s;
display: flex;
flex-direction: column;
}
.order-sender.show { transform: translateY(0); }
.header {
display: flex; justify-content: space-between;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.title { font-size: 32rpx; font-weight: 600; }
.close { font-size: 44rpx; color: #999; padding: 0 12rpx; }
.content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 40rpx;
}
.icon { font-size: 80rpx; margin-bottom: 20rpx; }
.notice {
font-size: 34rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
}
.sub {
font-size: 24rpx;
color: #999;
}
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
}
.order-sender {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 72vh;
max-height: 900rpx;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
z-index: 2001;
transform: translateY(100%);
transition: transform 0.25s;
display: flex;
flex-direction: column;
}
.order-sender.show {
transform: translateY(0);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.title {
font-size: 32rpx;
font-weight: 600;
}
.close {
font-size: 44rpx;
color: #999;
padding: 0 12rpx;
}
.search-row {
padding: 16rpx 24rpx;
flex-shrink: 0;
}
.search-input {
background: #f5f5f5;
border-radius: 32rpx;
padding: 16rpx 28rpx;
font-size: 28rpx;
}
.order-list {
flex: 1;
height: 0;
padding: 0 24rpx 24rpx;
box-sizing: border-box;
}
.loading-tip,
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 60rpx 0;
}
.order-item {
background: #fafafa;
border-radius: 16rpx;
padding: 20rpx 24rpx;
margin-bottom: 16rpx;
border: 1rpx solid #eee;
}
.order-item:active {
background: #f0f7ff;
}
.order-item-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.order-id {
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.order-status {
font-size: 22rpx;
color: #07c160;
background: #e8f8ee;
padding: 4rpx 14rpx;
border-radius: 20rpx;
}
.order-desc {
font-size: 26rpx;
color: #666;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 8rpx;
}
.order-item-bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-price {
font-size: 28rpx;
color: #ff6b00;
font-weight: 600;
}
.order-time {
font-size: 22rpx;
color: #aaa;
}
.order-send-hint {
display: block;
font-size: 22rpx;
color: #007aff;
margin-top: 10rpx;
}
+15
View File
@@ -0,0 +1,15 @@
Component({
properties: {
visible: { type: Boolean, value: false },
title: { type: String, value: '频道' },
channelNo: { type: String, value: '' },
images: { type: Array, value: [] },
},
methods: {
preventMove() {},
onClose() {
this.triggerEvent('close');
},
},
});
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+24
View File
@@ -0,0 +1,24 @@
<view class="pindao-mask" wx:if="{{visible}}" catchtouchmove="preventMove">
<view class="pindao-panel" catchtap="preventMove">
<view class="pindao-head flexb">
<text class="pindao-title">{{title}}</text>
<view class="pindao-close" bindtap="onClose">×</view>
</view>
<view class="pindao-channel" wx:if="{{channelNo}}">
<text class="pindao-channel-label">频道号</text>
<text class="pindao-channel-no">{{channelNo}}</text>
</view>
<scroll-view class="pindao-scroll" scroll-y enhanced show-scrollbar="{{false}}">
<view wx:if="{{!images || images.length === 0}}" class="pindao-empty">暂无频道内容</view>
<image
wx:for="{{images}}"
wx:key="index"
class="pindao-img"
src="{{item}}"
mode="widthFix"
lazy-load="{{false}}"
show-menu-by-longpress
/>
</scroll-view>
</view>
</view>
+88
View File
@@ -0,0 +1,88 @@
.pindao-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.pindao-panel {
width: 100%;
max-height: 80vh;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.pindao-head {
padding: 28rpx 32rpx 16rpx;
align-items: center;
}
.pindao-title {
font-size: 32rpx;
font-weight: 600;
color: #222;
}
.pindao-close {
width: 56rpx;
height: 56rpx;
line-height: 52rpx;
text-align: center;
font-size: 40rpx;
color: #999;
}
.pindao-channel {
padding: 0 32rpx 20rpx;
text-align: center;
}
.pindao-channel-label {
display: block;
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.pindao-channel-no {
font-size: 36rpx;
font-weight: 700;
color: #c9a962;
}
.pindao-scroll {
flex: 1;
max-height: 60vh;
padding: 0 24rpx 24rpx;
box-sizing: border-box;
}
.pindao-img {
width: 100%;
display: block;
margin-bottom: 16rpx;
border-radius: 12rpx;
}
.pindao-empty {
text-align: center;
color: #999;
padding: 48rpx 0;
font-size: 28rpx;
}
.flexb {
display: flex;
justify-content: space-between;
}
+102 -32
View File
@@ -41,15 +41,24 @@ Component({
async show(config, onCloseCallback) {
const { title, images, duration, showMuteCheckbox, serverTime } = config;
this.onCloseCallback = onCloseCallback;
this._showToken = (this._showToken || 0) + 1;
const token = this._showToken;
let safeImages = JSON.parse(JSON.stringify(images || []));
// 兼容 url / image_url
safeImages = safeImages.map((img) => ({
...img,
url: img.url || img.image_url || '',
text: img.text || '',
})).filter((img) => !!img.url);
const hasImages = safeImages.length > 0;
let hasTextOnly = false, textContent = '';
if (!hasImages) {
hasTextOnly = true;
textContent = config.text || '暂无内容';
}
this.setData({
visible: true,
isMinimized: false,
@@ -65,36 +74,91 @@ Component({
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,
});
if (!hasImages) return;
// 等 OSS 域名就绪再拼接,避免相对路径导致整图空白
await this.ensureOssBase(4000);
if (token !== this._showToken) return;
const processed = safeImages.map((img) => ({
url: this.getFullImageUrl(img.url),
text: img.text,
height: 0,
}));
console.log('[popup-notice] 图片URL:', processed.map((p) => p.url));
this.setData({ processedImages: processed });
// 可选:量真实高度(失败也不影响 widthFix 展示)
const screenWidth = wx.getSystemInfoSync().windowWidth;
const containerWidth = Math.min(screenWidth * 0.86, 310);
const imageWidth = Math.max(containerWidth - 30, 200);
for (let i = 0; i < processed.length; i++) {
if (token !== this._showToken) return;
const fullUrl = processed[i].url;
if (!fullUrl || !/^https?:\/\//.test(fullUrl)) {
console.error('[popup-notice] OSS未就绪或URL非法', fullUrl);
continue;
}
try {
const res = await this.getImageInfoPromise(fullUrl);
const realHeight = (imageWidth * res.height) / res.width;
this.setData({ [`processedImages[${i}].height`]: realHeight });
} catch (e) {
console.error('获取图片信息失败', fullUrl, e);
}
this.setData({ processedImages: processed });
}
},
/** 等待 / 回读本地缓存,确保能拼出完整 COS 地址 */
ensureOssBase(timeoutMs = 3000) {
const app = getApp();
const readOss = () => this.resolveOssBase();
if (readOss()) return Promise.resolve(readOss());
return new Promise((resolve) => {
let done = false;
const finish = () => {
if (done) return;
done = true;
if (app.off) app.off('configApplied', onCfg);
clearInterval(timer);
clearTimeout(timerOut);
resolve(readOss());
};
const onCfg = () => finish();
if (app.on) app.on('configApplied', onCfg);
const timer = setInterval(() => {
if (readOss()) finish();
}, 100);
const timerOut = setTimeout(finish, timeoutMs);
// 主动再读一次缓存配置
try {
if (typeof app.readConfigFromStorage === 'function') {
const cached = app.readConfigFromStorage();
if (cached && app.applyDynamicConfig) app.applyDynamicConfig(cached);
}
} catch (e) {}
if (readOss()) finish();
});
},
resolveOssBase() {
const app = getApp();
let base = (app.globalData && app.globalData.ossImageUrl) || '';
if (!base && app && typeof app.readConfigFromStorage === 'function') {
try {
const cached = app.readConfigFromStorage();
base = (cached && cached.cos && cached.cos.ossImageUrl) || '';
if (base && app.globalData) app.globalData.ossImageUrl = base;
} catch (e) {}
}
return base || '';
},
getImageInfoPromise(url) {
return new Promise((resolve, reject) => {
@@ -204,12 +268,18 @@ Component({
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;
if (relativeUrl.startsWith('http://') || relativeUrl.startsWith('https://')) {
return relativeUrl;
}
// 后端 cos.ossImageUrl 带尾斜杠;相对路径不带前导 /
let base = this.resolveOssBase();
if (!base) {
console.warn('[popup-notice] ossImageUrl 为空,无法拼接', relativeUrl);
return '';
}
if (!base.endsWith('/')) base += '/';
const clean = String(relativeUrl).replace(/^\/+/, '');
return base + clean;
},
preventMove() {},
+6 -4
View File
@@ -25,15 +25,17 @@
<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;"
<image
class="block-image"
src="{{item.url}}"
mode="widthFix"
style="width:100%; {{item.height > 0 ? 'height:' + item.height + 'px;' : ''}} display:block;"
data-index="{{index}}"
data-url="{{item.url}}"
bindtap="onCustomPreview"
binderror="onImageError"
lazy-load="{{false}}"
show-menu-by-longpress="{{true}}"
/>
</view>
</view>
+14 -8
View File
@@ -15,14 +15,18 @@
}
.popup-container {
width: 90%;
height: 85vh;
width: 86%;
max-width: 620rpx;
/* 必须给明确高度:scroll-view 依赖父级高度,height:auto 会导致图片区塌成 0 */
height: 68vh;
max-height: 68vh;
background: white;
border-radius: 32rpx;
border-radius: 28rpx;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.28);
}
.popup-container.fullscreen {
@@ -78,6 +82,7 @@
.scroll-wrapper {
flex: 1;
min-height: 0;
overflow: hidden;
width: 100%;
}
@@ -138,7 +143,7 @@
.countdown-tip {
text-align: center;
font-size: 28rpx;
color: #9333ea;
color: #ff6600;
margin-bottom: 20rpx;
}
@@ -156,10 +161,11 @@
/* 🔥 关闭按钮(替换原有 button,增加扫光) */
.close-btn {
width: 600rpx;
height: 100rpx;
width: 86%;
max-width: 480rpx;
height: 88rpx;
background: #1a1a1a;
border-radius: 60rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
@@ -223,7 +229,7 @@
height: 120rpx;
border-radius: 50%;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
border: 2rpx solid rgba(196, 181, 253, 0.6);
border: 2rpx solid rgba(255, 215, 0, 0.6);
background: #f0f0f0;
display: block;
transform: translateZ(0);
+58
View File
@@ -0,0 +1,58 @@
const STORAGE_KEY = 'tousuFloatHidden';
Component({
data: {
hidden: false,
},
lifetimes: {
attached() {
this._syncHidden();
},
},
pageLifetimes: {
show() {
this._syncHidden();
},
},
methods: {
_syncHidden() {
let hidden = false;
try {
hidden = wx.getStorageSync(STORAGE_KEY) === 1 || wx.getStorageSync(STORAGE_KEY) === '1';
} catch (e) {}
if (hidden !== this.data.hidden) {
this.setData({ hidden });
}
},
go() {
wx.navigateTo({
url: '/pages/gongdan/create/create',
fail: (err) => {
console.error('打开投诉页失败', err);
wx.showToast({ title: '打开失败,请重试', icon: 'none' });
},
});
},
onDismiss(e) {
if (e && e.stopPropagation) e.stopPropagation();
wx.showModal({
title: '隐藏投诉入口',
content: '隐藏后可在「我的 / 工单」中继续投诉。需要恢复时可清除小程序缓存。',
confirmText: '不再显示',
cancelText: '取消',
success: (res) => {
if (!res.confirm) return;
try {
wx.setStorageSync(STORAGE_KEY, 1);
} catch (err) {}
this.setData({ hidden: true });
},
});
},
},
});
+1
View File
@@ -0,0 +1 @@
{"component":true}
+6
View File
@@ -0,0 +1,6 @@
<view class="tousu-wrap" wx:if="{{!hidden}}">
<view class="float" catchtap="go">投诉</view>
<view class="dismiss-row" catchtap="onDismiss">
<text class="dismiss-label">不再显示</text>
</view>
</view>
+37
View File
@@ -0,0 +1,37 @@
.tousu-wrap {
position: fixed;
right: 24rpx;
bottom: 280rpx;
z-index: 999;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6rpx;
}
.float {
min-width: 96rpx;
height: 72rpx;
padding: 0 22rpx;
border-radius: 36rpx;
background: rgba(55, 55, 55, 0.72);
color: #f5f5f5;
font-size: 26rpx;
font-weight: 500;
letter-spacing: 1rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
}
.dismiss-row {
padding: 4rpx 8rpx;
}
.dismiss-label {
font-size: 20rpx;
color: rgba(120, 120, 120, 0.9);
text-decoration: underline;
text-underline-offset: 4rpx;
}
+11
View File
@@ -0,0 +1,11 @@
/**
* 小程序所属俱乐部配置 — 每个小程序工程编译前改这里即可。
* 星阙 = xq;星之界 = xzj;龙先生 = lxs(与后台 club 表 club_id 一致,勿写成 lsx)。
* 不要依赖后端猜测:前端必须明确所属 club。
*/
export const CLUB_ID = 'lxs';
/** 与 club 表 wx_appid 一致,登录时可传给后端反查 club */
export const WX_APP_ID = 'wx7ff90e9d024fcdb8';
export const CLUB_NAME = '龙先生电竞';
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,3 @@
# 总会员抢单/展示修复前备份
日期: 2026-07-10
原文件见同目录。
@@ -0,0 +1,36 @@
/**
* 从服务端刷新打手会员/押金/积分等到 globalData(抢单、考试等页共用)
*/
import request from './request.js';
export async function refreshDashouMembership(app) {
app = app || getApp();
try {
const res = await request({ url: '/yonghu/dashouxinxi', method: 'POST' });
if (res && res.data && res.data.code == 200) {
const data = res.data.data || {};
const clumber = Array.isArray(data.clumber) ? data.clumber : [];
app.globalData.clumber = clumber;
if (data.yajin !== undefined) app.globalData.yajin = data.yajin;
if (data.jifen !== undefined) app.globalData.jinfen = data.jifen;
if (data.zhanghaostatus !== undefined) app.globalData.zhanghaoStatus = data.zhanghaostatus;
if (data.dashouzhuangtai !== undefined) app.globalData.dashouzhuangtai = data.dashouzhuangtai;
return clumber;
}
} catch (e) {
console.error('refreshDashouMembership failed', e);
}
return app.globalData.clumber || [];
}
/** 用户是否持有某会员类型(与抢单卡片判断一致;支持总会员 covers) */
export function userHasHuiyuan(huiyuanList, huiyuanId) {
if (!huiyuanId) return true;
const list = huiyuanList || [];
const req = String(huiyuanId);
return list.some((h) => {
if (String(h.huiyuanid) === req || String(h.huiyuan_id) === req) return true;
const covers = h.included_huiyuan_ids || h.covers || [];
return Array.isArray(covers) && covers.some((id) => String(id) === req);
});
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,476 @@
// pages/order-pool/order-pool.js
const app = getApp();
import request from '../../utils/request.js';
import PopupService from '../../services/popupService.js';
Page({
data: {
// 1. 商品类型相关
shangpinleixing: [],
xuanzhongLeixingId: null,
// 2. 订单列表与分页相关
dingdanList: [],
page: 1,
pageSize: 10,
hasMore: true,
isLoading: false,
// 3. 全局状态字段
dashoustatus: null,
zhuanghaoStatus: null,
dashouzhuangtai: null,
uid: null,
huiyuanList: [],
yajin: 0,
jifen: 0,
// 4. 刷新控制字段
scrollViewRefreshing: false,
lastRefreshTime: 0,
refreshCooldown: 2000,
isLoadingMore: false,
// 5. 切换类型冷却时间控制
lastSwitchTime: 0,
switchCooldown: 1500,
isSwitching: false,
// 6. 全局OSS地址
ossImageUrl: app.globalData.ossImageUrl || '',
// 7. 授权状态控制
showUnauthorized: false,
unauthorizedMsg: '您尚未开启此功能',
// 商品轮播展示(只读 globalData,无额外接口)
lunboList: [],
},
async onLoad(options) {
this.syncShopBannerFromGlobal();
this.loadGlobalStatus();
this.checkAuthorization();
},
// 页面隐藏时清理弹窗视图
onHide: function () {
const popupComp = this.selectComponent('#popupNotice');
if (popupComp && popupComp.cleanup) {
popupComp.cleanup();
}
},
onShow() {
this.syncShopBannerFromGlobal();
this.registerNotificationComponent();
this.loadGlobalStatus();
this.checkAuthorization();
},
registerNotificationComponent() {
const notificationComp = this.selectComponent('#global-notification');
if (notificationComp && notificationComp.showNotification) {
app.globalData.globalNotification = {
show: (data) => notificationComp.showNotification(data),
hide: () => notificationComp.hideNotification()
};
}
},
/** 同步点单端已加载的商品轮播图,仅展示用 */
syncShopBannerFromGlobal() {
const oss = app.globalData.ossImageUrl || '';
const lunboList = (app.globalData.shangpinlunbo || []).map((url) => {
if (!url) return '';
return url.startsWith('http') ? url : oss + url;
}).filter(Boolean);
this.setData({ lunboList });
},
loadGlobalStatus() {
const globalData = app.globalData;
const dashoustatusCache = wx.getStorageSync('dashoustatus');
const dashoustatus = dashoustatusCache !== '' ? dashoustatusCache : null;
let zhuanghaoStatus = globalData.zhanghaoStatus;
if (zhuanghaoStatus === undefined || zhuanghaoStatus === null) {
zhuanghaoStatus = wx.getStorageSync('zhanghaoStatus');
}
this.setData({
dashoustatus: dashoustatus,
zhuanghaoStatus: zhuanghaoStatus,
dashouzhuangtai: globalData.dashouzhuangtai,
uid: wx.getStorageSync('uid'),
huiyuanList: globalData.clumber || [],
yajin: globalData.yajin || 0,
jifen: globalData.jinfen || 0
});
},
checkAuthorization() {
const { dashoustatus, zhuanghaoStatus } = this.data;
const isDashouValid = (dashoustatus && dashoustatus == 1) || false;
const isZhuanghaoValid = (zhuanghaoStatus == 1);
const isAuthorized = isDashouValid && isZhuanghaoValid;
if (!isAuthorized) {
this.setData({ showUnauthorized: true });
let msg = '';
if (!isDashouValid) msg = '您尚未开启身份';
this.setData({ unauthorizedMsg: msg || '无法使用抢单功能' });
} else {
this.setData({ showUnauthorized: false });
if (!this.data.shangpinleixing.length) {
this.loadShangpinLeixing();
} else if (this.data.xuanzhongLeixingId) {
this.loadDingdanList(true);
}
PopupService.checkAndShow(this, 'jiedanchi');
}
},
async loadShangpinLeixing() {
wx.showLoading({ title: '加载商品类型...' });
try {
const res = await request({
url: '/dingdan/dsqdhqddlx',
method: 'POST',
header: { 'content-type': 'application/json' }
});
if (res.statusCode === 200 && res.data) {
const data = res.data;
let list = [];
if (data.code === 200 || data.code === 0) {
if (data.data && Array.isArray(data.data.list)) {
list = data.data.list;
} else if (data.data && Array.isArray(data.data)) {
list = data.data;
} else if (Array.isArray(data.list)) {
list = data.list;
} else if (Array.isArray(data)) {
list = data;
}
}
const processedList = this.processTupianUrl(list);
this.setData({
shangpinleixing: processedList,
xuanzhongLeixingId: processedList[0]?.id || null
});
if (this.data.xuanzhongLeixingId) {
this.loadDingdanList(true);
}
} else {
wx.showToast({ title: res.data?.msg || '加载商品类型失败', icon: 'none', duration: 2000 });
}
} catch (error) {
console.error('加载商品类型失败:', error);
wx.showToast({ title: '网络错误,加载失败', icon: 'none', duration: 2000 });
} finally {
wx.hideLoading();
}
},
processTupianUrl(list) {
const ossUrl = app.globalData.ossImageUrl || '';
return list.map(item => ({
...item,
full_tupian_url: item.tupian_url && !item.tupian_url.startsWith('http') ? ossUrl + item.tupian_url : (item.tupian_url || '/images/default-type.png')
}));
},
async selectLeixing(e) {
const now = Date.now();
if (now - this.data.lastSwitchTime < this.data.switchCooldown) {
wx.showToast({
title: `操作太快,请${Math.ceil((this.data.switchCooldown - (now - this.data.lastSwitchTime)) / 1000)}秒后再试`,
icon: 'none',
duration: 1500
});
return;
}
if (this.data.isSwitching) return;
const leixingId = e.currentTarget.dataset.id;
if (leixingId === this.data.xuanzhongLeixingId) return;
this.setData({ isSwitching: true, lastSwitchTime: now });
wx.showLoading({ title: '切换中...' });
try {
this.setData({
xuanzhongLeixingId: leixingId,
dingdanList: [],
page: 1,
hasMore: true
});
await this.loadDingdanList(true);
} catch (error) {
console.error('切换类型失败:', error);
} finally {
this.setData({ isSwitching: false });
wx.hideLoading();
}
},
async loadDingdanList(isRefresh = false) {
if (this.data.isLoading || !this.data.xuanzhongLeixingId) return;
const loadPage = isRefresh ? 1 : this.data.page;
if (!isRefresh && !this.data.hasMore) return;
this.setData({
isLoading: true,
isLoadingMore: !isRefresh
});
try {
const res = await request({
url: '/dingdan/ddhq',
method: 'POST',
data: {
leixing_id: this.data.xuanzhongLeixingId,
page: loadPage,
page_size: this.data.pageSize
}
});
this.setData({
isLoading: false,
isLoadingMore: false,
scrollViewRefreshing: false
});
if (res.data.code === 200 || res.data.code === 0) {
const newList = res.data.data.list || [];
const hasMore = res.data.data.has_more || false;
const processedList = newList.map(item => this.processDingdanItem(item));
const updatedList = isRefresh ? processedList : [...this.data.dingdanList, ...processedList];
updatedList.sort((a, b) => (b.creat_time || '').localeCompare(a.creat_time || ''));
this.setData({
dingdanList: updatedList,
page: loadPage,
hasMore: hasMore
});
if (isRefresh) {
this.setData({ lastRefreshTime: Date.now() });
}
} else {
wx.showToast({ title: res.data.msg || '加载失败', icon: 'none' });
}
} catch (error) {
console.error('加载订单失败:', error);
this.setData({
isLoading: false,
isLoadingMore: false,
scrollViewRefreshing: false
});
wx.showToast({ title: '网络请求失败', icon: 'none' });
}
},
processDingdanItem(item) {
const ossUrl = app.globalData.ossImageUrl || '';
let fullTupianUrl = '';
if (item.tupian) {
fullTupianUrl = !item.tupian.startsWith('http') ? ossUrl + item.tupian : item.tupian;
} else {
const leixing = this.data.shangpinleixing.find(l => l.id === item.leixing_id);
fullTupianUrl = leixing ? leixing.full_tupian_url : '/images/default-order.png';
}
const isZhiding = item.zhuangtai === 7;
let zhidingAvatar = '';
if (item.zhiding_avatar) {
zhidingAvatar = !item.zhiding_avatar.startsWith('http') ? ossUrl + item.zhiding_avatar : item.zhiding_avatar;
}
const zhidingNicheng = item.zhiding_nicheng || '';
return {
...item,
full_tupian_url: fullTupianUrl,
isZhiding: isZhiding,
isPingtai: item.pingtai == 1,
isShangjia: item.pingtai == 2,
zhiding_avatar_full: zhidingAvatar,
zhiding_nicheng: zhidingNicheng
};
},
goToChongzhiPage(failureType, huiyuanId = null, requiredYajin = 0) {
let url = '/pages/fighter-recharge/fighter-recharge';
let params = {};
switch (failureType) {
case 'huiyuan':
url = '/pages/fighter-recharge/fighter-recharge';
params = { scrollTo: 'member' };
break;
case 'yajin':
url = '/pages/fighter-recharge/fighter-deposit';
params = { requiredYajin: requiredYajin };
break;
case 'jifen':
url = '/pages/fighter-recharge/fighter-recharge';
params = { scrollTo: 'jifen' };
break;
}
if (huiyuanId) params.huiyuanId = huiyuanId;
const queryString = Object.keys(params).map(key => `${key}=${encodeURIComponent(params[key])}`).join('&');
const fullUrl = `${url}?${queryString}`;
wx.navigateTo({
url: fullUrl,
fail: (err) => {
console.error('跳转失败:', err);
wx.showToast({ title: '跳转失败,请稍后重试', icon: 'none' });
}
});
},
async onQiangdanTap(e) {
const dingdanItem = e.currentTarget.dataset.item;
if (!dingdanItem) return;
if (!this.data.dashoustatus || this.data.dashoustatus != 1) {
wx.showToast({ title: '请先开启接单员身份', icon: 'none' });
return;
}
if (this.data.zhuanghaoStatus != 1) {
wx.showToast({ title: '账号异常,无法接单', icon: 'none' });
return;
}
if (this.data.dashouzhuangtai != 1) {
wx.showToast({ title: '您有订单正在接待中', icon: 'none' });
return;
}
if (dingdanItem.isZhiding) {
if (!this.data.uid || this.data.uid != dingdanItem.zhiding_uid) {
wx.showToast({ title: '此订单为指定单', icon: 'none' });
return;
}
}
if (dingdanItem.yaoqiuleixing == 1) {
const hasHuiyuan = this.data.huiyuanList.some(h => h.huiyuanid == dingdanItem.huiyuan_id);
if (!hasHuiyuan) {
wx.showToast({ title: '未开通对应会员,无法抢单', icon: 'none', duration: 2000 });
setTimeout(() => this.goToChongzhiPage('huiyuan', dingdanItem.huiyuan_id), 500);
return;
}
}
if (dingdanItem.yaoqiuleixing == 2) {
if (this.data.yajin < dingdanItem.yajin) {
wx.showToast({ title: `押金不足,需${dingdanItem.yajin}`, icon: 'none', duration: 2000 });
setTimeout(() => this.goToChongzhiPage('yajin', null, dingdanItem.yajin), 500);
return;
}
}
if (this.data.jifen < 5) {
wx.showToast({ title: '积分不足,至少需要5积分', icon: 'none', duration: 2000 });
setTimeout(() => this.goToChongzhiPage('jifen'), 500);
return;
}
const that = this;
wx.showModal({
title: '确认抢单',
content: `确定要抢此订单吗?${dingdanItem.isZhiding ? '(指定订单)' : ''}`,
success: async function (res) {
if (res.confirm) {
wx.showLoading({ title: '抢单中...', mask: true });
try {
const qiangdanRes = await request({
url: '/dingdan/qiangdan',
method: 'POST',
data: { dingdan_id: dingdanItem.dingdan_id }
});
wx.hideLoading();
if (qiangdanRes.data.code === 200 || qiangdanRes.data.code === 0) {
that.showQiangdanSuccessEffect();
if (app.globalData) {
app.globalData.dashouzhuangtai = 0;
that.setData({ dashouzhuangtai: 0 });
}
const newList = that.data.dingdanList.filter(item => item.dingdan_id !== dingdanItem.dingdan_id);
that.setData({ dingdanList: newList });
} else {
wx.showToast({ title: qiangdanRes.data.msg || '抢单失败', icon: 'none' });
}
} catch (error) {
wx.hideLoading();
console.error('抢单请求失败:', error);
wx.showToast({ title: '网络错误,抢单失败', icon: 'none' });
}
}
}
});
},
showQiangdanSuccessEffect() {
wx.showToast({ title: '抢单成功!', icon: 'success', duration: 2000 });
},
onViewDetail(e) {
const { type, content } = e.currentTarget.dataset;
wx.showModal({
title: type === 'jieshao' ? '订单介绍' : '订单备注',
content: content,
showCancel: false,
confirmText: '知道了'
});
},
onReachBottom() {
const now = Date.now();
if (now - this.data.lastRefreshTime < this.data.refreshCooldown) {
wx.showToast({
title: `操作太快,请${Math.ceil((this.data.refreshCooldown - (now - this.data.lastRefreshTime)) / 1000)}秒后再试`,
icon: 'none',
duration: 1500
});
return;
}
if (this.data.isLoading || this.data.isLoadingMore) return;
if (this.data.hasMore && !this.data.isLoading && this.data.xuanzhongLeixingId) {
this.setData({ page: this.data.page + 1 });
this.loadDingdanList(false);
}
},
onPullDownRefresh() {
const now = Date.now();
if (now - this.data.lastRefreshTime < this.data.refreshCooldown) {
this.setData({ scrollViewRefreshing: false });
wx.showToast({
title: `操作太快,请${Math.ceil((this.data.refreshCooldown - (now - this.data.lastRefreshTime)) / 1000)}秒后再试`,
icon: 'none',
duration: 1500
});
return;
}
if (this.data.isLoading) {
this.setData({ scrollViewRefreshing: false });
return;
}
this.setData({
scrollViewRefreshing: true,
lastRefreshTime: now
});
if (this.data.xuanzhongLeixingId) {
this.setData({ page: 1, hasMore: true });
this.loadDingdanList(true);
} else {
this.setData({ scrollViewRefreshing: false });
}
},
goToDashouRegister() {
wx.navigateTo({
url: '/pages/fighter/fighter'
});
}
});
+533 -121
View File
@@ -1,9 +1,25 @@
// pages/qiangdan/qiangdan.js
const app = getApp();
import request from '../../utils/request.js';
import { normalizeOrderTags } from '../../utils/order-tags.js';
import { refreshDashouMembership, userHasHuiyuan } from '../../utils/dashou-profile.js';
import { getDefaultAvatarUrl } from '../../utils/avatar.js';
import { warmupPindaoConfig } from '../../utils/miniapp-icons.js';
import PopupService from '../../services/popupService.js';
import { reconnectForRole } from '../../utils/role-tab-bar.js';
import { ensurePhoneAuth } from '../../utils/phone-auth.js';
import { fetchGonggaoLunbo, isGonggaoCacheValid, getCachedGonggaoLunbo } from '../../utils/display-config.js';
import { checkPenaltyForGrab } from '../../utils/grab-order-gate.js';
import { ensureRoleAgreement } from '../../utils/role-agreement-gate.js';
import { fetchMyZhidingOrders, submitZhidingResponse, processZhidingList, filterZhidingBannerList, ZHIDING_HF_MAP } from '../../utils/zhiding-order.js';
import { isFighterOnly, FIGHTER_MINE, isDashouCertified, canEnterAcceptOrder, kickToLoginPage } from '../../utils/fighter-only.js';
/** 成交指标芯片:有真实样本才展示,过滤假 0% */
import { buildDealStatChips } from '../../utils/deal-stat-display.js';
import { consumeBadgeFields, mergeTypePendingCounts } from '../../utils/dashou-badges.js';
/** 抢单池轮播 page_key(勿与点单页 order_pool 混用) */
const ACCEPT_ORDER_PAGE_KEY = 'accept_order';
Page({
data: {
@@ -30,7 +46,7 @@ Page({
// 4. 刷新控制字段
scrollViewRefreshing: false,
lastRefreshTime: 0,
refreshCooldown: 2000,
refreshCooldown: 0,
isLoadingMore: false,
// 5. 切换类型冷却
@@ -47,16 +63,142 @@ Page({
// 商品轮播展示(只读 globalData,无额外接口)
lunboList: [],
gonggao: '',
examRequired: false,
examPassed: false,
_examChecked: false,
myZhidingList: [],
highlightOrderId: '',
myZhidingCount: 0,
defaultAvatarUrl: '',
showZhidingDetail: false,
zhidingDetailOrder: null,
showLaterModal: false,
laterOrderId: '',
laterNote: '',
},
async onLoad(options) {
this.syncShopBannerFromGlobal();
// 未登录 / 未认证:立刻去登录页,禁止在抢单池空转卡死
if (isFighterOnly() && !canEnterAcceptOrder()) {
wx.reLaunch({ url: FIGHTER_MINE });
return;
}
const defAvatar = getDefaultAvatarUrl(app);
const highlight = (options && options.highlight) ? String(options.highlight) : '';
this.setData({ defaultAvatarUrl: defAvatar, highlightOrderId: highlight });
if (options && options.highlight) {
this._pendingHighlight = String(options.highlight);
}
if (app.globalData._acceptOrderSessionReady && app.globalData._acceptOrderPageCache) {
const cache = { ...app.globalData._acceptOrderPageCache };
this.data.shangpinleixing = cache.shangpinleixing || [];
const dingdanList = Array.isArray(cache.dingdanList)
? cache.dingdanList.map((item) => this.processDingdanItem(item))
: [];
this.setData({
...cache,
shangpinleixing: cache.shangpinleixing || [],
dingdanList,
bankuaiBiaoqian: cache.bankuaiBiaoqian || [],
lunboList: cache.lunboList || [],
});
if ((!cache.lunboList || cache.lunboList.length === 0) && !cache.gonggao && isGonggaoCacheValid(app, ACCEPT_ORDER_PAGE_KEY)) {
const gonggaoCached = getCachedGonggaoLunbo(app, ACCEPT_ORDER_PAGE_KEY);
const lunboList = this.processLunboUrls((gonggaoCached && gonggaoCached.shangpinlunbo) || []);
const gonggao = (gonggaoCached && gonggaoCached.shangpingonggao) || '';
this.setData({ lunboList, gonggao });
this.persistPageCache({ lunboList, gonggao });
}
this.loadGlobalStatus();
this.registerNotificationComponent();
await this.syncDashouProfileFromServer();
if (!this.data.shangpinleixing || this.data.shangpinleixing.length === 0) {
await this.loadShangpinLeixing(true, false, false);
} else if (this.data.xuanzhongLeixingId) {
await this.loadDingdanList(true, true);
}
this.persistPageCache();
this._skipShowRefresh = true;
await this.loadMyZhidingOrders(true);
this.tryShowAcceptOrderPopup();
return;
}
this.loadGlobalStatus();
await this.loadShangpinLeixing();
const banner = await this.loadGonggaoAndLunbo(false);
await this.syncDashouProfileFromServer();
await this.loadShangpinLeixing(true, false, false);
await this.loadMyZhidingOrders(true);
this.persistPageCache(banner);
app.globalData._acceptOrderSessionReady = true;
this.tryShowAcceptOrderPopup();
this._skipShowRefresh = true;
},
async tryShowAcceptOrderPopup() {
if (app.globalData._acceptOrderPopupDone) return;
// 先确保 OSS 配置已加载,再拉弹窗(否则相对 URL 拼不出图)
await this.ensureOssReadyForPopup(4000);
if (app.globalData._acceptOrderPopupDone) return;
app.globalData._acceptOrderPopupDone = true;
PopupService.checkAndShow(this, 'jiedan');
},
ensureOssReadyForPopup(timeoutMs = 3000) {
const hasOss = () => !!(app.globalData && app.globalData.ossImageUrl);
if (hasOss()) return Promise.resolve(true);
try {
const cached = app.readConfigFromStorage && app.readConfigFromStorage();
if (cached && app.applyDynamicConfig) {
app.applyDynamicConfig(cached);
}
} catch (e) {}
if (hasOss()) return Promise.resolve(true);
return new Promise((resolve) => {
let finished = false;
const done = () => {
if (finished) return;
finished = true;
if (app.off) app.off('configApplied', onCfg);
clearInterval(poll);
clearTimeout(to);
resolve(hasOss());
};
const onCfg = () => done();
if (app.on) app.on('configApplied', onCfg);
const poll = setInterval(() => { if (hasOss()) done(); }, 100);
const to = setTimeout(done, timeoutMs);
});
},
persistPageCache(extra = {}) {
const d = this.data;
app.globalData._acceptOrderPageCache = {
shangpinleixing: d.shangpinleixing,
xuanzhongLeixingId: d.xuanzhongLeixingId,
dingdanList: d.dingdanList,
page: d.page,
hasMore: d.hasMore,
bankuaiBiaoqian: d.bankuaiBiaoqian,
xuanzhongBiaoqianId: d.xuanzhongBiaoqianId,
lunboList: extra.lunboList ?? d.lunboList,
gonggao: extra.gonggao ?? d.gonggao,
lastRefreshTime: d.lastRefreshTime,
ossImageUrl: d.ossImageUrl,
};
},
onHide: function () {
// 只释放 PopupService 锁,不强制关掉正在展示的弹窗内容(避免刚拼好 URL 就被清掉)
PopupService.reset();
},
onUnload: function () {
const popupComp = this.selectComponent('#popupNotice');
if (popupComp && popupComp.cleanup) {
popupComp.cleanup();
@@ -64,65 +206,101 @@ Page({
},
async onShow() {
const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'dashou' });
if (!phoneOk) return;
if (isFighterOnly() && !canEnterAcceptOrder()) {
wx.reLaunch({ url: FIGHTER_MINE });
return;
}
// 每次进入抢单池都校验手机号认证
{
const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'dashou' });
if (!phoneOk) return;
}
this.syncShopBannerFromGlobal();
this.registerNotificationComponent();
this.loadGlobalStatus();
await this.syncDashouProfileFromServer();
warmupPindaoConfig(app);
if (wx.getStorageSync('uid')) {
// IM 延后连接,避免切 Tab 时卡顿;订单列表不自动刷新,由用户下拉刷新
setTimeout(() => {
reconnectForRole('dashou');
if (app.startImWhenReady) app.startImWhenReady();
}, 600);
reconnectForRole('dashou');
if (app.startImWhenReady) app.startImWhenReady();
}
await this.checkExamStatus(false);
this.setData({ defaultAvatarUrl: getDefaultAvatarUrl(app) });
await this.loadMyZhidingOrders(true);
const hl = app.globalData._acceptOrderHighlight;
if (hl) {
app.globalData._acceptOrderHighlight = '';
this.setData({ highlightOrderId: hl });
await this.loadMyZhidingOrders(true);
}
if (this._skipShowRefresh) {
this._skipShowRefresh = false;
return;
}
if (app.globalData._acceptOrderSessionReady && !this._silentRefreshRunning) {
this._silentRefreshRunning = true;
this.silentRefreshOnShow().finally(() => {
this._silentRefreshRunning = false;
});
}
},
/** 同步点单端已加载的商品轮播图,仅展示用 */
syncShopBannerFromGlobal() {
const oss = app.globalData.ossImageUrl || '';
const lunboList = (app.globalData.shangpinlunbo || []).map((url) => {
/** 进入页面静默刷新(无 loading 遮罩、不清空列表) */
async silentRefreshOnShow() {
try {
const banner = await this.loadGonggaoAndLunbo(true);
await this.syncDashouProfileFromServer();
const leixingId = this.data.xuanzhongLeixingId;
if (leixingId) {
await this.loadDingdanList(true, true);
await this.loadBankuaiBiaoqian();
}
await this.loadMyZhidingOrders(true);
this.loadGlobalStatus();
this.persistPageCache(banner);
} catch (e) {
console.error('静默刷新失败:', e);
}
},
/** 公告 + 轮播(首次/下拉强制拉接口,其余读 globalData 或页面缓存) */
processLunboUrls(urlList) {
const oss = app.globalData.ossImageUrl || this.data.ossImageUrl || '';
return (urlList || []).map((url) => {
if (!url) return '';
return url.startsWith('http') ? url : oss + url;
}).filter(Boolean);
this.setData({ lunboList });
},
async refreshDashouProfileSilent() {
try {
const res = await request({ url: '/yonghu/dashouxinxi', method: 'POST' });
if (!res || res.data.code != 200) return;
const data = res.data.data || {};
const patch = {
dashouNicheng: data.dashounicheng || '',
zhanghaoStatus: data.zhanghaostatus || '',
dashouzhuangtai: data.dashouzhuangtai || '',
yajin: data.yajin || 0,
jifen: data.jifen || 0,
clumber: data.clumber || [],
};
Object.assign(app.globalData, {
dashouNicheng: patch.dashouNicheng,
zhanghaoStatus: patch.zhanghaoStatus,
dashouzhuangtai: patch.dashouzhuangtai,
yajin: patch.yajin,
jinfen: patch.jifen,
clumber: patch.clumber,
});
if (data.dashoustatus !== undefined) {
wx.setStorageSync('dashoustatus', data.dashoustatus);
app.globalData.dashoustatus = data.dashoustatus;
async loadGonggaoAndLunbo(forceFetch = false) {
const cached = getCachedGonggaoLunbo(app, ACCEPT_ORDER_PAGE_KEY);
let lunboRaw = (cached && cached.shangpinlunbo) || [];
let gonggaoText = (cached && cached.shangpingonggao) || '';
if (forceFetch || !isGonggaoCacheValid(app, ACCEPT_ORDER_PAGE_KEY)) {
try {
const data = await fetchGonggaoLunbo(app, ACCEPT_ORDER_PAGE_KEY);
if (data) {
lunboRaw = data.shangpinlunbo || [];
gonggaoText = data.shangpingonggao || '';
}
} catch (e) {
const fallback = getCachedGonggaoLunbo(app, ACCEPT_ORDER_PAGE_KEY);
lunboRaw = (fallback && fallback.shangpinlunbo) || [];
gonggaoText = (fallback && fallback.shangpingonggao) || '';
}
this.setData({
zhuanghaoStatus: patch.zhanghaoStatus,
dashouzhuangtai: patch.dashouzhuangtai,
huiyuanList: patch.clumber,
yajin: patch.yajin,
jifen: patch.jifen,
});
} catch (e) {}
}
const lunboList = this.processLunboUrls(lunboRaw);
const gonggao = gonggaoText;
this.setData({ lunboList, gonggao });
return { lunboList, gonggao };
},
registerNotificationComponent() {
@@ -148,9 +326,36 @@ Page({
});
},
/** 拉取最新打手信息(会员/押金/积分),并刷新订单卡片上的会员展示 */
async syncDashouProfileFromServer() {
if (!wx.getStorageSync('token')) return;
try {
const res = await request({ url: '/yonghu/dashouxinxi', method: 'POST' });
if (res && res.data && res.data.code == 200 && res.data.data) {
const data = res.data.data;
if (Number(data.zhanghaostatus) !== 1) {
kickToLoginPage(
data.fengjin_yuanyin
? `账号状态异常,您的账号已被封禁。原因:${data.fengjin_yuanyin}`
: '账号状态异常,您的账号已被封禁,请联系客服'
);
return;
}
}
} catch (e) {}
await refreshDashouMembership(app);
this.loadGlobalStatus();
if (this.data.dingdanList && this.data.dingdanList.length) {
this.setData({
dingdanList: this.data.dingdanList.map((item) => this.processDingdanItem(item)),
});
}
},
// 加载商品类型
async loadShangpinLeixing() {
wx.showLoading({ title: '加载商品类型...' });
async loadShangpinLeixing(autoLoadOrders = false, preserveSelection = false, showLoading = true) {
if (showLoading) wx.showLoading({ title: '加载商品类型...' });
const prevId = this.data.xuanzhongLeixingId;
try {
const res = await request({
url: '/dingdan/dsqdhqddlx',
@@ -166,15 +371,18 @@ Page({
else if (Array.isArray(data.list)) list = data.list;
else if (Array.isArray(data)) list = data;
}
const processedList = this.processTupianUrl(list);
const processedList = mergeTypePendingCounts(this.processTupianUrl(list), data.data?.type_pending_counts);
consumeBadgeFields(data.data || {});
const firstId = processedList[0]?.id || null;
const keepPrev = preserveSelection && prevId && processedList.some((i) => i.id === prevId);
const selectedId = keepPrev ? prevId : firstId;
this.setData({
shangpinleixing: processedList,
xuanzhongLeixingId: processedList[0]?.id || null
xuanzhongLeixingId: selectedId,
});
if (this.data.xuanzhongLeixingId) {
this.loadDingdanList(true);
// 🆕 加载板块标签
this.loadBankuaiBiaoqian();
if (autoLoadOrders && selectedId) {
await this.loadDingdanList(true);
await this.loadBankuaiBiaoqian();
}
} else {
wx.showToast({ title: res.data?.msg || '加载商品类型失败', icon: 'none' });
@@ -182,7 +390,7 @@ Page({
} catch (error) {
wx.showToast({ title: '网络错误,加载失败', icon: 'none' });
} finally {
wx.hideLoading();
if (showLoading) wx.hideLoading();
}
},
@@ -249,8 +457,8 @@ Page({
xuanzhongBiaoqianId: 0 // 重置标签筛选
});
await this.loadDingdanList(true);
// 🆕 切换类型后加载新标签
await this.loadBankuaiBiaoqian();
this.persistPageCache();
} catch (error) {
console.error('切换类型失败:', error);
} finally {
@@ -270,19 +478,24 @@ Page({
hasMore: true
});
await this.loadDingdanList(true);
this.persistPageCache();
},
// 加载订单列表
async loadDingdanList(isRefresh = false) {
if (this.data.isLoading || !this.data.xuanzhongLeixingId) return;
async loadDingdanList(isRefresh = false, silent = false) {
if ((!silent && this.data.isLoading) || !this.data.xuanzhongLeixingId) return;
if (silent && this._listRequesting) return;
const loadPage = isRefresh ? 1 : this.data.page;
if (!isRefresh && !this.data.hasMore) return;
this.setData({
isLoading: true,
isLoadingMore: !isRefresh
});
if (!silent) {
this.setData({
isLoading: true,
isLoadingMore: !isRefresh,
});
}
this._listRequesting = true;
try {
const res = await request({
@@ -302,11 +515,18 @@ Page({
isLoadingMore: false,
scrollViewRefreshing: false
});
this._listRequesting = false;
if (res.data.code === 200 || res.data.code === 0) {
const newList = res.data.data.list || [];
const hasMore = res.data.data.has_more || false;
const serverClumber = res.data.data.clumber;
const payload = res.data.data || {};
consumeBadgeFields(payload);
if (payload.type_pending_counts) {
const merged = mergeTypePendingCounts(this.data.shangpinleixing, payload.type_pending_counts);
this.setData({ shangpinleixing: merged });
}
const newList = payload.list || [];
const hasMore = payload.has_more || false;
const serverClumber = payload.clumber;
if (Array.isArray(serverClumber) && serverClumber.length) {
app.globalData.clumber = serverClumber;
this.setData({ huiyuanList: serverClumber });
@@ -322,6 +542,7 @@ Page({
page: loadPage,
hasMore: hasMore
});
this.persistPageCache();
if (isRefresh) {
this.setData({ lastRefreshTime: Date.now() });
@@ -336,25 +557,36 @@ Page({
isLoadingMore: false,
scrollViewRefreshing: false
});
wx.showToast({ title: '网络请求失败', icon: 'none' });
this._listRequesting = false;
if (!silent) {
wx.showToast({ title: '网络请求失败', icon: 'none' });
}
}
},
// 处理单条订单数据(包含标签、会员判断)
processDingdanItem(item) {
const ossUrl = app.globalData.ossImageUrl || '';
const leixing = this.data.shangpinleixing.find((l) => l.id == item.leixing_id);
let fullTupianUrl = '';
if (item.tupian) {
fullTupianUrl = !item.tupian.startsWith('http') ? ossUrl + item.tupian : item.tupian;
// 卡片左上角优先用订单类型图,没有再退回默认图(不用商品图/头像顶替类型)
if (leixing && leixing.full_tupian_url) {
fullTupianUrl = leixing.full_tupian_url;
} else if (leixing && leixing.tupian_url) {
fullTupianUrl = leixing.tupian_url.startsWith('http')
? leixing.tupian_url
: ossUrl + leixing.tupian_url;
} else {
const leixing = this.data.shangpinleixing.find(l => l.id === item.leixing_id);
fullTupianUrl = leixing ? leixing.full_tupian_url : '/images/default-order.png';
fullTupianUrl = '/images/default-order.png';
}
const isZhiding = item.zhuangtai === 7;
const uid = this.data.uid;
const isMyZhiding = isZhiding && uid && String(item.zhiding_uid) === String(uid);
let zhidingAvatar = '';
const defAvatar = getDefaultAvatarUrl(app);
let zhidingAvatar = defAvatar;
if (item.zhiding_avatar) {
zhidingAvatar = !item.zhiding_avatar.startsWith('http')
? ossUrl + item.zhiding_avatar
@@ -362,33 +594,49 @@ Page({
}
const zhidingNicheng = item.zhiding_nicheng || '';
// 🆕 判断用户是否有订单所需的会员(优先后端 hasRequiredMember)
// 订单要求指定会员时,核对用户是否持有该会员类型(优先后端字段)
let hasRequiredMember = true;
if (item.hasRequiredMember !== undefined) {
hasRequiredMember = !!item.hasRequiredMember;
} else if (item.has_required_member !== undefined) {
hasRequiredMember = !!item.has_required_member;
} else if (item.yaoqiuleixing == 1 && item.huiyuan_id) {
const userHasIt = this.data.huiyuanList.some(h =>
(h.huiyuanid == item.huiyuan_id) || (h.huiyuan_id == item.huiyuan_id)
);
hasRequiredMember = userHasIt;
hasRequiredMember = userHasHuiyuan(this.data.huiyuanList, item.huiyuan_id);
}
let shangjiaAvatar = getDefaultAvatarUrl(app);
const sjAvatar = (item.sj_avatar || '').trim();
if (sjAvatar) {
shangjiaAvatar = sjAvatar.startsWith('http') ? sjAvatar : ossUrl + sjAvatar;
}
const tags = normalizeOrderTags(item)
const dealStatChips = buildDealStatChips(item.shangjia_deal_stat, {
dealLabel: '成交率',
fineLabel: '罚款率',
});
return {
...item,
full_tupian_url: fullTupianUrl,
leixing_icon_url: fullTupianUrl,
shangjia_avatar_full: shangjiaAvatar,
isZhiding: isZhiding,
isMyZhiding,
isHighlighted: this.data.highlightOrderId && item.dingdan_id === this.data.highlightOrderId,
zhiding_hf: item.zhiding_hf,
zhiding_hf_text: item.zhiding_hf_text || ZHIDING_HF_MAP[item.zhiding_hf] || '',
zhiding_dhj_sm: item.zhiding_dhj_sm || '',
isPingtai: item.pingtai == 1,
isShangjia: item.pingtai == 2,
zhiding_avatar_full: zhidingAvatar,
zhiding_nicheng: zhidingNicheng,
// 🆕 三层标签(后端返回,直接保留)
xuqiu_biaoqian: item.xuqiu_biaoqian || [],
dashou_biaoqian: item.dashou_biaoqian || [],
shangjia_biaoqian: item.shangjia_biaoqian || [],
...tags,
shangjia_youzhi: !!item.shangjia_youzhi,
// 🆕 是否有查看价格的权限
hasRequiredMember: hasRequiredMember,
dealStatChips,
};
},
@@ -399,13 +647,16 @@ Page({
switch (failureType) {
case 'huiyuan':
params = { needScroll: '0', scrollTo: 'member' };
url = '/pages/fighter-recharge/fighter-recharge';
params = { scrollTo: 'member' };
break;
case 'yajin':
params = { needScroll: '1', scrollTo: 'bottom', requiredYajin: requiredYajin };
url = '/pages/fighter-recharge/fighter-deposit';
params = { requiredYajin: requiredYajin };
break;
case 'jifen':
params = { needScroll: '1', scrollTo: 'bottom' };
url = '/pages/fighter-recharge/fighter-recharge';
params = { scrollTo: 'jifen' };
break;
}
@@ -424,11 +675,63 @@ Page({
});
},
/** 抢单前考试状态(仅更新状态,不自动跳转考试页) */
async checkExamStatus() {
try {
const res = await request({
url: '/jituan/dashou-exam/status',
method: 'POST',
header: { 'content-type': 'application/json' },
});
const body = res?.data;
if (body && (body.code === 200 || body.code === 0) && body.data) {
const d = body.data;
this.setData({
examRequired: !!d.exam_required,
examPassed: !!d.exam_passed,
});
}
} catch (e) {
console.error('考试状态检查失败', e);
}
return true;
},
// 抢单按钮(原封不动)
async onQiangdanTap(e) {
const dingdanItem = e.currentTarget.dataset.item;
if (!dingdanItem) return;
const penalty = await checkPenaltyForGrab();
if (penalty.blocked) {
wx.showToast({
title: penalty.msg,
icon: 'none',
duration: 2000,
});
setTimeout(() => {
wx.navigateTo({ url: penalty.url });
}, 500);
return;
}
await this.checkExamStatus();
if (this.data.examRequired && !this.data.examPassed) {
wx.showModal({
title: '须通过接单考试',
content: '抢单前需先通过接单考试,是否现在去考试?',
confirmText: '去考试',
cancelText: '暂不',
success: (r) => {
if (r.confirm) {
wx.navigateTo({ url: '/pages/dashou-exam/dashou-exam' });
}
},
});
return;
}
// 校验1: 打手身份
if (!this.data.dashoustatus || this.data.dashoustatus != 1) {
wx.showToast({ title: '请先开启接单员身份', icon: 'none' });
@@ -453,9 +756,7 @@ Page({
}
// 校验5: 会员要求
if (dingdanItem.yaoqiuleixing == 1) {
const hasHuiyuan = this.data.huiyuanList.some(h =>
(h.huiyuanid == dingdanItem.huiyuan_id) || (h.huiyuan_id == dingdanItem.huiyuan_id)
);
const hasHuiyuan = userHasHuiyuan(this.data.huiyuanList, dingdanItem.huiyuan_id);
if (!hasHuiyuan) {
wx.showToast({
title: '未开通对应会员,无法抢单',
@@ -495,6 +796,9 @@ Page({
return;
}
const signed = await ensureRoleAgreement('dashou', 'qiangdan');
if (!signed) return;
// 确认框
const that = this;
wx.showModal({
@@ -521,6 +825,8 @@ Page({
item => item.dingdan_id !== dingdanItem.dingdan_id
);
that.setData({ dingdanList: newList });
that.persistPageCache();
that.loadMyZhidingOrders(true);
} else {
wx.showToast({
title: qiangdanRes.data.msg || '抢单失败',
@@ -551,43 +857,138 @@ Page({
});
},
onReachBottom() {
const now = Date.now();
const lastTime = this.data.lastRefreshTime;
const cooldown = this.data.refreshCooldown;
async loadMyZhidingOrders(silent = false) {
if (!this.data.uid && !wx.getStorageSync('uid')) return;
try {
const raw = await fetchMyZhidingOrders();
const uid = this.data.uid || wx.getStorageSync('uid');
const highlight = this.data.highlightOrderId || this._pendingHighlight || '';
const list = filterZhidingBannerList(processZhidingList(raw, app, uid, highlight));
this.setData({ myZhidingList: list, myZhidingCount: list.length });
if (highlight && list.some((o) => o.dingdan_id === highlight)) {
this.setData({ highlightOrderId: highlight });
this._pendingHighlight = '';
}
} catch (e) {
if (!silent) console.warn('loadMyZhidingOrders', e);
}
},
if (now - lastTime < cooldown) {
wx.showToast({
title: `操作太快,请${Math.ceil((cooldown - (now - lastTime)) / 1000)}秒后再试`,
icon: 'none',
duration: 1500
});
openZhidingDetail(e) {
const item = e.currentTarget.dataset.item;
if (!item) return;
this.setData({ showZhidingDetail: true, zhidingDetailOrder: item });
},
closeZhidingDetail() {
this.setData({ showZhidingDetail: false, zhidingDetailOrder: null });
},
async onZhidingReject(e) {
const item = e.currentTarget.dataset.item;
if (!item) return;
wx.showModal({
title: '确认婉拒',
content: '婉拒后订单将退回抢单大厅,确定不想接此单吗?',
confirmText: '不想接',
confirmColor: '#e64340',
success: async (res) => {
if (!res.confirm) return;
wx.showLoading({ title: '提交中...', mask: true });
try {
const body = await submitZhidingResponse(item.dingdan_id, 2);
wx.hideLoading();
if (body && (body.code === 200 || body.code === 0)) {
wx.showToast({ title: body.msg || '已婉拒', icon: 'none' });
this.closeZhidingDetail();
await this.loadMyZhidingOrders(true);
await this.loadDingdanList(true, true);
} else {
wx.showToast({ title: body?.msg || '操作失败', icon: 'none' });
}
} catch (err) {
wx.hideLoading();
wx.showToast({ title: '网络错误', icon: 'none' });
}
},
});
},
onZhidingLater(e) {
const item = e.currentTarget.dataset.item;
if (!item) return;
this.setData({
showZhidingDetail: false,
zhidingDetailOrder: null,
showLaterModal: true,
laterOrderId: item.dingdan_id,
laterNote: item.zhiding_dhj_sm || '',
});
},
closeLaterModal() {
this.setData({ showLaterModal: false, laterOrderId: '', laterNote: '' });
},
onLaterNoteInput(e) {
this.setData({ laterNote: e.detail.value });
},
async confirmZhidingLater() {
const { laterOrderId, laterNote } = this.data;
if (!laterNote.trim()) {
wx.showToast({ title: '请填写预计接待时间', icon: 'none' });
return;
}
wx.showLoading({ title: '提交中...', mask: true });
try {
const body = await submitZhidingResponse(laterOrderId, 3, laterNote.trim());
wx.hideLoading();
if (body && (body.code === 200 || body.code === 0)) {
const note = laterNote.trim();
wx.showToast({ title: `已记录:${note}`, icon: 'none', duration: 3500 });
this.closeLaterModal();
this.closeZhidingDetail();
await this.loadMyZhidingOrders(true);
await this.loadDingdanList(true, true);
} else {
wx.showToast({ title: body?.msg || '操作失败', icon: 'none' });
}
} catch (e) {
wx.hideLoading();
wx.showToast({ title: '网络错误', icon: 'none' });
}
},
async onZhidingAccept(e) {
const item = e.currentTarget.dataset.item;
if (!item) return;
wx.showLoading({ title: '处理中...', mask: true });
try {
await submitZhidingResponse(item.dingdan_id, 1);
wx.hideLoading();
} catch (e) {
wx.hideLoading();
}
const processed = this.processDingdanItem(item);
this.onQiangdanTap({ currentTarget: { dataset: { item: processed } } });
},
noop() {},
goToComplaintCreate() {
wx.navigateTo({ url: '/pages/gongdan/create/create' });
},
onReachBottom() {
if (this.data.isLoading || this.data.isLoadingMore) return;
if (this.data.hasMore && !this.data.isLoading && this.data.xuanzhongLeixingId) {
this.setData({ page: this.data.page + 1 });
this.loadDingdanList(false);
}
},
onPullDownRefresh() {
const now = Date.now();
const lastTime = this.data.lastRefreshTime;
const cooldown = this.data.refreshCooldown;
if (now - lastTime < cooldown) {
this.setData({ scrollViewRefreshing: false });
wx.showToast({
title: `操作太快,请${Math.ceil((cooldown - (now - lastTime)) / 1000)}秒后再试`,
icon: 'none',
duration: 1500
});
return;
}
async onPullDownRefresh() {
if (this.data.isLoading) {
this.setData({ scrollViewRefreshing: false });
return;
@@ -595,15 +996,26 @@ Page({
this.setData({
scrollViewRefreshing: true,
lastRefreshTime: now,
lastRefreshTime: Date.now(),
page: 1,
hasMore: true
hasMore: true,
});
if (this.data.xuanzhongLeixingId) {
this.refreshDashouProfileSilent();
this.loadDingdanList(true);
} else {
try {
const banner = await this.loadGonggaoAndLunbo(true);
await this.syncDashouProfileFromServer();
await this.loadShangpinLeixing(false, true, false);
const leixingId = this.data.xuanzhongLeixingId;
if (leixingId) {
await this.loadBankuaiBiaoqian();
await this.loadDingdanList(true);
}
await this.loadMyZhidingOrders(true);
this.loadGlobalStatus();
this.persistPageCache(banner);
} catch (e) {
console.error('下拉刷新失败:', e);
} finally {
this.setData({ scrollViewRefreshing: false });
}
}
+7 -4
View File
@@ -1,12 +1,15 @@
{
"navigationBarTitleText": "抢单大厅",
"navigationBarBackgroundColor": "#c4b5fd",
"navigationBarTitleText": "抢单列表",
"navigationBarBackgroundColor": "#f7dc51",
"backgroundColorTop": "#f7dc51",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": false,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f3ff",
"backgroundColor": "#fff8e1",
"usingComponents": {
"chenghao-tag": "/components/chenghao-tag/chenghao-tag",
"global-notification": "/components/global-notification/global-notification"
"global-notification": "/components/global-notification/global-notification",
"kefu-float": "/components/kefu-float/kefu-float",
"popup-notice": "/components/popup-notice/popup-notice"
}
}
+235 -44
View File
@@ -18,7 +18,7 @@
refresher-default-style="black"
refresher-background="#f5f3ff"
refresher-background="#fff8e1"
refresher-triggered="{{scrollViewRefreshing}}"
@@ -38,6 +38,44 @@
<!-- 公告置顶 -->
<view class="tousu-top-bar" catchtap="goToComplaintCreate">
<text class="tousu-top-bar-txt">投诉 / 反馈</text>
<text class="tousu-top-bar-sub">点这里提交工单</text>
<text class="tousu-top-bar-arrow">›</text>
</view>
<view class="xym-gonggao-bar" wx:if="{{gonggao}}">
<image class="xym-gonggao-ico" src="https://bintao.xmxym88.com/xcx/bintao/38.png" mode="aspectFit"/>
<text class="xym-gonggao-txt">{{gonggao}}</text>
</view>
<!-- 被指定订单提醒 -->
<view class="my-zhiding-banner" wx:if="{{myZhidingList.length > 0}}">
<view class="my-zhiding-banner-head">
<view class="my-zhiding-head-left">
<text class="my-zhiding-badge">指定给您</text>
<text class="my-zhiding-count-num">{{myZhidingCount || myZhidingList.length}}单</text>
</view>
<text class="my-zhiding-sub">请确认是否接待</text>
</view>
<view wx:for="{{myZhidingList}}" wx:key="dingdan_id" class="my-zhiding-card {{item.isHighlighted ? 'my-zhiding-card--highlight' : ''}}">
<view class="my-zhiding-card-main" data-item="{{item}}" bindtap="openZhidingDetail">
<view class="my-zhiding-card-top">
<text class="my-zhiding-order-id">单号 {{item.dingdan_id}}</text>
<text class="my-zhiding-reward">¥{{item.dashou_fencheng || 0}}</text>
</view>
<text class="my-zhiding-desc line2">{{item.jieshao || '暂无介绍'}}</text>
<text class="my-zhiding-status" wx:if="{{item.zhiding_hf_text}}">您的回复:{{item.zhiding_hf_text}}{{item.zhiding_dhj_sm ? ' · ' + item.zhiding_dhj_sm : ''}}</text>
</view>
<view class="my-zhiding-actions">
<view class="zhiding-act zhiding-act--ghost" data-item="{{item}}" catchtap="onZhidingReject">不想接</view>
<view class="zhiding-act zhiding-act--ghost" data-item="{{item}}" catchtap="onZhidingLater">等会接</view>
<view class="zhiding-act zhiding-act--primary" data-item="{{item}}" catchtap="onZhidingAccept">立即接待</view>
</view>
</view>
</view>
<!-- 商品轮播(只读 globalData,与点单端同源) -->
<view class="xym-lunbo-container" wx:if="{{lunboList.length > 0}}">
@@ -50,7 +88,7 @@
indicator-color="rgba(255,255,255,0.6)"
indicator-active-color="#9333ea"
indicator-active-color="#ffd061"
autoplay="{{lunboList.length > 1}}"
@@ -74,11 +112,11 @@
</view>
<view class="xym-section-gap" wx:if="{{lunboList.length > 0}}"></view>
<!-- 商品类型选择 -->
<view class="leixing-quyu">
<view class="leixing-quyu dingdanxiang1 filter-card mx20">
<scroll-view class="leixing-scroll" scroll-x enhanced show-scrollbar="{{false}}">
@@ -98,7 +136,10 @@
<view wx:if="{{xuanzhongLeixingId == item.id}}" class="guangyun-effect"></view>
<image class="leixing-tupian" src="{{item.full_tupian_url}}" mode="aspectFill"/>
<view class="leixing-pic-wrap">
<image class="leixing-tupian" src="{{item.full_tupian_url}}" mode="aspectFill"/>
<view class="leixing-badge" wx:if="{{item.pending_badge}}">{{item.pending_badge}}</view>
</view>
<text class="leixing-jieshao">{{item.jieshao}}</text>
@@ -176,7 +217,7 @@
<!-- 平台订单(逍遥梦卡片布局) -->
<view wx:if="{{item.isPingtai}}" class="xym-order-item xym-pingtai-order" data-item="{{item}}">
<view wx:if="{{item.isPingtai}}" class="xym-order-item xym-pingtai-order {{item.isMyZhiding ? 'order-my-zhiding' : ''}} {{item.isHighlighted ? 'order-zhiding-highlight' : ''}}" data-item="{{item}}">
<view class="xym-pingtai-head flexb">
@@ -190,16 +231,6 @@
<view class="order-con">
<view wx:if="{{item.isZhiding}}" class="zhiding-strip myflex light">
<text class="zhiding-label">指定</text>
<image wx:if="{{item.zhiding_avatar_full}}" class="zhiding-avatar" src="{{item.zhiding_avatar_full}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<view class="order-row myflex">
<image class="o-logo" src="{{item.full_tupian_url}}" mode="aspectFill"/>
@@ -212,6 +243,20 @@
</view>
<block wx:if="{{item.isZhiding}}">
<view class="zhiding-strip light">
<view class="zhiding-strip-top myflex">
<text class="zhiding-label">指定</text>
<image class="zhiding-avatar" src="{{item.zhiding_avatar_full || defaultAvatarUrl}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<view wx:if="{{item.dashou_biaoqian.length > 0 || item.zhiding_identity_biaoqian.length > 0}}" class="zhiding-strip-tags">
<chenghao-tag wx:for="{{item.dashou_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
<chenghao-tag wx:for="{{item.zhiding_identity_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</view>
</block>
<view class="reward-badge platform-reward" wx:if="{{item.hasRequiredMember}}">
<text class="reward-yen sm">¥</text>
@@ -226,9 +271,9 @@
</view>
<view wx:if="{{item.beizhu}}" class="remark-block gold" data-type="beizhu" data-content="{{item.beizhu}}" bindtap="onViewDetail">
<view class="remark-block gold" data-type="beizhu" data-content="{{item.beizhu}}" bindtap="onViewDetail">
<text class="remark-label">备注:</text>{{item.beizhu}}
<text class="remark-label">备注:</text>{{item.beizhu || '暂无'}}
</view>
@@ -236,10 +281,10 @@
<text class="xuqiu-label">需求标签</text>
<scroll-view class="biaoqian-inline-scroll" scroll-x>
<chenghao-tag wx:for="{{item.xuqiu_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
<scroll-view class="biaoqian-inline-scroll" scroll-x enable-flex>
<view class="biaoqian-inline-inner">
<chenghao-tag wx:for="{{item.xuqiu_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</scroll-view>
</view>
@@ -248,7 +293,7 @@
<text class="o-amt light">平台派单</text>
<view class="qiangdan flexa btn-bg btn-qiang btn-qiang-brown" data-item="{{item}}" bindtap="onQiangdanTap">抢单</view>
<view class="qiangdan flexa btn-bg btn-qiang btn-qiang-brown" data-item="{{item}}" bindtap="onQiangdanTap">{{item.isMyZhiding ? '立即接待' : '抢单'}}</view>
</view>
@@ -260,9 +305,87 @@
<!-- 优质商家订单(金牌卡片) -->
<view wx:elif="{{item.isShangjia && item.shangjia_youzhi}}" class="xym-order-item gold-card {{item.isMyZhiding ? 'order-my-zhiding' : ''}} {{item.isHighlighted ? 'order-zhiding-highlight' : ''}}" data-item="{{item}}">
<view class="gold-head flexb">
<view class="kehuduan-banner-wrap">
<image class="gold-banner" src="https://bintao.xmxym88.com/xcx/bintao/82.png" mode="widthFix"/>
</view>
</view>
<view class="gold-body-wrap">
<view class="order-con gold-inner">
<view class="order-row myflex">
<image class="o-logo" src="{{item.leixing_icon_url}}" mode="aspectFill"/>
<view class="o-col">
<view class="o-goods line2" data-type="jieshao" data-content="{{item.jieshao}}" bindtap="onViewDetail">{{item.jieshao || '暂无介绍'}}</view>
<view class="tag-row myflex">
<text class="tag-pill tag-youzhi">优质商家</text>
</view>
</view>
</view>
<block wx:if="{{item.isZhiding}}">
<view class="zhiding-strip">
<view class="zhiding-strip-top myflex">
<text class="zhiding-label">指定</text>
<image class="zhiding-avatar" src="{{item.zhiding_avatar_full || defaultAvatarUrl}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<view wx:if="{{item.dashou_biaoqian.length > 0 || item.zhiding_identity_biaoqian.length > 0}}" class="zhiding-strip-tags">
<chenghao-tag wx:for="{{item.dashou_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
<chenghao-tag wx:for="{{item.zhiding_identity_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</view>
</block>
<view class="reward-badge normal-reward gold-reward" wx:if="{{item.hasRequiredMember}}">
<text class="reward-yen sm">¥</text>
<text class="reward-num dark">{{item.dashou_fencheng || 0}}</text>
</view>
<view class="reward-badge member-tip" wx:else>
<text class="reward-tip">未开通会员</text>
</view>
<view class="remark-block dark" data-type="beizhu" data-content="{{item.beizhu}}" bindtap="onViewDetail">
<text class="remark-label dark-t">商家备注:</text>{{item.beizhu || '暂无'}}
</view>
<view wx:if="{{item.shangjia_biaoqian.length > 0 || item.shangjia_identity_biaoqian.length > 0 || item.xuqiu_biaoqian.length > 0}}" class="xuqiu-tags-row">
<scroll-view wx:if="{{item.shangjia_biaoqian.length > 0}}" class="biaoqian-inline-scroll" scroll-x enable-flex>
<view class="biaoqian-inline-inner">
<chenghao-tag wx:for="{{item.shangjia_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</scroll-view>
<scroll-view wx:if="{{item.shangjia_identity_biaoqian.length > 0}}" class="biaoqian-inline-scroll" scroll-x enable-flex style="margin-top:8rpx">
<view class="biaoqian-inline-inner">
<chenghao-tag wx:for="{{item.shangjia_identity_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</scroll-view>
<scroll-view wx:if="{{item.xuqiu_biaoqian.length > 0}}" class="biaoqian-inline-scroll" scroll-x enable-flex style="margin-top:8rpx">
<view class="biaoqian-inline-inner">
<chenghao-tag wx:for="{{item.xuqiu_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</scroll-view>
</view>
<view class="merchant-plain flexb">
<view class="merchant-user myflex">
<image class="m-avatar" src="{{item.shangjia_avatar_full}}" mode="aspectFill"/>
<view class="m-info">
<view class="m-name m-name-light line1">{{item.sjnicheng || '未知商家'}}</view>
<view class="m-sn m-sn-light">发布于 {{item.creat_time}}</view>
<view class="merchant-stat-row" wx:if="{{item.dealStatChips.length}}">
<text class="merchant-stat {{c.hl ? 'highlight' : ''}}" wx:for="{{item.dealStatChips}}" wx:for-item="c" wx:key="k">{{c.t}}</text>
</view>
</view>
</view>
<view class="qiangdan flexa btn-bg btn-qiang" data-item="{{item}}" bindtap="onQiangdanTap">{{item.isMyZhiding ? '立即接待' : '抢单'}}</view>
</view>
<view class="o-foot flexb kehuduan-foot gold-foot-text-only">
<text class="o-amt-grey">商家派单</text>
</view>
</view>
</view>
</view>
<!-- 商家订单(逍遥梦卡片布局) -->
<view wx:elif="{{item.isShangjia}}" class="xym-order-item xym-shangjia-order" data-item="{{item}}">
<view wx:elif="{{item.isShangjia}}" class="xym-order-item xym-shangjia-order {{item.isMyZhiding ? 'order-my-zhiding' : ''}} {{item.isHighlighted ? 'order-zhiding-highlight' : ''}}" data-item="{{item}}">
<view class="xym-shangjia-head flexb">
@@ -276,21 +399,30 @@
<view class="order-con">
<view wx:if="{{item.isZhiding}}" class="zhiding-strip myflex">
<text class="zhiding-label">指定</text>
<image wx:if="{{item.zhiding_avatar_full}}" class="zhiding-avatar" src="{{item.zhiding_avatar_full}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<block wx:if="{{item.isZhiding}}">
<view class="zhiding-strip">
<view class="zhiding-strip-top myflex">
<text class="zhiding-label">指定</text>
<image class="zhiding-avatar" src="{{item.zhiding_avatar_full || defaultAvatarUrl}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<view wx:if="{{item.dashou_biaoqian.length > 0 || item.zhiding_identity_biaoqian.length > 0}}" class="zhiding-strip-tags">
<chenghao-tag wx:for="{{item.dashou_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
<chenghao-tag wx:for="{{item.zhiding_identity_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</view>
</block>
<view class="merchant-line myflex">
<image class="m-avatar-sm" src="{{item.full_tupian_url}}" mode="aspectFill"/>
<image class="m-avatar-sm" src="{{item.shangjia_avatar_full}}" mode="aspectFill"/>
<text class="m-name-sm">{{item.sjnicheng || '未知商家'}}</text>
<view class="m-meta-col">
<text class="m-name-sm">{{item.sjnicheng || '未知商家'}}</text>
<view class="merchant-stat-row" wx:if="{{item.dealStatChips.length}}">
<text class="merchant-stat {{c.hl ? 'highlight' : ''}}" wx:for="{{item.dealStatChips}}" wx:for-item="c" wx:key="k">{{c.t}}</text>
</view>
</view>
</view>
@@ -318,24 +450,33 @@
</view>
<view wx:if="{{item.beizhu}}" class="remark-block dark" data-type="beizhu" data-content="{{item.beizhu}}" bindtap="onViewDetail">
<view class="remark-block dark" data-type="beizhu" data-content="{{item.beizhu}}" bindtap="onViewDetail">
<text class="remark-label">商家备注:</text>{{item.beizhu}}
<text class="remark-label">商家备注:</text>{{item.beizhu || '暂无'}}
</view>
<view wx:if="{{item.shangjia_biaoqian.length > 0 || item.xuqiu_biaoqian.length > 0}}" class="xuqiu-tags-row">
<view wx:if="{{item.shangjia_biaoqian.length > 0 || item.shangjia_identity_biaoqian.length > 0 || item.xuqiu_biaoqian.length > 0}}" class="xuqiu-tags-row">
<scroll-view wx:if="{{item.shangjia_biaoqian.length > 0}}" class="biaoqian-inline-scroll" scroll-x>
<chenghao-tag wx:for="{{item.shangjia_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
<scroll-view wx:if="{{item.shangjia_biaoqian.length > 0}}" class="biaoqian-inline-scroll" scroll-x enable-flex>
<view class="biaoqian-inline-inner">
<chenghao-tag wx:for="{{item.shangjia_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</scroll-view>
<scroll-view wx:if="{{item.xuqiu_biaoqian.length > 0}}" class="biaoqian-inline-scroll" scroll-x style="margin-top:8rpx">
<scroll-view wx:if="{{item.shangjia_identity_biaoqian.length > 0}}" class="biaoqian-inline-scroll" scroll-x enable-flex style="margin-top:8rpx">
<chenghao-tag wx:for="{{item.xuqiu_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
<view class="biaoqian-inline-inner">
<chenghao-tag wx:for="{{item.shangjia_identity_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</scroll-view>
<scroll-view wx:if="{{item.xuqiu_biaoqian.length > 0}}" class="biaoqian-inline-scroll" scroll-x enable-flex style="margin-top:8rpx">
<view class="biaoqian-inline-inner">
<chenghao-tag wx:for="{{item.xuqiu_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</scroll-view>
</view>
@@ -344,7 +485,7 @@
<text class="o-amt">商家派单</text>
<view class="qiangdan flexa btn-bg btn-qiang" data-item="{{item}}" bindtap="onQiangdanTap">抢单</view>
<view class="qiangdan flexa btn-bg btn-qiang" data-item="{{item}}" bindtap="onQiangdanTap">{{item.isMyZhiding ? '立即接待' : '抢单'}}</view>
</view>
@@ -384,8 +525,58 @@
<global-notification id="global-notification" />
<kefu-float bottom="200rpx" />
<popup-notice id="popupNotice" />
<!-- 指定单详情 -->
<view class="zhiding-modal-mask" wx:if="{{showZhidingDetail}}" catchtap="closeZhidingDetail">
<view class="zhiding-modal" catchtap="noop">
<view class="zhiding-modal-head">
<text class="zhiding-modal-title">指定订单详情</text>
<text class="zhiding-modal-close" catchtap="closeZhidingDetail">×</text>
</view>
<view class="zhiding-modal-body" wx:if="{{zhidingDetailOrder}}">
<view class="zhiding-modal-row"><text class="lbl">单号</text><text>{{zhidingDetailOrder.dingdan_id}}</text></view>
<view class="zhiding-modal-row"><text class="lbl">分成</text><text class="reward">¥{{zhidingDetailOrder.dashou_fencheng || 0}}</text></view>
<view class="zhiding-modal-row col"><text class="lbl">介绍</text><text>{{zhidingDetailOrder.jieshao || '暂无'}}</text></view>
<view class="zhiding-modal-row col" wx:if="{{zhidingDetailOrder.beizhu}}"><text class="lbl">备注</text><text>{{zhidingDetailOrder.beizhu}}</text></view>
<view class="zhiding-modal-row" wx:if="{{zhidingDetailOrder.zhiding_hf_text}}"><text class="lbl">您的回复</text><text>{{zhidingDetailOrder.zhiding_hf_text}}</text></view>
</view>
<view class="my-zhiding-actions zhiding-modal-actions" wx:if="{{zhidingDetailOrder}}">
<view class="zhiding-act zhiding-act--ghost" data-item="{{zhidingDetailOrder}}" catchtap="onZhidingReject">不想接</view>
<view class="zhiding-act zhiding-act--ghost" data-item="{{zhidingDetailOrder}}" catchtap="onZhidingLater">等会接</view>
<view class="zhiding-act zhiding-act--primary" data-item="{{zhidingDetailOrder}}" catchtap="onZhidingAccept">立即接待</view>
</view>
</view>
</view>
<!-- 等会接时间 -->
<view class="zhiding-modal-mask zhiding-modal-mask--top" wx:if="{{showLaterModal}}" catchtap="closeLaterModal">
<view class="zhiding-modal zhiding-modal--sm" catchtap="noop">
<view class="zhiding-modal-head">
<text class="zhiding-modal-title">等会接</text>
<text class="zhiding-modal-close" catchtap="closeLaterModal">×</text>
</view>
<view class="zhiding-modal-body">
<text class="later-hint">请填写预计接待时间,例如「30分钟后」「今晚8点」</text>
<input
class="later-input"
placeholder="预计接待时间"
placeholder-class="later-input-ph"
value="{{laterNote}}"
bindinput="onLaterNoteInput"
maxlength="50"
focus="{{showLaterModal}}"
adjust-position="{{true}}"
confirm-type="done"
bindconfirm="confirmZhidingLater"
/>
</view>
<view class="later-confirm" catchtap="confirmZhidingLater">确认</view>
</view>
</view>
<tab-bar />
</view>
+250 -5
View File
@@ -1,5 +1,11 @@
/* pages/qiangdan/qiangdan.wxss - 高级机甲风格重构版(对称优化) */
/* pages/qiangdan/qiangdan.wxss - 逍遥梦抢单页 */
@import '../../styles/dashou-xym-theme.wxss';
@import '../../styles/dashou-xym-order-card.wxss';
page {
background-color: #f7dc51;
}
.qiangdan-page {
height: 100vh;
display: flex;
@@ -8,6 +14,34 @@
overflow: hidden;
position: relative;
}
.tousu-top-bar {
display: flex;
align-items: center;
gap: 12rpx;
margin: 16rpx 24rpx 8rpx;
padding: 18rpx 24rpx;
border-radius: 14rpx;
background: linear-gradient(90deg, #e85a3a 0%, #f07858 100%);
box-shadow: 0 6rpx 18rpx rgba(232, 90, 58, 0.28);
z-index: 5;
position: relative;
}
.tousu-top-bar-txt {
color: #fff;
font-size: 28rpx;
font-weight: 600;
}
.tousu-top-bar-sub {
flex: 1;
color: rgba(255,255,255,0.88);
font-size: 22rpx;
}
.tousu-top-bar-arrow {
color: #fff;
font-size: 36rpx;
line-height: 1;
}
/* 背景动态纹理,增加层次感 */
.qiangdan-page::before {
@@ -88,6 +122,31 @@
transition: all 0.3s ease;
border: 2rpx solid rgba(0, 200, 255, 0.4);
}
.leixing-pic-wrap {
position: relative;
width: 120rpx;
height: 120rpx;
z-index: 1;
}
.leixing-badge {
position: absolute;
top: -8rpx;
right: -12rpx;
min-width: 32rpx;
height: 32rpx;
padding: 0 8rpx;
box-sizing: border-box;
border-radius: 16rpx;
background: #fa3534;
color: #fff;
font-size: 20rpx;
line-height: 32rpx;
text-align: center;
font-weight: 600;
z-index: 3;
}
.leixing-active .leixing-tupian {
box-shadow: 0 16rpx 40rpx rgba(0, 200, 255, 0.5);
@@ -126,6 +185,7 @@
flex: 1;
height: 0;
-webkit-overflow-scrolling: touch;
background: transparent;
}
.scroll-bottom-spacer {
@@ -486,7 +546,7 @@
background: rgba(0, 0, 0, 0.3);
padding: 8rpx 20rpx;
border-radius: 60rpx;
border: 1rpx solid rgba(196, 181, 253, 0.3);
border: 1rpx solid rgba(255, 215, 0, 0.3);
}
/* 分佣图标占位符,用户可替换为自己图标 */
@@ -504,10 +564,10 @@
.fenyong-price {
font-size: 44rpx;
color: #c4b5fd;
color: #ffd700;
font-weight: 800;
margin-right: 4rpx;
text-shadow: 0 0 20px rgba(196, 181, 253, 0.8);
text-shadow: 0 0 20px rgba(255, 215, 0, 0.8);
}
.fenyong-unit {
@@ -764,4 +824,189 @@
max-width: 50%;
}
@import '../../styles/dashou-xym-order-card.wxss';
/* 被指定订单(星阙功能保留,不影响星之界卡片 UI) */
.my-zhiding-banner {
margin: 16rpx 24rpx 0;
padding: 22rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #ff9500 0%, #ff6b00 100%);
border: 3rpx solid #ff4500;
box-shadow: 0 12rpx 32rpx rgba(255, 69, 0, 0.45);
position: relative;
z-index: 20;
}
.my-zhiding-banner-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.my-zhiding-head-left {
display: flex;
align-items: center;
gap: 12rpx;
}
.my-zhiding-badge {
font-size: 26rpx;
font-weight: 800;
color: #ff4500;
background: #fff;
padding: 8rpx 18rpx;
border-radius: 999rpx;
}
.my-zhiding-count-num {
font-size: 32rpx;
font-weight: 800;
color: #fff;
}
.my-zhiding-sub {
font-size: 26rpx;
color: #fff;
font-weight: 700;
}
.my-zhiding-card {
background: #fff;
border-radius: 16rpx;
padding: 18rpx;
margin-bottom: 12rpx;
border: 2rpx solid #ffe0b2;
}
.my-zhiding-card--highlight {
border-color: #ff8c00;
box-shadow: 0 0 0 4rpx rgba(255, 140, 0, 0.15);
}
.my-zhiding-card-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.my-zhiding-order-id {
font-size: 24rpx;
color: #666;
}
.my-zhiding-reward {
font-size: 30rpx;
font-weight: 700;
color: #e64340;
}
.my-zhiding-desc {
font-size: 28rpx;
color: #333;
line-height: 1.5;
}
.my-zhiding-status {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #059669;
}
.my-zhiding-actions {
display: flex;
gap: 12rpx;
margin-top: 16rpx;
}
.zhiding-act {
flex: 1;
text-align: center;
padding: 14rpx 0;
border-radius: 999rpx;
font-size: 26rpx;
font-weight: 600;
}
.zhiding-act--ghost {
background: #f3f4f6;
color: #666;
}
.zhiding-act--primary {
background: linear-gradient(90deg, #ff8c00, #ff6b00);
color: #fff;
}
.order-my-zhiding {
border: 3rpx solid #ff4500 !important;
box-shadow: 0 0 0 6rpx rgba(255, 69, 0, 0.25), 0 12rpx 36rpx rgba(255, 69, 0, 0.35) !important;
}
.order-zhiding-highlight {
animation: zhidingPulse 1.5s ease-in-out 2;
}
@keyframes zhidingPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 140, 0, 0.3); }
50% { box-shadow: 0 0 0 12rpx rgba(255, 140, 0, 0); }
}
.zhiding-modal-mask {
position: fixed;
left: 0; right: 0; top: 0; bottom: 0;
background: rgba(0, 0, 0, 0.62);
z-index: 10050;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
}
.zhiding-modal-mask--top {
z-index: 10060;
}
.zhiding-modal {
width: 86%;
max-width: 600rpx;
max-height: 72vh;
background: #ffffff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 24rpx 64rpx rgba(0, 0, 0, 0.35);
display: flex;
flex-direction: column;
}
.zhiding-modal--sm { max-width: 540rpx; }
.zhiding-modal-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 22rpx 26rpx;
border-bottom: 1rpx solid #eee;
background: #fff;
flex-shrink: 0;
}
.zhiding-modal-title { font-size: 30rpx; font-weight: 700; color: #222; }
.zhiding-modal-close { font-size: 44rpx; color: #666; line-height: 1; padding: 0 8rpx; }
.zhiding-modal-body {
padding: 22rpx 26rpx;
background: #fff;
max-height: 52vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.zhiding-modal-row {
display: flex;
justify-content: space-between;
margin-bottom: 16rpx;
font-size: 28rpx;
color: #333;
}
.zhiding-modal-row.col { flex-direction: column; gap: 8rpx; }
.zhiding-modal-row .lbl { color: #999; font-size: 24rpx; }
.zhiding-modal-row .reward { color: #e64340; font-weight: 700; }
.zhiding-modal-actions { padding: 0 28rpx 28rpx; margin-top: 0; background: #fff; }
.later-hint { font-size: 26rpx; color: #444; display: block; margin-bottom: 16rpx; }
.later-input {
width: 100%;
box-sizing: border-box;
border: 2rpx solid #ddd;
border-radius: 12rpx;
padding: 22rpx 20rpx;
font-size: 28rpx;
color: #222;
background: #f9fafb;
min-height: 88rpx;
}
.later-input-ph { color: #aaa; }
.later-confirm {
margin: 0 28rpx 28rpx;
text-align: center;
padding: 24rpx;
background: linear-gradient(90deg, #ff8c00, #ff4500);
color: #fff;
border-radius: 999rpx;
font-size: 30rpx;
font-weight: 700;
}
+99
View File
@@ -0,0 +1,99 @@
// pages/alipay-webview/alipay-webview.js
// 支付宝当面付二维码展示页(替代原 web-view 方案,避免 alipay:// scheme 限制)
// 通用化:支持通过 pollUrl 参数指定轮询接口,兼容 code===0(订单)和 code===200(Czjilu类)双成功码
// 支付成功后通过 EventChannel 通知原页面刷新状态
const app = getApp()
import request from '../../utils/request.js'
Page({
data: {
qrImage: '',
orderId: '',
pollUrl: '/dingdan/fucha', // 默认订单轮询接口
statusText: '等待支付...',
showBack: false,
pollCount: 0,
maxPollCount: 60,
isPaid: false
},
onLoad(options) {
const orderId = options.orderId || ''
// base64 图片较大,通过 globalData 传递避免 URL 长度限制
const qrImage = app.globalData.alipayQrImage || ''
app.globalData.alipayQrImage = ''
// 允许调用方指定轮询接口(需 URL 编码),未指定则用默认订单接口
const pollUrl = options.pollUrl ? decodeURIComponent(options.pollUrl) : '/dingdan/fucha'
this.setData({ qrImage, orderId, pollUrl })
// 获取 opener 事件通道,用于支付成功后通知原页面刷新状态
this.openerEventChannel = this.getOpenerEventChannel ? this.getOpenerEventChannel() : null
this._pollTimer = null
this._hidden = false
if (orderId) {
this.startPolling()
}
},
startPolling() {
if (this.data.isPaid) return
if (this.data.pollCount >= this.data.maxPollCount) {
this.setData({ statusText: '支付确认超时,如已支付请点击返回', showBack: true })
return
}
this.setData({ pollCount: this.data.pollCount + 1 })
this.pollOrderStatus()
},
pollOrderStatus() {
if (this.data.isPaid) return
request({
url: this.data.pollUrl,
method: 'POST',
data: { dingdanid: this.data.orderId }
})
.then(res => {
if (this.data.isPaid) return
// 兼容两种成功码:订单接口 code===0,Czjilu类接口 code===200
if (res.statusCode === 200 && res.data && (res.data.code === 0 || res.data.code === 200)) {
this.setData({ isPaid: true, statusText: '支付成功!', showBack: true })
wx.showToast({ title: '支付成功!', icon: 'success', duration: 1500 })
// 通知原页面刷新状态(原页面通过 wx.navigateTo events 注册 onAlipaySuccess)
if (this.openerEventChannel && this.openerEventChannel.emit) {
try {
this.openerEventChannel.emit('onAlipaySuccess', { orderId: this.data.orderId, data: res.data })
} catch (e) {}
}
} else {
this.setData({ statusText: '等待支付...' })
this._pollTimer = setTimeout(() => this.startPolling(), 3000)
}
})
.catch(() => {
if (!this.data.isPaid) {
this._pollTimer = setTimeout(() => this.startPolling(), 3000)
}
})
},
onBack() {
wx.navigateBack()
},
onHide() {
// 切后台时暂停轮询并清理定时器,但不标记为已支付,以便 onShow 恢复
this._hidden = true
if (this._pollTimer) {
clearTimeout(this._pollTimer)
this._pollTimer = null
}
},
onShow() {
// 从后台恢复时,如果未支付且未超时,恢复轮询
if (this._hidden && !this.data.isPaid && this.data.orderId &&
this.data.pollCount < this.data.maxPollCount) {
this._hidden = false
this.startPolling()
}
}
})
+4
View File
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "支付宝支付",
"usingComponents": {}
}
+9
View File
@@ -0,0 +1,9 @@
<view class="qr-container">
<view class="qr-title">支付宝扫码支付</view>
<image wx:if="{{qrImage}}" class="qr-image" src="{{qrImage}}" mode="aspectFit" />
<view class="qr-tip">请使用 <text class="qr-emph">支付宝</text> APP 扫描上方二维码完成支付</view>
<view class="qr-status">{{statusText}}</view>
<view class="qr-actions" wx:if="{{showBack}}">
<view class="qr-btn" bindtap="onBack">返回</view>
</view>
</view>
+45
View File
@@ -0,0 +1,45 @@
.qr-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 40rpx;
min-height: 100vh;
background: #f5f5f5;
}
.qr-title {
font-size: 36rpx;
font-weight: 700;
color: #333;
margin-bottom: 40rpx;
}
.qr-image {
width: 500rpx;
height: 500rpx;
background: #fff;
border-radius: 12rpx;
}
.qr-tip {
font-size: 26rpx;
color: #999;
margin-top: 30rpx;
text-align: center;
}
.qr-emph {
color: #1677ff;
font-weight: 600;
}
.qr-status {
font-size: 28rpx;
color: #1677ff;
margin-top: 20rpx;
}
.qr-actions {
margin-top: 40rpx;
}
.qr-btn {
background: #1677ff;
color: #fff;
border-radius: 8rpx;
padding: 18rpx 80rpx;
font-size: 30rpx;
}
+51 -2
View File
@@ -303,14 +303,28 @@ Page({
return;
}
if (applyFee > 0) {
await this.startPayProcess();
this.choosePayMethod();
} else {
await this.doFreeApply();
}
},
// 选择支付方式
choosePayMethod() {
wx.showActionSheet({
itemList: ['微信支付', '支付宝(扫码)'],
success: (res) => {
if (res.tapIndex === 0) {
this.startPayProcess('wechat');
} else if (res.tapIndex === 1) {
this.startPayProcess('alipay');
}
}
});
},
// 支付流程
async startPayProcess() {
async startPayProcess(payMethod = 'wechat') {
this.setData({ payLoading: true });
try {
const res = await request({
@@ -323,11 +337,20 @@ Page({
reapply: this.data.isReapply,
jilu_id: this.data.applyJiluId,
shenheguan_id: this.data.selectedKaoheguanId, // 新增
pay_method: payMethod === 'alipay' ? 'alipay' : 'wechat'
}
});
if (res.data.code !== 200) throw new Error(res.data.message || '下单失败');
const payParams = res.data.payParams;
const dingdanid = res.data.dingdanid;
// 支付宝分支:跳转二维码页
if (payMethod === 'alipay' || (payParams && payParams.pay_method === 'alipay')) {
this.setData({ payLoading: false });
this.triggerAlipayPayment(payParams, dingdanid);
return;
}
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
@@ -349,6 +372,32 @@ Page({
}
},
// ========== 支付宝扫码支付封装 ==========
triggerAlipayPayment(payParams, orderId) {
const qrImage = payParams && payParams.qr_image ? payParams.qr_image : '';
if (!qrImage) {
wx.showToast({ title: '支付宝二维码生成失败', icon: 'none' });
return;
}
const app = getApp();
app.globalData.alipayQrImage = qrImage;
const pollUrl = encodeURIComponent('/yonghu/khzffc');
const self = this;
wx.navigateTo({
url: `/pages/alipay-webview/alipay-webview?orderId=${orderId}&pollUrl=${pollUrl}`,
events: {
onAlipaySuccess: () => {
// 复用微信流成功逻辑:关闭弹窗并刷新考核记录列表
wx.showToast({ title: '申请成功', icon: 'success' });
self.closeApplyModal();
self.fetchAllTags();
self.setData({ recordList: [], recordPage: 1 });
self.fetchMyRecords(true);
}
}
});
},
// 轮询支付结果
async pollPayStatus(dingdanid) {
for (let i = 0; i < 10; i++) {
+1 -1
View File
@@ -132,7 +132,7 @@ page {
border-radius: 20rpx;
font-weight: 500;
}
.status-0 { background: #F3E8FF; color: #9333ea; }
.status-0 { background: #FFF3E0; color: #EF6C00; }
.status-1 { background: #E8F5E9; color: #2E7D32; }
.status-2 { background: #FFEBEE; color: #C62828; }
.status-3 { background: #E3F2FD; color: #1565C0; }
+11 -3
View File
@@ -1,6 +1,8 @@
// pages/category/category.js
const app = getApp()
import { createPage } from '../../utils/base-page.js';
import { buildClubHeaders } from '../../utils/club-context.js';
import { isFighterOnly, resolveFighterOnlyLaunchUrl } from '../../utils/fighter-only.js';
Page(createPage({
data: {
@@ -24,6 +26,10 @@ Page(createPage({
},
onLoad(options) {
if (isFighterOnly()) {
wx.reLaunch({ url: resolveFighterOnlyLaunchUrl() });
return;
}
this.setData({
ossImageUrl: app.globalData.ossImageUrl
})
@@ -31,6 +37,10 @@ Page(createPage({
},
onShow() {
if (isFighterOnly()) {
wx.reLaunch({ url: resolveFighterOnlyLaunchUrl() });
return;
}
this.registerNotificationComponent();
},
@@ -114,9 +124,7 @@ Page(createPage({
wx.request({
url: app.globalData.apiBaseUrl + '/shangpin/shangpinhuoqu/',
method: 'POST',
header: {
'content-type': 'application/json'
},
header: buildClubHeaders({ 'content-type': 'application/json' }),
success(res) {
if (res.statusCode === 200 && res.data) {
const data = res.data
+43 -43
View File
@@ -60,13 +60,13 @@
.search-btn {
margin-left: 20rpx;
box-shadow: 0 6rpx 20rpx rgba(147, 51, 234, 0.3);
box-shadow: 0 6rpx 20rpx rgba(255, 170, 0, 0.3);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.search-btn:active {
transform: scale(0.95);
box-shadow: 0 4rpx 15rpx rgba(147, 51, 234, 0.4);
box-shadow: 0 4rpx 15rpx rgba(255, 170, 0, 0.4);
}
.search-btn-text {
@@ -127,7 +127,7 @@
transform: translate(-50%, -50%);
width: 300rpx;
height: 300rpx;
background: radial-gradient(circle, rgba(168, 85, 247, 0.1) 0%, transparent 70%);
background: radial-gradient(circle, rgba(255, 204, 0, 0.1) 0%, transparent 70%);
z-index: -1;
}
@@ -167,8 +167,8 @@
.leixing-active .leixing-image-box {
transform: scale(1.12);
box-shadow:
0 18rpx 40rpx rgba(168, 85, 247, 0.3),
0 8rpx 16rpx rgba(147, 51, 234, 0.2);
0 18rpx 40rpx rgba(255, 204, 0, 0.3),
0 8rpx 16rpx rgba(255, 170, 0, 0.2);
}
.leixing-image {
@@ -185,7 +185,7 @@
right: -3rpx;
bottom: -3rpx;
border-radius: 50%;
background: linear-gradient(135deg, #c4b5fd, #9333ea, #7c3aed);
background: linear-gradient(135deg, #ffcc00, #ffaa00, #ff8800);
opacity: 0;
transition: opacity 0.4s;
z-index: -1;
@@ -223,10 +223,10 @@
}
.leixing-active .leixing-text {
color: #8b5cf6;
color: #ff9900;
font-weight: 600;
transform: translateY(-2rpx);
text-shadow: 0 2rpx 4rpx rgba(168, 85, 247, 0.2);
text-shadow: 0 2rpx 4rpx rgba(255, 153, 0, 0.2);
}
.leixing-active-bar {
@@ -234,12 +234,12 @@
bottom: -4rpx;
width: 44rpx;
height: 4rpx;
background: linear-gradient(90deg, #c4b5fd, #9333ea);
background: linear-gradient(90deg, #ffcc00, #ffaa00);
border-radius: 2rpx;
opacity: 0;
transform: scaleX(0);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 2rpx 8rpx rgba(147, 51, 234, 0.4);
box-shadow: 0 2rpx 8rpx rgba(255, 170, 0, 0.4);
}
.leixing-active-bar-show {
@@ -261,7 +261,7 @@
.border-effect-active {
opacity: 1;
border-color: rgba(168, 85, 247, 0.5);
border-color: rgba(255, 204, 0, 0.5);
animation: borderGlow 2s infinite;
}
@@ -295,7 +295,7 @@
}
.zhuanqu-item:active {
background: rgba(168, 85, 247, 0.05);
background: rgba(255, 204, 0, 0.05);
}
.zhuanqu-item-content {
@@ -320,9 +320,9 @@
}
.zhuanqu-active .zhuanqu-name {
color: #8b5cf6;
color: #ff9900;
font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(168, 85, 247, 0.2);
text-shadow: 0 2rpx 4rpx rgba(255, 153, 0, 0.2);
}
.zhuanqu-count {
@@ -332,7 +332,7 @@
}
.zhuanqu-active .zhuanqu-count {
color: #9333ea;
color: #ffaa00;
font-weight: 500;
}
@@ -343,7 +343,7 @@
transform: translateY(-50%);
width: 6rpx;
height: 44rpx;
background: linear-gradient(180deg, #a855f7, #9333ea);
background: linear-gradient(180deg, #ffcc00, #ffaa00);
border-radius: 3rpx 0 0 3rpx;
opacity: 0;
transition: all 0.3s;
@@ -353,7 +353,7 @@
.zhuanqu-indicator-glow {
opacity: 1;
animation: indicatorGlow 2s infinite;
box-shadow: 0 0 20rpx rgba(168, 85, 247, 0.5);
box-shadow: 0 0 20rpx rgba(255, 204, 0, 0.5);
}
.indicator-sparkle {
@@ -372,7 +372,7 @@
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(168, 85, 247, 0.05), transparent);
background: linear-gradient(90deg, transparent, rgba(255, 204, 0, 0.05), transparent);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
@@ -389,7 +389,7 @@
width: 0;
height: 0;
border-radius: 50%;
background: rgba(168, 85, 247, 0.1);
background: rgba(255, 204, 0, 0.1);
transform: translate(-50%, -50%);
opacity: 0;
pointer-events: none;
@@ -463,7 +463,7 @@
right: -4rpx;
bottom: -4rpx;
border-radius: 18rpx;
background: linear-gradient(135deg, rgba(168, 85, 247, 0.2), transparent);
background: linear-gradient(135deg, rgba(255, 204, 0, 0.2), transparent);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
@@ -479,7 +479,7 @@
right: 0;
width: 20rpx;
height: 20rpx;
background: linear-gradient(135deg, #a855f7, transparent 70%);
background: linear-gradient(135deg, #ffcc00, transparent 70%);
border-radius: 0 14rpx 0 0;
}
@@ -514,7 +514,7 @@
left: 0;
width: 0;
height: 2rpx;
background: linear-gradient(90deg, #c4b5fd, #9333ea);
background: linear-gradient(90deg, #ffcc00, #ffaa00);
transition: width 0.3s;
}
@@ -537,20 +537,20 @@
.price-icon {
font-size: 24rpx;
color: #9333ea;
color: #ff6600;
font-weight: 500;
}
.price-integer {
font-size: 36rpx; /* 适当调整大小 */
color: #9333ea;
color: #ff6600;
font-weight: 600;
margin-left: 2rpx;
}
.price-decimal {
font-size: 24rpx;
color: #9333ea;
color: #ff6600;
font-weight: 500;
}
@@ -573,11 +573,11 @@
.detail-btn {
position: relative;
padding: 12rpx 28rpx;
background: linear-gradient(135deg, #c4b5fd, #9333ea);
background: linear-gradient(135deg, #ffcc00, #ffaa00);
border-radius: 40rpx;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
overflow: hidden;
box-shadow: 0 6rpx 20rpx rgba(147, 51, 234, 0.2);
box-shadow: 0 6rpx 20rpx rgba(255, 170, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
@@ -586,7 +586,7 @@
.detail-btn:active {
transform: scale(0.95);
box-shadow: 0 4rpx 15rpx rgba(147, 51, 234, 0.4);
box-shadow: 0 4rpx 15rpx rgba(255, 170, 0, 0.4);
}
.detail-btn-text {
@@ -659,7 +659,7 @@
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, transparent, rgba(168, 85, 247, 0.03), transparent);
background: linear-gradient(135deg, transparent, rgba(255, 204, 0, 0.03), transparent);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
@@ -676,7 +676,7 @@
width: 0;
height: 0;
border-radius: 50%;
background: rgba(168, 85, 247, 0.1);
background: rgba(255, 204, 0, 0.1);
transform: translate(-50%, -50%);
opacity: 0;
pointer-events: none;
@@ -707,19 +707,19 @@
}
.shangpin-empty-btn {
background: linear-gradient(135deg, #c4b5fd, #9333ea);
background: linear-gradient(135deg, #ffcc00, #ffaa00);
color: white;
font-size: 26rpx;
font-weight: 500;
padding: 16rpx 44rpx;
border-radius: 40rpx;
box-shadow: 0 8rpx 24rpx rgba(147, 51, 234, 0.3);
box-shadow: 0 8rpx 24rpx rgba(255, 170, 0, 0.3);
transition: all 0.3s;
}
.shangpin-empty-btn:active {
transform: scale(0.95);
box-shadow: 0 4rpx 16rpx rgba(147, 51, 234, 0.4);
box-shadow: 0 4rpx 16rpx rgba(255, 170, 0, 0.4);
}
.load-more-line {
@@ -859,7 +859,7 @@
right: -4rpx;
bottom: -4rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, rgba(168, 85, 247, 0.2), transparent);
background: linear-gradient(135deg, rgba(255, 204, 0, 0.2), transparent);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
@@ -896,7 +896,7 @@
.info-tag {
padding: 6rpx 12rpx;
background: rgba(168, 85, 247, 0.1);
background: rgba(255, 204, 0, 0.1);
border-radius: 6rpx;
}
@@ -921,7 +921,7 @@
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, transparent, rgba(168, 85, 247, 0.05), transparent);
background: linear-gradient(135deg, transparent, rgba(255, 204, 0, 0.05), transparent);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
@@ -977,7 +977,7 @@
width: 100%;
height: 100%;
border: 4rpx solid transparent;
border-top-color: #a855f7;
border-top-color: #ffcc00;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@ -1028,7 +1028,7 @@
left: 50%;
width: 12rpx;
height: 12rpx;
background: #a855f7;
background: #ffcc00;
border-radius: 50%;
transform: translateX(-50%);
animation: spin 1s linear infinite;
@@ -1045,7 +1045,7 @@
left: 50%;
width: 200rpx;
height: 200rpx;
background: radial-gradient(circle, rgba(168, 85, 247, 0.2) 0%, transparent 70%);
background: radial-gradient(circle, rgba(255, 204, 0, 0.2) 0%, transparent 70%);
transform: translate(-50%, -50%);
z-index: -1;
}
@@ -1076,19 +1076,19 @@
@keyframes borderGlow {
0%, 100% {
border-color: rgba(168, 85, 247, 0.3);
border-color: rgba(255, 204, 0, 0.3);
}
50% {
border-color: rgba(168, 85, 247, 0.8);
border-color: rgba(255, 204, 0, 0.8);
}
}
@keyframes indicatorGlow {
0%, 100% {
box-shadow: 0 0 20rpx rgba(168, 85, 247, 0.3);
box-shadow: 0 0 20rpx rgba(255, 204, 0, 0.3);
}
50% {
box-shadow: 0 0 30rpx rgba(168, 85, 247, 0.8);
box-shadow: 0 0 30rpx rgba(255, 204, 0, 0.8);
}
}
+165 -69
View File
@@ -1,7 +1,14 @@
const app = getApp();
import { formatDate } from '../../static/lib/utils';
import { resolveAvatarUrl, getDefaultAvatarUrl } from '../../utils/avatar.js';
import { resolveAvatarUrl, getDefaultAvatarUrl, resolveAvatarFromStorage, subscribeConfigAvatarRefresh } from '../../utils/avatar.js';
import { getLocalImUserId } from '../../utils/im-user.js';
import { recordConversationOpen } from '../../utils/group-chat.js';
import {
fetchHistoryMessages,
getOldestHistoryTimestamp,
mergeHistoryMessages,
waitChatImReady,
} from '../../utils/chat-history.js';
Page({
data: {
@@ -25,29 +32,58 @@ Page({
pendingImage: '',
pendingImageFile: null,
keyboardHeight: 0,
bottomSafeHeight: 10
bottomSafeHeight: 140,
defaultAvatarUrl: '',
},
onLoad(options) {
const defAvatar = getDefaultAvatarUrl(app);
if (options.data) {
try {
const p = JSON.parse(decodeURIComponent(options.data));
this.setData({
toUserId: p.toUserId || '',
toName: p.toName || '聊天',
toAvatar: resolveAvatarUrl(p.toAvatar, app),
toAvatar: resolveAvatarUrl(p.toAvatar, app) || defAvatar,
defaultAvatarUrl: defAvatar,
});
} catch (e) {}
} catch (e) {
this.setData({ defaultAvatarUrl: defAvatar });
}
} else {
this.setData({ defaultAvatarUrl: defAvatar });
}
wx.setNavigationBarTitle({ title: this.data.toName });
this.initCurrentUser();
this._unsubConfigAvatar = subscribeConfigAvatarRefresh(this, () => {
const def = getDefaultAvatarUrl(app);
this.setData({
defaultAvatarUrl: def,
toAvatar: resolveAvatarUrl(this.data.toAvatar, app) || def,
});
this.refreshCurrentUserAvatar();
});
},
onShow() {
const defAvatar = getDefaultAvatarUrl(app);
this.setData({
defaultAvatarUrl: defAvatar,
toAvatar: resolveAvatarUrl(this.data.toAvatar, app) || defAvatar,
});
this.refreshCurrentUserAvatar();
if (this.data.toUserId) {
if (!app.globalData.pageState) app.globalData.pageState = {};
app.globalData.pageState.lastOpenedConvId = this.data.toUserId;
recordConversationOpen({ userId: this.data.toUserId });
}
this.ensureChatReady();
},
onHide() {
if (this.data.toUserId) {
recordConversationOpen({ userId: this.data.toUserId });
}
this.clearPageListeners();
this.markPrivateMessageAsRead();
setTimeout(() => {
@@ -55,27 +91,34 @@ Page({
}, 250);
},
ensureChatReady() {
onUnload() {
if (this._unsubConfigAvatar) {
this._unsubConfigAvatar();
this._unsubConfigAvatar = null;
}
},
async ensureChatReady() {
const targetUserId = getLocalImUserId(app);
if (!targetUserId) return;
const ready = () => {
const ready = async () => {
this.setupAllListeners();
this.loadHistory(true);
await this.loadHistory(true);
};
if (app.ensureConnection) {
const ret = app.ensureConnection();
if (ret && typeof ret.then === 'function') {
ret.then(ready).catch(ready);
} else {
setTimeout(ready, 300);
try {
const ok = await waitChatImReady(app, 12000);
if (ok) {
await ready();
return;
}
return;
} catch (e) {
console.warn('[私聊] IM 连接失败', e);
}
if (this.isConnected() && wx.goEasy.im?.userId === targetUserId) {
ready();
await ready();
return;
}
this.connectGoEasy(targetUserId);
@@ -133,14 +176,26 @@ Page({
const imId = getLocalImUserId(app);
const uid = wx.getStorageSync('uid');
const nick = app.globalData.nicheng || app.globalData.dashouNicheng || wx.getStorageSync('nicheng') || '';
const defAvatar = getDefaultAvatarUrl(app);
const avatar = resolveAvatarFromStorage(app) || defAvatar;
this.setData({
currentUser: {
id: imId || ('Ds' + uid),
name: nick || `用户${uid}`,
avatar: resolveAvatarUrl(wx.getStorageSync('touxiang'), app),
avatar,
},
});
},
refreshCurrentUserAvatar() {
const defAvatar = getDefaultAvatarUrl(app);
const cu = this.data.currentUser;
if (!cu) return;
const avatar = resolveAvatarUrl(cu.avatar || wx.getStorageSync('touxiang'), app) || defAvatar;
if (avatar !== cu.avatar) {
this.setData({ currentUser: { ...cu, avatar } });
}
},
fixAvatar(url) {
return resolveAvatarUrl(url, app);
},
@@ -156,6 +211,19 @@ Page({
normalizeMessage(msg) {
if (!msg) return msg;
if (!msg.payload) msg.payload = {};
const defAvatar = this.data.defaultAvatarUrl || getDefaultAvatarUrl(app);
const myId = this.data.currentUser?.id;
if (myId && msg.senderId === myId) {
msg.senderData = {
name: this.data.currentUser.name,
avatar: resolveAvatarUrl(this.data.currentUser.avatar, app) || defAvatar,
};
} else {
msg.senderData = {
name: this.data.toName,
avatar: resolveAvatarUrl(this.data.toAvatar, app) || defAvatar,
};
}
if (msg.type === 'image') {
const p = msg.payload;
if (!p.url) p.url = p.thumbnail || p.thumb || p.imageUrl || '';
@@ -188,63 +256,91 @@ Page({
onPullDownRefresh() {
if (this.data.loadingHistory) return;
if (!this.data.hasMore) {
wx.showToast({ title: '没有更早的消息了', icon: 'none' });
this.setData({ isRefreshing: false });
wx.stopPullDownRefresh();
return;
}
this.setData({ isRefreshing: true });
this.loadHistory(false).finally(() => { this.setData({ isRefreshing: false }); wx.stopPullDownRefresh(); });
this.loadHistory(false).finally(() => {
this.setData({ isRefreshing: false });
wx.stopPullDownRefresh();
});
},
loadHistory(refresh) {
if (!refresh && !this.data.hasMore) return Promise.resolve();
return new Promise((resolve) => {
this.setData({ loadingHistory: true });
const { toUserId, lastTimestamp } = this.data;
const ts = refresh ? null : lastTimestamp;
wx.goEasy.im.history({
type: wx.GoEasy.IM_SCENE.PRIVATE, id: toUserId, lastTimestamp: ts, limit: 20,
onSuccess: (res) => {
let list = res.content || [];
list.sort((a, b) => a.timestamp - b.timestamp);
list.forEach((m, idx) => {
m = this.normalizeMessage(m);
list[idx] = m;
m.formattedTime = formatDate(m.timestamp);
if (idx === 0) m.showTime = true;
else m.showTime = (m.timestamp - list[idx-1].timestamp) / 60000 > 5;
if (m.senderId === this.data.currentUser.id) {
m.senderData = {
name: this.data.currentUser.name,
avatar: resolveAvatarUrl(this.data.currentUser.avatar, app),
};
} else {
m.senderData = {
name: this.data.toName,
avatar: resolveAvatarUrl(this.data.toAvatar, app),
};
}
});
let final = refresh ? list : [...list, ...this.data.messages];
if (!refresh) {
for (let i=0; i<final.length; i++) {
if (i===0) final[i].showTime = true;
else final[i].showTime = (final[i].timestamp - final[i-1].timestamp) / 60000 > 5;
}
}
const ids = new Set();
const unique = [];
for (const m of final) { if (!ids.has(m.messageId)) { ids.add(m.messageId); unique.push(m); } }
const update = {
messages: unique,
hasMore: list.length >= 20,
lastTimestamp: unique.length ? unique[0].timestamp : lastTimestamp,
loadingHistory: false
};
if (refresh) update.scrollToView = 'msg-bottom';
this.setData(update);
if (refresh) this.markPrivateMessageAsRead();
resolve();
},
onFailed: () => { this.setData({ loadingHistory: false }); resolve(); }
mapHistoryMessage(m, idx, list) {
m = this.normalizeMessage(m);
m.formattedTime = formatDate(m.timestamp);
if (idx === 0) m.showTime = true;
else m.showTime = (m.timestamp - list[idx - 1].timestamp) / 60000 > 5;
if (m.senderId === this.data.currentUser?.id) {
m.senderData = {
name: this.data.currentUser.name,
avatar: resolveAvatarUrl(this.data.currentUser.avatar, app),
};
} else {
m.senderData = {
name: this.data.toName,
avatar: resolveAvatarUrl(this.data.toAvatar, app),
};
}
return m;
},
async loadHistory(refresh) {
if (!refresh && !this.data.hasMore) return;
if (this.data.loadingHistory) return;
if (!this.data.toUserId) return;
this.setData({ loadingHistory: true });
try {
const ready = await waitChatImReady(app, 12000);
if (!ready) {
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
return;
}
const ts = refresh
? null
: getOldestHistoryTimestamp(this.data.messages, this.data.lastTimestamp);
if (!refresh && ts == null) {
this.setData({ hasMore: false });
wx.showToast({ title: '没有更早的消息了', icon: 'none' });
return;
}
const raw = await fetchHistoryMessages({
scene: wx.GoEasy.IM_SCENE.PRIVATE,
id: this.data.toUserId,
lastTimestamp: ts,
limit: 20,
});
});
const merged = mergeHistoryMessages({
incoming: raw,
existing: this.data.messages,
refresh,
limit: 20,
mapMessage: (m, idx, list) => this.mapHistoryMessage(m, idx, list),
});
const update = {
messages: merged.messages,
hasMore: merged.hasMore,
lastTimestamp: merged.lastTimestamp,
};
if (refresh) update.scrollToView = 'msg-bottom';
this.setData(update);
if (refresh) this.markPrivateMessageAsRead();
} catch (e) {
console.warn('[私聊] 历史消息加载失败', e);
if (!refresh) {
wx.showToast({ title: '加载历史消息失败', icon: 'none' });
}
} finally {
this.setData({ loadingHistory: false });
}
},
listenNewMsg() {
+4 -4
View File
@@ -4,13 +4,13 @@
refresher-enabled="{{true}}"
refresher-triggered="{{isRefreshing}}"
bindrefresherrefresh="onPullDownRefresh"
style="top: 20rpx; bottom: calc({{bottomSafeHeight}}rpx + {{keyboardHeight}}px + env(safe-area-inset-bottom) + 20rpx);">
style="top: 20rpx; bottom: calc({{bottomSafeHeight}}rpx + {{keyboardHeight}}px + env(safe-area-inset-bottom) + 24rpx);">
<view class="chat-loading-more" wx:if="{{loadingHistory}}">加载更早消息...</view>
<block wx:for="{{messages}}" wx:key="messageId">
<view class="chat-msg-wrapper" data-messageid="{{item.messageId}}" bindlongpress="showAction">
<view class="chat-time-tag" wx:if="{{item.showTime}}">{{item.formattedTime}}</view>
<view class="chat-msg-row {{item.senderId === currentUser.id ? 'chat-msg-row--right' : 'chat-msg-row--left'}}">
<image wx:if="{{item.senderId !== currentUser.id}}" class="chat-avatar" src="{{item.senderData.avatar || toAvatar}}" mode="aspectFill" binderror="onPeerAvatarError" />
<image wx:if="{{item.senderId !== currentUser.id}}" class="chat-avatar" src="{{toAvatar || defaultAvatarUrl}}" mode="aspectFill" binderror="onPeerAvatarError" />
<view class="chat-bubble {{item.senderId === currentUser.id ? 'chat-bubble--right' : 'chat-bubble--left'}}"
data-messageid="{{item.messageId}}"
data-text="{{item.type==='text' ? item.payload.text : ''}}"
@@ -23,14 +23,14 @@
<text class="chat-order-info">ID: {{item.payload.orderId}}</text>
</view>
</view>
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{item.senderData.avatar || currentUser.avatar}}" mode="aspectFill" binderror="onSelfAvatarError" data-index="{{index}}" />
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{currentUser.avatar || defaultAvatarUrl}}" mode="aspectFill" binderror="onSelfAvatarError" data-index="{{index}}" />
</view>
<view wx:if="{{item.senderId === currentUser.id && item.status === 'success'}}" class="chat-read-tag">
{{item.read ? '已读' : '未读'}}
</view>
</view>
</block>
<view id="msg-bottom" style="height:20rpx;"></view>
<view id="msg-bottom" style="height:56rpx;"></view>
</scroll-view>
<view class="chat-input-area" style="bottom: {{keyboardHeight}}px; padding-bottom: calc(10rpx + env(safe-area-inset-bottom));">
+247 -79
View File
@@ -1,7 +1,14 @@
const app = getApp();
import { formatDate } from '../../static/lib/utils';
import { getLocalImUserId } from '../../utils/im-user.js';
import { resolveAvatarUrl } from '../../utils/avatar.js';
import { resolveAvatarUrl, getDefaultAvatarUrl, resolveAvatarFromStorage, subscribeConfigAvatarRefresh, ensureAvatarUrl } from '../../utils/avatar.js';
import {
fetchHistoryMessages,
getOldestHistoryTimestamp,
mergeHistoryMessages,
waitChatImReady,
} from '../../utils/chat-history.js';
import { matchAutoReply, getWelcomeText } from '../../utils/scriptService.js';
Page({
data: {
@@ -29,11 +36,14 @@ Page({
pendingImage: '',
pendingImageFile: null,
keyboardHeight: 0,
bottomSafeHeight: 10
bottomSafeHeight: 140,
defaultAvatarUrl: '',
welcomeText: '你好,请问有什么可以帮到您的?',
},
onLoad(options) {
let teamId = '', teamName = '客服', teamAvatar = '';
const defAvatar = getDefaultAvatarUrl(app);
if (options.data) {
try {
const p = JSON.parse(decodeURIComponent(options.data));
@@ -43,38 +53,102 @@ Page({
} catch (e) {
}
}
if (!teamAvatar) {
teamAvatar = app.globalData.ossImageUrl + app.globalData.morentouxiang;
let teamAvatarResolved = teamAvatar ? ensureAvatarUrl(teamAvatar, app) : defAvatar;
if (!teamAvatarResolved || !teamAvatarResolved.startsWith('http')) {
teamAvatarResolved = defAvatar;
}
this.setData({ teamId, teamName, teamAvatar });
this.setData({ teamId, teamName, teamAvatar: teamAvatarResolved, defaultAvatarUrl: defAvatar });
wx.setNavigationBarTitle({ title: teamName });
this.initCurrentUser();
this.loadWelcomeScript();
this._unsubConfigAvatar = subscribeConfigAvatarRefresh(this, () => {
const def = getDefaultAvatarUrl(app);
this.setData({
defaultAvatarUrl: def,
teamAvatar: def,
});
this.refreshCurrentUserAvatar();
});
},
onUnload() {
if (this._unsubConfigAvatar) {
this._unsubConfigAvatar();
this._unsubConfigAvatar = null;
}
},
onShow() {
const defAvatar = getDefaultAvatarUrl(app);
const patch = {
defaultAvatarUrl: defAvatar,
teamAvatar: defAvatar,
};
this.setData(patch);
this.refreshCurrentUserAvatar();
this.autoConnect();
},
refreshCurrentUserAvatar() {
const defAvatar = getDefaultAvatarUrl(app);
const cu = this.data.currentUser;
if (!cu) return;
const avatar = resolveAvatarUrl(cu.avatar || wx.getStorageSync('touxiang'), app) || defAvatar;
if (avatar !== cu.avatar) {
this.setData({ currentUser: { ...cu, avatar } });
}
},
onHide() {
this.clearAllListeners();
},
autoConnect() {
async loadWelcomeScript() {
try {
const text = await getWelcomeText();
if (!text) return;
this.setData({ welcomeText: text });
const msgs = this.data.messages;
const idx = msgs.findIndex((m) => m.messageId === 'welcome-cs');
if (idx >= 0) {
const updated = [...msgs];
updated[idx] = { ...updated[idx], payload: { ...updated[idx].payload, text } };
this.setData({ messages: updated });
}
} catch (e) {
console.warn('loadWelcomeScript failed', e);
}
},
async autoConnect() {
const targetUserId = getLocalImUserId(app);
if (!targetUserId) return;
const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
if (status === 'connected' || status === 'reconnected') {
const currentUserId = wx.goEasy.im ? wx.goEasy.im.userId : null;
if (currentUserId === targetUserId) {
this.setupAllListeners();
this.loadHistory(true);
const ready = async () => {
this.setupAllListeners();
await this.loadHistory(true);
};
try {
const ok = await waitChatImReady(app, 12000);
if (ok) {
await ready();
return;
} else {
wx.goEasy.disconnect();
}
} catch (e) {
console.warn('[客服] IM 连接失败', e);
}
this.connectGoEasy(targetUserId);
if (this.isConnected()) {
await ready();
} else {
this.connectGoEasy(targetUserId);
}
},
isConnected() {
const s = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected';
return s === 'connected' || s === 'reconnected';
},
connectGoEasy(userId) {
@@ -84,7 +158,7 @@ Page({
id: userId,
data: {
name: currentUser.name,
avatar: resolveAvatarUrl(currentUser.avatar, app)
avatar: resolveAvatarUrl(currentUser.avatar, app) || getDefaultAvatarUrl(app)
},
onSuccess: () => {
this.setupAllListeners();
@@ -119,11 +193,13 @@ Page({
initCurrentUser() {
const imId = getLocalImUserId(app);
const uid = wx.getStorageSync('uid');
const defAvatar = getDefaultAvatarUrl(app);
const avatar = resolveAvatarFromStorage(app) || defAvatar;
this.setData({
currentUser: {
id: imId || ('Boss' + uid),
name: app.globalData.currentUser?.name || `用户${uid}`,
avatar: resolveAvatarUrl(wx.getStorageSync('touxiang'), app),
name: app.globalData.currentUser?.name || wx.getStorageSync('nicheng') || `用户${uid}`,
avatar,
},
});
},
@@ -169,66 +245,89 @@ Page({
this.setData({ messages: msgs });
},
mapHistoryMessage(m, idx, list) {
m = this.normalizeMessage(m);
m.formattedTime = formatDate(m.timestamp);
if (idx === 0) m.showTime = true;
else m.showTime = (m.timestamp - list[idx - 1].timestamp) / 60000 > 5;
if (m.senderId === this.data.currentUser?.id) {
const av = resolveAvatarUrl(this.data.currentUser.avatar, app) || this.data.defaultAvatarUrl;
m.senderData = { name: this.data.currentUser.name, avatar: av };
} else {
const av = resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl;
m.senderData = { name: this.data.teamName, avatar: av };
}
return m;
},
injectWelcomeMessage(messages) {
const list = Array.isArray(messages) ? [...messages] : [];
const welcomeId = 'welcome-cs';
if (!list.some((m) => m.messageId === welcomeId)) {
const welcome = this.createWelcomeMessage();
welcome.messageId = welcomeId;
welcome.showTime = true;
list.unshift(welcome);
}
return list;
},
// 历史消息
loadHistory(refresh) {
if (!refresh && !this.data.hasMore) return Promise.resolve();
return new Promise((resolve) => {
this.setData({ loadingHistory: true });
const { teamId, lastTimestamp } = this.data;
const ts = refresh ? null : lastTimestamp;
wx.goEasy.im.history({
type: wx.GoEasy.IM_SCENE.CS,
id: teamId,
async loadHistory(refresh) {
if (!refresh && !this.data.hasMore) return;
if (this.data.loadingHistory) return;
if (!this.data.teamId) return;
this.setData({ loadingHistory: true });
try {
const ready = await waitChatImReady(app, 12000);
if (!ready) {
wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' });
return;
}
const ts = refresh
? null
: getOldestHistoryTimestamp(this.data.messages, this.data.lastTimestamp);
if (!refresh && ts == null) {
this.setData({ hasMore: false });
wx.showToast({ title: '没有更早的消息了', icon: 'none' });
return;
}
const raw = await fetchHistoryMessages({
scene: wx.GoEasy.IM_SCENE.CS,
id: this.data.teamId,
lastTimestamp: ts,
limit: 20,
onSuccess: (res) => {
let list = res.content || [];
list.sort((a, b) => a.timestamp - b.timestamp);
list.forEach((m, idx) => {
m = this.normalizeMessage(m);
list[idx] = m;
m.formattedTime = formatDate(m.timestamp);
if (idx === 0) m.showTime = true;
else m.showTime = (m.timestamp - list[idx-1].timestamp) / 60000 > 5;
if (m.senderId === this.data.currentUser.id) {
m.senderData = { name: this.data.currentUser.name, avatar: this.data.currentUser.avatar };
} else {
m.senderData = { name: this.data.teamName, avatar: this.data.teamAvatar };
}
});
let final = refresh ? list : [...list, ...this.data.messages];
if (!refresh) {
for (let i=0; i<final.length; i++) {
if (i===0) final[i].showTime = true;
else final[i].showTime = (final[i].timestamp - final[i-1].timestamp) / 60000 > 5;
}
}
const ids = new Set();
const unique = [];
for (const m of final) { if (!ids.has(m.messageId)) { ids.add(m.messageId); unique.push(m); } }
// 插入欢迎语
const welcomeId = 'welcome-cs';
if (!unique.some(m => m.messageId === welcomeId)) {
const welcome = this.createWelcomeMessage();
welcome.messageId = welcomeId;
welcome.showTime = true;
unique.unshift(welcome);
}
const update = {
messages: unique,
hasMore: list.length >= 20,
lastTimestamp: unique.length ? unique[0].timestamp : lastTimestamp,
loadingHistory: false
};
if (refresh) update.scrollToView = 'msg-bottom';
this.setData(update);
resolve();
},
onFailed: () => { this.setData({ loadingHistory: false }); resolve(); }
});
});
let merged = mergeHistoryMessages({
incoming: raw,
existing: this.data.messages,
refresh,
limit: 20,
mapMessage: (m, idx, list) => this.mapHistoryMessage(m, idx, list),
});
merged.messages = this.injectWelcomeMessage(merged.messages);
merged.lastTimestamp = getOldestHistoryTimestamp(merged.messages, merged.lastTimestamp);
const update = {
messages: merged.messages,
hasMore: merged.hasMore,
lastTimestamp: merged.lastTimestamp,
};
if (refresh) update.scrollToView = 'msg-bottom';
this.setData(update);
} catch (e) {
console.warn('[客服] 历史消息加载失败', e);
if (!refresh) {
wx.showToast({ title: '加载历史消息失败', icon: 'none' });
}
} finally {
this.setData({ loadingHistory: false });
}
},
createWelcomeMessage() {
@@ -238,8 +337,11 @@ Page({
timestamp: Date.now(),
senderId: 'system',
receiverId: this.data.currentUser?.id,
senderData: { name: '系统消息', avatar: '' },
payload: { text: '你好,请问有什么可以帮到您的?' },
senderData: {
name: this.data.teamName || '客服',
avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl,
},
payload: { text: this.data.welcomeText || '你好,请问有什么可以帮到您的?' },
status: 'success',
read: false,
formattedTime: formatDate(Date.now()),
@@ -255,7 +357,10 @@ Page({
if (msg.senderId === this.data.currentUser.id) return;
msg = this.normalizeMessage(msg);
msg.formattedTime = formatDate(msg.timestamp);
msg.senderData = { name: this.data.teamName, avatar: this.data.teamAvatar };
msg.senderData = {
name: this.data.teamName,
avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl,
};
const msgs = this.data.messages;
const last = msgs.length ? msgs[msgs.length-1] : null;
msg.showTime = last ? (msg.timestamp - last.timestamp) / 60000 > 5 : true;
@@ -370,11 +475,53 @@ Page({
});
wx.goEasy.im.sendMessage({
message:msg,
onSuccess:() => this.updateMsg(id, 'success'),
onSuccess:() => {
this.updateMsg(id, 'success');
this.tryAutoReply(text);
},
onFailed:() => this.updateMsg(id, 'failed')
});
},
async tryAutoReply(userText) {
try {
const result = await matchAutoReply(userText);
if (result && result.matched && result.content) {
this.insertCsAutoReply(result);
}
} catch (e) {
console.warn('tryAutoReply failed', e);
}
},
insertCsAutoReply(result) {
const replyType = (result && result.reply_type) || 'text';
const content = (result && result.content) || '';
if (!content) return;
const isImage = replyType === 'image';
const msg = {
messageId: 'auto-reply-' + Date.now(),
type: isImage ? 'image' : 'text',
timestamp: Date.now(),
senderId: this.data.teamId,
receiverId: this.data.currentUser?.id,
senderData: {
name: this.data.teamName,
avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl,
},
payload: isImage ? { url: content } : { text: content },
status: 'success',
read: false,
formattedTime: formatDate(Date.now()),
showTime: this.needShow(Date.now()),
};
if (isImage) msg.imageUrl = content;
this.setData({
messages: [...this.data.messages, msg],
scrollToView: 'msg-bottom',
});
},
sendImageMsg(filePath, fileObj) {
const { currentUser, teamId, teamName, teamAvatar } = this.data;
if (!teamId || !currentUser || !filePath) return;
@@ -521,9 +668,30 @@ Page({
onKeyboardHeightChange(e) { this.setData({ keyboardHeight: e.detail.height }); },
onTeamAvatarError() {
this.setData({ teamAvatar: getDefaultAvatarUrl(app) });
},
onSelfAvatarError() {
const def = getDefaultAvatarUrl(app);
const cu = this.data.currentUser;
if (cu) {
this.setData({ currentUser: { ...cu, avatar: def } });
}
},
onPullDownRefresh() {
if (this.data.loadingHistory) return;
if (!this.data.hasMore) {
wx.showToast({ title: '没有更早的消息了', icon: 'none' });
this.setData({ isRefreshing: false });
wx.stopPullDownRefresh();
return;
}
this.setData({ isRefreshing: true });
this.loadHistory(false).finally(() => { this.setData({ isRefreshing: false }); wx.stopPullDownRefresh(); });
this.loadHistory(false).finally(() => {
this.setData({ isRefreshing: false });
wx.stopPullDownRefresh();
});
}
});
+4 -4
View File
@@ -11,7 +11,7 @@
<view class="chat-time-tag" wx:if="{{item.showTime}}">{{item.formattedTime}}</view>
<view class="chat-msg-row {{item.senderId === currentUser.id ? 'chat-msg-row--right' : 'chat-msg-row--left'}}">
<!-- 对方头像(客服) -->
<image wx:if="{{item.senderId !== currentUser.id}}" class="chat-avatar" src="{{teamAvatar}}" mode="aspectFill" />
<image wx:if="{{item.senderId !== currentUser.id}}" class="chat-avatar" src="{{defaultAvatarUrl}}" mode="aspectFill" binderror="onTeamAvatarError" />
<view class="chat-bubble {{item.senderId === currentUser.id ? 'chat-bubble--right' : 'chat-bubble--left'}}"
data-messageid="{{item.messageId}}"
data-text="{{item.type==='text' ? item.payload.text : ''}}"
@@ -24,19 +24,19 @@
<view class="chat-order-info">
<text>订单号:{{item.payload.dingdan_id || '无'}}</text>
<text>服务:{{item.payload.jieshao || ''}}</text>
<text style="color:#a855f7;font-weight:bold">¥{{item.payload.jine}}</text>
<text style="color:#e6a23c;font-weight:bold">¥{{item.payload.jine}}</text>
</view>
<button class="chat-copy-btn" catchtap="showOrderDetailPopup" data-order="{{item.payload}}" size="mini">查看详情</button>
</view>
<text wx:else class="chat-msg-text">{{item.payload.text || '[未知消息]'}}</text>
</view>
<!-- 自己头像 -->
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{currentUser.avatar}}" mode="aspectFill" />
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{currentUser.avatar || defaultAvatarUrl}}" mode="aspectFill" binderror="onSelfAvatarError" />
</view>
<!-- 🔥 已读/未读标签已彻底移除 -->
</view>
</block>
<view id="msg-bottom" style="height:20rpx;"></view>
<view id="msg-bottom" style="height:56rpx;"></view>
</scroll-view>
<!-- 底部输入区域(保持不变) -->
+283
View File
@@ -0,0 +1,283 @@
import { createPage, request } from '../../utils/base-page.js';
import { refreshDashouMembership } from '../../utils/dashou-profile.js';
import { isApiSuccess, getApiMsg } from '../../utils/api-helper.js';
const LABELS = ['A', 'B', 'C', 'D', 'E', 'F'];
function toSlotNum(slot) {
const n = Number(slot);
return Number.isFinite(n) ? n : slot;
}
function normalizeSlots(slots) {
return (slots || []).map(toSlotNum);
}
function slotsEqual(a, b) {
const sa = normalizeSlots(a).sort((x, y) => x - y);
const sb = normalizeSlots(b).sort((x, y) => x - y);
return sa.length === sb.length && sa.every((v, i) => v == sb[i]);
}
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
Page(createPage({
data: {
loading: true,
alreadyPassed: false,
finished: false,
blocked: false,
blockTitle: '',
blockReason: '',
needRecharge: false,
questions: [],
currentIndex: 0,
currentQuestion: null,
questionType: 1,
total: 0,
wrongCount: 0,
maxWrong: 0,
correctCount: 0,
selectedSlots: [],
showExplanation: false,
},
onLoad() {
this.initExam();
},
showBlocked(title, reason, needRecharge = false) {
this.setData({
loading: false,
blocked: true,
blockTitle: title,
blockReason: reason,
needRecharge: !!needRecharge,
});
},
/** 进入页先查 status:不满足条件留在本页提示,不自动退出 */
async initExam() {
this.setData({ loading: true, blocked: false, needRecharge: false });
try {
await refreshDashouMembership(getApp());
const res = await request({ url: '/jituan/dashou-exam/status', method: 'POST' });
const body = res?.data;
if (!isApiSuccess(body)) {
this.showBlocked('无法开始考试', getApiMsg(body) || '无法获取考试状态,请稍后重试');
return;
}
const d = body.data || {};
if (!d.exam_enabled) {
this.showBlocked(
'未开启考试',
'本俱乐部尚未开启接单考试。若后台已开启,请确认配置的是当前小程序对应俱乐部。'
);
return;
}
if (d.exam_passed) {
this.setData({ loading: false, alreadyPassed: true });
return;
}
const cfg = d.config || {};
if (cfg.require_member && !d.has_member) {
this.showBlocked('须先开通会员', '本俱乐部要求开通会员后才能参加考试', true);
return;
}
if ((d.pool_count || 0) < 1) {
this.showBlocked(
'暂无可用考题',
'考试已开启,但题库没有「已上架」的题目。请在后台确认题目已上架,且每题至少两个选项并标记正确答案。'
);
return;
}
await this.loadBundle();
} catch (e) {
this.showBlocked('网络错误', '请检查网络后下拉重新进入本页');
}
},
async loadBundle() {
this.setData({ loading: true, finished: false, showExplanation: false, blocked: false });
try {
const res = await request({ url: '/jituan/dashou-exam/bundle', method: 'POST' });
const body = res?.data;
if (!isApiSuccess(body)) {
this.showBlocked('加载考题失败', getApiMsg(body) || '请稍后重试或联系管理员');
return;
}
const d = body.data || {};
if (d.already_passed) {
this.setData({ alreadyPassed: true, loading: false });
return;
}
const cfg = d.config || {};
const prepared = (d.questions || []).map((q) => this.prepareQuestion(q)).filter((q) => (q.displayOptions || []).length > 0);
if (!prepared.length) {
this.showBlocked(
'题目配置不完整',
'已抽到的题目缺少有效选项,请在后台检查题目是否至少两个选项并标记正确答案。'
);
return;
}
this.setData({
loading: false,
questions: prepared,
total: prepared.length,
currentIndex: 0,
wrongCount: 0,
correctCount: 0,
maxWrong: cfg.max_wrong || 0,
currentQuestion: prepared[0],
questionType: prepared[0].question_type || 1,
selectedSlots: [],
});
} catch (e) {
this.showBlocked('网络错误', '拉取考题失败,请稍后重试');
}
},
prepareQuestion(q) {
const oss = getApp().globalData.ossImageUrl || '';
const displayImages = (q.images || []).map((u) => (u.startsWith('http') ? u : oss + u));
const shuffled = shuffle(q.options || []).map((opt, idx) => ({
slot: toSlotNum(opt.slot),
text: opt.text,
label: LABELS[idx] || String(idx + 1),
selected: false,
}));
return {
...q,
question_type: Number(q.question_type) || 1,
displayImages,
displayOptions: shuffled,
correctSlots: normalizeSlots(q.correct_slots || []),
};
},
applySelectedToOptions(selectedSlots) {
const q = this.data.currentQuestion;
if (!q) return;
const set = normalizeSlots(selectedSlots);
const displayOptions = (q.displayOptions || []).map((opt) => ({
...opt,
selected: set.some((s) => s == opt.slot),
}));
this.setData({
selectedSlots: set,
currentQuestion: { ...q, displayOptions },
});
},
onSelectOption(e) {
const slot = toSlotNum(e.currentTarget.dataset.slot);
const q = this.data.currentQuestion;
if (!q || slot === undefined || slot === null || slot === '') return;
if (q.question_type === 2) {
const set = [...this.data.selectedSlots];
const i = set.findIndex((s) => s == slot);
if (i >= 0) set.splice(i, 1);
else set.push(slot);
if (this.data.showExplanation) {
this.setData({ showExplanation: false });
}
this.applySelectedToOptions(set);
return;
}
this.applySelectedToOptions([slot]);
if (this.data.showExplanation) {
this.setData({ showExplanation: false });
}
setTimeout(() => this.gradeAnswer([slot]), 120);
},
confirmMulti() {
if (this.data.showExplanation) {
this.setData({ showExplanation: false });
this.applySelectedToOptions([]);
return;
}
if (!this.data.selectedSlots.length) {
wx.showToast({ title: '请选择答案', icon: 'none' });
return;
}
this.gradeAnswer([...this.data.selectedSlots]);
},
/** 前端本地判分(bundle 已含正确答案与解析) */
gradeAnswer(selected) {
const q = this.data.currentQuestion;
const correct = q.correctSlots || [];
const isRight = slotsEqual(selected, correct);
if (!isRight) {
const wrongCount = this.data.wrongCount + 1;
this.setData({ wrongCount, showExplanation: true });
if (wrongCount > this.data.maxWrong) {
wx.showModal({
title: '答题失败',
content: `${q.explanation || '本题答错了'}。错题过多,将重新开始本轮考试。`,
showCancel: false,
success: () => this.loadBundle(),
});
} else {
wx.showToast({ title: '答错了,请查看解析', icon: 'none' });
}
return;
}
const correctCount = this.data.correctCount + 1;
const next = this.data.currentIndex + 1;
if (next >= this.data.total) {
this.submitPassed(correctCount);
return;
}
const nextQ = this.data.questions[next];
this.setData({
correctCount,
currentIndex: next,
currentQuestion: nextQ,
questionType: nextQ.question_type,
selectedSlots: [],
showExplanation: false,
});
},
async submitPassed(correctCount) {
try {
const res = await request({ url: '/jituan/dashou-exam/mark-passed', method: 'POST' });
const body = res?.data;
if (isApiSuccess(body)) {
this.setData({ finished: true, correctCount });
} else {
wx.showToast({ title: getApiMsg(body) || '提交失败', icon: 'none' });
}
} catch (e) {
wx.showToast({ title: '提交失败', icon: 'none' });
}
},
goRecharge() {
wx.navigateTo({ url: '/pages/fighter-recharge/fighter-recharge' });
},
retryInit() {
this.initExam();
},
goBack() {
const pages = getCurrentPages();
if (pages.length > 1) {
wx.navigateBack();
} else {
wx.switchTab({ url: '/pages/accept-order/accept-order' });
}
},
}));
+6
View File
@@ -0,0 +1,6 @@
{
"usingComponents": {},
"navigationBarTitleText": "接单考试",
"navigationBarBackgroundColor": "#c4b5fd",
"navigationBarTextStyle": "black"
}
+66
View File
@@ -0,0 +1,66 @@
<view class="exam-page">
<view wx:if="{{loading}}" class="center-tip">正在检查考试资格...</view>
<view wx:elif="{{blocked}}" class="center-box">
<text class="tip-title">{{blockTitle}}</text>
<text class="tip-desc">{{blockReason}}</text>
<view class="btn-row">
<view wx:if="{{needRecharge}}" class="btn-primary block" bindtap="goRecharge">去开通会员</view>
<view class="btn-outline block" bindtap="retryInit">重试</view>
<view class="btn-outline block" bindtap="goBack">返回</view>
</view>
</view>
<view wx:elif="{{alreadyPassed}}" class="center-box">
<text class="tip-title">已通过考试</text>
<text class="tip-desc">无需重复考试,可直接抢单</text>
<view class="btn-primary block" bindtap="goBack">返回</view>
</view>
<view wx:elif="{{finished}}" class="center-box">
<text class="tip-title">考试通过</text>
<text class="tip-desc">恭喜通过,现在可以去抢单了</text>
<view class="btn-primary block" bindtap="goBack">去抢单</view>
</view>
<view wx:elif="{{currentQuestion}}" class="exam-body">
<view class="exam-card">
<view class="progress">第 {{currentIndex + 1}} / {{total}} 题</view>
<view class="progress-sub">已错 {{wrongCount}} 题 · 最多可错 {{maxWrong}} 题</view>
<view class="q-type-tag">{{questionType === 2 ? '多选题' : '单选题'}}</view>
<view class="q-stem">{{currentQuestion.stem}}</view>
<view class="q-images" wx:if="{{currentQuestion.displayImages && currentQuestion.displayImages.length}}">
<image wx:for="{{currentQuestion.displayImages}}" wx:key="*this" class="q-img" src="{{item}}" mode="widthFix" />
</view>
<view class="options">
<view
wx:for="{{currentQuestion.displayOptions}}"
wx:key="slot"
class="opt-item {{item.selected ? 'opt-active' : ''}}"
data-slot="{{item.slot}}"
bindtap="onSelectOption"
>
<view class="opt-radio {{item.selected ? 'opt-radio-on' : ''}}">
<view wx:if="{{item.selected}}" class="opt-radio-dot"></view>
</view>
<view class="opt-body">
<text class="opt-label">{{item.label}}</text>
<text class="opt-text">{{item.text}}</text>
</view>
</view>
</view>
<view wx:if="{{showExplanation}}" id="explain-box" class="explain-box">
<text class="explain-title">解析</text>
<text class="explain-text">{{currentQuestion.explanation || '暂无解析'}}</text>
</view>
</view>
<view class="foot-actions" wx:if="{{questionType === 2}}">
<view class="btn-primary foot-btn" bindtap="confirmMulti">{{showExplanation ? '重新作答' : '确认本题'}}</view>
</view>
</view>
</view>
+228
View File
@@ -0,0 +1,228 @@
.exam-page {
min-height: 100vh;
background: linear-gradient(180deg, #f5f3ff 0%, #fff3d6 100%);
padding: 24rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.center-tip,
.center-box {
padding: 80rpx 40rpx;
text-align: center;
}
.tip-title {
font-size: 36rpx;
font-weight: 600;
display: block;
margin-bottom: 16rpx;
color: #333;
}
.tip-desc {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 32rpx;
line-height: 1.6;
}
.btn-primary {
background: linear-gradient(90deg, #c4b5fd, #a78bfa);
padding: 22rpx 48rpx;
border-radius: 48rpx;
font-weight: 600;
font-size: 30rpx;
color: #6d28d9;
text-align: center;
}
.btn-primary.block {
display: block;
width: 100%;
box-sizing: border-box;
}
.btn-row {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-top: 24rpx;
}
.btn-outline {
padding: 20rpx 48rpx;
border-radius: 48rpx;
border: 2rpx solid #ddd;
color: #666;
font-size: 28rpx;
text-align: center;
background: #fff;
}
.btn-outline.block {
display: block;
width: 100%;
box-sizing: border-box;
}
.exam-body {
display: flex;
flex-direction: column;
min-height: calc(100vh - 48rpx);
}
.exam-card {
flex: 1;
background: #fff;
border-radius: 24rpx;
padding: 28rpx;
box-shadow: 0 8rpx 32rpx rgba(73, 47, 0, 0.06);
}
.progress {
font-size: 28rpx;
color: #6d28d9;
font-weight: 600;
}
.progress-sub {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
margin-bottom: 24rpx;
}
.q-type-tag {
display: inline-block;
font-size: 22rpx;
color: #b8860b;
background: #f5f3ff;
border: 1rpx solid #c4b5fd;
border-radius: 8rpx;
padding: 4rpx 16rpx;
margin-bottom: 16rpx;
}
.q-stem {
font-size: 32rpx;
font-weight: 600;
line-height: 1.6;
margin-bottom: 24rpx;
color: #222;
}
.q-images {
margin-bottom: 24rpx;
}
.q-img {
width: 100%;
border-radius: 12rpx;
margin-bottom: 12rpx;
}
.options {
margin-top: 8rpx;
}
.opt-item {
background: #fafafa;
border: 2rpx solid #eee;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
display: flex;
align-items: flex-start;
gap: 20rpx;
}
.opt-active {
border-color: #c4b5fd;
background: #fffdf0;
}
.opt-radio {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
flex-shrink: 0;
margin-top: 4rpx;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.opt-radio-on {
border-color: #c4b5fd;
background: #f5f3ff;
}
.opt-radio-dot {
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background: #c4b5fd;
}
.opt-body {
flex: 1;
display: flex;
gap: 12rpx;
align-items: flex-start;
}
.opt-label {
font-weight: 700;
color: #6d28d9;
min-width: 40rpx;
font-size: 28rpx;
}
.opt-text {
flex: 1;
font-size: 28rpx;
line-height: 1.5;
color: #333;
}
.explain-box {
background: #fffbf0;
border-left: 6rpx solid #e6a23c;
padding: 20rpx 24rpx;
margin-top: 24rpx;
border-radius: 0 12rpx 12rpx 0;
}
.explain-title {
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
font-size: 28rpx;
}
.explain-text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
.foot-actions {
margin-top: 24rpx;
padding: 0 8rpx;
}
.foot-btn {
width: 100%;
box-sizing: border-box;
min-height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
line-height: 1.2;
padding: 0 32rpx;
}
+14
View File
@@ -0,0 +1,14 @@
/** 旧二维码兼容:dashouduan → 打手中心,inviteCode 自动注册逻辑不变 */
import { parseSceneOptions } from '../../utils/base-page.js';
Page({
onLoad(options) {
const parsed = parseSceneOptions(options || {});
const inviteCode = parsed.inviteCode || options.inviteCode || '';
let url = '/pages/fighter/fighter';
if (inviteCode) {
url += `?inviteCode=${encodeURIComponent(inviteCode)}`;
}
wx.reLaunch({ url });
},
});
+1
View File
@@ -0,0 +1 @@
{ "navigationBarTitleText": "加载中", "usingComponents": {} }
+1
View File
@@ -0,0 +1 @@
<view class="tip">正在进入打手端…</view>
+1
View File
@@ -0,0 +1 @@
.tip { display:flex; align-items:center; justify-content:center; min-height:100vh; color:#999; font-size:28rpx; }
+278 -129
View File
@@ -1,6 +1,9 @@
// pages/edit/edit.js
// 点单端设置页;纯打手端时:头像走 /yonghu/xiugai,昵称走 /yonghu/dsgxxx,手机只读(点单端已认证手机号)
const app = getApp()
import upload from '../../utils/upload.js'
import request from '../../utils/request.js'
import { isFighterOnly } from '../../utils/fighter-only.js'
Page({
data: {
@@ -15,54 +18,55 @@ Page({
newAvatarFile: null,
isLoading: true,
isAvatarActive: false,
nicknameError: ''
nicknameError: '',
// 打手扩展字段缓存,更新昵称时原样带回,避免后端把空串覆盖掉
_dsDianhua: '',
_dsWeixin: '',
_dsJieshao: '',
fighterMode: false,
},
onLoad() {
const fighterMode = isFighterOnly()
this.setData({ fighterMode })
wx.setNavigationBarTitle({ title: '编辑资料' })
this.loadUserData()
},
onShow() {
this.registerNotificationComponent();
},
registerNotificationComponent() {
const notificationComp = this.selectComponent('#global-notification');
if (notificationComp && notificationComp.showNotification) {
app.globalData.globalNotification = {
show: (data) => notificationComp.showNotification(data),
hide: () => notificationComp.hideNotification()
};
if (fighterMode) {
this.loadFighterModeData()
} else {
this.loadUserData()
}
},
// 加载用户数据
onShow() {
this.registerNotificationComponent()
},
registerNotificationComponent() {
const notificationComp = this.selectComponent('#global-notification')
if (notificationComp && notificationComp.showNotification) {
app.globalData.globalNotification = {
show: (data) => notificationComp.showNotification(data),
hide: () => notificationComp.hideNotification(),
}
}
},
// ——— 点单端:原逻辑 ———
loadUserData() {
try {
const uid = wx.getStorageSync('uid') || ''
const nicheng = wx.getStorageSync('nicheng') || ''
const phone = wx.getStorageSync('phone') || ''
const touxiang = wx.getStorageSync('touxiang') || ''
const ossImageUrl = app.globalData.ossImageUrl || ''
const defaultAvatar = app.globalData.morentouxiang || ''
let avatarUrl = ''
if (touxiang) {
avatarUrl = touxiang.startsWith('http') ? touxiang : `${ossImageUrl}${touxiang}`
} else if (defaultAvatar) {
avatarUrl = defaultAvatar.startsWith('http') ? defaultAvatar : `${ossImageUrl}${defaultAvatar}`
}
this.setData({
avatarUrl,
avatarUrl: this.buildAvatarUrl(touxiang),
uid,
nicheng,
phone,
originalAvatar: touxiang,
originalNicheng: nicheng,
isLoading: false
isLoading: false,
})
} catch (error) {
console.error('加载用户数据失败:', error)
@@ -71,157 +75,302 @@ Page({
}
},
// 微信头像选择
// ——— 纯打手端:手机/头像用点单端存储;昵称用打手资料 ———
async loadFighterModeData() {
this.setData({ isLoading: true })
try {
const uid = wx.getStorageSync('uid') || ''
const phone = wx.getStorageSync('phone') || ''
const touxiang = wx.getStorageSync('touxiang') || ''
let nicheng =
wx.getStorageSync('dsnc') ||
app.globalData.dashouNicheng ||
''
let dsDianhua = wx.getStorageSync('dsdianhua') || ''
let dsWeixin = wx.getStorageSync('dsweixin') || ''
let dsJieshao = wx.getStorageSync('dsjieshao') || ''
// 昵称缺失时拉打手资料(不碰点单端昵称)
if (!nicheng || !String(nicheng).trim()) {
try {
const res = await request({ url: '/yonghu/dszjxxhq', method: 'POST' })
if (res && res.data && res.data.code === 200 && res.data.data) {
const d = res.data.data
nicheng = d.nicheng || ''
dsDianhua = d.dianhua || dsDianhua
dsWeixin = d.wechat || dsWeixin
dsJieshao = d.jieshao || dsJieshao
wx.setStorageSync('dsnc', nicheng)
wx.setStorageSync('dsdianhua', dsDianhua)
wx.setStorageSync('dsweixin', dsWeixin)
wx.setStorageSync('dsjieshao', dsJieshao)
}
} catch (e) {
console.warn('拉取打手昵称失败', e)
}
}
this.setData({
avatarUrl: this.buildAvatarUrl(touxiang),
uid,
phone,
nicheng,
originalAvatar: touxiang,
originalNicheng: nicheng,
_dsDianhua: dsDianhua,
_dsWeixin: dsWeixin,
_dsJieshao: dsJieshao,
isLoading: false,
})
} catch (error) {
console.error('加载打手设置数据失败:', error)
this.setData({ isLoading: false })
wx.showToast({ title: '加载数据失败', icon: 'none' })
}
},
buildAvatarUrl(touxiang) {
const ossImageUrl = app.globalData.ossImageUrl || ''
const defaultAvatar = app.globalData.morentouxiang || ''
if (touxiang) {
return touxiang.startsWith('http') ? touxiang : `${ossImageUrl}${touxiang}`
}
if (defaultAvatar) {
return defaultAvatar.startsWith('http') ? defaultAvatar : `${ossImageUrl}${defaultAvatar}`
}
return ''
},
onChooseAvatar(e) {
const avatarTempPath = e.detail.avatarUrl
this.setData({
avatarUrl: avatarTempPath,
newAvatarFile: avatarTempPath
}, () => {
this.checkChanges()
})
this.setData(
{
avatarUrl: avatarTempPath,
newAvatarFile: avatarTempPath,
},
() => this.checkChanges()
)
},
chooseAvatar() {
this.setData({ isAvatarActive: true })
setTimeout(() => {
this.setData({ isAvatarActive: false })
}, 500)
setTimeout(() => this.setData({ isAvatarActive: false }), 500)
},
// 昵称输入
onNicknameInput(e) {
const value = e.detail.value.trim()
this.setData({
nicheng: value,
nicknameError: ''
}, () => {
this.checkChanges()
})
this.setData(
{
nicheng: value,
nicknameError: '',
},
() => this.checkChanges()
)
},
// 检查变化
checkChanges() {
const { originalNicheng, nicheng, newAvatarFile } = this.data;
const hasChanges = (nicheng !== originalNicheng) || (newAvatarFile !== null);
this.setData({ hasChanges });
const { originalNicheng, nicheng, newAvatarFile } = this.data
const hasChanges = nicheng !== originalNicheng || newAvatarFile !== null
this.setData({ hasChanges })
},
validateData() {
const { nicheng } = this.data
let hasError = false
this.setData({ nicknameError: '' })
if (nicheng && nicheng.length > 20) {
this.setData({ nicknameError: '昵称不能超过20个字符' })
hasError = true
}
if (nicheng !== this.data.originalNicheng && nicheng.trim() === '') {
this.setData({ nicknameError: '昵称不能为空' })
hasError = true
}
if (hasError) {
if (this.data.nicknameError) {
wx.showToast({ title: this.data.nicknameError, icon: 'none' })
}
return false
}
return true
},
// 保存
async onSave() {
if (!this.data.hasChanges) {
wx.showToast({ title: '没有需要保存的更改', icon: 'none' });
return;
wx.showToast({ title: '没有需要保存的更改', icon: 'none' })
return
}
if (this.data.isSaving) return;
// 验证数据
if (!this.validateData()) return;
this.setData({ isSaving: true });
wx.showLoading({ title: '保存中...', mask: true });
if (this.data.isSaving) return
if (!this.validateData()) return
this.setData({ isSaving: true })
wx.showLoading({ title: '保存中...', mask: true })
try {
const formData = {};
if (this.data.nicheng !== this.data.originalNicheng) {
formData.nicheng = this.data.nicheng;
}
const res = await upload({
url: '/yonghu/xiugai',
formData,
filePath: this.data.newAvatarFile,
fileName: 'avatar'
});
if (res.statusCode === 200 && res.data) {
const response = res.data;
if (response.code === 0) {
await this.handleUpdateSuccess(response.data);
} else {
throw new Error(response.msg || '保存失败');
}
if (this.data.fighterMode) {
await this.saveFighterMode()
} else {
throw new Error('网络错误');
await this.saveOrderMode()
}
} catch (error) {
console.error('保存失败:', error);
if (error.message.includes('认证失败') || error.message.includes('401')) {
console.error('保存失败:', error)
if (error.message && (error.message.includes('认证失败') || error.message.includes('401'))) {
wx.showModal({
title: '提示',
content: '登录已过期,请重新登录',
complete: (res) => {
if (res.confirm) {
wx.removeStorageSync('token');
wx.removeStorageSync('userInfo');
wx.switchTab({ url: '/pages/mine/mine' });
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
wx.reLaunch({ url: '/pages/fighter/fighter' })
}
}
});
},
})
} else {
wx.showToast({ title: error.message || '保存失败', icon: 'none' });
wx.showToast({ title: error.message || '保存失败', icon: 'none' })
}
} finally {
wx.hideLoading();
this.setData({ isSaving: false });
wx.hideLoading()
this.setData({ isSaving: false })
}
},
// 验证数据
validateData() {
const { nicheng } = this.data;
let hasError = false;
this.setData({ nicknameError: '' });
if (nicheng && nicheng.length > 20) {
this.setData({ nicknameError: '昵称不能超过20个字符' });
hasError = true;
/** 点单端原保存:昵称+头像 → /yonghu/xiugai */
async saveOrderMode() {
const formData = {}
if (this.data.nicheng !== this.data.originalNicheng) {
formData.nicheng = this.data.nicheng
}
if (nicheng !== this.data.originalNicheng && nicheng.trim() === '') {
this.setData({ nicknameError: '昵称不能为空' });
hasError = true;
const res = await upload({
url: '/yonghu/xiugai',
formData,
filePath: this.data.newAvatarFile,
fileName: 'avatar',
})
if (res.statusCode === 200 && res.data && res.data.code === 0) {
await this.handleUpdateSuccess(res.data.data || {})
} else {
throw new Error((res.data && (res.data.msg || res.data.message)) || '保存失败')
}
if (hasError) {
if (this.data.nicknameError) {
wx.showToast({ title: this.data.nicknameError, icon: 'none' });
},
/**
* 纯打手端混调:
* - 头像变了 → /yonghu/xiugai(点单端,只传头像,不改点单昵称)
* - 昵称变了 → /yonghu/dsgxxx(打手端,带回其它打手字段防覆盖)
* 手机号只读,不提交
*/
async saveFighterMode() {
const nickChanged = this.data.nicheng !== this.data.originalNicheng
const avatarChanged = !!this.data.newAvatarFile
let avatarData = null
let nickData = null
if (avatarChanged) {
const res = await upload({
url: '/yonghu/xiugai',
formData: {},
filePath: this.data.newAvatarFile,
fileName: 'avatar',
})
if (!(res.statusCode === 200 && res.data && res.data.code === 0)) {
throw new Error((res.data && (res.data.msg || res.data.message)) || '头像保存失败')
}
return false;
avatarData = res.data.data || {}
}
return true;
if (nickChanged) {
const postData = {
nicheng: this.data.nicheng,
dianhua: this.data._dsDianhua || wx.getStorageSync('dsdianhua') || '',
wechat: this.data._dsWeixin || wx.getStorageSync('dsweixin') || '',
jieshao: this.data._dsJieshao || wx.getStorageSync('dsjieshao') || '',
}
const res = await request({
url: '/yonghu/dsgxxx',
method: 'POST',
data: postData,
})
if (!(res && res.data && res.data.code === 200)) {
throw new Error((res && res.data && res.data.msg) || '昵称保存失败')
}
nickData = res.data.data || postData
}
this.handleFighterUpdateSuccess(avatarData, nickData)
},
handleFighterUpdateSuccess(avatarData, nickData) {
const ossImageUrl = app.globalData.ossImageUrl || ''
let avatarUrl = this.data.avatarUrl
let originalAvatar = this.data.originalAvatar
let originalNicheng = this.data.originalNicheng
let nicheng = this.data.nicheng
if (avatarData && avatarData.touxiang != null) {
wx.setStorageSync('touxiang', avatarData.touxiang)
originalAvatar = avatarData.touxiang
avatarUrl = avatarData.touxiang.startsWith('http')
? avatarData.touxiang
: `${ossImageUrl}${avatarData.touxiang}`
}
if (nickData) {
const newNick = nickData.nicheng != null ? nickData.nicheng : nicheng
nicheng = newNick
originalNicheng = newNick
wx.setStorageSync('dsnc', newNick)
app.globalData.dashouNicheng = newNick
if (nickData.dianhua != null) wx.setStorageSync('dsdianhua', nickData.dianhua || '')
if (nickData.wechat != null) wx.setStorageSync('dsweixin', nickData.wechat || '')
if (nickData.jieshao != null) wx.setStorageSync('dsjieshao', nickData.jieshao || '')
}
this.setData({
avatarUrl,
nicheng,
originalAvatar,
originalNicheng,
newAvatarFile: null,
hasChanges: false,
_dsDianhua: wx.getStorageSync('dsdianhua') || this.data._dsDianhua,
_dsWeixin: wx.getStorageSync('dsweixin') || this.data._dsWeixin,
_dsJieshao: wx.getStorageSync('dsjieshao') || this.data._dsJieshao,
})
wx.showToast({ title: '保存成功', icon: 'success', duration: 2000 })
},
// 更新成功处理
async handleUpdateSuccess(updateData) {
if (updateData.touxiang !== undefined && updateData.touxiang !== null) {
wx.setStorageSync('touxiang', updateData.touxiang);
wx.setStorageSync('touxiang', updateData.touxiang)
}
if (updateData.nicheng !== undefined && updateData.nicheng !== null) {
wx.setStorageSync('nicheng', updateData.nicheng);
wx.setStorageSync('nicheng', updateData.nicheng)
}
if (updateData.nicheng) {
app.globalData.userNicheng = updateData.nicheng;
app.globalData.userNicheng = updateData.nicheng
}
const ossImageUrl = app.globalData.ossImageUrl || '';
let avatarUrl = this.data.avatarUrl;
const ossImageUrl = app.globalData.ossImageUrl || ''
let avatarUrl = this.data.avatarUrl
if (updateData.touxiang) {
avatarUrl = updateData.touxiang.startsWith('http') ?
updateData.touxiang : `${ossImageUrl}${updateData.touxiang}`;
avatarUrl = updateData.touxiang.startsWith('http')
? updateData.touxiang
: `${ossImageUrl}${updateData.touxiang}`
}
this.setData({
avatarUrl,
originalAvatar: updateData.touxiang || this.data.originalAvatar,
originalNicheng: updateData.nicheng || this.data.originalNicheng,
newAvatarFile: null,
hasChanges: false
});
wx.showToast({
title: '保存成功',
icon: 'success',
duration: 2000
});
}
});
hasChanges: false,
})
wx.showToast({ title: '保存成功', icon: 'success', duration: 2000 })
},
})
+4 -4
View File
@@ -27,14 +27,14 @@
<view class="item-value readonly">{{uid || '••••••'}}</view>
</view>
<!-- 昵称(可编辑) -->
<!-- 昵称(可编辑;纯打手端展示/保存的是打手昵称) -->
<view class="form-item">
<view class="item-label">昵称</view>
<view class="item-label">{{fighterMode ? '服务昵称' : '昵称'}}</view>
<view class="input-container">
<input
class="item-input {{nicknameError ? 'error' : ''}}"
type="text"
placeholder="请输入昵称"
placeholder="{{fighterMode ? '请输入服务昵称' : '请输入昵称'}}"
placeholder-class="placeholder"
maxlength="20"
value="{{nicheng}}"
@@ -44,7 +44,7 @@
</view>
</view>
<!-- 手机号(只读展示,不可修改) -->
<!-- 手机号(只读:点单端已认证号码,不可手填) -->
<view class="form-item">
<view class="item-label">手机号</view>
<view class="item-value readonly">{{phone || '未绑定'}}</view>
+1 -1
View File
@@ -1,6 +1,6 @@
/* pages/edit/edit.wxss */
page {
background: linear-gradient(135deg, #faf5ff 0%, #f0f8ff 100%);
background: linear-gradient(135deg, #fffaf0 0%, #f0f8ff 100%);
min-height: 100vh;
}
+1 -1
View File
@@ -4,7 +4,7 @@
"chenghao-tag": "/components/chenghao-tag/chenghao-tag"
},
"backgroundTextStyle": "dark",
"navigationBarBackgroundColor": "#fff8e1",
"navigationBarBackgroundColor": "#f7dc51",
"navigationBarTitleText": "常规发单",
"navigationBarTextStyle": "black",
"backgroundColor": "#f5f5f5",
+7 -7
View File
@@ -289,7 +289,7 @@ page {
}
.filter-switch.active {
background: linear-gradient(180deg, #c4b5fd, #a78bfa);
background: linear-gradient(180deg, #fae04d, #ffc0a3);
color: #492f00;
}
@@ -323,7 +323,7 @@ page {
.copy-link-btn {
padding: 8rpx 20rpx;
background: linear-gradient(180deg, #c4b5fd, #a78bfa);
background: linear-gradient(180deg, #fae04d, #ffc0a3);
color: #492f00;
border-radius: 15rpx;
font-size: 22rpx;
@@ -337,7 +337,7 @@ page {
.load-more-btn {
display: inline-block;
padding: 15rpx 40rpx;
background: linear-gradient(180deg, #c4b5fd, #a78bfa);
background: linear-gradient(180deg, #fae04d, #ffc0a3);
border-radius: 30rpx;
color: #492f00;
font-size: 26rpx;
@@ -373,7 +373,7 @@ page {
.generate-modal-grid-btn,
.copy-modal-grid-btn,
.edit-modal-grid-btn {
background: linear-gradient(180deg, #c4b5fd, #a78bfa);
background: linear-gradient(180deg, #fae04d, #ffc0a3);
color: #492f00;
}
@@ -484,7 +484,7 @@ page {
}
.confirm-add-grid-btn {
background: linear-gradient(180deg, #c4b5fd, #a78bfa);
background: linear-gradient(180deg, #fae04d, #ffc0a3);
color: #492f00;
}
@@ -538,7 +538,7 @@ page {
}
.confirm-new-link-grid-btn {
background: linear-gradient(180deg, #c4b5fd, #a78bfa);
background: linear-gradient(180deg, #fae04d, #ffc0a3);
color: #492f00;
}
@@ -569,7 +569,7 @@ page {
width: 90rpx;
height: 90rpx;
border: 8rpx solid #eee;
border-top: 8rpx solid #9333ea;
border-top: 8rpx solid #ffd061;
border-radius: 50%;
animation: spin 1.2s linear infinite;
margin-bottom: 35rpx;
+3 -3
View File
@@ -1,7 +1,7 @@
/* 页面容器 */
.container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f3ff 0%, #e6f7ff 100%);
background: linear-gradient(135deg, #fffacd 0%, #e6f7ff 100%);
padding: 20rpx;
box-sizing: border-box;
}
@@ -27,7 +27,7 @@
height: 180rpx;
border-radius: 50%;
border: 6rpx solid #ffffff;
box-shadow: 0 0 30rpx rgba(196, 181, 253, 0.4);
box-shadow: 0 0 30rpx rgba(255, 215, 0, 0.4);
z-index: 2;
position: relative;
background-color: #f5f5f5;
@@ -38,7 +38,7 @@
width: 220rpx;
height: 220rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(196, 181, 253, 0.2) 0%, transparent 70%);
background: radial-gradient(circle, rgba(255, 215, 0, 0.2) 0%, transparent 70%);
z-index: 1;
animation: halo-pulse 2s infinite ease-in-out;
}
+139
View File
@@ -0,0 +1,139 @@
import request from '../../utils/request.js';
import { getOrderStatusText } from '../../utils/api-helper.js';
const PAGE_SIZE = 15;
const TYPE_CONFIG = {
commission: { title: '佣金记录', statuses: [3, 8] },
settle: { title: '结算记录', statuses: [3] },
withdraw: { title: '提现记录', api: 'withdraw' },
deduct: { title: '扣款记录', api: 'deduct' },
};
Page({
data: {
pageTitle: '交易明细',
type: 'commission',
statusBar: 20,
list: [],
page: 1,
hasMore: true,
loading: false,
refreshing: false,
},
onLoad(options) {
const sys = wx.getSystemInfoSync();
const type = (options.type || 'commission').trim();
const cfg = TYPE_CONFIG[type] || TYPE_CONFIG.commission;
this.setData({
type,
pageTitle: cfg.title,
statusBar: sys.statusBarHeight || 20,
});
this.loadList(true);
},
goBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/fighter/fighter' }) });
},
onRefresh() {
this.setData({ refreshing: true });
this.loadList(true).finally(() => this.setData({ refreshing: false }));
},
onLoadMore() {
if (this.data.hasMore && !this.data.loading) {
this.loadList(false);
}
},
async loadList(refresh) {
const { type, loading } = this.data;
if (loading) return;
const page = refresh ? 1 : this.data.page;
this.setData({ loading: true });
try {
let rows = [];
let hasMore = false;
if (type === 'withdraw') {
const res = await request({
url: '/yonghu/tixianjiluhq',
method: 'POST',
data: { page, limit: PAGE_SIZE },
});
const body = res?.data;
if (body && (body.code === 200 || body.code === 0)) {
const raw = body.data?.list || [];
rows = raw.map((item, idx) => ({
id: item.id || item.jilu_id || `${page}-${idx}`,
title: item.beizhu || item.status_text || item.zhuangtai_text || '提现',
time: item.creat_time || item.create_time || item.tijiao_shijian || '',
amount: `-¥${item.jine || item.amount || item.tixian_jine || '0.00'}`,
minus: true,
}));
hasMore = raw.length >= PAGE_SIZE;
}
} else if (type === 'deduct') {
const res = await request({
url: '/yonghu/dsfklbhq',
method: 'POST',
data: { page, page_size: PAGE_SIZE, zhuangtai: 0 },
});
const body = res?.data;
if (body && (body.code === 200 || body.code === 0)) {
const raw = body.data?.list || [];
rows = raw.map((item, idx) => ({
id: item.fadan_id || item.id || `${page}-${idx}`,
title: item.yuanyin || item.beizhu || item.fakuan_yuanyin || '扣款',
time: item.creat_time || item.create_time || '',
amount: `-¥${item.jine || item.fakuan_jine || item.fakuanjine || '0.00'}`,
minus: true,
}));
hasMore = raw.length >= PAGE_SIZE;
}
} else {
const cfg = TYPE_CONFIG[type] || TYPE_CONFIG.commission;
const res = await request({
url: '/dingdan/dshqdingdan',
method: 'POST',
data: {
zhuangtai_list: cfg.statuses,
page,
page_size: PAGE_SIZE,
},
});
const body = res?.data;
if (body && (body.code === 200 || body.code === 0)) {
const raw = body.data?.list || [];
rows = raw.map((item) => {
const amt = item.jine || item.dashou_fencheng || '0.00';
const brief = (item.jieshao || '').trim();
const title = brief
? (brief.length > 18 ? `${brief.slice(0, 18)}…` : brief)
: `订单 ${item.dingdan_id || ''}`;
return {
id: item.dingdan_id,
title,
time: item.wancheng_shijian || item.jiesuan_shijian || item.creat_time || getOrderStatusText(item.zhuangtai),
amount: `+¥${amt}`,
minus: false,
};
});
hasMore = body.data?.has_more ?? raw.length >= PAGE_SIZE;
}
}
const list = refresh ? rows : [...this.data.list, ...rows];
this.setData({
list,
page: refresh ? 2 : page + 1,
hasMore,
});
} catch (e) {
console.error('ledger load fail', e);
} finally {
this.setData({ loading: false });
}
},
});
+4
View File
@@ -0,0 +1,4 @@
{
"navigationStyle": "custom",
"usingComponents": {}
}
+20
View File
@@ -0,0 +1,20 @@
<view class="ledger-page safe-area">
<view class="status-bar" style="height:{{statusBar}}px"></view>
<view class="nav-bar flexb">
<text class="back" bindtap="goBack">‹</text>
<text class="nav-title">{{pageTitle}}</text>
<text class="back placeholder"></text>
</view>
<scroll-view scroll-y class="ledger-scroll" refresher-enabled refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh" bindscrolltolower="onLoadMore">
<view wx:if="{{loading && list.length === 0}}" class="empty-box">加载中...</view>
<view wx:elif="{{list.length === 0}}" class="empty-box">暂无记录</view>
<view wx:for="{{list}}" wx:key="id" class="list-item flexb">
<view class="item-main">
<view class="row-t">{{item.title}}</view>
<view class="row-d">{{item.time}}</view>
</view>
<text class="row-r {{item.minus ? 'minus' : ''}}">{{item.amount}}</text>
</view>
<view wx:if="{{list.length > 0 && !hasMore}}" class="load-tip">— 已加载全部 —</view>
</scroll-view>
</view>
+75
View File
@@ -0,0 +1,75 @@
@import '../../styles/xym-layout.wxss';
.ledger-page {
min-height: 100vh;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
.nav-bar {
padding: 0 24rpx 16rpx;
}
.back {
width: 60rpx;
font-size: 48rpx;
color: #333;
line-height: 1;
}
.back.placeholder {
visibility: hidden;
}
.nav-title {
font-size: 34rpx;
font-weight: 700;
color: #222;
}
.ledger-scroll {
flex: 1;
height: 0;
padding: 0 24rpx;
box-sizing: border-box;
}
.list-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
}
.row-t {
font-size: 28rpx;
color: #222;
font-weight: 600;
}
.row-d {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.row-r {
font-size: 30rpx;
font-weight: 700;
color: #492f00;
flex-shrink: 0;
margin-left: 16rpx;
}
.row-r.minus {
color: #ff4c45;
}
.empty-box,
.load-tip {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 80rpx 0;
}
File diff suppressed because it is too large. Load diff
+6 -6
View File
@@ -296,9 +296,9 @@
}
.zhuangtai-shensuzhong {
background: rgba(147, 51, 234, 0.2);
border-color: #9333ea;
box-shadow: 0 0 8rpx rgba(147, 51, 234, 0.2);
background: rgba(255, 165, 0, 0.2);
border-color: #FFA500;
box-shadow: 0 0 8rpx rgba(255, 165, 0, 0.2);
}
.zhuangtai-weizhi {
@@ -657,9 +657,9 @@
}
.cbtn-shensu {
background: rgba(147, 51, 234, 0.1);
border-color: #9333ea;
box-shadow: 0 0 12rpx rgba(147, 51, 234, 0.2);
background: rgba(255, 165, 0, 0.1);
border-color: #FFA500;
box-shadow: 0 0 12rpx rgba(255, 165, 0, 0.2);
}
.cbtn-shensu-disabled {
File diff suppressed because it is too large. Load diff
@@ -99,6 +99,15 @@
<text class="item-label">订单状态:</text>
<text class="item-value zhuangtai-text">{{zhuangtaiWenziMap[jibenShuju.zhuangtai] || '未知状态'}}</text>
</view>
<view class="info-item-row auto-settle-row" wx:if="{{autoSettleTip || autoSettleClock}}">
<text class="item-label">自动结算:</text>
<view class="auto-settle-value">
<text class="auto-settle-clock" wx:if="{{autoSettleClock}}">{{autoSettleClock}}</text>
<text class="item-value auto-settle-text">{{autoSettleTip}}</text>
<text class="auto-settle-expire" wx:if="{{autoSettleExpireAt}}">到期 {{autoSettleExpireAt}}</text>
</view>
</view>
<!-- 发单平台信息 -->
<view class="info-item-row" wx:if="{{jibenShuju.fadanpingtai !== undefined && jibenShuju.fadanpingtai !== null}}">
@@ -108,6 +117,55 @@
</text>
</view>
</view>
<!-- 组队面板 -->
<view class="team-area" wx:if="{{jibenShuju.zhuangtai == 2 || team}}">
<view class="info-title">组队</view>
<block wx:if="{{!team && jibenShuju.zhuangtai == 2 && isLeader}}">
<text class="team-hint">本单进行中,可开启组队招募(最多再招 3 人)</text>
<view class="team-open-btn" bindtap="openTeamForm">开启组队</view>
</block>
<block wx:if="{{team}}">
<view class="info-item-row">
<text class="item-label">状态:</text>
<text class="item-value">{{teamStatusText}}</text>
</view>
<view class="info-item-row">
<text class="item-label">人数:</text>
<text class="item-value">已入队 {{team.joined_count}} / 目标 {{team.need_count}}</text>
</view>
<view class="info-item-row">
<text class="item-label">分红:</text>
<text class="item-value jine-green">¥{{teamShareText}}({{team.share_mode == 'fixed' ? '固定' : '平均'}})</text>
</view>
<view class="info-item-row" wx:if="{{team.remark}}">
<text class="item-label">备注:</text>
<text class="item-value">{{team.remark}}</text>
</view>
<view class="team-members" wx:if="{{team.members && team.members.length}}">
<text class="section-subtitle">组队接待者</text>
<block wx:for="{{team.members}}" wx:key="user_uid">
<view class="team-member-row">
<text class="tm-role">{{item.role_text || (item.role == 'leader' ? '队长' : '队友')}}</text>
<text class="tm-name">{{item.nicheng || '打手'}}</text>
<text class="tm-uid">{{item.user_uid}}</text>
<text class="tm-game" wx:if="{{item.game_id}}">游戏ID {{item.game_id}}</text>
<view class="copy-btn" wx:if="{{item.game_id}}" bindtap="fuzhiWenben" data-text="{{item.game_id}}" catchtap="fuzhiWenben">
<text class="copy-text">复制</text>
</view>
</view>
</block>
</view>
<view class="team-actions">
<view class="team-chat-btn" bindtap="goToTeamGroupChat">进入订单群聊</view>
<view class="team-action-btn ghost" wx:if="{{isLeader && team.status == 1 && jibenShuju.zhuangtai == 2}}" bindtap="lockTeam">关停招募</view>
</view>
</block>
</view>
<!-- 商家信息区域 -->
<view
@@ -151,14 +209,28 @@
<!-- 退款信息区域 -->
<view
class="tuikuan-area"
wx:if="{{showRefundInfo && xiangxiShuju.tuikuan_liyou}}"
wx:if="{{showRefundInfo && (xiangxiShuju.tuikuan_liyou || xiangxiShuju.tuikuan_tupian.length)}}"
>
<view class="info-title">退款信息</view>
<view class="info-item-row">
<view class="info-item-row" wx:if="{{xiangxiShuju.tuikuan_liyou}}">
<text class="item-label">退款理由:</text>
<text class="item-value">{{xiangxiShuju.tuikuan_liyou}}</text>
</view>
<view class="info-item-row" wx:if="{{xiangxiShuju.tuikuan_tupian.length}}" style="flex-direction:column;align-items:flex-start;">
<text class="item-label" style="margin-bottom:12rpx;">相关图片:</text>
<view style="display:flex;flex-wrap:wrap;">
<block wx:for="{{xiangxiShuju.tuikuan_tupian}}" wx:key="*this">
<image
src="{{item}}"
mode="aspectFill"
style="width:140rpx;height:140rpx;margin:0 12rpx 12rpx 0;border-radius:8rpx;"
bindtap="previewTuikuanTupian"
data-url="{{item}}"
/>
</block>
</view>
</view>
</view>
<!-- 处罚申请信息区域 -->
@@ -179,7 +251,7 @@
<text class="item-label">申请状态:</text>
<text
class="item-value"
style="color: {{xiangxiShuju.chufa_zhuangtai == 0 ? '#9333ea' : (xiangxiShuju.chufa_zhuangtai == 1 ? '#4CAF50' : '#FF4444')}}"
style="color: {{xiangxiShuju.chufa_zhuangtai == 0 ? '#FF9800' : (xiangxiShuju.chufa_zhuangtai == 1 ? '#4CAF50' : '#FF4444')}}"
>
{{xiangxiShuju.chufa_zhuangtai == 0 ? '审核中' : (xiangxiShuju.chufa_zhuangtai == 1 ? '处罚成功' : '处罚驳回')}}
</text>
@@ -200,7 +272,7 @@
<!-- 接单员交付历史(只读模式) -->
<view class="dashou-jiaofu-area" wx:if="{{!isModifying && xiangxiShuju.dashou_jiaofu.length > 0}}">
<view class="info-title">我的交付</view>
<view class="info-title">{{isLeader ? '订单交付' : '订单交付(只读)'}}</view>
<!-- 接单员交付图片 -->
<view class="dashou-jiaofu-container">
@@ -232,8 +304,8 @@
</view>
</view>
<!-- 🆕 修改模式下的可编辑区域(当 isModifying 为 true 时显示) -->
<view class="dashou-jiaofu-area" wx:if="{{isModifying}}">
<!-- 修改模式(仅队长 canModify) -->
<view class="dashou-jiaofu-area" wx:if="{{isModifying && canModify}}">
<view class="info-title">修改交付</view>
<!-- 图片编辑区域 -->
@@ -299,8 +371,8 @@
</view>
</view>
<!-- 提交订单区域(仅状态=2时显示,保持不变) -->
<view class="tijiao-area" wx:if="{{jibenShuju.zhuangtai == 2}}">
<!-- 提交订单:仅队长(或非组队接单人) -->
<view class="tijiao-area" wx:if="{{jibenShuju.zhuangtai == 2 && canSubmit}}">
<view class="info-title">提交订单</view>
<!-- 选择图片 -->
@@ -367,8 +439,8 @@
<!-- ========== 底部按钮区域 ========== -->
<view class="bottom-btns-container" wx:if="{{!isLoading}}">
<!-- 🆕 修改模式下的按钮(取消/确认) -->
<block wx:if="{{isModifying}}">
<!-- 修改模式按钮(仅队长 canModify) -->
<block wx:if="{{isModifying && canModify}}">
<view class="btn-row">
<view
class="bottom-btn cancel-modify-btn"
@@ -417,8 +489,8 @@
</view>
</view>
<!-- 🆕 新增:修改按钮(仅当状态为4或8时显示) -->
<view class="btn-row" wx:if="{{jibenShuju.zhuangtai == 4 || jibenShuju.zhuangtai == 8}}">
<!-- 修改交付:仅队长 -->
<view class="btn-row" wx:if="{{canModify && (jibenShuju.zhuangtai == 4 || jibenShuju.zhuangtai == 8)}}">
<view
class="bottom-btn modify-btn full-width"
bindtap="enterModify"
@@ -428,15 +500,21 @@
</view>
</view>
<!-- 提交按钮(仅状态=2时显示) -->
<!-- 提交:仅队长 -->
<view
class="bottom-btn submit-btn"
wx:if="{{jibenShuju.zhuangtai == 2}}"
wx:if="{{canSubmit && jibenShuju.zhuangtai == 2}}"
bindtap="tijiaoDingdan"
>
<text class="btn-text">{{isSubmitting ? '提交中...' : '立即提交'}}</text>
<view class="btn-shine"></view>
</view>
<view
class="team-member-tip"
wx:if="{{team && !isLeader && jibenShuju.zhuangtai == 2}}"
>
<text>组队中仅队长可提交交付,请等待队长操作</text>
</view>
</block>
</view>
@@ -472,6 +550,49 @@
</view>
</view>
</view>
<!-- 开启组队弹窗 -->
<view class="modal-container" wx:if="{{showTeamForm}}">
<view class="modal-mask" bindtap="closeTeamForm"></view>
<view class="modal-content team-form-modal">
<view class="modal-header">
<text class="modal-title">开启组队</text>
<view class="modal-close" bindtap="closeTeamForm">×</view>
</view>
<view class="modal-body">
<view class="tf-row">
<text class="tf-label">招募人数(1~3)</text>
<view class="tf-need">
<view class="need-btn {{teamForm.need_count == 1 ? 'on' : ''}}" data-n="1" bindtap="setTeamNeed">1</view>
<view class="need-btn {{teamForm.need_count == 2 ? 'on' : ''}}" data-n="2" bindtap="setTeamNeed">2</view>
<view class="need-btn {{teamForm.need_count == 3 ? 'on' : ''}}" data-n="3" bindtap="setTeamNeed">3</view>
</view>
</view>
<view class="tf-row">
<text class="tf-label">分钱方式</text>
<view class="tf-need">
<view class="need-btn {{teamForm.share_mode == 'fixed' ? 'on' : ''}}" data-m="fixed" bindtap="setTeamMode">固定值</view>
<view class="need-btn {{teamForm.share_mode == 'average' ? 'on' : ''}}" data-m="average" bindtap="setTeamMode">平均值</view>
</view>
</view>
<view class="tf-row" wx:if="{{teamForm.share_mode == 'fixed'}}">
<text class="tf-label">人均金额(元)</text>
<input class="tf-input" type="digit" placeholder="队友每人" value="{{teamForm.per_person_amount}}" bindinput="onTeamPerPerson" />
</view>
<view class="tf-row">
<text class="tf-label">队长游戏ID</text>
<input class="tf-input" placeholder="必填,队内可见" value="{{teamForm.leader_game_id}}" bindinput="onTeamGameId" maxlength="64" />
</view>
<view class="tf-row">
<text class="tf-label">组队备注(可选)</text>
<input class="tf-input" placeholder="大厅可见" value="{{teamForm.remark}}" bindinput="onTeamRemark" maxlength="200" />
</view>
<view class="tf-actions">
<view class="team-open-btn" bindtap="submitOpenTeam">{{teamSubmitting ? '提交中...' : '确认开启'}}</view>
</view>
</view>
</view>
</view>
<global-notification id="global-notification" />
<popup-notice id="popupNotice" />
File diff suppressed because it is too large. Load diff
+181 -28
View File
@@ -2,6 +2,16 @@ const app = getApp();
import { createPage, request } from '../../utils/base-page.js';
import { reconnectForRole } from '../../utils/role-tab-bar.js';
import { getOrderStatusText } from '../../utils/api-helper.js';
import {
attachAutoSettleFields,
buildAutoSettleDisplay,
startAutoSettleTicker,
stopAutoSettleTicker,
} from '../../utils/auto-settle-display.js';
import { sanitizeMyDealStat, DEAL_STAT_HELP } from '../../utils/deal-stat-display.js';
import { consumeBadgeFields } from '../../utils/dashou-badges.js';
import { isFighterOnly, canEnterAcceptOrder, getLoginPageUrl } from '../../utils/fighter-only.js';
import { ensurePhoneAuth } from '../../utils/phone-auth.js';
Page(createPage({
data: {
@@ -9,7 +19,7 @@ Page(createPage({
shangpinleixing: [],
xuanzhongLeixingId: null,
// 订单类型: normal 普通, peihu 陪护
// 订单类型固定普通;旧陪护入口已下线(与组队无关)
orderType: 'normal',
// 搜索关键字
@@ -19,11 +29,12 @@ Page(createPage({
statusList: [
{ name: '全部', key: 'all', zhuangtaiList: [], color: '#333333' },
{ name: '进行中', key: 'jinxingzhong', zhuangtaiList: [2], color: '#2196F3' },
{ name: '退款中', key: 'tuikuanzhong', zhuangtaiList: [4], color: '#9333ea' },
{ 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' }
{ name: '结算中', key: 'jiesuanzhong', zhuangtaiList: [8], color: '#FF5722' },
{ name: '待自动结算', key: 'dai_zidong_jiesuan', zhuangtaiList: [8], color: '#C62828', autoSettlePending: true },
],
currentStatusKey: 'all',
@@ -35,7 +46,8 @@ Page(createPage({
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 }
jiesuanzhong: { list: [], page: 1, hasMore: true, isLoading: false },
dai_zidong_jiesuan: { list: [], page: 1, hasMore: true, isLoading: false },
},
currentList: [],
@@ -45,22 +57,62 @@ Page(createPage({
scrollViewRefreshing: false,
defaultImg: '/images/default-order.png',
ossImageUrl: app.globalData.ossImageUrl || '',
dealStat: null,
dealStatChips: [],
showDealStatHelp: false,
},
onLoad() {
if (isFighterOnly() && !canEnterAcceptOrder()) {
wx.reLaunch({ url: getLoginPageUrl() });
return;
}
wx.setNavigationBarTitle({ title: '我的接单' });
this.loadShangpinLeixing();
this.loadShangpinLeixing().then(() => {
if (this.data.xuanzhongLeixingId) {
return this.loadCurrentStatusOrders(true);
}
}).finally(() => {
this._ordersSessionReady = true;
this._skipShowRefresh = true;
});
this.registerNotificationComponent();
},
onShow() {
async onShow() {
if (isFighterOnly() && !canEnterAcceptOrder()) {
wx.reLaunch({ url: getLoginPageUrl() });
return;
}
const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'dashou' });
if (!phoneOk) return;
this.registerNotificationComponent();
this.loadDealStat();
const app = getApp();
const filterKey = app.globalData._fighterOrdersStatusKey;
if (filterKey) {
app.globalData._fighterOrdersStatusKey = null;
if (this.data.statusList.some((s) => s.key === filterKey)) {
this.setData({ currentStatusKey: filterKey });
}
if (this.data.xuanzhongLeixingId) {
this.loadCurrentStatusOrders(true);
}
}
if (wx.getStorageSync('uid')) {
reconnectForRole('dashou');
if (app.startImWhenReady) app.startImWhenReady();
}
if (this.data.currentStatusKey && this.data.xuanzhongLeixingId) {
this.loadCurrentStatusOrders(true);
if (this._skipShowRefresh) {
this._skipShowRefresh = false;
return;
}
if (this._ordersSessionReady && this.data.xuanzhongLeixingId && !this._silentRefreshRunning) {
this._silentRefreshRunning = true;
this.loadCurrentStatusOrders(true, true).finally(() => {
this._silentRefreshRunning = false;
});
}
},
@@ -102,9 +154,11 @@ Page(createPage({
},
switchOrderType(e) {
// 陪护入口已隐藏;保留方法避免旧事件残留报错
const type = e.currentTarget.dataset.type;
if (type !== 'normal') return;
if (type === this.data.orderType) return;
this.setData({ orderType: type });
this.setData({ orderType: 'normal' });
this.resetCurrentStatusData();
this.loadCurrentStatusOrders(true);
},
@@ -128,6 +182,35 @@ Page(createPage({
this.loadCurrentStatusOrders(true);
},
async loadDealStat() {
try {
const res = await request({
url: '/dingdan/wo-de-deal-stat',
method: 'POST',
data: { role: 'dashou' },
header: { 'content-type': 'application/json' },
});
if (res.data && (res.data.code === 0 || res.data.code === 200)) {
const cleaned = sanitizeMyDealStat(res.data.data, 'dashou');
this.setData({
dealStat: cleaned,
dealStatChips: (cleaned && cleaned.chips) || [],
});
}
} catch (e) {
/* 无样本静默 */
}
},
onDealStatHelpTap() {
wx.showModal({
title: '指标说明',
content: DEAL_STAT_HELP.dashou,
showCancel: false,
confirmText: '知道了',
});
},
switchStatus(e) {
const key = e.currentTarget.dataset.key;
if (key === this.data.currentStatusKey) return;
@@ -141,17 +224,21 @@ Page(createPage({
},
// 加载订单(核心)
async loadCurrentStatusOrders(isRefresh = false) {
async loadCurrentStatusOrders(isRefresh = false, silent = false) {
const key = this.data.currentStatusKey;
const tabData = this.data.dsDingdanShuju[key];
if (tabData.isLoading) return;
if (tabData.isLoading && !silent) return;
if (silent && this._listRequesting) return;
const page = isRefresh ? 1 : tabData.page;
this.setData({
[`dsDingdanShuju.${key}.isLoading`]: true,
isLoading: true,
isLoadingMore: !isRefresh
});
if (!silent) {
this.setData({
[`dsDingdanShuju.${key}.isLoading`]: true,
isLoading: true,
isLoadingMore: !isRefresh,
});
}
this._listRequesting = true;
try {
const apiUrl = this.data.orderType === 'peihu'
@@ -167,6 +254,9 @@ Page(createPage({
leixing_id: this.data.xuanzhongLeixingId,
keyword: this.data.searchKeyword || undefined
};
if (statusItem && statusItem.autoSettlePending) {
params.auto_settle_pending = 1;
}
const res = await request({
url: apiUrl,
@@ -177,11 +267,13 @@ Page(createPage({
const code = res.data.code;
if (code === 200 || code === 0) {
const newList = res.data.data.list || [];
const hasMore = res.data.data.has_more || false;
const payload = res.data.data || {};
consumeBadgeFields(payload);
const newList = payload.list || [];
const hasMore = payload.has_more || false;
// ✅ 图片URL拼接(核心)
const processed = newList.map(item => ({
const processed = newList.map(item => attachAutoSettleFields({
...item,
zhuangtaiZh: getOrderStatusText(item.zhuangtai),
zhuangtaiColor: statusItem.color,
@@ -208,14 +300,20 @@ Page(createPage({
}
} 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();
if (!silent) {
wx.showToast({ title: err.message || '加载失败', icon: 'none' });
}
if (!silent) {
this.setData({
[`dsDingdanShuju.${key}.isLoading`]: false,
isLoading: false,
isLoadingMore: false,
scrollViewRefreshing: false
});
this.refreshCurrentListView();
}
} finally {
this._listRequesting = false;
}
},
@@ -227,6 +325,53 @@ Page(createPage({
hasMore: tabData.hasMore,
isLoading: tabData.isLoading
});
this.syncAutoSettleListTicker();
},
syncAutoSettleListTicker() {
const key = this.data.currentStatusKey;
const has = (this.data.currentList || []).some((it) => it && it.auto_settle_eligible);
if (!has) {
stopAutoSettleTicker(this);
return;
}
startAutoSettleTicker(this, {
onTick: () => {
const k = this.data.currentStatusKey;
const list = this.data.currentList || [];
const now = Date.now();
let changed = false;
const next = list.map((it) => {
if (!it || !it.auto_settle_eligible) return it;
const disp = buildAutoSettleDisplay(it, now);
if (!disp) {
if (it.autoSettleClock || it.autoSettleTip) {
changed = true;
return { ...it, autoSettleTip: '', autoSettleClock: '', autoSettleRemainText: '' };
}
return it;
}
if (it.autoSettleClock !== disp.clockText || it.autoSettleTip !== disp.tip) {
changed = true;
return {
...it,
autoSettleTip: disp.tip,
autoSettleExpireAt: disp.expireAt,
autoSettleRemainText: disp.remainText,
autoSettleClock: disp.clockText,
autoSettleRemainSeconds: disp.remainSeconds,
};
}
return it;
});
if (changed) {
this.setData({
currentList: next,
[`dsDingdanShuju.${k}.list`]: next,
});
}
},
});
},
resetCurrentStatusData() {
@@ -285,5 +430,13 @@ Page(createPage({
}
},
onImageError() {}
onImageError() {},
onHide() {
stopAutoSettleTicker(this);
},
onUnload() {
stopAutoSettleTicker(this);
},
}))
+2 -1
View File
@@ -1,6 +1,7 @@
{
"usingComponents": {
"global-notification": "/components/global-notification/global-notification"
"global-notification": "/components/global-notification/global-notification",
"tab-bar": "/tab-bar/index"
},
"navigationBarTitleText": "我的接单",
"enablePullDownRefresh": false,
+16 -7
View File
@@ -17,19 +17,21 @@
</scroll-view>
</view>
<!-- 筛选行 -->
<!-- 筛选行:组队已独立 Tab;旧「陪护订单」半成品入口已隐藏 -->
<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">
<view class="search-box search-box-full">
<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="deal-stat-bar" wx:if="{{dealStat}}">
<text class="deal-stat-item" wx:if="{{dealStat.deal_rate_text}}">结算率 {{dealStat.deal_rate_text}}</text>
<text class="deal-stat-item" wx:if="{{dealStat.fined_rate_text}}">被罚款率 {{dealStat.fined_rate_text}}</text>
</view>
<!-- 主体:左右布局 -->
<view class="main-container">
<!-- 左侧状态栏(固定) -->
@@ -77,8 +79,15 @@
<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-title-row">
<text class="card-title">{{item.jieshao || '暂无描述'}}</text>
<text class="team-badge" wx:if="{{item.has_team}}">组队</text>
</view>
<view class="card-id" wx:if="{{item.dingdan_id}}">ID: {{item.dingdan_id}}</view>
<view class="card-team-meta" wx:if="{{item.has_team}}">
{{item.team_role_text || '组队'}} · {{item.team_status_text}} · {{item.team_joined}}/{{item.team_need}}人
</view>
<view class="card-auto-settle" wx:if="{{item.autoSettleTip}}">{{item.autoSettleTip}}</view>
</view>
<view class="card-right">
<view class="card-status" style="color: {{item.zhuangtaiColor}};">{{item.zhuangtaiZh}}</view>
+52 -1
View File
@@ -9,6 +9,30 @@ page {
flex-direction: column;
background: #f5f6fa;
}
.deal-stat-bar {
margin: 12rpx 24rpx;
padding: 14rpx 20rpx;
background: #fff;
border-radius: 14rpx;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
flex-shrink: 0;
}
.deal-stat-item {
font-size: 22rpx;
color: #37474f;
background: #eceff1;
padding: 4rpx 14rpx;
border-radius: 8rpx;
}
.card-auto-settle {
margin-top: 8rpx;
font-size: 20rpx;
color: #c62828;
line-height: 1.3;
}
/* 商品类型区 */
.leixing-area {
@@ -92,6 +116,10 @@ page {
padding: 0 20rpx;
height: 68rpx;
}
.search-box-full {
width: 100%;
margin-right: 0;
}
.search-icon {
width: 32rpx;
height: 32rpx;
@@ -224,6 +252,28 @@ page {
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
flex: 1;
min-width: 0;
}
.card-title-row {
display: flex;
align-items: flex-start;
gap: 10rpx;
}
.team-badge {
flex-shrink: 0;
margin-top: 4rpx;
font-size: 20rpx;
color: #1f8a7c;
background: rgba(31, 138, 124, 0.12);
padding: 2rpx 10rpx;
border-radius: 6rpx;
font-weight: 600;
}
.card-team-meta {
font-size: 22rpx;
color: #1f8a7c;
margin-top: 6rpx;
}
.card-id {
font-size: 22rpx;
@@ -346,4 +396,5 @@ page {
color: #999;
}
@import '../../styles/dashou-xym-orders-theme.wxss';
@import '../../styles/dashou-xym-orders-theme.wxss';
@import '../../styles/dashou-xym-orders-page.wxss';
@@ -0,0 +1,16 @@
# 排行榜页 UI 改造备份
备份时间:2026-07-06
## 备份文件
- `fighter-rank.wxml` / `fighter-rank.wxss` / `fighter-rank.js` / `fighter-rank.json`
## 改造说明
- 视觉对齐参考图:黄色顶栏、白底 Tab、领奖台、4 名以后列表
- **接口与 JS 业务逻辑不变**(仅 DATE_LIST 展示顺序、Tab 文案微调)
## 回滚
将本目录内四个文件覆盖回 `pages/fighter-rank/` 即可。
@@ -0,0 +1,477 @@
/**
* 排行榜
* 默认 POST /yonghu/phbhqsj
* 星之界(xzj) POST /yonghu/xzjphbhqsj(邀请人数排序等专用规则)
*/
import request from '../../utils/request.js'
import { resolveAvatarUrl, getDefaultAvatarUrl } from '../../utils/avatar.js'
import { CLUB_ID } from '../../config/club-config.js'
const IS_XZJ = CLUB_ID === 'xzj'
const RANK_API = IS_XZJ ? '/yonghu/xzjphbhqsj' : '/yonghu/phbhqsj'
const CLOSED_DATES = ['昨日', '上周', '上月']
const ROLE_LIST = [
{ key: 'dashou', label: '接单员' },
{ key: 'guanshi', label: '管事' },
{ key: 'zuzhang', label: '组长' },
{ key: 'shangjia', label: '商家' },
]
const DATE_LIST = [
{ key: '今日', label: '今日' },
{ key: '本周', label: '本周' },
{ key: '本月', label: '本月' },
{ key: '总榜', label: '总榜' },
{ key: '昨日', label: '昨日' },
{ key: '上周', label: '上周' },
{ key: '上月', label: '上月' },
]
const ROLE_META = {
dashou: {
title: '接单员排行榜',
sortField: 'chengjiao_zonge',
sortLabel: '分红总额',
mainType: 'money',
metrics: [
{ field: 'jiedan_zongliang', label: '接单量', type: 'int' },
{ field: 'jiedan_zonge', label: '接单额', type: 'money' },
{ field: 'chengjiao_zongliang', label: '成交量', type: 'int' },
],
},
guanshi: {
title: '管事排行榜',
sortField: 'shouru_zonge',
sortLabel: '收入总额',
mainType: 'money',
metrics: [
{ field: 'yaoqing_dashou_shu', label: '邀请', type: 'int' },
{ field: 'chongzhi_dashou_shu', label: '充值', type: 'int' },
],
},
zuzhang: {
title: '组长排行榜',
sortField: 'shouru_zonge',
sortLabel: '收入总额',
mainType: 'money',
metrics: [
{ field: 'yaoqing_guanshi_shu', label: '邀请', type: 'int' },
{ field: 'fenyong_jine', label: '分佣', type: 'money' },
],
},
shangjia: {
title: '商家排行榜',
sortField: 'jiesuan_jine',
sortLabel: '成交总额',
mainType: 'money',
metrics: [
{ field: 'jiesuan_dingdan_shu', label: '成交量', type: 'int' },
{ field: 'paifa_dingdan_shu', label: '派单量', type: 'int' },
{ field: 'paifa_jine', label: '派单额', type: 'money' },
],
},
}
/** 星之界专用展示与排序字段 */
const XZJ_ROLE_META = {
dashou: {
title: '接单员排行榜',
sortField: 'chengjiao_zonge',
sortLabel: '成交金额',
mainType: 'money',
metrics: [
{ field: 'jiedan_zongliang', label: '接单量', type: 'int' },
{ field: 'jiedan_zonge', label: '接单额', type: 'money' },
{ field: 'chengjiao_zongliang', label: '成交量', type: 'int' },
],
},
guanshi: {
title: '管事排行榜',
sortField: 'yaoqing_dashou_shu',
sortLabel: '邀请人数',
mainType: 'int',
metrics: [
{ field: 'wuxiao_yaoqing_dashou_shu', label: '无效邀请', type: 'int' },
{ field: 'chongzhi_dashou_shu', label: '有效人数', type: 'int' },
{ field: 'shouru_zonge', label: '收入金额', type: 'money' },
],
},
zuzhang: {
title: '组长排行榜',
sortField: 'yaoqing_guanshi_shu',
sortLabel: '邀请人数',
mainType: 'int',
metrics: [
{ field: 'wuxiao_yaoqing_guanshi_shu', label: '无效邀请', type: 'int' },
{ field: 'youxiao_guanshi_shu', label: '有效人数', type: 'int' },
{ field: 'shouru_zonge', label: '收入金额', type: 'money' },
],
},
shangjia: {
title: '商家排行榜',
sortField: 'paifa_jine',
sortLabel: '派单流水',
mainType: 'money',
metrics: [
{ field: 'paifa_dingdan_shu', label: '派单量', type: 'int' },
{ field: 'paifa_jine', label: '派单流水', type: 'money' },
],
},
}
const ACTIVE_ROLE_META = IS_XZJ ? XZJ_ROLE_META : ROLE_META
function isInvalidAvatarPath(path) {
if (path === null || path === undefined) return true
if (typeof path !== 'string') return true
const s = path.trim()
return !s || s === 'null' || s === 'undefined' || s === 'none'
}
function getDefaultAvatar(app) {
return getDefaultAvatarUrl(app)
}
function buildAvatar(path, app) {
app = app || getApp()
if (isInvalidAvatarPath(path)) return getDefaultAvatar(app)
return resolveAvatarUrl(path, app) || getDefaultAvatar(app)
}
Page({
data: {
imgUrls: {},
roleList: ROLE_LIST,
dateList: DATE_LIST,
currentRole: 'dashou',
currentDate: '今日',
fanwei: 'jituan',
myClubId: CLUB_ID || 'xq',
roleMeta: ROLE_META.dashou,
sortLabel: '分红总额',
isLoading: true,
rankList: [],
topThree: [],
restList: [],
isEmpty: false,
defaultAvatar: '',
rewardInfo: null,
showClaimBar: false,
claimLoading: false,
},
onLoad(options) {
const type = options.type || options.rankType || 'dashou'
const validRole = ACTIVE_ROLE_META[type] ? type : 'dashou'
this.initPage()
this.applyRole(validRole, false)
this.fetchRankList()
},
onPullDownRefresh() {
this.fetchRankList().finally(() => wx.stopPullDownRefresh())
},
initPage() {
this.syncDefaultAvatar()
const g = getApp().globalData
const base = (g.ossImageUrl || '') + 'beijing/paihangbang/'
this.setData({
imgUrls: {
pageBg: base + 'page-bg.jpg',
cardBg: base + 'list-card-bg.png',
emptyIcon: base + 'empty.png',
refreshIcon: base + 'icon-refresh.png',
},
})
},
/** 与 app.js 一致:ossImageUrl + morentouxiang,配置异步加载后 onShow 再同步 */
syncDefaultAvatar() {
const def = getDefaultAvatar()
if (def && def !== this.data.defaultAvatar) {
this.setData({ defaultAvatar: def })
} else if (!this.data.defaultAvatar && def) {
this.setData({ defaultAvatar: def })
}
},
/** 配置晚于页面加载时,补全空头像 */
ensureAvatars() {
const def = getDefaultAvatar()
if (!def) return
const g = getApp().globalData
const oss = g.ossImageUrl || ''
const { rankList, topThree, restList } = this.data
if (!rankList.length) {
if (def !== this.data.defaultAvatar) this.setData({ defaultAvatar: def })
return
}
const isValidUrl = (url) => {
if (!url || typeof url !== 'string') return false
if (url.startsWith('http://') || url.startsWith('https://')) return true
return !!(oss && url.indexOf(oss) === 0)
}
const fix = (item) => ({
...item,
avatar: isValidUrl(item.avatar) ? item.avatar : def,
})
this.setData({
defaultAvatar: def,
rankList: rankList.map(fix),
topThree: topThree.map(fix),
restList: restList.map(fix),
})
},
applyRole(role, reload = true) {
const meta = ACTIVE_ROLE_META[role]
wx.setNavigationBarTitle({ title: meta.title })
this.setData({ currentRole: role, roleMeta: meta, sortLabel: meta.sortLabel })
if (reload) this.fetchRankList()
},
onRoleTap(e) {
const role = e.currentTarget.dataset.role
if (!role || role === this.data.currentRole) return
this.applyRole(role)
},
onDateTap(e) {
const date = e.currentTarget.dataset.date
if (!date || date === this.data.currentDate) return
this.setData({ currentDate: date })
this.fetchRankList()
},
onScopeTap(e) {
const fanwei = e.currentTarget.dataset.fanwei
if (!fanwei || fanwei === this.data.fanwei) return
this.setData({ fanwei })
this.fetchRankList()
},
onShowRules() {
const r = this.data.rewardInfo
if (!r || !r.tiers || !r.tiers.length) {
wx.showToast({ title: '暂无奖励规则', icon: 'none' })
return
}
const lines = r.tiers.map((t) => {
const label = t.label || `第${t.rank_from}${t.rank_to > t.rank_from ? '-' + t.rank_to : ''}名`
return `${label}:¥${t.amount}`
})
const tip = r.period_status === 'open' ? '(本期进行中,结束后可领取)' : ''
wx.showModal({
title: `${r.title || '排行榜奖励'}${tip}`,
content: [
`俱乐部:${r.club_id || this.data.myClubId}`,
`排序:${r.sort_label || ''}`,
...lines,
r.description || '',
].filter(Boolean).join('\n'),
showCancel: false,
})
},
async onClaimTap() {
const claim = this.data.rewardInfo && this.data.rewardInfo.my_claim
if (!claim || !claim.claim_id || this.data.claimLoading) return
this.setData({ claimLoading: true })
try {
const res = await request({
url: '/yonghu/phbjllq',
method: 'POST',
data: { claim_id: claim.claim_id },
})
const body = res?.data || {}
if (body.code === 200) {
wx.showToast({ title: `已领取 ¥${claim.amount}`, icon: 'success' })
await this.fetchRewardInfo()
} else {
wx.showToast({ title: body.msg || '领取失败', icon: 'none' })
}
} catch (e) {
wx.showToast({ title: '领取失败', icon: 'none' })
} finally {
this.setData({ claimLoading: false })
}
},
onRefreshTap() {
this.fetchRankList()
},
formatMoney(val) {
const n = parseFloat(val)
return Number.isNaN(n) ? '0.00' : n.toFixed(2)
},
formatInt(val) {
const n = parseInt(val, 10)
return Number.isNaN(n) ? '0' : String(n)
},
normalizeItem(raw, index, role) {
const meta = ACTIVE_ROLE_META[role]
const app = getApp()
const uid = raw.yonghuid || raw.uid || ''
const nicheng = raw.nicheng || raw.nick || '用户'
const def = getDefaultAvatar(app)
const avatar = buildAvatar(raw.touxiang || raw.avatar, app) || def
const metrics = meta.metrics.map((m) => {
const v = raw[m.field]
return {
label: m.label,
value: m.type === 'money' ? this.formatMoney(v) : this.formatInt(v),
}
})
const sortRaw = raw[meta.sortField] ?? raw.metric_value
const mainType = raw.metric_label ? (Number.isInteger(Number(sortRaw)) && !String(sortRaw).includes('.') ? 'int' : 'money') : (meta.mainType || 'money')
const mainValue = mainType === 'money'
? this.formatMoney(sortRaw)
: this.formatInt(sortRaw)
return {
mingci: raw.mingci || index + 1,
uid,
nicheng,
avatar,
clubName: raw.club_name || raw.club_id || '',
rewardAmount: raw.reward_amount || 0,
mainType,
mainPrefix: mainType === 'money' ? '¥' : '',
mainValue,
mainLabel: raw.metric_label || meta.sortLabel,
metrics,
}
},
applyRewardToList(list, rewardInfo, fanwei) {
if (fanwei !== 'club') return list
if (!rewardInfo || !rewardInfo.rank_rewards) return list
const map = rewardInfo.rank_rewards
return list.map((item) => ({
...item,
rewardAmount: map[item.mingci] || item.rewardAmount || 0,
}))
},
async fetchRewardInfo() {
try {
const res = await request({
url: '/yonghu/phbjlxx',
method: 'POST',
data: {
shenfen: this.data.currentRole,
riqi: this.data.currentDate,
club_id: this.data.myClubId,
},
})
const body = res?.data || {}
if (body.code !== 200 || !body.data) return
const rewardInfo = body.data
const showClaimBar = !!(rewardInfo.my_claim && rewardInfo.my_claim.status === 'pending')
const { rankList, fanwei } = this.data
const merged = this.applyRewardToList(rankList, rewardInfo, fanwei)
this.setData({
rewardInfo,
showClaimBar,
rankList: merged,
topThree: merged.slice(0, 3),
restList: merged.slice(3),
})
} catch (e) {
console.warn('奖励信息加载失败', e)
}
},
async fetchRankList() {
this.setData({ isLoading: true, isEmpty: false, showClaimBar: false })
try {
const { currentRole, currentDate, fanwei, myClubId } = this.data
let list = []
let rewardFromRank = null
if (fanwei === 'club') {
const res = await request({
url: '/yonghu/phbjlphb',
method: 'POST',
data: {
shenfen: currentRole,
riqi: currentDate,
fanwei: 'club',
club_id: myClubId,
},
})
const body = res?.data || {}
if (body.code === 200 && body.data) {
list = body.data.list || []
rewardFromRank = body.data.reward || null
if (body.data.sort_label) {
this.setData({ sortLabel: body.data.sort_label })
}
}
} else {
const res = await request({
url: RANK_API,
method: 'POST',
data: { shenfen: currentRole, riqi: currentDate },
})
const body = res?.data || {}
const ok = body.code === 200 || body.code === 0
list = ok && body.data
? (body.data.list || body.data.paihang_list || body.data.rank_list || [])
: []
}
if (!Array.isArray(list)) list = []
const role = currentRole
let normalized = list.slice(0, 50).map((item, i) => this.normalizeItem(item, i, role))
if (rewardFromRank) {
normalized = this.applyRewardToList(normalized, rewardFromRank, 'club')
this.setData({ rewardInfo: rewardFromRank, showClaimBar: !!(rewardFromRank.my_claim && rewardFromRank.my_claim.status === 'pending') })
}
this.setData({
rankList: normalized,
topThree: normalized.slice(0, 3),
restList: normalized.slice(3),
isEmpty: normalized.length === 0,
isLoading: false,
})
this.ensureAvatars()
if (!rewardFromRank) {
await this.fetchRewardInfo()
}
} catch (e) {
console.error('排行榜加载失败', e)
wx.showToast({ title: '加载失败', icon: 'none' })
this.setData({ rankList: [], topThree: [], restList: [], isEmpty: true, isLoading: false })
}
},
onAvatarError(e) {
const { zone, index } = e.currentTarget.dataset
const fallback = getDefaultAvatar()
if (!fallback) return
const key = zone === 'top' ? `topThree[${index}].avatar` : `restList[${index}].avatar`
this.setData({ [key]: fallback })
},
registerNotificationComponent() {
const app = getApp()
const comp = this.selectComponent('#global-notification')
if (comp?.showNotification) {
app.globalData.globalNotification = {
show: (d) => comp.showNotification(d),
hide: () => comp.hideNotification(),
}
}
},
onShow() {
this.syncDefaultAvatar()
this.ensureAvatars()
this.registerNotificationComponent()
},
})
@@ -0,0 +1,11 @@
{
"usingComponents": {
"global-notification": "/components/global-notification/global-notification"
},
"navigationBarTitleText": "排行榜",
"navigationBarBackgroundColor": "#0d1117",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true,
"backgroundColor": "#0d1117",
"backgroundTextStyle": "light"
}
@@ -0,0 +1,162 @@
<view class="rank-page">
<image class="page-bg" src="{{imgUrls.pageBg}}" mode="aspectFill" />
<view class="top-bar">
<view class="top-row">
<text class="page-title">{{roleMeta.title}}</text>
<view class="refresh-wrap" bindtap="onRefreshTap">
<image class="refresh-ico" src="{{imgUrls.refreshIcon}}" mode="aspectFit" />
</view>
</view>
<scroll-view scroll-x class="role-scroll" enhanced show-scrollbar="{{false}}">
<view class="role-line">
<view
wx:for="{{roleList}}" wx:key="key"
class="role-chip {{currentRole === item.key ? 'on' : ''}}"
data-role="{{item.key}}" bindtap="onRoleTap"
>{{item.label}}</view>
</view>
</scroll-view>
<view class="date-grid">
<view
wx:for="{{dateList}}" wx:key="key"
class="date-chip {{currentDate === item.key ? 'on' : ''}}"
data-date="{{item.key}}" bindtap="onDateTap"
>{{item.label}}</view>
</view>
<text class="sort-tip">{{currentDate}} · 按{{sortLabel}} · TOP50</text>
<view class="scope-row">
<view class="scope-chip {{fanwei === 'jituan' ? 'on' : ''}}" data-fanwei="jituan" bindtap="onScopeTap">集团总榜</view>
<view class="scope-chip {{fanwei === 'club' ? 'on' : ''}}" data-fanwei="club" bindtap="onScopeTap">本俱乐部榜</view>
</view>
<view class="reward-banner" wx:if="{{rewardInfo && rewardInfo.scheme_enabled}}" bindtap="onShowRules">
<view class="reward-banner-main">
<text class="reward-tag">🏆 {{rewardInfo.title || '排行榜奖励'}}</text>
<text class="reward-club">俱乐部 {{rewardInfo.club_id || myClubId}}</text>
<text class="reward-status" wx:if="{{rewardInfo.period_status === 'open'}}">进行中 · 结束后可领</text>
<text class="reward-status pending" wx:elif="{{rewardInfo.my_claim}}">待领取 ¥{{rewardInfo.my_claim.amount}}</text>
<text class="reward-status claimed" wx:elif="{{rewardInfo.my_claimed}}">已领取 ¥{{rewardInfo.my_claimed.amount}}</text>
</view>
<text class="reward-link">规则 ›</text>
</view>
</view>
<view class="state-wrap" wx:if="{{isLoading}}">
<view class="loader"></view>
<text>加载中</text>
</view>
<view class="state-wrap" wx:elif="{{isEmpty}}">
<image class="state-img" src="{{imgUrls.emptyIcon}}" mode="aspectFit" />
<text>暂无排行数据</text>
</view>
<scroll-view wx:else scroll-y class="body-scroll" enhanced show-scrollbar="{{false}}">
<!-- 前三名 · 纯 CSS 领奖台 -->
<view class="podium-panel" wx:if="{{topThree.length > 0}}">
<view class="podium-cols">
<!-- 榜二 -->
<view class="p-col p-second" wx:if="{{topThree[1]}}">
<view class="p-badge badge-silver"><text>2</text></view>
<view class="p-avatar-wrap wrap-silver">
<image class="p-avatar" src="{{topThree[1].avatar || defaultAvatar}}" mode="aspectFill" data-zone="top" data-index="1" binderror="onAvatarError" />
</view>
<text class="p-name">{{topThree[1].nicheng}}</text>
<text class="p-uid">ID {{topThree[1].uid}}</text>
<view class="p-amount">
<text class="p-money">{{topThree[1].mainPrefix}}{{topThree[1].mainValue}}</text>
<text class="p-label">{{topThree[1].mainLabel}}</text>
<text class="p-reward" wx:if="{{topThree[1].rewardAmount > 0}}">奖 ¥{{topThree[1].rewardAmount}}</text>
</view>
<view class="p-metrics">
<text class="p-metric" wx:for="{{topThree[1].metrics}}" wx:key="label" wx:for-item="m">{{m.label}} {{m.value}}</text>
</view>
<view class="p-pedestal pedestal-silver"></view>
</view>
<!-- 榜一 -->
<view class="p-col p-first" wx:if="{{topThree[0]}}">
<view class="p-badge badge-first"><text>1</text></view>
<view class="p-avatar-wrap wrap-first">
<image class="p-avatar p-avatar-lg" src="{{topThree[0].avatar || defaultAvatar}}" mode="aspectFill" data-zone="top" data-index="0" binderror="onAvatarError" />
</view>
<text class="p-name p-name-first">{{topThree[0].nicheng}}</text>
<text class="p-uid">ID {{topThree[0].uid}}</text>
<view class="p-amount">
<text class="p-money p-money-first">{{topThree[0].mainPrefix}}{{topThree[0].mainValue}}</text>
<text class="p-label">{{topThree[0].mainLabel}}</text>
<text class="p-reward" wx:if="{{topThree[0].rewardAmount > 0}}">奖 ¥{{topThree[0].rewardAmount}}</text>
</view>
<view class="p-metrics">
<text class="p-metric" wx:for="{{topThree[0].metrics}}" wx:key="label" wx:for-item="m">{{m.label}} {{m.value}}</text>
</view>
<view class="p-pedestal pedestal-first"></view>
</view>
<!-- 榜三 -->
<view class="p-col p-third" wx:if="{{topThree[2]}}">
<view class="p-badge badge-bronze"><text>3</text></view>
<view class="p-avatar-wrap wrap-bronze">
<image class="p-avatar" src="{{topThree[2].avatar || defaultAvatar}}" mode="aspectFill" data-zone="top" data-index="2" binderror="onAvatarError" />
</view>
<text class="p-name">{{topThree[2].nicheng}}</text>
<text class="p-uid">ID {{topThree[2].uid}}</text>
<view class="p-amount">
<text class="p-money">{{topThree[2].mainPrefix}}{{topThree[2].mainValue}}</text>
<text class="p-label">{{topThree[2].mainLabel}}</text>
<text class="p-reward" wx:if="{{topThree[2].rewardAmount > 0}}">奖 ¥{{topThree[2].rewardAmount}}</text>
</view>
<view class="p-metrics">
<text class="p-metric" wx:for="{{topThree[2].metrics}}" wx:key="label" wx:for-item="m">{{m.label}} {{m.value}}</text>
</view>
<view class="p-pedestal pedestal-bronze"></view>
</view>
</view>
</view>
<!-- 4-50 名 -->
<view class="list-wrap" wx:if="{{restList.length > 0}}">
<view class="list-title-row">
<text class="list-title">完整榜单</text>
<text class="list-range">第 4 - {{rankList.length}} 名</text>
</view>
<view class="list-card" wx:for="{{restList}}" wx:key="mingci">
<image class="list-card-bg" src="{{imgUrls.cardBg}}" mode="aspectFill" />
<view class="list-card-body">
<view class="lc-rank"><text>{{item.mingci}}</text></view>
<image class="lc-avatar" src="{{item.avatar || defaultAvatar}}" mode="aspectFill" data-zone="rest" data-index="{{index}}" binderror="onAvatarError" />
<view class="lc-info">
<text class="lc-name">{{item.nicheng}}</text>
<text class="lc-uid">ID {{item.uid}}<text wx:if="{{item.clubName && fanwei === 'jituan'}}"> · {{item.clubName}}</text></text>
<view class="lc-tags">
<text class="lc-tag" wx:for="{{item.metrics}}" wx:key="label" wx:for-item="m">{{m.label}} {{m.value}}</text>
</view>
</view>
<view class="lc-score">
<text class="lc-money">{{item.mainPrefix}}{{item.mainValue}}</text>
<text class="lc-label">{{item.mainLabel}}</text>
<text class="lc-reward" wx:if="{{item.rewardAmount > 0}}">奖¥{{item.rewardAmount}}</text>
</view>
</view>
</view>
</view>
<view class="bottom-gap"></view>
</scroll-view>
<view class="claim-bar" wx:if="{{showClaimBar && rewardInfo.my_claim}}">
<view class="claim-text">
<text class="claim-title">🎉 第{{rewardInfo.my_claim.mingci}}名 · 待领取</text>
<text class="claim-amount">¥{{rewardInfo.my_claim.amount}}</text>
</view>
<button class="claim-btn" loading="{{claimLoading}}" bindtap="onClaimTap">立即领取</button>
</view>
</view>
<global-notification id="global-notification" />
@@ -0,0 +1,559 @@
page {
background: #0d1117;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
}
/* 主题色 */
/* 选中/金额: #6BA3F7 浅蓝: #8EB8FF 银: #A8B4C4 铜: #C4A882 */
.rank-page {
min-height: 100vh;
position: relative;
}
.page-bg {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
}
/* ========== 顶栏 ========== */
.top-bar {
position: relative;
z-index: 10;
padding: 16rpx 28rpx 20rpx;
}
.top-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.page-title {
font-size: 38rpx;
font-weight: 700;
color: #fff;
}
.refresh-wrap {
width: 60rpx; height: 60rpx;
border-radius: 50%;
background: rgba(255,255,255,0.14);
display: flex;
align-items: center;
justify-content: center;
}
.refresh-ico {
width: 32rpx; height: 32rpx;
opacity: 0.8;
}
.role-scroll {
width: 100%;
white-space: nowrap;
margin-bottom: 16rpx;
}
.role-line {
display: inline-flex;
gap: 12rpx;
}
.role-chip {
display: inline-block;
padding: 12rpx 28rpx;
border-radius: 999rpx;
font-size: 26rpx;
color: rgba(255,255,255,0.65);
background: rgba(20, 26, 40, 0.75);
border: 1rpx solid rgba(255,255,255,0.12);
}
.role-chip.on {
color: #fff;
font-weight: 600;
background: rgba(107, 163, 247, 0.45);
border-color: rgba(107, 163, 247, 0.65);
box-shadow: 0 0 20rpx rgba(107, 163, 247, 0.2);
}
.date-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10rpx;
margin-bottom: 12rpx;
}
.date-chip {
text-align: center;
padding: 14rpx 0;
border-radius: 14rpx;
font-size: 24rpx;
color: rgba(255,255,255,0.65);
background: rgba(20, 26, 40, 0.72);
border: 1rpx solid rgba(255,255,255,0.08);
}
.date-chip.on {
color: #A8CCFF;
font-weight: 600;
background: rgba(107, 163, 247, 0.35);
border-color: rgba(107, 163, 247, 0.55);
}
.sort-tip {
display: block;
font-size: 22rpx;
color: rgba(255,255,255,0.45);
margin-top: 8rpx;
}
.scope-row {
display: flex;
gap: 12rpx;
margin-top: 16rpx;
}
.scope-chip {
padding: 10rpx 24rpx;
border-radius: 999rpx;
font-size: 24rpx;
color: rgba(255,255,255,0.65);
background: rgba(20, 26, 40, 0.75);
border: 1rpx solid rgba(255,255,255,0.12);
}
.scope-chip.on {
color: #fff;
background: rgba(255, 193, 7, 0.35);
border-color: rgba(255, 193, 7, 0.5);
}
.reward-banner {
margin-top: 16rpx;
padding: 20rpx 24rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, rgba(255,193,7,0.2), rgba(107,163,247,0.15));
border: 1rpx solid rgba(255,193,7,0.35);
display: flex;
align-items: center;
justify-content: space-between;
}
.reward-banner-main {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.reward-tag { font-size: 28rpx; font-weight: 600; color: #ffd666; }
.reward-club { font-size: 22rpx; color: rgba(255,255,255,0.7); }
.reward-status { font-size: 22rpx; color: rgba(255,255,255,0.55); }
.reward-status.pending { color: #52c41a; font-weight: 600; }
.reward-status.claimed { color: #8c8c8c; }
.reward-link { font-size: 24rpx; color: #8EB8FF; }
.p-reward {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: #ffd666;
font-weight: 600;
}
.lc-reward {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: #ffd666;
}
.claim-bar {
position: fixed;
left: 0; right: 0; bottom: 0;
z-index: 100;
padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(13,17,23,0.2), rgba(13,17,23,0.95));
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1rpx solid rgba(255,193,7,0.3);
}
.claim-text { display: flex; flex-direction: column; }
.claim-title { font-size: 26rpx; color: #fff; }
.claim-amount { font-size: 36rpx; font-weight: 700; color: #ffd666; }
.claim-btn {
margin: 0;
padding: 0 40rpx;
height: 72rpx;
line-height: 72rpx;
font-size: 28rpx;
background: linear-gradient(135deg, #ffc107, #ff9800);
color: #1a1a1a;
border-radius: 999rpx;
border: none;
}
.bottom-gap {
height: 160rpx;
}
/* ========== 状态 ========== */
.state-wrap {
position: relative;
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
padding: 140rpx 0;
color: rgba(255,255,255,0.45);
font-size: 28rpx;
}
.loader {
width: 48rpx; height: 48rpx;
border: 3rpx solid rgba(255,255,255,0.12);
border-top-color: #6BA3F7;
border-radius: 50%;
animation: spin 0.7s linear infinite;
margin-bottom: 20rpx;
}
.state-img {
width: 160rpx; height: 160rpx;
margin-bottom: 20rpx;
opacity: 0.65;
}
/* ========== 滚动 ========== */
.body-scroll {
position: relative;
z-index: 5;
height: calc(100vh - 300rpx);
}
/* ========== 领奖台(纯 CSS) ========== */
.podium-panel {
margin: 8rpx 24rpx 24rpx;
padding: 28rpx 12rpx 0;
border-radius: 28rpx;
background: rgba(14, 18, 30, 0.88);
border: 1rpx solid rgba(255, 255, 255, 0.1);
}
.podium-cols {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 10rpx;
}
.p-col {
flex: 1;
max-width: 220rpx;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.p-first {
max-width: 250rpx;
margin-bottom: 16rpx;
}
.p-badge {
width: 44rpx; height: 44rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14rpx;
font-size: 24rpx;
font-weight: 800;
}
.p-first .p-badge {
width: 52rpx; height: 52rpx;
font-size: 28rpx;
}
.badge-first {
color: #fff;
background: linear-gradient(145deg, #6BA3F7, #4A7FD4);
box-shadow: 0 4rpx 16rpx rgba(107, 163, 247, 0.4);
}
.badge-silver {
color: #fff;
background: linear-gradient(145deg, #A8B4C4, #7A8899);
box-shadow: 0 4rpx 12rpx rgba(168, 180, 196, 0.25);
}
.badge-bronze {
color: #fff;
background: linear-gradient(145deg, #C4A882, #9A7858);
box-shadow: 0 4rpx 12rpx rgba(196, 168, 130, 0.25);
}
.p-avatar-wrap {
border-radius: 50%;
padding: 4rpx;
margin-bottom: 12rpx;
}
.wrap-first {
padding: 5rpx;
background: linear-gradient(145deg, #8EB8FF, #5B8FD8);
box-shadow: 0 6rpx 24rpx rgba(107, 163, 247, 0.3);
}
.wrap-silver {
background: linear-gradient(145deg, #C8D0DA, #909AA8);
}
.wrap-bronze {
background: linear-gradient(145deg, #D4B896, #A88860);
}
.p-avatar {
width: 100rpx; height: 100rpx;
border-radius: 50%;
display: block;
background: #1a2030;
}
.p-avatar-lg {
width: 120rpx; height: 120rpx;
}
.p-name {
font-size: 26rpx;
font-weight: 600;
color: rgba(255,255,255,0.92);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.p-name-first {
font-size: 28rpx;
color: #fff;
}
.p-uid {
margin-top: 4rpx;
font-size: 20rpx;
color: rgba(255,255,255,0.35);
}
.p-amount {
margin: 12rpx 0 10rpx;
text-align: center;
}
.p-money {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #8EB8FF;
letter-spacing: -0.5rpx;
}
.p-money-first {
font-size: 34rpx;
color: #A8CCFF;
}
.p-label {
display: block;
margin-top: 2rpx;
font-size: 20rpx;
color: rgba(255,255,255,0.35);
}
.p-metrics {
display: flex;
flex-wrap: wrap;
gap: 6rpx;
justify-content: center;
margin-bottom: 16rpx;
min-height: 36rpx;
}
.p-metric {
font-size: 18rpx;
color: rgba(255,255,255,0.55);
background: rgba(255,255,255,0.1);
padding: 4rpx 10rpx;
border-radius: 8rpx;
}
/* 底座 · 圆角柱体,非方块 */
.p-pedestal {
width: 88%;
border-radius: 20rpx 20rpx 0 0;
position: relative;
}
.p-pedestal::before {
content: '';
position: absolute;
top: 0; left: 10%; right: 10%;
height: 2rpx;
background: rgba(255,255,255,0.15);
border-radius: 2rpx;
}
.pedestal-first {
height: 100rpx;
background: linear-gradient(180deg, rgba(107,163,247,0.55) 0%, rgba(107,163,247,0.22) 100%);
border: 1rpx solid rgba(107,163,247,0.35);
border-bottom: none;
}
.pedestal-silver {
height: 72rpx;
background: linear-gradient(180deg, rgba(168,180,196,0.45) 0%, rgba(168,180,196,0.18) 100%);
border: 1rpx solid rgba(168,180,196,0.28);
border-bottom: none;
}
.pedestal-bronze {
height: 56rpx;
background: linear-gradient(180deg, rgba(196,168,130,0.45) 0%, rgba(196,168,130,0.18) 100%);
border: 1rpx solid rgba(196,168,130,0.28);
border-bottom: none;
}
/* ========== 列表 ========== */
.list-wrap {
padding: 0 24rpx;
}
.list-title-row {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 8rpx 8rpx 20rpx;
}
.list-title {
font-size: 30rpx;
font-weight: 700;
color: rgba(255,255,255,0.85);
}
.list-range {
font-size: 22rpx;
color: rgba(255,255,255,0.3);
}
.list-card {
position: relative;
margin-bottom: 14rpx;
border-radius: 18rpx;
overflow: hidden;
border: 1rpx solid rgba(255,255,255,0.05);
}
.list-card-bg {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
}
.list-card-body {
position: relative;
z-index: 1;
display: flex;
align-items: center;
padding: 20rpx 22rpx;
background: rgba(12, 16, 28, 0.92);
backdrop-filter: blur(8px);
}
.lc-rank {
width: 48rpx;
flex-shrink: 0;
text-align: center;
}
.lc-rank text {
font-size: 32rpx;
font-weight: 700;
color: rgba(255,255,255,0.18);
}
.lc-avatar {
width: 80rpx; height: 80rpx;
border-radius: 50%;
flex-shrink: 0;
margin: 0 16rpx 0 6rpx;
border: 2rpx solid rgba(107, 163, 247, 0.2);
background: #1a2030;
}
.lc-info { flex: 1; min-width: 0; }
.lc-name {
display: block;
font-size: 28rpx;
font-weight: 600;
color: rgba(255,255,255,0.92);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.lc-uid {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: rgba(255,255,255,0.32);
}
.lc-tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-top: 8rpx;
}
.lc-tag {
font-size: 20rpx;
color: rgba(255,255,255,0.5);
background: rgba(255,255,255,0.1);
padding: 2rpx 10rpx;
border-radius: 6rpx;
}
.lc-score {
flex-shrink: 0;
text-align: right;
margin-left: 10rpx;
}
.lc-money {
display: block;
font-size: 28rpx;
font-weight: 700;
color: #8EB8FF;
}
.lc-label {
display: block;
margin-top: 2rpx;
font-size: 20rpx;
color: rgba(255,255,255,0.32);
}
+78 -75
View File
@@ -1,119 +1,121 @@
/**
* 鎺掕姒? * 榛樿 POST /yonghu/phbhqsj
* 鏄熶箣鐣?xzj) POST /yonghu/xzjphbhqsj锛堥個璇蜂汉鏁版帓搴忕瓑涓撶敤瑙勫垯锛? */
/**
* 排行榜
* 默认 POST /yonghu/phbhqsj
* 星之界(xzj) POST /yonghu/xzjphbhqsj(邀请人数排序等专用规则)
*/
import request from '../../utils/request.js'
import { resolveAvatarUrl, getDefaultAvatarUrl } from '../../utils/avatar.js'
import { CLUB_ID } from '../../config/club-config.js'
const IS_XZJ = CLUB_ID === 'xzj'
const RANK_API = IS_XZJ ? '/yonghu/xzjphbhqsj' : '/yonghu/phbhqsj'
const CLOSED_DATES = ['鏄ㄦ棩', '涓婂懆', '涓婃湀']
const CLOSED_DATES = ['昨日', '上周', '上月']
const ROLE_LIST = [
{ key: 'dashou', label: '鎺ュ崟鍛? },
{ key: 'guanshi', label: '绠′簨' },
{ key: 'zuzhang', label: '缁勯暱' },
{ key: 'shangjia', label: '鍟嗗' },
{ key: 'dashou', label: '接单员' },
{ key: 'guanshi', label: '管事' },
{ key: 'zuzhang', label: '组长' },
{ key: 'shangjia', label: '商家' },
]
const DATE_LIST = [
{ key: '浠婃棩', label: '浠婃棩' },
{ key: '鏄ㄦ棩', label: '鏄ㄦ棩' },
{ key: '鏈懆', label: '鏈懆' },
{ key: '涓婂懆', label: '涓婂懆' },
{ key: '鏈湀', label: '鏈湀' },
{ key: '涓婃湀', label: '涓婃湀' },
{ key: '鎬绘', label: '鎬绘' },
{ key: '今日', label: '今日' },
{ key: '昨日', label: '昨日' },
{ key: '本周', label: '本周' },
{ key: '上周', label: '上周' },
{ key: '本月', label: '本月' },
{ key: '上月', label: '上月' },
{ key: '总榜', label: '总榜' },
]
const ROLE_META = {
dashou: {
title: '鎺ュ崟鍛樻帓琛屾',
title: '接单员排行榜',
sortField: 'chengjiao_zonge',
sortLabel: '鍒嗙孩鎬婚',
sortLabel: '分红总额',
mainType: 'money',
metrics: [
{ field: 'jiedan_zongliang', label: '鎺ュ崟閲?, type: 'int' },
{ field: 'jiedan_zonge', label: '鎺ュ崟棰?, type: 'money' },
{ field: 'chengjiao_zongliang', label: '鎴愪氦閲?, type: 'int' },
{ field: 'jiedan_zongliang', label: '接单量', type: 'int' },
{ field: 'jiedan_zonge', label: '接单额', type: 'money' },
{ field: 'chengjiao_zongliang', label: '成交量', type: 'int' },
],
},
guanshi: {
title: '绠′簨鎺掕姒?,
title: '管事排行榜',
sortField: 'shouru_zonge',
sortLabel: '鏀跺叆鎬婚',
sortLabel: '收入总额',
mainType: 'money',
metrics: [
{ field: 'yaoqing_dashou_shu', label: '閭€璇?, type: 'int' },
{ field: 'chongzhi_dashou_shu', label: '鍏呭€?, type: 'int' },
{ field: 'yaoqing_dashou_shu', label: '邀请', type: 'int' },
{ field: 'chongzhi_dashou_shu', label: '充值', type: 'int' },
],
},
zuzhang: {
title: '缁勯暱鎺掕姒?,
title: '组长排行榜',
sortField: 'shouru_zonge',
sortLabel: '鏀跺叆鎬婚',
sortLabel: '收入总额',
mainType: 'money',
metrics: [
{ field: 'yaoqing_guanshi_shu', label: '閭€璇?, type: 'int' },
{ field: 'fenyong_jine', label: '鍒嗕剑', type: 'money' },
{ field: 'yaoqing_guanshi_shu', label: '邀请', type: 'int' },
{ field: 'fenyong_jine', label: '分佣', type: 'money' },
],
},
shangjia: {
title: '鍟嗗鎺掕姒?,
title: '商家排行榜',
sortField: 'jiesuan_jine',
sortLabel: '鎴愪氦鎬婚',
sortLabel: '成交总额',
mainType: 'money',
metrics: [
{ field: 'jiesuan_dingdan_shu', label: '鎴愪氦閲?, type: 'int' },
{ field: 'paifa_dingdan_shu', label: '娲惧崟閲?, type: 'int' },
{ field: 'paifa_jine', label: '娲惧崟棰?, type: 'money' },
{ field: 'jiesuan_dingdan_shu', label: '成交量', type: 'int' },
{ field: 'paifa_dingdan_shu', label: '派单量', type: 'int' },
{ field: 'paifa_jine', label: '派单额', type: 'money' },
],
},
}
/** 鏄熶箣鐣屼笓鐢ㄥ睍绀轰笌鎺掑簭瀛楁 */
/** 星之界专用展示与排序字段 */
const XZJ_ROLE_META = {
dashou: {
title: '鎺ュ崟鍛樻帓琛屾',
title: '接单员排行榜',
sortField: 'chengjiao_zonge',
sortLabel: '鎴愪氦閲戦',
sortLabel: '成交金额',
mainType: 'money',
metrics: [
{ field: 'jiedan_zongliang', label: '鎺ュ崟閲?, type: 'int' },
{ field: 'jiedan_zonge', label: '鎺ュ崟棰?, type: 'money' },
{ field: 'chengjiao_zongliang', label: '鎴愪氦閲?, type: 'int' },
{ field: 'jiedan_zongliang', label: '接单量', type: 'int' },
{ field: 'jiedan_zonge', label: '接单额', type: 'money' },
{ field: 'chengjiao_zongliang', label: '成交量', type: 'int' },
],
},
guanshi: {
title: '绠′簨鎺掕姒?,
title: '管事排行榜',
sortField: 'chongzhi_dashou_shu',
sortLabel: '鏈夋晥浜烘暟',
sortLabel: '有效人数',
mainType: 'int',
metrics: [
{ field: 'wuxiao_yaoqing_dashou_shu', label: '鏃犳晥閭€璇?, type: 'int' },
{ field: 'yaoqing_dashou_shu', label: '閭€璇蜂汉鏁?, type: 'int' },
{ field: 'shouru_zonge', label: '鏀剁泭鎬婚', type: 'money' },
{ field: 'wuxiao_yaoqing_dashou_shu', label: '无效邀请', type: 'int' },
{ field: 'yaoqing_dashou_shu', label: '邀请人数', type: 'int' },
{ field: 'shouru_zonge', label: '收益总额', type: 'money' },
],
},
zuzhang: {
title: '缁勯暱鎺掕姒?,
title: '组长排行榜',
sortField: 'shouru_zonge',
sortLabel: '鏀剁泭鎬婚',
sortLabel: '收益总额',
mainType: 'money',
metrics: [
{ field: 'wuxiao_yaoqing_guanshi_shu', label: '鏃犳晥閭€璇?, type: 'int' },
{ field: 'youxiao_guanshi_shu', label: '鏈夋晥浜烘暟', type: 'int' },
{ field: 'yaoqing_guanshi_shu', label: '閭€璇蜂汉鏁?, type: 'int' },
{ field: 'wuxiao_yaoqing_guanshi_shu', label: '无效邀请', type: 'int' },
{ field: 'youxiao_guanshi_shu', label: '有效人数', type: 'int' },
{ field: 'yaoqing_guanshi_shu', label: '邀请人数', type: 'int' },
],
},
shangjia: {
title: '鍟嗗鎺掕姒?,
title: '商家排行榜',
sortField: 'paifa_jine',
sortLabel: '娲惧崟娴佹按',
sortLabel: '派单流水',
mainType: 'money',
metrics: [
{ field: 'paifa_dingdan_shu', label: '娲惧崟閲?, type: 'int' },
{ field: 'paifa_jine', label: '娲惧崟娴佹按', type: 'money' },
{ field: 'paifa_dingdan_shu', label: '派单量', type: 'int' },
{ field: 'paifa_jine', label: '派单流水', type: 'money' },
],
},
}
@@ -143,11 +145,12 @@ Page({
roleList: ROLE_LIST,
dateList: DATE_LIST,
currentRole: 'dashou',
currentDate: '浠婃棩',
currentDate: '今日',
fanwei: 'club',
showJituanScope: false, // 涓存椂闅愯棌闆嗗洟鎬绘锛屾仮澶嶆椂鏀?true 骞惰皟鏁?fanwei 榛樿鍊? myClubId: CLUB_ID || 'xq',
showJituanScope: false, // 临时隐藏集团总榜,恢复时改 true 并调整 fanwei 默认值
myClubId: CLUB_ID || 'xq',
roleMeta: ROLE_META.dashou,
sortLabel: '鍒嗙孩鎬婚',
sortLabel: '分红总额',
isLoading: true,
rankList: [],
topThree: [],
@@ -183,7 +186,7 @@ Page({
})
},
/** 涓?app.js 涓€鑷达細ossImageUrl + morentouxiang锛岄厤缃紓姝ュ姞杞藉悗 onShow 鍐嶅悓姝?*/
/** 与 app.js 一致:ossImageUrl + morentouxiang,配置异步加载后 onShow 再同步 */
syncDefaultAvatar() {
const def = getDefaultAvatar()
if (def && def !== this.data.defaultAvatar) {
@@ -193,7 +196,7 @@ Page({
}
},
/** 閰嶇疆鏅氫簬椤甸潰鍔犺浇鏃讹紝琛ュ叏绌哄ご鍍?*/
/** 配置晚于页面加载时,补全空头像 */
ensureAvatars() {
const def = getDefaultAvatar()
if (!def) return
@@ -223,7 +226,7 @@ Page({
applyRole(role, reload = true) {
const meta = ACTIVE_ROLE_META[role]
wx.setNavigationBarTitle({ title: '鎬绘帓琛屾' })
wx.setNavigationBarTitle({ title: '总排行榜' })
this.setData({ currentRole: role, roleMeta: meta, sortLabel: meta.sortLabel })
if (reload) this.fetchRankList()
},
@@ -252,19 +255,19 @@ Page({
onShowRules() {
const r = this.data.rewardInfo
if (!r || !r.tiers || !r.tiers.length) {
wx.showToast({ title: '鏆傛棤濂栧姳瑙勫垯', icon: 'none' })
wx.showToast({ title: '暂无奖励规则', icon: 'none' })
return
}
const lines = r.tiers.map((t) => {
const label = t.label || `绗?{t.rank_from}${t.rank_to > t.rank_from ? '-' + t.rank_to : ''}鍚峘
return `${label}锛毬?{t.amount}`
const label = t.label || `第${t.rank_from}${t.rank_to > t.rank_from ? '-' + t.rank_to : ''}名`
return `${label}:¥${t.amount}`
})
const tip = r.period_status === 'open' ? '锛堟湰鏈熻繘琛屼腑锛岀粨鏉熷悗鍙鍙栵級' : ''
const tip = r.period_status === 'open' ? '(本期进行中,结束后可领取)' : ''
wx.showModal({
title: `${r.title || '鎺掕姒滃鍔?}${tip}`,
title: `${r.title || '排行榜奖励'}${tip}`,
content: [
`淇变箰閮細${r.club_id || this.data.myClubId}`,
`鎺掑簭锛?{r.sort_label || ''}`,
`俱乐部:${r.club_id || this.data.myClubId}`,
`排序:${r.sort_label || ''}`,
...lines,
r.description || '',
].filter(Boolean).join('\n'),
@@ -284,13 +287,13 @@ Page({
})
const body = res?.data || {}
if (body.code === 200) {
wx.showToast({ title: `宸查鍙?楼${claim.amount}`, icon: 'success' })
wx.showToast({ title: `已领取 ¥${claim.amount}`, icon: 'success' })
await this.fetchRewardInfo()
} else {
wx.showToast({ title: body.msg || '棰嗗彇澶辫触', icon: 'none' })
wx.showToast({ title: body.msg || '领取失败', icon: 'none' })
}
} catch (e) {
wx.showToast({ title: '棰嗗彇澶辫触', icon: 'none' })
wx.showToast({ title: '领取失败', icon: 'none' })
} finally {
this.setData({ claimLoading: false })
}
@@ -314,7 +317,7 @@ Page({
const meta = ACTIVE_ROLE_META[role]
const app = getApp()
const uid = raw.yonghuid || raw.uid || ''
const nicheng = raw.nicheng || raw.nick || (role === 'dashou' ? '鎵撴墜' : role === 'guanshi' ? '绠′簨' : role === 'zuzhang' ? '缁勯暱' : role === 'shangjia' ? '鍟嗗' : '鐢ㄦ埛')
const nicheng = raw.nicheng || raw.nick || (role === 'dashou' ? '打手' : role === 'guanshi' ? '管事' : role === 'zuzhang' ? '组长' : role === 'shangjia' ? '商家' : '用户')
const def = getDefaultAvatar(app)
const avatar = buildAvatar(raw.touxiang || raw.avatar, app) || def
const metrics = meta.metrics.map((m) => {
@@ -345,7 +348,7 @@ Page({
clubName: raw.club_name || raw.club_id || '',
rewardAmount: raw.reward_amount || 0,
mainType,
mainPrefix: mainType === 'money' ? '楼' : '',
mainPrefix: mainType === 'money' ? '¥' : '',
mainValue,
mainLabel: (IS_XZJ ? meta.sortLabel : (raw.metric_label || meta.sortLabel)),
metrics,
@@ -387,7 +390,7 @@ Page({
restList: merged.slice(3),
})
} catch (e) {
console.warn('濂栧姳淇℃伅鍔犺浇澶辫触', e)
console.warn('奖励信息加载失败', e)
}
},
@@ -432,7 +435,7 @@ Page({
if (!Array.isArray(list)) list = []
const role = currentRole
// 鍚嶆椤哄簭瀹屽叏娌跨敤鍚庣杩斿洖锛屽墠绔笉鍋?sort / 閲嶆帓
// 名次顺序完全沿用后端返回,前端不做 sort / 重排
let normalized = list.slice(0, 50).map((item, i) => this.normalizeItem(item, i, role))
if (rewardFromRank) {
normalized = this.applyRewardToList(normalized, rewardFromRank, 'club')
@@ -450,8 +453,8 @@ Page({
await this.fetchRewardInfo()
}
} catch (e) {
console.error('鎺掕姒滃姞杞藉け璐?, e)
wx.showToast({ title: '鍔犺浇澶辫触', icon: 'none' })
console.error('排行榜加载失败', e)
wx.showToast({ title: '加载失败', icon: 'none' })
this.setData({ rankList: [], topThree: [], restList: [], isEmpty: true, isLoading: false })
}
},
+5 -5
View File
@@ -2,10 +2,10 @@
"usingComponents": {
"global-notification": "/components/global-notification/global-notification"
},
"navigationBarTitleText": "排行榜",
"navigationBarBackgroundColor": "#0d1117",
"navigationBarTextStyle": "white",
"navigationBarTitleText": "总排行榜",
"navigationBarBackgroundColor": "#FFE566",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"backgroundColor": "#0d1117",
"backgroundTextStyle": "light"
"backgroundColor": "#FFE566",
"backgroundTextStyle": "dark"
}
+80 -75
View File
@@ -1,30 +1,47 @@
<view class="rank-page">
<image class="page-bg" src="{{imgUrls.pageBg}}" mode="aspectFill" />
<view class="top-bar">
<view class="top-row">
<text class="page-title">{{roleMeta.title}}</text>
<view class="refresh-wrap" bindtap="onRefreshTap">
<image class="refresh-ico" src="{{imgUrls.refreshIcon}}" mode="aspectFit" />
<!-- 顶区:黄底 + 白 Tab + 时间筛选 -->
<view class="header-zone">
<view class="role-tabs">
<view
wx:for="{{roleList}}" wx:key="key"
class="role-tab {{currentRole === item.key ? 'active' : ''}}"
data-role="{{item.key}}" bindtap="onRoleTap"
>
<text class="role-tab-text">{{item.label}}</text>
<view class="role-tab-line" wx:if="{{currentRole === item.key}}"></view>
</view>
</view>
<scroll-view scroll-x class="role-scroll" enhanced show-scrollbar="{{false}}">
<view class="role-line">
<scroll-view scroll-x class="date-scroll" enhanced show-scrollbar="{{false}}">
<view class="date-line">
<view
wx:for="{{roleList}}" wx:key="key"
class="role-chip {{currentRole === item.key ? 'on' : ''}}"
data-role="{{item.key}}" bindtap="onRoleTap"
wx:for="{{dateList}}" wx:key="key"
class="date-tab {{currentDate === item.key ? 'active' : ''}}"
data-date="{{item.key}}" bindtap="onDateTap"
>{{item.label}}</view>
</view>
</scroll-view>
<view class="date-grid">
<view
wx:for="{{dateList}}" wx:key="key"
class="date-chip {{currentDate === item.key ? 'on' : ''}}"
data-date="{{item.key}}" bindtap="onDateTap"
>{{item.label}}</view>
<view class="scope-row">
<!-- 临时隐藏集团总榜,恢复时取消 wx:if 注释 -->
<view wx:if="{{showJituanScope}}" class="scope-tab {{fanwei === 'jituan' ? 'active' : ''}}" data-fanwei="jituan" bindtap="onScopeTap">集团总榜</view>
<view class="scope-tab {{fanwei === 'club' ? 'active' : ''}}" data-fanwei="club" bindtap="onScopeTap">本俱乐部榜</view>
<view class="refresh-btn" bindtap="onRefreshTap">
<image class="refresh-ico" src="{{imgUrls.refreshIcon}}" mode="aspectFit" />
</view>
</view>
<text class="sort-tip">{{currentDate}} · 按{{sortLabel}} · TOP50</text>
<view class="reward-banner" wx:if="{{rewardInfo && rewardInfo.scheme_enabled}}" bindtap="onShowRules">
<view class="reward-banner-main">
<text class="reward-tag">🏆 {{rewardInfo.title || '排行榜奖励'}}</text>
<text class="reward-club">俱乐部 {{rewardInfo.club_id || myClubId}}</text>
<text class="reward-status" wx:if="{{rewardInfo.period_status === 'open'}}">进行中 · 结束后可领</text>
<text class="reward-status pending" wx:elif="{{rewardInfo.my_claim}}">待领取 ¥{{rewardInfo.my_claim.amount}}</text>
<text class="reward-status claimed" wx:elif="{{rewardInfo.my_claimed}}">已领取 ¥{{rewardInfo.my_claimed.amount}}</text>
</view>
<text class="reward-link">规则 ›</text>
</view>
</view>
<view class="state-wrap" wx:if="{{isLoading}}">
@@ -38,97 +55,85 @@
</view>
<scroll-view wx:else scroll-y class="body-scroll" enhanced show-scrollbar="{{false}}">
<!-- 前三名领奖台 -->
<view class="podium-zone" wx:if="{{topThree.length > 0}}">
<view class="podium-row">
<!-- 前三名 · 纯 CSS 领奖台 -->
<view class="podium-panel" wx:if="{{topThree.length > 0}}">
<view class="podium-cols">
<!-- 榜二 -->
<view class="p-col p-second" wx:if="{{topThree[1]}}">
<view class="p-badge badge-silver"><text>2</text></view>
<view class="p-avatar-wrap wrap-silver">
<view class="podium-col col-2" wx:if="{{topThree[1]}}">
<view class="crown crown-silver">👑</view>
<view class="avatar-ring ring-silver">
<image class="p-avatar" src="{{topThree[1].avatar || defaultAvatar}}" mode="aspectFill" data-zone="top" data-index="1" binderror="onAvatarError" />
</view>
<text class="p-name">{{topThree[1].nicheng}}</text>
<text class="p-uid">ID {{topThree[1].uid}}</text>
<view class="p-amount">
<text class="p-money">¥{{topThree[1].mainValue}}</text>
<text class="p-label">{{topThree[1].mainLabel}}</text>
</view>
<text class="p-label">{{topThree[1].mainLabel}}</text>
<text class="p-amount">{{topThree[1].mainPrefix}}{{topThree[1].mainValue}}</text>
<view class="p-metrics">
<text class="p-metric" wx:for="{{topThree[1].metrics}}" wx:key="label" wx:for-item="m">{{m.label}} {{m.value}}</text>
</view>
<view class="p-pedestal pedestal-silver"></view>
<text class="p-reward" wx:if="{{topThree[1].rewardAmount > 0}}">奖 ¥{{topThree[1].rewardAmount}}</text>
<view class="pedestal pedestal-2"></view>
</view>
<!-- 榜一 -->
<view class="p-col p-first" wx:if="{{topThree[0]}}">
<view class="p-badge badge-first"><text>1</text></view>
<view class="p-avatar-wrap wrap-first">
<view class="podium-col col-1" wx:if="{{topThree[0]}}">
<view class="crown crown-gold">👑</view>
<view class="avatar-ring ring-gold">
<image class="p-avatar p-avatar-lg" src="{{topThree[0].avatar || defaultAvatar}}" mode="aspectFill" data-zone="top" data-index="0" binderror="onAvatarError" />
</view>
<text class="p-name p-name-first">{{topThree[0].nicheng}}</text>
<text class="p-uid">ID {{topThree[0].uid}}</text>
<view class="p-amount">
<text class="p-money p-money-first">¥{{topThree[0].mainValue}}</text>
<text class="p-label">{{topThree[0].mainLabel}}</text>
</view>
<text class="p-name p-name-lg">{{topThree[0].nicheng}}</text>
<text class="p-label">{{topThree[0].mainLabel}}</text>
<text class="p-amount p-amount-lg">{{topThree[0].mainPrefix}}{{topThree[0].mainValue}}</text>
<view class="p-metrics">
<text class="p-metric" wx:for="{{topThree[0].metrics}}" wx:key="label" wx:for-item="m">{{m.label}} {{m.value}}</text>
</view>
<view class="p-pedestal pedestal-first"></view>
<text class="p-reward" wx:if="{{topThree[0].rewardAmount > 0}}">奖 ¥{{topThree[0].rewardAmount}}</text>
<view class="pedestal pedestal-1"></view>
</view>
<!-- 榜三 -->
<view class="p-col p-third" wx:if="{{topThree[2]}}">
<view class="p-badge badge-bronze"><text>3</text></view>
<view class="p-avatar-wrap wrap-bronze">
<view class="podium-col col-3" wx:if="{{topThree[2]}}">
<view class="crown crown-bronze">👑</view>
<view class="avatar-ring ring-bronze">
<image class="p-avatar" src="{{topThree[2].avatar || defaultAvatar}}" mode="aspectFill" data-zone="top" data-index="2" binderror="onAvatarError" />
</view>
<text class="p-name">{{topThree[2].nicheng}}</text>
<text class="p-uid">ID {{topThree[2].uid}}</text>
<view class="p-amount">
<text class="p-money">¥{{topThree[2].mainValue}}</text>
<text class="p-label">{{topThree[2].mainLabel}}</text>
</view>
<text class="p-label">{{topThree[2].mainLabel}}</text>
<text class="p-amount">{{topThree[2].mainPrefix}}{{topThree[2].mainValue}}</text>
<view class="p-metrics">
<text class="p-metric" wx:for="{{topThree[2].metrics}}" wx:key="label" wx:for-item="m">{{m.label}} {{m.value}}</text>
</view>
<view class="p-pedestal pedestal-bronze"></view>
<text class="p-reward" wx:if="{{topThree[2].rewardAmount > 0}}">奖 ¥{{topThree[2].rewardAmount}}</text>
<view class="pedestal pedestal-3"></view>
</view>
</view>
</view>
<!-- 4-50 名 -->
<view class="list-wrap" wx:if="{{restList.length > 0}}">
<view class="list-title-row">
<text class="list-title">完整榜单</text>
<text class="list-range">第 4 - {{rankList.length}} 名</text>
</view>
<view class="list-card" wx:for="{{restList}}" wx:key="mingci">
<image class="list-card-bg" src="{{imgUrls.cardBg}}" mode="aspectFill" />
<view class="list-card-body">
<view class="lc-rank"><text>{{item.mingci}}</text></view>
<image class="lc-avatar" src="{{item.avatar || defaultAvatar}}" mode="aspectFill" data-zone="rest" data-index="{{index}}" binderror="onAvatarError" />
<view class="lc-info">
<text class="lc-name">{{item.nicheng}}</text>
<text class="lc-uid">ID {{item.uid}}</text>
<view class="lc-tags">
<text class="lc-tag" wx:for="{{item.metrics}}" wx:key="label" wx:for-item="m">{{m.label}} {{m.value}}</text>
</view>
</view>
<view class="lc-score">
<text class="lc-money">¥{{item.mainValue}}</text>
<text class="lc-label">{{item.mainLabel}}</text>
<!-- 4 名以后列表 -->
<view class="list-panel" wx:if="{{restList.length > 0}}">
<view class="list-row" wx:for="{{restList}}" wx:key="mingci">
<text class="li-rank">{{item.mingci}}</text>
<image class="li-avatar" src="{{item.avatar || defaultAvatar}}" mode="aspectFill" data-zone="rest" data-index="{{index}}" binderror="onAvatarError" />
<text class="li-name">{{item.nicheng}}</text>
<view class="li-right">
<text class="li-amount">{{item.mainPrefix}}{{item.mainValue}}</text>
<text class="li-label">{{item.mainLabel}}</text>
<view class="li-tags">
<text class="li-tag" wx:for="{{item.metrics}}" wx:key="label" wx:for-item="m">{{m.label}} {{m.value}}</text>
</view>
<text class="li-reward" wx:if="{{item.rewardAmount > 0}}">奖¥{{item.rewardAmount}}</text>
</view>
</view>
</view>
<view class="bottom-gap"></view>
</scroll-view>
<view class="claim-bar" wx:if="{{showClaimBar && rewardInfo.my_claim}}">
<view class="claim-text">
<text class="claim-title">🎉 第{{rewardInfo.my_claim.mingci}}名 · 待领取</text>
<text class="claim-amount">¥{{rewardInfo.my_claim.amount}}</text>
</view>
<button class="claim-btn" loading="{{claimLoading}}" bindtap="onClaimTap">立即领取</button>
</view>
</view>
<global-notification id="global-notification" />
+384 -330
View File
@@ -1,290 +1,434 @@
page {
background: #0d1117;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
background: #ffe566;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
}
/* 主题色 */
/* 选中/金额: #6BA3F7 浅蓝: #8EB8FF 银: #A8B4C4 铜: #C4A882 */
.rank-page {
min-height: 100vh;
position: relative;
background: linear-gradient(180deg, #ffe566 0%, #ffe566 420rpx, #fff8dc 520rpx, #ffffff 620rpx);
}
.page-bg {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
}
/* ========== 顶栏 ========== */
.top-bar {
/* ========== 顶区 ========== */
.header-zone {
position: relative;
z-index: 10;
padding: 16rpx 28rpx 20rpx;
padding-bottom: 8rpx;
}
.top-row {
.role-tabs {
display: flex;
align-items: stretch;
background: #ffffff;
margin: 0 0 0;
padding: 0 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.role-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx 0 18rpx;
position: relative;
}
.role-tab-text {
font-size: 28rpx;
color: #666666;
line-height: 1.2;
}
.role-tab.active .role-tab-text {
color: #1a1a1a;
font-weight: 700;
font-size: 30rpx;
}
.role-tab-line {
position: absolute;
bottom: 8rpx;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
border-radius: 6rpx;
background: #3d2914;
}
.date-scroll {
width: 100%;
white-space: nowrap;
padding: 20rpx 0 8rpx;
}
.date-line {
display: inline-flex;
padding: 0 20rpx;
gap: 8rpx;
}
.date-tab {
display: inline-block;
padding: 10rpx 22rpx;
font-size: 26rpx;
color: rgba(0, 0, 0, 0.45);
border-radius: 8rpx;
}
.date-tab.active {
color: #1a1a1a;
font-weight: 700;
position: relative;
}
.date-tab.active::after {
content: '';
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
width: 36rpx;
height: 4rpx;
border-radius: 4rpx;
background: #3d2914;
}
.scope-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
gap: 12rpx;
padding: 8rpx 24rpx 12rpx;
}
.page-title {
font-size: 38rpx;
font-weight: 700;
color: #fff;
.scope-tab {
padding: 8rpx 20rpx;
border-radius: 999rpx;
font-size: 22rpx;
color: rgba(0, 0, 0, 0.55);
background: rgba(255, 255, 255, 0.55);
border: 1rpx solid rgba(0, 0, 0, 0.06);
}
.refresh-wrap {
width: 60rpx; height: 60rpx;
.scope-tab.active {
color: #1a1a1a;
font-weight: 600;
background: #ffffff;
border-color: rgba(0, 0, 0, 0.12);
}
.refresh-btn {
margin-left: auto;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: rgba(255,255,255,0.14);
background: rgba(255, 255, 255, 0.7);
display: flex;
align-items: center;
justify-content: center;
}
.refresh-ico {
width: 32rpx; height: 32rpx;
opacity: 0.8;
width: 28rpx;
height: 28rpx;
opacity: 0.65;
}
.role-scroll {
width: 100%;
white-space: nowrap;
margin-bottom: 16rpx;
.reward-banner {
margin: 0 24rpx 12rpx;
padding: 18rpx 22rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.85);
border: 1rpx solid rgba(255, 193, 7, 0.35);
display: flex;
align-items: center;
justify-content: space-between;
}
.role-line {
display: inline-flex;
gap: 12rpx;
.reward-banner-main {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.role-chip {
display: inline-block;
padding: 12rpx 28rpx;
border-radius: 999rpx;
font-size: 26rpx;
color: rgba(255,255,255,0.65);
background: rgba(20, 26, 40, 0.75);
border: 1rpx solid rgba(255,255,255,0.12);
}
.role-chip.on {
color: #fff;
font-weight: 600;
background: rgba(107, 163, 247, 0.45);
border-color: rgba(107, 163, 247, 0.65);
box-shadow: 0 0 20rpx rgba(107, 163, 247, 0.2);
}
.date-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10rpx;
margin-bottom: 12rpx;
}
.date-chip {
text-align: center;
padding: 14rpx 0;
border-radius: 14rpx;
font-size: 24rpx;
color: rgba(255,255,255,0.65);
background: rgba(20, 26, 40, 0.72);
border: 1rpx solid rgba(255,255,255,0.08);
}
.date-chip.on {
color: #A8CCFF;
font-weight: 600;
background: rgba(107, 163, 247, 0.35);
border-color: rgba(107, 163, 247, 0.55);
}
.sort-tip {
font-size: 22rpx;
color: rgba(255,255,255,0.3);
}
.reward-tag { font-size: 26rpx; font-weight: 600; color: #d48806; }
.reward-club { font-size: 22rpx; color: #666; }
.reward-status { font-size: 22rpx; color: #999; }
.reward-status.pending { color: #52c41a; font-weight: 600; }
.reward-status.claimed { color: #8c8c8c; }
.reward-link { font-size: 24rpx; color: #d48806; }
/* ========== 状态 ========== */
.state-wrap {
position: relative;
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
padding: 140rpx 0;
color: rgba(255,255,255,0.45);
padding: 120rpx 0;
color: rgba(0, 0, 0, 0.45);
font-size: 28rpx;
}
.loader {
width: 48rpx; height: 48rpx;
border: 3rpx solid rgba(255,255,255,0.12);
border-top-color: #6BA3F7;
width: 48rpx;
height: 48rpx;
border: 3rpx solid rgba(0, 0, 0, 0.08);
border-top-color: #ffb800;
border-radius: 50%;
animation: spin 0.7s linear infinite;
margin-bottom: 20rpx;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.state-img {
width: 160rpx; height: 160rpx;
width: 160rpx;
height: 160rpx;
margin-bottom: 20rpx;
opacity: 0.65;
opacity: 0.5;
}
/* ========== 滚动 ========== */
/* ========== 滚动区 ========== */
.body-scroll {
position: relative;
z-index: 5;
height: calc(100vh - 300rpx);
height: calc(100vh - 280rpx);
}
/* ========== 领奖台(纯 CSS) ========== */
.podium-panel {
margin: 8rpx 24rpx 24rpx;
padding: 28rpx 12rpx 0;
border-radius: 28rpx;
background: rgba(14, 18, 30, 0.88);
border: 1rpx solid rgba(255, 255, 255, 0.1);
/* ========== 领奖台 ========== */
.podium-zone {
padding: 16rpx 16rpx 0;
}
.podium-cols {
.podium-row {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 10rpx;
gap: 6rpx;
}
.p-col {
.podium-col {
flex: 1;
max-width: 220rpx;
max-width: 230rpx;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.p-first {
max-width: 250rpx;
margin-bottom: 16rpx;
.col-1 {
max-width: 260rpx;
margin-bottom: 10rpx;
}
.p-badge {
width: 44rpx; height: 44rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14rpx;
font-size: 24rpx;
font-weight: 800;
}
.p-first .p-badge {
width: 52rpx; height: 52rpx;
.crown {
font-size: 28rpx;
line-height: 1;
margin-bottom: 6rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.15));
}
.badge-first {
color: #fff;
background: linear-gradient(145deg, #6BA3F7, #4A7FD4);
box-shadow: 0 4rpx 16rpx rgba(107, 163, 247, 0.4);
.col-1 .crown {
font-size: 34rpx;
}
.badge-silver {
color: #fff;
background: linear-gradient(145deg, #A8B4C4, #7A8899);
box-shadow: 0 4rpx 12rpx rgba(168, 180, 196, 0.25);
}
.badge-bronze {
color: #fff;
background: linear-gradient(145deg, #C4A882, #9A7858);
box-shadow: 0 4rpx 12rpx rgba(196, 168, 130, 0.25);
}
.p-avatar-wrap {
.avatar-ring {
border-radius: 50%;
padding: 4rpx;
margin-bottom: 12rpx;
padding: 6rpx;
margin-bottom: 10rpx;
background: #ffffff;
}
.wrap-first {
padding: 5rpx;
background: linear-gradient(145deg, #8EB8FF, #5B8FD8);
box-shadow: 0 6rpx 24rpx rgba(107, 163, 247, 0.3);
.ring-gold {
padding: 8rpx;
box-shadow: 0 0 0 4rpx #ffd700, 0 8rpx 20rpx rgba(255, 183, 0, 0.35);
}
.wrap-silver {
background: linear-gradient(145deg, #C8D0DA, #909AA8);
.ring-silver {
box-shadow: 0 0 0 4rpx #b8c4d4, 0 6rpx 16rpx rgba(120, 140, 180, 0.25);
}
.wrap-bronze {
background: linear-gradient(145deg, #D4B896, #A88860);
.ring-bronze {
box-shadow: 0 0 0 4rpx #cd9a5a, 0 6rpx 16rpx rgba(205, 127, 50, 0.25);
}
.p-avatar {
width: 100rpx; height: 100rpx;
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: block;
background: #1a2030;
background: #f0f0f0;
}
.p-avatar-lg {
width: 120rpx; height: 120rpx;
width: 116rpx;
height: 116rpx;
}
.p-name {
font-size: 26rpx;
font-weight: 600;
color: rgba(255,255,255,0.92);
color: #1a1a1a;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
margin-bottom: 8rpx;
}
.p-name-first {
.p-name-lg {
font-size: 28rpx;
color: #fff;
}
.p-uid {
margin-top: 4rpx;
font-size: 20rpx;
color: rgba(255,255,255,0.35);
}
.p-amount {
margin: 12rpx 0 10rpx;
text-align: center;
}
.p-money {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #8EB8FF;
letter-spacing: -0.5rpx;
}
.p-money-first {
font-size: 34rpx;
color: #A8CCFF;
}
.p-label {
display: block;
font-size: 22rpx;
color: #999999;
line-height: 1.2;
}
.p-amount {
font-size: 34rpx;
font-weight: 800;
color: #ffb800;
line-height: 1.3;
margin-top: 2rpx;
}
.p-amount-lg {
font-size: 40rpx;
}
.p-sub-label {
font-size: 22rpx;
color: #999999;
margin-top: 6rpx;
}
.p-sub-value {
font-size: 28rpx;
font-weight: 700;
color: #ffb800;
line-height: 1.2;
}
.p-sub-value-lg {
font-size: 32rpx;
}
.p-reward {
margin-top: 4rpx;
font-size: 20rpx;
color: rgba(255,255,255,0.35);
color: #d48806;
font-weight: 600;
}
.pedestal {
width: 92%;
margin-top: 14rpx;
border-radius: 16rpx 16rpx 0 0;
background: linear-gradient(180deg, #ffe08a 0%, #ffd04d 100%);
box-shadow: inset 0 2rpx 0 rgba(255, 255, 255, 0.5);
}
.pedestal-1 {
height: 88rpx;
background: linear-gradient(180deg, #ffe08a 0%, #ffc933 100%);
}
.pedestal-2 {
height: 64rpx;
background: linear-gradient(180deg, #ffe08a 0%, #ffd04d 100%);
}
.pedestal-3 {
height: 48rpx;
background: linear-gradient(180deg, #ffe08a 0%, #ffd04d 100%);
}
/* ========== 列表区(白底) ========== */
.list-panel {
margin-top: 8rpx;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
padding: 24rpx 28rpx 0;
min-height: 400rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.list-row {
display: flex;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f3f3f3;
}
.list-row:last-child {
border-bottom: none;
}
.li-rank {
width: 44rpx;
flex-shrink: 0;
font-size: 32rpx;
font-weight: 700;
color: #333333;
text-align: center;
}
.li-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
flex-shrink: 0;
margin: 0 16rpx 0 8rpx;
background: #f0f0f0;
}
.li-name {
flex: 1;
min-width: 0;
font-size: 28rpx;
font-weight: 500;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.li-right {
flex-shrink: 0;
text-align: right;
margin-left: 12rpx;
}
.li-amount {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
line-height: 1.3;
}
.li-orders {
display: block;
margin-top: 4rpx;
font-size: 24rpx;
color: #999999;
}
.li-reward {
display: block;
margin-top: 4rpx;
font-size: 20rpx;
color: #d48806;
}
.sort-tip {
display: block;
padding: 0 24rpx 8rpx;
font-size: 22rpx;
color: rgba(0, 0, 0, 0.45);
}
.p-metrics {
@@ -292,176 +436,86 @@ page {
flex-wrap: wrap;
gap: 6rpx;
justify-content: center;
margin-bottom: 16rpx;
min-height: 36rpx;
margin: 8rpx 0 10rpx;
max-width: 100%;
}
.p-metric {
font-size: 18rpx;
color: rgba(255,255,255,0.55);
background: rgba(255,255,255,0.1);
font-size: 20rpx;
color: #888;
background: rgba(255, 255, 255, 0.65);
padding: 4rpx 10rpx;
border-radius: 8rpx;
}
/* 底座 · 圆角柱体,非方块 */
.p-pedestal {
width: 88%;
border-radius: 20rpx 20rpx 0 0;
position: relative;
}
.p-pedestal::before {
content: '';
position: absolute;
top: 0; left: 10%; right: 10%;
height: 2rpx;
background: rgba(255,255,255,0.15);
border-radius: 2rpx;
}
.pedestal-first {
height: 100rpx;
background: linear-gradient(180deg, rgba(107,163,247,0.55) 0%, rgba(107,163,247,0.22) 100%);
border: 1rpx solid rgba(107,163,247,0.35);
border-bottom: none;
}
.pedestal-silver {
height: 72rpx;
background: linear-gradient(180deg, rgba(168,180,196,0.45) 0%, rgba(168,180,196,0.18) 100%);
border: 1rpx solid rgba(168,180,196,0.28);
border-bottom: none;
}
.pedestal-bronze {
height: 56rpx;
background: linear-gradient(180deg, rgba(196,168,130,0.45) 0%, rgba(196,168,130,0.18) 100%);
border: 1rpx solid rgba(196,168,130,0.28);
border-bottom: none;
}
/* ========== 列表 ========== */
.list-wrap {
padding: 0 24rpx;
}
.list-title-row {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 8rpx 8rpx 20rpx;
}
.list-title {
font-size: 30rpx;
font-weight: 700;
color: rgba(255,255,255,0.85);
}
.list-range {
font-size: 22rpx;
color: rgba(255,255,255,0.3);
}
.list-card {
position: relative;
margin-bottom: 14rpx;
border-radius: 18rpx;
overflow: hidden;
border: 1rpx solid rgba(255,255,255,0.05);
}
.list-card-bg {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
}
.list-card-body {
position: relative;
z-index: 1;
display: flex;
align-items: center;
padding: 20rpx 22rpx;
background: rgba(12, 16, 28, 0.92);
backdrop-filter: blur(8px);
}
.lc-rank {
width: 48rpx;
flex-shrink: 0;
text-align: center;
}
.lc-rank text {
font-size: 32rpx;
font-weight: 700;
color: rgba(255,255,255,0.18);
}
.lc-avatar {
width: 80rpx; height: 80rpx;
border-radius: 50%;
flex-shrink: 0;
margin: 0 16rpx 0 6rpx;
border: 2rpx solid rgba(107, 163, 247, 0.2);
background: #1a2030;
}
.lc-info { flex: 1; min-width: 0; }
.lc-name {
.li-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: rgba(255,255,255,0.92);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.lc-uid {
display: block;
margin-top: 4rpx;
margin-top: 2rpx;
font-size: 22rpx;
color: rgba(255,255,255,0.32);
color: #999;
}
.lc-tags {
.li-tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-top: 8rpx;
gap: 6rpx;
justify-content: flex-end;
margin-top: 6rpx;
max-width: 280rpx;
}
.lc-tag {
.li-tag {
font-size: 20rpx;
color: rgba(255,255,255,0.5);
background: rgba(255,255,255,0.1);
padding: 2rpx 10rpx;
color: #888;
background: #f5f5f5;
padding: 2rpx 8rpx;
border-radius: 6rpx;
}
.lc-score {
flex-shrink: 0;
text-align: right;
margin-left: 10rpx;
.bottom-gap {
height: 160rpx;
}
.lc-money {
display: block;
font-size: 28rpx;
/* ========== 领取栏 ========== */
.claim-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.claim-text {
display: flex;
flex-direction: column;
}
.claim-title {
font-size: 26rpx;
color: #333;
}
.claim-amount {
font-size: 36rpx;
font-weight: 700;
color: #8EB8FF;
color: #ffb800;
}
.lc-label {
display: block;
margin-top: 2rpx;
font-size: 20rpx;
color: rgba(255,255,255,0.32);
.claim-btn {
margin: 0;
padding: 0 40rpx;
height: 72rpx;
line-height: 72rpx;
font-size: 28rpx;
background: linear-gradient(135deg, #ffc107, #ffb300);
color: #1a1a1a;
border-radius: 999rpx;
border: none;
}
.bottom-gap { height: 48rpx; }
+497
View File
@@ -0,0 +1,497 @@
// pages/fighter-recharge/fighter-deposit.js — 平台履约金/保证金独立页
import request from '../../utils/request';
import PopupService from '../../services/popupService.js';
import { ensurePhoneAuth } from '../../utils/phone-auth.js';
import { ICON_KEYS, resolveConfiguredIcon, resolveMiniappIcon, ICON_FOLDER, refreshPindaoConfig } from '../../utils/miniapp-icons.js';
import { resolveAvatarFromStorage, onAvatarImageError } from '../../utils/avatar.js';
const app = getApp();
const DEFAULT_RULES = [
'履约金有效期为永久有效',
'超过15天未接单,可联系客服申请退还',
'缴纳后可优先接锁单/平台单等待遇',
];
const DEFAULT_BENEFITS = [
{ label: '平台订单优先', key: ICON_KEYS.FIGHTER_DEPOSIT_BENEFIT_1 },
{ label: '快人一步', key: ICON_KEYS.FIGHTER_DEPOSIT_BENEFIT_2 },
{ label: '优质接单', key: ICON_KEYS.FIGHTER_DEPOSIT_BENEFIT_3 },
{ label: '解锁平台单', key: ICON_KEYS.FIGHTER_DEPOSIT_BENEFIT_4 },
];
Page({
data: {
imgUrls: { pageBg: '', hero: '' },
avatarUrl: '',
nickname: '',
uid: '',
yajin: 0,
yajinDisplay: '0.00',
yajinActive: false,
depositRules: DEFAULT_RULES,
depositBenefits: DEFAULT_BENEFITS,
rulesTitle: '履约金规则',
benefitsTitle: '平台履约金权益',
pageCopy: null,
agreed: false,
payAmountLabel: '',
yajinAmount: '100',
quickAmounts: ['100', '500', '1000', '5000'],
showAmountModal: false,
showPayMethodModal: false,
showBalanceModal: false,
showConfirmModal: false,
balanceOptions: [],
selectedBalanceId: null,
selectedBalanceInfo: null,
currentYajinAmount: null,
currentDingdanid: '',
payLoading: false,
loadingText: '处理中...',
},
onLoad(options) {
this._jumpParams = options || {};
this.initImages();
this.loadUserInfo();
this.fetchPageCopy();
},
onHide() {
this.selectComponent('#popupNotice')?.cleanup?.();
},
async onShow() {
const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'dashou' });
if (!phoneOk) return;
this.registerNotification();
this.loadYajin();
this.refreshConfiguredIcons();
if (!this.data.pageCopy) {
this.fetchPageCopy();
}
PopupService.checkAndShow(this, 'dashouchongzhi');
const preset = this._jumpParams?.presetAmount || this._jumpParams?.requiredYajin;
if (preset && parseFloat(preset) > 0) {
this.setData({ yajinAmount: String(parseInt(preset, 10)) });
}
},
registerNotification() {
const comp = this.selectComponent('#global-notification');
if (comp?.showNotification) {
app.globalData.globalNotification = {
show: (data) => comp.showNotification(data),
hide: () => comp.hideNotification(),
};
}
},
resolveOssUrl(path) {
if (!path) return '';
if (path.startsWith('http')) return path;
return (app.globalData.ossImageUrl || '') + path;
},
async fetchPageCopy() {
try {
const res = await request({
url: '/jituan/recharge-page-copy',
method: 'POST',
data: { page_key: 'yajin' },
});
if (res.data && res.data.code === 0 && res.data.data) {
this.setData({ pageCopy: res.data.data.regions || {} });
} else {
this.setData({ pageCopy: {} });
}
} catch (e) {
console.warn('fetchPageCopy failed', e);
this.setData({ pageCopy: {} });
}
this.applyPageCopy();
},
applyPageCopy() {
const copy = this.data.pageCopy || {};
const patch = {};
const rulesRegion = copy.rules;
if (rulesRegion && Array.isArray(rulesRegion.items) && rulesRegion.items.length > 0) {
patch.depositRules = rulesRegion.items
.map((it) => (typeof it === 'string' ? it : (it && it.text) || ''))
.filter(Boolean);
if (rulesRegion.title) patch.rulesTitle = rulesRegion.title;
} else {
patch.depositRules = DEFAULT_RULES;
patch.rulesTitle = '履约金规则';
}
const benefitsRegion = copy.benefits;
const fb = (key) => `${ICON_FOLDER}/${key}.png`;
if (benefitsRegion && Array.isArray(benefitsRegion.items) && benefitsRegion.items.length > 0) {
patch.depositBenefits = benefitsRegion.items.map((item, i) => {
const fallback = DEFAULT_BENEFITS[i];
const iconUrl = item.icon_url
? this.resolveOssUrl(item.icon_url)
: (fallback
? resolveMiniappIcon(app, fallback.key, fb(fallback.key))
: '');
return {
label: item.label || item.title || '',
iconUrl,
};
});
if (benefitsRegion.title) patch.benefitsTitle = benefitsRegion.title;
} else {
patch.depositBenefits = DEFAULT_BENEFITS.map((item) => ({
label: item.label,
iconUrl: resolveMiniappIcon(app, item.key, fb(item.key)),
}));
patch.benefitsTitle = '平台履约金权益';
}
this.setData(patch);
},
initImages() {
const fb = (key) => `${ICON_FOLDER}/${key}.png`;
const depositBenefits = DEFAULT_BENEFITS.map((item) => ({
label: item.label,
iconUrl: resolveMiniappIcon(app, item.key, fb(item.key)),
}));
this.setData({
imgUrls: {
pageBg: resolveConfiguredIcon(app, ICON_KEYS.FIGHTER_DEPOSIT_BG),
hero: resolveConfiguredIcon(app, ICON_KEYS.FIGHTER_DEPOSIT_HERO),
},
depositBenefits,
});
if (this.data.pageCopy) {
this.applyPageCopy();
}
},
async refreshConfiguredIcons() {
try {
await refreshPindaoConfig(app);
} catch (e) {
console.warn('refreshConfiguredIcons failed', e);
}
this.initImages();
this.loadUserInfo();
},
loadUserInfo() {
const gd = app.globalData || {};
this.setData({
avatarUrl: resolveAvatarFromStorage(app),
nickname: gd.dashouNicheng || gd.nicheng || '打手',
uid: wx.getStorageSync('uid') || gd.uid || '',
});
},
onAvatarError() {
onAvatarImageError(this, 'avatarUrl', app);
},
loadYajin() {
const yajin = parseFloat(app.globalData.yajin || 0) || 0;
this.setData({
yajin,
yajinDisplay: yajin.toFixed(2),
yajinActive: yajin > 0,
});
},
toggleAgree() {
this.setData({ agreed: !this.data.agreed });
},
onTapPay() {
if (!this.data.agreed) {
wx.showToast({ title: '请先阅读并同意缴纳规则', icon: 'none' });
return;
}
this.setData({ showAmountModal: true });
},
hideAmountModal() {
this.setData({ showAmountModal: false });
},
onYajinInput(e) {
let v = (e.detail.value || '').replace(/[^\d]/g, '');
if (v) {
const n = parseInt(v, 10);
if (n > 10000) v = '10000';
if (n < 1) v = '1';
}
this.setData({ yajinAmount: v });
},
setQuickAmount(e) {
this.setData({ yajinAmount: e.currentTarget.dataset.amount });
},
onConfirmAmount() {
const amount = this.data.yajinAmount;
if (!amount || amount < 1 || amount > 10000) {
wx.showToast({ title: '请输入1-10000元', icon: 'none' });
return;
}
this.hideAmountModal();
this.setData({
currentYajinAmount: amount,
payAmountLabel: `(¥ ${amount})`,
showPayMethodModal: true,
});
},
hidePayMethodModal() {
this.setData({ showPayMethodModal: false });
},
onPayMethodSelect(e) {
const method = e.currentTarget.dataset.method;
this.hidePayMethodModal();
if (method === 'wx' || method === 'alipay') {
const payMethod = method === 'alipay' ? 'alipay' : 'wx';
this.setData({ yajinAmount: this.data.currentYajinAmount }, () => this.handleYajinPay(payMethod));
} else {
this.fetchBalanceOptions();
}
},
async fetchBalanceOptions() {
this.showLoading('获取抵扣信息...');
try {
const res = await request({
url: '/shangpin/czhqdy',
method: 'POST',
data: {
leixing: 2,
yajin_jine: parseInt(this.data.currentYajinAmount, 10),
},
});
if (res.data.code === 200) {
this.setData({ balanceOptions: res.data.data || [], showBalanceModal: true });
} else {
wx.showToast({ title: res.data.message || '获取失败', icon: 'none' });
}
} catch (err) {
wx.showToast({ title: '网络错误', icon: 'none' });
} finally {
this.hideLoading();
}
},
hideBalanceModal() {
this.setData({ showBalanceModal: false });
},
onBalanceSelect(e) {
const id = e.currentTarget.dataset.id;
const selected = this.data.balanceOptions.find((o) => o.id === id);
if (!selected) return;
this.setData({
selectedBalanceId: id,
selectedBalanceInfo: selected,
showConfirmModal: true,
showBalanceModal: false,
});
},
hideConfirmModal() {
this.setData({ showConfirmModal: false });
},
async confirmBalancePay() {
const { selectedBalanceId, currentYajinAmount } = this.data;
if (!selectedBalanceId) return;
this.hideConfirmModal();
this.showLoading('抵扣支付中...');
try {
const res = await request({
url: '/shangpin/dsqrgmdh',
method: 'POST',
data: {
leixing: 2,
shenfen_id: selectedBalanceId,
yajin_jine: parseInt(currentYajinAmount, 10),
},
});
if (res.data.code === 200) {
const data = res.data.data || {};
if (data.yajin !== undefined) app.globalData.yajin = data.yajin;
this.loadYajin();
wx.showToast({ title: '缴纳成功', icon: 'success' });
this.showPaySuccessPopup();
} else {
wx.showToast({ title: res.data.message || '购买失败', icon: 'none' });
}
} catch (err) {
wx.showToast({ title: '网络错误', icon: 'none' });
} finally {
this.hideLoading();
}
},
confirmWechatPurchase() {
return new Promise((resolve) => {
wx.showModal({
title: '确认微信支付',
content: '您即将通过微信支付缴纳履约金。购买成功后不可随意退款,请确认无误后再继续。',
confirmText: '确认购买',
cancelText: '再想想',
success: (res) => resolve(!!res.confirm),
fail: () => resolve(false),
});
});
},
async handleYajinPay(payMethod = 'wx') {
const amount = this.data.yajinAmount;
if (!amount || amount < 1 || amount > 10000) {
wx.showToast({ title: '请输入1-10000元', icon: 'none' });
return;
}
// 支付宝无需微信确认弹窗
if (payMethod !== 'alipay' && !(await this.confirmWechatPurchase())) return;
this.showLoading('发起支付中...');
try {
const res = await request({
url: '/shangpin/yajingoumai',
method: 'POST',
data: {
jine: parseInt(amount, 10),
pay_method: payMethod === 'alipay' ? 'alipay' : 'wechat',
},
});
if (res.data.code === 200) {
const payParams = res.data.payParams;
const dingdanid = res.data.dingdanid;
this.setData({ currentDingdanid: dingdanid });
// 支付宝分支:跳转二维码页
if (payMethod === 'alipay' || (payParams && payParams.pay_method === 'alipay')) {
this.hideLoading();
this.triggerAlipayPayment(payParams, dingdanid);
return;
}
await this.wechatPay(payParams);
} else {
wx.showToast({ title: res.data.message || '支付发起失败', icon: 'none' });
}
} catch (err) {
wx.showToast({ title: '支付失败', icon: 'none' });
} finally {
this.hideLoading();
}
},
// ========== 支付宝扫码支付封装 ==========
triggerAlipayPayment(payParams, orderId) {
const qrImage = payParams && payParams.qr_image ? payParams.qr_image : '';
if (!qrImage) {
wx.showToast({ title: '支付宝二维码生成失败', icon: 'none' });
return;
}
const app = getApp();
app.globalData.alipayQrImage = qrImage;
const pollUrl = encodeURIComponent('/shangpin/yajinlunxun');
const self = this;
wx.navigateTo({
url: `/pages/alipay-webview/alipay-webview?orderId=${orderId}&pollUrl=${pollUrl}`,
events: {
// 支付宝支付成功后,更新押金全局变量并刷新页面
onAlipaySuccess: (res) => {
if (res && res.data && res.data.yajin !== undefined) {
app.globalData.yajin = res.data.yajin;
}
self.loadYajin();
wx.showToast({ title: '履约金缴纳成功', icon: 'success', duration: 2000 });
self.showPaySuccessPopup();
}
}
});
},
wechatPay(payParams) {
return new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'MD5',
paySign: payParams.paySign,
success: async () => {
wx.showToast({ title: '支付成功!确认中...', icon: 'none' });
try {
await this.startPolling();
resolve();
} catch (e) {
reject(e);
}
},
fail: async (res) => {
wx.showToast({ title: res.errMsg?.includes('cancel') ? '已取消支付' : '支付失败', icon: 'none' });
try {
await request({
url: '/shangpin/yajinshibai',
method: 'POST',
data: { dingdanid: this.data.currentDingdanid },
});
} catch (e) { /* ignore */ }
reject(new Error(res.errMsg));
},
});
});
},
async startPolling() {
const dingdanid = this.data.currentDingdanid;
if (!dingdanid) return;
this.showLoading('确认支付结果...');
for (let i = 0; i < 10; i++) {
try {
const res = await request({
url: '/shangpin/yajinlunxun',
method: 'POST',
data: { dingdanid },
});
if (res.data.code === 200) {
if (res.data.yajin !== undefined) app.globalData.yajin = res.data.yajin;
this.loadYajin();
this.hideLoading();
wx.showToast({ title: '履约金缴纳成功', icon: 'success' });
this.showPaySuccessPopup();
return;
}
} catch (e) { /* retry */ }
await new Promise((r) => setTimeout(r, 2000));
}
this.hideLoading();
wx.showToast({ title: '确认超时,请稍后查看余额', icon: 'none' });
},
showLoading(text) {
this.setData({ payLoading: true, loadingText: text || '处理中...' });
},
hideLoading() {
this.setData({ payLoading: false });
},
/** 缴纳成功后弹出后台可配置公告(页面标识 yajinchenggong) */
showPaySuccessPopup() {
try {
PopupService.reset();
} catch (e) { /* ignore */ }
setTimeout(() => {
PopupService.checkAndShow(this, 'yajinchenggong');
}, 450);
},
stopPropagation() {},
});
@@ -0,0 +1,10 @@
{
"usingComponents": {
"global-notification": "/components/global-notification/global-notification",
"popup-notice": "/components/popup-notice/popup-notice"
},
"navigationBarTitleText": "平台履约金",
"navigationBarBackgroundColor": "#FFF9F0",
"navigationBarTextStyle": "black",
"backgroundColor": "#FFF9F0"
}
+137
View File
@@ -0,0 +1,137 @@
<!-- 平台履约金 / 保证金充值页 -->
<view class="deposit-page">
<image wx:if="{{imgUrls.pageBg}}" class="page-bg" src="{{imgUrls.pageBg}}" mode="widthFix" />
<view class="page-bg-fallback" wx:else></view>
<view class="page-body">
<view class="user-row">
<image class="user-avatar" src="{{avatarUrl}}" mode="aspectFill" binderror="onAvatarError" />
<view class="user-info">
<text class="user-name">{{nickname || '打手'}}</text>
<text class="user-id">会员ID: {{uid || '--'}}</text>
</view>
</view>
<view class="deposit-card">
<view class="deposit-card-left">
<text class="deposit-card-title">平台接单履约金</text>
<view class="deposit-price-row">
<text class="deposit-currency">¥</text>
<text class="deposit-price">{{yajinDisplay}}</text>
</view>
<text class="deposit-validity">有效期:永久有效</text>
</view>
<view class="deposit-card-right">
<view class="icon-clip icon-clip-deposit" wx:if="{{imgUrls.hero}}">
<image class="icon-clip-img" src="{{imgUrls.hero}}" mode="aspectFill" />
</view>
<view class="deposit-status">{{yajinActive ? '已缴纳' : '未激活'}}</view>
</view>
<view class="deposit-shimmer"></view>
</view>
<view class="section-card">
<text class="section-title">{{rulesTitle || '履约金规则'}}</text>
<view class="rule-list">
<view wx:for="{{depositRules}}" wx:key="index" class="rule-item">
<text class="rule-num">{{index + 1}}.</text>
<text class="rule-text">{{item}}</text>
</view>
</view>
</view>
<view class="section-card">
<text class="section-title">{{benefitsTitle || '平台履约金权益'}}</text>
<view class="benefit-grid">
<view wx:for="{{depositBenefits}}" wx:key="label" class="benefit-item">
<view wx:if="{{item.iconUrl}}" class="icon-clip icon-clip-deposit-benefit">
<image class="icon-clip-img" src="{{item.iconUrl}}" mode="aspectFill" />
</view>
<text>{{item.label}}</text>
</view>
</view>
</view>
</view>
<view class="footer-bar">
<view class="agree-row" bindtap="toggleAgree">
<view class="agree-check {{agreed ? 'agree-check--on' : ''}}">{{agreed ? '✓' : ''}}</view>
<text class="agree-text">已阅读并同意</text>
<text class="agree-link">《平台履约金缴纳规则》</text>
</view>
<view class="pay-btn {{agreed ? '' : 'pay-btn--disabled'}}" bindtap="onTapPay">
<text>缴纳履约金{{payAmountLabel}}</text>
<view class="pay-btn-shimmer"></view>
</view>
</view>
</view>
<!-- 金额输入弹窗 -->
<view class="modal-mask" wx:if="{{showAmountModal}}" bindtap="hideAmountModal">
<view class="modal-panel" catchtap="stopPropagation">
<text class="modal-title">缴纳履约金</text>
<text class="modal-sub">支持 1-10000 元整数</text>
<view class="amount-input-wrap">
<text class="amount-prefix">¥</text>
<input class="amount-input" type="digit" value="{{yajinAmount}}" bindinput="onYajinInput" focus="{{showAmountModal}}" />
</view>
<view class="quick-row">
<view wx:for="{{quickAmounts}}" wx:key="*this" class="quick-chip {{yajinAmount == item ? 'active' : ''}}" bindtap="setQuickAmount" data-amount="{{item}}">{{item}}元</view>
</view>
<view class="modal-actions">
<view class="modal-btn cancel" bindtap="hideAmountModal">取消</view>
<view class="modal-btn confirm" bindtap="onConfirmAmount">下一步</view>
</view>
</view>
</view>
<!-- 支付方式 -->
<view class="modal-mask" wx:if="{{showPayMethodModal}}" bindtap="hidePayMethodModal">
<view class="modal-panel" catchtap="stopPropagation">
<text class="modal-title">选择支付方式</text>
<view class="pay-method-item" bindtap="onPayMethodSelect" data-method="wx">
<text class="pay-method-name">微信支付</text>
<text class="pay-method-desc">安全快捷</text>
</view>
<view class="pay-method-item" bindtap="onPayMethodSelect" data-method="alipay">
<text class="pay-method-name">支付宝</text>
<text class="pay-method-desc">扫码支付</text>
</view>
<view class="pay-method-item" bindtap="onPayMethodSelect" data-method="balance">
<text class="pay-method-name">余额抵扣</text>
<text class="pay-method-desc">使用佣金或分红</text>
</view>
</view>
</view>
<view class="modal-mask" wx:if="{{showBalanceModal}}" bindtap="hideBalanceModal">
<view class="modal-panel" catchtap="stopPropagation">
<text class="modal-title">选择抵扣身份</text>
<view wx:for="{{balanceOptions}}" wx:key="id" class="balance-item" bindtap="onBalanceSelect" data-id="{{item.id}}">
<text>{{item.jieshao}}</text>
<text class="balance-need">需¥{{item.xuyao}}</text>
</view>
</view>
</view>
<view class="modal-mask" wx:if="{{showConfirmModal}}" bindtap="hideConfirmModal">
<view class="modal-panel" catchtap="stopPropagation">
<text class="modal-title">确认抵扣</text>
<view class="confirm-row"><text>身份</text><text>{{selectedBalanceInfo.jieshao}}</text></view>
<view class="confirm-row"><text>金额</text><text>¥{{selectedBalanceInfo.xuyao}}</text></view>
<view class="modal-actions">
<view class="modal-btn cancel" bindtap="hideConfirmModal">取消</view>
<view class="modal-btn confirm" bindtap="confirmBalancePay">确认支付</view>
</view>
</view>
</view>
<view class="loading-mask" wx:if="{{payLoading}}">
<view class="loading-box">
<view class="loading-spinner"></view>
<text>{{loadingText}}</text>
</view>
</view>
<global-notification id="global-notification" />
<popup-notice id="popupNotice" />
+279
View File
@@ -0,0 +1,279 @@
@import '../../styles/xym-recharge-shared.wxss';
.deposit-page {
min-height: 100vh;
background: #fff9f0;
padding-bottom: calc(220rpx + env(safe-area-inset-bottom));
position: relative;
}
.page-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
}
.page-bg-fallback {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 360rpx;
background: linear-gradient(180deg, #ffe082 0%, #fff9f0 100%);
z-index: 0;
}
.page-body {
position: relative;
z-index: 1;
padding: 24rpx 30rpx 0;
}
.user-row {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.user-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
margin-right: 20rpx;
background: #eee;
}
.user-name {
display: block;
font-size: 32rpx;
font-weight: 700;
color: #222;
}
.user-id {
display: block;
margin-top: 6rpx;
font-size: 24rpx;
color: #999;
}
.deposit-card {
position: relative;
overflow: hidden;
border-radius: 24rpx;
padding: 32rpx 28rpx;
background: linear-gradient(135deg, #fff8e1 0%, #ffe0b2 100%);
display: flex;
justify-content: space-between;
align-items: stretch;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 183, 77, 0.25);
}
.deposit-card-left {
flex: 1;
min-width: 0;
}
.deposit-card-title {
font-size: 30rpx;
font-weight: 700;
color: #492f00;
}
.deposit-price-row {
display: flex;
align-items: baseline;
margin: 16rpx 0 8rpx;
}
.deposit-currency {
font-size: 36rpx;
font-weight: 700;
color: #222;
margin-right: 4rpx;
}
.deposit-price {
font-size: 64rpx;
font-weight: 800;
color: #222;
line-height: 1;
}
.deposit-validity {
font-size: 24rpx;
color: #947641;
}
.deposit-card-right {
position: relative;
width: 200rpx;
display: flex;
align-items: center;
justify-content: center;
}
.icon-clip {
overflow: hidden;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.5);
}
.icon-clip-img {
width: 100%;
height: 100%;
display: block;
}
.icon-clip-deposit {
width: 180rpx;
height: 180rpx;
border-radius: 40rpx;
}
.icon-clip-deposit-benefit {
width: 36rpx;
height: 36rpx;
border-radius: 14rpx;
}
.deposit-status {
position: absolute;
right: 0;
bottom: 0;
padding: 6rpx 16rpx;
border-radius: 8rpx;
background: rgba(0, 0, 0, 0.08);
font-size: 22rpx;
color: #666;
}
.deposit-shimmer {
position: absolute;
inset: 0;
background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.45) 50%, transparent 60%);
animation: card-shimmer 3s infinite;
pointer-events: none;
}
.section-card {
background: #fff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.section-title {
display: block;
text-align: center;
font-size: 30rpx;
font-weight: 700;
color: #333;
margin-bottom: 20rpx;
}
.rule-item {
display: flex;
gap: 8rpx;
margin-bottom: 12rpx;
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
.benefit-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx 16rpx;
}
.benefit-item {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 26rpx;
color: #555;
}
.footer-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));
background: rgba(255, 249, 240, 0.96);
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 10;
}
.agree-row {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 16rpx;
font-size: 24rpx;
color: #666;
}
.agree-check {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
margin-right: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
color: #fff;
}
.agree-check--on {
background: #f5a623;
border-color: #f5a623;
}
.agree-link {
color: #e6a23c;
}
.pay-btn {
position: relative;
overflow: hidden;
height: 96rpx;
border-radius: 48rpx;
background: linear-gradient(90deg, #fae04d, #ffc0a3);
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 700;
color: #492f00;
box-shadow: 0 8rpx 20rpx rgba(245, 166, 35, 0.35);
}
.pay-btn--disabled {
opacity: 0.55;
}
.pay-btn-shimmer {
position: absolute;
inset: 0;
background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.5) 50%, transparent 60%);
animation: btn-shimmer 2.5s infinite;
pointer-events: none;
}
@keyframes card-shimmer {
0% { transform: translateX(-120%); }
100% { transform: translateX(120%); }
}
@keyframes btn-shimmer {
0% { transform: translateX(-120%); }
100% { transform: translateX(120%); }
}
+416 -308
View File
@@ -1,72 +1,62 @@
// pages/dashou-chongzhi/index.js
// pages/fighter-recharge/fighter-recharge.js
import request from '../../utils/request';
import PopupService from '../../services/popupService.js';
import { ensurePhoneAuth } from '../../utils/phone-auth.js';
import { ICON_KEYS, resolveConfiguredIcon, resolveMiniappIcon, ICON_FOLDER, refreshPindaoConfig } from '../../utils/miniapp-icons.js';
import { resolveAvatarFromStorage, onAvatarImageError } from '../../utils/avatar.js';
import { refreshDashouMembership } from '../../utils/dashou-profile.js';
const app = getApp();
const PLATFORM_ADVANTAGE_META = [
{ key: ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_1, title: '订单丰富', desc: '平台单、商家单多种类型' },
{ key: ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_2, title: '俱乐部专属', desc: '按俱乐部独立配置权益' },
{ key: ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_3, title: '客服支持', desc: '遇到问题随时联系客服' },
{ key: ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_4, title: '安全可靠', desc: '资金与订单平台保障' },
];
Page({
data: {
// ========== 全局数据 ==========
clubmber: [], // 会员列表(注意:全局变量中是 clumber,这里保持原样)
yajin: 0, // 押金
jifen: 0, // 积分(注意:全局变量中是 jinfen)
jifenProgress: 0, // 积分进度条角度
// ========== 会员商品列表 ==========
huiyuanList: [], // 从后端获取的会员列表
currentHuiyuanIndex: 0, // 当前选中的会员索引
currentHuiyuan: {}, // 当前选中的会员详情
// ========== 押金充值相关 ==========
showYajinModal: false, // 显示押金弹窗
yajinAmount: '100', // 押金金额
// ========== 支付相关 ==========
payLoading: false, // 支付加载状态
imgUrls: { pageBg: '' },
avatarUrl: '',
nickname: '',
uid: '',
memberBadge: '会员',
agreed: false,
memberBenefits: [],
platformAdvantages: [],
clubmber: [],
jifen: 0,
huiyuanList: [],
currentHuiyuanIndex: 0,
currentHuiyuan: {},
payLoading: false,
loadingText: '支付中...',
// ========== 订单ID记录 ==========
currentDingdanid: '', // 当前操作的订单ID
// ========== 计算高度 ==========
huiyuanListHeight: 120,
// ========== 会员详情规则 ==========
currentDingdanid: '',
detailRules: [],
// ========== 新增:跳转参数处理 ==========
// 用于接收从其他页面跳转过来的参数,决定是否自动滚动
rulesTitle: '',
benefitsTitle: '',
advantagesTitle: '平台优势',
pageCopy: null,
jumpParams: {},
// ========== 新增:支付方式选择 ==========
showPayMethodModal: false, // 支付方式选择弹窗
currentBuyType: null, // 当前购买类型:1会员 2押金 3积分
currentHuiyuanId: null, // 当前选中的会员ID(用于会员购买)
currentBuyIsTrial: false, // 正式/体验:仅体验禁余额抵扣
currentYajinAmount: null, // 当前押金金额(用于押金购买)
// ========== 新增:余额抵扣身份选择 ==========
showBalanceModal: false, // 余额抵扣身份选择弹窗
balanceOptions: [], // 后端返回的身份列表
selectedBalanceId: null, // 选中的身份ID
// ========== 新增:余额抵扣确认弹窗 ==========
showConfirmModal: false, // 余额抵扣确认弹窗
selectedBalanceInfo: null, // 选中的身份详情(用于确认弹窗)
// ========== 新增:会员详情弹窗 ==========
showPayMethodModal: false,
currentBuyType: null,
currentHuiyuanId: null,
currentBuyIsTrial: false,
showBalanceModal: false,
balanceOptions: [],
selectedBalanceId: null,
showConfirmModal: false,
selectedBalanceInfo: null,
showMemberModal: false,
currentMemberDetail: null,
},
// ========== 生命周期函数 ==========
onLoad(options) {
//console.log('页面加载,跳转参数:', options);
// ✅ 新增:保存跳转参数
this.setData({
jumpParams: options || {}
});
this.setData({ jumpParams: options || {} });
this.initImages();
this.loadUserInfo();
this.initPage();
},
@@ -86,6 +76,7 @@ onHide() {
this.registerNotificationComponent();
this.loadFromGlobalData();
this.checkAndRefreshData();
this.refreshConfiguredIcons();
// 调用统一弹窗组件检查并展示公告
PopupService.checkAndShow(this, 'dashouchongzhi');
@@ -112,16 +103,210 @@ onHide() {
// ========== 初始化页面 ==========
async initPage() {
// 从全局数据加载
this.loadFromGlobalData();
// 获取会员商品列表
await this.fetchPageCopy();
await this.fetchHuiyuanList();
// 计算会员列表高度
this.calculateHuiyuanHeight();
},
async fetchPageCopy() {
try {
const res = await request({
url: '/jituan/recharge-page-copy',
method: 'POST',
data: { page_key: 'huiyuan' },
});
if (res.data && res.data.code === 0 && res.data.data) {
this.setData({ pageCopy: res.data.data.regions || {} });
} else {
this.setData({ pageCopy: {} });
}
} catch (e) {
console.warn('fetchPageCopy failed', e);
this.setData({ pageCopy: {} });
}
},
initImages() {
this.setData({
imgUrls: {
pageBg: resolveConfiguredIcon(app, ICON_KEYS.FIGHTER_RECHARGE_VIP_BG),
},
platformAdvantages: this.buildPlatformAdvantages(),
});
},
async refreshConfiguredIcons() {
try {
await refreshPindaoConfig(app);
} catch (e) {
console.warn('refreshConfiguredIcons failed', e);
}
this.initImages();
const list = this.data.huiyuanList || [];
const idx = this.data.currentHuiyuanIndex || 0;
if (list.length) {
this.applyCurrentHuiyuan(list[idx] || {}, idx);
}
this.loadUserInfo();
},
_icon(key, fallbackRel) {
return resolveMiniappIcon(app, key, fallbackRel);
},
buildPlatformAdvantages() {
const fb = (name) => `${ICON_FOLDER}/${name}.png`;
const region = (this.data.pageCopy || {}).advantages;
if (region && Array.isArray(region.items) && region.items.length > 0) {
return region.items.map((item, i) => {
const fallbackKey = [
ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_1,
ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_2,
ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_3,
ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_4,
][i];
const iconUrl = item.icon_url
? this.resolveOssUrl(item.icon_url)
: (fallbackKey ? this._icon(fallbackKey, fb(fallbackKey)) : '');
return {
title: item.title || '',
desc: item.desc || '',
iconUrl,
};
});
}
const keys = [
ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_1,
ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_2,
ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_3,
ICON_KEYS.FIGHTER_RECHARGE_ADVANTAGE_4,
];
return PLATFORM_ADVANTAGE_META.map((item, i) => ({
title: item.title,
desc: item.desc,
iconUrl: this._icon(keys[i], fb(keys[i])),
}));
},
loadUserInfo() {
const gd = app.globalData || {};
this.setData({
avatarUrl: resolveAvatarFromStorage(app),
nickname: gd.dashouNicheng || gd.nicheng || '打手',
uid: wx.getStorageSync('uid') || gd.uid || '',
});
},
onAvatarError() {
onAvatarImageError(this, 'avatarUrl', app);
},
resolveOssUrl(path) {
if (!path) return '';
if (path.startsWith('http')) return path;
return (app.globalData.ossImageUrl || '') + path;
},
buildMemberBenefits(huiyuan = {}) {
const region = (this.data.pageCopy || {}).benefits;
const fb = (name) => `${ICON_FOLDER}/${name}.png`;
if (region && Array.isArray(region.items) && region.items.length > 0) {
const fallbackKeys = [
ICON_KEYS.FIGHTER_RECHARGE_BENEFIT_1,
ICON_KEYS.FIGHTER_RECHARGE_BENEFIT_2,
ICON_KEYS.FIGHTER_RECHARGE_BENEFIT_3,
];
return region.items.map((item, i) => {
const key = fallbackKeys[i];
const iconUrl = item.icon_url
? this.resolveOssUrl(item.icon_url)
: (key ? this._icon(key, fb(key)) : '');
return {
title: item.title || '',
desc: item.desc || '',
iconUrl,
accentColor: item.accent_color || '',
accentStyle: item.accent_color ? `color:${item.accent_color}` : '',
};
});
}
const name = huiyuan.mingzi || '会员';
const formalPrice = huiyuan.jiage || '0';
const trialPrice = huiyuan.trial_price || '0';
const meta = [
{
key: ICON_KEYS.FIGHTER_RECHARGE_BENEFIT_1,
title: '充值立得',
desc: `开通${name}正式版 ¥${formalPrice},享优先接单`,
},
{
key: ICON_KEYS.FIGHTER_RECHARGE_BENEFIT_2,
title: '体验尝鲜',
desc: huiyuan.can_buy_trial
? `体验版 ¥${trialPrice}/${huiyuan.trial_days || 0}天,终身1次`
: '正式会员支持余额抵扣',
},
{
key: ICON_KEYS.FIGHTER_RECHARGE_BENEFIT_3,
title: '专属特权',
desc: '解锁平台单、优质接单等待遇',
},
];
return meta.map((item) => ({
title: item.title,
desc: item.desc,
iconUrl: this._icon(item.key, fb(item.key)),
}));
},
applyCurrentHuiyuan(huiyuan, index) {
const copy = this.data.pageCopy || {};
const name = huiyuan.mingzi || '会员';
const rulesTitle = (copy.rules && copy.rules.title)
|| `${name}规则`;
const benefitsTitle = (copy.benefits && copy.benefits.title)
|| `${name}专属权益`;
const advantagesTitle = (copy.advantages && copy.advantages.title)
|| '平台优势';
this.setData({
currentHuiyuanIndex: index,
currentHuiyuan: huiyuan,
detailRules: this.buildDetailRules(huiyuan),
memberBenefits: this.buildMemberBenefits(huiyuan),
platformAdvantages: this.buildPlatformAdvantages(),
rulesTitle,
benefitsTitle,
advantagesTitle,
memberBadge: huiyuan.mingzi || '会员',
});
},
toggleAgree() {
this.setData({ agreed: !this.data.agreed });
},
goToDeposit() {
wx.navigateTo({ url: '/pages/fighter-recharge/fighter-deposit' });
},
onTapTrial(e) {
if (!this.data.agreed) {
wx.showToast({ title: '请先阅读并同意会员协议', icon: 'none' });
return;
}
this.onBuyHuiyuanClick(e);
},
onTapFormal(e) {
if (!this.data.agreed) {
wx.showToast({ title: '请先阅读并同意会员协议', icon: 'none' });
return;
}
this.onBuyHuiyuanClick(e);
},
stopPropagation() {},
// ========== 从全局变量加载数据 ==========
loadFromGlobalData() {
const app = getApp();
@@ -130,37 +315,12 @@ onHide() {
// ✅ 重要:保持字段对应关系
// 全局变量中是 clumber,页面中是 clubmber
// 全局变量中是 jinfen,页面中是 jifen
this.setData({
clubmber: globalData.clumber || [],
yajin: globalData.yajin || 0,
jifen: globalData.jinfen || 0 // ✅ 注意:这里从 jinfen 读取
});
// 计算积分进度条
this.calculateJifenProgress();
},
const rawClubmber = globalData.clumber || [];
const clubmber = rawClubmber.filter((c) => c && c.huiyuanid);
// ========== 计算积分进度条角度 ==========
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
clubmber: clubmber,
jifen: globalData.jinfen || 0,
});
},
@@ -174,17 +334,15 @@ onHide() {
});
if (res.data.code === 200) {
const huiyuanList = res.data.data || [];
// 处理数据,标记已购买的会员
const processedList = this.processHuiyuanList(huiyuanList);
const currentHuiyuan = processedList[0] || {};
this.setData({
huiyuanList: processedList,
currentHuiyuan,
detailRules: this.buildDetailRules(currentHuiyuan)
});
const huiyuanList = this.processHuiyuanList(res.data.data || []);
let index = 0;
const targetId = this.data.jumpParams.huiyuanId;
if (targetId) {
const found = huiyuanList.findIndex((h) => String(h.id) === String(targetId));
if (found >= 0) index = found;
}
this.applyCurrentHuiyuan(huiyuanList[index] || {}, index);
this.setData({ huiyuanList });
} else {
wx.showToast({
title: res.data.message || '获取会员列表失败',
@@ -211,7 +369,9 @@ onHide() {
return {
...item,
isBought: !!boughtItem,
buyInfo: boughtItem || null
buyInfo: boughtItem || null,
cardImageUrl: this.resolveOssUrl(item.card_image),
cardBgUrl: this.resolveOssUrl(item.card_bg),
};
});
},
@@ -220,43 +380,34 @@ onHide() {
onSwiperChange(e) {
const current = e.detail.current;
const huiyuanList = this.data.huiyuanList || [];
const currentHuiyuan = huiyuanList[current] || {};
this.setData({
currentHuiyuanIndex: current,
currentHuiyuan,
detailRules: this.buildDetailRules(currentHuiyuan)
});
this.applyCurrentHuiyuan(huiyuanList[current] || {}, current);
},
// ========== 手动选择会员 ==========
selectHuiyuan(e) {
const index = e.currentTarget.dataset.index;
const huiyuanList = this.data.huiyuanList || [];
const currentHuiyuan = huiyuanList[index] || {};
this.setData({
currentHuiyuanIndex: index,
currentHuiyuan,
detailRules: this.buildDetailRules(currentHuiyuan)
});
this.applyCurrentHuiyuan(huiyuanList[index] || {}, index);
},
buildDetailRules(huiyuan = {}) {
const region = (this.data.pageCopy || {}).rules;
if (region && Array.isArray(region.items) && region.items.length > 0) {
return region.items
.map((it) => (typeof it === 'string' ? it : (it && it.text) || ''))
.filter(Boolean)
.slice(0, 4);
}
const formalDays = huiyuan.formal_days || 30;
const rules = [
`正式会员有效期为${formalDays}天,从购买当天开始计算`,
'会员期间享受优先接单特权',
'支持随时续费,未过期续费天数叠加,已过期从今天重新计算'
`有效期${formalDays}天`,
'可接多种订单类型',
'支持续费,天数叠加',
'正式会员支持余额抵扣',
];
if (huiyuan.can_buy_trial) {
rules.push(`体验会员终身仅可购买1次(${huiyuan.trial_days || 0}天),体验期仅限制佣金提现`);
rules[3] = `体验版¥${huiyuan.trial_price}/${huiyuan.trial_days}天`;
}
if (huiyuan.trial_enabled && !huiyuan.can_buy_trial) {
rules.push('您已使用过该会员的体验资格');
}
rules.push('体验会员仅支持微信支付;正式会员可使用佣金、分红或押金抵扣');
return rules;
return rules.slice(0, 4);
},
// ========== 格式化到期时间 ==========
@@ -310,109 +461,10 @@ onHide() {
// ========== 处理自动滚动 ==========
handleAutoScroll() {
const { jumpParams } = this.data;
if (!jumpParams || !jumpParams.needScroll) return;
if (!jumpParams || jumpParams.scrollTo !== 'jifen') 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();
}
wx.pageScrollTo({ selector: '#jifen-section', duration: 300 });
}, 600);
},
// ========== 积分充值相关 ==========
@@ -443,7 +495,7 @@ onHide() {
},
// ========== 会员购买相关 ==========
async handleHuiyuanBuy(e) {
async handleHuiyuanBuy(e, payMethod = 'wx') {
const huiyuanId = e.currentTarget.dataset.id;
const isTrial = e.currentTarget.dataset.trial === true || e.currentTarget.dataset.trial === 'true';
@@ -455,18 +507,10 @@ onHide() {
return;
}
if (isTrial) {
const { currentHuiyuan, huiyuanList } = this.data;
const target = (currentHuiyuan && currentHuiyuan.id === huiyuanId)
? currentHuiyuan
: (huiyuanList || []).find(item => item.id === huiyuanId);
if (target && !target.can_buy_trial) {
wx.showToast({
title: '您已使用过该会员的体验资格',
icon: 'none'
});
return;
}
// 支付宝无需微信确认弹窗
if (payMethod !== 'alipay') {
const confirmed = await this.confirmWechatPurchase('huiyuan');
if (!confirmed) return;
}
this.showLoading('发起会员支付中...');
@@ -477,18 +521,26 @@ onHide() {
method: 'POST',
data: {
huiyuanid: huiyuanId,
is_trial: isTrial
is_trial: isTrial,
pay_method: payMethod === 'alipay' ? 'alipay' : 'wechat'
}
});
if (res.data.code === 200) {
const payParams = res.data.payParams;
const dingdanid = res.data.dingdanid;
this.setData({
currentDingdanid: dingdanid
});
// 支付宝分支:跳转二维码页
if (payMethod === 'alipay' || (payParams && payParams.pay_method === 'alipay')) {
this.hideLoading();
this.triggerAlipayPayment(payParams, dingdanid, 'huiyuan');
return;
}
await this.wechatPay(payParams, 'huiyuan', huiyuanId);
} else {
wx.showToast({
@@ -508,9 +560,9 @@ onHide() {
},
// ========== 积分购买(微信支付)—— 已替换为正确的后端接口 ==========
async handleJifenBuy(e) {
async handleJifenBuy(e, payMethod = 'wx') {
const disabled = e.currentTarget.dataset.disabled;
// 检查积分是否已满
if (disabled === 'true') {
wx.showToast({ title: '积分已满,无需补充', icon: 'none' });
@@ -524,20 +576,36 @@ onHide() {
return;
}
// 支付宝无需微信确认弹窗
if (payMethod !== 'alipay') {
const confirmed = await this.confirmWechatPurchase('jifen');
if (!confirmed) return;
}
this.showLoading('发起积分支付中...');
try {
const res = await request({
url: '/shangpin/jifenbc', // 正确的积分下单接口
method: 'POST',
data: {}
data: {
pay_method: payMethod === 'alipay' ? 'alipay' : 'wechat'
}
});
if (res.data.code === 200) {
const payParams = res.data.payParams;
const dingdanid = res.data.dingdanid;
this.setData({ currentDingdanid: dingdanid });
// 支付宝分支:跳转二维码页
if (payMethod === 'alipay' || (payParams && payParams.pay_method === 'alipay')) {
this.hideLoading();
this.triggerAlipayPayment(payParams, dingdanid, 'jifen');
return;
}
await this.wechatPay(payParams, 'jifen');
} else {
wx.showToast({ title: res.data.message || '积分支付发起失败', icon: 'none' });
@@ -549,6 +617,37 @@ onHide() {
}
},
// ========== 支付宝扫码支付封装 ==========
// 跳转到 alipay-webview 展示二维码,并按 payType 指定轮询接口
triggerAlipayPayment(payParams, orderId, payType) {
const qrImage = payParams && payParams.qr_image ? payParams.qr_image : '';
if (!qrImage) {
wx.showToast({ title: '支付宝二维码生成失败', icon: 'none' });
return;
}
const app = getApp();
app.globalData.alipayQrImage = qrImage;
let pollUrl = '/dingdan/fucha';
if (payType === 'huiyuan') pollUrl = '/shangpin/huiyuanlx';
else if (payType === 'jifen') pollUrl = '/shangpin/jifenlunxun';
const encodedPollUrl = encodeURIComponent(pollUrl);
const self = this;
wx.navigateTo({
url: `/pages/alipay-webview/alipay-webview?orderId=${orderId}&pollUrl=${encodedPollUrl}`,
events: {
// 支付宝支付成功后,复用微信支付的更新逻辑刷新全局变量与页面
onAlipaySuccess: (res) => {
if (self.updateAfterPayment) {
self.updateAfterPayment(payType, null, res && res.data).then(() => {
wx.showToast({ title: self.getSuccessMessage(payType), icon: 'success', duration: 2000 });
self.showPaySuccessPopup(payType);
}).catch(() => {});
}
}
}
});
},
// ========== 微信支付封装 ==========
async wechatPay(payParams, payType, extraData = null) {
return new Promise((resolve, reject) => {
@@ -641,6 +740,7 @@ onHide() {
icon: 'success',
duration: 2000
});
this.showPaySuccessPopup(payType);
return;
}
@@ -749,10 +849,11 @@ onHide() {
break;
case 'huiyuan':
if (responseData && responseData.huiyuan) {
if (responseData && responseData.jifen !== undefined) {
app.globalData.jinfen = responseData.jifen;
await this.updateClubmberData(responseData.huiyuan);
}
// 从服务端拉完整 clumber(含总会员子类型展开),避免本地只写总会员 ID
await refreshDashouMembership(app);
await this.refreshHuiyuanData(extraData);
break;
}
@@ -772,34 +873,29 @@ onHide() {
// ========== 刷新会员数据 ==========
async refreshHuiyuanData(huiyuanId) {
await refreshDashouMembership(app);
this.loadFromGlobalData();
await this.fetchHuiyuanList();
},
// ========== 更新会员数据到全局变量 ==========
// ========== 更新会员数据到全局变量(兼容旧回调;优先服务端 clumber) ==========
async updateClubmberData(huiyuanData) {
const app = getApp();
const clumber = await refreshDashouMembership(app);
if (clumber && clumber.length) {
this.setData({ clubmber: [...clumber] });
return;
}
const globalClubmber = app.globalData.clumber || [];
const { huiyuanid, huiyuanming, daoqi } = huiyuanData;
const index = globalClubmber.findIndex(item => item.huiyuanid === huiyuanid);
const { huiyuanid, huiyuanming, daoqi } = huiyuanData || {};
if (!huiyuanid) return;
const index = globalClubmber.findIndex((item) => item.huiyuanid === huiyuanid);
if (index >= 0) {
globalClubmber[index].daoqi = daoqi;
} else {
globalClubmber.push({
huiyuanid: huiyuanid,
huiyuanming: huiyuanming,
daoqi: daoqi
});
globalClubmber.push({ huiyuanid, huiyuanming, daoqi });
}
app.globalData.clumber = globalClubmber;
this.setData({
clubmber: [...globalClubmber]
});
this.setData({ clubmber: [...globalClubmber] });
},
// ========== 检查并刷新数据 ==========
@@ -817,6 +913,23 @@ onHide() {
}
},
/** 支付成功后弹出后台可配置的图文公告(阅读时长=0 即可直接关闭) */
showPaySuccessPopup(payType) {
const keyMap = {
huiyuan: 'huiyuanchenggong',
yajin: 'yajinchenggong',
jifen: 'jifenchenggong',
};
const pageKey = keyMap[payType];
if (!pageKey) return;
try {
PopupService.reset();
} catch (e) { /* ignore */ }
setTimeout(() => {
PopupService.checkAndShow(this, pageKey);
}, 450);
},
showLoading(text = '加载中...') {
this.setData({
payLoading: true,
@@ -834,12 +947,33 @@ onHide() {
return new Promise(resolve => setTimeout(resolve, ms));
},
/** 微信支付下单前确认(仅微信购买会员/押金/积分) */
confirmWechatPurchase(kind) {
const labelMap = {
huiyuan: '会员',
yajin: '押金',
jifen: '积分',
};
const label = labelMap[kind] || '服务';
return new Promise((resolve) => {
wx.showModal({
title: '确认微信支付',
content: `您即将通过微信支付购买${label}。请点击「确认购买」后才会发起支付;购买成功后不可随意退款,请确认无误后再继续。`,
confirmText: '确认购买',
cancelText: '再想想',
success: (res) => resolve(!!res.confirm),
fail: () => resolve(false),
});
});
},
// ========== 新增功能(余额抵扣等) ==========
onBuyHuiyuanClick(e) {
const huiyuanId = e.currentTarget.dataset.id;
const isTrial = e.currentTarget.dataset.trial === true || e.currentTarget.dataset.trial === 'true';
if (isTrial) {
this.setData({ currentBuyIsTrial: true });
this.handleHuiyuanBuy(e);
return;
}
@@ -848,25 +982,7 @@ onHide() {
currentBuyType: 1,
currentHuiyuanId: huiyuanId,
currentBuyIsTrial: false,
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
showPayMethodModal: true,
});
},
@@ -877,30 +993,22 @@ onHide() {
// 🔥 修改点2:选择支付方式时再次校验积分状态(防止异步过程中积分变化)
onPayMethodSelect(e) {
const method = e.currentTarget.dataset.method;
const { currentBuyType, currentHuiyuanId, currentYajinAmount, jifen } = this.data;
const { currentBuyType, currentHuiyuanId, jifen } = this.data;
this.hidePayMethodModal();
// 如果是积分购买,且积分已满,直接拒绝
if (currentBuyType === 3 && jifen >= 10) {
wx.showToast({
title: '积分已满,无需补充',
icon: 'none'
});
wx.showToast({ title: '积分已满,无需补充', icon: 'none' });
return;
}
if (method === 'wx') {
if (method === 'wx' || method === 'alipay') {
const payMethod = method === 'alipay' ? 'alipay' : 'wx';
if (currentBuyType === 1) {
this.handleHuiyuanBuy({
currentTarget: { dataset: { id: currentHuiyuanId, trial: 'false' } }
});
} else if (currentBuyType === 2) {
this.setData({ yajinAmount: currentYajinAmount }, () => {
this.handleYajinPay();
});
currentTarget: { dataset: { id: currentHuiyuanId, trial: this.data.currentBuyIsTrial ? 'true' : 'false' } },
}, payMethod);
} else if (currentBuyType === 3) {
// ✅ 调用已修正的积分支付函数
this.handleJifenBuy({ currentTarget: { dataset: { disabled: 'false' } } });
this.handleJifenBuy({ currentTarget: { dataset: { disabled: 'false' } } }, payMethod);
}
} else if (method === 'balance') {
this.fetchBalanceOptions();
@@ -909,7 +1017,7 @@ onHide() {
// 🔥 修改点3:请求余额抵扣选项前校验积分状态
async fetchBalanceOptions() {
const { currentBuyType, currentHuiyuanId, currentYajinAmount, currentBuyIsTrial, jifen } = this.data;
const { currentBuyType, currentHuiyuanId, currentBuyIsTrial, jifen } = this.data;
if (currentBuyType === 1 && currentBuyIsTrial) {
wx.showToast({ title: '体验会员仅支持微信支付', icon: 'none' });
@@ -934,9 +1042,8 @@ onHide() {
data: {
leixing: currentBuyType,
huiyuan_id: currentBuyType === 1 ? currentHuiyuanId : undefined,
is_trial: currentBuyType === 1 ? false : undefined,
yajin_jine: currentBuyType === 2 ? parseInt(currentYajinAmount) : undefined,
}
is_trial: currentBuyType === 1 ? currentBuyIsTrial : undefined,
},
});
if (res.data.code === 200) {
const options = res.data.data || [];
@@ -977,7 +1084,7 @@ onHide() {
},
async confirmBalancePay() {
const { selectedBalanceId, currentBuyType, currentHuiyuanId, currentYajinAmount, currentBuyIsTrial } = this.data;
const { selectedBalanceId, currentBuyType, currentHuiyuanId, currentBuyIsTrial } = this.data;
if (!selectedBalanceId) return;
this.hideConfirmModal();
@@ -992,18 +1099,19 @@ onHide() {
shenfen_id: selectedBalanceId,
huiyuan_id: currentBuyType === 1 ? currentHuiyuanId : undefined,
is_trial: currentBuyType === 1 ? currentBuyIsTrial : undefined,
yajin_jine: currentBuyType === 2 ? parseInt(currentYajinAmount) : undefined,
}
},
});
if (res.data.code === 200) {
const responseData = res.data.data || {};
const payType = currentBuyType === 1 ? 'huiyuan' : 'jifen';
await this.updateAfterPayment(
currentBuyType === 1 ? 'huiyuan' : (currentBuyType === 2 ? 'yajin' : 'jifen'),
payType,
currentBuyType === 1 ? currentHuiyuanId : null,
responseData
);
wx.showToast({ title: '购买成功', icon: 'success' });
this.showPaySuccessPopup(payType);
} else {
wx.showToast({ title: res.data.message || '购买失败', icon: 'none' });
}
+9 -11
View File
@@ -1,12 +1,10 @@
{
"usingComponents": {
"global-notification": "/components/global-notification/global-notification"
},
"navigationBarTitleText": "充值中心",
"navigationBarBackgroundColor": "#0a0e2a",
"navigationBarTextStyle": "white",
"backgroundColor": "#0a0e2a",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50
}
"usingComponents": {
"global-notification": "/components/global-notification/global-notification",
"popup-notice": "/components/popup-notice/popup-notice"
},
"navigationBarTitleText": "VIP",
"navigationBarBackgroundColor": "#FFF9F0",
"navigationBarTextStyle": "black",
"backgroundColor": "#FFF9F0"
}
+184 -498
View File
@@ -1,528 +1,214 @@
<!--pages/dashou-chongzhi/index.wxml-->
<view class="page-container">
<!-- ❌ 删除自定义导航栏区块 -->
<!-- VIP 会员 + 积分充值页 -->
<view class="vip-page">
<image wx:if="{{imgUrls.pageBg}}" class="page-bg" src="{{imgUrls.pageBg}}" mode="widthFix" />
<view class="page-bg-fallback" wx:else></view>
<!-- ======================= -->
<!-- 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 class="page-body">
<view class="user-row">
<image class="user-avatar" src="{{avatarUrl}}" mode="aspectFill" binderror="onAvatarError" />
<view class="user-info">
<text class="user-name">{{nickname || '打手'}}</text>
<text class="user-id">会员ID: {{uid || '--'}}</text>
</view>
<text class="section-subtitle">选择你的会员</text>
<view class="member-badge">{{memberBadge}}</view>
</view>
<!-- 会员卡片滑动区域 -->
<swiper
class="vip-swiper"
current="{{currentHuiyuanIndex}}"
circular="{{true}}"
previous-margin="120rpx"
next-margin="120rpx"
duration="500"
easing-function="easeOutCubic"
<swiper
class="vip-swiper"
current="{{currentHuiyuanIndex}}"
circular="{{huiyuanList.length > 1}}"
previous-margin="40rpx"
next-margin="40rpx"
duration="400"
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 class="vip-card {{index === currentHuiyuanIndex ? 'vip-card--active' : ''}}" data-index="{{index}}" bindtap="selectHuiyuan">
<image wx:if="{{item.cardBgUrl}}" class="vip-card-bg-img" src="{{item.cardBgUrl}}" mode="aspectFill" />
<view class="vip-card-bg" wx:else></view>
<view class="vip-card-inner">
<view class="vip-card-left">
<view class="vip-title-row">
<text class="vip-name">{{item.mingzi}}</text>
<view class="vip-status">{{item.isBought ? '已激活' : '未激活'}}</view>
</view>
<view class="vip-price-row">
<text class="vip-currency">¥</text>
<text class="vip-price">{{item.jiage}}</text>
</view>
<text class="vip-validity">有效期 {{item.formal_days || 30}} 天</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">/{{item.formal_days || 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 class="vip-card-right">
<view class="icon-clip icon-clip-vip" wx:if="{{item.cardImageUrl}}">
<image class="icon-clip-img" src="{{item.cardImageUrl}}" mode="aspectFill" />
</view>
<view class="vip-logo-fallback" wx:else>
<text class="vip-logo-text">VIP</text>
</view>
</view>
</view>
<!-- 选中状态指示器 -->
<view class="card-indicator" wx:if="{{index === currentHuiyuanIndex}}">
<view class="indicator-triangle"></view>
<view class="indicator-glow"></view>
</view>
<view class="vip-shimmer"></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 class="section-head">
<view class="section-bar"></view>
<text class="section-title">{{rulesTitle || ((currentHuiyuan.mingzi || '会员') + '规则')}}</text>
</view>
<scroll-view class="rules-scroll" scroll-x enable-flex>
<view class="rules-row">
<view wx:for="{{detailRules}}" wx:key="index" class="rule-card">
<view class="rule-num">{{index + 1}}</view>
<text class="rule-text">{{item}}</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>
</scroll-view>
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">{{benefitsTitle || ((currentHuiyuan.mingzi || '会员') + '专属权益')}}</text>
</view>
<view class="benefits-row">
<view wx:for="{{memberBenefits}}" wx:key="title" class="benefit-card">
<view wx:if="{{item.iconUrl}}" class="icon-clip icon-clip-benefit">
<image class="icon-clip-img" src="{{item.iconUrl}}" mode="aspectFill" />
</view>
<text class="benefit-title">{{item.title}}</text>
<text class="benefit-desc" style="{{item.accentStyle}}">{{item.desc}}</text>
</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>
<text class="price-days">/{{currentHuiyuan.formal_days || 30}}天</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>
<text class="buy-desc trial-hint" wx:if="{{currentHuiyuan.can_buy_trial}}">
体验版仅微信支付 ¥{{currentHuiyuan.trial_price}}/{{currentHuiyuan.trial_days}}天,终身1次
</text>
</view>
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">{{advantagesTitle || '平台优势'}}</text>
</view>
<view class="advantage-grid">
<view wx:for="{{platformAdvantages}}" wx:key="title" class="advantage-item">
<view wx:if="{{item.iconUrl}}" class="icon-clip icon-clip-advantage">
<image class="icon-clip-img" src="{{item.iconUrl}}" mode="aspectFill" />
</view>
<view class="buy-action">
<view class="tech-buy-btn {{currentHuiyuan.isBought ? 'renew' : ''}}"
bindtap="onBuyHuiyuanClick" data-id="{{currentHuiyuan.id}}" data-trial="false">
<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
wx:if="{{currentHuiyuan.can_buy_trial}}"
class="tech-buy-btn trial-buy-btn"
bindtap="onBuyHuiyuanClick"
data-id="{{currentHuiyuan.id}}"
data-trial="true"
>
<view class="buy-btn-bg"></view>
<text class="buy-btn-text">购买体验版</text>
</view>
<text class="advantage-title">{{item.title}}</text>
<text class="advantage-desc">{{item.desc}}</text>
</view>
</view>
<view id="jifen-section" class="jifen-section">
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">积分充值</text>
</view>
<view class="jifen-card">
<view class="jifen-left">
<text class="jifen-label">当前积分</text>
<text class="jifen-value">{{jifen}}</text>
<text class="jifen-tip" wx:if="{{jifen < 10}}">积分不足将影响接单与提现</text>
<text class="jifen-tip jifen-tip--ok" wx:else>积分充足</text>
</view>
<view class="jifen-btn {{jifen >= 10 ? 'jifen-btn--disabled' : ''}}" bindtap="onJifenClick" data-disabled="{{jifen >= 10}}">
{{jifen >= 10 ? '已满' : '补充积分'}}
</view>
</view>
</view>
<view class="owned-section" wx:if="{{clubmber.length > 0}}">
<view class="section-head">
<view class="section-bar"></view>
<text class="section-title">我的会员</text>
</view>
<view wx:for="{{clubmber}}" wx:key="huiyuanid" class="owned-item" data-item="{{item}}" bindtap="showMemberDetail">
<text class="owned-name">{{item.huiyuanming}}</text>
<text class="owned-expire">到期 {{formatDaoqiTime(item.daoqi)}}</text>
</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 class="footer-bar">
<view class="deposit-banner" bindtap="goToDeposit">
<text class="deposit-banner-text">缴纳履约金,快人一步</text>
<text class="deposit-banner-link">去缴纳 ></text>
</view>
<view class="agree-row" bindtap="toggleAgree">
<view class="agree-check {{agreed ? 'agree-check--on' : ''}}">{{agreed ? '✓' : ''}}</view>
<text class="agree-text">已阅读并同意</text>
<text class="agree-link">《{{currentHuiyuan.mingzi || '会员'}}协议》</text>
</view>
<view class="buy-row">
<view
wx:if="{{currentHuiyuan.can_buy_trial}}"
class="buy-btn buy-btn--outline"
bindtap="onTapTrial"
data-id="{{currentHuiyuan.id}}"
data-trial="true">
体验会员 (¥{{currentHuiyuan.trial_price}})
</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
class="buy-btn buy-btn--primary {{currentHuiyuan.can_buy_trial ? '' : 'buy-btn--full'}}"
bindtap="onTapFormal"
data-id="{{currentHuiyuan.id}}"
data-trial="false">
{{currentHuiyuan.isBought ? '续费正式会员' : '升级正式会员'}} (¥{{currentHuiyuan.jiage}})
<view class="buy-btn-shimmer"></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">星阙网络 © 科技</view>
</view>
</view>
<!-- 通知组件 -->
<global-notification id="global-notification" />
<!-- 支付方式 -->
<view class="modal-mask" wx:if="{{showPayMethodModal}}" bindtap="hidePayMethodModal">
<view class="modal-panel" catchtap="stopPropagation">
<text class="modal-title">选择支付方式</text>
<view class="pay-method-item" bindtap="onPayMethodSelect" data-method="wx">
<text class="pay-method-name">微信支付</text>
<text class="pay-method-desc">安全快捷</text>
</view>
<view class="pay-method-item" bindtap="onPayMethodSelect" data-method="alipay">
<text class="pay-method-name">支付宝</text>
<text class="pay-method-desc">扫码支付</text>
</view>
<view class="pay-method-item" bindtap="onPayMethodSelect" data-method="balance">
<text class="pay-method-name">余额抵扣</text>
<text class="pay-method-desc">使用佣金或分红</text>
</view>
</view>
</view>
<!-- 🆕 统一弹窗组件 -->
<popup-notice id="popupNotice" />
<view class="modal-mask" wx:if="{{showBalanceModal}}" bindtap="hideBalanceModal">
<view class="modal-panel" catchtap="stopPropagation">
<text class="modal-title">选择抵扣身份</text>
<view wx:for="{{balanceOptions}}" wx:key="id" class="balance-item" bindtap="onBalanceSelect" data-id="{{item.id}}">
<text>{{item.jieshao}}</text>
<text class="balance-need">需¥{{item.xuyao}}</text>
</view>
</view>
</view>
<view class="modal-mask" wx:if="{{showConfirmModal}}" bindtap="hideConfirmModal">
<view class="modal-panel" catchtap="stopPropagation">
<text class="modal-title">确认抵扣</text>
<view class="confirm-row"><text>身份</text><text>{{selectedBalanceInfo.jieshao}}</text></view>
<view class="confirm-row"><text>金额</text><text>¥{{selectedBalanceInfo.xuyao}}</text></view>
<view class="modal-actions">
<view class="modal-btn cancel" bindtap="hideConfirmModal">取消</view>
<view class="modal-btn confirm" bindtap="confirmBalancePay">确认支付</view>
</view>
</view>
</view>
<view class="modal-mask" wx:if="{{showMemberModal}}" bindtap="hideMemberModal">
<view class="modal-panel" catchtap="stopPropagation">
<text class="modal-title">会员详情</text>
<view class="confirm-row"><text>名称</text><text>{{currentMemberDetail.huiyuanming}}</text></view>
<view class="confirm-row"><text>到期</text><text>{{currentMemberDetail.daoqi}}</text></view>
<view class="confirm-row"><text>剩余</text><text>{{formatRemainTime(currentMemberDetail.daoqi)}}</text></view>
</view>
</view>
<view class="loading-mask" wx:if="{{payLoading}}">
<view class="loading-box">
<view class="loading-spinner"></view>
<text>{{loadingText}}</text>
</view>
</view>
<global-notification id="global-notification" />
<popup-notice id="popupNotice" />
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,13 @@
{
"usingComponents": {
"global-notification": "/components/global-notification/global-notification",
"chenghao-tag": "/components/chenghao-tag/chenghao-tag",
"tab-bar": "/tab-bar/index",
"pindao-modal": "/components/pindao-modal/pindao-modal"
},
"navigationStyle": "custom",
"backgroundColor": "#fff8e1",
"backgroundTextStyle": "dark",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50
}
@@ -0,0 +1,15 @@
{
"usingComponents": {
"global-notification": "/components/global-notification/global-notification",
"chenghao-tag": "/components/chenghao-tag/chenghao-tag",
"tab-bar": "/tab-bar/index",
"pindao-modal": "/components/pindao-modal/pindao-modal",
"kefu-float": "/components/kefu-float/kefu-float",
"popup-notice": "/components/popup-notice/popup-notice"
},
"navigationStyle": "custom",
"backgroundColor": "#fff8e1",
"backgroundTextStyle": "dark",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50
}
@@ -0,0 +1,217 @@
<!-- 打手端个人中心 - 逍遥梦布局,逻辑/事件不变 -->
<!-- 未注册 -->
<block wx:if="{{!isDashou && !isGuanshi && !isZuzhang && !isKaoheguan}}">
<view class="user-page unreg-page">
<view class="page-header">
<view class="status-bar" style="height:{{statusBar}}px"></view>
<view class="nav-bar" style="height:{{navBar - statusBar}}px"><text class="nav-title">我的</text></view>
</view>
<view class="unreg-area">
<view class="panel shadow unreg-card">
<text class="unreg-title">接单员认证</text>
<input class="unreg-input" type="text" placeholder="输入邀请码" maxlength="100" value="{{inviteCode}}" bindinput="onInviteCodeInput" placeholder-class="ph" />
<view class="btn-bg unreg-btn" bindtap="onRegister"><text class="unreg-btn-text">立即注册</text></view>
<view class="unreg-btn-secondary" bindtap="quickRegisterAsPlatform"><text>平台直签注册</text></view>
<view class="unreg-link" bindtap="switchToNormal"><text>返回点单端</text></view>
<view class="unreg-link" bindtap="clearCache"><text>退出登录</text></view>
</view>
</view>
<tab-bar/>
</view>
</block>
<!-- 已注册 -->
<block wx:else>
<view class="user-page xym-user">
<view class="page-header">
<view class="status-bar" style="height:{{statusBar}}px"></view>
<view class="nav-bar" style="height:{{navBar - statusBar}}px">
<text class="nav-title">我的</text>
</view>
</view>
<scroll-view
class="content"
scroll-y
enhanced
show-scrollbar="{{false}}"
refresher-enabled="{{true}}"
refresher-threshold="80"
refresher-default-style="black"
refresher-background="#fff8e1"
refresher-triggered="{{scrollViewRefreshing}}"
bindrefresherrefresh="onPullDownRefresh"
>
<view class="user-info flexb">
<view class="flex">
<image class="avatar" src="{{avatarUrl || imgUrls.avatarDefault}}" mode="aspectFill" bindtap="previewAvatar"/>
<view class="user-text-col">
<view class="nickname-row">
<text class="nickname">{{dashouNicheng || '接单员'}}</text>
<view class="deposit-tag" wx:if="{{yajin && yajin != '0' && yajin != '0.00'}}">保证金</view>
<view class="role-tags">
<text class="role-tag">接单员</text>
<text class="role-tag" wx:if="{{isGuanshi}}">管事</text>
<text class="role-tag" wx:if="{{isZuzhang}}">组长</text>
<text class="role-tag" wx:if="{{isKaoheguan}}">考核</text>
</view>
</view>
<view class="sn-row flex" bindtap="copyUid">
<text class="sn">ID: {{uid || '--'}}</text>
<image class="copy-ico" src="{{imgUrls.iconCopy}}" mode="aspectFit"/>
</view>
</view>
</view>
<view class="user-header-actions">
<image class="header-action-ico" src="{{imgUrls.iconRefresh}}" mode="aspectFit" bindtap="onTapRefresh"/>
<image class="setting-ico" src="{{imgUrls.iconEdit}}" mode="aspectFit" bindtap="goToModifyInfo"/>
</view>
</view>
<view class="badge-zone" wx:if="{{chenghaoList.length > 0 || (isGuanshi && guanshiChenghaoList.length > 0) || identityTagList.length > 0}}">
<scroll-view scroll-x class="badge-scroll" wx:if="{{chenghaoList.length > 0}}">
<view class="badge-tag-wrap" wx:for="{{chenghaoList}}" wx:key="index">
<chenghao-tag mingcheng="{{item.mingcheng}}" texiaoJson="{{item.texiao_json}}"/>
</view>
</scroll-view>
<scroll-view scroll-x class="badge-scroll" wx:if="{{isGuanshi && guanshiChenghaoList.length > 0}}">
<view class="badge-tag-wrap" wx:for="{{guanshiChenghaoList}}" wx:key="index">
<chenghao-tag mingcheng="{{item.mingcheng}}" texiaoJson="{{item.texiao_json}}"/>
</view>
</scroll-view>
<scroll-view scroll-x class="badge-scroll identity-tag-scroll" wx:if="{{identityTagList.length > 0}}">
<view class="badge-tag-wrap" wx:for="{{identityTagList}}" wx:key="id">
<chenghao-tag mingcheng="{{item.mingcheng}}" texiaoJson="{{item.texiao_json}}"/>
</view>
</scroll-view>
</view>
<view class="wallet-card flexb">
<view bindtap="goToWalletDetail">
<view class="w-label">我的佣金(元)</view>
<view class="w-num">¥{{totalAssets}}</view>
<view class="w-sub">可提现 ¥{{yongjin || '0.00'}} · 查看</view>
</view>
<view class="wallet-btns flex">
<view class="btn-outline" catchtap="goToRecharge">充值</view>
<view class="btn-outline" catchtap="goToTotalWithdraw">去提现</view>
</view>
</view>
<view class="banner-row flex" wx:if="{{showRechargeBanners}}">
<image wx:if="{{imgUrls.bannerVip}}" class="banner-img" src="{{imgUrls.bannerVip}}" mode="widthFix" bindtap="goToRechargeMember"/>
<image wx:if="{{imgUrls.bannerDeposit}}" class="banner-img" src="{{imgUrls.bannerDeposit}}" mode="widthFix" bindtap="goToRechargeDeposit"/>
</view>
<view class="recharge-text-row flex" wx:else>
<view class="recharge-text-btn" catchtap="goToRechargeMember">充值会员</view>
<view class="recharge-text-btn" catchtap="goToRechargeDeposit">充值保证金</view>
</view>
<view class="qiepian-wrap" bindtap="onQiepianTap">
<image class="qiepian-banner" src="{{qiepianBanner}}" mode="widthFix"/>
</view>
<view class="freeze-row flexa">
<view class="freeze-col flexmc">
<text class="freeze-num">{{jinfen || '0'}}</text>
<text class="freeze-lbl">积分</text>
</view>
<view class="freeze-col flexmc">
<text class="freeze-num">{{yajin || '0.00'}}</text>
<text class="freeze-lbl">保证金</text>
</view>
<view class="freeze-col flexmc">
<text class="freeze-num">{{yongjin || '0.00'}}</text>
<text class="freeze-lbl">佣金</text>
</view>
</view>
<view wx:if="{{pendingPenaltyCount > 0}}" class="tip-bar flexb" bindtap="goToMyPunishment">
<view class="flex">
<image class="tip-ico" src="{{penaltyTipIcon}}" mode="aspectFit"/>
<text>你有{{pendingPenaltyCount}}条罚单待处理, 前往处理</text>
</view>
<text class="tip-arrow">›</text>
</view>
<view class="my-box shadow order-nav-box">
<view class="panel-title flexb" bindtap="goToAllOrders">
<text class="lg">我的订单</text>
<text class="muted">全部订单 ›</text>
</view>
<view class="order-nav">
<view wx:for="{{orderNavXym}}" wx:key="name" class="nav-item" data-status="{{item.statusKey}}" bindtap="goToOrdersWithStatus">
<view class="icon-wrap">
<view wx:if="{{item.showBadge && item.badge > 0}}" class="nav-badge">{{item.badge}}</view>
<image class="nav-icon" src="{{item.icon}}" mode="aspectFit"/>
</view>
<text class="sm">{{item.name}}</text>
</view>
</view>
</view>
<view class="my-box shadow trade-box">
<view class="section-hd">交易明细</view>
<view class="trade-list">
<view wx:for="{{tradeNavXym}}" wx:key="name" class="trade-item" data-type="{{item.type}}" bindtap="goToTradeNav">
<image class="trade-icon" src="{{item.icon}}" mode="aspectFit"/>
<text class="sm">{{item.name}}</text>
</view>
</view>
</view>
<view class="my-box shadow invite-box">
<view class="section-hd">好友邀请</view>
<view class="invite-row">
<view wx:for="{{inviteNavXym}}" wx:key="name" class="invite-item" data-action="{{item.action}}" bindtap="onInviteNavTap">
<image class="invite-icon" src="{{item.icon}}" mode="aspectFit"/>
<text class="sm">{{item.name}}</text>
</view>
</view>
</view>
<view class="my-box shadow more-box">
<view class="section-hd">更多功能</view>
<view class="func-list">
<view
wx:for="{{moreFuncList}}"
wx:key="action"
class="func-item-xym"
bindtap="onMoreFuncTap"
data-action="{{item.action}}"
data-auth-type="{{item.authType}}"
data-rank-type="{{item.rankType}}"
>
<view class="func-icon-wrap"><image class="func-icon-xym" src="{{item.icon}}" mode="aspectFit"/></view>
<text class="func-txt-xym">{{item.name}}</text>
</view>
</view>
</view>
<view class="load-tip">— 已加载全部 —</view>
<view class="content-bottom"></view>
</scroll-view>
<view class="follow-kuaishou {{followClass}}" bindtap="toggleFollowKuaishou">
<image class="ks-icon" src="{{ksIcon}}" mode="aspectFit"/>
<text wx:if="{{followExpanded}}" class="ks-text" catchtap="goToGuanzhuKs">关注快手</text>
</view>
<tab-bar/>
</view>
</block>
<view class="loading-mask" wx:if="{{isLoading}}">
<view class="loading-spinner"></view>
</view>
<global-notification id="global-notification"/>
<popup-notice id="popupNotice"/>
<pindao-modal
visible="{{pindaoVisible}}"
title="{{pindaoTitle}}"
channelNo="{{pindaoChannelNo}}"
images="{{pindaoImages}}"
bind:close="closePindaoModal"
/>
@@ -0,0 +1,240 @@
<!-- 打手端个人中心 - 逍遥梦布局,逻辑/事件不变 -->
<!-- 未注册 -->
<block wx:if="{{!isDashou && !isGuanshi && !isZuzhang && !isKaoheguan}}">
<view class="user-page unreg-page">
<view class="page-header">
<view class="status-bar" style="height:{{statusBar}}px"></view>
<view class="nav-bar" style="height:{{navBar - statusBar}}px"><text class="nav-title">我的</text></view>
</view>
<view class="unreg-area">
<view class="panel shadow unreg-card">
<text class="unreg-title">接单员认证</text>
<input class="unreg-input" type="text" placeholder="输入邀请码" maxlength="100" value="{{inviteCode}}" bindinput="onInviteCodeInput" placeholder-class="ph" />
<view class="btn-bg unreg-btn" bindtap="onRegister"><text class="unreg-btn-text">立即注册</text></view>
<view class="unreg-btn-secondary" bindtap="quickRegisterAsPlatform"><text>平台直签注册</text></view>
<view class="unreg-link" bindtap="switchToNormal"><text>返回点单端</text></view>
<view class="unreg-link" bindtap="clearCache"><text>清除缓存,重新登录</text></view>
</view>
</view>
<tab-bar/>
<kefu-float bottom="200rpx" />
</view>
</block>
<!-- 已注册 -->
<block wx:else>
<view class="user-page xym-user">
<view class="page-header">
<view class="status-bar" style="height:{{statusBar}}px"></view>
<view class="nav-bar" style="height:{{navBar - statusBar}}px">
<text class="nav-title">我的</text>
</view>
</view>
<scroll-view
class="content"
scroll-y
enhanced
show-scrollbar="{{false}}"
refresher-enabled="{{true}}"
refresher-threshold="80"
refresher-default-style="black"
refresher-background="#fff8e1"
refresher-triggered="{{scrollViewRefreshing}}"
bindrefresherrefresh="onPullDownRefresh"
>
<view class="user-info flexb">
<view class="flex">
<image class="avatar" src="{{avatarUrl || imgUrls.avatarDefault}}" mode="aspectFill" bindtap="previewAvatar"/>
<view class="user-text-col">
<view class="nickname-row">
<text class="nickname">{{dashouNicheng || '接单员'}}</text>
<view class="deposit-tag" wx:if="{{yajin && yajin != '0' && yajin != '0.00'}}">保证金</view>
<view class="role-tags">
<text class="role-tag">接单员</text>
<text class="role-tag" wx:if="{{isGuanshi}}">管事</text>
<text class="role-tag" wx:if="{{isZuzhang}}">组长</text>
<text class="role-tag" wx:if="{{isKaoheguan}}">考核</text>
</view>
</view>
<view class="sn-row flex" bindtap="copyUid">
<text class="sn">ID: {{uid || '--'}}</text>
<image class="copy-ico" src="{{imgUrls.iconCopy}}" mode="aspectFit"/>
</view>
</view>
</view>
<view class="user-header-actions">
<image class="header-action-ico" src="{{imgUrls.iconRefresh}}" mode="aspectFit" bindtap="onTapRefresh"/>
<image class="setting-ico" src="{{imgUrls.iconEdit}}" mode="aspectFit" bindtap="goToModifyInfo"/>
</view>
</view>
<!-- 被指定订单(星阙功能保留) -->
<view class="my-zhiding-mine-strip" wx:if="{{myZhidingCount > 0 && isDashou}}" bindtap="goToZhidingFromMine">
<view class="my-zhiding-mine-strip-left">
<text class="my-zhiding-mine-badge">指定</text>
<text class="my-zhiding-mine-count">{{myZhidingCount}}单待接待</text>
</view>
<text class="my-zhiding-mine-arrow">去处理 ›</text>
</view>
<view class="badge-zone" wx:if="{{chenghaoList.length > 0 || (isGuanshi && guanshiChenghaoList.length > 0) || identityTagList.length > 0}}">
<scroll-view scroll-x class="badge-scroll" wx:if="{{chenghaoList.length > 0}}">
<view class="badge-tag-wrap" wx:for="{{chenghaoList}}" wx:key="index">
<chenghao-tag mingcheng="{{item.mingcheng}}" texiaoJson="{{item.texiao_json}}"/>
</view>
</scroll-view>
<scroll-view scroll-x class="badge-scroll" wx:if="{{isGuanshi && guanshiChenghaoList.length > 0}}">
<view class="badge-tag-wrap" wx:for="{{guanshiChenghaoList}}" wx:key="index">
<chenghao-tag mingcheng="{{item.mingcheng}}" texiaoJson="{{item.texiao_json}}"/>
</view>
</scroll-view>
<scroll-view scroll-x class="badge-scroll identity-tag-scroll" wx:if="{{identityTagList.length > 0}}">
<view class="badge-tag-wrap" wx:for="{{identityTagList}}" wx:key="id">
<chenghao-tag mingcheng="{{item.mingcheng}}" texiaoJson="{{item.texiao_json}}"/>
</view>
</scroll-view>
</view>
<view class="wallet-card flexb">
<view bindtap="goToWalletDetail">
<view class="w-label">我的佣金(元)</view>
<view class="w-num">¥{{totalAssets}}</view>
<view class="w-sub">可提现 ¥{{yongjin || '0.00'}} · 查看</view>
</view>
<view class="wallet-btns flex">
<view class="btn-outline" catchtap="goToRecharge">充值</view>
<view class="btn-outline" catchtap="goToTotalWithdraw">去提现</view>
</view>
</view>
<view class="banner-row flex" wx:if="{{showRechargeBanners}}">
<image wx:if="{{imgUrls.bannerVip}}" class="banner-img" src="{{imgUrls.bannerVip}}" mode="widthFix" bindtap="goToRecharge"/>
<image wx:if="{{imgUrls.bannerDeposit}}" class="banner-img" src="{{imgUrls.bannerDeposit}}" mode="widthFix" bindtap="goToRecharge"/>
</view>
<view class="recharge-text-row flex" wx:else>
<view class="recharge-text-btn" catchtap="goToRecharge">充值会员</view>
<view class="recharge-text-btn" catchtap="goToRecharge">充值保证金</view>
</view>
<view class="qiepian-wrap" bindtap="onQiepianTap">
<image class="qiepian-banner" src="{{qiepianBanner}}" mode="widthFix"/>
</view>
<view class="freeze-row flexa">
<view class="freeze-col flexmc">
<text class="freeze-num">{{jinfen || '0'}}</text>
<text class="freeze-lbl">积分</text>
</view>
<view class="freeze-col flexmc">
<text class="freeze-num">{{yajin || '0.00'}}</text>
<text class="freeze-lbl">保证金</text>
</view>
<view class="freeze-col flexmc">
<text class="freeze-num">{{yongjin || '0.00'}}</text>
<text class="freeze-lbl">佣金</text>
</view>
</view>
<view class="stats-bar" wx:if="{{isGuanshi || isZuzhang || isKaoheguan}}">
<view class="stat-cell" wx:if="{{isGuanshi}}"><text class="stat-val">{{yaoqingzongshu || 0}}</text><text class="stat-lbl">邀请总数</text></view>
<view class="stat-cell" wx:if="{{isGuanshi}}"><text class="stat-val">{{yichongzhiDashou || 0}}</text><text class="stat-lbl">已充值</text></view>
<view class="stat-cell" wx:if="{{isGuanshi}}"><text class="stat-val">{{fenyongzonge || '0.00'}}</text><text class="stat-lbl">分成总额</text></view>
<view class="stat-cell" wx:if="{{isGuanshi}}"><text class="stat-val">{{zhuanhualv || 0}}%</text><text class="stat-lbl">有效率</text></view>
<view class="stat-cell" wx:if="{{isZuzhang}}"><text class="stat-val">{{guanshiCount || 0}}</text><text class="stat-lbl">邀请管事</text></view>
<view class="stat-cell" wx:if="{{isZuzhang}}"><text class="stat-val">{{fenhongZonge || '0.00'}}</text><text class="stat-lbl">分红总额</text></view>
<view class="stat-cell" wx:if="{{isKaoheguan}}"><text class="stat-val">{{shenheZongshu || 0}}</text><text class="stat-lbl">审核总数</text></view>
<view class="stat-cell" wx:if="{{isKaoheguan}}"><text class="stat-val">{{tongguoZongshu || 0}}</text><text class="stat-lbl">通过总数</text></view>
<view class="stat-cell" wx:if="{{isKaoheguan}}"><text class="stat-val">{{pendingShenheCount || 0}}</text><text class="stat-lbl">待审核</text></view>
</view>
<view wx:if="{{pendingPenaltyCount > 0}}" class="tip-bar flexb" bindtap="goToMyPunishment">
<view class="flex">
<image class="tip-ico" src="https://bintao.xmxym88.com/xcx/bintao/51.png" mode="aspectFit"/>
<text>你有{{pendingPenaltyCount}}条罚单待处理, 前往处理</text>
</view>
<text class="tip-arrow">›</text>
</view>
<view class="my-box shadow order-nav-box">
<view class="panel-title flexb" bindtap="goToAllOrders">
<text class="lg">我的订单</text>
<text class="muted">全部订单 ›</text>
</view>
<view class="order-nav">
<view wx:for="{{orderNavXym}}" wx:key="name" class="nav-item" data-status="{{item.statusKey}}" bindtap="goToOrdersWithStatus">
<view class="icon-wrap">
<view wx:if="{{item.showBadge && item.badge > 0}}" class="nav-badge">{{item.badge}}</view>
<image class="nav-icon" src="{{item.icon}}" mode="aspectFit"/>
</view>
<text class="sm">{{item.name}}</text>
</view>
</view>
</view>
<view class="my-box shadow trade-box">
<view class="section-hd">交易明细</view>
<view class="trade-list">
<view wx:for="{{tradeNavXym}}" wx:key="name" class="trade-item" data-type="{{item.type}}" bindtap="goToTradeNav">
<image class="trade-icon" src="{{item.icon}}" mode="aspectFit"/>
<text class="sm">{{item.name}}</text>
</view>
</view>
</view>
<view class="my-box shadow invite-box">
<view class="section-hd">好友邀请</view>
<view class="invite-row">
<view wx:for="{{inviteNavXym}}" wx:key="name" class="invite-item" data-action="{{item.action}}" bindtap="onInviteNavTap">
<image class="invite-icon" src="{{item.icon}}" mode="aspectFit"/>
<text class="sm">{{item.name}}</text>
</view>
</view>
</view>
<view class="my-box shadow more-box">
<view class="section-hd">更多功能</view>
<view class="func-list">
<view
wx:for="{{moreFuncList}}"
wx:key="action"
class="func-item-xym"
bindtap="onMoreFuncTap"
data-action="{{item.action}}"
data-auth-type="{{item.authType}}"
data-rank-type="{{item.rankType}}"
>
<view class="func-icon-wrap"><image class="func-icon-xym" src="{{item.icon}}" mode="aspectFit"/></view>
<text class="func-txt-xym">{{item.name}}</text>
</view>
</view>
</view>
<view class="load-tip">— 已加载全部 —</view>
<view class="content-bottom"></view>
</scroll-view>
<view class="follow-kuaishou {{followClass}}" bindtap="toggleFollowKuaishou">
<image class="ks-icon" src="{{ksIcon}}" mode="aspectFit"/>
<text wx:if="{{followExpanded}}" class="ks-text" catchtap="goToGuanzhuKs">关注快手</text>
</view>
<tab-bar/>
<kefu-float bottom="200rpx" />
</view>
</block>
<view class="loading-mask" wx:if="{{isLoading}}">
<view class="loading-spinner"></view>
</view>
<global-notification id="global-notification"/>
<popup-notice id="popupNotice"/>
<pindao-modal
visible="{{pindaoVisible}}"
title="{{pindaoTitle}}"
channelNo="{{pindaoChannelNo}}"
images="{{pindaoImages}}"
bind:close="closePindaoModal"
/>
@@ -0,0 +1,618 @@
/* 打手端个人中心 - 逍遥梦 UI(逻辑不变) */
@import '../../styles/dashou-xym-user.wxss';
page {
background: #fff8e1;
color: #333;
}
.flex { display: flex; align-items: center; }
.flexb { display: flex; align-items: center; justify-content: space-between; }
.flexa { display: flex; align-items: center; justify-content: space-around; }
.flexmc { display: flex; flex-direction: column; align-items: center; justify-content: center; }
.user-page {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
background: linear-gradient(180deg, #f7dc51 0%, #fff 28%, #fff8e1 100%);
position: relative;
}
.page-header {
position: relative;
z-index: 2;
flex-shrink: 0;
}
.status-bar,
.nav-bar,
.content,
.unreg-area {
position: relative;
z-index: 2;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: center;
}
.nav-bar-actions {
position: relative;
justify-content: center;
}
.nav-refresh-ico {
position: absolute;
right: 24rpx;
width: 44rpx;
height: 44rpx;
padding: 8rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 700;
color: #343434;
}
.content {
flex: 1;
height: 0;
padding-bottom: 0;
}
.refresh-float {
position: fixed;
right: 24rpx;
top: calc(env(safe-area-inset-top) + 100rpx);
z-index: 10;
width: 56rpx;
height: 56rpx;
background: rgba(255, 255, 255, 0.85);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.refresh-float-ico {
width: 32rpx;
height: 32rpx;
}
.user-info {
padding: 10rpx 30rpx;
}
.avatar {
width: 110rpx;
height: 110rpx;
border-radius: 50%;
border: 4rpx solid #f1f7ff;
margin-right: 20rpx;
flex-shrink: 0;
}
.nickname {
font-size: 38rpx;
font-weight: 700;
color: #222;
}
.nickname-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8rpx;
}
.role-tags {
display: flex;
flex-wrap: wrap;
gap: 6rpx;
}
.role-tag {
font-size: 20rpx;
padding: 2rpx 12rpx;
border-radius: 8rpx;
background: linear-gradient(180deg, #fae04d, #ffc0a3);
color: #492f00;
}
.sn-row {
display: flex;
align-items: center;
margin-top: 8rpx;
}
.sn {
color: #999;
font-size: 24rpx;
}
.copy-ico {
width: 24rpx;
height: 24rpx;
margin-left: 8rpx;
opacity: 0.6;
}
.user-header-actions {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
}
.header-action-ico {
width: 48rpx;
height: 48rpx;
padding: 6rpx;
flex-shrink: 0;
}
.setting-ico {
width: 53rpx;
height: 53rpx;
flex-shrink: 0;
}
.badge-zone {
padding: 0 24rpx 8rpx;
}
.badge-scroll {
white-space: nowrap;
width: 100%;
}
.badge-tag-wrap {
display: inline-block;
transform: scale(0.72);
transform-origin: left center;
margin-right: -8rpx;
}
.deposit-tag {
margin-left: 8rpx;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
background: linear-gradient(90deg, #e8c547, #8b6914);
color: #fff;
}
.wallet-card {
margin: 15rpx 30rpx 0;
padding: 24rpx 20rpx;
border-radius: 26rpx;
background: #fef6d4;
align-items: center;
}
.wallet-left {
flex: 1;
min-width: 0;
}
.w-label {
color: #666;
font-size: 26rpx;
}
.w-num {
font-size: 44rpx;
font-weight: 700;
color: #222;
margin-top: 10rpx;
}
.w-sub {
color: #666;
font-size: 26rpx;
margin-top: 8rpx;
}
.wallet-btns {
flex-shrink: 0;
}
.wallet-btns .btn-outline {
margin-left: 12rpx;
}
.btn-outline {
padding: 10rpx 24rpx;
font-size: 26rpx;
font-weight: 700;
border: 2rpx solid #fff;
border-radius: 60rpx;
background: linear-gradient(180deg, #fae04d, #ffc0a3);
color: #492f00;
white-space: nowrap;
}
.banner-row {
margin: 20rpx 24rpx 0;
align-items: flex-start;
}
.banner-row .banner-img {
width: 48%;
height: auto;
display: block;
border-radius: 16rpx;
overflow: hidden;
background: transparent;
}
.banner-row .banner-img:first-child {
margin-right: 2%;
}
.recharge-text-row {
margin: 20rpx 24rpx 0;
gap: 16rpx;
}
.recharge-text-btn {
flex: 1;
text-align: center;
padding: 22rpx 12rpx;
border-radius: 16rpx;
font-size: 28rpx;
font-weight: 600;
color: #492f00;
background: linear-gradient(180deg, #fff8e1, #ffe8b8);
border: 1rpx solid rgba(201, 169, 98, 0.45);
}
.freeze-row {
margin: 16rpx 30rpx 0;
padding: 20rpx 10rpx;
border-radius: 15rpx;
background: #fef6d4;
}
.freeze-col {
flex: 1;
min-width: 0;
}
.freeze-num {
font-size: 32rpx;
font-weight: 700;
color: #7e893c;
}
.freeze-lbl {
font-size: 24rpx;
color: #7e893c;
font-weight: 700;
margin-top: 4rpx;
}
.btn-bg {
background: linear-gradient(180deg, #fae04d, #ffc0a3);
border-radius: 30px;
color: #492f00;
}
.withdraw {
padding: 14rpx 28rpx;
font-size: 26rpx;
font-weight: 700;
border: 2rpx solid #fff;
flex-shrink: 0;
margin-left: 16rpx;
}
.vip-strip {
margin: 16rpx 30rpx 0;
padding: 20rpx 24rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #3d3428, #1a1a2e);
}
.vip-strip-title {
display: block;
font-size: 28rpx;
font-weight: 700;
color: #fcd270;
}
.vip-strip-sub {
display: block;
font-size: 22rpx;
color: rgba(252, 210, 112, 0.65);
margin-top: 4rpx;
}
.vip-strip-btn {
padding: 10rpx 24rpx;
font-size: 24rpx;
font-weight: 700;
}
.panel {
margin: 20rpx 20rpx 12rpx;
background: #fdfcfa;
border-radius: 20rpx;
padding: 20rpx 10rpx 10rpx;
}
.shadow {
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
}
.panel-title {
padding: 8rpx 25rpx 10rpx;
}
.lg {
font-size: 32rpx;
font-weight: 700;
color: #343434;
}
.order-nav {
padding: 10rpx 0 20rpx;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
align-items: flex-start;
}
.nav-item {
width: 20%;
box-sizing: border-box;
padding: 10rpx 0;
position: relative;
}
.exam-tag {
font-size: 20rpx;
margin-top: 4rpx;
padding: 2rpx 10rpx;
border-radius: 8rpx;
}
.exam-tag-pass {
color: #52c41a;
background: rgba(82, 196, 26, 0.12);
}
.exam-tag-pending {
color: #fa8c16;
background: rgba(250, 140, 16, 0.12);
}
.nav-icon {
width: 62rpx;
height: 62rpx;
margin-bottom: 10rpx;
border-radius: 16rpx;
}
.sm {
font-size: 24rpx;
color: #666;
}
.func-grid {
display: flex;
flex-wrap: wrap;
padding: 10rpx 0;
}
.func-item {
width: 25%;
padding: 20rpx 0;
}
.func-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 8rpx;
}
.func-txt {
font-size: 24rpx;
color: #666;
text-align: center;
}
.func-tag {
font-size: 20rpx;
color: #bbb;
margin-top: 4rpx;
}
.func-tag-done {
color: #c9a962;
}
.rank-hub-tip {
font-size: 22rpx;
color: #999;
font-weight: 500;
}
.rank-hub-scroll {
width: 100%;
white-space: nowrap;
}
.rank-hub-track {
display: inline-flex;
padding: 8rpx 16rpx 20rpx;
gap: 16rpx;
}
.rank-tile {
display: inline-block;
width: 280rpx;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.1);
}
.rank-tile-inner {
display: flex;
align-items: center;
padding: 24rpx 20rpx;
min-height: 120rpx;
box-sizing: border-box;
}
.rank-tile-icon {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.25);
flex-shrink: 0;
margin-right: 16rpx;
}
.rank-tile-text {
flex: 1;
min-width: 0;
}
.rank-tile-name {
display: block;
font-size: 28rpx;
font-weight: 700;
color: #fff;
}
.rank-tile-sub {
display: block;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.82);
margin-top: 6rpx;
}
.rank-tile-go {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
font-weight: 600;
flex-shrink: 0;
padding: 6rpx 14rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.2);
}
.rank-theme-blue {
background: linear-gradient(135deg, #3d6eb5 0%, #6ba3f7 100%);
}
.rank-theme-gold {
background: linear-gradient(135deg, #a8842a 0%, #f5d563 100%);
}
.rank-theme-gold .rank-tile-name,
.rank-theme-gold .rank-tile-sub,
.rank-theme-gold .rank-tile-go {
color: #3d2a00;
}
.rank-theme-purple {
background: linear-gradient(135deg, #6b4fbf 0%, #9b7fe8 100%);
}
.rank-theme-orange {
background: linear-gradient(135deg, #d4652a 0%, #f5a962 100%);
}
/* 未注册 */
.unreg-page .unreg-area {
flex: 1;
padding: 40rpx 30rpx 16rpx;
overflow-y: auto;
}
.unreg-card {
padding: 40rpx 30rpx !important;
}
.unreg-title {
font-size: 32rpx;
font-weight: 700;
display: block;
text-align: center;
margin-bottom: 24rpx;
}
.unreg-input {
height: 80rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
background: #f7f7f7;
padding: 0 24rpx;
font-size: 28rpx;
border: 1rpx solid #eee;
}
.ph { color: #bbb; }
.unreg-btn {
text-align: center;
padding: 20rpx;
font-size: 28rpx;
font-weight: 700;
margin-bottom: 16rpx;
}
.unreg-btn-text { color: #492f00; }
.unreg-btn-secondary {
text-align: center;
padding: 18rpx;
font-size: 26rpx;
color: #666;
background: #f5f5f5;
border-radius: 12rpx;
margin-bottom: 12rpx;
}
.unreg-link {
text-align: center;
padding: 16rpx;
font-size: 24rpx;
color: #999;
}
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(0, 0, 0, 0.08);
border-top-color: #ffd061;
border-radius: 50%;
animation: fighterSpin 0.8s linear infinite;
}
@keyframes fighterSpin {
to { transform: rotate(360deg); }
}
Loaded 100 of 327 files, more files were not shown because too many files have changed in this diff. Show more