perf(trade): 优化商品购买页面的规格展示和折扣计算,支持订单取消

This commit is contained in:
xiaojunnuo
2026-05-29 00:56:30 +08:00
parent c7a9363422
commit 6624769032
16 changed files with 452 additions and 240 deletions
@@ -370,21 +370,6 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "certd.myWallet",
name: "MyWallet",
path: "/certd/wallet",
component: "/certd/wallet/index.vue",
meta: {
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
},
icon: "ion:wallet-outline",
auth: true,
keepAlive: true,
},
},
{
title: "certd.inviteCommission",
name: "InviteCommission",
@@ -400,6 +385,21 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "certd.myWallet",
name: "MyWallet",
path: "/certd/wallet",
component: "/certd/wallet/index.vue",
meta: {
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
},
icon: "ion:wallet-outline",
auth: true,
keepAlive: true,
},
},
{
title: "certd.paymentReturn",
name: "PaymentReturn",
@@ -14,21 +14,30 @@ export default function (): CreateCrudOptionsRet {
toolbar: { show: false },
rowHandle: { show: false },
columns: {
inviteeUserId: {
title: "被推广用户ID",
type: "number",
column: { width: 140 },
},
inviteCode: {
title: "推广码",
type: "text",
column: { width: 160 },
},
createTime: {
title: "推广时间",
title: "邀请时间",
type: "datetime",
column: { width: 180 },
},
simpleUser: {
title: "被邀请用户名",
type: "text",
column: {
minWidth: 180,
cellRender({ row }) {
const simpleUser = row.simpleUser;
if (!simpleUser) {
return row.inviteeUserId ? `用户${row.inviteeUserId} (${row.inviteeUserId})` : "-";
}
return simpleUser.displayName || `${simpleUser.username || "-"} (${simpleUser.id})`;
},
},
},
inviteCode: {
title: "邀请码",
type: "text",
column: { width: 160 },
},
},
},
};
@@ -15,6 +15,11 @@ export default function (): CreateCrudOptionsRet {
toolbar: { show: false },
rowHandle: { show: false },
columns: {
createTime: {
title: "时间",
type: "datetime",
column: { width: 180 },
},
amount: {
title: "收益金额",
type: "number",
@@ -56,11 +61,6 @@ export default function (): CreateCrudOptionsRet {
type: "text",
column: { minWidth: 220 },
},
createTime: {
title: "时间",
type: "datetime",
column: { width: 180 },
},
},
},
};
@@ -13,8 +13,14 @@
<div v-if="loaded && enabled && inviteInfo.enabled" class="invite-body">
<div class="invite-summary-grid">
<div v-for="item in summaryCards" :key="item.key" class="summary-card">
<div class="summary-title">{{ item.title }}</div>
<div class="summary-value" :class="item.className">{{ item.value }}</div>
<div class="summary-card-main">
<div class="summary-title">{{ item.title }}</div>
<div class="summary-value" :class="item.className">{{ item.value }}</div>
</div>
<div v-if="item.key === 'totalIncome'" class="withdraw-action">
<div class="withdraw-available">可提现 {{ moneyText(inviteInfo.wallet?.availableAmount) }}</div>
<a-button class="summary-action-button" type="primary" @click="gotoWallet">提现</a-button>
</div>
</div>
</div>
@@ -47,14 +53,18 @@
<span class="info-label">我的等级</span>
<div class="info-content level-info-content">
<span class="level-name-text">{{ inviteInfo.currentLevel?.name || "未设置" }}</span>
<span v-if="inviteInfo.currentLevel" class="current-level-rate">{{ inviteInfo.currentLevel.commissionRate }}%</span>
<span v-if="inviteInfo.currentLevel" class="current-level-rate">
<span class="current-level-rate-label">返佣比例</span>
<span class="current-level-rate-value">{{ inviteInfo.currentLevel.commissionRate }}%</span>
</span>
<span v-if="inviteInfo.currentLevel" class="level-rate-desc">好友付费后按此比例计算佣金</span>
</div>
<fs-icon class="level-open-icon" icon="ion:chevron-forward-outline" />
</div>
</div>
<a-tabs v-model:active-key="activeTab" class="invite-tabs" @change="handleTabChange">
<a-tab-pane key="invitees" tab="推广成功用户">
<a-tab-pane key="invitees" tab="邀请成功">
<fs-crud v-if="activeTab === 'invitees'" ref="inviteesCrudRef" class="invite-crud" v-bind="inviteesCrudBinding" />
</a-tab-pane>
<a-tab-pane key="logs" tab="收益记录">
@@ -72,6 +82,17 @@
<a-modal v-model:open="levelDialogOpen" title="推广等级" width="820px" wrap-class-name="invite-level-modal" :footer="null">
<div class="level-modal-subtitle">推广越多等级越高返佣比例越高</div>
<div class="level-progress-box">
<div>
<div class="level-progress-label">当前累计推广金额</div>
<div class="level-progress-value">¥ {{ amountToYuan(inviteInfo.summary.promotionAmount) }}</div>
</div>
<div class="level-progress-desc">
<template v-if="inviteInfo.nextLevel">距离下一等级{{ inviteInfo.nextLevel.name }}还差 {{ amountToYuan(inviteInfo.nextLevel.gapAmount) }} </template>
<template v-else-if="inviteInfo.currentLevel?.levelType === 'exclusive'">当前为专属等级不参与自动升级</template>
<template v-else>已达到当前可自动升级的最高等级</template>
</div>
</div>
<div class="level-card-grid modal-level-grid">
<div v-for="level in visibleLevels" :key="level.id" class="level-card" :class="{ active: level.id === inviteInfo.currentLevel?.id }">
<div class="level-name">
@@ -83,7 +104,8 @@
</div>
<div class="level-rate-label">佣金比例</div>
<div class="level-rate">{{ level.commissionRate }}%</div>
<div class="level-threshold">累计推广 {{ amountToYuan(level.minAmount) }} </div>
<div v-if="level.levelType === 'exclusive'" class="level-threshold exclusive-threshold">平台指定专属等级</div>
<div v-else class="level-threshold">累计推广 {{ amountToYuan(level.minAmount) }} </div>
<a-tag v-if="level.id === inviteInfo.currentLevel?.id" class="current-tag" color="blue">当前等级</a-tag>
<div v-else-if="level.id === inviteInfo.nextLevel?.id" class="next-gap">还差 {{ amountToYuan(inviteInfo.nextLevel.gapAmount) }}</div>
</div>
@@ -119,6 +141,7 @@
import { computed, nextTick, onActivated, onMounted, reactive, ref } from "vue";
import { FsIcon, useFs } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue";
import { useRouter } from "vue-router";
import * as api from "./api";
import createInviteesCrudOptions from "./crud-invitees";
import createLogsCrudOptions from "./crud-logs";
@@ -127,6 +150,7 @@ import { util } from "/@/utils";
defineOptions({ name: "InviteCommission" });
const router = useRouter();
const settingStore = useSettingStore();
const enabled = ref(false);
const activeTab = ref("invitees");
@@ -144,6 +168,7 @@ const inviteInfo = reactive<any>({
inviteLink: "",
agreementContent: "",
summary: { totalIncomeAmount: 0, monthIncomeAmount: 0, promotionAmount: 0, inviteeCount: 0 },
wallet: { availableAmount: 0 },
currentLevel: null,
nextLevel: null,
levelList: [],
@@ -173,6 +198,12 @@ const summaryCards = computed(() => [
value: moneyText(inviteInfo.summary.monthIncomeAmount),
className: "income",
},
{
key: "promotionAmount",
title: "累计推广金额",
value: moneyText(inviteInfo.summary.promotionAmount),
className: "promotion",
},
{
key: "inviteeCount",
title: "已推广人数",
@@ -191,6 +222,10 @@ function levelIcon(level: any) {
return level?.icon || "ion:ribbon-outline";
}
function gotoWallet() {
router.push({ path: "/certd/wallet" });
}
function openAgreementDialog(needOpenPlan: boolean) {
agreementDialogNeedOpen.value = needOpenPlan;
agreementAgree.value = false;
@@ -326,7 +361,7 @@ onActivated(async () => {
.invite-summary-grid {
flex: none;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-bottom: 18px;
}
@@ -351,11 +386,19 @@ onActivated(async () => {
.summary-card {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 112px;
overflow: hidden;
padding: 22px;
}
.summary-card-main {
min-width: 0;
}
.summary-title {
margin-bottom: 10px;
color: hsl(var(--muted-foreground));
@@ -376,6 +419,30 @@ onActivated(async () => {
color: #3478f6;
}
.summary-value.promotion {
color: #16a085;
}
.summary-action-button {
flex: none;
min-width: 72px;
}
.withdraw-action {
display: flex;
align-items: flex-end;
flex: none;
flex-direction: column;
gap: 8px;
}
.withdraw-available {
color: hsl(var(--muted-foreground));
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
.invite-link-panel {
flex: none;
padding: 16px 18px;
@@ -450,22 +517,56 @@ onActivated(async () => {
}
.current-level-rate {
display: inline-flex;
align-items: center;
flex: none;
height: 26px;
margin-left: 6px;
overflow: hidden;
border: 1px solid rgba(197, 138, 53, 0.22);
border-radius: 6px;
background: rgba(197, 138, 53, 0.08);
color: #c58a35;
font-size: 13px;
font-weight: 700;
}
.current-level-rate-label {
height: 100%;
padding: 0 8px;
border-right: 1px solid rgba(197, 138, 53, 0.18);
background: rgba(197, 138, 53, 0.1);
color: #8a5a16;
font-weight: 500;
line-height: 24px;
}
.current-level-rate-value {
padding: 0 8px;
line-height: 24px;
}
.level-info-content {
display: flex;
align-items: center;
gap: 4px;
gap: 6px;
min-width: 0;
flex-wrap: wrap;
}
.level-name-text {
flex: none;
color: hsl(var(--foreground));
font-weight: 600;
}
.level-rate-desc {
min-width: 180px;
color: hsl(var(--muted-foreground));
font-size: 12px;
line-height: 20px;
}
.level-open-icon {
flex: none;
color: hsl(var(--muted-foreground));
@@ -608,6 +709,10 @@ onActivated(async () => {
text-align: center;
}
.exclusive-threshold {
color: #8a5a16;
}
.current-tag {
display: table;
margin: 10px auto 0;
@@ -622,6 +727,36 @@ onActivated(async () => {
margin-top: 12px;
}
.level-progress-box {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 14px;
border: 1px solid rgba(52, 120, 246, 0.16);
border-radius: 8px;
background: rgba(248, 250, 252, 0.86);
}
.level-progress-label {
color: hsl(var(--muted-foreground));
font-size: 12px;
}
.level-progress-value {
margin-top: 2px;
color: #16a085;
font-size: 22px;
font-weight: 700;
line-height: 28px;
}
.level-progress-desc {
color: #3478f6;
font-size: 13px;
text-align: right;
}
.invite-agreement-content {
max-height: 360px;
padding: 12px;
@@ -680,6 +815,15 @@ onActivated(async () => {
.level-card-grid {
grid-template-columns: 1fr;
}
.level-progress-box {
align-items: flex-start;
flex-direction: column;
}
.level-progress-desc {
text-align: left;
}
}
}
</style>
@@ -4,20 +4,33 @@
<div class="flex-o mt-5">
<span class="label">{{ $t("certd.order.package") }}</span>{{ product.title }}
</div>
<div class="flex-o mt-5">
<div v-if="product.intro" class="flex-o mt-5">
<span class="label">{{ $t("certd.order.description") }}</span>{{ product.intro }}
</div>
<div class="flex-o mt-5">
<div class="order-spec-row mt-5">
<span class="label">{{ $t("certd.order.specifications") }}</span>
<span class="flex-o flex-wrap">
<span class="flex-o"> {{ $t("certd.order.pipeline") }}<suite-value class="ml-5" :model-value="product.content.maxPipelineCount" :unit="$t('certd.order.unit.pieces')" /> </span>
<span class="flex-o"> {{ $t("certd.order.totalDomain") }}<suite-value class="ml-5" :model-value="product.content.maxDomainCount" :unit="$t('certd.order.unit.count')" /> </span>
<span class="flex-o" style="padding-left: 2em">
- {{ $t("certd.order.includedWildcardDomain") }}<suite-value class="ml-5" :model-value="product.content.maxWildcardDomainCount" :unit="$t('certd.order.unit.count')" />
</span>
<span class="flex-o"> {{ $t("certd.order.deployTimes") }}<suite-value class="ml-5" :model-value="product.content.maxDeployCount" :unit="$t('certd.order.unit.times')" /> </span>
<span class="flex-o"> {{ $t("certd.order.monitorCount") }}<suite-value class="ml-5" :model-value="product.content.maxMonitorCount" :unit="$t('certd.order.unit.times')" /> </span>
</span>
<div class="spec-grid">
<div class="spec-item">
<div class="spec-name">{{ $t("certd.order.totalDomain") }}</div>
<suite-value :model-value="product.content.maxDomainCount" :unit="$t('certd.order.unit.count')" />
</div>
<div class="spec-item">
<div class="spec-name">{{ $t("certd.order.includedWildcardDomain") }}</div>
<suite-value :model-value="product.content.maxWildcardDomainCount" :unit="$t('certd.order.unit.count')" />
</div>
<div class="spec-item">
<div class="spec-name">{{ $t("certd.order.pipeline") }}</div>
<suite-value :model-value="product.content.maxPipelineCount" :unit="$t('certd.order.unit.pieces')" />
</div>
<div class="spec-item">
<div class="spec-name">{{ $t("certd.order.deployTimes") }}</div>
<suite-value :model-value="product.content.maxDeployCount" :unit="$t('certd.order.unit.times')" />
</div>
<div class="spec-item">
<div class="spec-name">{{ $t("certd.order.monitorCount") }}</div>
<suite-value :model-value="product.content.maxMonitorCount" :unit="$t('certd.order.unit.times')" />
</div>
</div>
</div>
<div class="flex-o mt-5">
@@ -26,10 +39,10 @@
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("certd.order.price") }}</span>
<price-input :edit="false" :model-value="durationSelected.price" zero-text="免费"></price-input>
<price-input :edit="false" :model-value="durationSelected.price" zero-text="0元"></price-input>
</div>
<div v-if="durationSelected.price > 0 && wallet.availableAmount > 0" class="flex-o mt-5">
<span class="label">返利抵扣</span>
<span class="label">余额抵扣</span>
<a-switch v-model:checked="formRef.useRebateBalance" />
<span class="ml-10">可用 {{ amountToYuan(wallet.availableAmount) }} 预计抵扣 {{ amountToYuan(expectedRebateAmount) }} </span>
</div>
@@ -58,7 +71,7 @@ import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
import { useRouter } from "vue-router";
import qrcode from "qrcode";
import * as api from "/@/views/certd/suite/api";
import { GetMyInvite } from "/@/views/certd/invite/api";
import { GetWalletSummary } from "/@/views/certd/wallet/api";
import { util } from "/@/utils";
const openRef = ref(false);
@@ -77,8 +90,7 @@ async function open(opts: OrderModalOpenReq) {
formRef.value.num = opts.num ?? 1;
formRef.value.useRebateBalance = false;
try {
const inviteInfo: any = await GetMyInvite();
wallet.value = inviteInfo.wallet || { availableAmount: 0 };
wallet.value = await GetWalletSummary();
} catch (e) {
wallet.value = { availableAmount: 0 };
}
@@ -261,6 +273,10 @@ defineExpose({
</script>
<style lang="less">
.order-box {
display: flex;
flex-direction: column;
gap: 10px;
.label {
width: 80px;
text-align: right;
@@ -268,6 +284,37 @@ defineExpose({
color: #686868;
flex: none;
}
.order-spec-row {
display: flex;
align-items: flex-start;
}
.spec-grid {
display: grid;
flex: 1;
min-width: 0;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.spec-item {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 42px;
padding: 8px 10px;
gap: 8px;
border: 1px solid rgba(52, 120, 246, 0.12);
border-radius: 8px;
background: rgba(248, 250, 252, 0.86);
}
.spec-name {
color: hsl(var(--foreground));
font-size: 13px;
line-height: 20px;
}
}
.modal-confirm-center {
@@ -39,7 +39,8 @@
<div class="flex-o duration-label">时长</div>
<div class="duration-list">
<div v-for="dp of product.durationPrices" :key="dp.duration" class="duration-item" :class="{ active: selected.duration === dp.duration }" @click="selected = dp">
{{ durationDict.dataMap[dp.duration]?.label }}
<span class="duration-text">{{ durationDict.dataMap[dp.duration]?.label }}</span>
<span v-if="discountText(dp)" class="duration-discount">{{ discountText(dp) }}</span>
</div>
</div>
</div>
@@ -48,7 +49,7 @@
<div class="flex-o">价格</div>
<div class="flex-o price-text">
<price-input style="color: red" :font-size="20" :model-value="selected?.price" :edit="false" zero-text="免费" />
<span class="ml-5" style="font-size: 12px"> / {{ durationDict.dataMap[selected.duration]?.label }}</span>
<span class="price-unit">/ {{ durationDict.dataMap[selected.duration]?.label }}</span>
</div>
</div>
@@ -61,7 +62,7 @@
import { durationDict } from "/@/views/certd/suite/api";
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
import { ref } from "vue";
import { computed, ref } from "vue";
import { dict, FsIcon } from "@fast-crud/fast-crud";
const props = defineProps<{
@@ -69,6 +70,20 @@ const props = defineProps<{
}>();
const selected = ref(props.product.durationPrices[0]);
const originalUnitPrice = computed(() => {
const unitPrices = (props.product.durationPrices || [])
.map((item: any) => {
const duration = Number(item.duration);
const price = Number(item.price);
if (!duration || duration <= 0 || price <= 0) {
return null;
}
return price / duration;
})
.filter((item: number | null): item is number => item != null);
return Math.max(...unitPrices, 0);
});
const productTypeDictRef = dict({
data: [
{ value: "suite", label: "套餐", color: "green" },
@@ -77,6 +92,20 @@ const productTypeDictRef = dict({
});
const emit = defineEmits(["order"]);
function discountText(durationPrice: any) {
const duration = Number(durationPrice.duration);
const price = Number(durationPrice.price);
if (!duration || duration <= 0 || price <= 0 || originalUnitPrice.value <= 0) {
return "";
}
const currentUnitPrice = price / duration;
const discount = Math.round((currentUnitPrice / originalUnitPrice.value) * 100) / 10;
if (discount >= 10) {
return "";
}
return `${discount}`;
}
async function doOrder() {
emit("order", { product: props.product, productId: props.product.id, duration: selected.value.duration, price: selected.value.price });
}
@@ -108,13 +137,21 @@ async function doOrder() {
.duration-list {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 4px;
.duration-item {
width: 45px;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
min-width: 56px;
height: 32px;
border: 1px solid #cdcdcd;
border-radius: 4px;
text-align: center;
padding: 2px;
margin: 2px;
padding: 3px 6px;
cursor: pointer;
line-height: 16px;
&:hover {
border-color: #1890ff;
@@ -124,6 +161,44 @@ async function doOrder() {
background-color: #c1eafb;
}
}
.duration-text {
display: block;
line-height: 20px;
white-space: nowrap;
}
.duration-discount {
position: absolute;
top: -9px;
right: -7px;
height: 16px;
padding: 0 4px;
border-radius: 8px 8px 8px 2px;
background: #ff4d4f;
color: #f5222d;
color: #fff;
font-size: 11px;
font-weight: 600;
line-height: 16px;
white-space: nowrap;
box-shadow: 0 2px 6px rgba(245, 34, 45, 0.24);
}
}
.price-text {
flex: none;
align-items: baseline;
justify-content: flex-end;
white-space: nowrap;
}
.price-unit {
flex: none;
margin-left: 5px;
color: hsl(var(--muted-foreground));
font-size: 12px;
white-space: nowrap;
}
}
</style>
@@ -26,11 +26,11 @@ export async function UpdateObj(obj: any) {
});
}
export async function DelObj(id: any) {
export async function CancelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
url: apiPrefix + "/cancel",
method: "post",
params: { id },
data: { id },
});
}
@@ -50,14 +50,6 @@ export async function GetDetail(id: any) {
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}
export async function SyncStatus(id: any) {
return await request({
url: apiPrefix + "/syncStatus",
@@ -1,18 +1,10 @@
import * as api from "./api";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { Modal } from "ant-design-vue";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
@@ -21,56 +13,34 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
width: 240,
width: 120,
fixed: "right",
buttons: {
view: { show: false },
edit: { show: false },
copy: { show: false },
remove: { show: false },
syncStatus: {
show: compute(({ row }) => {
return row.status === "wait_pay";
}),
text: "同步订单状态",
title: "同步订单状态",
text: null,
tooltip: { title: "同步订单状态" },
icon: "ant-design:sync-outlined",
type: "link",
click: async ({ row }) => {
Modal.confirm({
@@ -83,6 +53,28 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
},
},
cancel: {
show: compute(({ row }) => {
return row.status === "wait_pay";
}),
title: "取消订单",
text: null,
tooltip: { title: "取消订单" },
icon: "ion:close-circle-outline",
type: "link",
click: async ({ row }) => {
Modal.confirm({
title: "确认取消订单?",
content: "取消后订单会关闭,已冻结的余额抵扣金额将自动退回。",
okText: "确认取消",
cancelText: "再想想",
onOk: async () => {
await api.CancelObj(row.id);
await crudExpose.doRefresh();
},
});
},
},
},
},
actionbar: {
@@ -152,7 +144,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
rebateAmount: {
title: "返利抵扣",
title: "余额抵扣",
type: "number",
column: {
width: 110,
@@ -201,7 +193,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
{ label: "支付宝", value: "alipay" },
{ label: "微信", value: "wxpay" },
{ label: "免费", value: "free" },
{ label: "返利余额", value: "rebate" },
{ label: "余额抵扣", value: "rebate" },
],
}),
column: {
@@ -3,13 +3,7 @@
<template #header>
<div class="title">我的订单</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
<fs-crud ref="crudRef" v-bind="crudBinding" />
</fs-page>
</template>
@@ -17,31 +11,11 @@
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
defineOptions({
name: "MyTrade",
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error("请先勾选记录");
}
};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {
@@ -1,6 +1,14 @@
import { CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import * as api from "./api";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
import { util } from "/@/utils";
function moneyText(amount: number) {
const yuan = util.amount.toYuan(Math.abs(amount || 0));
if (amount < 0) {
return `${yuan}`;
}
return `¥${yuan}`;
}
export default function (): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -24,6 +32,7 @@ export default function (): CreateCrudOptionsRet {
{ label: "收益入账", value: "income", color: "success" },
{ label: "余额抵扣", value: "consume", color: "default" },
{ label: "提现冻结", value: "withdraw_freeze", color: "warning" },
{ label: "提现成功", value: "withdraw", color: "success" },
{ label: "提现成功", value: "withdraw_success", color: "success" },
{ label: "提现退回", value: "withdraw_reject", color: "processing" },
],
@@ -35,7 +44,10 @@ export default function (): CreateCrudOptionsRet {
type: "number",
column: {
width: 120,
component: { name: PriceInput, vModel: "modelValue", edit: false },
cellRender({ value }) {
const amount = Number(value || 0);
return <span class={amount < 0 ? "text-green-500" : "text-red-500"}>{moneyText(amount)}</span>;
},
},
},
balanceAfter: {
@@ -43,7 +55,9 @@ export default function (): CreateCrudOptionsRet {
type: "number",
column: {
width: 130,
component: { name: PriceInput, vModel: "modelValue", edit: false },
cellRender({ value }) {
return <span class="text-red-500">{moneyText(Number(value || 0))}</span>;
},
},
},
remark: {
@@ -72,6 +72,7 @@ export default function (): CreateCrudOptionsRet {
title: "升级金额",
type: "number",
form: {
show: compute(({ form }) => form.levelType !== "exclusive"),
component: { name: PriceInput, vModel: "modelValue", edit: true },
rules: [{ required: true, message: "请输入升级金额" }],
},
@@ -100,7 +101,7 @@ export default function (): CreateCrudOptionsRet {
}),
form: {
value: "normal",
helper: "专属等级可由管理员手动指定,不参与普通用户自动升级。",
helper: "专属等级可由管理员手动指定,不参与普通用户自动升级。专属等级不会在普通用户端展示。",
},
column: { width: 120, align: "center" },
},
@@ -23,7 +23,6 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
initialForm: {
userId: row.userId,
levelId: row.levelId,
levelLocked: row.levelLocked === true,
},
columns: {
levelId: {
@@ -33,20 +32,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
form: {
col: { span: 24 },
rules: [{ required: true, message: "请选择推广等级" }],
},
},
levelLocked: {
title: "锁定等级",
type: "dict-switch",
dict: dict({
data: [
{ label: "自动升级", value: false, color: "success" },
{ label: "锁定", value: true, color: "warning" },
],
}),
form: {
col: { span: 24 },
helper: "专属等级会自动锁定,不参与自动升级。",
helper: "专属等级将锁定为当前等级,普通等级将按累计推广金额自动升级。",
},
},
},
@@ -80,8 +66,17 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
},
columns: {
userId: { title: "用户ID", type: "number", search: { show: true }, column: { width: 100 } },
username: { title: "用户名", type: "text", search: { show: true }, column: { width: 160 } },
userDisplay: { title: "显示名称", type: "text", column: { width: 160 } },
username: {
title: "用户名",
type: "text",
search: { show: true },
column: {
width: 180,
cellRender({ row }) {
return row.simpleUser?.displayName || row.userDisplay || row.username || row.userId;
},
},
},
enabled: {
title: "开通状态",
type: "dict-switch",
@@ -33,7 +33,8 @@
</div>
<div class="level-rate-label">佣金比例</div>
<div class="level-rate">{{ item.commissionRate || 0 }}%</div>
<div class="level-threshold">累计推广 {{ amountToYuan(item.minAmount) }} </div>
<div v-if="item.levelType === 'exclusive'" class="level-threshold exclusive-threshold">平台指定专属等级</div>
<div v-else class="level-threshold">累计推广 {{ amountToYuan(item.minAmount) }} </div>
<div class="level-meta">
<a-tag :color="item.disabled ? 'default' : 'success'">{{ item.disabled ? "已禁用" : "已启用" }}</a-tag>
<span>排序 {{ item.sort || 0 }}</span>
@@ -232,6 +233,10 @@ onActivated(() => {
text-align: center;
}
.exclusive-threshold {
color: #8a5a16;
}
.level-meta {
display: flex;
align-items: center;
@@ -26,11 +26,11 @@ export async function UpdateObj(obj: any) {
});
}
export async function DelObj(id: any) {
export async function CancelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
url: apiPrefix + "/cancel",
method: "post",
params: { id },
data: { id },
});
}
@@ -50,14 +50,6 @@ export async function GetDetail(id: any) {
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}
export async function UpdatePaid(id: any) {
return await request({
url: apiPrefix + "/updatePaid",
@@ -1,17 +1,10 @@
import * as api from "./api";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { Modal } from "ant-design-vue";
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
@@ -20,43 +13,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -99,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
@@ -67,7 +34,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
toolbar: { show: false },
rowHandle: {
width: 320,
width: 150,
fixed: "right",
buttons: {
view: {
@@ -79,11 +46,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
edit: {
show: false,
},
remove: {
show: false,
},
syncStatus: {
show: compute(({ row }) => {
return row.status === "wait_pay";
}),
text: "同步订单状态",
title: "同步订单状态",
text: null,
tooltip: { title: "同步订单状态" },
icon: "ant-design:sync-outlined",
type: "link",
click: async ({ row }) => {
Modal.confirm({
@@ -96,11 +69,36 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
},
},
cancel: {
show: compute(({ row }) => {
return row.status === "wait_pay";
}),
title: "取消订单",
text: null,
tooltip: { title: "取消订单" },
icon: "ion:close-circle-outline",
type: "link",
click({ row }) {
Modal.confirm({
title: "确认取消订单?",
content: "取消后订单会关闭,已冻结的余额抵扣金额将自动退回。",
okText: "确认取消",
cancelText: "再想想",
onOk: async () => {
await api.CancelObj(row.id);
await crudExpose.doRefresh();
},
});
},
},
updatePaid: {
show: compute(({ row }) => {
return row.status === "wait_pay";
}),
text: "确认已支付",
title: "确认已支付",
text: null,
tooltip: { title: "确认已支付" },
icon: "ant-design:check-circle-outlined",
type: "link",
click({ row }) {
Modal.confirm({
@@ -175,7 +173,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
rebateAmount: {
title: "返利抵扣",
title: "余额抵扣",
type: "number",
column: {
width: 110,
@@ -224,7 +222,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
{ label: "支付宝", value: "alipay" },
{ label: "微信", value: "wxpay" },
{ label: "免费", value: "free" },
{ label: "返利余额", value: "rebate" },
{ label: "余额抵扣", value: "rebate" },
],
}),
column: {
@@ -6,13 +6,7 @@
<span class="sub"> </span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
<fs-crud ref="crudRef" v-bind="crudBinding" />
</fs-page>
</template>
@@ -20,31 +14,11 @@
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
defineOptions({
name: "TradeManager",
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error("请先勾选记录");
}
};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {