7 Commits

Author SHA1 Message Date
987e3cfefc Merge branch 'aigc' of https://git.nianxx.cn/zoujing/YGChatCS into home3.0
# Conflicts:
#	src/components/TopNavBar/index.vue
2026-07-23 09:59:37 +08:00
f0843e0f12 Merge branch 'aigc' of https://git.nianxx.cn/zoujing/YGChatCS into home3.0
# Conflicts:
#	src/pages-aigc/recharge/components/CustomAmountCard/index.vue
#	src/pages-aigc/recharge/components/RechargeFooter/index.vue
#	src/pages-aigc/recharge/components/RechargePackageList/index.vue
#	src/pages-aigc/recharge/recharge.vue
#	src/pages-aigc/useTemplate/useTemplate.vue
2026-07-22 16:48:21 +08:00
duanshuwen
54a83bad5d refactor(top-nav-bar): add custom back prop
- add customBack prop to enable explicit custom back handling
- rewrite back navigation logic to use the new prop instead of inferring from emit listeners
- remove unused getCurrentInstance import
- update three AIGC page components to use the custom-back prop
2026-07-20 19:12:11 +08:00
duanshuwen
c7062e0b51 fix(sharedWork): replace back nav with uni.reLaunch
remove the page stack navigation check, directly relaunch to the index page, add a fail toast for reLaunch failures
2026-07-20 15:02:48 +08:00
c399bba109 Merge branch 'remove-aigc' of https://git.nianxx.cn/zoujing/YGChatCS into home3.0 2026-07-20 14:53:10 +08:00
duanshuwen
bfebed44b0 chore: remove recharge and switch to test mode
Remove all recharge-related pages, components, and API endpoints.
Update developVersion flag to enable test environment with 400 free credits for all users.
Revise PointInsufficientDialog to reflect closed recharge channel, update button text and actions.
Remove recharge actions from all top bars and generate confirmation panels.
Add new user agreement page for service terms, update related API calls.
Restyle AigcConsentDialog to use sectioned content layout.
Fix visibility timing logic in GiftPointsToast component.
2026-07-20 14:50:14 +08:00
54e6d423ae feat: 支付可能重复的问题修复 2026-07-20 12:45:52 +08:00
28 changed files with 442 additions and 572 deletions

View File

