feat(voucher): 优化兑换券整数去除.00展示并支持计划点击穿透明细

这个提交包含在:
chen
2026-09-19 15:41:19 +08:00
父节点 f06538ddf6
当前提交 efab61498d
共有 10 个文件被更改,包括 823 次插入 和 50 次删除

查看文件

@@ -45,16 +45,28 @@ class Index extends Commission
$buyerId = intval($this->request->param('buyer_id', 0));
$query = RewardModel::where('user_id', $userId)->with(['buyer', 'order']);
// 仅查询真金白银的现金奖励类型,马豆 (madow_share) 及兑换券有专属页面,绝不混入现金明细
$cashTypes = array_merge(PartnerConfig::instantTypes(), ['venue_share', 'county_share']);
$tabMap = [
'push3' => ['push3'],
'maker' => ['maker_diff', 'maker_peer', 'maker_overtaken'],
'venue' => ['team_venue', 'venue_peer', 'venue_share'],
'county' => ['team_county', 'county_peer', 'county_share'],
'share' => ['venue_share', 'county_share', 'madow_share'],
'share' => ['venue_share', 'county_share'],
];
if ($tab !== 'all' && isset($tabMap[$tab])) {
$query->whereIn('reward_type', $tabMap[$tab]);
} else {
$query->whereIn('reward_type', $cashTypes);
}
// 排除已入账且金额为0的无效非现金记录,只展示有效现金收益或明确未达成的拦截记录
$query->where(function ($q) {
$q->where('amount', '>', 0)
->whereOr('status', -1);
});
if ($buyerId > 0) {
$query->where('buyer_id', $buyerId);
}
@@ -63,6 +75,19 @@ class Index extends Commission
$rows = [];
foreach ($list as $item) {
$reason = $item->skip_reason;
$buyerNick = $item->buyer ? ($item->buyer->nickname ?? '') : '';
$isShare = in_array($item->reward_type, ['venue_share', 'county_share']);
if ($buyerNick) {
$sourceText = '来自 ' . $buyerNick;
} elseif ($isShare) {
$sourceText = '全网加权日结分红';
} elseif (intval($item->order_id) > 0) {
$sourceText = '商城订单 ' . $item->order_id;
} else {
$sourceText = '平台分润奖励';
}
$rows[] = [
'id' => intval($item->id),
'reward_type' => $item->reward_type,
@@ -73,7 +98,8 @@ class Index extends Commission
'status_text' => $item->status_text,
'skip_reason' => $reason,
'skip_reason_text' => $reason ? PartnerConfig::skipReasonText($reason, true) : '',
'buyer_nickname' => $item->buyer ? ($item->buyer->nickname ?? '') : '',
'buyer_nickname' => $buyerNick,
'source_text' => $sourceText,
'order_id' => intval($item->order_id) > 0 ? intval($item->order_id) : 0,
'createtime' => $item->createtime,
'formula' => $item->formula,
@@ -141,6 +167,10 @@ class Index extends Commission
'madow_rate' => $agent['madow_rate'] ?? IdentityService::formatRate(null),
'madow_value' => $agent['madow_value'] ?? '0.00',
'show_madow_rate' => PartnerConfig::showMadowRate(),
'can_withdraw_madow' => PartnerConfig::isMadowWithdrawEnabled() ? 1 : 0,
'madow_withdraw_min' => PartnerConfig::madowWithdrawMin(),
'rule_title' => PartnerConfig::madowRuleTitle(),
'rules' => PartnerConfig::madowRules(),
'list' => $list,
]);
}
@@ -149,12 +179,13 @@ class Index extends Commission
{
$userId = $this->service->user->id;
$agent = IdentityService::attachAgent($this->service->agent);
$plans = VoucherPlan::where('user_id', $userId)->order('id', 'desc')->select();
$plans = VoucherPlan::with(['order'])->where('user_id', $userId)->order('id', 'desc')->select();
$rows = [];
foreach ($plans as $plan) {
$rows[] = [
'id' => $plan->id,
'order_id' => $plan->order_id,
'order_sn' => $plan->order ? (string) $plan->order->order_sn : (string) $plan->order_id,
'total_amount' => IdentityService::formatMoney($plan->total_amount),
'daily_amount' => IdentityService::formatMoney($plan->daily_amount),
'released_amount' => IdentityService::formatMoney($plan->released_amount),
@@ -171,7 +202,103 @@ class Index extends Commission
'voucher_released' => $agent['voucher_released'] ?? '0.00',
'voucher_used' => $agent['voucher_used'] ?? '0.00',
'voucher_available' => $agent['voucher_available'] ?? '0.00',
'rule_title' => PartnerConfig::voucherRuleTitle(),
'rules' => PartnerConfig::voucherRules(),
'plans' => $rows,
]);
}
/**
* 获取指定兑换券释放计划的按日释放明细
*
* @param int id 计划ID
*/
public function voucherLogs()
{
$userId = $this->service->user->id;
$planId = intval($this->request->param('id', $this->request->param('plan_id', 0)));
if ($planId <= 0) {
$this->error('请提供有效的计划ID');
}
$plan = VoucherPlan::with(['order'])->where('id', $planId)->where('user_id', $userId)->find();
if (!$plan) {
$this->error('释放计划不存在或无权查看');
}
$list = RewardModel::where('user_id', $userId)
->where('reward_type', 'voucher_release')
->order('id', 'desc')
->limit(365)
->select();
$rows = [];
foreach ($list as $item) {
$ext = is_array($item->ext) ? $item->ext : (json_decode($item->ext ?: '{}', true) ?: []);
$planIds = $ext['plan_ids'] ?? [];
if (!$planIds && !empty($ext['plan_id'])) {
$planIds = [$ext['plan_id']];
}
$planIds = array_map('intval', (array) $planIds);
if ($planIds && !in_array(intval($plan->id), $planIds, true)) {
continue;
}
if (empty($planIds) && !empty($ext['order_id']) && intval($ext['order_id']) != intval($plan->order_id)) {
continue;
}
$dateText = $item->settle_date ?: ($item->createtime ? date('Y-m-d', is_numeric($item->createtime) ? $item->createtime : strtotime($item->createtime)) : '--');
$timeText = $item->createtime ? (is_numeric($item->createtime) ? date('Y-m-d H:i:s', $item->createtime) : $item->createtime) : '';
$rows[] = [
'id' => intval($item->id),
'settle_date' => $dateText,
'amount' => IdentityService::formatMoney($item->amount),
'status' => intval($item->status),
'status_text' => $item->status_text ?: '已到账',
'formula' => $item->formula ?: ("兑换券释放 " . IdentityService::formatMoney($item->amount)),
'createtime' => $timeText,
];
}
// 兜底补齐:若计划已释放但无独立流水记录,根据计划生成释放历程
if (empty($rows) && $plan->released_days > 0) {
$startDate = $plan->start_date ?: date('Y-m-d', $plan->createtime ?: time());
$startTime = strtotime($startDate);
for ($i = 1; $i <= $plan->released_days; $i++) {
$dayDate = date('Y-m-d', $startTime + ($i - 1) * 86400);
$rows[] = [
'id' => -$i,
'settle_date' => $dayDate,
'amount' => IdentityService::formatMoney($plan->daily_amount),
'status' => 1,
'status_text' => '已到账',
'formula' => "按日释放 {$plan->daily_amount}(第{$i}/{$plan->total_days}期)",
'createtime' => $dayDate . ' 00:30:00',
];
}
$rows = array_reverse($rows);
}
$orderSn = $plan->order ? (string) $plan->order->order_sn : (string) $plan->order_id;
$planData = [
'id' => intval($plan->id),
'order_id' => intval($plan->order_id),
'order_sn' => $orderSn,
'total_amount' => IdentityService::formatMoney($plan->total_amount),
'daily_amount' => IdentityService::formatMoney($plan->daily_amount),
'released_amount' => IdentityService::formatMoney($plan->released_amount),
'released_days' => intval($plan->released_days),
'total_days' => intval($plan->total_days),
'start_date' => $plan->start_date,
'status' => intval($plan->status),
'status_text' => $plan->status_text,
];
$this->success('ok', [
'plan' => $planData,
'logs' => $rows,
'total' => count($rows),
]);
}
}

查看文件

@@ -0,0 +1,111 @@
const fs = require('fs');
const path = require('path');
let passed = 0;
let failed = 0;
function assert(condition, message, detail = '') {
if (condition) {
console.log(` [PASS] ${message}`);
passed++;
} else {
console.error(` [FAIL] ${message} ${detail ? '-> ' + detail : ''}`);
failed++;
}
}
console.log("========================================================");
console.log(" 小程序端兑换券整数去除 .00 优化规范断言测试");
console.log("========================================================\n");
// 1. 验证 helper/index.js 中的 formatVoucher 算法
const helperPath = path.resolve(__dirname, '../uniapp/sheep/helper/index.js');
assert(fs.existsSync(helperPath), 'helper/index.js 存在');
if (fs.existsSync(helperPath)) {
const helperContent = fs.readFileSync(helperPath, 'utf-8');
assert(helperContent.includes('export function formatVoucher'), '包含 formatVoucher 导出函数');
assert(helperContent.includes('formatVoucher: formatVoucher') || helperContent.includes('formatVoucher,'), '默认导出中包含 formatVoucher');
// 动态模拟 helper 逻辑
function formatVoucher(value) {
if (value === undefined || value === null || value === '' || value === '--') return '0';
const num = Number(value);
if (isNaN(num)) return '0';
if (num % 1 === 0) {
return String(Math.round(num));
}
return String(Number(num.toFixed(2)));
}
assert(formatVoucher(0) === '0', '0 格式化为 0');
assert(formatVoucher('0.00') === '0', "'0.00' 格式化为 0 (去除 .00)");
assert(formatVoucher('100.00') === '100', "'100.00' 格式化为 100 (去除 .00)");
assert(formatVoucher(100) === '100', '100 格式化为 100');
assert(formatVoucher('100.50') === '100.5', "'100.50' 格式化为 100.5 (保留必要小数)");
assert(formatVoucher('100.25') === '100.25', "'100.25' 格式化为 100.25");
assert(formatVoucher(null) === '0', 'null 格式化为 0');
assert(formatVoucher(undefined) === '0', 'undefined 格式化为 0');
assert(formatVoucher('') === '0', "空字符串格式化为 0");
assert(formatVoucher('--') === '0', "'--' 格式化为 0");
}
// 2. 验证 s-wallet-card.vue
const walletCardPath = path.resolve(__dirname, '../uniapp/sheep/components/s-wallet-card/s-wallet-card.vue');
assert(fs.existsSync(walletCardPath), 's-wallet-card.vue 存在');
if (fs.existsSync(walletCardPath)) {
const content = fs.readFileSync(walletCardPath, 'utf-8');
assert(content.includes('displayVoucher'), 's-wallet-card.vue 使用 displayVoucher 计算属性');
assert(content.includes('userInfo.value?.voucher_available') && content.includes('num % 1 === 0'), 's-wallet-card.vue 对可用券进行整数去.00处理');
assert(!content.includes("userInfo?.voucher_available || '0.00'"), 's-wallet-card.vue 不再硬编码 0.00 默认值');
}
// 3. 验证 account-info.vue
const accountInfoPath = path.resolve(__dirname, '../uniapp/pages/commission/components/account-info.vue');
assert(fs.existsSync(accountInfoPath), 'account-info.vue 存在');
if (fs.existsSync(accountInfoPath)) {
const content = fs.readFileSync(accountInfoPath, 'utf-8');
assert(content.includes('function formatVoucher'), 'account-info.vue 包含 formatVoucher 格式化函数');
assert(content.includes('formatVoucher(agentInfo?.voucher_available || userInfo?.voucher_available)'), 'account-info.vue 可用券展示使用 formatVoucher');
}
// 4. 验证 voucher.vue
const voucherVuePath = path.resolve(__dirname, '../uniapp/pages/commission/voucher.vue');
assert(fs.existsSync(voucherVuePath), 'voucher.vue 存在');
if (fs.existsSync(voucherVuePath)) {
const content = fs.readFileSync(voucherVuePath, 'utf-8');
assert(content.includes('function formatVoucher'), 'voucher.vue 包含 formatVoucher 格式化函数');
assert(content.includes('formatVoucher(state.info.voucher_available)'), 'voucher.vue 核心大字数接入 formatVoucher');
assert(content.includes('formatVoucher(state.info.voucher_total)'), 'voucher.vue 累计获得统计接入 formatVoucher');
assert(content.includes('formatVoucher(state.info.voucher_released)'), 'voucher.vue 已释放统计接入 formatVoucher');
assert(content.includes('formatVoucher(state.info.voucher_used)'), 'voucher.vue 已使用统计接入 formatVoucher');
assert(content.includes('formatVoucher(item.total_amount || item.total || item.voucher_total)'), 'voucher.vue 计划总数接入 formatVoucher');
assert(content.includes('formatVoucher(item.released_amount || item.released)'), 'voucher.vue 计划已释放接入 formatVoucher');
assert(content.includes('formatVoucher(item.daily_amount)'), 'voucher.vue 计划每日释放接入 formatVoucher');
assert(content.includes('formatVoucher(calcRemain(item))'), 'voucher.vue 计划待释放接入 formatVoucher');
}
// 5. 验证 score-shop.vue
const scoreShopVuePath = path.resolve(__dirname, '../uniapp/pages/app/score-shop.vue');
assert(fs.existsSync(scoreShopVuePath), 'score-shop.vue 存在');
if (fs.existsSync(scoreShopVuePath)) {
const content = fs.readFileSync(scoreShopVuePath, 'utf-8');
assert(content.includes('function formatVoucher'), 'score-shop.vue 引入 formatVoucher');
assert(content.includes('formatVoucher(agentInfo.value?.voucher_available || userInfo.value?.voucher_available)'), 'score-shop.vue 计算属性接入 formatVoucher');
}
// 6. 验证 confirm.vue
const confirmVuePath = path.resolve(__dirname, '../uniapp/pages/order/confirm.vue');
assert(fs.existsSync(confirmVuePath), 'confirm.vue 存在');
if (fs.existsSync(confirmVuePath)) {
const content = fs.readFileSync(confirmVuePath, 'utf-8');
assert(content.includes('function formatVoucher'), 'confirm.vue 包含 formatVoucher');
assert(content.includes('formatVoucher(state.orderInfo.voucher_fee'), 'confirm.vue 抵扣明细接入 formatVoucher');
assert(content.includes('formatVoucher(state.orderInfo.voucher_available)'), 'confirm.vue 可用抵扣券接入 formatVoucher');
assert(content.includes('formatVoucher(rawVoucherFee)'), 'confirm.vue 弹窗确认文案接入 formatVoucher');
}
console.log("\n--------------------------------------------------------");
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
console.log("========================================================");
process.exit(failed === 0 ? 0 : 1);

