Compare commits

...

2 Commits

Author SHA1 Message Date
xiaojunnuo b083b3cc41 style(trade crud): adjust order and style for order operation buttons
add order prop for syncStatus, cancel and updatePaid buttons, set cancel button to danger style for both trade pages, unify button display order logic
2026-05-29 01:00:14 +08:00
xiaojunnuo 6624769032 perf(trade): 优化商品购买页面的规格展示和折扣计算,支持订单取消 2026-05-29 00:56:30 +08:00
16 changed files with 460 additions and 241 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,35 @@ 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: {
order: 10,
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 +54,30 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
},
},
cancel: {
order: 99,
show: compute(({ row }) => {
return row.status === "wait_pay";
}),
title: "取消订单",
text: null,
tooltip: { title: "取消订单" },
icon: "ion:close-circle-outline",
type: "link",
danger: true,
click: async ({ row }) => {
Modal.confirm({
title: "确认取消订单?",
content: "取消后订单会关闭,已冻结的余额抵扣金额将自动退回。",
okText: "确认取消",
cancelText: "再想想",
onOk: async () => {
await api.CancelObj(row.id);
await crudExpose.doRefresh();
},
});
},
},
},
},
actionbar: {
@@ -152,7 +147,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
rebateAmount: {
title: "返利抵扣",
title: "余额抵扣",
type: "number",
column: {
width: 110,
@@ -201,7 +196,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,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
edit: {
show: false,
},
remove: {
show: false,
},
syncStatus: {
order: 10,
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 +70,39 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
},
},
updatePaid: {
cancel: {
order: 99,
show: compute(({ row }) => {
return row.status === "wait_pay";
}),
text: "确认已支付",
title: "取消订单",
text: null,
tooltip: { title: "取消订单" },
icon: "ion:close-circle-outline",
type: "link",
danger: true,
click({ row }) {
Modal.confirm({
title: "确认取消订单?",
content: "取消后订单会关闭,已冻结的余额抵扣金额将自动退回。",
okText: "确认取消",
cancelText: "再想想",
onOk: async () => {
await api.CancelObj(row.id);
await crudExpose.doRefresh();
},
});
},
},
updatePaid: {
order: 20,
show: compute(({ row }) => {
return row.status === "wait_pay";
}),
title: "确认已支付",
text: null,
tooltip: { title: "确认已支付" },
icon: "ant-design:check-circle-outlined",
type: "link",
click({ row }) {
Modal.confirm({
@@ -175,7 +177,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
rebateAmount: {
title: "返利抵扣",
title: "余额抵扣",
type: "number",
column: {
width: 110,
@@ -224,7 +226,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(() => {