feat(aigc): add travel postcard AIGC feature
Add complete travel postcard AIGC creation workflow including: - New AIGC pages: template selection home, result detail, and history records - Reusable UI components: template card, flow steps, result preview, action buttons, record lists - Static assets including template images, fonts and styles - Updated TopNavBar component to support menu button alignment - Register new AIGC page routes in pages.json - Add mock data for templates, user records and result details
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
<view :style="{ height: statusBarHeight + 'px' }" v-if="!hideStatusBar"></view>
|
||||
|
||||
<!-- 导航栏内容 -->
|
||||
<view class="flex flex-items-center flex-justify-between border-box pl-8 pr-8"
|
||||
:style="{ height: navBarHeight + 'px' }">
|
||||
<view
|
||||
class="flex flex-items-center flex-justify-between border-box pl-8 pr-8"
|
||||
:style="navContentStyle"
|
||||
>
|
||||
<!-- 左侧返回按钮 -->
|
||||
<view class="nav-bar-left flex flex-items-center flex-justify-center" v-if="showBack" @click="handleBack">
|
||||
<uni-icons type="left" size="20" :color="backIconColor" />
|
||||
@@ -32,7 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, getCurrentInstance, onMounted, ref } from "vue";
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
@@ -82,10 +84,15 @@ const props = defineProps({
|
||||
default: "center", // 'center' | 'left'
|
||||
validator: (value) => ["center", "left"].includes(value),
|
||||
},
|
||||
alignWithMenuButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义emits
|
||||
const emit = defineEmits(["back"]);
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
// 系统信息
|
||||
const statusBarHeight = ref(0);
|
||||
@@ -94,7 +101,8 @@ const navBarHeight = ref(44); // 默认导航栏高度
|
||||
// 获取系统信息
|
||||
onMounted(() => {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0;
|
||||
const systemStatusBarHeight = systemInfo.statusBarHeight || 0;
|
||||
statusBarHeight.value = systemStatusBarHeight;
|
||||
|
||||
// 根据平台设置导航栏高度
|
||||
if (systemInfo.platform === "ios") {
|
||||
@@ -102,6 +110,18 @@ onMounted(() => {
|
||||
} else {
|
||||
navBarHeight.value = 48;
|
||||
}
|
||||
|
||||
if (
|
||||
props.alignWithMenuButton &&
|
||||
typeof uni.getMenuButtonBoundingClientRect === "function"
|
||||
) {
|
||||
const menuButton = uni.getMenuButtonBoundingClientRect();
|
||||
|
||||
if (menuButton?.height && typeof menuButton.top === "number") {
|
||||
statusBarHeight.value = Math.max(menuButton.top, systemStatusBarHeight);
|
||||
navBarHeight.value = menuButton.height;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 计算导航栏样式类
|
||||
@@ -122,15 +142,27 @@ const navBarStyle = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const navContentStyle = computed(() => {
|
||||
return {
|
||||
height: navBarHeight.value + "px",
|
||||
};
|
||||
});
|
||||
|
||||
const hasBackListener = computed(() => {
|
||||
const vnodeProps = instance?.vnode?.props || {};
|
||||
return Boolean(vnodeProps.onBack);
|
||||
});
|
||||
|
||||
// 处理返回事件
|
||||
const handleBack = () => {
|
||||
emit("back");
|
||||
// 如果没有监听back事件,默认执行返回上一页
|
||||
if (!emit("back")) {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
});
|
||||
if (hasBackListener.value) {
|
||||
emit("back");
|
||||
return;
|
||||
}
|
||||
// 如果没有监听back事件,默认执行返回上一页
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
91
src/pages-aigc/components/AigcTopBar/index.vue
Normal file
91
src/pages-aigc/components/AigcTopBar/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<view class="aigc-topbar" :style="topbarStyle">
|
||||
<view class="aigc-nav-row">
|
||||
<view class="aigc-topbar-back" @tap="emit('back')">
|
||||
<uni-icons class="aigc-back-icon" fontFamily="znicons" size="22" color="#172033">
|
||||
{{ zniconsMap["zn-nav-arrow-left"] }}
|
||||
</uni-icons>
|
||||
</view>
|
||||
|
||||
<view v-if="showActions" class="aigc-topbar-actions">
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<view class="aigc-history-button" @tap="emit('history')">
|
||||
<uni-icons class="aigc-history-icon" fontFamily="znicons" size="22" color="#436783">
|
||||
{{ zniconsMap["zn-clock"] }}
|
||||
</uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { zniconsMap } from "@/static/fonts/znicons.js";
|
||||
|
||||
defineProps({
|
||||
points: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
rechargeLabel: {
|
||||
type: String,
|
||||
default: "充值",
|
||||
},
|
||||
showActions: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["back", "history", "recharge"]);
|
||||
|
||||
const statusBarHeight = ref(0);
|
||||
const navHeight = ref(52);
|
||||
const capsuleSafeWidth = ref(0);
|
||||
|
||||
const topbarStyle = computed(() => ({
|
||||
"--aigc-status-height": `${statusBarHeight.value}px`,
|
||||
"--aigc-nav-height": `${navHeight.value}px`,
|
||||
"--aigc-capsule-safe-width": `${capsuleSafeWidth.value}px`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0;
|
||||
|
||||
const menuButton =
|
||||
typeof uni.getMenuButtonBoundingClientRect === "function"
|
||||
? uni.getMenuButtonBoundingClientRect()
|
||||
: null;
|
||||
|
||||
if (menuButton?.width && menuButton?.left) {
|
||||
const menuTopGap = Math.max(menuButton.top - statusBarHeight.value, 0);
|
||||
const windowWidth = systemInfo.windowWidth || 375;
|
||||
|
||||
navHeight.value = Math.max(menuButton.height + menuTopGap * 2, 44);
|
||||
capsuleSafeWidth.value = Math.max(windowWidth - menuButton.left + 8, 96);
|
||||
return;
|
||||
}
|
||||
|
||||
navHeight.value = statusBarHeight.value > 0 ? 44 : 52;
|
||||
capsuleSafeWidth.value = 0;
|
||||
} catch (error) {
|
||||
statusBarHeight.value = 0;
|
||||
navHeight.value = 52;
|
||||
capsuleSafeWidth.value = 0;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
138
src/pages-aigc/components/AigcTopBar/styles/index.scss
Normal file
138
src/pages-aigc/components/AigcTopBar/styles/index.scss
Normal file
@@ -0,0 +1,138 @@
|
||||
@font-face {
|
||||
font-family: znicons;
|
||||
src: url("@/static/fonts/znicons.ttf");
|
||||
}
|
||||
|
||||
.aigc-topbar {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
flex: 0 0 auto;
|
||||
padding: var(--aigc-status-height) 12px 8px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(235, 249, 255, 0.92) 0%,
|
||||
rgba(240, 251, 247, 0.72) 66%,
|
||||
rgba(240, 251, 247, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.aigc-nav-row {
|
||||
height: var(--aigc-nav-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-right: var(--aigc-capsule-safe-width);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aigc-topbar-back {
|
||||
flex: 0 0 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
color: #172033;
|
||||
box-shadow: 0 8px 18px rgba(18, 48, 56, 0.08);
|
||||
}
|
||||
|
||||
.aigc-back-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translateX(-1px);
|
||||
}
|
||||
|
||||
.aigc-topbar-actions {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 36px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 3px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.76);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
box-shadow: 0 8px 18px rgba(18, 48, 56, 0.08);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aigc-history-button,
|
||||
.aigc-wallet-button {
|
||||
height: 36px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aigc-history-button {
|
||||
width: 36px;
|
||||
flex: 0 0 36px;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: #436783;
|
||||
}
|
||||
|
||||
.aigc-history-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aigc-wallet-button {
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 0 9px 0 8px;
|
||||
background: #102942;
|
||||
color: #ffffff;
|
||||
box-shadow:
|
||||
0 8px 18px rgba(18, 48, 56, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.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;
|
||||
align-items: baseline;
|
||||
overflow: hidden;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aigc-wallet-number {
|
||||
margin-right: 2px;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
font-weight: 900;
|
||||
}
|
||||
29
src/pages-aigc/detail/components/ContinuationCard/index.vue
Normal file
29
src/pages-aigc/detail/components/ContinuationCard/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<view class="continuation-card">
|
||||
<view class="continuation-copy">
|
||||
<text class="continuation-title">{{ info.title }}</text>
|
||||
<text class="continuation-desc">{{ info.desc }}</text>
|
||||
</view>
|
||||
|
||||
<view class="continuation-button" @tap="emit('upgrade')">
|
||||
{{ info.buttonText }}
|
||||
</view>
|
||||
|
||||
<text class="continuation-cost">{{ info.cost }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
info: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["upgrade"]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
.continuation-card {
|
||||
min-height: 84px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 116px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(32, 207, 117, 0.14), rgba(255, 255, 255, 0.82)),
|
||||
rgba(255, 255, 255, 0.82);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.continuation-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.continuation-title,
|
||||
.continuation-desc,
|
||||
.continuation-cost {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.continuation-title {
|
||||
color: #172033;
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.continuation-desc {
|
||||
margin-top: 4px;
|
||||
color: #667386;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.continuation-button {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
background: #172951;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 40px;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.continuation-cost {
|
||||
grid-column: 1 / -1;
|
||||
color: #12a765;
|
||||
font-size: 10px;
|
||||
line-height: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
14
src/pages-aigc/detail/components/ResultActions/index.vue
Normal file
14
src/pages-aigc/detail/components/ResultActions/index.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<view class="result-actions">
|
||||
<view class="result-action is-primary" @tap="emit('save')">保存图片</view>
|
||||
<view class="result-action is-secondary" @tap="emit('share')">分享好友</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const emit = defineEmits(["save", "share"]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
.result-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.result-action {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.result-action.is-primary {
|
||||
background: #20cf75;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8px 16px rgba(32, 207, 117, 0.18);
|
||||
}
|
||||
|
||||
.result-action.is-secondary {
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
color: #526073;
|
||||
}
|
||||
21
src/pages-aigc/detail/components/ResultMeta/index.vue
Normal file
21
src/pages-aigc/detail/components/ResultMeta/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<view class="result-meta">
|
||||
<view v-for="item in items" :key="item.label" class="result-meta-item">
|
||||
<text class="result-meta-label">{{ item.label }}</text>
|
||||
<text class="result-meta-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
.result-meta {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.result-meta-item {
|
||||
min-width: 0;
|
||||
height: 62px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
color: #172033;
|
||||
font-size: 11px;
|
||||
line-height: 15px;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.result-meta-label,
|
||||
.result-meta-value {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-meta-label {
|
||||
margin-bottom: 5px;
|
||||
color: #8c98a8;
|
||||
font-size: 10px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.result-meta-value {
|
||||
color: #173252;
|
||||
}
|
||||
25
src/pages-aigc/detail/components/ResultPreview/index.vue
Normal file
25
src/pages-aigc/detail/components/ResultPreview/index.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<view class="result-preview" :style="{ '--result-accent': result.accent }">
|
||||
<image class="result-preview-image" :src="result.cover" mode="aspectFill" />
|
||||
<view class="result-preview-shade" />
|
||||
|
||||
<view class="result-preview-caption">
|
||||
<text class="result-preview-badge">{{ result.variantLabel }}</text>
|
||||
<text class="result-preview-title">{{ result.title }}</text>
|
||||
<text class="result-preview-desc">{{ result.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
result: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
.result-preview {
|
||||
position: relative;
|
||||
height: 360px;
|
||||
overflow: hidden;
|
||||
border-radius: 26px;
|
||||
background: var(--result-accent, #0a4d46);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 18px 34px rgba(17, 35, 49, 0.12);
|
||||
}
|
||||
|
||||
.result-preview-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result-preview-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0.02) 0%,
|
||||
rgba(0, 0, 0, 0.16) 48%,
|
||||
rgba(0, 0, 0, 0.74) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.result-preview-caption {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
left: 18px;
|
||||
}
|
||||
|
||||
.result-preview-badge {
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
font-size: 9px;
|
||||
line-height: 20px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.7px;
|
||||
}
|
||||
|
||||
.result-preview-title,
|
||||
.result-preview-desc {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-preview-title {
|
||||
margin-top: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 23px;
|
||||
line-height: 30px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.result-preview-desc {
|
||||
margin-top: 6px;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
16
src/pages-aigc/detail/data/result.js
Normal file
16
src/pages-aigc/detail/data/result.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import glassBoatPostcard from "../../home/assets/glass-boat-postcard.gif";
|
||||
|
||||
export const aigcDetailResult = {
|
||||
id: "glass-boat-postcard-result",
|
||||
title: "鸳鸯湖明信片",
|
||||
cover: glassBoatPostcard,
|
||||
accent: "#0a4d46",
|
||||
variantLabel: "图片版",
|
||||
resultLabel: "图片版",
|
||||
consumedText: "100积分",
|
||||
desc: "已生成可保存、可分享的高清旅行图片",
|
||||
continuationTitle: "创意续作",
|
||||
continuationDesc: "让这张图动起来 · 5秒内动效视频",
|
||||
continuationCost: "追加消耗 100积分",
|
||||
continuationButtonText: "升级为动效视频",
|
||||
};
|
||||
105
src/pages-aigc/detail/detail.vue
Normal file
105
src/pages-aigc/detail/detail.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<view class="aigc-detail-page">
|
||||
<AigcTopBar :points="pointBalance" @back="handleBack" @history="handleOpenRecords" @recharge="handleRecharge" />
|
||||
|
||||
<view class="aigc-detail-content">
|
||||
<ResultPreview :result="result" />
|
||||
<ResultMeta :items="metaItems" />
|
||||
<ContinuationCard :info="continuationInfo" @upgrade="handleUpgradeToVideo" />
|
||||
<ResultActions @save="handleSave" @share="handleShare" />
|
||||
|
||||
<view class="aigc-detail-text-actions">
|
||||
<view class="aigc-detail-text-action" @tap="handleRegenerate">再生成一版</view>
|
||||
<view class="aigc-detail-text-action" @tap="handleChangeTemplate">换个模板</view>
|
||||
<view class="aigc-detail-text-action" @tap="handleOpenRecords">历史记录</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
|
||||
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 result = aigcDetailResult;
|
||||
|
||||
const metaItems = computed(() => [
|
||||
{
|
||||
label: "结果",
|
||||
value: result.resultLabel,
|
||||
},
|
||||
{
|
||||
label: "已消耗",
|
||||
value: result.consumedText,
|
||||
},
|
||||
{
|
||||
label: "余额",
|
||||
value: `${pointBalance.value}积分`,
|
||||
},
|
||||
]);
|
||||
|
||||
const continuationInfo = computed(() => ({
|
||||
title: result.continuationTitle,
|
||||
desc: result.continuationDesc,
|
||||
cost: result.continuationCost,
|
||||
buttonText: result.continuationButtonText,
|
||||
}));
|
||||
|
||||
const showPlaceholderToast = (title) => {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRecharge = () => {
|
||||
showPlaceholderToast("积分充值");
|
||||
};
|
||||
|
||||
const handleUpgradeToVideo = () => {
|
||||
showPlaceholderToast("升级为动效视频");
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
showPlaceholderToast("保存图片");
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
showPlaceholderToast("分享好友");
|
||||
};
|
||||
|
||||
const handleRegenerate = () => {
|
||||
showPlaceholderToast("再生成一版");
|
||||
};
|
||||
|
||||
const handleChangeTemplate = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages-aigc/home/home",
|
||||
fail: () => showPlaceholderToast("换个模板"),
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenRecords = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages-aigc/record/record",
|
||||
fail: () => showPlaceholderToast("历史记录"),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
54
src/pages-aigc/detail/styles/index.scss
Normal file
54
src/pages-aigc/detail/styles/index.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
.aigc-detail-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-detail-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aigc-detail-text-actions {
|
||||
height: 50px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin: 12px 22px 0;
|
||||
border-radius: 50px;
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
}
|
||||
|
||||
.aigc-detail-text-action {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #20a765;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aigc-detail-text-action + .aigc-detail-text-action {
|
||||
border-left: 1px solid rgba(32, 167, 101, 0.16);
|
||||
}
|
||||
BIN
src/pages-aigc/home/assets/card-bridge.png
Normal file
BIN
src/pages-aigc/home/assets/card-bridge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/pages-aigc/home/assets/card-culture-1.png
Normal file
BIN
src/pages-aigc/home/assets/card-culture-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/pages-aigc/home/assets/card-waterfall.png
Normal file
BIN
src/pages-aigc/home/assets/card-waterfall.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 772 KiB |
BIN
src/pages-aigc/home/assets/glass-boat-postcard.gif
Normal file
BIN
src/pages-aigc/home/assets/glass-boat-postcard.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 MiB |
25
src/pages-aigc/home/components/FlowSteps/index.vue
Normal file
25
src/pages-aigc/home/components/FlowSteps/index.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<view class="flow-steps">
|
||||
<view
|
||||
v-for="(step, index) in steps"
|
||||
:key="step"
|
||||
class="flow-step"
|
||||
>
|
||||
<text class="flow-index">{{ index + 1 }}</text>
|
||||
<text class="flow-label">{{ step }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
steps: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
50
src/pages-aigc/home/components/FlowSteps/styles/index.scss
Normal file
50
src/pages-aigc/home/components/FlowSteps/styles/index.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
.flow-steps {
|
||||
height: 55px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
align-items: center;
|
||||
margin: 12px 16px 0;
|
||||
padding: 4px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
box-shadow: 0 8px 18px rgba(18, 48, 56, 0.05);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
min-width: 0;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
padding: 0 4px;
|
||||
border-radius: 14px;
|
||||
background: transparent;
|
||||
color: #576276;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.flow-index {
|
||||
flex: 0 0 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #20cf75;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flow-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
44
src/pages-aigc/home/components/TemplateCard/index.vue
Normal file
44
src/pages-aigc/home/components/TemplateCard/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<view
|
||||
class="template-card"
|
||||
:class="[
|
||||
active ? 'is-active' : '',
|
||||
template.tone ? `is-${template.tone}` : '',
|
||||
]"
|
||||
:style="{ '--memory-accent': template.accent || '#0f7069' }"
|
||||
>
|
||||
<view class="template-media">
|
||||
<image class="template-image" :src="template.cover" 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>
|
||||
<button class="template-select" type="default" @tap.stop="emit('select')">
|
||||
{{ template.buttonText }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["select"]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
147
src/pages-aigc/home/components/TemplateCard/styles/index.scss
Normal file
147
src/pages-aigc/home/components/TemplateCard/styles/index.scss
Normal file
@@ -0,0 +1,147 @@
|
||||
.template-card {
|
||||
position: relative;
|
||||
flex: 0 0 318px;
|
||||
width: 318px;
|
||||
height: 494px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(180deg, #0f7069, #082226);
|
||||
color: #ffffff;
|
||||
text-align: left;
|
||||
box-shadow: 0 18px 34px rgba(24, 37, 50, 0.18);
|
||||
transform: scale(0.96);
|
||||
transform-origin: center;
|
||||
transition: box-shadow 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.template-card.is-active {
|
||||
box-shadow: 0 22px 42px rgba(24, 37, 50, 0.2);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.template-card.is-postcard .template-copy {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 34%),
|
||||
linear-gradient(180deg, #0a4d46, #082226);
|
||||
}
|
||||
|
||||
.template-card.is-postcard .template-copy::before {
|
||||
background: linear-gradient(180deg, rgba(10, 77, 70, 0), #0a4d46);
|
||||
}
|
||||
|
||||
.template-media {
|
||||
position: relative;
|
||||
flex: 0 0 358px;
|
||||
height: 358px;
|
||||
overflow: hidden;
|
||||
background: var(--memory-accent);
|
||||
}
|
||||
|
||||
.template-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-media-shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background:
|
||||
radial-gradient(92% 45% at 50% 28%, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0) 62%),
|
||||
linear-gradient(180deg, rgba(4, 22, 24, 0), rgba(4, 22, 24, 0.02) 58%, rgba(4, 22, 24, 0.24));
|
||||
}
|
||||
|
||||
.template-copy {
|
||||
position: relative;
|
||||
flex: 0 0 136px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding: 15px 18px 14px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 34%),
|
||||
linear-gradient(180deg, #0f7069, #092128);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.template-copy::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -34px;
|
||||
height: 34px;
|
||||
background: linear-gradient(180deg, rgba(15, 112, 105, 0), #0f7069);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.template-badge {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 18px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 9px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 10px;
|
||||
line-height: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.template-title {
|
||||
width: 178px;
|
||||
color: #ffffff;
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
width: 178px;
|
||||
margin-top: 4px;
|
||||
color: rgba(255, 255, 255, 0.76);
|
||||
font-size: 11px;
|
||||
line-height: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.template-note {
|
||||
max-width: 172px;
|
||||
margin-top: 5px;
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
font-size: 9px;
|
||||
line-height: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.template-select {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
min-width: 108px;
|
||||
height: 40px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #172033;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.template-select::after {
|
||||
border: 0;
|
||||
}
|
||||
198
src/pages-aigc/home/components/TemplateCarousel/index.vue
Normal file
198
src/pages-aigc/home/components/TemplateCarousel/index.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<view class="template-carousel">
|
||||
<view class="template-heading">
|
||||
<view>
|
||||
<text class="template-heading-title">选择模板</text>
|
||||
<text class="template-heading-subtitle">左右滑动预览效果</text>
|
||||
</view>
|
||||
<text class="template-heading-badge">同源双版本</text>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="template-rail"
|
||||
scroll-x
|
||||
show-scrollbar="false"
|
||||
scroll-with-animation
|
||||
:scroll-left="railScrollLeft"
|
||||
@scroll="handleScroll"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchend="handleTouchEnd"
|
||||
@touchcancel="handleTouchEnd"
|
||||
>
|
||||
<view class="template-track">
|
||||
<TemplateCard
|
||||
v-for="(template, index) in templates"
|
||||
:key="template.id || index"
|
||||
:template="template"
|
||||
:active="index === modelValue"
|
||||
@select="handleSelect(template, index)"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="template-dots">
|
||||
<view
|
||||
v-for="(template, index) in templates"
|
||||
:key="`${template.id || index}-dot`"
|
||||
class="template-dot"
|
||||
:class="{ 'is-active': index === modelValue }"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import TemplateCard from "../TemplateCard/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
templates: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "select"]);
|
||||
|
||||
const cardStep = 330;
|
||||
const cardWidth = 318;
|
||||
const sidePadding = 16;
|
||||
const switchThreshold = 88;
|
||||
const screenWidth = ref(375);
|
||||
const railScrollLeft = ref(0);
|
||||
const currentScrollLeft = ref(0);
|
||||
|
||||
let touchStartLeft = 0;
|
||||
let touchStartIndex = 0;
|
||||
let isTouching = false;
|
||||
let settleTimer = null;
|
||||
|
||||
const getMaxIndex = () => Math.max(props.templates.length - 1, 0);
|
||||
|
||||
const clampIndex = (index) => Math.max(0, Math.min(getMaxIndex(), index));
|
||||
|
||||
const getScreenWidth = () => {
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
return Number(systemInfo.windowWidth) || 375;
|
||||
} catch (error) {
|
||||
return 375;
|
||||
}
|
||||
};
|
||||
|
||||
const getCenterOffset = () => (screenWidth.value - cardWidth) / 2 - sidePadding;
|
||||
|
||||
const getTargetScrollLeft = (index) => {
|
||||
const targetIndex = clampIndex(index);
|
||||
if (targetIndex === 0) return 0;
|
||||
return Math.round(targetIndex * cardStep - getCenterOffset());
|
||||
};
|
||||
|
||||
const getNearestIndex = (left) =>
|
||||
clampIndex(Math.round((left + getCenterOffset()) / cardStep));
|
||||
|
||||
const updateScreenWidth = (width) => {
|
||||
screenWidth.value = Number(width) || getScreenWidth();
|
||||
scrollToIndex(props.modelValue);
|
||||
};
|
||||
|
||||
const handleWindowResize = (event) => {
|
||||
updateScreenWidth(event?.size?.windowWidth);
|
||||
};
|
||||
|
||||
const setRailScrollLeft = async (targetLeft, force = false) => {
|
||||
await nextTick();
|
||||
|
||||
if (force && railScrollLeft.value === targetLeft) {
|
||||
railScrollLeft.value = targetLeft > 0 ? targetLeft - 1 : 1;
|
||||
setTimeout(() => {
|
||||
railScrollLeft.value = targetLeft;
|
||||
currentScrollLeft.value = targetLeft;
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
railScrollLeft.value = targetLeft;
|
||||
currentScrollLeft.value = targetLeft;
|
||||
};
|
||||
|
||||
const scrollToIndex = (index, force = false) => {
|
||||
setRailScrollLeft(getTargetScrollLeft(index), force);
|
||||
};
|
||||
|
||||
const settleToIndex = (index, force = true) => {
|
||||
const targetIndex = clampIndex(index);
|
||||
if (targetIndex !== props.modelValue) {
|
||||
emit("update:modelValue", targetIndex);
|
||||
}
|
||||
scrollToIndex(targetIndex, force);
|
||||
};
|
||||
|
||||
const handleScroll = (event) => {
|
||||
currentScrollLeft.value = event.detail?.scrollLeft || 0;
|
||||
|
||||
if (!isTouching) {
|
||||
clearTimeout(settleTimer);
|
||||
settleTimer = setTimeout(() => {
|
||||
settleToIndex(getNearestIndex(currentScrollLeft.value));
|
||||
}, 140);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchStart = () => {
|
||||
clearTimeout(settleTimer);
|
||||
isTouching = true;
|
||||
touchStartLeft = currentScrollLeft.value;
|
||||
touchStartIndex = clampIndex(props.modelValue);
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!isTouching) return;
|
||||
|
||||
isTouching = false;
|
||||
clearTimeout(settleTimer);
|
||||
|
||||
const offset = currentScrollLeft.value - touchStartLeft;
|
||||
const direction = offset > 0 ? 1 : -1;
|
||||
const targetIndex =
|
||||
Math.abs(offset) >= switchThreshold
|
||||
? touchStartIndex + direction
|
||||
: touchStartIndex;
|
||||
|
||||
settleToIndex(targetIndex);
|
||||
};
|
||||
|
||||
const handleSelect = (template, index) => {
|
||||
settleToIndex(index);
|
||||
emit("select", template, index);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.templates.length],
|
||||
() => {
|
||||
scrollToIndex(props.modelValue);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
updateScreenWidth();
|
||||
if (typeof uni.onWindowResize === "function") {
|
||||
uni.onWindowResize(handleWindowResize);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typeof uni.offWindowResize === "function") {
|
||||
uni.offWindowResize(handleWindowResize);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
.template-carousel {
|
||||
max-width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-heading {
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 16px 20px;
|
||||
}
|
||||
|
||||
.template-heading-title,
|
||||
.template-heading-subtitle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-heading-title {
|
||||
color: #172033;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.template-heading-subtitle {
|
||||
margin-top: 1px;
|
||||
color: #7a8596;
|
||||
font-size: 10px;
|
||||
line-height: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.template-heading-badge {
|
||||
height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(32, 207, 117, 0.12);
|
||||
color: #10a765;
|
||||
font-size: 11px;
|
||||
line-height: 26px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.template-rail {
|
||||
max-width: 100vw;
|
||||
height: 560px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.template-rail::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.template-track {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
height: 560px;
|
||||
padding: 0 41px 0 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.template-dots {
|
||||
height: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.template-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(23, 32, 51, 0.16);
|
||||
}
|
||||
|
||||
.template-dot.is-active {
|
||||
width: 18px;
|
||||
background: #20cf75;
|
||||
}
|
||||
53
src/pages-aigc/home/data/templates.js
Normal file
53
src/pages-aigc/home/data/templates.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import glassBoatPostcard from "../assets/glass-boat-postcard.gif";
|
||||
import bridgeCover from "../assets/card-bridge.png";
|
||||
import waterfallCover from "../assets/card-waterfall.png";
|
||||
import cultureCover from "../assets/card-culture-1.png";
|
||||
|
||||
export const aigcTemplates = [
|
||||
{
|
||||
id: "glass-boat-postcard",
|
||||
title: "鸳鸯湖明信片",
|
||||
desc: "适合朋友圈分享的旅行纪念内容",
|
||||
note: "图片版100积分 · 动效视频版200积分/5秒内",
|
||||
badge: "同源双版本",
|
||||
buttonText: "选这个模板",
|
||||
cover: glassBoatPostcard,
|
||||
tone: "postcard",
|
||||
accent: "#0a4d46",
|
||||
},
|
||||
{
|
||||
id: "bridge-memory",
|
||||
title: "小七孔古桥留影",
|
||||
desc: "把古桥与湖光做成专属旅行大片",
|
||||
note: "图片版100积分 · 动效视频版200积分/5秒内",
|
||||
badge: "同源双版本",
|
||||
buttonText: "选这个模板",
|
||||
cover: bridgeCover,
|
||||
tone: "emerald",
|
||||
accent: "#0f7069",
|
||||
},
|
||||
{
|
||||
id: "waterfall-story",
|
||||
title: "翠谷瀑布旅拍",
|
||||
desc: "适合清爽夏日氛围的动态内容",
|
||||
note: "图片版100积分 · 动效视频版200积分/5秒内",
|
||||
badge: "同源双版本",
|
||||
buttonText: "选这个模板",
|
||||
cover: waterfallCover,
|
||||
tone: "aqua",
|
||||
accent: "#155e75",
|
||||
},
|
||||
{
|
||||
id: "culture-postcard",
|
||||
title: "瑶山古寨印象",
|
||||
desc: "把民族风情做成旅行收藏卡",
|
||||
note: "图片版100积分 · 动效视频版200积分/5秒内",
|
||||
badge: "同源双版本",
|
||||
buttonText: "选这个模板",
|
||||
cover: cultureCover,
|
||||
tone: "forest",
|
||||
accent: "#36563d",
|
||||
},
|
||||
];
|
||||
|
||||
export const aigcFlowSteps = ["选模板", "传素材", "添积分", "出结果"];
|
||||
54
src/pages-aigc/home/home.vue
Normal file
54
src/pages-aigc/home/home.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<view class="aigc-home-page">
|
||||
<AigcTopBar :points="pointBalance" @back="handleBack" @history="handleHistory" @recharge="handleRecharge" />
|
||||
|
||||
<view class="aigc-home-browser">
|
||||
<TemplateCarousel v-model="activeIndex" :templates="templates" @select="handleSelectTemplate" />
|
||||
|
||||
<FlowSteps :steps="steps" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/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 activeIndex = ref(0);
|
||||
const templates = aigcTemplates;
|
||||
const steps = aigcFlowSteps;
|
||||
|
||||
const showPlaceholderToast = (title) => {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack();
|
||||
}
|
||||
};
|
||||
|
||||
const handleHistory = () => {
|
||||
showPlaceholderToast("最近任务");
|
||||
};
|
||||
|
||||
const handleRecharge = () => {
|
||||
showPlaceholderToast("积分充值");
|
||||
};
|
||||
|
||||
const handleSelectTemplate = (template) => {
|
||||
showPlaceholderToast(template.title);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
26
src/pages-aigc/home/styles/index.scss
Normal file
26
src/pages-aigc/home/styles/index.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.aigc-home-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100vw;
|
||||
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-home-browser {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 4px 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
30
src/pages-aigc/record/components/RecordCard/index.vue
Normal file
30
src/pages-aigc/record/components/RecordCard/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<view class="record-card">
|
||||
<image class="record-card-image" :src="record.image" mode="aspectFill" />
|
||||
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<view class="record-card-action" @tap="emit('view', record)">
|
||||
查看
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
record: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["view"]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
.record-card {
|
||||
height: 90px;
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr) 54px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfdfc 100%);
|
||||
border: 1px solid rgba(224, 236, 230, 0.92);
|
||||
box-shadow: 0 10px 24px rgba(29, 58, 47, 0.055);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.record-card + .record-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.record-card-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
background: #edf5f1;
|
||||
}
|
||||
|
||||
.record-card-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.record-card-title,
|
||||
.record-card-time,
|
||||
.record-card-status {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.record-card-title {
|
||||
color: #162235;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.record-card-time {
|
||||
margin-top: 6px;
|
||||
color: #7d8c97;
|
||||
font-size: 11px;
|
||||
line-height: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.record-card-status {
|
||||
margin-top: 4px;
|
||||
color: #10b86b;
|
||||
font-size: 10px;
|
||||
line-height: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.record-card-action {
|
||||
justify-self: end;
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
background: #eef8f3;
|
||||
color: #263f52;
|
||||
font-size: 11px;
|
||||
line-height: 28px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ellipsis-1 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
35
src/pages-aigc/record/components/RecordList/index.vue
Normal file
35
src/pages-aigc/record/components/RecordList/index.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<view class="record-list-wrap">
|
||||
<view class="record-list">
|
||||
<RecordCard
|
||||
v-for="record in records"
|
||||
:key="record.id"
|
||||
:record="record"
|
||||
@view="handleView"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<text class="record-note">仅保留最近20天生成记录</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RecordCard from "../RecordCard/index.vue";
|
||||
|
||||
defineProps({
|
||||
records: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["view"]);
|
||||
|
||||
const handleView = (record) => {
|
||||
emit("view", record);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
.record-list-wrap {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-note {
|
||||
display: block;
|
||||
margin-top: 28px;
|
||||
color: #9aa8b6;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
18
src/pages-aigc/record/data/records.js
Normal file
18
src/pages-aigc/record/data/records.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import glassBoatPostcard from "@/pages-aigc/home/assets/glass-boat-postcard.gif";
|
||||
|
||||
export const aigcRecords = [
|
||||
{
|
||||
id: "record-lake-postcard",
|
||||
title: "鸳鸯湖明信片",
|
||||
time: "2026-06-29 15:46:12",
|
||||
status: "图片版 · 已完成 · 可升级动效视频",
|
||||
image: glassBoatPostcard,
|
||||
},
|
||||
{
|
||||
id: "record-lake-postcard-video",
|
||||
title: "鸳鸯湖明信片",
|
||||
time: "2026-06-29 15:58:40",
|
||||
status: "动效视频版 · 5秒内 · 由图片版升级",
|
||||
image: glassBoatPostcard,
|
||||
},
|
||||
];
|
||||
46
src/pages-aigc/record/record.vue
Normal file
46
src/pages-aigc/record/record.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<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>
|
||||
|
||||
<RecordList
|
||||
:records="records"
|
||||
@view="handleViewRecord"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||
import RecordList from "./components/RecordList/index.vue";
|
||||
import { aigcRecords } from "./data/records.js";
|
||||
|
||||
const records = aigcRecords;
|
||||
|
||||
const handleBack = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack();
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewRecord = (record) => {
|
||||
uni.showToast({
|
||||
title: record.title,
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
41
src/pages-aigc/record/styles/index.scss
Normal file
41
src/pages-aigc/record/styles/index.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
.aigc-record-page {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
padding: 0 18px;
|
||||
background: #ffffff;
|
||||
color: #162235;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aigc-record-nav {
|
||||
position: relative;
|
||||
min-height: 82px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aigc-record-nav :deep(.top-nav-bar) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.aigc-record-nav :deep(.top-nav-bar > .flex) {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.aigc-record-nav :deep(.nav-bar-left),
|
||||
.aigc-record-nav :deep(.nav-bar-right) {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.aigc-record-nav :deep(.nav-bar-center) {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.aigc-record-nav :deep(.nav-bar-center text) {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -158,14 +158,26 @@
|
||||
{
|
||||
"root": "pages-aigc",
|
||||
"pages": [
|
||||
{
|
||||
"path": "home",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"path": "home/home",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "detail/detail",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "record/record",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextstyle": "black",
|
||||
|
||||
Reference in New Issue
Block a user