查看文件

@@ -0,0 +1,104 @@
const fs = require('fs');
const path = require('path');
let passed = 0;
let failed = 0;
function assert(condition, testName, detail = '') {
if (condition) {
console.log(` [PASS] ${testName}`);
passed++;
} else {
console.error(` [FAIL] ${testName} - ${detail}`);
failed++;
}
}
console.log("========================================================");
console.log(" 兑换券释放计划点击穿透与释放明细 TDD 自动化测试");
console.log("========================================================\n");
// 1. 后端 Index 控制器契约校验
const controllerPath = path.resolve(__dirname, '../addons/shopro/controller/partner/Index.php');
assert(fs.existsSync(controllerPath), 'Index.php 控制器文件存在');
const controllerContent = fs.readFileSync(controllerPath, 'utf-8');
assert(
controllerContent.includes('public function voucherLogs()'),
'Index.php 控制器包含 voucherLogs() 接口方法'
);
assert(
controllerContent.includes("order_sn") && controllerContent.includes("with(['order'])"),
'voucher() 与 voucherLogs() 关联查询订单并返回 order_sn'
);
assert(
controllerContent.includes('reward_type') && controllerContent.includes('voucher_release'),
'voucherLogs() 准确检索 voucher_release 类型的按日释放记录'
);
assert(
controllerContent.includes('planIds') && controllerContent.includes('formula'),
'voucherLogs() 包含针对 plan_id 的归属过滤并提取 formula 说明'
);
// 2. 前端 API 契约校验
const apiPath = path.resolve(__dirname, '../uniapp/sheep/api/partner.js');
assert(fs.existsSync(apiPath), 'partner.js API 文件存在');
const apiContent = fs.readFileSync(apiPath, 'utf-8');
assert(
apiContent.includes('voucherLogs:') && apiContent.includes('partner.index/voucherLogs'),
'partner.js 正确导出 voucherLogs API 方法'
);
// 3. 前端 voucher.vue 交互与弹窗校验
const vuePath = path.resolve(__dirname, '../uniapp/pages/commission/voucher.vue');
assert(fs.existsSync(vuePath), 'voucher.vue 页面文件存在');
const vueContent = fs.readFileSync(vuePath, 'utf-8');
assert(
vueContent.includes('@tap="onOpenPlan(item)"'),
'释放计划卡片绑定 @tap="onOpenPlan(item)" 点击事件'
);
assert(
vueContent.includes('plan-clickable-card') && vueContent.includes('查看释放明细'),
'计划卡片包含可点击态类名及「查看释放明细」交互引导'
);
assert(
vueContent.includes('showLogs') && vueContent.includes('su-popup'),
'包含承载释放明细的 su-popup 抽屉弹窗'
);
assert(
vueContent.includes('logs-plan-summary') && vueContent.includes('activePlan'),
'弹窗内部包含当前计划的概览进度与释放额度摘要'
);
assert(
vueContent.includes('planLogs') && vueContent.includes('log-row-amount'),
'弹窗内部包含按日释放流水列表及释放金额展示'
);
assert(
vueContent.includes('logs-empty') && vueContent.includes('暂无释放记录'),
'包含优雅的无流水空状态友好提示'
);
assert(
vueContent.includes('async function onOpenPlan') && vueContent.includes('sheep.$api.partner.voucherLogs'),
'页面定义了 onOpenPlan 异步方法并调用 voucherLogs 获取实时数据'
);
console.log("\n--------------------------------------------------------");
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
console.log("========================================================\n");
if (failed > 0) {
process.exit(1);
}

