397 lines
12 KiB
Vue
397 lines
12 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 合作页:hero、能力卡、创作流程、滚动客户原声、CTA+表单、伙伴。
|
||
* 不做 PC 鼠标 3D tilt;触控用 art-card-press + CSS 滚动。
|
||
*/
|
||
import { computed, reactive, ref } from 'vue'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import { useToast } from '@wot-ui/ui'
|
||
import AppNavbar from '@/components/layout/AppNavbar.vue'
|
||
import AppPageShell from '@/components/layout/AppPageShell.vue'
|
||
import ContentPageHeader from '../../components/ContentPageHeader.vue'
|
||
import { getTestimonials, getPartners, submitInquiry } from '@/api'
|
||
import { resolveMediaUrl } from '@/utils/request'
|
||
|
||
interface Testimonial {
|
||
id?: number
|
||
name?: string
|
||
role?: string
|
||
content?: string
|
||
avatar?: string
|
||
}
|
||
|
||
interface Partner {
|
||
id?: number
|
||
name?: string
|
||
logo?: string
|
||
url?: string
|
||
}
|
||
|
||
const toast = useToast()
|
||
const loading = ref(false)
|
||
const submitting = ref(false)
|
||
const marqueePaused = ref(false)
|
||
const processOpen = ref(0)
|
||
const testimonials = ref<Testimonial[]>([])
|
||
const partners = ref<Partner[]>([])
|
||
const form = reactive({
|
||
name: '',
|
||
email: '',
|
||
company: '',
|
||
message: '',
|
||
})
|
||
|
||
const capabilities = [
|
||
{ icon: 'application', title: '前端架构设计', desc: '为复杂的大型应用提供可扩展的架构方案。', tone: 'blue' },
|
||
{ icon: 'code-block', title: '创意交互开发', desc: '用动画与细节打造令人过目难忘的体验。', tone: 'gold' },
|
||
{ icon: 'mobile', title: '跨平台应用', desc: '使用 UniApp 等技术交付高质量移动端。', tone: 'purple' },
|
||
]
|
||
|
||
const processSteps = [
|
||
{ title: '灵感 & 探索', desc: '深入理解业务需求,进行竞品分析,寻找视觉灵感,确定设计方向。' },
|
||
{ title: '架构 & 设计', desc: '设计高保真原型,规划技术架构,确定数据流向,将抽象想法具象化。' },
|
||
{ title: '编码 & 雕琢', desc: '编写干净可维护的代码,加入微交互与动效,让页面活起来。' },
|
||
{ title: '测试 & 交付', desc: '多端测试、性能优化与上线配置,确保交付物稳定可靠。' },
|
||
]
|
||
|
||
/** 双行弹幕:复制一份实现无缝滚动 */
|
||
const marqueeList = computed(() => {
|
||
const list = testimonials.value
|
||
if (!list.length) return []
|
||
return [...list, ...list]
|
||
})
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const [t, p] = await Promise.all([
|
||
getTestimonials().catch(() => []),
|
||
getPartners().catch(() => []),
|
||
])
|
||
testimonials.value = (t || []) as Testimonial[]
|
||
partners.value = (p || []) as Partner[]
|
||
}
|
||
finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onShow(load)
|
||
|
||
const scrollToForm = () => {
|
||
uni.pageScrollTo({ selector: '#inquiry-form', duration: 300 })
|
||
}
|
||
|
||
const onSubmit = async () => {
|
||
if (!form.name || !form.email || !form.message) {
|
||
toast.show('请填写姓名、邮箱与留言')
|
||
return
|
||
}
|
||
submitting.value = true
|
||
try {
|
||
await submitInquiry({ ...form })
|
||
toast.success('提交成功')
|
||
form.name = ''
|
||
form.email = ''
|
||
form.company = ''
|
||
form.message = ''
|
||
}
|
||
catch (e: unknown) {
|
||
toast.error(e instanceof Error ? e.message : '提交失败')
|
||
}
|
||
finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<AppPageShell>
|
||
<AppNavbar title="合作" show-back />
|
||
<view class="page-pad">
|
||
<ContentPageHeader
|
||
eyebrow="Services"
|
||
title="共创数字未来"
|
||
desc="用代码构建骨架,用设计触动灵魂。"
|
||
/>
|
||
|
||
<view class="cap-grid">
|
||
<view
|
||
v-for="(c, i) in capabilities"
|
||
:key="i"
|
||
class="art-card art-card-press cap-card"
|
||
:class="`cap-card--${c.tone}`"
|
||
>
|
||
<view class="cap-icon">
|
||
<wd-icon :name="c.icon" size="28px" color="rgb(212,179,131)" />
|
||
</view>
|
||
<text class="cap-title">{{ c.title }}</text>
|
||
<text class="cap-desc text-art-muted">{{ c.desc }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="block">
|
||
<view class="block-title">创作流程</view>
|
||
<view class="process art-card">
|
||
<view
|
||
v-for="(step, i) in processSteps"
|
||
:key="i"
|
||
class="process-item"
|
||
:class="{ 'process-item--open': processOpen === i }"
|
||
@click="processOpen = processOpen === i ? -1 : i"
|
||
>
|
||
<view class="process-head">
|
||
<text class="process-no font-mono-label text-art-accent">{{ String(i + 1).padStart(2, '0') }}</text>
|
||
<text class="process-title">{{ step.title }}</text>
|
||
<wd-icon
|
||
:name="processOpen === i ? 'arrow-up' : 'arrow-down'"
|
||
size="14px"
|
||
color="rgb(136,136,136)"
|
||
/>
|
||
</view>
|
||
<text v-if="processOpen === i" class="step-desc text-art-muted">{{ step.desc }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="marqueeList.length" class="block marquee-block">
|
||
<view class="block-title">客户原声</view>
|
||
<view
|
||
class="marquee-viewport"
|
||
@touchstart="marqueePaused = true"
|
||
@touchend="marqueePaused = false"
|
||
>
|
||
<view class="marquee-row" :class="{ 'is-paused': marqueePaused }">
|
||
<view
|
||
v-for="(item, idx) in marqueeList"
|
||
:key="'a-' + idx"
|
||
class="danmaku art-card"
|
||
>
|
||
<image
|
||
v-if="item.avatar"
|
||
class="avatar"
|
||
:src="resolveMediaUrl(String(item.avatar))"
|
||
mode="aspectFill"
|
||
/>
|
||
<view v-else class="avatar avatar--empty" />
|
||
<text v-if="item.name" class="who text-art-accent">{{ item.name }}</text>
|
||
<text class="quote">"{{ item.content }}"</text>
|
||
</view>
|
||
</view>
|
||
<view class="marquee-row marquee-row--rev" :class="{ 'is-paused': marqueePaused }">
|
||
<view
|
||
v-for="(item, idx) in marqueeList"
|
||
:key="'b-' + idx"
|
||
class="danmaku art-card"
|
||
>
|
||
<image
|
||
v-if="item.avatar"
|
||
class="avatar"
|
||
:src="resolveMediaUrl(String(item.avatar))"
|
||
mode="aspectFill"
|
||
/>
|
||
<view v-else class="avatar avatar--empty" />
|
||
<text v-if="item.name" class="who text-art-accent">{{ item.name }}</text>
|
||
<text class="quote">"{{ item.content }}"</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="cta art-card">
|
||
<view class="cta-glow" />
|
||
<text class="cta-title">准备好开始了吗?</text>
|
||
<text class="cta-desc text-art-muted">无论是一个疯狂的想法,还是一个具体的业务需求,我都乐意倾听。</text>
|
||
<wd-button
|
||
type="primary"
|
||
custom-style="background:#d4b383;border-color:#d4b383;color:#171717;"
|
||
@click="scrollToForm"
|
||
>
|
||
发起合作咨询
|
||
</wd-button>
|
||
</view>
|
||
|
||
<view id="inquiry-form" class="block">
|
||
<view class="block-title">提交咨询</view>
|
||
<view class="art-card form-card">
|
||
<wd-input v-model="form.name" label="姓名" placeholder="您的姓名" clearable />
|
||
<wd-input v-model="form.email" label="邮箱" placeholder="联系邮箱" clearable />
|
||
<wd-input v-model="form.company" label="公司" placeholder="可选" clearable />
|
||
<wd-textarea v-model="form.message" label="留言" placeholder="项目需求或合作意向" />
|
||
<wd-button
|
||
block
|
||
type="primary"
|
||
:loading="submitting"
|
||
custom-style="margin-top:24rpx;background:#d4b383;border-color:#d4b383;color:#171717;"
|
||
@click="onSubmit"
|
||
>
|
||
提交
|
||
</wd-button>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="partners.length" class="block">
|
||
<text class="trusted font-mono-label text-art-muted">Trusted By</text>
|
||
<view class="partner-grid">
|
||
<view v-for="(p, idx) in partners" :key="p.id || idx" class="art-card partner">
|
||
<image
|
||
v-if="p.logo"
|
||
class="partner-logo"
|
||
:src="resolveMediaUrl(String(p.logo))"
|
||
mode="aspectFit"
|
||
/>
|
||
<text class="partner-name">{{ p.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<wd-loading v-if="loading" />
|
||
</view>
|
||
</AppPageShell>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.page-pad { padding: 24rpx 32rpx 100rpx; }
|
||
.cap-grid { display: flex; flex-direction: column; gap: 20rpx; margin-bottom: 48rpx; }
|
||
.cap-card { padding: 36rpx 32rpx; }
|
||
.cap-icon {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(212, 179, 131, 0.12);
|
||
margin-bottom: 24rpx;
|
||
}
|
||
.cap-title {
|
||
display: block;
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
color: rgb(var(--art-text));
|
||
margin-bottom: 12rpx;
|
||
}
|
||
.cap-desc { font-size: 26rpx; line-height: 1.6; }
|
||
.block { margin-bottom: 56rpx; }
|
||
.block-title {
|
||
font-size: 36rpx;
|
||
color: rgb(var(--art-text));
|
||
margin-bottom: 24rpx;
|
||
font-family: 'Playfair Display', serif;
|
||
font-style: italic;
|
||
}
|
||
.process { overflow: hidden; }
|
||
.process-item {
|
||
padding: 28rpx 32rpx;
|
||
border-bottom: 1px solid var(--art-card-border);
|
||
}
|
||
.process-item:last-child { border-bottom: none; }
|
||
.process-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
.process-no { flex-shrink: 0; }
|
||
.process-title {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: rgb(var(--art-text));
|
||
font-weight: 600;
|
||
}
|
||
.step-desc {
|
||
display: block;
|
||
font-size: 26rpx;
|
||
line-height: 1.65;
|
||
padding: 16rpx 0 4rpx 56rpx;
|
||
}
|
||
.marquee-block { overflow: hidden; }
|
||
.marquee-viewport { display: flex; flex-direction: column; gap: 20rpx; }
|
||
.marquee-row {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
width: max-content;
|
||
animation: marquee 36s linear infinite;
|
||
}
|
||
.marquee-row--rev { animation-name: marquee-rev; animation-duration: 42s; }
|
||
.marquee-row.is-paused { animation-play-state: paused; }
|
||
@keyframes marquee {
|
||
from { transform: translateX(0); }
|
||
to { transform: translateX(-50%); }
|
||
}
|
||
@keyframes marquee-rev {
|
||
from { transform: translateX(-50%); }
|
||
to { transform: translateX(0); }
|
||
}
|
||
.danmaku {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
padding: 16rpx 22rpx;
|
||
max-width: 520rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.avatar {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
.avatar--empty { background: rgba(212, 179, 131, 0.25); }
|
||
.who { font-size: 22rpx; flex-shrink: 0; }
|
||
.quote {
|
||
font-size: 24rpx;
|
||
color: rgb(var(--art-text));
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 360rpx;
|
||
}
|
||
.cta {
|
||
position: relative;
|
||
overflow: hidden;
|
||
padding: 48rpx 36rpx;
|
||
margin-bottom: 48rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
gap: 20rpx;
|
||
}
|
||
.cta-glow {
|
||
position: absolute;
|
||
top: -40rpx;
|
||
width: 280rpx;
|
||
height: 280rpx;
|
||
border-radius: 50%;
|
||
background: rgba(212, 179, 131, 0.22);
|
||
filter: blur(40px);
|
||
pointer-events: none;
|
||
}
|
||
.cta-title {
|
||
position: relative;
|
||
font-size: 40rpx;
|
||
font-family: 'Playfair Display', serif;
|
||
font-style: italic;
|
||
color: rgb(var(--art-text));
|
||
}
|
||
.cta-desc { position: relative; font-size: 26rpx; line-height: 1.6; max-width: 560rpx; }
|
||
.form-card { padding: 24rpx; }
|
||
.trusted {
|
||
display: block;
|
||
text-align: center;
|
||
margin-bottom: 24rpx;
|
||
letter-spacing: 0.18em;
|
||
}
|
||
.partner-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16rpx; }
|
||
.partner {
|
||
padding: 28rpx 20rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
opacity: 0.85;
|
||
}
|
||
.partner-logo { width: 96rpx; height: 64rpx; }
|
||
.partner-name { font-size: 24rpx; color: rgb(var(--art-text)); text-align: center; }
|
||
</style>
|