215 lines
6.3 KiB
Vue
215 lines
6.3 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* 关于页:档案、联系、技术栈、经历时间轴(对齐 PC About 信息架构)。
|
|
*/
|
|
import { computed, ref } from 'vue'
|
|
import { onShow } from '@dcloudio/uni-app'
|
|
import AppNavbar from '@/components/layout/AppNavbar.vue'
|
|
import AppPageShell from '@/components/layout/AppPageShell.vue'
|
|
import ContentPageHeader from '../../components/ContentPageHeader.vue'
|
|
import { getAbout, type AboutProfile } from '@/api'
|
|
import { resolveMediaUrl, rewriteHtmlMediaUrls } from '@/utils/request'
|
|
|
|
const loading = ref(false)
|
|
const profile = ref<AboutProfile | null>(null)
|
|
|
|
function normalizeProfile(data: AboutProfile | AboutProfile[] | null): AboutProfile | null {
|
|
if (!data) return null
|
|
if (Array.isArray(data)) return data[0] || null
|
|
return data
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const data = await getAbout()
|
|
profile.value = normalizeProfile(data as AboutProfile | AboutProfile[])
|
|
}
|
|
catch (e: unknown) {
|
|
uni.showToast({ title: e instanceof Error ? e.message : '加载失败', icon: 'none' })
|
|
}
|
|
finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onShow(load)
|
|
|
|
const avatarUrl = computed(() => resolveMediaUrl(String(profile.value?.avatar || '')))
|
|
const techList = computed(() => {
|
|
const t = profile.value?.techStack
|
|
return Array.isArray(t) ? t.map(String) : []
|
|
})
|
|
const experiences = computed(() => profile.value?.experiences || [])
|
|
const bioHtml = computed(() => rewriteHtmlMediaUrls(String(profile.value?.bio || '')))
|
|
|
|
const copyText = (label: string, value?: string) => {
|
|
const v = String(value || '').trim()
|
|
if (!v) return
|
|
uni.setClipboardData({
|
|
data: v,
|
|
success: () => uni.showToast({ title: `${label}已复制`, icon: 'none' }),
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<AppPageShell>
|
|
<AppNavbar title="关于" show-back />
|
|
<view class="page-pad">
|
|
<view v-if="profile" class="animate-reveal">
|
|
<ContentPageHeader eyebrow="个人档案" title="关于我" />
|
|
|
|
<view class="identity">
|
|
<image
|
|
v-if="avatarUrl"
|
|
class="avatar"
|
|
:src="avatarUrl"
|
|
mode="aspectFill"
|
|
/>
|
|
<view class="identity-meta">
|
|
<text class="name">{{ profile.name || '关于我' }}</text>
|
|
<view v-if="profile.location" class="loc text-art-muted">
|
|
<wd-icon name="location" size="14px" color="rgb(212,179,131)" />
|
|
<text>{{ profile.location }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<rich-text v-if="bioHtml" class="bio" :nodes="bioHtml" />
|
|
|
|
<view v-if="profile.email || profile.wechat" class="art-card contact">
|
|
<text class="block-title">联系方式</text>
|
|
<view
|
|
v-if="profile.email"
|
|
class="contact-row"
|
|
@click="copyText('邮箱', profile.email)"
|
|
>
|
|
<wd-icon name="email" size="18px" color="rgb(212,179,131)" />
|
|
<text class="contact-text">{{ profile.email }}</text>
|
|
</view>
|
|
<view
|
|
v-if="profile.wechat"
|
|
class="contact-row"
|
|
@click="copyText('微信', profile.wechat)"
|
|
>
|
|
<wd-icon name="message" size="18px" color="rgb(212,179,131)" />
|
|
<text class="contact-text">WeChat: {{ profile.wechat }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="techList.length" class="art-card panel">
|
|
<text class="block-title">技术栈</text>
|
|
<view class="pills">
|
|
<text v-for="(t, i) in techList" :key="i" class="pill">{{ t }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="experiences.length" class="art-card panel">
|
|
<text class="block-title">经历</text>
|
|
<view class="timeline">
|
|
<view
|
|
v-for="(exp, i) in experiences"
|
|
:key="i"
|
|
class="exp"
|
|
>
|
|
<view class="dot" :class="{ 'dot--first': i === 0 }" />
|
|
<view class="exp-body">
|
|
<text class="exp-year font-mono-label" :class="i === 0 ? 'text-art-accent' : 'text-art-muted'">
|
|
{{ exp.year }}
|
|
</text>
|
|
<text class="exp-role">{{ exp.role }}</text>
|
|
<text class="exp-company text-art-muted">{{ exp.company }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<wd-loading v-if="loading" />
|
|
<wd-empty v-if="!loading && !profile" description="暂无资料" />
|
|
</view>
|
|
</AppPageShell>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.page-pad { padding: 24rpx 32rpx 80rpx; }
|
|
.identity {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 28rpx;
|
|
margin-bottom: 32rpx;
|
|
}
|
|
.avatar {
|
|
width: 144rpx;
|
|
height: 144rpx;
|
|
border-radius: 50%;
|
|
border: 2rpx solid rgba(212, 179, 131, 0.55);
|
|
flex-shrink: 0;
|
|
}
|
|
.identity-meta { display: flex; flex-direction: column; gap: 12rpx; min-width: 0; }
|
|
.name {
|
|
font-size: 40rpx;
|
|
color: rgb(var(--art-text));
|
|
font-weight: 600;
|
|
}
|
|
.loc { display: flex; align-items: center; gap: 8rpx; font-size: 26rpx; }
|
|
.bio {
|
|
display: block;
|
|
font-size: 30rpx;
|
|
line-height: 1.8;
|
|
color: rgb(var(--art-muted));
|
|
margin-bottom: 36rpx;
|
|
}
|
|
.contact, .panel {
|
|
padding: 28rpx 32rpx;
|
|
margin-bottom: 28rpx;
|
|
}
|
|
.block-title {
|
|
display: block;
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: rgb(var(--art-text));
|
|
margin-bottom: 20rpx;
|
|
}
|
|
.contact-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
padding: 14rpx 0;
|
|
}
|
|
.contact-text { font-size: 26rpx; color: rgb(var(--art-muted)); }
|
|
.pills { display: flex; flex-wrap: wrap; gap: 12rpx; }
|
|
.pill {
|
|
padding: 10rpx 18rpx;
|
|
border-radius: 999rpx;
|
|
border: 1px solid var(--art-card-border);
|
|
font-size: 22rpx;
|
|
color: rgb(var(--art-text));
|
|
background: rgba(212, 179, 131, 0.06);
|
|
}
|
|
.timeline {
|
|
border-left: 2rpx solid var(--art-card-border);
|
|
margin-left: 10rpx;
|
|
padding-left: 28rpx;
|
|
}
|
|
.exp {
|
|
position: relative;
|
|
margin-bottom: 28rpx;
|
|
}
|
|
.dot {
|
|
position: absolute;
|
|
left: -36rpx;
|
|
top: 10rpx;
|
|
width: 14rpx;
|
|
height: 14rpx;
|
|
border-radius: 50%;
|
|
background: rgba(127, 127, 127, 0.35);
|
|
}
|
|
.dot--first { background: rgb(var(--art-accent)); }
|
|
.exp-body { display: flex; flex-direction: column; gap: 6rpx; }
|
|
.exp-year { font-size: 22rpx; }
|
|
.exp-role { font-size: 28rpx; color: rgb(var(--art-text)); font-weight: 600; }
|
|
.exp-company { font-size: 24rpx; }
|
|
</style>
|