feat(aigc): fully integrate backend API and replace mock data
- Replace all static mock data with real backend API calls for templates, generation tasks, credit balance and recharge - Remove deprecated mock data files including templates.js, versionOptions.js, records.js and packages.js - Add useCurrentCredit composable for fetching and managing user credit balance - Update all components to align with new backend data shapes (e.g. templateItemId instead of id) - Add loading and disabled states for interactive UI elements - Implement full WeChat mini-program payment flow for credit recharge - Fix template and record card UI styling and media handling - Set developVersion flag to true for testing environment
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
<view class="aigc-wallet-button" @tap="emit('recharge')">
|
||||
<text class="aigc-wallet-label">{{ rechargeLabel }}</text>
|
||||
<text class="aigc-wallet-points">
|
||||
<text class="aigc-wallet-number">{{ points }}</text>
|
||||
<text class="aigc-wallet-number">{{ displayPoints }}</text>
|
||||
积分
|
||||
</text>
|
||||
</view>
|
||||
@@ -30,7 +30,7 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { zniconsMap } from "@/static/fonts/znicons.js";
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
points: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
@@ -57,6 +57,8 @@ const topbarStyle = computed(() => ({
|
||||
"--aigc-capsule-safe-width": `${capsuleSafeWidth.value}px`,
|
||||
}));
|
||||
|
||||
const displayPoints = computed(() => Number(props.points) || 0);
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
|
||||
@@ -18,17 +18,23 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
|
||||
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
|
||||
import ContinuationCard from "./components/ContinuationCard/index.vue";
|
||||
import ResultActions from "./components/ResultActions/index.vue";
|
||||
import ResultMeta from "./components/ResultMeta/index.vue";
|
||||
import ResultPreview from "./components/ResultPreview/index.vue";
|
||||
import { aigcDetailResult } from "./data/result.js";
|
||||
|
||||
const pointBalance = ref(300);
|
||||
const { pointBalance, fetchCurrentCredit } = useCurrentCredit();
|
||||
const result = aigcDetailResult;
|
||||
|
||||
onShow(() => {
|
||||
fetchCurrentCredit();
|
||||
});
|
||||
|
||||
const metaItems = computed(() => [
|
||||
{
|
||||
label: "结果",
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<template>
|
||||
<view
|
||||
class="template-card"
|
||||
:class="[
|
||||
active ? 'is-active' : '',
|
||||
template.tone ? `is-${template.tone}` : '',
|
||||
]"
|
||||
:style="{ '--memory-accent': template.accent || '#0f7069' }"
|
||||
:class="{ 'is-active': active }"
|
||||
>
|
||||
<view class="template-media">
|
||||
<video
|
||||
v-if="isVideoTemplate"
|
||||
v-if="templateMediaUrl && isVideoTemplate"
|
||||
class="template-video"
|
||||
:src="template.cover"
|
||||
:src="templateMediaUrl"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
@@ -20,17 +16,17 @@
|
||||
:show-center-play-btn="false"
|
||||
object-fit="cover"
|
||||
/>
|
||||
<image v-else class="template-image" :src="template.cover" mode="aspectFill" />
|
||||
<image v-else-if="templateMediaUrl" class="template-image" :src="templateMediaUrl" mode="aspectFill" />
|
||||
<view class="template-media-shade" />
|
||||
</view>
|
||||
|
||||
<view class="template-copy">
|
||||
<text class="template-badge">{{ template.badge }}</text>
|
||||
<text class="template-title ellipsis-1">{{ template.title }}</text>
|
||||
<text class="template-desc ellipsis-1">{{ template.desc }}</text>
|
||||
<text class="template-note ellipsis-1">{{ template.note }}</text>
|
||||
<text class="template-badge">{{ template.templateTag }}</text>
|
||||
<text class="template-title ellipsis-1">{{ template.templateTitle || template.templateName }}</text>
|
||||
<text class="template-desc ellipsis-1">{{ template.templateSubTitle }}</text>
|
||||
<text class="template-note ellipsis-1">{{ template.templateDescription }}</text>
|
||||
<button class="template-select" type="default" @tap.stop="emit('select')">
|
||||
{{ template.buttonText }}
|
||||
选这个模板
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -54,11 +50,12 @@ const emit = defineEmits(["select"]);
|
||||
|
||||
const VIDEO_EXT_REGEXP = /\.(mp4|mov|webm|m4v)(\?.*)?$/i;
|
||||
|
||||
const isVideoTemplate = computed(() => {
|
||||
const mediaType = props.template.type || props.template.mediaType;
|
||||
if (mediaType === "video") return true;
|
||||
const templateMediaUrl = computed(() => {
|
||||
return props.template.templateContentUrl || props.template.coverPhotoUrl || "";
|
||||
});
|
||||
|
||||
return VIDEO_EXT_REGEXP.test(props.template.cover || "");
|
||||
const isVideoTemplate = computed(() => {
|
||||
return VIDEO_EXT_REGEXP.test(templateMediaUrl.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
flex: 0 0 var(--template-card-media-height, 358px);
|
||||
height: var(--template-card-media-height, 358px);
|
||||
overflow: hidden;
|
||||
background: var(--memory-accent);
|
||||
background: #0f7069;
|
||||
}
|
||||
|
||||
.template-image,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<view class="template-track">
|
||||
<TemplateCard
|
||||
v-for="(template, index) in templates"
|
||||
:key="template.id || index"
|
||||
:key="template.templateId || index"
|
||||
:template="template"
|
||||
:active="index === modelValue"
|
||||
@select="handleSelect(template, index)"
|
||||
@@ -33,7 +33,7 @@
|
||||
<view class="template-dots">
|
||||
<view
|
||||
v-for="(template, index) in templates"
|
||||
:key="`${template.id || index}-dot`"
|
||||
:key="`${template.templateId || index}-dot`"
|
||||
class="template-dot"
|
||||
:class="{ 'is-active': index === modelValue }"
|
||||
/>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
export const aigcTemplates = [
|
||||
{
|
||||
id: "glass-boat-postcard",
|
||||
title: "鸳鸯湖明信片",
|
||||
desc: "适合朋友圈分享的旅行纪念内容",
|
||||
note: "图片版100积分 · 动效视频版200积分/5秒内",
|
||||
badge: "同源双版本",
|
||||
buttonText: "选这个模板",
|
||||
cover: "https://oss.nianxx.cn/mp/static/xiaoqi/glass-boat-postcard.gif",
|
||||
tone: "postcard",
|
||||
accent: "#0a4d46",
|
||||
},
|
||||
{
|
||||
id: "bridge-memory",
|
||||
title: "秘境探险时光",
|
||||
desc: "在鸳鸯湖划船时留下美好回忆",
|
||||
note: "图片版100积分 · 动效视频版200积分/5秒内",
|
||||
badge: "同源双版本",
|
||||
buttonText: "选这个模板",
|
||||
cover: "https://oss.nianxx.cn/config/2f642480fdd79f40a07a56c79317a77c.mp4",
|
||||
tone: "emerald",
|
||||
accent: "#0f7069",
|
||||
},
|
||||
{
|
||||
id: "waterfall-story",
|
||||
title: "卧龙潭粘土拍立得",
|
||||
desc: "用粘土数字形象,从画框中走向屏幕前",
|
||||
note: "图片版100积分 · 动效视频版200积分/5秒内",
|
||||
badge: "同源双版本",
|
||||
buttonText: "选这个模板",
|
||||
cover: "https://oss.nianxx.cn/config/70cef08f598ac30bf42c34ed31dda9bd.mp4",
|
||||
tone: "aqua",
|
||||
accent: "#155e75",
|
||||
},
|
||||
// {
|
||||
// id: "culture-postcard",
|
||||
// title: "瑶山古寨印象",
|
||||
// desc: "把民族风情做成旅行收藏卡",
|
||||
// note: "图片版100积分 · 动效视频版200积分/5秒内",
|
||||
// badge: "同源双版本",
|
||||
// buttonText: "选这个模板",
|
||||
// cover: "https://oss.nianxx.cn/mp/static/xiaoqi/card-culture-1.png",
|
||||
// tone: "forest",
|
||||
// accent: "#36563d",
|
||||
// },
|
||||
];
|
||||
|
||||
export const aigcFlowSteps = ["选模板", "传素材", "添积分", "出结果"];
|
||||
@@ -14,18 +14,40 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
|
||||
import { getAigcGiftPointsToastClosed, setAigcGiftPointsToastClosed } from "@/constant/aigc.js";
|
||||
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
|
||||
import { getAigcTemplateList } from "@/request/api/AigcApi.js";
|
||||
import GiftPointsToast from "./components/GiftPointsToast/index.vue";
|
||||
import TemplateCarousel from "./components/TemplateCarousel/index.vue";
|
||||
import FlowSteps from "./components/FlowSteps/index.vue";
|
||||
import { aigcFlowSteps, aigcTemplates } from "./data/templates.js";
|
||||
|
||||
const pointBalance = ref(300);
|
||||
const { pointBalance, fetchCurrentCredit } = useCurrentCredit();
|
||||
const activeIndex = ref(0);
|
||||
const giftToastVisible = ref(!getAigcGiftPointsToastClosed());
|
||||
const templates = aigcTemplates;
|
||||
const steps = aigcFlowSteps;
|
||||
const templates = ref([]);
|
||||
const steps = ["选模板", "传素材", "添积分", "出结果"];
|
||||
|
||||
const fetchTemplateList = async () => {
|
||||
try {
|
||||
const res = await getAigcTemplateList();
|
||||
if (res?.code === 0 && Array.isArray(res.data)) {
|
||||
templates.value = res.data;
|
||||
activeIndex.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("获取AIGC模板列表失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
fetchTemplateList();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
fetchCurrentCredit();
|
||||
});
|
||||
|
||||
const handleBack = () => {
|
||||
const pages = getCurrentPages();
|
||||
@@ -52,8 +74,11 @@ const handleCloseGiftToast = () => {
|
||||
};
|
||||
|
||||
const handleSelectTemplate = (template) => {
|
||||
const templateId = template?.templateId || "";
|
||||
if (!templateId) return;
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages-aigc/useTemplate/useTemplate?templateId=${encodeURIComponent(template.id || "")}`
|
||||
url: `/pages-aigc/useTemplate/useTemplate?templateId=${encodeURIComponent(templateId)}`
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
22
src/pages-aigc/hooks/useCurrentCredit.js
Normal file
22
src/pages-aigc/hooks/useCurrentCredit.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ref } from "vue";
|
||||
import { getCurrentCredit } from "@/request/api/AigcApi.js";
|
||||
|
||||
export function useCurrentCredit() {
|
||||
const pointBalance = ref(0);
|
||||
|
||||
const fetchCurrentCredit = async () => {
|
||||
try {
|
||||
const res = await getCurrentCredit();
|
||||
if (res?.code === 0) {
|
||||
pointBalance.value = Number(res.data) || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取当前积分失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
pointBalance,
|
||||
fetchCurrentCredit,
|
||||
};
|
||||
}
|
||||
@@ -2,8 +2,7 @@
|
||||
<view class="balance-card">
|
||||
<view class="balance-copy">
|
||||
<text class="balance-label">当前可用积分</text>
|
||||
<text class="balance-value">{{ balance.points }}</text>
|
||||
<text class="balance-rule">{{ balance.ruleText }}</text>
|
||||
<text class="balance-value">{{ points }}</text>
|
||||
</view>
|
||||
|
||||
<view class="balance-ledger" @tap="emit('ledger')">积分明细</view>
|
||||
@@ -12,9 +11,9 @@
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
balance: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
points: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view :class="['custom-card', { 'is-selected': selected }]" @tap="emit('select')">
|
||||
<text class="custom-title">自选金额</text>
|
||||
<view class="custom-card is-selected">
|
||||
<text class="custom-title">充值金额</text>
|
||||
|
||||
<label class="custom-input-wrap">
|
||||
<text class="custom-currency">¥</text>
|
||||
@@ -8,43 +8,29 @@
|
||||
class="custom-input"
|
||||
type="number"
|
||||
:value="modelValue"
|
||||
@focus="emit('select')"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<view class="custom-hint">
|
||||
<text>按 1 元 = 100 积分换算</text>
|
||||
<text>当前可得 {{ currentPoints }} 积分</text>
|
||||
<text>请输入整数金额</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "select"]);
|
||||
|
||||
const currentPoints = computed(() => {
|
||||
const amount = Number.parseInt(props.modelValue || "0", 10);
|
||||
return Number.isNaN(amount) ? 0 : amount * 100;
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const handleInput = (event) => {
|
||||
const rawValue = event?.detail?.value ?? event?.target?.value ?? "";
|
||||
const value = String(rawValue).replace(/\D/g, "");
|
||||
emit("select");
|
||||
emit("update:modelValue", value);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<template>
|
||||
<view class="recharge-footer">
|
||||
<text class="recharge-gain">
|
||||
本次获得 <text class="recharge-gain-value">{{ points }}</text> 积分
|
||||
支付金额 <text class="recharge-gain-value">¥{{ formattedPrice }}</text>
|
||||
</text>
|
||||
|
||||
<view class="recharge-pay-button" @tap="emit('pay')">
|
||||
支付 ¥{{ formattedPrice }}
|
||||
<view
|
||||
class="recharge-pay-button"
|
||||
:class="{ 'is-disabled': loading || price <= 0 }"
|
||||
@tap="emit('pay')"
|
||||
>
|
||||
{{ loading ? "支付中..." : "立即支付" }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -14,14 +18,14 @@
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
points: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["pay"]);
|
||||
|
||||
@@ -42,3 +42,7 @@
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recharge-pay-button.is-disabled {
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
export const rechargeBalance = {
|
||||
points: 0,
|
||||
ruleText: "图片版 100积分/次 · 动效视频版 200积分/次",
|
||||
};
|
||||
|
||||
export const rechargePackages = [
|
||||
{
|
||||
id: "points-1000",
|
||||
points: 1000,
|
||||
price: 9.5,
|
||||
originalPrice: 10,
|
||||
discount: "9.5折",
|
||||
},
|
||||
{
|
||||
id: "points-3000",
|
||||
points: 3000,
|
||||
price: 27,
|
||||
originalPrice: 30,
|
||||
discount: "9折",
|
||||
},
|
||||
{
|
||||
id: "points-5000",
|
||||
points: 5000,
|
||||
price: 42.5,
|
||||
originalPrice: 50,
|
||||
discount: "8.5折",
|
||||
},
|
||||
];
|
||||
@@ -13,25 +13,17 @@
|
||||
|
||||
<view class="aigc-recharge-content">
|
||||
<BalanceCard
|
||||
:balance="balance"
|
||||
:points="balancePoints"
|
||||
@ledger="handleOpenLedger"
|
||||
/>
|
||||
|
||||
<RechargePackageList
|
||||
:packages="packages"
|
||||
:selected-id="selectedId"
|
||||
@select="handleSelectPackage"
|
||||
/>
|
||||
|
||||
<CustomAmountCard
|
||||
v-model="customAmount"
|
||||
:selected="selectedId === customId"
|
||||
@select="handleSelectCustom"
|
||||
/>
|
||||
|
||||
<RechargeFooter
|
||||
:points="payPoints"
|
||||
:price="payPrice"
|
||||
:loading="paying"
|
||||
@pay="handlePay"
|
||||
/>
|
||||
</view>
|
||||
@@ -40,51 +32,34 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
|
||||
import { rechargeCredit } from "@/request/api/AigcApi.js";
|
||||
import BalanceCard from "./components/BalanceCard/index.vue";
|
||||
import CustomAmountCard from "./components/CustomAmountCard/index.vue";
|
||||
import RechargeFooter from "./components/RechargeFooter/index.vue";
|
||||
import RechargePackageList from "./components/RechargePackageList/index.vue";
|
||||
import { rechargeBalance, rechargePackages } from "./data/packages.js";
|
||||
|
||||
const customId = "custom";
|
||||
const balance = rechargeBalance;
|
||||
const packages = rechargePackages;
|
||||
const selectedId = ref("points-3000");
|
||||
const customAmount = ref("2");
|
||||
const { pointBalance: balancePoints, fetchCurrentCredit } = useCurrentCredit();
|
||||
const customAmount = ref("1");
|
||||
const paying = ref(false);
|
||||
|
||||
const selectedPackage = computed(() =>
|
||||
packages.find((item) => item.id === selectedId.value)
|
||||
);
|
||||
|
||||
const customPrice = computed(() => {
|
||||
const payPrice = computed(() => {
|
||||
const amount = Number.parseInt(customAmount.value || "0", 10);
|
||||
return Number.isNaN(amount) ? 0 : amount;
|
||||
});
|
||||
|
||||
const payPoints = computed(() => {
|
||||
if (selectedId.value === customId) {
|
||||
return customPrice.value * 100;
|
||||
}
|
||||
|
||||
return selectedPackage.value?.points || 0;
|
||||
});
|
||||
|
||||
const payPrice = computed(() => {
|
||||
if (selectedId.value === customId) {
|
||||
return customPrice.value;
|
||||
}
|
||||
|
||||
return selectedPackage.value?.price || 0;
|
||||
});
|
||||
|
||||
const showPlaceholderToast = (title) => {
|
||||
const showToast = (title) => {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
fetchCurrentCredit();
|
||||
});
|
||||
|
||||
const handleBack = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
@@ -95,20 +70,74 @@ const handleBack = () => {
|
||||
const handleOpenLedger = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages-aigc/pointsDetails/pointsDetails",
|
||||
fail: () => showPlaceholderToast("积分明细"),
|
||||
fail: () => showToast("积分明细"),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectPackage = (item) => {
|
||||
selectedId.value = item.id;
|
||||
const requestWechatPay = (payData) => {
|
||||
const { nonceStr, packageVal, paySign, signType, timeStamp } = payData || {};
|
||||
|
||||
if (!nonceStr || !packageVal || !paySign || !signType || !timeStamp) {
|
||||
showToast("支付参数错误,请重试");
|
||||
return;
|
||||
}
|
||||
|
||||
// #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",
|
||||
});
|
||||
fetchCurrentCredit();
|
||||
},
|
||||
fail: () => {
|
||||
showToast("支付失败,请重试");
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
const handleSelectCustom = () => {
|
||||
selectedId.value = customId;
|
||||
};
|
||||
const handlePay = async () => {
|
||||
if (paying.value) return;
|
||||
|
||||
const handlePay = () => {
|
||||
showPlaceholderToast(`支付 ¥${payPrice.value}`);
|
||||
const rechargeAmount = payPrice.value;
|
||||
if (!Number.isInteger(rechargeAmount) || rechargeAmount <= 0) {
|
||||
showToast("请输入正确的充值金额");
|
||||
return;
|
||||
}
|
||||
|
||||
paying.value = true;
|
||||
uni.showLoading({ title: "正在发起支付..." });
|
||||
|
||||
try {
|
||||
const res = await rechargeCredit({
|
||||
rechargeAmount,
|
||||
payWay: "0",
|
||||
paySource: "1",
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (res?.code === 0 && res.data) {
|
||||
requestWechatPay(res.data);
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(res?.msg || "充值下单失败,请重试");
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error("充值下单失败", error);
|
||||
showToast("充值下单失败,请重试");
|
||||
} finally {
|
||||
paying.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<template>
|
||||
<view class="record-card">
|
||||
<image class="record-card-image" :src="record.image" mode="aspectFill" />
|
||||
<video
|
||||
v-if="isVideoRecord"
|
||||
class="record-card-image"
|
||||
:src="mediaUrl"
|
||||
:controls="false"
|
||||
:show-play-btn="false"
|
||||
:show-center-play-btn="false"
|
||||
object-fit="cover"
|
||||
/>
|
||||
<image v-else-if="mediaUrl" class="record-card-image" :src="mediaUrl" mode="aspectFill" />
|
||||
<view v-else class="record-card-image record-card-media-placeholder" />
|
||||
|
||||
<view class="record-card-copy">
|
||||
<text class="record-card-title ellipsis-1">{{ record.title }}</text>
|
||||
<text class="record-card-time ellipsis-1">{{ record.time }}</text>
|
||||
<text class="record-card-status ellipsis-1">{{ record.status }}</text>
|
||||
<text class="record-card-title ellipsis-1">{{ title }}</text>
|
||||
<text class="record-card-time ellipsis-1">{{ record.createTime }}</text>
|
||||
<text class="record-card-status ellipsis-1">{{ statusText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="record-card-action" @tap="emit('view', record)">
|
||||
@@ -15,7 +25,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
record: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
@@ -23,6 +35,45 @@ defineProps({
|
||||
});
|
||||
|
||||
const emit = defineEmits(["view"]);
|
||||
|
||||
const GENERATOR_TYPE_TEXT = {
|
||||
0: "图片版",
|
||||
1: "视频版",
|
||||
};
|
||||
|
||||
const TASK_STATUS_TEXT = {
|
||||
0: "排队中",
|
||||
1: "生成中",
|
||||
2: "已完成",
|
||||
3: "失败",
|
||||
4: "生成视频中",
|
||||
};
|
||||
|
||||
const title = computed(() => props.record.itemTitle || props.record.taskId || "");
|
||||
|
||||
const mediaUrl = computed(() => {
|
||||
return props.record.imageResultUrl || props.record.videoResultUrl || "";
|
||||
});
|
||||
|
||||
const isVideoRecord = computed(() => {
|
||||
return !props.record.imageResultUrl && Boolean(props.record.videoResultUrl);
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
const generatorType = props.record.generatorType;
|
||||
const taskStatus = props.record.taskStatus;
|
||||
const generatorCost = props.record.generatorCost;
|
||||
const typeText =
|
||||
GENERATOR_TYPE_TEXT[generatorType] || (generatorType === undefined ? "" : `类型${generatorType}`);
|
||||
const stateText =
|
||||
TASK_STATUS_TEXT[taskStatus] || (taskStatus === undefined ? "" : `状态${taskStatus}`);
|
||||
const costText =
|
||||
generatorCost === undefined || generatorCost === null || generatorCost === ""
|
||||
? ""
|
||||
: `消耗${generatorCost}积分`;
|
||||
|
||||
return [typeText, stateText, costText].filter(Boolean).join(" · ");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
background: #edf5f1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.record-card-media-placeholder {
|
||||
background: linear-gradient(135deg, #edf5f1 0%, #f6faf8 100%);
|
||||
}
|
||||
|
||||
.record-card-copy {
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
<view class="record-list">
|
||||
<RecordCard
|
||||
v-for="record in records"
|
||||
:key="record.id"
|
||||
:key="record.taskId"
|
||||
:record="record"
|
||||
@view="handleView"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<text class="record-note">仅保留最近20天生成记录</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export const aigcRecords = [
|
||||
{
|
||||
id: "record-lake-postcard",
|
||||
title: "鸳鸯湖明信片",
|
||||
time: "2026-06-29 15:46:12",
|
||||
status: "图片版 · 已完成 · 可升级动效视频",
|
||||
image: "https://oss.nianxx.cn/mp/static/xiaoqi/glass-boat-postcard.gif",
|
||||
},
|
||||
{
|
||||
id: "record-lake-postcard-video",
|
||||
title: "鸳鸯湖明信片",
|
||||
time: "2026-06-29 15:58:40",
|
||||
status: "动效视频版 · 5秒内 · 由图片版升级",
|
||||
image: "https://oss.nianxx.cn/mp/static/xiaoqi/glass-boat-postcard.gif",
|
||||
},
|
||||
];
|
||||
@@ -1,30 +1,61 @@
|
||||
<template>
|
||||
<view class="aigc-record-page">
|
||||
<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"
|
||||
/>
|
||||
</view>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="records"
|
||||
class="aigc-record-page"
|
||||
bg-color="#ffffff"
|
||||
safe-area-inset-bottom
|
||||
@query="queryList"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<RecordList
|
||||
:records="records"
|
||||
@view="handleViewRecord"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<template #empty>
|
||||
<CustomEmpty statusText="暂无生成记录" />
|
||||
</template>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||
import CustomEmpty from "@/components/CustomEmpty/index.vue";
|
||||
import { getAigcGeneratorTaskList } from "@/request/api/AigcApi.js";
|
||||
import RecordList from "./components/RecordList/index.vue";
|
||||
import { aigcRecords } from "./data/records.js";
|
||||
|
||||
const records = aigcRecords;
|
||||
const records = ref([]);
|
||||
const paging = ref(null);
|
||||
|
||||
const queryList = async (pageNum, pageSize) => {
|
||||
try {
|
||||
const res = await getAigcGeneratorTaskList({ pageNum, pageSize });
|
||||
if (res?.code === 0 && Array.isArray(res.data?.records)) {
|
||||
paging.value?.complete(res.data.records);
|
||||
return;
|
||||
}
|
||||
|
||||
paging.value?.complete([]);
|
||||
} catch (error) {
|
||||
console.error("获取AIGC生成任务列表失败", error);
|
||||
paging.value?.complete(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
const pages = getCurrentPages();
|
||||
@@ -35,7 +66,7 @@ const handleBack = () => {
|
||||
|
||||
const handleViewRecord = (record) => {
|
||||
uni.showToast({
|
||||
title: record.title,
|
||||
title: record.itemTitle || record.taskId || "",
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,30 +8,44 @@
|
||||
:is-mask-click="false"
|
||||
>
|
||||
<view class="photo-pick-drawer">
|
||||
<view class="photo-pick-close" @tap="emit('close')">
|
||||
<uni-icons type="closeempty" size="30" color="#8fa2ba" />
|
||||
</view>
|
||||
|
||||
<text class="photo-pick-title">选择照片</text>
|
||||
|
||||
<view class="photo-pick-card">
|
||||
<view class="photo-pick-plus">+</view>
|
||||
<text class="photo-pick-card-title">上传照片或视频素材</text>
|
||||
<text class="photo-pick-card-desc">
|
||||
选择后会自动进行清晰度、人脸数量和遮挡检测
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="photo-pick-actions">
|
||||
<view class="photo-pick-action" hover-class="is-pressed" @tap="emit('camera')">
|
||||
拍照
|
||||
<view
|
||||
class="photo-pick-close"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
@tap="handleClose"
|
||||
>
|
||||
<uni-icons type="closeempty" size="30" color="#8fa2ba" />
|
||||
</view>
|
||||
<view class="photo-pick-action" hover-class="is-pressed" @tap="emit('album')">
|
||||
相册
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="photo-pick-hint">建议使用半身照,避免墨镜、口罩、强逆光。</text>
|
||||
<text class="photo-pick-title">选择照片</text>
|
||||
|
||||
<view class="photo-pick-card">
|
||||
<view class="photo-pick-plus">+</view>
|
||||
<text class="photo-pick-card-title">上传照片素材</text>
|
||||
<text class="photo-pick-card-desc">
|
||||
选择后会自动进行清晰度、人脸数量和遮挡检测
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="photo-pick-actions">
|
||||
<view
|
||||
class="photo-pick-action"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
:hover-class="loading ? 'none' : 'is-pressed'"
|
||||
@tap="handleCamera"
|
||||
>
|
||||
拍照
|
||||
</view>
|
||||
<view
|
||||
class="photo-pick-action"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
:hover-class="loading ? 'none' : 'is-pressed'"
|
||||
@tap="handleAlbum"
|
||||
>
|
||||
相册
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="photo-pick-hint">建议使用半身照,避免墨镜、口罩、强逆光。</text>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
@@ -39,9 +53,34 @@
|
||||
<script setup>
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "camera", "album"]);
|
||||
const popupRef = ref(null);
|
||||
|
||||
const handleClose = () => {
|
||||
if (props.loading) return;
|
||||
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const handleCamera = () => {
|
||||
if (props.loading) return;
|
||||
|
||||
emit("camera");
|
||||
};
|
||||
|
||||
const handleAlbum = () => {
|
||||
if (props.loading) return;
|
||||
|
||||
emit("album");
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
popupRef.value?.open();
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.photo-pick-close.is-disabled {
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.photo-pick-title {
|
||||
display: block;
|
||||
margin: 1px 0 17px;
|
||||
@@ -111,6 +115,10 @@
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.photo-pick-action.is-disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.photo-pick-hint {
|
||||
display: block;
|
||||
margin: 10px 0 0;
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
<view
|
||||
class="template-version-hero"
|
||||
:class="`is-${variant}`"
|
||||
:style="{ '--template-accent': template.accent || '#0a4d46' }"
|
||||
>
|
||||
<video
|
||||
v-if="isVideoTemplate"
|
||||
v-if="templateMediaUrl && isVideoTemplate"
|
||||
class="template-version-video"
|
||||
:src="template.cover"
|
||||
:src="templateMediaUrl"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
@@ -16,13 +15,13 @@
|
||||
:show-center-play-btn="false"
|
||||
object-fit="cover"
|
||||
/>
|
||||
<image v-else class="template-version-image" :src="template.cover" mode="aspectFill" />
|
||||
<image v-else-if="templateMediaUrl" class="template-version-image" :src="templateMediaUrl" mode="aspectFill" />
|
||||
<view class="template-version-shade" />
|
||||
|
||||
<view class="template-version-copy">
|
||||
<text class="template-version-badge">{{ template.badge || "同源双版本" }}</text>
|
||||
<text class="template-version-title">{{ template.title }}</text>
|
||||
<text class="template-version-desc">{{ template.desc }}</text>
|
||||
<text class="template-version-badge">{{ template.templateTag }}</text>
|
||||
<text class="template-version-title">{{ template.templateTitle || template.templateName }}</text>
|
||||
<text class="template-version-desc">{{ template.templateSubTitle || template.templateDescription }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -43,11 +42,12 @@ const props = defineProps({
|
||||
|
||||
const VIDEO_EXT_REGEXP = /\.(mp4|mov|webm|m4v)(\?.*)?$/i;
|
||||
|
||||
const isVideoTemplate = computed(() => {
|
||||
const mediaType = props.template.type || props.template.mediaType;
|
||||
if (mediaType === "video") return true;
|
||||
const templateMediaUrl = computed(() => {
|
||||
return props.template.templateContentUrl || props.template.coverPhotoUrl || "";
|
||||
});
|
||||
|
||||
return VIDEO_EXT_REGEXP.test(props.template.cover || "");
|
||||
const isVideoTemplate = computed(() => {
|
||||
return VIDEO_EXT_REGEXP.test(templateMediaUrl.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
@tap="emit('select', option)"
|
||||
>
|
||||
<view class="version-option-copy">
|
||||
<text class="version-option-title">{{ option.label }}</text>
|
||||
<text class="version-option-desc">{{ option.desc }}</text>
|
||||
<text class="version-option-title">{{ option.itemTitle || option.templateItemId }}</text>
|
||||
<text class="version-option-desc">{{ option.itemSubTitle }}</text>
|
||||
<text class="version-option-type">{{ generatorTypeText }}</text>
|
||||
</view>
|
||||
|
||||
<text class="version-option-cost">{{ option.cost }}积分</text>
|
||||
<text class="version-option-cost">{{ option.generatorCost }}积分</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
@@ -27,6 +30,12 @@ defineProps({
|
||||
});
|
||||
|
||||
const emit = defineEmits(["select"]);
|
||||
|
||||
const generatorTypeText = computed(() => {
|
||||
if (props.option.generatorType === 0) return "图片生成";
|
||||
if (props.option.generatorType === 1) return "视频生成";
|
||||
return `类型 ${props.option.generatorType}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
.version-option-title,
|
||||
.version-option-desc,
|
||||
.version-option-type,
|
||||
.version-option-cost {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
@@ -51,6 +52,14 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.version-option-type {
|
||||
margin-top: 4px;
|
||||
color: #9aa6b8;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.version-option-cost {
|
||||
color: #12a765;
|
||||
font-size: 15px;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<view class="version-option-list">
|
||||
<VersionOptionCard
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:key="option.templateItemId"
|
||||
:option="option"
|
||||
:selected="option.value === selectedValue"
|
||||
:selected="option.templateItemId === selectedValue"
|
||||
@select="emit('select', $event)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export const versionOptions = [
|
||||
{
|
||||
value: "image",
|
||||
label: "图片版",
|
||||
desc: "生成一张高清旅行图片,适合保存和分享。",
|
||||
cost: 100,
|
||||
},
|
||||
{
|
||||
value: "video",
|
||||
label: "动效视频版 · 5秒内",
|
||||
desc: "先生成图片底图,再升级为 5秒内动效视频。",
|
||||
cost: 200,
|
||||
},
|
||||
];
|
||||
@@ -14,27 +14,27 @@
|
||||
|
||||
<GenerateConfirmPanel
|
||||
v-if="currentStep === 'generateConfirm'"
|
||||
:cost="currentVersion.cost || 100"
|
||||
:cost="currentCost"
|
||||
:balance="pointBalance"
|
||||
@generate="handleGenerate"
|
||||
@recharge="handleRecharge"
|
||||
/>
|
||||
<GeneratingProgressPanel
|
||||
v-else-if="currentStep === 'generating'"
|
||||
:cost="currentVersion.cost || 100"
|
||||
:cost="currentCost"
|
||||
:progress="8"
|
||||
@complete="handleGenerateComplete"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<VersionOptionList
|
||||
:options="versionOptions"
|
||||
:selected-value="selectedVersion"
|
||||
@select="handleSelectVersion"
|
||||
:options="templateItems"
|
||||
:selected-value="selectedTemplateItemId"
|
||||
@select="handleSelectTemplateItem"
|
||||
/>
|
||||
|
||||
<text class="aigc-use-template-note">
|
||||
动效视频版会先生成图片底图,再升级为 5秒内动效视频。
|
||||
动效视频版会先生图,再升级为 5 秒内动效视频。
|
||||
</text>
|
||||
</template>
|
||||
</view>
|
||||
@@ -51,6 +51,7 @@
|
||||
</view>
|
||||
<view v-if="currentStep === 'photoPick'" class="aigc-use-template-popup-host">
|
||||
<PhotoPickDrawer
|
||||
:loading="isUploading"
|
||||
@close="handleCloseGuide"
|
||||
@camera="handlePickCamera"
|
||||
@album="handlePickAlbum"
|
||||
@@ -58,14 +59,14 @@
|
||||
</view>
|
||||
<view v-if="currentStep === 'photoConfirm'" class="aigc-use-template-popup-host">
|
||||
<PhotoConfirmDrawer
|
||||
:avatar-src="guideAvatar"
|
||||
:avatar-src="selectedImageLocalPath || guideAvatar"
|
||||
@close="handleClosePhotoConfirm"
|
||||
@confirm="handleConfirmPhoto"
|
||||
/>
|
||||
</view>
|
||||
<PointInsufficientDialog
|
||||
v-if="pointDialogVisible"
|
||||
:cost="currentVersion.cost || 100"
|
||||
:cost="currentCost"
|
||||
:balance="pointBalance"
|
||||
@cancel="handleClosePointDialog"
|
||||
@recharge="handlePointRecharge"
|
||||
@@ -75,9 +76,15 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
|
||||
import { aigcTemplates } from "@/pages-aigc/home/data/templates.js";
|
||||
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
|
||||
import {
|
||||
createAigcGeneratorTask,
|
||||
getAigcTemplateItemList,
|
||||
getAigcTemplateList,
|
||||
} from "@/request/api/AigcApi.js";
|
||||
import { updateImageFile } from "@/request/api/UpdateFile.js";
|
||||
import guideAvatar from "./assets/xiaoqi-avatar.png";
|
||||
import GenerateConfirmPanel from "./components/GenerateConfirmPanel/index.vue";
|
||||
import GeneratingProgressPanel from "./components/GeneratingProgressPanel/index.vue";
|
||||
@@ -88,24 +95,34 @@ import PointInsufficientDialog from "./components/PointInsufficientDialog/index.
|
||||
import TemplateVersionHero from "./components/TemplateVersionHero/index.vue";
|
||||
import VersionOptionList from "./components/VersionOptionList/index.vue";
|
||||
import { negativeExamples, positiveExamples } from "./data/photoGuideExamples.js";
|
||||
import { versionOptions } from "./data/versionOptions.js";
|
||||
|
||||
const pointBalance = ref(300);
|
||||
const { pointBalance, fetchCurrentCredit } = useCurrentCredit();
|
||||
const templateId = ref("");
|
||||
const selectedVersion = ref("image");
|
||||
const templates = ref([]);
|
||||
const templateItems = ref([]);
|
||||
const selectedTemplateItemId = ref("");
|
||||
const selectedImageLocalPath = ref("");
|
||||
const uploadedImageUrl = ref("");
|
||||
const createdTaskId = ref("");
|
||||
const currentStep = ref("version");
|
||||
const pointDialogVisible = ref(false);
|
||||
const isUploading = ref(false);
|
||||
const isCreatingTask = ref(false);
|
||||
|
||||
const currentTemplate = computed(() => {
|
||||
return aigcTemplates.find((item) => item.id === templateId.value) || aigcTemplates[0] || {};
|
||||
return templates.value.find((item) => item.templateId === templateId.value) || templates.value[0] || {};
|
||||
});
|
||||
|
||||
const currentVersion = computed(() => {
|
||||
return versionOptions.find((item) => item.value === selectedVersion.value) || versionOptions[0] || {};
|
||||
const currentTemplateItem = computed(() => {
|
||||
return (
|
||||
templateItems.value.find((item) => item.templateItemId === selectedTemplateItemId.value) ||
|
||||
templateItems.value[0] ||
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
onLoad((query = {}) => {
|
||||
templateId.value = query.templateId || "";
|
||||
const currentCost = computed(() => {
|
||||
return Number(currentTemplateItem.value.generatorCost) || 0;
|
||||
});
|
||||
|
||||
const showPlaceholderToast = (title) => {
|
||||
@@ -115,6 +132,57 @@ const showPlaceholderToast = (title) => {
|
||||
});
|
||||
};
|
||||
|
||||
const resetSelectedPhoto = () => {
|
||||
selectedImageLocalPath.value = "";
|
||||
uploadedImageUrl.value = "";
|
||||
};
|
||||
|
||||
const fetchTemplateList = async () => {
|
||||
try {
|
||||
const res = await getAigcTemplateList();
|
||||
if (res?.code === 0 && Array.isArray(res.data)) {
|
||||
templates.value = res.data;
|
||||
if (!templateId.value && res.data[0]?.templateId) {
|
||||
templateId.value = res.data[0].templateId;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("获取AIGC模板列表失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTemplateItemList = async () => {
|
||||
if (!templateId.value) {
|
||||
templateItems.value = [];
|
||||
selectedTemplateItemId.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getAigcTemplateItemList({ templateId: templateId.value });
|
||||
if (res?.code === 0 && Array.isArray(res.data)) {
|
||||
templateItems.value = res.data;
|
||||
selectedTemplateItemId.value = res.data[0]?.templateItemId || "";
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("获取AIGC模板生成项失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPageData = async () => {
|
||||
await fetchTemplateList();
|
||||
await fetchTemplateItemList();
|
||||
};
|
||||
|
||||
onLoad((query = {}) => {
|
||||
templateId.value = query.templateId || "";
|
||||
fetchPageData();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
fetchCurrentCredit();
|
||||
});
|
||||
|
||||
const handleBack = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
@@ -136,8 +204,11 @@ const handleRecharge = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectVersion = (option) => {
|
||||
selectedVersion.value = option.value;
|
||||
const handleSelectTemplateItem = (option) => {
|
||||
if (!option?.templateItemId) return;
|
||||
|
||||
selectedTemplateItemId.value = option.templateItemId;
|
||||
resetSelectedPhoto();
|
||||
currentStep.value = "uploadGuide";
|
||||
};
|
||||
|
||||
@@ -149,12 +220,91 @@ const handleConfirmGuide = () => {
|
||||
currentStep.value = "photoPick";
|
||||
};
|
||||
|
||||
const getChosenFilePath = (res) => {
|
||||
return res?.tempFiles?.[0]?.tempFilePath || res?.tempFilePaths?.[0] || res?.tempFiles?.[0]?.path || "";
|
||||
};
|
||||
|
||||
const isChooseCanceled = (error) => {
|
||||
const message = String(error?.errMsg || error?.message || "");
|
||||
return message.includes("cancel") || message.includes("取消");
|
||||
};
|
||||
|
||||
const handleChooseImageSuccess = async (res) => {
|
||||
const filePath = getChosenFilePath(res);
|
||||
if (!filePath) {
|
||||
showPlaceholderToast("请选择图片");
|
||||
return;
|
||||
}
|
||||
|
||||
selectedImageLocalPath.value = filePath;
|
||||
uploadedImageUrl.value = "";
|
||||
isUploading.value = true;
|
||||
uni.showLoading({
|
||||
title: "上传中",
|
||||
mask: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const uploadRes = await updateImageFile(filePath);
|
||||
if (uploadRes?.code === 0 && uploadRes.data) {
|
||||
uploadedImageUrl.value = uploadRes.data;
|
||||
currentStep.value = "photoConfirm";
|
||||
} else {
|
||||
resetSelectedPhoto();
|
||||
showPlaceholderToast(uploadRes?.msg || "图片上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
resetSelectedPhoto();
|
||||
console.warn("上传AIGC生成图片失败", error);
|
||||
showPlaceholderToast("图片上传失败");
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
uni.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const handleChooseImageFail = (error) => {
|
||||
if (!isChooseCanceled(error)) {
|
||||
console.warn("选择AIGC生成图片失败", error);
|
||||
showPlaceholderToast("选择图片失败");
|
||||
}
|
||||
};
|
||||
|
||||
const chooseImageWithSource = (sourceType) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ["image"],
|
||||
sourceType: [sourceType],
|
||||
sizeType: ["compressed"],
|
||||
success: handleChooseImageSuccess,
|
||||
fail: handleChooseImageFail,
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ["compressed"],
|
||||
sourceType: [sourceType],
|
||||
success: handleChooseImageSuccess,
|
||||
fail: handleChooseImageFail,
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
const chooseAndUploadImage = (sourceType) => {
|
||||
if (isUploading.value) return;
|
||||
|
||||
chooseImageWithSource(sourceType);
|
||||
};
|
||||
|
||||
const handlePickCamera = () => {
|
||||
currentStep.value = "photoConfirm";
|
||||
chooseAndUploadImage("camera");
|
||||
};
|
||||
|
||||
const handlePickAlbum = () => {
|
||||
currentStep.value = "photoConfirm";
|
||||
chooseAndUploadImage("album");
|
||||
};
|
||||
|
||||
const handleClosePhotoConfirm = () => {
|
||||
@@ -162,11 +312,31 @@ const handleClosePhotoConfirm = () => {
|
||||
};
|
||||
|
||||
const handleConfirmPhoto = () => {
|
||||
if (!uploadedImageUrl.value) {
|
||||
showPlaceholderToast("请先上传图片");
|
||||
currentStep.value = "photoPick";
|
||||
return;
|
||||
}
|
||||
|
||||
currentStep.value = "generateConfirm";
|
||||
};
|
||||
|
||||
const handleGenerate = () => {
|
||||
const cost = Number(currentVersion.value.cost) || 0;
|
||||
const handleGenerate = async () => {
|
||||
if (isCreatingTask.value) return;
|
||||
|
||||
const templateItemId = currentTemplateItem.value.templateItemId;
|
||||
if (!templateItemId) {
|
||||
showPlaceholderToast("请选择生成类型");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!uploadedImageUrl.value) {
|
||||
showPlaceholderToast("请先上传图片");
|
||||
currentStep.value = "photoPick";
|
||||
return;
|
||||
}
|
||||
|
||||
const cost = currentCost.value;
|
||||
const balance = Number(pointBalance.value) || 0;
|
||||
|
||||
if (balance < cost) {
|
||||
@@ -174,12 +344,42 @@ const handleGenerate = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
currentStep.value = "generating";
|
||||
isCreatingTask.value = true;
|
||||
uni.showLoading({
|
||||
title: "创建中",
|
||||
mask: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await createAigcGeneratorTask({
|
||||
templateItemId,
|
||||
imageUrlList: [uploadedImageUrl.value],
|
||||
});
|
||||
|
||||
if (res?.code === 0 && res.data) {
|
||||
createdTaskId.value = res.data;
|
||||
currentStep.value = "generating";
|
||||
fetchCurrentCredit();
|
||||
} else {
|
||||
showPlaceholderToast(res?.msg || "创建任务失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("创建AIGC生成任务失败", error);
|
||||
showPlaceholderToast("创建任务失败");
|
||||
} finally {
|
||||
isCreatingTask.value = false;
|
||||
uni.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateComplete = () => {
|
||||
if (!createdTaskId.value) {
|
||||
showPlaceholderToast("任务创建失败");
|
||||
return;
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: "/pages-aigc/detail/detail",
|
||||
url: `/pages-aigc/detail/detail?taskId=${encodeURIComponent(createdTaskId.value)}`,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
34
src/request/api/AigcApi.js
Normal file
34
src/request/api/AigcApi.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import request from "../base/request";
|
||||
|
||||
function getAigcTemplateList() {
|
||||
return request.get("/hotelBiz/aigc/aigcTemplateList", {});
|
||||
}
|
||||
|
||||
function getAigcTemplateItemList(args) {
|
||||
return request.post("/hotelBiz/aigc/aigcTemplateItemList", args);
|
||||
}
|
||||
|
||||
function getAigcGeneratorTaskList(args) {
|
||||
return request.post("/hotelBiz/aigcGeneratorTask/aigcGeneratorTaskList", args);
|
||||
}
|
||||
|
||||
function createAigcGeneratorTask(args) {
|
||||
return request.post("/hotelBiz/aigcGeneratorTask/createAigcGeneratorTask", args);
|
||||
}
|
||||
|
||||
function getCurrentCredit() {
|
||||
return request.get("/hotelBiz/credit/current", {});
|
||||
}
|
||||
|
||||
function rechargeCredit(args) {
|
||||
return request.post("/hotelBiz/credit/recharge", args);
|
||||
}
|
||||
|
||||
export {
|
||||
getAigcTemplateList,
|
||||
getAigcTemplateItemList,
|
||||
getAigcGeneratorTaskList,
|
||||
createAigcGeneratorTask,
|
||||
getCurrentCredit,
|
||||
rechargeCredit,
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { getServiceUrl } from "../api/GetServiceUrlApi";
|
||||
const versionValue = "1.1.3";
|
||||
|
||||
/// 是否是测试版本, 测试版本为true, 发布版本为false
|
||||
const developVersion = false;
|
||||
const developVersion = true;
|
||||
|
||||
// 获取服务地址
|
||||
const getEvnUrl = async () => {
|
||||
|
||||
Reference in New Issue
Block a user