mirror of
https://github.com/certd/certd.git
synced 2026-06-10 18:57:33 +08:00
feat: 商业版支持邀请推广功能
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<fs-page class="page-cert-dns-persist">
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="title">DNS持久验证记录</div>
|
||||
<div class="text-orange-500 mt-5">当前仅 Let's Encrypt 测试环境可以申请 DNS 持久验证证书。</div>
|
||||
<div class="title">
|
||||
DNS持久验证记录
|
||||
<span class="red sub" style="color: red">当前仅 Let's Encrypt 测试环境可以申请 DNS 持久验证证书。</span>
|
||||
</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
<div class="invite-info-row">
|
||||
<span class="info-label">我的等级:</span>
|
||||
<a-button type="link" class="level-button" @click="levelDialogOpen = true">
|
||||
<span v-if="inviteInfo.currentLevel" class="level-medal" :class="levelMedalClass(inviteInfo.currentLevel)">{{ levelMedal(inviteInfo.currentLevel) }}</span>
|
||||
<span v-if="inviteInfo.currentLevel" class="level-medal">
|
||||
<fs-icon :icon="levelIcon(inviteInfo.currentLevel)" />
|
||||
</span>
|
||||
<span>{{ inviteInfo.currentLevel?.name || "未设置" }}</span>
|
||||
<span v-if="inviteInfo.currentLevel" class="current-level-rate">{{ inviteInfo.currentLevel.commissionRate }}%</span>
|
||||
</a-button>
|
||||
@@ -61,9 +63,11 @@
|
||||
<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">
|
||||
<span class="level-medal" :class="levelMedalClass(level)">{{ levelMedal(level) }}</span>
|
||||
<span class="level-medal">
|
||||
<fs-icon :icon="levelIcon(level)" />
|
||||
</span>
|
||||
{{ level.name }}
|
||||
<a-tag v-if="level.isHidden" color="orange">专属</a-tag>
|
||||
<a-tag v-if="level.levelType === 'exclusive'" color="orange">专属</a-tag>
|
||||
</div>
|
||||
<div class="level-rate-label">佣金比例</div>
|
||||
<div class="level-rate">{{ level.commissionRate }}%</div>
|
||||
@@ -86,7 +90,8 @@
|
||||
@ok="handleAgreementOk"
|
||||
@cancel="closeAgreementDialog"
|
||||
>
|
||||
<div class="invite-agreement-content">{{ agreementText }}</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="invite-agreement-content editor-content-view" v-html="agreementText"></div>
|
||||
<div v-if="agreementDialogNeedOpen" class="invite-agreement-confirm">
|
||||
<a-checkbox v-model:checked="agreementAgree">我已阅读并同意推广协议</a-checkbox>
|
||||
</div>
|
||||
@@ -119,7 +124,7 @@ const agreementDialogOpen = ref(false);
|
||||
const agreementDialogNeedOpen = ref(false);
|
||||
const agreementAgree = ref(false);
|
||||
const agreementSubmitting = ref(false);
|
||||
const defaultAgreementContent = "请遵守平台推广规则,不得通过虚假注册、刷单、恶意诱导等方式获取收益。平台有权对异常推广行为进行核查,并根据实际情况暂停结算或关闭激励计划资格。";
|
||||
const defaultAgreementContent = "<p>请遵守平台推广规则,不得通过虚假注册、刷单、恶意诱导等方式获取收益。平台有权对异常推广行为进行核查,并根据实际情况暂停结算或关闭激励计划资格。</p>";
|
||||
|
||||
const inviteInfo = reactive<any>({
|
||||
enabled: false,
|
||||
@@ -170,38 +175,8 @@ const visibleLevels = computed(() => {
|
||||
|
||||
const agreementText = computed(() => inviteInfo.agreementContent?.trim() || defaultAgreementContent);
|
||||
|
||||
function levelMedal(level: any) {
|
||||
const name = `${level?.name || ""}`;
|
||||
if (name.includes("青铜")) {
|
||||
return "铜";
|
||||
}
|
||||
if (name.includes("白银")) {
|
||||
return "银";
|
||||
}
|
||||
if (name.includes("黄金")) {
|
||||
return "金";
|
||||
}
|
||||
if (name.includes("钻石")) {
|
||||
return "钻";
|
||||
}
|
||||
return name.slice(0, 1) || "L";
|
||||
}
|
||||
|
||||
function levelMedalClass(level: any) {
|
||||
const name = `${level?.name || ""}`;
|
||||
if (name.includes("青铜")) {
|
||||
return "bronze";
|
||||
}
|
||||
if (name.includes("白银")) {
|
||||
return "silver";
|
||||
}
|
||||
if (name.includes("黄金")) {
|
||||
return "gold";
|
||||
}
|
||||
if (name.includes("钻石")) {
|
||||
return "diamond";
|
||||
}
|
||||
return "default";
|
||||
function levelIcon(level: any) {
|
||||
return level?.icon || "ion:ribbon-outline";
|
||||
}
|
||||
|
||||
function openAgreementDialog(needOpenPlan: boolean) {
|
||||
@@ -419,33 +394,10 @@ onActivated(async () => {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #f4d7a1;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #8a5a16;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.level-medal.bronze {
|
||||
background: #f5d6b7;
|
||||
color: #9a5b22;
|
||||
}
|
||||
|
||||
.level-medal.silver {
|
||||
background: #e5e7eb;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.level-medal.gold {
|
||||
background: #f8df9b;
|
||||
color: #926c15;
|
||||
}
|
||||
|
||||
.level-medal.diamond {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.invite-tabs {
|
||||
@@ -520,33 +472,10 @@ onActivated(async () => {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #f4d7a1;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #8a5a16;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.level-medal.bronze {
|
||||
background: #f5d6b7;
|
||||
color: #9a5b22;
|
||||
}
|
||||
|
||||
.level-medal.silver {
|
||||
background: #e5e7eb;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.level-medal.gold {
|
||||
background: #f8df9b;
|
||||
color: #926c15;
|
||||
}
|
||||
|
||||
.level-medal.diamond {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.level-rate-label {
|
||||
@@ -591,11 +520,19 @@ onActivated(async () => {
|
||||
max-height: 360px;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 6px;
|
||||
background: hsl(var(--card));
|
||||
line-height: 1.7;
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-agreement-confirm {
|
||||
|
||||
@@ -8,6 +8,10 @@ export async function GetWithdrawSetting() {
|
||||
return await request({ url: "/wallet/withdraw/setting/get", method: "post" });
|
||||
}
|
||||
|
||||
export async function GetWalletSetting() {
|
||||
return await request({ url: "/wallet/settings/get", method: "post" });
|
||||
}
|
||||
|
||||
export async function SaveWithdrawSetting(data: any) {
|
||||
return await request({ url: "/wallet/withdraw/setting/save", method: "post", data });
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
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 { useUserStore } from "/@/store/user";
|
||||
|
||||
function buildPrivateFileUrl(key: string) {
|
||||
const userStore = useUserStore();
|
||||
return `/api/basic/file/download?token=${userStore.getToken}&key=${encodeURIComponent(key)}`;
|
||||
}
|
||||
|
||||
export default function (): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
@@ -53,22 +47,6 @@ export default function (): CreateCrudOptionsRet {
|
||||
}),
|
||||
column: { width: 110 },
|
||||
},
|
||||
realName: { title: "真实姓名", type: "text", column: { width: 120 } },
|
||||
account: { title: "收款账号", type: "text", column: { width: 180 } },
|
||||
bankName: { title: "开户银行", type: "text", column: { width: 160 } },
|
||||
qrCode: {
|
||||
title: "收款二维码",
|
||||
type: "text",
|
||||
column: {
|
||||
width: 120,
|
||||
cellRender({ value }) {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
return <a-image src={buildPrivateFileUrl(value)} width={48} />;
|
||||
},
|
||||
},
|
||||
},
|
||||
auditRemark: { title: "审核备注", type: "text", column: { minWidth: 180 } },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,19 +6,11 @@
|
||||
<div class="wallet-body">
|
||||
<div class="wallet-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>
|
||||
</div>
|
||||
|
||||
<div class="wallet-action-panel">
|
||||
<div class="wallet-action-title">提现操作</div>
|
||||
<div class="wallet-action-content">
|
||||
<a-button class="wallet-action-button" type="primary" @click="openWithdrawSetting">提现设置</a-button>
|
||||
<div class="withdraw-amount-field">
|
||||
<a-input-number v-model:value="withdrawAmountYuan" class="withdraw-amount-input" :min="0" addon-before="提现金额" addon-after="元" />
|
||||
<div class="summary-card-main">
|
||||
<div class="summary-title">{{ item.title }}</div>
|
||||
<div class="summary-value" :class="item.className">{{ item.value }}</div>
|
||||
</div>
|
||||
<a-button class="wallet-action-button" type="primary" @click="applyWithdraw">申请提现</a-button>
|
||||
<a-button v-if="item.key === 'availableAmount'" class="summary-action-button" type="primary" @click="openWithdrawDialog">申请提现</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,9 +27,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onActivated, onMounted, reactive, ref } from "vue";
|
||||
import { computed, h, onActivated, onMounted, reactive, ref } from "vue";
|
||||
import { compute, dict, useFs } from "@fast-crud/fast-crud";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { Button, notification } from "ant-design-vue";
|
||||
import * as api from "./api";
|
||||
import createLogsCrudOptions from "./crud-logs";
|
||||
import createWithdrawCrudOptions from "./crud-withdraw";
|
||||
@@ -48,7 +40,6 @@ import { useUserStore } from "/@/store/user";
|
||||
defineOptions({ name: "MyWallet" });
|
||||
|
||||
const summary = reactive<any>({ availableAmount: 0, frozenAmount: 0, totalIncomeAmount: 0, totalWithdrawAmount: 0 });
|
||||
const withdrawAmountYuan = ref(0);
|
||||
const loaded = ref(false);
|
||||
const activeTab = ref("withdraw");
|
||||
const { openFormDialog } = useFormDialog();
|
||||
@@ -101,8 +92,18 @@ async function loadWalletSummary() {
|
||||
}
|
||||
|
||||
async function openWithdrawSetting() {
|
||||
const setting: any = await api.GetWithdrawSetting();
|
||||
const [setting, walletSetting]: any[] = await Promise.all([api.GetWithdrawSetting(), api.GetWalletSetting()]);
|
||||
const enabledChannels = walletSetting?.withdrawChannels?.length ? walletSetting.withdrawChannels : ["alipay", "bank"];
|
||||
const enabledBanks = walletSetting?.withdrawBanks?.length ? walletSetting.withdrawBanks : [];
|
||||
const channelOptions = [
|
||||
{ label: "支付宝", value: "alipay" },
|
||||
{ label: "银行卡", value: "bank" },
|
||||
].filter(item => enabledChannels.includes(item.value));
|
||||
const bankOptions = enabledBanks.map((item: string) => ({ label: item, value: item }));
|
||||
const initialForm = Object.assign({ channel: "alipay", realName: "", account: "", bankName: "" }, setting || {});
|
||||
if (!enabledChannels.includes(initialForm.channel)) {
|
||||
initialForm.channel = enabledChannels[0] || "alipay";
|
||||
}
|
||||
await openFormDialog({
|
||||
title: "提现设置",
|
||||
wrapper: {
|
||||
@@ -114,10 +115,7 @@ async function openWithdrawSetting() {
|
||||
title: "提现渠道",
|
||||
type: "dict-radio",
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: "支付宝", value: "alipay" },
|
||||
{ label: "银行卡", value: "bank" },
|
||||
],
|
||||
data: channelOptions,
|
||||
}),
|
||||
form: {
|
||||
col: { span: 24 },
|
||||
@@ -142,19 +140,13 @@ async function openWithdrawSetting() {
|
||||
},
|
||||
qrCode: {
|
||||
title: "收款二维码",
|
||||
type: "cropper-uploader",
|
||||
type: "avatar-uploader",
|
||||
form: {
|
||||
col: { span: 24 },
|
||||
helper: "上传支付宝收款二维码图片",
|
||||
show: compute(({ form }) => form.channel !== "bank"),
|
||||
component: {
|
||||
vModel: "modelValue",
|
||||
valueType: "key",
|
||||
cropper: {
|
||||
aspectRatio: 1,
|
||||
autoCropArea: 1,
|
||||
viewMode: 0,
|
||||
},
|
||||
onReady: null,
|
||||
uploader: {
|
||||
type: "form",
|
||||
action: "/basic/file/upload?token=" + userStore.getToken,
|
||||
@@ -174,10 +166,16 @@ async function openWithdrawSetting() {
|
||||
},
|
||||
bankName: {
|
||||
title: "开户银行",
|
||||
type: "text",
|
||||
form: {
|
||||
col: { span: 24 },
|
||||
show: compute(({ form }) => form.channel === "bank"),
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: bankOptions,
|
||||
showSearch: true,
|
||||
placeholder: "请选择开户银行",
|
||||
},
|
||||
rules: [{ required: compute(({ form }) => form.channel === "bank"), message: "请输入开户银行" }],
|
||||
},
|
||||
},
|
||||
@@ -192,9 +190,54 @@ async function openWithdrawSetting() {
|
||||
});
|
||||
}
|
||||
|
||||
async function applyWithdraw() {
|
||||
await api.ApplyWithdraw(util.amount.toCent(withdrawAmountYuan.value || 0));
|
||||
withdrawAmountYuan.value = 0;
|
||||
async function openWithdrawDialog() {
|
||||
await openFormDialog({
|
||||
title: "申请提现",
|
||||
wrapper: {
|
||||
width: 520,
|
||||
},
|
||||
initialForm: {
|
||||
amountYuan: null,
|
||||
},
|
||||
body: () =>
|
||||
h("div", { class: "withdraw-dialog-tip" }, [
|
||||
h("span", "提现前需要先设置提现账号。"),
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
size: "small",
|
||||
type: "link",
|
||||
onClick: openWithdrawSetting,
|
||||
},
|
||||
() => "提现设置"
|
||||
),
|
||||
]),
|
||||
columns: {
|
||||
amountYuan: {
|
||||
title: "提现金额",
|
||||
form: {
|
||||
col: { span: 24 },
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
vModel: "value",
|
||||
min: 0,
|
||||
precision: 2,
|
||||
addonAfter: "元",
|
||||
style: { width: "100%" },
|
||||
},
|
||||
rules: [{ required: true, message: "请输入提现金额" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
async onSubmit(form: any) {
|
||||
await applyWithdraw(form.amountYuan);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function applyWithdraw(amountYuan: number) {
|
||||
await api.ApplyWithdraw(util.amount.toCent(amountYuan || 0));
|
||||
activeTab.value = "withdraw";
|
||||
await loadWalletSummary();
|
||||
await Promise.all([withdrawCrudExpose.doRefresh(), logsCrudExpose.doRefresh()]);
|
||||
notification.success({ message: "提现申请已提交" });
|
||||
@@ -243,8 +286,7 @@ onActivated(async () => {
|
||||
background: hsl(var(--background-deep));
|
||||
}
|
||||
|
||||
.wallet-summary-grid,
|
||||
.wallet-action-panel {
|
||||
.wallet-summary-grid {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
@@ -256,7 +298,6 @@ onActivated(async () => {
|
||||
}
|
||||
|
||||
.summary-card,
|
||||
.wallet-action-panel,
|
||||
.wallet-tabs {
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
@@ -265,10 +306,18 @@ onActivated(async () => {
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 112px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.summary-card-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
margin-bottom: 10px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
@@ -294,45 +343,8 @@ onActivated(async () => {
|
||||
color: #3478f6;
|
||||
}
|
||||
|
||||
.wallet-action-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 16px;
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.wallet-action-title {
|
||||
.summary-action-button {
|
||||
flex: none;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wallet-action-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wallet-action-button {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.withdraw-amount-field {
|
||||
flex: 0 1 280px;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.withdraw-amount-field,
|
||||
.withdraw-amount-input,
|
||||
.withdraw-amount-input.ant-input-number-group-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wallet-tabs {
|
||||
@@ -373,24 +385,23 @@ onActivated(async () => {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wallet-action-panel {
|
||||
align-items: stretch;
|
||||
.summary-card {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wallet-action-content {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.wallet-action-button,
|
||||
.withdraw-amount-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.withdraw-amount-field {
|
||||
flex-basis: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.withdraw-dialog-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #d9e8ff;
|
||||
border-radius: 6px;
|
||||
background: #f5f9ff;
|
||||
color: #315174;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,6 +24,10 @@ export async function UpdateLevel(data: any) {
|
||||
return await request({ url: "/sys/invite/level/update", method: "post", data });
|
||||
}
|
||||
|
||||
export async function DeleteLevel(id: number) {
|
||||
return await request({ url: "/sys/invite/level/delete", method: "post", params: { id } });
|
||||
}
|
||||
|
||||
export async function GetUserLevels(query: any) {
|
||||
return await request({ url: "/sys/invite/user/page", method: "post", data: query });
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import PriceInput from "/@/views/sys/suite/product/price-input.vue";
|
||||
|
||||
export default function (): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
query.sort = { prop: "sort", asc: true };
|
||||
return await api.GetLevels(query);
|
||||
};
|
||||
const addRequest = async ({ form }: AddReq) => {
|
||||
@@ -14,8 +15,7 @@ export default function (): CreateCrudOptionsRet {
|
||||
return await api.UpdateLevel(form);
|
||||
};
|
||||
const delRequest = async ({ row }: DelReq) => {
|
||||
row.disabled = true;
|
||||
return await api.UpdateLevel(row);
|
||||
return await api.DeleteLevel(row.id);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -49,6 +49,25 @@ export default function (): CreateCrudOptionsRet {
|
||||
},
|
||||
column: { width: 140 },
|
||||
},
|
||||
icon: {
|
||||
title: "等级图标",
|
||||
type: "icon",
|
||||
form: {
|
||||
value: "ion:ribbon-outline",
|
||||
rules: [{ required: true, message: "请选择等级图标" }],
|
||||
},
|
||||
column: {
|
||||
width: 90,
|
||||
align: "center",
|
||||
component: {
|
||||
name: "fs-icon",
|
||||
vModel: "icon",
|
||||
style: {
|
||||
fontSize: "22px",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
minAmount: {
|
||||
title: "升级金额",
|
||||
type: "number",
|
||||
@@ -70,23 +89,29 @@ export default function (): CreateCrudOptionsRet {
|
||||
},
|
||||
column: { width: 110, align: "center", cellRender: ({ value }) => `${value || 0}%` },
|
||||
},
|
||||
isHidden: {
|
||||
title: "隐藏等级",
|
||||
type: "dict-switch",
|
||||
levelType: {
|
||||
title: "等级类型",
|
||||
type: "dict-radio",
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: "普通等级", value: false, color: "success" },
|
||||
{ label: "隐藏等级", value: true, color: "warning" },
|
||||
{ label: "普通等级", value: "normal", color: "success" },
|
||||
{ label: "专属等级", value: "exclusive", color: "warning" },
|
||||
],
|
||||
}),
|
||||
form: { value: false },
|
||||
form: {
|
||||
value: "normal",
|
||||
helper: "专属等级可由管理员手动指定,不参与普通用户自动升级。",
|
||||
},
|
||||
column: { width: 120, align: "center" },
|
||||
},
|
||||
sort: {
|
||||
title: "排序",
|
||||
type: "number",
|
||||
form: { value: 10 },
|
||||
column: { width: 90, align: "center" },
|
||||
form: {
|
||||
value: 10,
|
||||
helper: "排序号越小越靠前。",
|
||||
},
|
||||
column: { width: 90, align: "center", sorter: true },
|
||||
},
|
||||
disabled: {
|
||||
title: "状态",
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
||||
}),
|
||||
form: {
|
||||
col: { span: 24 },
|
||||
helper: "隐藏等级会自动锁定,不参与自动升级。",
|
||||
helper: "专属等级会自动锁定,不参与自动升级。",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -111,13 +111,13 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
||||
}),
|
||||
column: { width: 110, align: "center" },
|
||||
},
|
||||
isHidden: {
|
||||
title: "隐藏等级",
|
||||
type: "dict-switch",
|
||||
levelType: {
|
||||
title: "等级类型",
|
||||
type: "dict-select",
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: "否", value: false, color: "default" },
|
||||
{ label: "是", value: true, color: "warning" },
|
||||
{ label: "普通等级", value: "normal", color: "success" },
|
||||
{ label: "专属等级", value: "exclusive", color: "warning" },
|
||||
],
|
||||
}),
|
||||
column: { width: 100, align: "center", show: compute(({ row }) => row.levelId) },
|
||||
|
||||
@@ -18,16 +18,17 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
||||
};
|
||||
|
||||
function renderWithdrawDetail(row: any) {
|
||||
const isBank = row.channel === "bank";
|
||||
return (
|
||||
<a-descriptions class={"w-full"} bordered column={2} size={"small"}>
|
||||
<a-descriptions class={"w-full"} bordered column={1} size={"small"}>
|
||||
<a-descriptions-item label="提现金额">
|
||||
<span class={"text-red-500"}>{row.amount / 100} 元</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="渠道类型">{row.channel === "bank" ? "银行卡" : "支付宝"}</a-descriptions-item>
|
||||
<a-descriptions-item label="用户名">{row.userDisplay || row.userId}</a-descriptions-item>
|
||||
<a-descriptions-item label="账号">{row.account || "-"}</a-descriptions-item>
|
||||
<a-descriptions-item label="开户行名称">{row.bankName || "-"}</a-descriptions-item>
|
||||
<a-descriptions-item label="收款二维码" span={2}>
|
||||
{row.qrCode ? <a-image src={buildPrivateFileUrl(row.qrCode)} width={160} /> : <span>-</span>}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提现金额">{row.amount / 100} 元</a-descriptions-item>
|
||||
{isBank ? <a-descriptions-item label="开户行名称">{row.bankName || "-"}</a-descriptions-item> : null}
|
||||
{!isBank ? <a-descriptions-item label="收款二维码">{row.qrCode ? <a-image src={buildPrivateFileUrl(row.qrCode)} width={160} /> : <span>-</span>}</a-descriptions-item> : null}
|
||||
</a-descriptions>
|
||||
);
|
||||
}
|
||||
@@ -37,6 +38,11 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
||||
title: "提现审核",
|
||||
wrapper: {
|
||||
width: 760,
|
||||
buttons: {
|
||||
ok: {
|
||||
text: "确认已转账完成",
|
||||
},
|
||||
},
|
||||
},
|
||||
body: () => renderWithdrawDetail(row),
|
||||
onSubmit: async () => {
|
||||
@@ -153,17 +159,16 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
||||
},
|
||||
realName: { title: "真实姓名", type: "text", search: { show: true }, column: { width: 120 } },
|
||||
account: { title: "收款账号", type: "text", column: { width: 180 } },
|
||||
bankName: { title: "开户银行", type: "text", column: { width: 160 } },
|
||||
qrCode: {
|
||||
title: "收款二维码",
|
||||
bankName: {
|
||||
title: "开户银行",
|
||||
type: "text",
|
||||
column: {
|
||||
width: 120,
|
||||
cellRender({ value }) {
|
||||
if (!value) {
|
||||
width: 160,
|
||||
cellRender({ row, value }) {
|
||||
if (row.channel !== "bank") {
|
||||
return "-";
|
||||
}
|
||||
return <a-image src={buildPrivateFileUrl(value)} width={48} />;
|
||||
return value || "-";
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,18 +3,94 @@
|
||||
<template #header>
|
||||
<div class="title">推广等级</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<a-empty v-if="levelList.length === 0" class="level-empty" />
|
||||
<div v-else class="level-card-grid">
|
||||
<div v-for="(item, index) of levelList" :key="item.id" class="level-card" :class="{ disabled: item.disabled }">
|
||||
<div class="level-card-actions">
|
||||
<a-tooltip title="编辑">
|
||||
<a-button type="text" size="small" @click="openEdit({ index, row: item })">
|
||||
<template #icon><fs-icon icon="ion:create-outline" /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="item.disabled ? '启用' : '禁用'">
|
||||
<a-button type="text" size="small" @click="toggleDisabled(item)">
|
||||
<template #icon><fs-icon :icon="item.disabled ? 'ion:play-outline' : 'ion:pause-outline'" /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="删除">
|
||||
<a-button type="text" danger size="small" @click="confirmRemove({ index, row: item })">
|
||||
<template #icon><fs-icon icon="ion:trash-outline" /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="level-name">
|
||||
<span class="level-medal">
|
||||
<fs-icon :icon="levelIcon(item)" />
|
||||
</span>
|
||||
{{ item.name }}
|
||||
<a-tag v-if="item.levelType === 'exclusive'" color="orange">专属</a-tag>
|
||||
</div>
|
||||
<div class="level-rate-label">佣金比例</div>
|
||||
<div class="level-rate">{{ item.commissionRate || 0 }}%</div>
|
||||
<div 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onActivated, onMounted } from "vue";
|
||||
import { computed, onActivated, onMounted } from "vue";
|
||||
import { Modal, notification } from "ant-design-vue";
|
||||
import { useFs } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud-level";
|
||||
import * as api from "./api";
|
||||
import { util } from "/@/utils";
|
||||
|
||||
defineOptions({ name: "SysInviteLevel" });
|
||||
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||
const levelList = computed(() => crudBinding.value?.data || []);
|
||||
|
||||
function amountToYuan(amount: number) {
|
||||
return util.amount.toYuan(amount || 0);
|
||||
}
|
||||
|
||||
function levelIcon(level: any) {
|
||||
return level?.icon || "ion:ribbon-outline";
|
||||
}
|
||||
|
||||
function openEdit(opts: any) {
|
||||
crudExpose.openEdit(opts);
|
||||
}
|
||||
|
||||
async function toggleDisabled(row: any) {
|
||||
await api.UpdateLevel({
|
||||
...row,
|
||||
disabled: !row.disabled,
|
||||
});
|
||||
notification.success({ message: row.disabled ? "已启用" : "已禁用" });
|
||||
await crudExpose.doRefresh();
|
||||
}
|
||||
|
||||
function confirmRemove(opts: any) {
|
||||
Modal.confirm({
|
||||
title: "确认删除推广等级?",
|
||||
content: "删除后不可恢复。如果该等级已被用户使用,可能会出现异常,请确认已完成数据处理。",
|
||||
okText: "确认删除",
|
||||
okType: "danger",
|
||||
onOk: async () => {
|
||||
await api.DeleteLevel(opts.row.id);
|
||||
notification.success({ message: "已删除" });
|
||||
await crudExpose.doRefresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
@@ -23,3 +99,130 @@ onActivated(() => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.page-sys-invite-level {
|
||||
.fs-crud-table {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.level-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.level-empty {
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.level-card {
|
||||
position: relative;
|
||||
min-height: 156px;
|
||||
padding: 16px;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
background: hsl(var(--card));
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background-color 0.2s;
|
||||
}
|
||||
|
||||
.level-card.disabled {
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
.level-card-actions {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
|
||||
.ant-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.fs-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.level-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 26px;
|
||||
padding: 0 72px;
|
||||
gap: 6px;
|
||||
color: hsl(var(--foreground));
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.level-medal {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #8a5a16;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.level-rate-label {
|
||||
margin-top: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.level-rate {
|
||||
margin-top: 2px;
|
||||
color: #c58a35;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.level-threshold {
|
||||
margin-top: 6px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.level-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.page-sys-invite-level {
|
||||
.level-card-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.page-sys-invite-level {
|
||||
.level-card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,16 +8,26 @@
|
||||
<a-form-item label="开启激励计划" name="enabled">
|
||||
<a-switch v-model:checked="settings.enabled" />
|
||||
</a-form-item>
|
||||
<a-form-item label="推广协议" name="agreementContent">
|
||||
<a-textarea v-model:value="settings.agreementContent" :rows="10" placeholder="请输入用户开通激励计划前需要确认的推广协议内容" />
|
||||
</a-form-item>
|
||||
<a-form-item label="最低提现金额" name="minWithdrawAmountYuan">
|
||||
<a-input-number v-model:value="settings.minWithdrawAmountYuan" :min="0" addon-after="元" />
|
||||
</a-form-item>
|
||||
<a-form-item label="提现渠道" name="withdrawChannels">
|
||||
<a-checkbox-group v-model:value="settings.withdrawChannels" :options="withdrawChannelOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-form-item v-if="bankChannelEnabled" label="开户银行" name="withdrawBanks">
|
||||
<a-select v-model:value="settings.withdrawBanks" mode="tags" :options="bankOptions" placeholder="请选择或输入支持的开户银行" :token-separators="[',', ',', '、']" />
|
||||
</a-form-item>
|
||||
<a-form-item label="推广协议" name="agreementContent">
|
||||
<fs-editor-wang5
|
||||
v-model="settings.agreementContent"
|
||||
:toolbar-config="{}"
|
||||
:editor-config="{ placeholder: '请输入用户开通激励计划前需要确认的推广协议内容' }"
|
||||
:uploader="editorUploader"
|
||||
:container="{ class: 'agreement-editor' }"
|
||||
style="height: 400px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label=" " :colon="false">
|
||||
<a-button type="primary" @click="saveSettings">保存设置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -26,20 +36,55 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
import { notification } from "ant-design-vue";
|
||||
import * as api from "./api";
|
||||
import { util } from "/@/utils";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import { useUserStore } from "/@/store/user";
|
||||
|
||||
defineOptions({ name: "SysInviteCommissionSetting" });
|
||||
|
||||
const defaultAgreement = "请遵守平台推广规则,不得通过虚假注册、刷单、恶意诱导等方式获取收益。平台有权对异常推广行为进行核查,并根据实际情况暂停结算或关闭激励计划资格。";
|
||||
const settings = reactive<any>({ enabled: false, agreementContent: "", minWithdrawAmountYuan: 0, withdrawChannels: ["alipay", "bank"] });
|
||||
const defaultAgreement = "<p>请遵守平台推广规则,不得通过虚假注册、刷单、恶意诱导等方式获取收益。平台有权对异常推广行为进行核查,并根据实际情况暂停结算或关闭激励计划资格。</p>";
|
||||
const defaultWithdrawBanks = [
|
||||
"中国工商银行",
|
||||
"中国农业银行",
|
||||
"中国银行",
|
||||
"中国建设银行",
|
||||
"交通银行",
|
||||
"招商银行",
|
||||
"中国邮政储蓄银行",
|
||||
"中信银行",
|
||||
"中国光大银行",
|
||||
"华夏银行",
|
||||
"中国民生银行",
|
||||
"广发银行",
|
||||
"平安银行",
|
||||
"兴业银行",
|
||||
"浦发银行",
|
||||
];
|
||||
const settings = reactive<any>({ enabled: false, agreementContent: "", minWithdrawAmountYuan: 0, withdrawChannels: ["alipay", "bank"], withdrawBanks: defaultWithdrawBanks });
|
||||
const withdrawChannelOptions = [
|
||||
{ label: "支付宝", value: "alipay" },
|
||||
{ label: "银行卡", value: "bank" },
|
||||
];
|
||||
const bankOptions = computed(() => defaultWithdrawBanks.map(item => ({ label: item, value: item })));
|
||||
const bankChannelEnabled = computed(() => settings.withdrawChannels?.includes("bank"));
|
||||
const userStore = useUserStore();
|
||||
const editorUploader = {
|
||||
type: "form",
|
||||
action: "/basic/file/upload?autoSave=true&token=" + userStore.getToken,
|
||||
name: "file",
|
||||
headers: {
|
||||
Authorization: "Bearer " + userStore.getToken,
|
||||
},
|
||||
successHandle(res: any) {
|
||||
return res;
|
||||
},
|
||||
buildUrl(res: any) {
|
||||
return res.url || `/api/basic/file/download?key=${encodeURIComponent(res.key)}`;
|
||||
},
|
||||
};
|
||||
|
||||
async function loadSettings() {
|
||||
const data: any = await api.GetSettings();
|
||||
@@ -47,19 +92,34 @@ async function loadSettings() {
|
||||
settings.agreementContent = data?.agreementContent || defaultAgreement;
|
||||
settings.minWithdrawAmountYuan = util.amount.toYuan(data?.minWithdrawAmount || 0);
|
||||
settings.withdrawChannels = data?.withdrawChannels?.length ? data.withdrawChannels : ["alipay", "bank"];
|
||||
settings.withdrawBanks = data?.withdrawBanks?.length ? data.withdrawBanks : defaultWithdrawBanks;
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const withdrawBanks = bankChannelEnabled.value ? (settings.withdrawBanks || []).map((item: string) => item?.trim()).filter(Boolean) : [];
|
||||
if (isBlankAgreement(settings.agreementContent)) {
|
||||
notification.warning({ message: "请填写推广协议内容" });
|
||||
return;
|
||||
}
|
||||
await api.SaveSettings({
|
||||
enabled: settings.enabled,
|
||||
agreementContent: settings.agreementContent || "",
|
||||
minWithdrawAmount: util.amount.toCent(settings.minWithdrawAmountYuan || 0),
|
||||
withdrawChannels: settings.withdrawChannels || [],
|
||||
withdrawBanks,
|
||||
});
|
||||
await useSettingStore().loadSysSettings();
|
||||
notification.success({ message: "保存成功" });
|
||||
}
|
||||
|
||||
function isBlankAgreement(content: string) {
|
||||
const text = `${content || ""}`
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/ /g, "")
|
||||
.trim();
|
||||
return !text;
|
||||
}
|
||||
|
||||
onMounted(loadSettings);
|
||||
</script>
|
||||
|
||||
@@ -71,5 +131,8 @@ onMounted(loadSettings);
|
||||
.settings-form {
|
||||
max-width: 860px;
|
||||
}
|
||||
.agreement-editor {
|
||||
min-height: 420px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex-o price-input">
|
||||
<a-input-number v-if="edit" prefix="¥" :value="priceValue" :precision="2" class="ml-5" @update:value="onPriceChange"> </a-input-number>
|
||||
<a-input-number v-if="edit" prefix="¥" :value="priceValue" :precision="2" class="price-input-number" @update:value="onPriceChange"> </a-input-number>
|
||||
<span v-else class="price-text" :style="style">{{ priceLabel }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -55,6 +55,12 @@ const onPriceChange = (price: number) => {
|
||||
|
||||
<style lang="less">
|
||||
.price-input {
|
||||
width: 100%;
|
||||
|
||||
.price-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ CREATE TABLE `cd_invite_level`
|
||||
(
|
||||
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||
`name` varchar(100),
|
||||
`icon` varchar(100) NOT NULL DEFAULT 'fluent-emoji-flat:2nd-place-medal',
|
||||
`sort` bigint NOT NULL DEFAULT 0,
|
||||
`min_amount` bigint NOT NULL DEFAULT 0,
|
||||
`commission_rate` bigint NOT NULL DEFAULT 0,
|
||||
`is_hidden` boolean NOT NULL DEFAULT false,
|
||||
`level_type` varchar(30) NOT NULL DEFAULT 'normal',
|
||||
`disabled` boolean NOT NULL DEFAULT false,
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
@@ -28,8 +29,8 @@ CREATE TABLE `cd_invite_user_plan`
|
||||
);
|
||||
CREATE UNIQUE INDEX `index_invite_user_plan_user_id` ON `cd_invite_user_plan` (`user_id`);
|
||||
|
||||
INSERT INTO `cd_invite_level` (`name`, `sort`, `min_amount`, `commission_rate`, `is_hidden`, `disabled`)
|
||||
VALUES ('青铜', 10, 0, 10, false, false),
|
||||
('白银', 20, 100000, 15, false, false),
|
||||
('黄金', 30, 500000, 20, false, false),
|
||||
('钻石', 40, 1000000, 30, false, false);
|
||||
INSERT INTO `cd_invite_level` (`name`, `icon`, `sort`, `min_amount`, `commission_rate`, `level_type`, `disabled`)
|
||||
VALUES ('青铜', 'fluent-emoji-flat:2nd-place-medal', 10, 0, 10, 'normal', false),
|
||||
('白银', 'fluent-emoji-flat:1st-place-medal', 20, 100000, 15, 'normal', false),
|
||||
('黄金', 'fluent-emoji-flat:3rd-place-medal', 30, 500000, 20, 'normal', false),
|
||||
('钻石', 'streamline-color:diamond-2', 40, 1000000, 30, 'normal', false);
|
||||
|
||||
@@ -5,10 +5,11 @@ CREATE TABLE "cd_invite_level"
|
||||
(
|
||||
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||
"name" varchar(100),
|
||||
"icon" varchar(100) NOT NULL DEFAULT 'fluent-emoji-flat:2nd-place-medal',
|
||||
"sort" bigint NOT NULL DEFAULT 0,
|
||||
"min_amount" bigint NOT NULL DEFAULT 0,
|
||||
"commission_rate" bigint NOT NULL DEFAULT 0,
|
||||
"is_hidden" boolean NOT NULL DEFAULT (false),
|
||||
"level_type" varchar(30) NOT NULL DEFAULT 'normal',
|
||||
"disabled" boolean NOT NULL DEFAULT (false),
|
||||
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||
@@ -28,8 +29,8 @@ CREATE TABLE "cd_invite_user_plan"
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_invite_user_plan_user_id" ON "cd_invite_user_plan" ("user_id");
|
||||
|
||||
INSERT INTO "cd_invite_level" ("name", "sort", "min_amount", "commission_rate", "is_hidden", "disabled")
|
||||
VALUES ('青铜', 10, 0, 10, false, false),
|
||||
('白银', 20, 100000, 15, false, false),
|
||||
('黄金', 30, 500000, 20, false, false),
|
||||
('钻石', 40, 1000000, 30, false, false);
|
||||
INSERT INTO "cd_invite_level" ("name", "icon", "sort", "min_amount", "commission_rate", "level_type", "disabled")
|
||||
VALUES ('青铜', 'fluent-emoji-flat:2nd-place-medal', 10, 0, 10, 'normal', false),
|
||||
('白银', 'fluent-emoji-flat:1st-place-medal', 20, 100000, 15, 'normal', false),
|
||||
('黄金', 'fluent-emoji-flat:3rd-place-medal', 30, 500000, 20, 'normal', false),
|
||||
('钻石', 'streamline-color:diamond-2', 40, 1000000, 30, 'normal', false);
|
||||
|
||||
@@ -5,10 +5,11 @@ CREATE TABLE "cd_invite_level"
|
||||
(
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"name" varchar(100),
|
||||
"icon" varchar(100) NOT NULL DEFAULT 'fluent-emoji-flat:2nd-place-medal',
|
||||
"sort" integer NOT NULL DEFAULT 0,
|
||||
"min_amount" integer NOT NULL DEFAULT 0,
|
||||
"commission_rate" integer NOT NULL DEFAULT 0,
|
||||
"is_hidden" boolean NOT NULL DEFAULT (false),
|
||||
"level_type" varchar(30) NOT NULL DEFAULT 'normal',
|
||||
"disabled" boolean NOT NULL DEFAULT (false),
|
||||
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||
@@ -28,8 +29,8 @@ CREATE TABLE "cd_invite_user_plan"
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_invite_user_plan_user_id" ON "cd_invite_user_plan" ("user_id");
|
||||
|
||||
INSERT INTO "cd_invite_level" ("name", "sort", "min_amount", "commission_rate", "is_hidden", "disabled")
|
||||
VALUES ('青铜', 10, 0, 10, false, false),
|
||||
('白银', 20, 100000, 15, false, false),
|
||||
('黄金', 30, 500000, 20, false, false),
|
||||
('钻石', 40, 1000000, 30, false, false);
|
||||
INSERT INTO "cd_invite_level" ("name", "icon", "sort", "min_amount", "commission_rate", "level_type", "disabled")
|
||||
VALUES ('青铜', 'fluent-emoji-flat:2nd-place-medal', 10, 0, 10, 'normal', false),
|
||||
('白银', 'fluent-emoji-flat:1st-place-medal', 20, 100000, 15, 'normal', false),
|
||||
('黄金', 'fluent-emoji-flat:3rd-place-medal', 30, 500000, 20, 'normal', false),
|
||||
('钻石', 'streamline-color:diamond-2', 40, 1000000, 30, 'normal', false);
|
||||
|
||||
@@ -137,6 +137,7 @@ export class MainConfiguration {
|
||||
});
|
||||
|
||||
logger.info('当前环境:', this.app.getEnv()); // prod
|
||||
// throw new Error("address family not supported")
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getImageDownloadOptions, isImageFile } from "./file-controller.js";
|
||||
import { FileController, getImageDownloadOptions, isImageFile } from "./file-controller.js";
|
||||
|
||||
describe("FileController.isImageFile", () => {
|
||||
it("detects uploaded logo image files", () => {
|
||||
@@ -37,3 +37,23 @@ describe("FileController.isImageFile", () => {
|
||||
assert.equal(getImageDownloadOptions("data/upload/private/user/cert.pem"), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FileController.upload", () => {
|
||||
it("auto saves uploaded file to public directory when autoSave is true", async () => {
|
||||
const controller = new FileController();
|
||||
controller.fileService = {
|
||||
async saveFile(userId: number, key: string, permission: string) {
|
||||
assert.equal(userId, 1);
|
||||
assert.equal(permission, "public");
|
||||
assert.equal(key.startsWith("tmpfile_key_"), true);
|
||||
return "/public/1/logo.png";
|
||||
},
|
||||
} as any;
|
||||
controller.ctx = { user: { id: 1 } } as any;
|
||||
|
||||
const res = await controller.upload([{ filename: "logo.png", data: "tmp/logo.png" }] as any, {}, "true");
|
||||
|
||||
assert.equal(res.data.key, "/public/1/logo.png");
|
||||
assert.equal(res.data.url, "/api/basic/file/download?key=%2Fpublic%2F1%2Flogo.png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ export class FileController extends BaseController {
|
||||
authService: AuthService;
|
||||
|
||||
@Post('/upload', { description: Constants.per.authOnly })
|
||||
async upload(@Files() files: UploadFileInfo<string>[], @Fields() fields: any) {
|
||||
async upload(@Files() files: UploadFileInfo<string>[], @Fields() fields: any, @Query('autoSave') autoSave: string) {
|
||||
console.log('files', files, fields);
|
||||
const cacheKey = uploadTmpFileCacheKey + nanoid();
|
||||
const file = files[0];
|
||||
@@ -51,6 +51,13 @@ export class FileController extends BaseController {
|
||||
ttl: 1000 * 60 * 60,
|
||||
}
|
||||
);
|
||||
if (autoSave === 'true') {
|
||||
const key = await this.fileService.saveFile(this.getUserId(), cacheKey, 'public');
|
||||
return this.ok({
|
||||
key,
|
||||
url: `/api/basic/file/download?key=${encodeURIComponent(key)}`,
|
||||
});
|
||||
}
|
||||
return this.ok({
|
||||
key: cacheKey,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user