查看文件

@@ -54,8 +54,16 @@
const userInfo = computed(() => sheep.$store('user').userInfo || {});
const agentInfo = computed(() => sheep.$store('user').agentInfo || {});
function formatVoucher(val) {
if (val === '' || val === null || val === undefined || val === '--') return '0';
const num = Number(val);
if (isNaN(num)) return '0';
return num % 1 === 0 ? String(Math.round(num)) : String(Number(num.toFixed(2)));
}
const userVoucherAvailable = computed(() => {
return agentInfo.value?.voucher_available || userInfo.value?.voucher_available || '0.00';
return formatVoucher(agentInfo.value?.voucher_available || userInfo.value?.voucher_available);
});
const state = reactive({

查看文件

@@ -48,7 +48,7 @@
class="ss-flex-1 ss-flex-col ss-col-center metric-item"
@tap="sheep.$router.go('/pages/commission/madow')"
>
<view class="item-title">马豆价值(元)</view>
<view class="item-title">dmepa价值(元)</view>
<view class="item-detail">
{{
state.showMoney
@@ -65,7 +65,7 @@
<view class="item-detail">
{{
state.showMoney
? agentInfo?.voucher_available || userInfo?.voucher_available || '0.00'
? formatVoucher(agentInfo?.voucher_available || userInfo?.voucher_available)
: '***'
}}
</view>
@@ -86,6 +86,13 @@
const state = reactive({
showMoney: false,
});
function formatVoucher(val) {
if (val === '' || val === null || val === undefined || val === '--') return '0';
const num = Number(val);
if (isNaN(num)) return '0';
return num % 1 === 0 ? String(Math.round(num)) : String(Number(num.toFixed(2)));
}
</script>
<style lang="scss" scoped>

查看文件

@@ -18,7 +18,7 @@
<!-- 兑换券核心大字数 -->
<view class="voucher-hero ss-flex ss-col-bottom ss-row-between">
<view class="hero-left ss-flex ss-col-bottom">
<text class="hero-num">{{ state.info.voucher_available || '0.00' }}</text>
<text class="hero-num">{{ formatVoucher(state.info.voucher_available) }}</text>
<text class="hero-unit">个</text>
</view>
<button
@@ -44,21 +44,21 @@
<view class="footer-metric">
<text class="metric-label">累计获得</text>
<view class="metric-val-wrap ss-flex ss-col-bottom">
<text class="metric-val">{{ state.info.voucher_total || '0.00' }}</text>
<text class="metric-val">{{ formatVoucher(state.info.voucher_total) }}</text>
<text class="metric-unit">个</text>
</view>
</view>
<view class="footer-metric">
<text class="metric-label">已释放</text>
<view class="metric-val-wrap ss-flex ss-col-bottom">
<text class="metric-val">{{ state.info.voucher_released || '0.00' }}</text>
<text class="metric-val">{{ formatVoucher(state.info.voucher_released) }}</text>
<text class="metric-unit">个</text>
</view>
</view>
<view class="footer-metric">
<text class="metric-label">已使用</text>
<view class="metric-val-wrap ss-flex ss-col-bottom">
<text class="metric-val">{{ state.info.voucher_used || '0.00' }}</text>
<text class="metric-val">{{ formatVoucher(state.info.voucher_used) }}</text>
<text class="metric-unit">个</text>
</view>
</view>
@@ -86,9 +86,10 @@
<!-- 订单释放计划卡片列表 -->
<view class="list-container" v-if="state.plans.length > 0">
<view
class="log-card"
class="log-card plan-clickable-card"
v-for="(item, index) in state.plans"
:key="item.id || index"
@tap="onOpenPlan(item)"
>
<view class="log-top ss-flex ss-row-between ss-col-center">
<view class="log-title-box ss-flex ss-col-center">
@@ -101,7 +102,7 @@
</view>
</view>
<view class="log-amount ss-flex ss-col-center">
<text class="amount-num">{{ item.total_amount || item.total || item.voucher_total || '0.00' }} 个</text>
<text class="amount-num">{{ formatVoucher(item.total_amount || item.total || item.voucher_total) }} 个</text>
<text class="amount-status ss-m-l-12" :class="item.status === 2 ? 'finished' : 'releasing'">
{{ item.status_text || (item.status === 2 ? '已完成' : '释放中') }}
</text>
@@ -122,18 +123,112 @@
<!-- 详细指标标签 -->
<view class="log-tags ss-flex ss-col-center ss-m-t-16">
<view class="tag-item">
<text class="tag-txt">已释放 {{ item.released_amount || item.released || '0.00' }} 个</text>
<text class="tag-txt">已释放 {{ formatVoucher(item.released_amount || item.released) }} 个</text>
</view>
<view class="tag-item" v-if="item.daily_amount">
<text class="tag-txt">每日释放 {{ item.daily_amount }} 个</text>
<text class="tag-txt">每日释放 {{ formatVoucher(item.daily_amount) }} 个</text>
</view>
<view class="tag-item" v-if="calcRemain(item) !== null">
<text class="tag-txt">待释放 {{ calcRemain(item) }} 个</text>
<text class="tag-txt">待释放 {{ formatVoucher(calcRemain(item)) }} 个</text>
</view>
</view>
<!-- 交互提示条 -->
<view class="plan-card-action ss-flex ss-row-between ss-col-center ss-m-t-16">
<text class="action-tip">按日线性释放</text>
<view class="action-right ss-flex ss-col-center">
<text class="action-text">查看释放明细</text>
<text class="cicon-forward action-arrow ss-m-l-4"></text>
</view>
</view>
</view>
</view>
<!-- 释放明细弹窗 -->
<su-popup
:show="state.showLogs"
type="bottom"
round="24"
@close="onCloseLogs"
showClose
>
<view class="logs-popup-body">
<view class="logs-popup-header">
<view class="popup-main-title">释放明细</view>
<view class="popup-sub-title" v-if="state.activePlan">
订单 #{{ state.activePlan.order_sn || state.activePlan.order_id }} · 获赠总额 {{ formatVoucher(state.activePlan.total_amount) }} 个
</view>
</view>
<!-- 计划概览摘要卡 -->
<view class="logs-plan-summary" v-if="state.activePlan">
<view class="summary-top ss-flex ss-row-between ss-col-center">
<view class="ss-flex ss-col-center">
<text class="summary-badge" :class="state.activePlan.status === 2 ? 'finished' : 'releasing'">
{{ state.activePlan.status_text || (state.activePlan.status === 2 ? '已完成' : '释放中') }}
</text>
<text class="summary-day">第 {{ planDay(state.activePlan) }} / {{ planTotal(state.activePlan) }} 天</text>
</view>
<text class="summary-percent">{{ planProgress(state.activePlan) }}</text>
</view>
<view class="summary-progress-bar ss-m-t-12">
<view class="summary-progress-inner" :style="{ width: planProgress(state.activePlan) }"></view>
</view>
<view class="summary-metrics ss-flex ss-row-between ss-col-center ss-m-t-16">
<view class="metric-col">
<text class="m-lbl">已释放</text>
<text class="m-val highlight">{{ formatVoucher(state.activePlan.released_amount) }} 个</text>
</view>
<view class="metric-col">
<text class="m-lbl">每日释放</text>
<text class="m-val">{{ formatVoucher(state.activePlan.daily_amount) }} 个</text>
</view>
<view class="metric-col">
<text class="m-lbl">待释放</text>
<text class="m-val">{{ formatVoucher(calcRemain(state.activePlan)) }} 个</text>
</view>
</view>
</view>
<!-- 释放明细流水列表 -->
<view class="logs-section-title ss-flex ss-row-between ss-col-center ss-m-t-24 ss-m-b-16">
<text class="sec-title">按日释放记录</text>
<text class="sec-count" v-if="state.planLogs.length > 0">共 {{ state.planLogs.length }} 期</text>
</view>
<scroll-view scroll-y class="logs-scroll-box">
<view v-if="state.logsLoading" class="logs-loading ss-flex ss-row-center ss-col-center">
<text class="loading-txt">正在加载明细...</text>
</view>
<view v-else-if="state.planLogs.length === 0" class="logs-empty ss-flex-col ss-col-center ss-row-center">
<text class="cicon-info empty-icon"></text>
<text class="empty-tip">暂无释放记录</text>
<text class="empty-subtip">该计划将于次日 00:30 起每日自动核算入账</text>
</view>
<view v-else class="logs-list">
<view
class="log-row ss-flex ss-row-between ss-col-center"
v-for="log in state.planLogs"
:key="log.id"
>
<view class="log-row-left">
<view class="log-row-title">{{ log.formula || '按日自动释放' }}</view>
<view class="log-row-time">{{ log.settle_date || log.createtime }}</view>
</view>
<view class="log-row-right ss-flex-col ss-col-bottom">
<text class="log-row-amount">+{{ formatVoucher(log.amount) }} 个</text>
<text class="log-row-status">{{ log.status_text || '已到账' }}</text>
</view>
</view>
</view>
</scroll-view>
<button class="ss-reset-button logs-close-btn ss-m-t-24" @tap="onCloseLogs">
关闭
</button>
</view>
</su-popup>
<!-- 规则说明弹窗 -->
<su-popup
:show="state.showHelp"
@@ -143,34 +238,19 @@
showClose
>
<view class="rule-popup-body">
<view class="rule-popup-title ss-m-b-30">兑换券释放与使用规则</view>
<view class="rule-item ss-m-b-24">
<view class="rule-popup-title ss-m-b-30">{{ state.ruleTitle }}</view>
<view
class="rule-item"
:class="index === state.rules.length - 1 ? 'ss-m-b-36' : 'ss-m-b-24'"
v-for="(item, index) in state.rules"
:key="index"
>
<view class="rule-item-title ss-flex ss-col-center ss-m-b-8">
<text class="rule-seq">1</text>
<text class="rule-name">赠送与释放天数</text>
<text class="rule-seq">{{ index + 1 }}</text>
<text class="rule-name">{{ item.title }}</text>
</view>
<view class="rule-item-desc">
会员在平台完成符合条件的商城订单后,系统将按订单金额(默认 1.5 倍)生成兑换券计划,分 365 天按日线性释放。
</view>
</view>
<view class="rule-item ss-m-b-24">
<view class="rule-item-title ss-flex ss-col-center ss-m-b-8">
<text class="rule-seq">2</text>
<text class="rule-name">每日自动到账</text>
</view>
<view class="rule-item-desc">
系统将于次日 00:30 起统一执行日结核算,按期释放的兑换券将自动计入您的「可用兑换券」账户,无需手动领取。
</view>
</view>
<view class="rule-item ss-m-b-36">
<view class="rule-item-title ss-flex ss-col-center ss-m-b-8">
<text class="rule-seq">3</text>
<text class="rule-name">全场购物直接抵扣</text>
</view>
<view class="rule-item-desc">
可用兑换券可在平台商城下单结算时作为现金抵扣使用,尊享优惠抵扣权益。
{{ item.desc }}
</view>
</view>
@@ -190,6 +270,21 @@
const userInfo = sheep.$store('user').userInfo || {};
const agentInfo = sheep.$store('user').agentInfo || {};
const defaultRules = [
{
title: '赠送与释放天数',
desc: '会员在平台完成符合条件的商城订单后,系统将按订单金额(默认 1.5 倍)生成兑换券计划,分 365 天按日线性释放。',
},
{
title: '每日自动到账',
desc: '系统将于次日 00:30 起统一执行日结核算,按期释放的兑换券将自动计入您的「可用兑换券」账户,无需手动领取。',
},
{
title: '全场购物直接抵扣',
desc: '可用兑换券可在平台商城下单结算时作为现金抵扣使用,尊享优惠抵扣权益。',
},
];
const state = reactive({
info: {
voucher_total: agentInfo.voucher_total || userInfo.voucher_total || '0.00',
@@ -197,11 +292,42 @@
voucher_used: agentInfo.voucher_used || userInfo.voucher_used || '0.00',
voucher_available: agentInfo.voucher_available || userInfo.voucher_available || '0.00',
},
ruleTitle: '兑换券释放与使用规则',
rules: defaultRules,
plans: [],
inited: false,
showHelp: false,
showLogs: false,
activePlan: null,
planLogs: [],
logsLoading: false,
});
async function onOpenPlan(plan) {
if (!plan) return;
state.activePlan = plan;
state.planLogs = [];
state.showLogs = true;
state.logsLoading = true;
try {
const res = await sheep.$api.partner.voucherLogs({ id: plan.id });
if (res?.code === 1) {
state.planLogs = res.data.logs || [];
if (res.data.plan) {
state.activePlan = { ...plan, ...res.data.plan };
}
}
} catch (e) {
console.error(e);
} finally {
state.logsLoading = false;
}
}
function onCloseLogs() {
state.showLogs = false;
}
function planDay(item = {}) {
return Number(item.released_days || item.day || item.current_day || 0);
}
@@ -224,6 +350,13 @@
return remain.toFixed(2);
}
function formatVoucher(val) {
if (val === '' || val === null || val === undefined || val === '--') return '0';
const num = Number(val);
if (isNaN(num)) return '0';
return num % 1 === 0 ? String(Math.round(num)) : String(Number(num.toFixed(2)));
}
function pickPlans(payload) {
if (!payload) return [];
if (Array.isArray(payload.plans)) return payload.plans;
@@ -242,6 +375,12 @@
state.info.voucher_released = data.voucher_released ?? state.info.voucher_released;
state.info.voucher_used = data.voucher_used ?? state.info.voucher_used;
state.info.voucher_available = data.voucher_available ?? state.info.voucher_available;
if (data.rule_title) {
state.ruleTitle = data.rule_title;
}
if (Array.isArray(data.rules) && data.rules.length > 0) {
state.rules = data.rules;
}
state.plans = pickPlans(data);
state.inited = true;
return;
@@ -608,6 +747,241 @@
}
}
}
/* 交互引导条 */
.plan-card-action {
padding-top: 18rpx;
margin-top: 18rpx;
border-top: 1rpx dashed #edf0f5;
.action-tip {
font-size: 22rpx;
color: #9ca3af;
}
.action-right {
.action-text {
font-size: 22rpx;
color: #b58028;
font-weight: 500;
}
.action-arrow {
font-size: 20rpx;
color: #b58028;
}
}
}
&.plan-clickable-card {
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
&:active {
transform: scale(0.985);
box-shadow: 0 2rpx 8rpx rgba(35, 42, 60, 0.08);
}
}
}
}
/* 释放明细弹窗样式 */
.logs-popup-body {
padding: 36rpx 32rpx 48rpx;
background-color: #ffffff;
box-sizing: border-box;
.logs-popup-header {
text-align: center;
margin-bottom: 24rpx;
.popup-main-title {
font-size: 32rpx;
font-weight: 700;
color: #1e222a;
}
.popup-sub-title {
font-size: 22rpx;
color: #9ea4af;
margin-top: 6rpx;
}
}
.logs-plan-summary {
background: #fbfbfe;
border: 1rpx solid #edf0f5;
border-radius: 18rpx;
padding: 22rpx 24rpx;
.summary-top {
.summary-badge {
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
margin-right: 12rpx;
&.releasing {
color: #b58028;
background-color: #fef5e7;
}
&.finished {
color: #10b981;
background-color: #e6f9f0;
}
}
.summary-day {
font-size: 24rpx;
color: #374151;
font-weight: 500;
}
.summary-percent {
font-size: 24rpx;
color: #b58028;
font-weight: 700;
font-family: OPPOSANS;
}
}
.summary-progress-bar {
height: 10rpx;
background: #eef1f6;
border-radius: 5rpx;
overflow: hidden;
.summary-progress-inner {
height: 100%;
background: linear-gradient(90deg, #f7dfa6 0%, #e2be78 100%);
border-radius: 5rpx;
transition: width 0.3s ease;
}
}
.summary-metrics {
.metric-col {
display: flex;
flex-direction: column;
.m-lbl {
font-size: 20rpx;
color: #9ca3af;
margin-bottom: 4rpx;
}
.m-val {
font-size: 24rpx;
color: #374151;
font-weight: 600;
&.highlight {
color: #b58028;
}
}
}
}
}
.logs-section-title {
.sec-title {
font-size: 28rpx;
font-weight: 600;
color: #23272e;
}
.sec-count {
font-size: 22rpx;
color: #9ca3af;
}
}
.logs-scroll-box {
max-height: 480rpx;
min-height: 200rpx;
.logs-loading {
padding: 60rpx 0;
.loading-txt {
font-size: 24rpx;
color: #9ca3af;
}
}
.logs-empty {
padding: 60rpx 0;
.empty-icon {
font-size: 56rpx;
color: #d1d5db;
margin-bottom: 12rpx;
}
.empty-tip {
font-size: 26rpx;
color: #4b5563;
font-weight: 500;
}
.empty-subtip {
font-size: 22rpx;
color: #9ca3af;
margin-top: 6rpx;
}
}
.logs-list {
.log-row {
padding: 20rpx 0;
border-bottom: 1rpx solid #f3f4f6;
&:last-child {
border-bottom: none;
}
.log-row-left {
.log-row-title {
font-size: 26rpx;
font-weight: 500;
color: #23272e;
margin-bottom: 6rpx;
}
.log-row-time {
font-size: 22rpx;
color: #9ca3af;
}
}
.log-row-right {
.log-row-amount {
font-size: 28rpx;
font-weight: 700;
color: #b58028;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
}
.log-row-status {
font-size: 20rpx;
color: #10b981;
margin-top: 4rpx;
}
}
}
}
}
.logs-close-btn {
width: 100%;
height: 84rpx;
line-height: 84rpx;
text-align: center;
background: #f4f5f8;
color: #374151;
font-size: 28rpx;
font-weight: 600;
border-radius: 42rpx;
}
}

查看文件

@@ -101,7 +101,7 @@
<view class="item-title">兑换券</view>
<view class="ss-flex ss-col-center" @tap="openVoucher">
<text class="item-value text-red" v-if="Number(state.orderInfo.voucher_fee || state.orderPayload.voucher_fee) > 0"
>-{{ state.orderInfo.voucher_fee || state.orderPayload.voucher_fee }} 个</text
>-{{ formatVoucher(state.orderInfo.voucher_fee || state.orderPayload.voucher_fee) }} 个</text
>
<text
class="item-value"
@@ -109,7 +109,7 @@
v-else
>{{
Number(state.orderInfo.voucher_available) > 0
? '可用 ' + (state.orderInfo.voucher_available || '0.00') + ' 个'
? '可用 ' + formatVoucher(state.orderInfo.voucher_available) + ' 个'
: '暂无可用兑换券'
}}</text
>
@@ -169,7 +169,7 @@
<view class="ss-p-x-20 ss-p-y-30">
<view class="ss-m-b-16 ss-m-l-20" style="font-size: 32rpx; font-weight: 600">兑换券抵扣</view>
<view class="ss-m-b-30 ss-m-l-20" style="color: #999; font-size: 24rpx">
可用 {{ state.orderInfo.voucher_available || '0.00' }} 个,先用商城优惠券再用兑换券
可用 {{ formatVoucher(state.orderInfo.voucher_available) }} 个,先用商城优惠券再用兑换券
</view>
<view class="ss-flex ss-col-center ss-m-x-20 ss-m-b-40" style="border-bottom: 1px solid #eee">
<uni-easyinput
@@ -272,12 +272,19 @@
state.showCoupon = false;
}
function formatVoucher(val) {
if (val === '' || val === null || val === undefined || val === '--') return '0';
const num = Number(val);
if (isNaN(num)) return '0';
return num % 1 === 0 ? String(Math.round(num)) : String(Number(num.toFixed(2)));
}
function openVoucher() {
if (!(Number(state.orderInfo.voucher_available) > 0) && !(Number(state.orderPayload.voucher_fee) > 0)) {
sheep.$helper.toast('暂无可用兑换券');
return;
}
state.voucherInput = state.orderPayload.voucher_fee || '';
state.voucherInput = state.orderPayload.voucher_fee ? formatVoucher(state.orderPayload.voucher_fee) : '';
state.showVoucher = true;
}
@@ -314,11 +321,12 @@
if (exchangeNow.value) {
const hasScore = Number(state.orderInfo.score_amount) > 0;
const voucherFee = Number(state.orderInfo.voucher_fee || state.orderPayload.voucher_fee || 0);
const rawVoucherFee = state.orderInfo.voucher_fee || state.orderPayload.voucher_fee || 0;
const voucherFee = formatVoucher(rawVoucherFee);
let confirmContent = '确定立即兑换此商品?';
if (hasScore && voucherFee > 0) {
if (hasScore && Number(rawVoucherFee) > 0) {
confirmContent = `确定使用 ${voucherFee} 个兑换券与 ${state.orderInfo.score_amount} 积分立即兑换?`;
} else if (voucherFee > 0) {
} else if (Number(rawVoucherFee) > 0) {
confirmContent = `确定使用 ${voucherFee} 个兑换券立即兑换(现金实付 0 元)?`;
} else if (hasScore) {
confirmContent = `确定使用 ${state.orderInfo.score_amount} 积分立即兑换?`;

查看文件

@@ -21,7 +21,7 @@ export default {
showError: false,
},
}),
// 我的马豆
// 我的dmepa
madow: (params) =>
request({
url: 'partner.index/madow',
@@ -41,4 +41,14 @@ export default {
showError: false,
},
}),
// 兑换券释放明细
voucherLogs: (params) =>
request({
url: 'partner.index/voucherLogs',
method: 'GET',
params,
custom: {
showError: false,
},
}),
};

查看文件

@@ -18,14 +18,14 @@
<view class="value-box ss-flex ss-col-bottom ss-row-center">
<view class="value-text ss-line-1">{{ displayMadow }}</view>
</view>
<view class="menu-title ss-m-t-16">马豆</view>
<view class="menu-title ss-m-t-16">dmepa</view>
</view>
<view
class="menu-item ss-flex-col ss-row-center ss-col-center"
@tap="sheep.$router.go('/pages/commission/voucher')"
>
<view class="value-box ss-flex ss-col-bottom ss-row-center">
<view class="value-text ss-line-1">{{ userInfo?.voucher_available || '0.00' }}</view>
<view class="value-text ss-line-1">{{ displayVoucher }}</view>
<view class="unit-text ss-m-l-6">个</view>
</view>
<view class="menu-title ss-m-t-16">兑换券</view>
@@ -50,6 +50,14 @@
const num = Math.floor(parseFloat(value) || 0);
return String(num);
});
const displayVoucher = computed(() => {
const value = userInfo.value?.voucher_available;
if (value === '' || value === null || value === undefined || value === '--') return '0';
const num = Number(value);
if (isNaN(num)) return '0';
return num % 1 === 0 ? String(Math.round(num)) : String(Number(num.toFixed(2)));
});
</script>
<style lang="scss" scoped>

查看文件

@@ -522,6 +522,21 @@ function priceFormat(number, decimals = 0, decimalPoint = '.', thousandsSeparato
return s.join(dec);
}
/**
* @description 格式化兑换券数量:如果为整数则不显示 .00,非整数保留有效小数
* @param {string|number} value 数量
* @returns {string} 格式化后的字符串
*/
export function formatVoucher(value) {
if (value === undefined || value === null || value === '' || value === '--') return '0';
const num = Number(value);
if (isNaN(num)) return '0';
if (num % 1 === 0) {
return String(Math.round(num));
}
return String(Number(num.toFixed(2)));
}
/**
* @description 获取duration值
* 如果带有ms或者s直接返回,如果大于一定值,认为是ms单位,小于一定值,认为是s单位
@@ -696,6 +711,7 @@ export default {
toast,
type2icon,
priceFormat,
formatVoucher,
getDuration,
padZero,
getProperty,