Files
wot-uniapp/src/subPackages/content/pages/services/index.vue
李琦 fe5a5b6c0f 1. 小程序端
2. 视频优化
2026-07-30 14:11:51 +08:00

397 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>