This commit is contained in:
wangzzpeng
2026-09-28 09:27:51 +08:00
parent 7bc84bbe5e
commit 6eb2bfcbfd
25 changed files with 2887 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import { computed } from 'vue'
import { formatBusinessType } from '@/utils/format'
import type { Case } from '@/types/business/cases'
interface Props {
item: Case
}
const props = defineProps<Props>()
/** 业务类型标签(设计稿:蓝色实心胶囊 + 白字) */
const tag = computed(() => formatBusinessType(props.item.category))
/**
* 封面图。
*
* <p>toCase 已把「非可访问地址」收口成空串,无封面时回退主题色底,
* 避免裂图或空白块。</p>
*/
const coverStyle = computed(() =>
props.item.cover ? { backgroundImage: `url(${props.item.cover})` } : undefined,
)
</script>
<template>
<!-- 设计稿:封面通栏(不随内容容器收窄、无圆角),左下叠类型标签 + 案例名 -->
<section class="case-hero" :style="coverStyle">
<div class="container case-hero__inner">
<span v-if="tag" class="case-hero__tag">{{ tag }}</span>
<h1 class="case-hero__title">
{{ props.item.title }}
</h1>
</div>
</section>
</template>
<style lang="less" scoped>
.case-hero {
position: relative;
width: 100%;
height: 260px;
background-color: @primary-color;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
&::after {
content: '';
position: absolute;
inset: 0;
// 底部压暗,保证白字可读
background: linear-gradient(180deg, rgba(15, 23, 42, 0) 42%, rgba(15, 23, 42, 0.55));
}
&__inner {
position: relative;
z-index: 1;
.flex(column, flex-end, flex-start);
gap: @spacing-sm;
height: 100%;
padding-bottom: @spacing-lg;
}
&__tag {
padding: 2px @spacing-sm;
border-radius: @radius-round;
background: @primary-color;
color: @white;
font-size: @font-size-sm;
line-height: 18px;
}
&__title {
color: @white;
font-size: @font-size-xxl;
font-weight: 600;
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.35);
// 最多两行,避免超长案例名撑破 hero
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
</style>

View File

@@ -0,0 +1,159 @@
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
keyword: string
/** 是否只看收藏 */
favoriteOnly: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:keyword': [keyword: string]
search: []
reset: []
toggleFavoriteOnly: []
}>()
const keyword = computed<string>({
get: () => props.keyword,
set: (value) => emit('update:keyword', value),
})
</script>
<template>
<div class="case-search">
<div class="case-search__field">
<svg class="case-search__icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.7-3.7" />
</svg>
<input
v-model="keyword"
class="case-search__input"
placeholder="关键字搜索"
@keyup.enter="emit('search')"
>
</div>
<button class="case-search__btn" @click="emit('reset')">
重置
</button>
<button class="case-search__btn case-search__btn--primary" @click="emit('search')">
搜索
</button>
<button
class="case-search__fav"
:class="{ 'is-active': favoriteOnly }"
@click="emit('toggleFavoriteOnly')"
>
<svg class="case-search__fav-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 20.5s-7.2-4.6-8.4-9A5.1 5.1 0 0 1 12 5.6a5.1 5.1 0 0 1 8.4 5.9c-1.2 4.4-8.4 9-8.4 9Z" />
</svg>
收藏列表
</button>
</div>
</template>
<style lang="less" scoped>
.case-search {
.flex(row, flex-start, center);
gap: @spacing-sm;
padding: @spacing-base @spacing-lg;
border-radius: @radius-base;
background: @white;
&__field {
position: relative;
flex: 1;
max-width: 520px;
}
&__icon {
position: absolute;
top: 50%;
left: 12px;
width: 16px;
height: 16px;
transform: translateY(-50%);
fill: none;
stroke: @text-color-secondary;
stroke-width: 1.8;
}
&__input {
width: 100%;
height: 36px;
padding: 0 @spacing-base 0 36px;
border: 1px solid transparent;
border-radius: @radius-base;
background: @input-bg;
color: @text-color;
font-size: @font-size-base;
outline: none;
transition: border-color 0.2s;
&::placeholder {
color: @text-color-secondary;
}
&:focus {
border-color: @primary-color;
}
}
&__btn {
height: 36px;
padding: 0 @spacing-lg;
border-radius: @radius-base;
background: @pill-bg;
color: @text-color;
font-size: @font-size-base;
transition: all 0.2s;
&:hover {
color: @primary-color;
}
&--primary {
background: @primary-color;
color: @white;
&:hover {
background: @primary-hover;
color: @white;
}
}
}
&__fav {
.flex(row, center);
gap: @spacing-xs;
margin-left: auto;
color: @heading-color;
font-size: @font-size-base;
transition: color 0.2s;
&:hover,
&.is-active {
color: @primary-color;
}
&.is-active .case-search__fav-icon {
fill: @danger-color;
stroke: @danger-color;
}
&-icon {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
transition: all 0.2s;
}
}
}
</style>