@@ -0,0 +1,59 @@
<template>
<view class="aigc-agreement-page">
<TopNavBar
title="智念AI用户规则"
background="#ffffff"
title-color="#172033"
back-icon-color="#172033"
:align-with-menu-button="true"
:z-index="3"
/>
<scroll-view class="aigc-agreement-scroll" scroll-y>
<view v-if="agreement" class="aigc-agreement-document">
<zero-markdown-view :markdown="agreement" />
</view>
<view v-else class="aigc-agreement-state">
<text>{{ loading ? "协议加载中..." : "协议内容加载失败" }}</text>
<button v-if="!loading" class="aigc-agreement-retry" @tap="fetchAgreement">
重新加载
</button>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import TopNavBar from "@/components/TopNavBar/index.vue";
import { getUserAgreement } from "@/request/api/AigcApi.js";
const agreement = ref("");
const loading = ref(false);
const fetchAgreement = async () => {
if (loading.value) return;
loading.value = true;
try {
const res = await getUserAgreement();
agreement.value =
res?.code === 0 && typeof res.data === "string" ? res.data.trim() : "";
} catch (error) {
agreement.value = "";
console.error("获取智念AI用户规则失败", error);
} finally {
loading.value = false;
}
};
onLoad(() => {
fetchAgreement();
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,54 @@
.aigc-agreement-page {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background: #ffffff;
}
.aigc-agreement-scroll {
height: 0;
flex: 1 1 auto;
min-height: 0;
background: #ffffff;
}
.aigc-agreement-document {
min-height: 100%;
padding: 20px 16px calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.aigc-agreement-state {
min-height: 240px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
color: #7b8798;
font-size: 14px;
line-height: 22px;
box-sizing: border-box;
}
.aigc-agreement-retry {
width: 120px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
margin: 16px 0 0;
padding: 0;
border-radius: 999px;
background: #102942;
color: #ffffff;
font-size: 14px;
line-height: 20px;
font-weight: 800;
}
.aigc-agreement-retry::after {
border: 0;
}

View File

@@ -10,8 +10,7 @@
</view>
<view v-if="showActions" class="aigc-topbar-actions">
<view class="aigc-wallet-button" @tap="emit('recharge')">
<text class="aigc-wallet-label">{{ rechargeLabel }}</text>
<view class="aigc-points-display">
<text class="aigc-wallet-points">
<text class="aigc-wallet-number">{{ displayPoints }}</text>
积分
@@ -37,10 +36,6 @@ const props = defineProps({
type: [Number, String],
default: 0,
},
rechargeLabel: {
type: String,
default: "充值",
},
showActions: {
type: Boolean,
default: true,
@@ -52,7 +47,7 @@ const props = defineProps({
},
});
const emit = defineEmits(["back", "history", "recharge"]);
const emit = defineEmits(["back", "history"]);
const statusBarHeight = ref(0);
const navHeight = ref(52);

View File

@@ -18,6 +18,7 @@
}
.aigc-nav-row {
position: relative;
height: var(--aigc-nav-height);
display: flex;
align-items: center;
@@ -53,12 +54,15 @@
}
.aigc-topbar-actions {
flex: 1 1 auto;
position: absolute;
left: 50%;
transform: translateX(-50%);
flex: 0 0 auto;
width: auto;
min-width: 0;
height: 36px;
display: grid;
grid-template-columns: minmax(0, 1fr) 36px;
grid-template-columns: auto 36px;
align-items: center;
gap: 6px;
border-radius: 999px;
@@ -68,7 +72,7 @@
}
.aigc-history-button,
.aigc-wallet-button {
.aigc-points-display {
height: 36px;
min-width: 0;
display: flex;
@@ -94,29 +98,15 @@
justify-content: center;
}
.aigc-wallet-button {
justify-content: space-between;
gap: 6px;
padding: 0 8px;
.aigc-points-display {
min-width: 84px;
max-width: 112px;
justify-content: center;
padding: 0 12px;
background: #102942;
color: #ffffff;
}
.aigc-wallet-label {
height: 24px;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
padding: 0 8px;
border-radius: 999px;
background: rgba(32, 207, 117, 0.18);
color: #50e093;
font-size: 10px;
line-height: 24px;
font-weight: 900;
white-space: nowrap;
}
.aigc-wallet-points {
min-width: 0;
display: inline-flex;

View File

@@ -1,6 +1,6 @@
<template>
<view class="aigc-detail-page">
<AigcTopBar :points="pointBalance" @back="handleBack" @history="handleOpenRecords" @recharge="handleRecharge" />
<AigcTopBar :points="pointBalance" @back="handleBack" @history="handleOpenRecords" />
<view class="aigc-detail-content">
<template v-if="taskDetail">
@@ -22,7 +22,7 @@
<RegenerateConfirmPopup ref="regeneratePopupRef" :cost="regenerateCost" :balance="pointBalance"
:loading="isRegenerating" @confirm="handleConfirmRegenerate" />
<PointInsufficientDialog v-if="pointDialogVisible" :cost="regenerateCost" :balance="pointBalance"
@cancel="handleClosePointDialog" @recharge="handlePointRecharge" />
@cancel="handleClosePointDialog" />
</view>
</template>
@@ -239,13 +239,6 @@ const handleBack = () => {
}
};
const handleRecharge = () => {
uni.navigateTo({
url: "/pages-aigc/recharge/recharge",
fail: () => showPlaceholderToast("打开充值页面失败"),
});
};
const getMediaFileExtension = (url, mediaType) => {
const pathname = String(url || "").split(/[?#]/)[0];
const matchedExtension = pathname.match(/\.([a-z0-9]{2,5})$/i)?.[1]?.toLowerCase();
@@ -572,11 +565,6 @@ const handleClosePointDialog = () => {
pointDialogVisible.value = false;
};
const handlePointRecharge = () => {
pointDialogVisible.value = false;
handleRecharge();
};
const handleChangeTemplate = () => {
uni.navigateTo({
url: "/pages-aigc/home/home",

View File

@@ -13,13 +13,24 @@
<text class="aigc-consent-title">温馨提示</text>
<view class="aigc-consent-content">
<text v-if="content">{{ content }}</text>
<text v-if="content" class="aigc-consent-paragraph">{{ content }}</text>
<template v-else>
<text>{{ defaultContentPrefix }}</text>
<text class="aigc-consent-rule" @tap.stop="handleRuleClick">
{{ ruleName }}
</text>
<text>{{ defaultContentSuffix }}</text>
<view class="aigc-consent-section">
<text class="aigc-consent-section-title">体验权益</text>
<text class="aigc-consent-paragraph">{{ experienceContent }}</text>
</view>
<view class="aigc-consent-section">
<text class="aigc-consent-section-title">服务说明</text>
<text class="aigc-consent-paragraph">{{ serviceContent }}</text>
<view class="aigc-consent-paragraph">
<text>请您在使用前仔细阅读</text>
<text class="aigc-consent-rule" @tap.stop="handleRuleClick">
{{ ruleName }}
</text>
<text>{{ ruleConsentSuffix }}</text>
</view>
</view>
</template>
</view>
@@ -66,16 +77,19 @@ const props = defineProps({
},
});
const emit = defineEmits(["update:visible", "cancel", "agree", "ruleClick"]);
const emit = defineEmits(["update:visible", "cancel", "agree"]);
const popupRef = ref(null);
const defaultContentPrefix = computed(
const experienceContent =
"当前体验版本赠送 400 积分,可生成 1 张高清旅行图片和 1 段 5 秒内动效视频。如需更多生成额度,敬请期待后续版本上线。";
const serviceContent = computed(
() =>
`欢迎使用【${props.companyName}】提供的AI内容升级服务。本服务通过人工智能技术AIGC您上传的照片或视频与景区场景模板进行创意优化,为您生成具有特定风格的旅行内容。请您仔细阅读`
`欢迎使用【${props.companyName}】提供的 AI 内容升级服务。本服务通过人工智能技术AIGC,对您上传的照片或视频进行处理,并结合景区场景模板,为您生成具有特定风格的旅行图片或视频内容。`
);
const defaultContentSuffix =
",您点击“我同意”等进行下一步操作的行为均视为您同意受本规则约束。如您不同意,您可以直接关闭页面。";
const ruleConsentSuffix =
"点击“我同意”并继续操作,即表示您已阅读、理解并同意遵守相关规则;如您不同意,可点击“取消”并退出本服务。";
const close = () => {
popupRef.value?.close();
@@ -93,7 +107,15 @@ const handleAgree = () => {
};
const handleRuleClick = () => {
emit("ruleClick");
uni.navigateTo({
url: "/pages-aigc/agreement/agreement",
fail: () => {
uni.showToast({
title: "打开用户规则失败",
icon: "none",
});
},
});
};
const syncPopupVisible = async (visible) => {

View File

@@ -31,6 +31,28 @@
text-align: justify;
}
.aigc-consent-section + .aigc-consent-section {
margin-top: 18px;
}
.aigc-consent-section-title,
.aigc-consent-paragraph {
display: block;
}
.aigc-consent-section-title {
margin-bottom: 6px;
color: #172033;
font-size: 16px;
line-height: 24px;
font-weight: 900;
text-align: left;
}
.aigc-consent-paragraph + .aigc-consent-paragraph {
margin-top: 10px;
}
.aigc-consent-rule {
color: #18bd78;
font-weight: 900;

View File

@@ -1,5 +1,5 @@
<template>
<view v-if="visible" class="gift-points-toast" role="status">
<view v-if="displayVisible" class="gift-points-toast" role="status">
<text class="gift-points-badge">{{ title }}</text>
<text class="gift-points-message">{{ message }}</text>
<view class="gift-points-close" @tap="handleClose">
@@ -9,14 +9,20 @@
</template>
<script setup>
defineProps({
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
const props = defineProps({
visible: {
type: Boolean,
default: true,
},
delay: {
type: Number,
default: 3000,
},
title: {
type: String,
default: "200积分已到账",
default: "400积分已到账",
},
message: {
type: String,
@@ -25,11 +31,50 @@ defineProps({
});
const emit = defineEmits(["close", "update:visible"]);
const displayVisible = ref(false);
const mounted = ref(false);
let showTimer = null;
const clearShowTimer = () => {
if (showTimer) {
clearTimeout(showTimer);
showTimer = null;
}
};
const syncVisible = (visible) => {
clearShowTimer();
displayVisible.value = false;
if (!visible || !mounted.value) return;
showTimer = setTimeout(() => {
showTimer = null;
displayVisible.value = props.visible;
}, Math.max(0, props.delay));
};
const handleClose = () => {
clearShowTimer();
displayVisible.value = false;
emit("update:visible", false);
emit("close");
};
watch(
() => props.visible,
(visible) => {
syncVisible(visible);
}
);
onMounted(() => {
mounted.value = true;
syncVisible(props.visible);
});
onBeforeUnmount(() => {
clearShowTimer();
});
</script>
<style scoped lang="scss">

View File

@@ -1,7 +1,7 @@
<template>
<view class="aigc-home-page">
<AigcTopBar :points="pointBalance" :left-action="isSharedWorkEntry ? 'home' : 'back'" @back="handleBack"
@history="handleHistory" @recharge="handleRecharge" />
@history="handleHistory" />
<view class="aigc-home-browser">
<GiftPointsToast :visible="giftToastVisible" @close="handleCloseGiftToast" />
@@ -86,12 +86,6 @@ const handleHistory = () => {
});
};
const handleRecharge = () => {
uni.navigateTo({
url: "/pages-aigc/recharge/recharge"
});
};
const handleCloseGiftToast = () => {
giftToastVisible.value = false;
setAigcGiftPointsToastClosed();

View File

@@ -3,7 +3,7 @@
safe-area-inset-bottom @query="queryList">
<template #top>
<TopNavBar title="积分明细" background="transparent" title-color="#172033" back-icon-color="#172033"
:align-with-menu-button="true" :z-index="3" @back="handleBack" />
custom-back :align-with-menu-button="true" :z-index="3" @back="handleBack" />
</template>
<view class="points-details-content">

View File

@@ -1,7 +1,6 @@
<template>
<view class="aigc-progress-page">
<AigcTopBar :points="pointBalance" recharge-label="充值" @back="handleBack" @history="handleViewTasks"
@recharge="handleRecharge" />
<AigcTopBar :points="pointBalance" @back="handleBack" @history="handleViewTasks" />
<scroll-view class="aigc-progress-body" scroll-y>
<view class="aigc-progress-content">
@@ -174,13 +173,6 @@ const handleViewTasks = () => {
});
};
const handleRecharge = () => {
uni.navigateTo({
url: "/pages-aigc/recharge/recharge",
fail: () => showToast("打开积分充值失败"),
});
};
const handleProgressComplete = () => {
if (isNavigatingToDetail || !taskId.value || !isTaskCompleted()) return;

View File

@@ -1,25 +0,0 @@
<template>
<view class="balance-card">
<view class="balance-copy">
<text class="balance-label">当前可用积分</text>
<text class="balance-value">{{ points }}</text>
</view>
<view class="balance-ledger" @tap="emit('ledger')">积分明细</view>
</view>
</template>
<script setup>
defineProps({
points: {
type: [Number, String],
default: 0,
},
});
const emit = defineEmits(["ledger"]);
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -1,60 +0,0 @@
.balance-card {
min-height: 116px;
display: grid;
grid-template-columns: 1fr 78px;
align-items: center;
gap: 12px;
padding: 18px;
border-radius: 22px;
background: #172951;
color: #ffffff;
box-sizing: border-box;
}
.balance-copy {
min-width: 0;
}
.balance-label,
.balance-value,
.balance-rule {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.balance-label {
color: rgba(255, 255, 255, 0.66);
font-size: 11px;
line-height: 15px;
font-weight: 800;
}
.balance-value {
margin: 4px 0;
color: #ffffff;
font-size: 34px;
line-height: 39px;
font-weight: 900;
}
.balance-rule {
color: rgba(255, 255, 255, 0.66);
font-size: 11px;
line-height: 15px;
font-weight: 800;
}
.balance-ledger {
height: 34px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
color: #ffffff;
font-size: 12px;
line-height: 34px;
font-weight: 900;
}

View File

@@ -1,84 +0,0 @@
.custom-card {
min-height: 90px;
display: grid;
grid-template-columns: minmax(0, 1fr) 118px;
grid-template-rows: 28px auto;
column-gap: 14px;
row-gap: 2px;
align-items: center;
margin-top: 12px;
padding: 14px 16px;
border: 1.5px solid transparent;
border-radius: 18px;
background: rgba(255, 255, 255, 0.8);
box-sizing: border-box;
}
.custom-card.is-selected {
border-color: #20cf75;
}
.custom-title {
grid-column: 1;
grid-row: 1;
display: block;
overflow: hidden;
color: #172033;
font-size: 14px;
line-height: 28px;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.custom-input-wrap {
grid-column: 2;
grid-row: 1 / 3;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 0 0 18px;
border-left: 1px solid rgba(82, 96, 115, 0.12);
color: #172033;
font-size: 17px;
line-height: 56px;
font-weight: 900;
box-sizing: border-box;
}
.custom-currency {
flex: 0 0 auto;
}
.custom-input {
width: 52px;
height: 56px;
min-height: 56px;
border: 0;
background: transparent;
color: #172033;
font-size: 18px;
line-height: 56px;
font-weight: 900;
}
.custom-hint {
grid-column: 1;
grid-row: 2;
min-width: 0;
display: grid;
gap: 1px;
color: #7a8596;
font-size: 11px;
line-height: 15px;
font-weight: 800;
}
.custom-hint text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -1,79 +0,0 @@
.recharge-footer {
position: sticky;
bottom: 12px;
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
padding: 12px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10px 24px rgba(18, 48, 56, 0.1);
box-sizing: border-box;
}
.recharge-agreement {
display: flex;
align-items: center;
box-sizing: border-box;
}
.recharge-agreement.is-disabled {
opacity: 0.56;
}
.recharge-agreement-text,
.recharge-agreement-link {
font-size: 14px;
line-height: 24px;
}
.recharge-agreement-text {
color: #667386;
}
.recharge-agreement-link {
color: $theme-color-600;
font-weight: 700;
}
.recharge-payment-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 142px;
align-items: center;
gap: 10px;
}
.recharge-gain {
min-width: 0;
overflow: hidden;
color: #667386;
font-size: 12px;
line-height: 18px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.recharge-gain-value {
color: #172033;
font-weight: 900;
}
.recharge-pay-button {
height: 46px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: #000000;
color: #ffffff;
font-size: 14px;
line-height: 46px;
font-weight: 900;
white-space: nowrap;
}
.recharge-pay-button.is-disabled {
opacity: 0.48;
}

View File

@@ -1,71 +0,0 @@
.package-section {
margin-top: 18px;
}
.package-title {
display: block;
margin-bottom: 10px;
color: #172033;
font-size: 15px;
line-height: 20px;
font-weight: 900;
}
.package-list {
display: grid;
gap: 10px;
}
.package-card {
height: 74px;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 8px;
padding: 12px 14px;
border: 1.5px solid transparent;
border-radius: 18px;
background: rgba(255, 255, 255, 0.82);
text-align: left;
box-sizing: border-box;
}
.package-card.is-selected {
border-color: #20cf75;
background: rgba(255, 255, 255, 0.72);
}
.package-copy {
min-width: 0;
}
.package-points,
.package-origin,
.package-price {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.package-points {
color: #172033;
font-size: 16px;
line-height: 22px;
font-weight: 900;
}
.package-origin {
margin-top: 6px;
color: #7a8596;
font-size: 11px;
line-height: 15px;
font-weight: 800;
}
.package-price {
color: #12a765;
font-size: 16px;
line-height: 22px;
font-weight: 900;
}

View File

@@ -1,31 +0,0 @@
.aigc-recharge-page {
position: relative;
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
min-height: 812px;
overflow: hidden;
color: #172033;
background:
radial-gradient(
130% 78% at 0% 0%,
rgba(255, 249, 226, 0.76),
rgba(255, 249, 226, 0) 42%
),
linear-gradient(180deg, #effaf5 0%, #f4fbf7 58%, #eef8ff 100%);
box-sizing: border-box;
}
.aigc-recharge-content {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 24px 12px;
scrollbar-width: none;
box-sizing: border-box;
}
.aigc-recharge-content::-webkit-scrollbar {
display: none;
}

View File

@@ -4,7 +4,7 @@
<template #top>
<view class="aigc-record-nav">
<TopNavBar title="最近任务" background="transparent" title-color="#162235" back-icon-color="#162235"
:align-with-menu-button="true" :z-index="1" @back="handleBack" />
custom-back :align-with-menu-button="true" :z-index="1" @back="handleBack" />
</view>
</template>

View File

@@ -2,7 +2,7 @@
<view class="shared-work-page">
<view class="shared-work-nav">
<TopNavBar :title="shareDetail.templateName || '旅行作品'" background="#fff" title-color="#173a5f"
back-icon-color="#173a5f" :align-with-menu-button="true" :z-index="3" @back="handleBack" />
back-icon-color="#173a5f" custom-back :align-with-menu-button="true" :z-index="3" @back="handleBack" />
</view>
<view class="shared-work-scroll">
@@ -106,13 +106,10 @@ const openAigcHome = () => {
};
const handleBack = () => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
return;
}
openAigcHome();
uni.reLaunch({
url: "/pages/index/index",
fail: () => showToast("返回首页失败"),
});
};
const handleCreateOwn = async () => {

View File

@@ -26,13 +26,6 @@
>
开始生成
</view>
<view
class="generate-action is-secondary"
hover-class="is-pressed"
@tap="emit('recharge')"
>
充值积分
</view>
</view>
</view>
</template>
@@ -49,7 +42,7 @@ defineProps({
},
});
const emit = defineEmits(["generate", "recharge"]);
const emit = defineEmits(["generate"]);
</script>
<style scoped lang="scss">

View File

@@ -86,9 +86,7 @@
.generate-confirm-actions {
height: 48px;
display: grid;
grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
gap: 10px;
display: block;
margin-top: 18px;
}
@@ -114,11 +112,6 @@
box-shadow: 0 12px 24px rgba(32, 207, 117, 0.25);
}
.generate-action.is-secondary {
background: rgba(255, 255, 255, 0.82);
color: #3d5066;
}
.generate-action.is-pressed {
opacity: 0.86;
}

View File

@@ -7,11 +7,12 @@
:safe-area="false"
:is-mask-click="false"
>
<view class="point-dialog point-recharge-dialog">
<text class="point-dialog-badge">充值提醒</text>
<view class="point-dialog point-insufficient-dialog">
<text class="point-dialog-badge">积分提醒</text>
<text class="point-dialog-title">当前积分不足</text>
<text class="point-dialog-desc">
次生成需要先冻结对应积分充值后可继续当前素材生成
平台当前为每位用户赠送 400 积分支持生成 1 张高清旅行图片及 1 5
秒内动效视频充值通道暂未开放如需更多额度敬请关注后续版本
</text>
<view class="point-dialog-summary">
@@ -24,25 +25,18 @@
<text class="point-summary-value">{{ normalizedCost }}</text>
</view>
<view class="point-summary-item is-shortage">
<text class="point-summary-label">需补</text>
<text class="point-summary-label">积分不</text>
<text class="point-summary-value">{{ shortage }}</text>
</view>
</view>
<view class="point-dialog-actions">
<view
class="point-dialog-button is-cancel"
class="point-dialog-button is-primary"
hover-class="is-pressed"
@tap="emit('cancel')"
>
稍后再说
</view>
<view
class="point-dialog-button is-primary"
hover-class="is-pressed"
@tap="emit('recharge')"
>
去充值
我知道了
</view>
</view>
</view>
@@ -63,7 +57,7 @@ const props = defineProps({
},
});
const emit = defineEmits(["cancel", "recharge"]);
const emit = defineEmits(["cancel"]);
const popupRef = ref(null);
const normalizedCost = computed(() => Math.max(0, Number(props.cost) || 0));

View File

@@ -8,7 +8,7 @@
box-sizing: border-box;
}
.point-recharge-dialog {
.point-insufficient-dialog {
padding-top: 24px;
}
@@ -92,9 +92,7 @@
}
.point-dialog-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
display: block;
margin-top: 28px;
}
@@ -114,12 +112,6 @@
box-sizing: border-box;
}
.point-dialog-button.is-cancel {
border: 1px solid #e2e6ea;
background: #ffffff;
color: #172033;
}
.point-dialog-button.is-primary {
background: #061e34;
color: #ffffff;

View File

@@ -210,8 +210,8 @@ const validateUserForms = () => {
return true;
};
// 处理支付点击事件
const handlePayClick = ThrottleUtils.createThrottle(async (goodsData) => {
// 处理支付点击事件 - 使用执行锁防止重复支付
const handlePayClick = ThrottleUtils.createExecutionLock(async (goodsData) => {
console.log("处理支付点击事件", userFormList.value);
// 预约日期,酒店类型不需要
if (orderData.value.reservationEnabled) {
@@ -267,72 +267,82 @@ const handlePayClick = ThrottleUtils.createThrottle(async (goodsData) => {
// 点击后立即展示 loading
uni.showLoading({ title: "正在提交订单..." });
const res = await orderPay(params);
console.log("确认订单---2:", res);
uni.hideLoading();
try {
const res = await orderPay(params);
console.log("确认订单---2:", res);
// 检查接口返回数据
if (!res || !res.data) {
// 检查接口返回数据
if (!res || !res.data) {
setTimeout(() => {
uni.showToast({
title: res.msg || "订单创建失败,请重试",
icon: "none",
});
}, 100);
return;
}
const { data } = res;
const { nonceStr, packageVal, paySign, signType, timeStamp } = data;
// 验证支付参数是否完整
if (!nonceStr || !packageVal || !paySign || !signType || !timeStamp) {
setTimeout(() => {
uni.showToast({ title: "支付参数错误,请重试", icon: "none" });
}, 100);
return;
}
// 关闭 loading
uni.hideLoading();
setTimeout(() => {
uni.showToast({
title: res.msg || "订单创建失败,请重试",
icon: "none",
});
}, 100);
return;
}
const { data } = res;
const { nonceStr, packageVal, paySign, signType, timeStamp } = data;
// 验证支付参数是否完整
if (!nonceStr || !packageVal || !paySign || !signType || !timeStamp) {
uni.hideLoading();
setTimeout(() => {
uni.showToast({ title: "支付参数错误,请重试", icon: "none" });
}, 100);
return;
}
// 在发起微信支付前关闭 loading避免与原生支付 UI 冲突)
uni.hideLoading();
// #ifdef MP-WEIXIN
// 调用微信支付
uni.requestPayment({
provider: "wxpay",
timeStamp: String(timeStamp), // 确保为字符串类型
nonceStr: String(nonceStr),
package: String(packageVal), // 确保为字符串类型
signType: String(signType),
paySign: String(paySign),
success: () => {
uni.showToast({
title: "支付成功",
icon: "success",
// #ifdef MP-WEIXIN
// 调用微信支付
await new Promise((resolve) => {
uni.requestPayment({
provider: "wxpay",
timeStamp: String(timeStamp), // 确保为字符串类型
nonceStr: String(nonceStr),
package: String(packageVal), // 确保为字符串类型
signType: String(signType),
paySign: String(paySign),
success: () => {
uni.navigateTo({
url: "/pages-order/order/list",
uni.showToast({
title: "支付成功",
icon: "success",
success: () => {
uni.navigateTo({
url: "/pages-order/order/list",
});
},
});
resolve();
},
fail: (e) => {
console.error("支付失败:", e);
uni.showToast({ title: "支付失败,请重试", icon: "none" });
resolve();
},
});
},
fail: (e) => {
console.error("支付失败:", e);
uni.showToast({ title: "支付失败,请重试", icon: "none" });
},
});
// #endif
});
// #endif
// #ifdef APP-PLUS
uni.showModal({
title: "提示",
content: "支付功能开发中",
showCancel: false,
});
// #endif
}, 1000);
// #ifdef APP-PLUS
uni.showModal({
title: "提示",
content: "支付功能开发中",
showCancel: false,
});
// #endif
} catch (error) {
console.error("支付流程出错:", error);
uni.showToast({ title: "支付失败,请重试", icon: "none" });
} finally {
// 确保 loading 被关闭
uni.hideLoading();
}
});
</script>
<style scoped lang="scss">

View File

@@ -27,7 +27,7 @@
<script setup>
import { defineProps, defineEmits, computed } from "vue";
import { orderPayNow } from "@/request/api/OrderApi";
import { DebounceUtils } from "@/utils";
import { ThrottleUtils } from "@/utils";
const props = defineProps({
orderData: {
@@ -64,8 +64,8 @@ const buttonText = computed(() => {
// 定义事件发射器
const emit = defineEmits(["refund", "refresh"]);
// 处理按钮点击事件
const handleButtonClick = DebounceUtils.createDebounce(async (orderData) => {
// 处理按钮点击事件 - 使用执行锁防止重复支付
const handleButtonClick = ThrottleUtils.createExecutionLock(async (orderData) => {
try {
// 再次预定跳转商品详情
if (["1", "2", "3", "4", "5", "6"].includes(statusCode.value)) {
@@ -89,7 +89,6 @@ const handleButtonClick = DebounceUtils.createDebounce(async (orderData) => {
// 检查接口返回数据
if (!res || !res.data) {
uni.hideLoading();
setTimeout(() => {
uni.showToast({ title: res.msg || "订单创建失败,请重试", icon: "none" });
}, 100);
@@ -101,38 +100,41 @@ const handleButtonClick = DebounceUtils.createDebounce(async (orderData) => {
// 验证支付参数是否完整
if (!nonceStr || !packageVal || !paySign || !signType || !timeStamp) {
uni.hideLoading();
setTimeout(() => {
uni.showToast({ title: "支付参数错误,请重试", icon: "none" });
}, 100);
return;
}
// 在发起微信支付前关闭 loading(避免与原生支付 UI 冲突)
// 关闭 loading
uni.hideLoading();
// #ifdef MP-WEIXIN
// 调用微信支付
uni.requestPayment({
provider: "wxpay",
timeStamp: String(timeStamp), // 确保为字符串类型
nonceStr: String(nonceStr),
package: String(packageVal), // 确保为字符串类型
signType: String(signType),
paySign: String(paySign),
success: () => {
uni.showToast({
title: "支付成功",
icon: "success",
success: () => {
console.log("支付成功,刷新订单详情");
emit("refresh", { orderId: orderId });
},
});
},
fail: () => {
uni.showToast({ title: "支付失败,请重试", icon: "none" });
},
// 调用微信支付 - 包装成 Promise 以便锁住执行状态
await new Promise((resolve) => {
uni.requestPayment({
provider: "wxpay",
timeStamp: String(timeStamp), // 确保为字符串类型
nonceStr: String(nonceStr),
package: String(packageVal), // 确保为字符串类型
signType: String(signType),
paySign: String(paySign),
success: () => {
uni.showToast({
title: "支付成功",
icon: "success",
success: () => {
console.log("支付成功,刷新订单详情");
emit("refresh", { orderId: orderId });
},
});
resolve();
},
fail: () => {
uni.showToast({ title: "支付失败,请重试", icon: "none" });
resolve();
},
});
});
// #endif
@@ -146,9 +148,12 @@ const handleButtonClick = DebounceUtils.createDebounce(async (orderData) => {
}
} catch (error) {
console.error("操作失败:", error);
uni.showToast({ title: "操作失败,请重试", icon: "none" });
} finally {
// 确保 loading 被关闭
uni.hideLoading();
}
}, 1000);
});
</script>
<style lang="scss" scoped>

View File

@@ -164,6 +164,12 @@
"navigationStyle": "custom"
}
},
{
"path": "agreement/agreement",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "useTemplate/useTemplate",
"style": {
@@ -188,12 +194,6 @@
"navigationStyle": "custom"
}
},
{
"path": "recharge/recharge",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pointsDetails/pointsDetails",
"style": {

View File

@@ -28,8 +28,8 @@ function getCreditLedgerPage(args) {
return request.post("/hotelBiz/credit/ledger/page", args);
}
function getPointsTopUpAgreement() {
return request.get("/hotelBiz/mainScene/pointsTopUpAgreement", {});
function getUserAgreement() {
return request.get("/hotelBiz/mainScene/userAgreement", {});
}
function createAigcGeneratorTaskShare(args) {
@@ -48,7 +48,7 @@ export {
createAigcGeneratorTask,
getCurrentCredit,
getCreditLedgerPage,
getPointsTopUpAgreement,
getUserAgreement,
createAigcGeneratorTaskShare,
getAigcGeneratorTaskShareDetail,
};

View File

@@ -280,7 +280,7 @@ export class DebounceUtils {
*/
export class ThrottleUtils {
/**
* 创建节流函数
* 创建节流函数(传统时间戳方式)
* @param {Function} func - 要节流的函数
* @param {number} delay - 节流延迟时间
* @returns {Function} 节流后的函数
@@ -297,6 +297,81 @@ export class ThrottleUtils {
}
};
}
/**
* 创建异步安全的节流函数(带执行状态锁)
* 适用于支付等需要等待完成的异步操作
* @param {Function} func - 要节流的异步函数
* @param {number} delay - 节流延迟时间(毫秒)
* @returns {Function} 节流后的函数
*/
static createAsyncThrottle(func, delay = 1000) {
let isExecuting = false; // 执行状态锁
let lastExecTime = 0; // 上次执行时间
return async function (...args) {
const now = Date.now();
// 如果正在执行中,直接返回
if (isExecuting) {
console.warn('函数正在执行中,跳过此次调用');
return;
}
// 如果还在节流时间内,直接返回
if (now - lastExecTime < delay) {
console.warn('节流时间内,跳过此次调用');
return;
}
// 加锁
isExecuting = true;
lastExecTime = now;
try {
// 执行异步函数
return await func.apply(this, args);
} catch (error) {
console.error('异步节流函数执行出错:', error);
throw error;
} finally {
// 无论成功失败都解锁
isExecuting = false;
}
};
}
/**
* 创建执行锁函数(最简单的防重复方式,只锁执行状态)
* 适用于支付等场景,上一次没完成前绝对不执行第二次
* @param {Function} func - 要加锁的函数
* @returns {Function} 加锁后的函数
*/
static createExecutionLock(func) {
let isExecuting = false;
return async function (...args) {
// 如果正在执行中,直接返回
if (isExecuting) {
console.warn('函数正在执行中,跳过此次调用');
return;
}
// 加锁
isExecuting = true;
try {
// 执行函数
return await func.apply(this, args);
} catch (error) {
console.error('执行锁函数执行出错:', error);
throw error;
} finally {
// 无论成功失败都解锁
isExecuting = false;
}
};
}
}
/**